Simplify the Python files used to write BSP servers for SourceKit-LSP tests

This commit is contained in:
Alex Hoppen
2024-09-18 19:26:19 -07:00
parent d8b41d4eff
commit 813a10e66c
12 changed files with 164 additions and 580 deletions

View File

@@ -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

View File

@@ -1,7 +0,0 @@
{
"name": "client name",
"version": "10",
"bspVersion": "2.0",
"languages": ["a", "b"],
"argv": ["server.py"]
}

View File

@@ -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

View File

@@ -1,7 +0,0 @@
{
"name": "client name",
"version": "10",
"bspVersion": "2.0",
"languages": ["a", "b"],
"argv": ["server.py"]
}

View File

@@ -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

View File

@@ -1,7 +0,0 @@
{
"name": "client name",
"version": "10",
"bspVersion": "2.0",
"languages": ["a", "b"],
"argv": ["server.py"]
}

View File

@@ -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

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -1,7 +0,0 @@
{
"name": "client name",
"version": "10",
"bspVersion": "2.0",
"languages": ["a", "b"],
"argv": ["server.py"]
}

View File

@@ -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