Skip to content

Commit 8d2e9eb

Browse files
committed
Test both TCP modes
Add a test that checks that "tcp server mode" works. Server meaning that this plugin acts as the TCP server and the langserver connects as TCP client.
1 parent 02143f8 commit 8d2e9eb

File tree

4 files changed

+86
-32
lines changed

4 files changed

+86
-32
lines changed

tests/server.py

Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
"""
22
A simple test server for integration tests.
33
4-
Only understands stdio.
5-
Uses the asyncio module and mypy types, so you'll need a modern Python.
4+
Can do JSON-RPC with stdio or TCP sockets as the transport.
65
76
To make this server reply to requests, send the $test/setResponse notification.
87
@@ -18,7 +17,6 @@
1817
Tests can await this request to make sure that they receive notification before code
1918
resumes (since response to request will arrive after requested notification).
2019
21-
TODO: Untested on Windows.
2220
"""
2321
from __future__ import annotations
2422

@@ -38,7 +36,7 @@
3836
import uuid
3937

4038
__package__ = "server"
41-
__version__ = "1.0.0"
39+
__version__ = "2.0.0"
4240

4341

4442
if sys.version_info[:2] < (3, 6):
@@ -484,24 +482,36 @@ def do_blocking_drain() -> None:
484482
# END: https://stackoverflow.com/a/52702646/990142
485483

486484

487-
async def main(tcp_port: int | None = None) -> bool:
485+
async def main(tcp_port: int | None = None, mode: str | None = None) -> bool:
488486
if tcp_port is not None:
489487

490-
class ClientConnectedCallback:
488+
if mode is None or mode == "server":
489+
print("running in TCP server mode", file=sys.stderr)
491490

492-
def __init__(self) -> None:
493-
self.received_shutdown = False
491+
class ClientConnectedCallback:
494492

495-
async def __call__(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None:
496-
session = Session(reader, writer)
497-
self.received_shutdown = await session.run_forever()
493+
def __init__(self) -> None:
494+
self.received_shutdown = False
495+
496+
async def __call__(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None:
497+
session = Session(reader, writer)
498+
self.received_shutdown = await session.run_forever()
499+
500+
callback = ClientConnectedCallback()
501+
server = await asyncio.start_server(callback, port=tcp_port)
502+
# NOTE: This is deliberately wrong -- we should stop serving once the exit notification is received.
503+
# But, it's good to have this botched logic here to make sure that servers shutdown in the integration tests.
504+
await server.serve_forever()
505+
return callback.received_shutdown
506+
507+
if mode is not None and mode == "client":
508+
print("running in TCP client mode", file=sys.stderr)
509+
reader, writer = await asyncio.open_connection(host=None, port=tcp_port)
510+
session = Session(reader, writer)
511+
return await session.run_forever()
512+
513+
return False
498514

499-
callback = ClientConnectedCallback()
500-
server = await asyncio.start_server(callback, port=tcp_port)
501-
# NOTE: This is deliberately wrong -- we should stop serving once the exit notification is received.
502-
# But, it's good to have this botched logic here to make sure that servers shutdown in the integration tests.
503-
await server.serve_forever()
504-
return callback.received_shutdown
505515
reader, writer = await stdio()
506516
session = Session(reader, writer)
507517
return await session.run_forever()
@@ -511,6 +521,7 @@ async def __call__(self, reader: asyncio.StreamReader, writer: asyncio.StreamWri
511521
parser = ArgumentParser(prog=__package__, description=__doc__)
512522
parser.add_argument("-v", "--version", action="store_true", help="print version and exit")
513523
parser.add_argument("-p", "--tcp-port", type=int)
524+
parser.add_argument("--mode", help="one of 'client' or 'server'", default="server")
514525
args = parser.parse_args()
515526
if args.version:
516527
print(__package__, __version__)
@@ -519,7 +530,7 @@ async def __call__(self, reader: asyncio.StreamReader, writer: asyncio.StreamWri
519530
asyncio.set_event_loop(loop)
520531
shutdown_received = False
521532
try:
522-
shutdown_received = loop.run_until_complete(main(args.tcp_port))
533+
shutdown_received = loop.run_until_complete(main(args.tcp_port, args.mode))
523534
except KeyboardInterrupt:
524535
pass
525536
loop.run_until_complete(loop.shutdown_asyncgens())

tests/setup.py

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,23 +41,42 @@ def result(self) -> Any:
4141
return self.__result
4242

4343

44-
def make_stdio_test_config() -> ClientConfig:
45-
return ClientConfig(
46-
name="TEST",
44+
def make_stdio_test_config(name: str, init_options: dict[str, Any]) -> ClientConfig:
45+
"""Start the fake language server in STDIO mode."""
46+
config = ClientConfig(
47+
name=name,
4748
command=["python3", join("$packages", "LSP", "tests", "server.py")],
4849
selector="text.plain",
4950
enabled=True,
5051
)
52+
config.initialization_options.assign(init_options)
53+
return config
5154

5255

53-
def make_tcp_test_config() -> ClientConfig:
54-
return ClientConfig(
55-
name="TEST",
56-
command=["python3", join("$packages", "LSP", "tests", "server.py"), "--tcp-port", "$port"],
56+
def make_tcp_server_test_config(name: str, init_options: dict[str, Any]) -> ClientConfig:
57+
"""Start the fake server in TCP mode, and make it act as the TCP server, awaiting a single client connection."""
58+
config = ClientConfig(
59+
name=name,
60+
command=["python3", join("$packages", "LSP", "tests", "server.py"), "--tcp-port", "$port", "--mode=server"],
5761
selector="text.plain",
5862
tcp_port=0, # select a free one for me
5963
enabled=True,
6064
)
65+
config.initialization_options.assign(init_options)
66+
return config
67+
68+
69+
def make_tcp_client_test_config(name: str, init_options: dict[str, Any]) -> ClientConfig:
70+
"""Start the fake server in TCP mode, and make it act as the TCP client, where it connects to the LSP plugin."""
71+
config = ClientConfig(
72+
name=name,
73+
command=["python3", join("$packages", "LSP", "tests", "server.py"), "--tcp-port", "$port", "--mode=client"],
74+
selector="text.plain",
75+
tcp_port=-1, # select a free one for me
76+
enabled=True,
77+
)
78+
config.initialization_options.assign(init_options)
79+
return config
6180

6281

6382
def add_config(config: ClientConfig) -> None:
@@ -82,7 +101,7 @@ def expand(s: str, w: sublime.Window) -> str:
82101
class TextDocumentTestCase(DeferrableTestCase):
83102
@classmethod
84103
def get_stdio_test_config(cls) -> ClientConfig:
85-
return make_stdio_test_config()
104+
return make_stdio_test_config("TEST", {})
86105

87106
@classmethod
88107
def setUpClass(cls) -> Generator:

tests/test_documents.py

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
from .setup import close_test_view
55
from .setup import expand
66
from .setup import make_stdio_test_config
7+
from .setup import make_tcp_client_test_config
8+
from .setup import make_tcp_server_test_config
79
from .setup import remove_config
810
from .setup import TIMEOUT_TIME
911
from .setup import YieldPromise
@@ -50,16 +52,17 @@ def setUp(self) -> Generator:
5052
self.assertTrue(self.window)
5153
self.session1 = None
5254
self.session2 = None
53-
self.config1 = make_stdio_test_config()
54-
self.config1.initialization_options.assign(initialization_options)
55-
self.config2 = make_stdio_test_config()
56-
self.config2.initialization_options.assign(initialization_options)
57-
self.config2.name = "TEST-2"
55+
self.session3 = None
56+
self.config1 = make_stdio_test_config("TEST-1", initialization_options)
57+
self.config2 = make_tcp_client_test_config("TEST-2", initialization_options)
58+
self.config3 = make_tcp_server_test_config("TEST-3", initialization_options)
5859
self.wm = windows.lookup(self.window)
5960
add_config(self.config1)
6061
add_config(self.config2)
62+
add_config(self.config3)
6163
self.wm.get_config_manager().all[self.config1.name] = self.config1
6264
self.wm.get_config_manager().all[self.config2.name] = self.config2
65+
self.wm.get_config_manager().all[self.config3.name] = self.config3
6366

6467
def test_sends_did_open_to_multiple_sessions(self) -> Generator:
6568
filename = expand(join("$packages", "LSP", "tests", "testfile.txt"), self.window)
@@ -76,14 +79,21 @@ def test_sends_did_open_to_multiple_sessions(self) -> Generator:
7679
yield {
7780
"condition": lambda: self.wm.get_session(self.config2.name, self.view.file_name()) is not None,
7881
"timeout": TIMEOUT_TIME}
82+
yield {
83+
"condition": lambda: self.wm.get_session(self.config3.name, self.view.file_name()) is not None,
84+
"timeout": TIMEOUT_TIME}
7985
self.session1 = self.wm.get_session(self.config1.name, self.view.file_name())
8086
self.session2 = self.wm.get_session(self.config2.name, self.view.file_name())
87+
self.session3 = self.wm.get_session(self.config3.name, self.view.file_name())
8188
self.assertIsNotNone(self.session1)
8289
self.assertIsNotNone(self.session2)
90+
self.assertIsNotNone(self.session3)
8391
self.assertEqual(self.session1.config.name, self.config1.name)
8492
self.assertEqual(self.session2.config.name, self.config2.name)
93+
self.assertEqual(self.session3.config.name, self.config3.name)
8594
yield {"condition": lambda: self.session1.state == ClientStates.READY, "timeout": TIMEOUT_TIME}
8695
yield {"condition": lambda: self.session2.state == ClientStates.READY, "timeout": TIMEOUT_TIME}
96+
yield {"condition": lambda: self.session3.state == ClientStates.READY, "timeout": TIMEOUT_TIME}
8797
yield from self.await_message("initialize")
8898
yield from self.await_message("initialized")
8999
yield from self.await_message("textDocument/didOpen")
@@ -103,6 +113,13 @@ def doCleanups(self) -> Generator:
103113
if self.session2:
104114
sublime.set_timeout_async(self.session2.end_async)
105115
yield lambda: self.session2.state == ClientStates.STOPPING
116+
if self.session3:
117+
sublime.set_timeout_async(self.session3.end_async)
118+
yield lambda: self.session3.state == ClientStates.STOPPING
119+
try:
120+
remove_config(self.config3)
121+
except ValueError:
122+
pass
106123
try:
107124
remove_config(self.config2)
108125
except ValueError:
@@ -111,24 +128,31 @@ def doCleanups(self) -> Generator:
111128
remove_config(self.config1)
112129
except ValueError:
113130
pass
131+
self.wm.get_config_manager().all.pop(self.config3.name, None)
114132
self.wm.get_config_manager().all.pop(self.config2.name, None)
115133
self.wm.get_config_manager().all.pop(self.config1.name, None)
116134
yield from super().doCleanups()
117135

118136
def await_message(self, method: str) -> Generator:
119137
promise1 = YieldPromise()
120138
promise2 = YieldPromise()
139+
promise3 = YieldPromise()
121140

122141
def handler1(params: Any) -> None:
123142
promise1.fulfill(params)
124143

125144
def handler2(params: Any) -> None:
126145
promise2.fulfill(params)
127146

147+
def handler3(params: Any) -> None:
148+
promise3.fulfill(params)
149+
128150
def error_handler(params: Any) -> None:
129151
debug("Got error:", params, "awaiting timeout :(")
130152

131153
self.session1.send_request(Request("$test/getReceived", {"method": method}), handler1, error_handler)
132154
self.session2.send_request(Request("$test/getReceived", {"method": method}), handler2, error_handler)
155+
self.session3.send_request(Request("$test/getReceived", {"method": method}), handler3, error_handler)
133156
yield {"condition": promise1, "timeout": TIMEOUT_TIME}
134157
yield {"condition": promise2, "timeout": TIMEOUT_TIME}
158+
yield {"condition": promise3, "timeout": TIMEOUT_TIME}

tests/test_views.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -395,7 +395,7 @@ def test_format_diagnostic_for_html(self) -> None:
395395
diagnostic2.pop("relatedInformation")
396396
self.assertIn("relatedInformation", diagnostic1)
397397
self.assertNotIn("relatedInformation", diagnostic2)
398-
client_config = make_stdio_test_config()
398+
client_config = make_stdio_test_config("TEST", {})
399399
# They should result in the same minihtml.
400400
self.assertEqual(
401401
format_diagnostic_for_html(self.view, client_config, diagnostic1, [], '#ffffff', "/foo/bar"),

0 commit comments

Comments
 (0)