455 lines
15 KiB
Python
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)))
|