Skip to content

Commit 1bbf62d

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 1bbf62d

File tree

1 file changed

+161
-96
lines changed

1 file changed

+161
-96
lines changed

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

Lines changed: 161 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -15,138 +15,203 @@
1515
# RUN: %{python} -u %s %{sourcekit-lsp} %t.dir/pkg | tee %t.run-log
1616
# RUN: %{FileCheck} --input-file %t.run-log %s
1717

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

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)
4692

4793
def main():
4894
parser = argparse.ArgumentParser()
49-
parser.add_argument('sourcekit_lsp')
50-
parser.add_argument('package')
95+
parser.add_argument("sourcekit_lsp")
96+
parser.add_argument("package")
5197
args = parser.parse_args()
5298

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-
99+
package_dir = Path(args.package)
100+
main_swift = package_dir / "Sources" / "exec" / "main.swift"
101+
clib_c = package_dir / "Sources" / "clib" / "clib.c"
102+
103+
connection = LspConnection(args.sourcekit_lsp)
104+
connection.send_request(
105+
"initialize",
106+
{
107+
"rootPath": args.package,
108+
"capabilities": {},
109+
"initializationOptions": {
110+
"listenToUnitEvents": False,
111+
},
112+
},
113+
)
114+
115+
connection.send_notification(
116+
"textDocument/didOpen",
117+
{
118+
"textDocument": {
119+
"uri": f"file://{main_swift}",
120+
"languageId": "swift",
121+
"version": 0,
122+
"text": main_swift.read_text(),
123+
}
124+
},
125+
)
126+
127+
connection.send_request("workspace/_pollIndex", {})
128+
foo_definition_response = connection.send_request(
129+
"textDocument/definition",
130+
{
131+
"textDocument": {"uri": f"file://{main_swift}"},
132+
"position": {"line": 3, "character": 6}, ## zero-based
133+
},
134+
)
135+
print("foo() definition response")
136+
# CHECK-LABEL: foo() definition response
137+
print(foo_definition_response)
81138
# CHECK: "result":[
82139
# CHECK-DAG: lib.swift
83140
# CHECK-DAG: "line":1
84141
# CHECK-DAG: "character":14
85142
# CHECK: ]
86143

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

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

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-
})
176+
connection.send_notification(
177+
"textDocument/didOpen",
178+
{
179+
"textDocument": {
180+
"uri": f"file://{clib_c}",
181+
"languageId": "c",
182+
"version": 0,
183+
"text": clib_c.read_text(),
184+
}
185+
},
186+
)
187+
188+
c_completion_response = connection.send_request(
189+
"textDocument/completion",
190+
{
191+
"textDocument": {"uri": f"file://{clib_c}"},
192+
"position": {"line": 2, "character": 22}, ## zero-based
193+
},
194+
)
195+
print("C completion response")
196+
# CHECK-LABEL: C completion response
197+
print(c_completion_response)
124198
# CHECK: "items":[
125199
# CHECK-DAG: "insertText":"clib_func"
126200
# Missing "clib_other" from clangd on rebranch - rdar://73762053
127201
# DISABLED-DAG: "insertText":"clib_other"
128202
# CHECK: ]
129203

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

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')
207+
return_code = connection.wait_for_exit(timeout=1)
208+
if return_code == 0:
209+
print("OK")
146210
else:
147-
print('error: sourcekit-lsp exited with code {}'.format(p.returncode))
148-
sys.exit(1)
211+
print(f"error: sourcekit-lsp exited with code {return_code}")
212+
sys.exit(1)
149213
# CHECK: OK
150214

215+
151216
if __name__ == "__main__":
152217
main()

0 commit comments

Comments
 (0)