Skip to content

Commit dbd4211

Browse files
committed
Rewrite test-sourcekit-lsp.py to not rely on --sync option in sourcekit-lsp
The `test-sourcekit-lsp` integration test sent all requests to `sourcekit-lsp` via stdin in one go and relies on the `--sync` option in sourcekit-lsp to handle one request at a time. It closes `stdin` when it reaches the end of the data it wants to send to sourcekit-lsp. With the refactored `JSONRPCConnection` implementation, this caused us to immediately close the connection, without waiting for any outstanding replies to be sent. Rewrite `test-sourcekit-lsp.py` to actually wait for the request results. This also allows us to delete the `--sync` option of sourcekit-lsp and test a configuration of sourcekit-lsp that is a lot closer to what actual users are going to use.
1 parent a7ef2ab commit dbd4211

File tree

1 file changed

+160
-96
lines changed

1 file changed

+160
-96
lines changed

test-sourcekit-lsp/test-sourcekit-lsp.py

Lines changed: 160 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -17,136 +17,200 @@
1717

1818
import argparse
1919
import json
20-
import os
2120
import subprocess
2221
import sys
22+
from pathlib import Path
23+
import re
24+
25+
26+
class LspConnection:
27+
def __init__(self, server_path: str):
28+
self.request_id = 0
29+
self.process = subprocess.Popen(
30+
[server_path],
31+
stdin=subprocess.PIPE,
32+
stdout=subprocess.PIPE,
33+
encoding="utf-8",
34+
)
35+
36+
def send_data(self, dict: dict[str, object]):
37+
"""
38+
Encode the given dict as JSON and send it to the LSP server with the 'Content-Length' header.
39+
"""
40+
assert self.process.stdin
41+
body = json.dumps(dict)
42+
data = "Content-Length: {}\r\n\r\n{}".format(len(body), body)
43+
self.process.stdin.write(data)
44+
self.process.stdin.flush()
45+
46+
def send_request(self, method: str, params: dict[str, object]) -> str:
47+
"""
48+
Send a request of the given method and parameters to the LSP server and wait for the response.
49+
"""
50+
self.request_id += 1
51+
52+
self.send_data(
53+
{
54+
"jsonrpc": "2.0",
55+
"id": self.request_id,
56+
"method": method,
57+
"params": params,
58+
}
59+
)
60+
61+
assert self.process.stdout
62+
# Read Content-Length: 123\r\n
63+
# Note: Even though the Content-Length header ends with \r\n, `readline` returns it with a single \n.
64+
header = self.process.stdout.readline()
65+
match = re.match(r"Content-Length: ([0-9]+)\n$", header)
66+
assert match, f"Expected Content-Length header, got '{header}'"
67+
68+
# The Content-Length header is followed by an empty line
69+
empty_line = self.process.stdout.readline()
70+
assert empty_line == "\n", f"Expected empty line, got '{empty_line}'"
71+
72+
# Read the actual response
73+
response = self.process.stdout.read(int(match.group(1)))
74+
assert (
75+
f'"id":{self.request_id}' in response
76+
), f"Expected response for request {self.request_id}, got '{response}'"
77+
return response
78+
79+
def send_notification(self, method: str, params: dict[str, object]):
80+
"""
81+
Send a notification to the LSP server. There's nothing to wait for in response
82+
"""
83+
self.send_data({"jsonrpc": "2.0", "method": method, "params": params})
84+
85+
def wait_for_exit(self, timeout: int) -> int:
86+
"""
87+
Wait for the LSP server to terminate.
88+
"""
89+
return self.process.wait(timeout)
2390

24-
class LspScript(object):
25-
def __init__(self):
26-
self.request_id = 0
27-
self.script = ''
28-
29-
def request(self, method, params):
30-
body = json.dumps({
31-
'jsonrpc': '2.0',
32-
'id': self.request_id,
33-
'method': method,
34-
'params': params
35-
})
36-
self.request_id += 1
37-
self.script += 'Content-Length: {}\r\n\r\n{}'.format(len(body), body)
38-
39-
def note(self, method, params):
40-
body = json.dumps({
41-
'jsonrpc': '2.0',
42-
'method': method,
43-
'params': params
44-
})
45-
self.script += 'Content-Length: {}\r\n\r\n{}'.format(len(body), body)
4691

4792
def main():
4893
parser = argparse.ArgumentParser()
49-
parser.add_argument('sourcekit_lsp')
50-
parser.add_argument('package')
94+
parser.add_argument("sourcekit_lsp")
95+
parser.add_argument("package")
5196
args = parser.parse_args()
5297

53-
lsp = LspScript()
54-
lsp.request('initialize', {
55-
'rootPath': args.package,
56-
'capabilities': {},
57-
'initializationOptions': {
58-
'listenToUnitEvents': False,
59-
}
60-
})
61-
62-
main_swift = os.path.join(args.package, 'Sources', 'exec', 'main.swift')
63-
with open(main_swift, 'r') as f:
64-
main_swift_content = f.read()
65-
66-
lsp.note('textDocument/didOpen', {
67-
'textDocument': {
68-
'uri': 'file://' + main_swift,
69-
'languageId': 'swift',
70-
'version': 0,
71-
'text': main_swift_content,
72-
}
73-
})
74-
75-
lsp.request('workspace/_pollIndex', {})
76-
lsp.request('textDocument/definition', {
77-
'textDocument': { 'uri': 'file://' + main_swift },
78-
'position': { 'line': 3, 'character': 6}, ## zero-based
79-
})
80-
98+
package_dir = Path(args.package)
99+
main_swift = package_dir / "Sources" / "exec" / "main.swift"
100+
clib_c = package_dir / "Sources" / "clib" / "clib.c"
101+
102+
connection = LspConnection(args.sourcekit_lsp)
103+
connection.send_request(
104+
"initialize",
105+
{
106+
"rootPath": args.package,
107+
"capabilities": {},
108+
"initializationOptions": {
109+
"listenToUnitEvents": False,
110+
},
111+
},
112+
)
113+
114+
connection.send_notification(
115+
"textDocument/didOpen",
116+
{
117+
"textDocument": {
118+
"uri": f"file://{main_swift}",
119+
"languageId": "swift",
120+
"version": 0,
121+
"text": main_swift.read_text(),
122+
}
123+
},
124+
)
125+
126+
connection.send_request("workspace/_pollIndex", {})
127+
foo_definition_response = connection.send_request(
128+
"textDocument/definition",
129+
{
130+
"textDocument": {"uri": f"file://{main_swift}"},
131+
"position": {"line": 3, "character": 6}, ## zero-based
132+
},
133+
)
134+
print("foo() definition response")
135+
# CHECK-LABEL: foo() definition response
136+
print(foo_definition_response)
81137
# CHECK: "result":[
82138
# CHECK-DAG: lib.swift
83139
# CHECK-DAG: "line":1
84140
# CHECK-DAG: "character":14
85141
# CHECK: ]
86142

87-
lsp.request('textDocument/definition', {
88-
'textDocument': { 'uri': 'file://' + main_swift },
89-
'position': { 'line': 4, 'character': 0}, ## zero-based
90-
})
91-
143+
clib_func_definition_response = connection.send_request(
144+
"textDocument/definition",
145+
{
146+
"textDocument": {"uri": f"file://{main_swift}"},
147+
"position": {"line": 4, "character": 0}, ## zero-based
148+
},
149+
)
150+
151+
print("clib_func() definition response")
152+
# CHECK-LABEL: clib_func() definition response
153+
print(clib_func_definition_response)
92154
# CHECK: "result":[
93155
# CHECK-DAG: clib.c
94156
# CHECK-DAG: "line":2
95157
# CHECK-DAG: "character":5
96158
# CHECK: ]
97159

98-
lsp.request('textDocument/completion', {
99-
'textDocument': { 'uri': 'file://' + main_swift },
100-
'position': { 'line': 3, 'character': 6}, ## zero-based
101-
})
160+
swift_completion_response = connection.send_request(
161+
"textDocument/completion",
162+
{
163+
"textDocument": {"uri": f"file://{main_swift}"},
164+
"position": {"line": 3, "character": 6}, ## zero-based
165+
},
166+
)
167+
print("Swift completion response")
168+
# CHECK-LABEL: Swift completion response
169+
print(swift_completion_response)
102170
# CHECK: "items":[
103171
# CHECK-DAG: "label":"foo()"
104172
# CHECK-DAG: "label":"self"
105173
# CHECK: ]
106174

107-
clib_c = os.path.join(args.package, 'Sources', 'clib', 'clib.c')
108-
with open(clib_c, 'r') as f:
109-
clib_c_content = f.read()
110-
111-
lsp.note('textDocument/didOpen', {
112-
'textDocument': {
113-
'uri': 'file://' + clib_c,
114-
'languageId': 'c',
115-
'version': 0,
116-
'text': clib_c_content,
117-
}
118-
})
119-
120-
lsp.request('textDocument/completion', {
121-
'textDocument': { 'uri': 'file://' + clib_c },
122-
'position': { 'line': 2, 'character': 22}, ## zero-based
123-
})
175+
connection.send_notification(
176+
"textDocument/didOpen",
177+
{
178+
"textDocument": {
179+
"uri": f"file://{clib_c}",
180+
"languageId": "c",
181+
"version": 0,
182+
"text": clib_c.read_text(),
183+
}
184+
},
185+
)
186+
187+
c_completion_response = connection.send_request(
188+
"textDocument/completion",
189+
{
190+
"textDocument": {"uri": f"file://{clib_c}"},
191+
"position": {"line": 2, "character": 22}, ## zero-based
192+
},
193+
)
194+
print("C completion response")
195+
# CHECK-LABEL: C completion response
196+
print(c_completion_response)
124197
# CHECK: "items":[
125198
# CHECK-DAG: "insertText":"clib_func"
126199
# Missing "clib_other" from clangd on rebranch - rdar://73762053
127200
# DISABLED-DAG: "insertText":"clib_other"
128201
# CHECK: ]
129202

130-
lsp.request('shutdown', {})
131-
lsp.note('exit', {})
132-
133-
print('==== INPUT ====')
134-
print(lsp.script)
135-
print('')
136-
print('==== OUTPUT ====')
203+
connection.send_request("shutdown", {})
204+
connection.send_notification("exit", {})
137205

138-
skargs = [args.sourcekit_lsp, '--sync', '-Xclangd', '-sync']
139-
p = subprocess.Popen(skargs, stdin=subprocess.PIPE, stdout=subprocess.PIPE, encoding='utf-8')
140-
out, _ = p.communicate(lsp.script)
141-
print(out)
142-
print('')
143-
144-
if p.returncode == 0:
145-
print('OK')
206+
return_code = connection.wait_for_exit(timeout=1)
207+
if return_code == 0:
208+
print("OK")
146209
else:
147-
print('error: sourcekit-lsp exited with code {}'.format(p.returncode))
148-
sys.exit(1)
210+
print(f"error: sourcekit-lsp exited with code {return_code}")
211+
sys.exit(1)
149212
# CHECK: OK
150213

214+
151215
if __name__ == "__main__":
152216
main()

0 commit comments

Comments
 (0)