172 lines
5.0 KiB
Python
172 lines
5.0 KiB
Python
"""Fixtures for clice LSP integration tests using pygls LanguageClient."""
|
|
|
|
import json
|
|
import asyncio
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
import pytest_asyncio
|
|
from lsprotocol.types import (
|
|
PROGRESS,
|
|
TEXT_DOCUMENT_PUBLISH_DIAGNOSTICS,
|
|
WINDOW_WORK_DONE_PROGRESS_CREATE,
|
|
ClientCapabilities,
|
|
Diagnostic,
|
|
InitializeParams,
|
|
InitializedParams,
|
|
ProgressParams,
|
|
PublishDiagnosticsParams,
|
|
WorkDoneProgressCreateParams,
|
|
WorkspaceFolder,
|
|
)
|
|
from pygls.lsp.client import BaseLanguageClient
|
|
|
|
|
|
def pytest_addoption(parser: pytest.Parser):
|
|
parser.addoption(
|
|
"--executable",
|
|
required=False,
|
|
help="Path to the clice executable.",
|
|
)
|
|
parser.addoption(
|
|
"--mode",
|
|
type=str,
|
|
choices=["pipe", "socket"],
|
|
default="pipe",
|
|
help="The connection mode to use.",
|
|
)
|
|
parser.addoption(
|
|
"--host",
|
|
type=str,
|
|
default="127.0.0.1",
|
|
help="The host to connect to (default: 127.0.0.1)",
|
|
)
|
|
parser.addoption(
|
|
"--port",
|
|
type=int,
|
|
default=50051,
|
|
help="The port to connect to",
|
|
)
|
|
|
|
|
|
class CliceClient(BaseLanguageClient):
|
|
"""Language client that tracks server-sent notifications."""
|
|
|
|
def __init__(self):
|
|
super().__init__("clice-test-client", "0.1.0")
|
|
self.diagnostics: dict[str, list[Diagnostic]] = {}
|
|
self.diagnostics_events: dict[str, asyncio.Event] = {}
|
|
self.progress_tokens: list[str] = []
|
|
self.progress_events: list[dict] = []
|
|
|
|
@self.feature(TEXT_DOCUMENT_PUBLISH_DIAGNOSTICS)
|
|
def on_diagnostics(params: PublishDiagnosticsParams):
|
|
self.diagnostics[params.uri] = list(params.diagnostics)
|
|
if params.uri in self.diagnostics_events:
|
|
self.diagnostics_events[params.uri].set()
|
|
|
|
@self.feature(WINDOW_WORK_DONE_PROGRESS_CREATE)
|
|
def on_create_progress(params: WorkDoneProgressCreateParams):
|
|
token = str(params.token) if isinstance(params.token, int) else params.token
|
|
self.progress_tokens.append(token)
|
|
return None
|
|
|
|
@self.feature(PROGRESS)
|
|
def on_progress(params: ProgressParams):
|
|
token = str(params.token) if isinstance(params.token, int) else params.token
|
|
self.progress_events.append({"token": token, "value": params.value})
|
|
|
|
def wait_for_diagnostics(self, uri: str) -> asyncio.Event:
|
|
"""Get or create an event that fires when diagnostics arrive for uri."""
|
|
if uri not in self.diagnostics_events:
|
|
self.diagnostics_events[uri] = asyncio.Event()
|
|
else:
|
|
self.diagnostics_events[uri].clear()
|
|
return self.diagnostics_events[uri]
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def executable(request) -> Path:
|
|
exe = request.config.getoption("--executable")
|
|
if not exe:
|
|
pytest.skip("--executable not provided")
|
|
|
|
path = Path(exe)
|
|
if sys.platform.startswith("win") and path.suffix.lower() != ".exe":
|
|
path_exe = path.with_name(path.name + ".exe")
|
|
if path_exe.exists() or not path.exists():
|
|
path = path_exe
|
|
|
|
if not path.exists():
|
|
pytest.exit(
|
|
f"Error: clice executable not found at '{exe}'. "
|
|
"Please ensure the path is correct.",
|
|
returncode=64,
|
|
)
|
|
return path.resolve()
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def test_data_dir():
|
|
path = Path(__file__).parent / "data"
|
|
data_dir = path.resolve()
|
|
|
|
# Generate compile_commands.json for hello_world
|
|
hw_dir = data_dir / "hello_world"
|
|
main_cpp = hw_dir / "main.cpp"
|
|
cdb_path = hw_dir / "compile_commands.json"
|
|
if main_cpp.exists() and not cdb_path.exists():
|
|
cdb = [
|
|
{
|
|
"directory": str(hw_dir),
|
|
"file": str(main_cpp),
|
|
"arguments": ["clang++", "-std=c++17", "-fsyntax-only", str(main_cpp)],
|
|
}
|
|
]
|
|
cdb_path.write_text(json.dumps(cdb, indent=2))
|
|
|
|
return data_dir
|
|
|
|
|
|
@pytest_asyncio.fixture
|
|
async def client(request, executable: Path, test_data_dir: Path):
|
|
"""Spawn clice server, yield pygls client, then shutdown+exit."""
|
|
config = request.config
|
|
mode = config.getoption("--mode")
|
|
|
|
cmd = [str(executable), "--mode", mode]
|
|
if mode == "socket":
|
|
host = config.getoption("--host")
|
|
port = config.getoption("--port")
|
|
cmd += ["--host", host, "--port", str(port)]
|
|
|
|
c = CliceClient()
|
|
await c.start_io(*cmd)
|
|
|
|
yield c
|
|
|
|
# Graceful shutdown
|
|
try:
|
|
await asyncio.wait_for(c.shutdown_async(None), timeout=3.0)
|
|
except Exception:
|
|
pass
|
|
try:
|
|
c.exit(None)
|
|
except Exception:
|
|
pass
|
|
|
|
# Wait briefly, then force-kill if still running
|
|
await asyncio.sleep(0.3)
|
|
if hasattr(c, "_server") and c._server is not None and c._server.returncode is None:
|
|
c._server.kill()
|
|
|
|
# Stop pygls client (with timeout to avoid hanging)
|
|
try:
|
|
c._stop_event.set()
|
|
for task in c._async_tasks:
|
|
task.cancel()
|
|
await asyncio.sleep(0.1)
|
|
except Exception:
|
|
pass
|