Skip to content

Commit ef8ee97

Browse files
authored
Merge pull request #133 from ahoppen/ahoppen/no-stderr-match-lsp
Rewrite test-sourcekit-lsp.py to not rely on `--sync` option in sourcekit-lsp
2 parents 09ee8d9 + 1bbf62d commit ef8ee97

File tree

1 file changed

+162
-99
lines changed

1 file changed

+162
-99
lines changed

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

Lines changed: 162 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22
# language services.
33

44
# REQUIRES: have-sourcekit-lsp
5-
# rdar://125139888
6-
# REQUIRES: platform=Darwin
75

86
# Make a sandbox dir.
97
# RUN: rm -rf %t.dir
@@ -14,141 +12,206 @@
1412
# RUN: %{FileCheck} --check-prefix CHECK-BUILD-LOG --input-file %t.build-log %s
1513
# CHECK-BUILD-LOG-NOT: error:
1614

17-
# 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
1816
# RUN: %{FileCheck} --input-file %t.run-log %s
1917

18+
from typing import Dict
2019
import argparse
2120
import json
22-
import os
2321
import subprocess
2422
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)
2591

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

4993
def main():
5094
parser = argparse.ArgumentParser()
51-
parser.add_argument('sourcekit_lsp')
52-
parser.add_argument('package')
95+
parser.add_argument("sourcekit_lsp")
96+
parser.add_argument("package")
5397
args = parser.parse_args()
5498

55-
lsp = LspScript()
56-
lsp.request('initialize', {
57-
'rootPath': args.package,
58-
'capabilities': {},
59-
'initializationOptions': {
60-
'listenToUnitEvents': False,
61-
}
62-
})
63-
64-
main_swift = os.path.join(args.package, 'Sources', 'exec', 'main.swift')
65-
with open(main_swift, 'r') as f:
66-
main_swift_content = f.read()
67-
68-
lsp.note('textDocument/didOpen', {
69-
'textDocument': {
70-
'uri': 'file://' + main_swift,
71-
'languageId': 'swift',
72-
'version': 0,
73-
'text': main_swift_content,
74-
}
75-
})
76-
77-
lsp.request('workspace/_pollIndex', {})
78-
lsp.request('textDocument/definition', {
79-
'textDocument': { 'uri': 'file://' + main_swift },
80-
'position': { 'line': 3, 'character': 6}, ## zero-based
81-
})
82-
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)
83138
# CHECK: "result":[
84139
# CHECK-DAG: lib.swift
85140
# CHECK-DAG: "line":1
86141
# CHECK-DAG: "character":14
87142
# CHECK: ]
88143

89-
lsp.request('textDocument/definition', {
90-
'textDocument': { 'uri': 'file://' + main_swift },
91-
'position': { 'line': 4, 'character': 0}, ## zero-based
92-
})
93-
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)
94155
# CHECK: "result":[
95156
# CHECK-DAG: clib.c
96157
# CHECK-DAG: "line":2
97158
# CHECK-DAG: "character":5
98159
# CHECK: ]
99160

100-
lsp.request('textDocument/completion', {
101-
'textDocument': { 'uri': 'file://' + main_swift },
102-
'position': { 'line': 3, 'character': 6}, ## zero-based
103-
})
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)
104171
# CHECK: "items":[
105172
# CHECK-DAG: "label":"foo()"
106173
# CHECK-DAG: "label":"self"
107174
# CHECK: ]
108175

109-
clib_c = os.path.join(args.package, 'Sources', 'clib', 'clib.c')
110-
with open(clib_c, 'r') as f:
111-
clib_c_content = f.read()
112-
113-
lsp.note('textDocument/didOpen', {
114-
'textDocument': {
115-
'uri': 'file://' + clib_c,
116-
'languageId': 'c',
117-
'version': 0,
118-
'text': clib_c_content,
119-
}
120-
})
121-
122-
lsp.request('textDocument/completion', {
123-
'textDocument': { 'uri': 'file://' + clib_c },
124-
'position': { 'line': 2, 'character': 22}, ## zero-based
125-
})
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)
126198
# CHECK: "items":[
127199
# CHECK-DAG: "insertText":"clib_func"
128200
# Missing "clib_other" from clangd on rebranch - rdar://73762053
129201
# DISABLED-DAG: "insertText":"clib_other"
130202
# CHECK: ]
131203

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

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

215+
153216
if __name__ == "__main__":
154217
main()

0 commit comments

Comments
 (0)