Files
clice/tests/integration/test_server.py
2026-03-22 23:37:08 +08:00

455 lines
15 KiB
Python

"""Integration tests for the clice MasterServer using pygls."""
import asyncio
from pathlib import Path
import pytest
from lsprotocol.types import (
ClientCapabilities,
CodeActionContext,
CodeActionParams,
CompletionParams,
DefinitionParams,
DidCloseTextDocumentParams,
DidOpenTextDocumentParams,
DidChangeTextDocumentParams,
DidSaveTextDocumentParams,
DocumentLinkParams,
DocumentSymbolParams,
FoldingRangeParams,
HoverParams,
InitializeParams,
InitializedParams,
InlayHintParams,
Position,
Range,
SemanticTokensParams,
SignatureHelpParams,
TextDocumentContentChangeWholeDocument,
TextDocumentIdentifier,
TextDocumentItem,
VersionedTextDocumentIdentifier,
WorkspaceFolder,
)
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _workspace_uri(test_data_dir: Path, name: str = "hello_world") -> str:
return (test_data_dir / name).as_uri()
def _file_uri(
test_data_dir: Path, name: str = "hello_world", file: str = "main.cpp"
) -> str:
return (test_data_dir / name / file).as_uri()
def _doc(uri: str) -> TextDocumentIdentifier:
return TextDocumentIdentifier(uri=uri)
async def _initialize(client, test_data_dir: Path, name: str = "hello_world"):
"""Initialize with a workspace folder."""
ws = test_data_dir / name
result = await client.initialize_async(
InitializeParams(
capabilities=ClientCapabilities(),
root_uri=ws.as_uri(),
workspace_folders=[WorkspaceFolder(uri=ws.as_uri(), name="test")],
)
)
client.initialized(InitializedParams())
return result
async def _open_file(
client, test_data_dir: Path, name: str = "hello_world", file: str = "main.cpp"
):
"""Open a text document."""
path = test_data_dir / name / file
content = path.read_text(encoding="utf-8")
uri = path.as_uri()
client.text_document_did_open(
DidOpenTextDocumentParams(
text_document=TextDocumentItem(
uri=uri,
language_id="cpp",
version=0,
text=content,
)
)
)
return uri, content
async def _wait_for_compilation(client, uri: str, timeout: float = 30.0):
"""Wait for diagnostics on the given URI."""
event = client.wait_for_diagnostics(uri)
await asyncio.wait_for(event.wait(), timeout=timeout)
async def _open_and_wait(
client,
test_data_dir: Path,
name: str = "hello_world",
file: str = "main.cpp",
timeout: float = 30.0,
):
"""Open file and wait for compilation diagnostics."""
uri, content = await _open_file(client, test_data_dir, name, file)
await _wait_for_compilation(client, uri, timeout)
return uri, content
# ---------------------------------------------------------------------------
# Server info & capabilities
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_server_info(client, test_data_dir):
result = await _initialize(client, test_data_dir)
assert result.server_info.name == "clice"
assert result.server_info.version == "0.1.0"
@pytest.mark.asyncio
async def test_capabilities(client, test_data_dir):
result = await _initialize(client, test_data_dir)
caps = result.capabilities
assert caps.hover_provider is True
assert caps.completion_provider is not None
assert caps.definition_provider is True
assert caps.document_symbol_provider is True
assert caps.folding_range_provider is True
assert caps.inlay_hint_provider is True
assert caps.code_action_provider is True
assert caps.semantic_tokens_provider is not None
# ---------------------------------------------------------------------------
# Initialization & shutdown
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_double_initialize_rejected(client, test_data_dir):
await _initialize(client, test_data_dir)
with pytest.raises(Exception):
await client.initialize_async(
InitializeParams(
capabilities=ClientCapabilities(),
workspace_folders=[],
)
)
@pytest.mark.asyncio
async def test_did_open_close_cycle(client, test_data_dir):
await _initialize(client, test_data_dir)
uri, _ = await _open_file(client, test_data_dir)
await asyncio.sleep(0.5)
client.text_document_did_close(DidCloseTextDocumentParams(text_document=_doc(uri)))
@pytest.mark.asyncio
async def test_shutdown_exit(client, test_data_dir):
await _initialize(client, test_data_dir)
await client.shutdown_async(None)
@pytest.mark.asyncio
async def test_feature_requests_after_close(client, test_data_dir):
await _initialize(client, test_data_dir)
uri, _ = await _open_file(client, test_data_dir)
client.text_document_did_close(DidCloseTextDocumentParams(text_document=_doc(uri)))
result = await client.text_document_hover_async(
HoverParams(text_document=_doc(uri), position=Position(line=0, character=0))
)
assert result is None
# ---------------------------------------------------------------------------
# Document handling
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_incremental_change(client, test_data_dir):
await _initialize(client, test_data_dir)
uri, content = await _open_file(client, test_data_dir)
for i in range(5):
content += f"\n// change {i}"
client.text_document_did_change(
DidChangeTextDocumentParams(
text_document=VersionedTextDocumentIdentifier(uri=uri, version=i + 1),
content_changes=[TextDocumentContentChangeWholeDocument(text=content)],
)
)
await asyncio.sleep(0.05)
await asyncio.sleep(1)
client.text_document_did_close(DidCloseTextDocumentParams(text_document=_doc(uri)))
@pytest.mark.asyncio
async def test_diagnostics_received(client, test_data_dir):
await _initialize(client, test_data_dir)
uri, _ = await _open_and_wait(client, test_data_dir)
assert uri in client.diagnostics
client.text_document_did_close(DidCloseTextDocumentParams(text_document=_doc(uri)))
# ---------------------------------------------------------------------------
# Feature requests (after compilation)
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_hover_before_compile(client, test_data_dir):
await _initialize(client, test_data_dir)
uri, _ = await _open_file(client, test_data_dir)
result = await client.text_document_hover_async(
HoverParams(text_document=_doc(uri), position=Position(line=0, character=0))
)
# May return null before compilation — that's fine
client.text_document_did_close(DidCloseTextDocumentParams(text_document=_doc(uri)))
@pytest.mark.asyncio
async def test_completion_request(client, test_data_dir):
await _initialize(client, test_data_dir)
uri, _ = await _open_and_wait(client, test_data_dir)
result = await client.text_document_completion_async(
CompletionParams(
text_document=_doc(uri), position=Position(line=0, character=0)
)
)
client.text_document_did_close(DidCloseTextDocumentParams(text_document=_doc(uri)))
@pytest.mark.asyncio
async def test_signature_help_request(client, test_data_dir):
await _initialize(client, test_data_dir)
uri, _ = await _open_and_wait(client, test_data_dir)
result = await client.text_document_signature_help_async(
SignatureHelpParams(
text_document=_doc(uri), position=Position(line=0, character=0)
)
)
client.text_document_did_close(DidCloseTextDocumentParams(text_document=_doc(uri)))
@pytest.mark.asyncio
async def test_definition_request(client, test_data_dir):
await _initialize(client, test_data_dir)
uri, _ = await _open_and_wait(client, test_data_dir)
result = await client.text_document_definition_async(
DefinitionParams(
text_document=_doc(uri), position=Position(line=0, character=4)
)
)
client.text_document_did_close(DidCloseTextDocumentParams(text_document=_doc(uri)))
@pytest.mark.asyncio
async def test_document_symbol_request(client, test_data_dir):
await _initialize(client, test_data_dir)
uri, _ = await _open_and_wait(client, test_data_dir)
result = await client.text_document_document_symbol_async(
DocumentSymbolParams(text_document=_doc(uri))
)
assert result is not None
client.text_document_did_close(DidCloseTextDocumentParams(text_document=_doc(uri)))
@pytest.mark.asyncio
async def test_folding_range_request(client, test_data_dir):
await _initialize(client, test_data_dir)
uri, _ = await _open_and_wait(client, test_data_dir)
result = await client.text_document_folding_range_async(
FoldingRangeParams(text_document=_doc(uri))
)
assert result is not None
client.text_document_did_close(DidCloseTextDocumentParams(text_document=_doc(uri)))
@pytest.mark.asyncio
async def test_semantic_tokens_request(client, test_data_dir):
await _initialize(client, test_data_dir)
uri, _ = await _open_and_wait(client, test_data_dir)
result = await client.text_document_semantic_tokens_full_async(
SemanticTokensParams(text_document=_doc(uri))
)
assert result is not None
client.text_document_did_close(DidCloseTextDocumentParams(text_document=_doc(uri)))
@pytest.mark.asyncio
async def test_inlay_hint_request(client, test_data_dir):
await _initialize(client, test_data_dir)
uri, _ = await _open_and_wait(client, test_data_dir)
result = await client.text_document_inlay_hint_async(
InlayHintParams(
text_document=_doc(uri),
range=Range(
start=Position(line=0, character=0), end=Position(line=10, character=0)
),
)
)
client.text_document_did_close(DidCloseTextDocumentParams(text_document=_doc(uri)))
@pytest.mark.asyncio
async def test_code_action_request(client, test_data_dir):
await _initialize(client, test_data_dir)
uri, _ = await _open_and_wait(client, test_data_dir)
result = await client.text_document_code_action_async(
CodeActionParams(
text_document=_doc(uri),
range=Range(
start=Position(line=0, character=0), end=Position(line=0, character=10)
),
context=CodeActionContext(diagnostics=[]),
)
)
client.text_document_did_close(DidCloseTextDocumentParams(text_document=_doc(uri)))
@pytest.mark.asyncio
async def test_document_link_request(client, test_data_dir):
await _initialize(client, test_data_dir)
uri, _ = await _open_and_wait(client, test_data_dir)
result = await client.text_document_document_link_async(
DocumentLinkParams(text_document=_doc(uri))
)
assert result is not None
client.text_document_did_close(DidCloseTextDocumentParams(text_document=_doc(uri)))
# ---------------------------------------------------------------------------
# Stress and edge cases
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_rapid_changes_stress(client, test_data_dir):
await _initialize(client, test_data_dir)
uri, content = await _open_file(client, test_data_dir)
for i in range(20):
content += f"\n// stress change {i}\n"
client.text_document_did_change(
DidChangeTextDocumentParams(
text_document=VersionedTextDocumentIdentifier(uri=uri, version=i + 1),
content_changes=[TextDocumentContentChangeWholeDocument(text=content)],
)
)
await asyncio.sleep(2)
client.text_document_did_close(DidCloseTextDocumentParams(text_document=_doc(uri)))
@pytest.mark.asyncio
async def test_save_notification(client, test_data_dir):
await _initialize(client, test_data_dir)
uri, _ = await _open_file(client, test_data_dir)
await asyncio.sleep(0.5)
client.text_document_did_save(DidSaveTextDocumentParams(text_document=_doc(uri)))
await asyncio.sleep(0.5)
client.text_document_did_close(DidCloseTextDocumentParams(text_document=_doc(uri)))
@pytest.mark.asyncio
async def test_hover_on_unknown_file(client, test_data_dir):
await _initialize(client, test_data_dir)
result = await client.text_document_hover_async(
HoverParams(
text_document=_doc("file:///nonexistent/fake.cpp"),
position=Position(line=0, character=0),
)
)
assert result is None
@pytest.mark.asyncio
async def test_all_features_after_compile_wait(client, test_data_dir):
"""After waiting for compilation, exercise all feature requests."""
await _initialize(client, test_data_dir)
uri, _ = await _open_and_wait(client, test_data_dir)
# Hover on 'add' (line 0, character 4)
hover = await client.text_document_hover_async(
HoverParams(text_document=_doc(uri), position=Position(line=0, character=4))
)
assert hover is not None
# Completion
completion = await client.text_document_completion_async(
CompletionParams(
text_document=_doc(uri), position=Position(line=5, character=18)
)
)
# Signature help
await client.text_document_signature_help_async(
SignatureHelpParams(
text_document=_doc(uri), position=Position(line=0, character=0)
)
)
# Definition on 'add'
await client.text_document_definition_async(
DefinitionParams(
text_document=_doc(uri), position=Position(line=0, character=4)
)
)
# Document symbols
symbols = await client.text_document_document_symbol_async(
DocumentSymbolParams(text_document=_doc(uri))
)
assert symbols is not None
# Folding ranges
folding = await client.text_document_folding_range_async(
FoldingRangeParams(text_document=_doc(uri))
)
assert folding is not None
# Semantic tokens
tokens = await client.text_document_semantic_tokens_full_async(
SemanticTokensParams(text_document=_doc(uri))
)
assert tokens is not None
# Document links
links = await client.text_document_document_link_async(
DocumentLinkParams(text_document=_doc(uri))
)
assert links is not None
# Code actions
await client.text_document_code_action_async(
CodeActionParams(
text_document=_doc(uri),
range=Range(
start=Position(line=0, character=0), end=Position(line=0, character=10)
),
context=CodeActionContext(diagnostics=[]),
)
)
# Inlay hints
await client.text_document_inlay_hint_async(
InlayHintParams(
text_document=_doc(uri),
range=Range(
start=Position(line=0, character=0), end=Position(line=10, character=0)
),
)
)
client.text_document_did_close(DidCloseTextDocumentParams(text_document=_doc(uri)))