Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 29 additions & 18 deletions tests/server.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
"""
A simple test server for integration tests.

Only understands stdio.
Uses the asyncio module and mypy types, so you'll need a modern Python.
Can do JSON-RPC with stdio or TCP sockets as the transport.

To make this server reply to requests, send the $test/setResponse notification.

Expand All @@ -18,7 +17,6 @@
Tests can await this request to make sure that they receive notification before code
resumes (since response to request will arrive after requested notification).

TODO: Untested on Windows.
"""
from __future__ import annotations

Expand All @@ -38,7 +36,7 @@
import uuid

__package__ = "server"
__version__ = "1.0.0"
__version__ = "2.0.0"


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


async def main(tcp_port: int | None = None) -> bool:
async def main(tcp_port: int | None = None, mode: str | None = None) -> bool:
Comment thread
rchl marked this conversation as resolved.
Outdated
if tcp_port is not None:

class ClientConnectedCallback:
if mode is None or mode == "server":
print("running in TCP server mode", file=sys.stderr)

def __init__(self) -> None:
self.received_shutdown = False
class ClientConnectedCallback:

async def __call__(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None:
session = Session(reader, writer)
self.received_shutdown = await session.run_forever()
def __init__(self) -> None:
self.received_shutdown = False

async def __call__(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None:
session = Session(reader, writer)
self.received_shutdown = await session.run_forever()

callback = ClientConnectedCallback()
server = await asyncio.start_server(callback, port=tcp_port)
# NOTE: This is deliberately wrong -- we should stop serving once the exit notification is received. But,
# it's good to have this botched logic here to make sure that servers shutdown in the integration tests.
await server.serve_forever()
return callback.received_shutdown

if mode is not None and mode == "client":
Comment thread
rchl marked this conversation as resolved.
Outdated
print("running in TCP client mode", file=sys.stderr)
reader, writer = await asyncio.open_connection(host=None, port=tcp_port)
session = Session(reader, writer)
return await session.run_forever()

return False

callback = ClientConnectedCallback()
server = await asyncio.start_server(callback, port=tcp_port)
# NOTE: This is deliberately wrong -- we should stop serving once the exit notification is received.
# But, it's good to have this botched logic here to make sure that servers shutdown in the integration tests.
await server.serve_forever()
return callback.received_shutdown
reader, writer = await stdio()
session = Session(reader, writer)
return await session.run_forever()
Expand All @@ -511,6 +521,7 @@ async def __call__(self, reader: asyncio.StreamReader, writer: asyncio.StreamWri
parser = ArgumentParser(prog=__package__, description=__doc__)
parser.add_argument("-v", "--version", action="store_true", help="print version and exit")
parser.add_argument("-p", "--tcp-port", type=int)
parser.add_argument("--mode", help="one of 'client' or 'server'", default="server")
Comment thread
rchl marked this conversation as resolved.
Outdated
args = parser.parse_args()
if args.version:
print(__package__, __version__)
Expand All @@ -519,7 +530,7 @@ async def __call__(self, reader: asyncio.StreamReader, writer: asyncio.StreamWri
asyncio.set_event_loop(loop)
shutdown_received = False
try:
shutdown_received = loop.run_until_complete(main(args.tcp_port))
shutdown_received = loop.run_until_complete(main(args.tcp_port, args.mode))
except KeyboardInterrupt:
pass
loop.run_until_complete(loop.shutdown_asyncgens())
Expand Down
35 changes: 27 additions & 8 deletions tests/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,23 +41,42 @@ def result(self) -> Any:
return self.__result


def make_stdio_test_config() -> ClientConfig:
return ClientConfig(
name="TEST",
def make_stdio_test_config(name: str, init_options: dict[str, Any]) -> ClientConfig:
Comment thread
rchl marked this conversation as resolved.
Outdated
"""Start the fake language server in STDIO mode."""
Comment thread
rchl marked this conversation as resolved.
Outdated
config = ClientConfig(
name=name,
command=["python3", join("$packages", "LSP", "tests", "server.py")],
selector="text.plain",
enabled=True,
)
config.initialization_options.assign(init_options)
return config
Comment thread
rchl marked this conversation as resolved.
Outdated


def make_tcp_test_config() -> ClientConfig:
return ClientConfig(
name="TEST",
command=["python3", join("$packages", "LSP", "tests", "server.py"), "--tcp-port", "$port"],
def make_tcp_server_test_config(name: str, init_options: dict[str, Any]) -> ClientConfig:
"""Start the fake server in TCP mode, and make it act as the TCP server, awaiting a single client connection."""
config = ClientConfig(
name=name,
command=["python3", join("$packages", "LSP", "tests", "server.py"), "--tcp-port", "$port", "--mode=server"],
selector="text.plain",
tcp_port=0, # select a free one for me
enabled=True,
)
config.initialization_options.assign(init_options)
return config


def make_tcp_client_test_config(name: str, init_options: dict[str, Any]) -> ClientConfig:
"""Start the fake server in TCP mode, and make it act as the TCP client, where it connects to the LSP plugin."""
config = ClientConfig(
name=name,
command=["python3", join("$packages", "LSP", "tests", "server.py"), "--tcp-port", "$port", "--mode=client"],
selector="text.plain",
tcp_port=-1, # select a free one for me
enabled=True,
)
config.initialization_options.assign(init_options)
return config


def add_config(config: ClientConfig) -> None:
Expand All @@ -82,7 +101,7 @@ def expand(s: str, w: sublime.Window) -> str:
class TextDocumentTestCase(DeferrableTestCase):
@classmethod
def get_stdio_test_config(cls) -> ClientConfig:
return make_stdio_test_config()
return make_stdio_test_config("TEST", {})

@classmethod
def setUpClass(cls) -> Generator:
Expand Down
34 changes: 29 additions & 5 deletions tests/test_documents.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
from .setup import close_test_view
from .setup import expand
from .setup import make_stdio_test_config
from .setup import make_tcp_client_test_config
from .setup import make_tcp_server_test_config
from .setup import remove_config
from .setup import TIMEOUT_TIME
from .setup import YieldPromise
Expand Down Expand Up @@ -50,16 +52,17 @@ def setUp(self) -> Generator:
self.assertTrue(self.window)
self.session1 = None
self.session2 = None
self.config1 = make_stdio_test_config()
self.config1.initialization_options.assign(initialization_options)
self.config2 = make_stdio_test_config()
self.config2.initialization_options.assign(initialization_options)
self.config2.name = "TEST-2"
self.session3 = None
self.config1 = make_stdio_test_config("TEST-1", initialization_options)
self.config2 = make_tcp_client_test_config("TEST-2", initialization_options)
self.config3 = make_tcp_server_test_config("TEST-3", initialization_options)
self.wm = windows.lookup(self.window)
add_config(self.config1)
add_config(self.config2)
add_config(self.config3)
self.wm.get_config_manager().all[self.config1.name] = self.config1
self.wm.get_config_manager().all[self.config2.name] = self.config2
self.wm.get_config_manager().all[self.config3.name] = self.config3

def test_sends_did_open_to_multiple_sessions(self) -> Generator:
filename = expand(join("$packages", "LSP", "tests", "testfile.txt"), self.window)
Expand All @@ -76,14 +79,21 @@ def test_sends_did_open_to_multiple_sessions(self) -> Generator:
yield {
"condition": lambda: self.wm.get_session(self.config2.name, self.view.file_name()) is not None,
"timeout": TIMEOUT_TIME}
yield {
"condition": lambda: self.wm.get_session(self.config3.name, self.view.file_name()) is not None,
"timeout": TIMEOUT_TIME}
self.session1 = self.wm.get_session(self.config1.name, self.view.file_name())
self.session2 = self.wm.get_session(self.config2.name, self.view.file_name())
self.session3 = self.wm.get_session(self.config3.name, self.view.file_name())
self.assertIsNotNone(self.session1)
self.assertIsNotNone(self.session2)
self.assertIsNotNone(self.session3)
self.assertEqual(self.session1.config.name, self.config1.name)
self.assertEqual(self.session2.config.name, self.config2.name)
self.assertEqual(self.session3.config.name, self.config3.name)
yield {"condition": lambda: self.session1.state == ClientStates.READY, "timeout": TIMEOUT_TIME}
yield {"condition": lambda: self.session2.state == ClientStates.READY, "timeout": TIMEOUT_TIME}
yield {"condition": lambda: self.session3.state == ClientStates.READY, "timeout": TIMEOUT_TIME}
yield from self.await_message("initialize")
yield from self.await_message("initialized")
yield from self.await_message("textDocument/didOpen")
Expand All @@ -103,6 +113,13 @@ def doCleanups(self) -> Generator:
if self.session2:
sublime.set_timeout_async(self.session2.end_async)
yield lambda: self.session2.state == ClientStates.STOPPING
if self.session3:
sublime.set_timeout_async(self.session3.end_async)
yield lambda: self.session3.state == ClientStates.STOPPING
try:
remove_config(self.config3)
except ValueError:
pass
try:
remove_config(self.config2)
except ValueError:
Expand All @@ -111,24 +128,31 @@ def doCleanups(self) -> Generator:
remove_config(self.config1)
except ValueError:
pass
self.wm.get_config_manager().all.pop(self.config3.name, None)
self.wm.get_config_manager().all.pop(self.config2.name, None)
self.wm.get_config_manager().all.pop(self.config1.name, None)
yield from super().doCleanups()

def await_message(self, method: str) -> Generator:
promise1 = YieldPromise()
promise2 = YieldPromise()
promise3 = YieldPromise()

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

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

def handler3(params: Any) -> None:
promise3.fulfill(params)

def error_handler(params: Any) -> None:
debug("Got error:", params, "awaiting timeout :(")

self.session1.send_request(Request("$test/getReceived", {"method": method}), handler1, error_handler)
self.session2.send_request(Request("$test/getReceived", {"method": method}), handler2, error_handler)
self.session3.send_request(Request("$test/getReceived", {"method": method}), handler3, error_handler)
yield {"condition": promise1, "timeout": TIMEOUT_TIME}
yield {"condition": promise2, "timeout": TIMEOUT_TIME}
yield {"condition": promise3, "timeout": TIMEOUT_TIME}
2 changes: 1 addition & 1 deletion tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,7 @@ def test_format_diagnostic_for_html(self) -> None:
diagnostic2.pop("relatedInformation")
self.assertIn("relatedInformation", diagnostic1)
self.assertNotIn("relatedInformation", diagnostic2)
client_config = make_stdio_test_config()
client_config = make_stdio_test_config("TEST", {})
# They should result in the same minihtml.
self.assertEqual(
format_diagnostic_for_html(self.view, client_config, diagnostic1, [], '#ffffff', "/foo/bar"),
Expand Down