From 813a10e66cb73bc274a2ed66ca43923c7d9a416c Mon Sep 17 00:00:00 2001 From: Alex Hoppen Date: Wed, 18 Sep 2024 19:26:19 -0700 Subject: [PATCH] Simplify the Python files used to write BSP servers for SourceKit-LSP tests --- .../INPUTS/AbstractBuildServer.py | 129 ++++++++++++++++++ .../buildServer.json | 7 - .../server.py | 75 ---------- .../buildServer.json | 7 - .../server.py | 98 ------------- .../buildServer.json | 7 - .../server.py | 94 ------------- .../server.py | 87 ++---------- .../server.py | 91 ++---------- .../server.py | 61 +-------- .../buildServer.json | 7 - .../server.py | 81 ----------- 12 files changed, 164 insertions(+), 580 deletions(-) create mode 100644 Sources/SKTestSupport/INPUTS/AbstractBuildServer.py delete mode 100644 Sources/SKTestSupport/INPUTS/BuildServerBuildSystemTests.testBuildTargetOutputs/buildServer.json delete mode 100755 Sources/SKTestSupport/INPUTS/BuildServerBuildSystemTests.testBuildTargetOutputs/server.py delete mode 100644 Sources/SKTestSupport/INPUTS/BuildServerBuildSystemTests.testBuildTargetSources/buildServer.json delete mode 100755 Sources/SKTestSupport/INPUTS/BuildServerBuildSystemTests.testBuildTargetSources/server.py delete mode 100644 Sources/SKTestSupport/INPUTS/BuildServerBuildSystemTests.testBuildTargets/buildServer.json delete mode 100755 Sources/SKTestSupport/INPUTS/BuildServerBuildSystemTests.testBuildTargets/server.py delete mode 100644 Sources/SKTestSupport/INPUTS/BuildServerBuildSystemTests.testSettings/buildServer.json delete mode 100755 Sources/SKTestSupport/INPUTS/BuildServerBuildSystemTests.testSettings/server.py diff --git a/Sources/SKTestSupport/INPUTS/AbstractBuildServer.py b/Sources/SKTestSupport/INPUTS/AbstractBuildServer.py new file mode 100644 index 00000000..f25f306e --- /dev/null +++ b/Sources/SKTestSupport/INPUTS/AbstractBuildServer.py @@ -0,0 +1,129 @@ +import json +import sys +from typing import Optional + + +class RequestError(Exception): + """ + An error that can be thrown from a request handling function in `AbstractBuildServer` to return an error response to + SourceKit-LSP. + """ + + code: int + message: str + + def __init__(self, code: int, message: str): + self.code = code + self.message = message + + +class AbstractBuildServer: + """ + An abstract class to implement a BSP server in Python for SourceKit-LSP testing purposes. + """ + + def run(self): + """ + Run the build server. This should be called from the top-level code of the build server's Python file. + """ + while True: + line = sys.stdin.readline() + if len(line) == 0: + break + + assert line.startswith("Content-Length:") + length = int(line[len("Content-Length:") :]) + sys.stdin.readline() + message = json.loads(sys.stdin.read(length)) + + try: + result = self.handle_message(message) + if result: + response_message: dict[str, object] = { + "jsonrpc": "2.0", + "id": message["id"], + "result": result, + } + self.send_raw_message(response_message) + except RequestError as e: + error_response_message: dict[str, object] = { + "jsonrpc": "2.0", + "id": message["id"], + "error": { + "code": e.code, + "message": e.message, + }, + } + self.send_raw_message(error_response_message) + + def handle_message(self, message: dict[str, object]) -> Optional[dict[str, object]]: + """ + Dispatch handling of the given method, received from SourceKit-LSP to the message handling function. + """ + method: str = str(message["method"]) + params: dict[str, object] = message["params"] # type: ignore + if method == "build/exit": + return self.exit(params) + elif method == "build/initialize": + return self.initialize(params) + elif method == "build/initialized": + return self.initialized(params) + elif method == "build/shutdown": + return self.shutdown(params) + elif method == "textDocument/registerForChanges": + return self.register_for_changes(params) + + # ignore other notifications + if "id" in message: + raise RequestError(code=-32601, message=f"Method not found: {method}") + + def send_raw_message(self, message: dict[str, object]): + """ + Send a raw message to SourceKit-LSP. The message needs to have all JSON-RPC wrapper fields. + + Subclasses should not call this directly + """ + message_str = json.dumps(message) + sys.stdout.buffer.write( + f"Content-Length: {len(message_str)}\r\n\r\n{message_str}".encode("utf-8") + ) + sys.stdout.flush() + + def send_notification(self, method: str, params: dict[str, object]): + """ + Send a notification with the given method and parameters to SourceKit-LSP. + """ + message: dict[str, object] = { + "jsonrpc": "2.0", + "method": method, + "params": params, + } + self.send_raw_message(message) + + # Message handling functions. + # Subclasses should override these to provide functionality. + + def exit(self, notification: dict[str, object]) -> None: + pass + + def initialize(self, request: dict[str, object]) -> dict[str, object]: + return { + "displayName": "test server", + "version": "0.1", + "bspVersion": "2.0", + "rootUri": "blah", + "capabilities": {"languageIds": ["a", "b"]}, + "data": { + "indexDatabasePath": "some/index/db/path", + "indexStorePath": "some/index/store/path", + }, + } + + def initialized(self, notification: dict[str, object]) -> None: + pass + + def register_for_changes(self, notification: dict[str, object]): + pass + + def shutdown(self, notification: dict[str, object]) -> None: + pass diff --git a/Sources/SKTestSupport/INPUTS/BuildServerBuildSystemTests.testBuildTargetOutputs/buildServer.json b/Sources/SKTestSupport/INPUTS/BuildServerBuildSystemTests.testBuildTargetOutputs/buildServer.json deleted file mode 100644 index cf9e70bd..00000000 --- a/Sources/SKTestSupport/INPUTS/BuildServerBuildSystemTests.testBuildTargetOutputs/buildServer.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "client name", - "version": "10", - "bspVersion": "2.0", - "languages": ["a", "b"], - "argv": ["server.py"] -} \ No newline at end of file diff --git a/Sources/SKTestSupport/INPUTS/BuildServerBuildSystemTests.testBuildTargetOutputs/server.py b/Sources/SKTestSupport/INPUTS/BuildServerBuildSystemTests.testBuildTargetOutputs/server.py deleted file mode 100755 index fb961d40..00000000 --- a/Sources/SKTestSupport/INPUTS/BuildServerBuildSystemTests.testBuildTargetOutputs/server.py +++ /dev/null @@ -1,75 +0,0 @@ -import json -import sys - - -while True: - line = sys.stdin.readline() - if len(line) == 0: - break - - assert line.startswith('Content-Length:') - length = int(line[len('Content-Length:'):]) - sys.stdin.readline() - message = json.loads(sys.stdin.read(length)) - - response = None - if message["method"] == "build/initialize": - response = { - "jsonrpc": "2.0", - "id": message["id"], - "result": { - "displayName": "test server", - "version": "0.1", - "bspVersion": "2.0", - "rootUri": "blah", - "capabilities": {"languageIds": ["a", "b"]}, - "data": { - "indexStorePath": "some/index/store/path" - } - } - } - elif message["method"] == "build/initialized": - continue - elif message["method"] == "build/shutdown": - response = { - "jsonrpc": "2.0", - "id": message["id"], - "result": None - } - elif message["method"] == "build/exit": - break - elif message["method"] == "buildTarget/outputPaths": - response = { - "jsonrpc": "2.0", - "id": message["id"], - "result": { - "items": [ - { - "target": {"uri": "build://target/a"}, - "outputPaths": [ - "file:///path/to/a/file", - "file:///path/to/a/file2" - ] - } - ] - } - } - # ignore other notifications - elif "id" in message: - response = { - "jsonrpc": "2.0", - "id": message["id"], - "error": { - "code": 123, - "message": "unhandled method {}".format(message["method"]), - } - } - - if response: - responseStr = json.dumps(response) - try: - sys.stdout.buffer.write(f"Content-Length: {len(responseStr)}\r\n\r\n{responseStr}".encode('utf-8')) - sys.stdout.flush() - except IOError: - # stdout closed, time to quit - break diff --git a/Sources/SKTestSupport/INPUTS/BuildServerBuildSystemTests.testBuildTargetSources/buildServer.json b/Sources/SKTestSupport/INPUTS/BuildServerBuildSystemTests.testBuildTargetSources/buildServer.json deleted file mode 100644 index cf9e70bd..00000000 --- a/Sources/SKTestSupport/INPUTS/BuildServerBuildSystemTests.testBuildTargetSources/buildServer.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "client name", - "version": "10", - "bspVersion": "2.0", - "languages": ["a", "b"], - "argv": ["server.py"] -} \ No newline at end of file diff --git a/Sources/SKTestSupport/INPUTS/BuildServerBuildSystemTests.testBuildTargetSources/server.py b/Sources/SKTestSupport/INPUTS/BuildServerBuildSystemTests.testBuildTargetSources/server.py deleted file mode 100755 index 0d410913..00000000 --- a/Sources/SKTestSupport/INPUTS/BuildServerBuildSystemTests.testBuildTargetSources/server.py +++ /dev/null @@ -1,98 +0,0 @@ -import json -import sys - - -while True: - line = sys.stdin.readline() - if len(line) == 0: - break - - assert line.startswith('Content-Length:') - length = int(line[len('Content-Length:'):]) - sys.stdin.readline() - message = json.loads(sys.stdin.read(length)) - - response = None - if message["method"] == "build/initialize": - response = { - "jsonrpc": "2.0", - "id": message["id"], - "result": { - "displayName": "test server", - "version": "0.1", - "bspVersion": "2.0", - "rootUri": "blah", - "capabilities": {"languageIds": ["a", "b"]}, - "data": { - "indexStorePath": "some/index/store/path" - } - } - } - elif message["method"] == "build/initialized": - continue - elif message["method"] == "build/shutdown": - response = { - "jsonrpc": "2.0", - "id": message["id"], - "result": None - } - elif message["method"] == "build/exit": - break - elif message["method"] == "buildTarget/sources": - response = { - "jsonrpc": "2.0", - "id": message["id"], - "result": { - "items": [ - { - "target": {"uri": "build://target/a"}, - "sources": [ - { - "uri": "file:///path/to/a/file", - "kind": 1, - "generated": False - }, - { - "uri": "file:///path/to/a/folder/", - "kind": 2, - "generated": False - } - ] - }, - { - "target": {"uri": "build://target/b"}, - "sources": [ - { - "uri": "file:///path/to/b/file", - "kind": 1, - "generated": False - }, - { - "uri": "file:///path/to/b/folder/", - "kind": 2, - "generated": False - } - ] - } - ] - } - } - # ignore other notifications - elif "id" in message: - response = { - "jsonrpc": "2.0", - "id": message["id"], - "error": { - "code": 123, - "message": "unhandled method {}".format(message["method"]), - } - } - - if response: - responseStr = json.dumps(response) - try: - sys.stdout.buffer.write(f"Content-Length: {len(responseStr)}\r\n\r\n{responseStr}".encode('utf-8')) - sys.stdout.flush() - except IOError: - # stdout closed, time to quit - break diff --git a/Sources/SKTestSupport/INPUTS/BuildServerBuildSystemTests.testBuildTargets/buildServer.json b/Sources/SKTestSupport/INPUTS/BuildServerBuildSystemTests.testBuildTargets/buildServer.json deleted file mode 100644 index cf9e70bd..00000000 --- a/Sources/SKTestSupport/INPUTS/BuildServerBuildSystemTests.testBuildTargets/buildServer.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "client name", - "version": "10", - "bspVersion": "2.0", - "languages": ["a", "b"], - "argv": ["server.py"] -} \ No newline at end of file diff --git a/Sources/SKTestSupport/INPUTS/BuildServerBuildSystemTests.testBuildTargets/server.py b/Sources/SKTestSupport/INPUTS/BuildServerBuildSystemTests.testBuildTargets/server.py deleted file mode 100755 index 51cd4580..00000000 --- a/Sources/SKTestSupport/INPUTS/BuildServerBuildSystemTests.testBuildTargets/server.py +++ /dev/null @@ -1,94 +0,0 @@ -import json -import sys - - -while True: - line = sys.stdin.readline() - if len(line) == 0: - break - - assert line.startswith('Content-Length:') - length = int(line[len('Content-Length:'):]) - sys.stdin.readline() - message = json.loads(sys.stdin.read(length)) - - response = None - if message["method"] == "build/initialize": - response = { - "jsonrpc": "2.0", - "id": message["id"], - "result": { - "displayName": "test server", - "version": "0.1", - "bspVersion": "2.0", - "rootUri": "blah", - "capabilities": {"languageIds": ["objective-c", "swift"]}, - "data": { - "indexStorePath": "some/index/store/path" - } - } - } - elif message["method"] == "build/initialized": - continue - elif message["method"] == "build/shutdown": - response = { - "jsonrpc": "2.0", - "id": message["id"], - "result": None - } - elif message["method"] == "build/exit": - break - elif message["method"] == "workspace/buildTargets": - response = { - "jsonrpc": "2.0", - "id": message["id"], - "result": { - "targets": [ - { - "id": {"uri": "target:first_target"}, - "displayName": "First Target", - "baseDirectory": "file:///some/dir", - "tags": ["library", "test"], - "capabilities": { - "canCompile": True, - "canTest": True, - "canRun": False - }, - "languageIds": ["objective-c", "swift"], - "dependencies": [] - }, - { - "id": {"uri": "target:second_target"}, - "displayName": "Second Target", - "baseDirectory": "file:///some/dir", - "tags": ["library", "test"], - "capabilities": { - "canCompile": True, - "canTest": False, - "canRun": False - }, - "languageIds": ["objective-c", "swift"], - "dependencies": [{"uri": "target:first_target"}] - } - ] - } - } - # ignore other notifications - elif "id" in message: - response = { - "jsonrpc": "2.0", - "id": message["id"], - "error": { - "code": 123, - "message": "unhandled method {}".format(message["method"]), - } - } - - if response: - responseStr = json.dumps(response) - try: - sys.stdout.buffer.write(f"Content-Length: {len(responseStr)}\r\n\r\n{responseStr}".encode('utf-8')) - sys.stdout.flush() - except IOError: - # stdout closed, time to quit - break diff --git a/Sources/SKTestSupport/INPUTS/BuildServerBuildSystemTests.testBuildTargetsChanged/server.py b/Sources/SKTestSupport/INPUTS/BuildServerBuildSystemTests.testBuildTargetsChanged/server.py index c3c4d17f..a9eb10eb 100755 --- a/Sources/SKTestSupport/INPUTS/BuildServerBuildSystemTests.testBuildTargetsChanged/server.py +++ b/Sources/SKTestSupport/INPUTS/BuildServerBuildSystemTests.testBuildTargetsChanged/server.py @@ -1,87 +1,26 @@ -import json -import os +from pathlib import Path import sys +sys.path.append(str(Path(__file__).parent.parent)) -def send(data): - dataStr = json.dumps(data) - try: - sys.stdout.buffer.write(f"Content-Length: {len(dataStr)}\r\n\r\n{dataStr}".encode('utf-8')) - sys.stdout.flush() - except IOError: - # stdout closed, time to quit - raise SystemExit(0) +from AbstractBuildServer import AbstractBuildServer -while True: - line = sys.stdin.readline() - if len(line) == 0: - break - - assert line.startswith('Content-Length:') - length = int(line[len('Content-Length:'):]) - sys.stdin.readline() - message = json.loads(sys.stdin.read(length)) - - response = None - notification = None - - if message["method"] == "build/initialize": - response = { - "jsonrpc": "2.0", - "id": message["id"], - "result": { - "displayName": "test server", - "version": "0.1", - "bspVersion": "2.0", - "rootUri": "blah", - "capabilities": {"languageIds": ["a", "b"]}, - "data": { - "indexStorePath": "some/index/store/path" - } - } - } - elif message["method"] == "build/initialized": - continue - elif message["method"] == "build/shutdown": - response = { - "jsonrpc": "2.0", - "id": message["id"], - "result": None - } - elif message["method"] == "build/exit": - break - elif message["method"] == "textDocument/registerForChanges": - response = { - "jsonrpc": "2.0", - "id": message["id"], - "result": None - } - if message["params"]["action"] == "register": - notification = { - "jsonrpc": "2.0", - "method": "buildTarget/didChange", - "params": { +class BuildServer(AbstractBuildServer): + def register_for_changes(self, notification: dict[str, object]): + if notification["action"] == "register": + self.send_notification( + "buildTarget/didChange", + { "changes": [ { "target": {"uri": "build://target/a"}, "kind": 1, - "data": {"key": "value"} + "data": {"key": "value"}, } ] - } - } + }, + ) - # ignore other notifications - elif "id" in message: - response = { - "jsonrpc": "2.0", - "id": message["id"], - "error": { - "code": -32600, - "message": "unhandled method {}".format(message["method"]), - } - } - if response: send(response) - if notification: send(notification) +BuildServer().run() diff --git a/Sources/SKTestSupport/INPUTS/BuildServerBuildSystemTests.testFileRegistration/server.py b/Sources/SKTestSupport/INPUTS/BuildServerBuildSystemTests.testFileRegistration/server.py index 26a4fa49..874d138b 100755 --- a/Sources/SKTestSupport/INPUTS/BuildServerBuildSystemTests.testFileRegistration/server.py +++ b/Sources/SKTestSupport/INPUTS/BuildServerBuildSystemTests.testFileRegistration/server.py @@ -1,85 +1,24 @@ -import json -import os +from pathlib import Path import sys +sys.path.append(str(Path(__file__).parent.parent)) -def send(data): - dataStr = json.dumps(data) - try: - sys.stdout.buffer.write(f"Content-Length: {len(dataStr)}\r\n\r\n{dataStr}".encode('utf-8')) - sys.stdout.flush() - except IOError: - # stdout closed, time to quit - raise SystemExit(0) +from AbstractBuildServer import AbstractBuildServer -while True: - line = sys.stdin.readline() - if len(line) == 0: - break - - assert line.startswith('Content-Length:') - length = int(line[len('Content-Length:'):]) - sys.stdin.readline() - message = json.loads(sys.stdin.read(length)) - - response = None - notification = None - - if message["method"] == "build/initialize": - response = { - "jsonrpc": "2.0", - "id": message["id"], - "result": { - "displayName": "test server", - "version": "0.1", - "bspVersion": "2.0", - "rootUri": "blah", - "capabilities": {"languageIds": ["a", "b"]}, - "data": { - "indexStorePath": "some/index/store/path" - } - } - } - elif message["method"] == "build/initialized": - continue - elif message["method"] == "build/shutdown": - response = { - "jsonrpc": "2.0", - "id": message["id"], - "result": None - } - elif message["method"] == "build/exit": - break - elif message["method"] == "textDocument/registerForChanges": - response = { - "jsonrpc": "2.0", - "id": message["id"], - "result": None - } - if message["params"]["action"] == "register": - notification = { - "jsonrpc": "2.0", - "method": "build/sourceKitOptionsChanged", - "params": { - "uri": message["params"]["uri"], +class BuildServer(AbstractBuildServer): + def register_for_changes(self, notification: dict[str, object]): + if notification["action"] == "register": + self.send_notification( + "build/sourceKitOptionsChanged", + { + "uri": notification["uri"], "updatedOptions": { "options": ["a", "b"], - "workingDirectory": "/some/dir" - } - } - } + "workingDirectory": "/some/dir", + }, + }, + ) - # ignore other notifications - elif "id" in message: - response = { - "jsonrpc": "2.0", - "id": message["id"], - "error": { - "code": -32600, - "message": "unhandled method {}".format(message["method"]), - } - } - if response: send(response) - if notification: send(notification) +BuildServer().run() diff --git a/Sources/SKTestSupport/INPUTS/BuildServerBuildSystemTests.testServerInitialize/server.py b/Sources/SKTestSupport/INPUTS/BuildServerBuildSystemTests.testServerInitialize/server.py index 2ae0f574..70dc871b 100755 --- a/Sources/SKTestSupport/INPUTS/BuildServerBuildSystemTests.testServerInitialize/server.py +++ b/Sources/SKTestSupport/INPUTS/BuildServerBuildSystemTests.testServerInitialize/server.py @@ -1,60 +1,13 @@ -import json +from pathlib import Path import sys +sys.path.append(str(Path(__file__).parent.parent)) -while True: - line = sys.stdin.readline() - if len(line) == 0: - break +from AbstractBuildServer import AbstractBuildServer - assert line.startswith('Content-Length:') - length = int(line[len('Content-Length:'):]) - sys.stdin.readline() - message = json.loads(sys.stdin.read(length)) - response = None - if message["method"] == "build/initialize": - response = { - "jsonrpc": "2.0", - "id": message["id"], - "result": { - "displayName": "test server", - "version": "0.1", - "bspVersion": "2.0", - "rootUri": "blah", - "capabilities": {"languageIds": ["a", "b"]}, - "data": { - "indexDatabasePath": "some/index/db/path", - "indexStorePath": "some/index/store/path" - } - } - } - elif message["method"] == "build/initialized": - continue - elif message["method"] == "build/shutdown": - response = { - "jsonrpc": "2.0", - "id": message["id"], - "result": None - } - elif message["method"] == "build/exit": - break - # ignore other notifications - elif "id" in message: - response = { - "jsonrpc": "2.0", - "id": message["id"], - "error": { - "code": 123, - "message": "unhandled method {}".format(message["method"]), - } - } +class BuildServer(AbstractBuildServer): + pass - if response: - responseStr = json.dumps(response) - try: - sys.stdout.buffer.write(f"Content-Length: {len(responseStr)}\r\n\r\n{responseStr}".encode('utf-8')) - sys.stdout.flush() - except IOError: - # stdout closed, time to quit - break + +BuildServer().run() diff --git a/Sources/SKTestSupport/INPUTS/BuildServerBuildSystemTests.testSettings/buildServer.json b/Sources/SKTestSupport/INPUTS/BuildServerBuildSystemTests.testSettings/buildServer.json deleted file mode 100644 index cf9e70bd..00000000 --- a/Sources/SKTestSupport/INPUTS/BuildServerBuildSystemTests.testSettings/buildServer.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "client name", - "version": "10", - "bspVersion": "2.0", - "languages": ["a", "b"], - "argv": ["server.py"] -} \ No newline at end of file diff --git a/Sources/SKTestSupport/INPUTS/BuildServerBuildSystemTests.testSettings/server.py b/Sources/SKTestSupport/INPUTS/BuildServerBuildSystemTests.testSettings/server.py deleted file mode 100755 index 2afd6e5b..00000000 --- a/Sources/SKTestSupport/INPUTS/BuildServerBuildSystemTests.testSettings/server.py +++ /dev/null @@ -1,81 +0,0 @@ -import json -import os -import sys - - -while True: - line = sys.stdin.readline() - if len(line) == 0: - break - - assert line.startswith('Content-Length:') - length = int(line[len('Content-Length:'):]) - sys.stdin.readline() - message = json.loads(sys.stdin.read(length)) - - response = None - if message["method"] == "build/initialize": - response = { - "jsonrpc": "2.0", - "id": message["id"], - "result": { - "displayName": "test server", - "version": "0.1", - "bspVersion": "2.0", - "rootUri": "blah", - "capabilities": {"languageIds": ["a", "b"]}, - "data": { - "indexStorePath": "some/index/store/path" - } - } - } - elif message["method"] == "build/initialized": - continue - elif message["method"] == "build/shutdown": - response = { - "jsonrpc": "2.0", - "id": message["id"], - "result": None - } - elif message["method"] == "build/exit": - break - elif message["method"] == "textDocument/sourceKitOptions": - file_path = message["params"]["uri"][len("file://"):] - if file_path.endswith(".missing"): - # simulate error response for unhandled file - response = { - "jsonrpc": "2.0", - "id": message["id"], - "error": { - "code": -32600, - "message": "unknown file {}".format(file_path), - } - } - else: - response = { - "jsonrpc": "2.0", - "id": message["id"], - "result": { - "options": ["-a", "-b"], - "workingDirectory": os.path.dirname(file_path), - } - } - # ignore other notifications - elif "id" in message: - response = { - "jsonrpc": "2.0", - "id": message["id"], - "error": { - "code": -32600, - "message": "unhandled method {}".format(message["method"]), - } - } - - if response: - responseStr = json.dumps(response) - try: - sys.stdout.buffer.write(f"Content-Length: {len(responseStr)}\r\n\r\n{responseStr}".encode('utf-8')) - sys.stdout.flush() - except IOError: - # stdout closed, time to quit - break