Skip to content

Commit fcd000f

Browse files
authored
Merge pull request #135 from ahoppen/ahoppen/6.0/ignore-notifications-from-lsp
[6.0] Rewrite test-sourcekit-lsp.py to not rely on `--sync` option in sourcekit-lsp
2 parents 86e36f6 + 456e12d commit fcd000f

File tree

1 file changed

+185
-97
lines changed

1 file changed

+185
-97
lines changed

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

Lines changed: 185 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -12,141 +12,229 @@
1212
# RUN: %{FileCheck} --check-prefix CHECK-BUILD-LOG --input-file %t.build-log %s
1313
# CHECK-BUILD-LOG-NOT: error:
1414

15-
# RUN: %{python} -u %s %{sourcekit-lsp} %t.dir/pkg 2>&1 | tee %t.run-log
15+
# 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 read_message_from_lsp_server(self) -> str:
48+
"""
49+
Read a single message sent from the LSP server to the client.
50+
This can be a request reply, notification or request sent from the server to the client.
51+
"""
52+
assert self.process.stdout
53+
# Read Content-Length: 123\r\n
54+
# Note: Even though the Content-Length header ends with \r\n, `readline` returns it with a single \n.
55+
header = self.process.stdout.readline()
56+
match = re.match(r"Content-Length: ([0-9]+)\n$", header)
57+
assert match, f"Expected Content-Length header, got '{header}'"
58+
59+
# The Content-Length header is followed by an empty line
60+
empty_line = self.process.stdout.readline()
61+
assert empty_line == "\n", f"Expected empty line, got '{empty_line}'"
62+
63+
# Read the actual response
64+
return self.process.stdout.read(int(match.group(1)))
65+
66+
def read_request_reply_from_lsp_server(self, request_id: int) -> str:
67+
"""
68+
Read all messages sent from the LSP server until we see a request reply.
69+
Assert that this request reply was for the given request_id and return it.
70+
"""
71+
message = self.read_message_from_lsp_server()
72+
message_obj = json.loads(message)
73+
if "result" not in message_obj:
74+
# We received a message that wasn't the request reply.
75+
# Log it, ignore it and wait for the next message.
76+
print("Received message")
77+
print(message)
78+
return self.read_request_reply_from_lsp_server(request_id)
79+
# We always wait for a request reply before sending the next request.
80+
# If we received a request reply, it should thus have the request ID of the last request that we sent.
81+
assert (
82+
message_obj["id"] == self.request_id
83+
), f"Expected response for request {self.request_id}, got '{message}'"
84+
return message
85+
86+
def send_request(self, method: str, params: Dict[str, object]) -> str:
87+
"""
88+
Send a request of the given method and parameters to the LSP server and wait for the response.
89+
"""
90+
self.request_id += 1
91+
92+
self.send_data(
93+
{
94+
"jsonrpc": "2.0",
95+
"id": self.request_id,
96+
"method": method,
97+
"params": params,
98+
}
99+
)
100+
101+
return self.read_request_reply_from_lsp_server(self.request_id)
102+
103+
def send_notification(self, method: str, params: Dict[str, object]):
104+
"""
105+
Send a notification to the LSP server. There's nothing to wait for in response
106+
"""
107+
self.send_data({"jsonrpc": "2.0", "method": method, "params": params})
108+
109+
def wait_for_exit(self, timeout: int) -> int:
110+
"""
111+
Wait for the LSP server to terminate.
112+
"""
113+
return self.process.wait(timeout)
23114

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

47116
def main():
48117
parser = argparse.ArgumentParser()
49-
parser.add_argument('sourcekit_lsp')
50-
parser.add_argument('package')
118+
parser.add_argument("sourcekit_lsp")
119+
parser.add_argument("package")
51120
args = parser.parse_args()
52121

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-
122+
package_dir = Path(args.package)
123+
main_swift = package_dir / "Sources" / "exec" / "main.swift"
124+
clib_c = package_dir / "Sources" / "clib" / "clib.c"
125+
126+
connection = LspConnection(args.sourcekit_lsp)
127+
connection.send_request(
128+
"initialize",
129+
{
130+
"rootPath": args.package,
131+
"capabilities": {},
132+
"initializationOptions": {
133+
"listenToUnitEvents": False,
134+
},
135+
},
136+
)
137+
138+
connection.send_notification(
139+
"textDocument/didOpen",
140+
{
141+
"textDocument": {
142+
"uri": f"file://{main_swift}",
143+
"languageId": "swift",
144+
"version": 0,
145+
"text": main_swift.read_text(),
146+
}
147+
},
148+
)
149+
150+
connection.send_request("workspace/_pollIndex", {})
151+
foo_definition_response = connection.send_request(
152+
"textDocument/definition",
153+
{
154+
"textDocument": {"uri": f"file://{main_swift}"},
155+
"position": {"line": 3, "character": 6}, ## zero-based
156+
},
157+
)
158+
print("foo() definition response")
159+
# CHECK-LABEL: foo() definition response
160+
print(foo_definition_response)
81161
# CHECK: "result":[
82162
# CHECK-DAG: lib.swift
83163
# CHECK-DAG: "line":1
84164
# CHECK-DAG: "character":14
85165
# CHECK: ]
86166

87-
lsp.request('textDocument/definition', {
88-
'textDocument': { 'uri': 'file://' + main_swift },
89-
'position': { 'line': 4, 'character': 0}, ## zero-based
90-
})
91-
167+
clib_func_definition_response = connection.send_request(
168+
"textDocument/definition",
169+
{
170+
"textDocument": {"uri": f"file://{main_swift}"},
171+
"position": {"line": 4, "character": 0}, ## zero-based
172+
},
173+
)
174+
175+
print("clib_func() definition response")
176+
# CHECK-LABEL: clib_func() definition response
177+
print(clib_func_definition_response)
92178
# CHECK: "result":[
93179
# CHECK-DAG: clib.c
94180
# CHECK-DAG: "line":2
95181
# CHECK-DAG: "character":5
96182
# CHECK: ]
97183

98-
lsp.request('textDocument/completion', {
99-
'textDocument': { 'uri': 'file://' + main_swift },
100-
'position': { 'line': 3, 'character': 6}, ## zero-based
101-
})
184+
swift_completion_response = connection.send_request(
185+
"textDocument/completion",
186+
{
187+
"textDocument": {"uri": f"file://{main_swift}"},
188+
"position": {"line": 3, "character": 6}, ## zero-based
189+
},
190+
)
191+
print("Swift completion response")
192+
# CHECK-LABEL: Swift completion response
193+
print(swift_completion_response)
102194
# CHECK: "items":[
103195
# CHECK-DAG: "label":"foo()"
104196
# CHECK-DAG: "label":"self"
105197
# CHECK: ]
106198

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-
})
199+
connection.send_notification(
200+
"textDocument/didOpen",
201+
{
202+
"textDocument": {
203+
"uri": f"file://{clib_c}",
204+
"languageId": "c",
205+
"version": 0,
206+
"text": clib_c.read_text(),
207+
}
208+
},
209+
)
210+
211+
c_completion_response = connection.send_request(
212+
"textDocument/completion",
213+
{
214+
"textDocument": {"uri": f"file://{clib_c}"},
215+
"position": {"line": 2, "character": 22}, ## zero-based
216+
},
217+
)
218+
print("C completion response")
219+
# CHECK-LABEL: C completion response
220+
print(c_completion_response)
124221
# CHECK: "items":[
125222
# CHECK-DAG: "insertText":"clib_func"
126223
# Missing "clib_other" from clangd on rebranch - rdar://73762053
127224
# DISABLED-DAG: "insertText":"clib_other"
128225
# CHECK: ]
129226

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

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')
230+
return_code = connection.wait_for_exit(timeout=1)
231+
if return_code == 0:
232+
print("OK")
146233
else:
147-
print('error: sourcekit-lsp exited with code {}'.format(p.returncode))
148-
sys.exit(1)
234+
print(f"error: sourcekit-lsp exited with code {return_code}")
235+
sys.exit(1)
149236
# CHECK: OK
150237

238+
151239
if __name__ == "__main__":
152240
main()

0 commit comments

Comments
 (0)