## Summary Replace the push-based compilation model with a pull-based (lazy) model where compilation is driven entirely by feature requests. ### Server core (`master_server.cpp/h`) - **Remove** `schedule_build()`, `run_build_drain()`, debounce timers, and `DocumentState` flags (`build_running`, `build_requested`, `drain_scheduled`) - **Remove** `debounce_ms` config field - `didOpen`/`didChange` only update `DocumentState` and mark `ast_dirty` — no compilation triggered - `didSave` marks dependent docs dirty via `CompileGraph::update()`, invalidates PCH hashes, marks **all** open documents `ast_dirty` (header saves), and queues background indexing - **Implement** `ensure_compiled(path_id)` — the pull-based entry point called by `forward_stateful()`/`forward_stateless()` before every feature request: 1. Fast-path if `!ast_dirty` 2. Compile C++20 module deps via `compile_graph->compile_deps()` 3. Build/reuse PCH via `ensure_pch()` (only attach on success) 4. Send `CompileParams` to stateful worker 5. Publish diagnostics, clear dirty, schedule indexing 6. Generation mismatch → return `false`, keep dirty for retry - `forward_stateless()` now also calls `compile_graph->compile_deps()` before stateless requests (completion/signatureHelp) - Move module-implementation-unit implicit dependency handling into `resolve_fn` (was duplicated in `run_build_drain` and `ensure_compiled`) ### CompileGraph (`compile_graph.cpp/h`) - **Add** `compile_deps(path_id)` — compiles all transitive module dependencies but NOT the file itself (used for plain .cpp files that `import` modules) - Unify `compile`/`compile_deps` via `compile_impl(path_id, ancestors, dispatch_self)` parameter - `compile_deps` compiles dependencies concurrently via `when_all` - Extract `finish()` lambda to deduplicate `compiling=false; completion->set()` cleanup across all exit paths - Use `std::ranges::remove` instead of legacy `std::remove` ### Test infrastructure (`conftest.py`) - `open_and_wait()` now sends a hover request to trigger `ensure_compiled()` (pull-based model requires a feature request to compile) - Fix URI handling: send percent-encoded URI on the wire, normalize for internal lookups, store diagnostics under both raw and normalized URI keys - Add `_normalize_uri()` helper using `urllib.parse.unquote` ### Integration tests - Update all tests for pull-based model: no more waiting on `didOpen` diagnostics - `_wait_for_index()` sends hover to trigger compilation before polling `workspace/symbol` - `test_hover_save_close` simplified — hover directly triggers compilation - `test_save_recompile` and `test_pch_*` wait for fresh diagnostics after hover-triggered recompilation ### Unit tests (`compile_graph_tests.cpp`) - Extract `compiled`/`graph` as TEST_SUITE members with `std::optional<CompileGraph>` - Extract `execute(callback)` helper to deduplicate event_loop boilerplate - Add 8 new `compile_deps` tests: no-deps, single dep, chain, diamond, failure, plain-cpp, concurrent dedup, resolve-once - Remove redundant `inline` on file-scope helpers ## Test plan - [x] Unit tests: 426 passed, 5 skipped - [x] Smoke tests: 1/1 passed - [x] Integration tests: 69 passed, 0 failed, no hangs 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
294 lines
11 KiB
Python
294 lines
11 KiB
Python
"""Integration tests for index-based LSP features: GoToDefinition, FindReferences,
|
|
CallHierarchy, TypeHierarchy, and WorkspaceSymbol."""
|
|
|
|
import asyncio
|
|
|
|
import pytest
|
|
from lsprotocol.types import (
|
|
CallHierarchyIncomingCallsParams,
|
|
CallHierarchyOutgoingCallsParams,
|
|
CallHierarchyPrepareParams,
|
|
DefinitionParams,
|
|
DidCloseTextDocumentParams,
|
|
Position,
|
|
ReferenceContext,
|
|
ReferenceParams,
|
|
TextDocumentIdentifier,
|
|
TypeHierarchyPrepareParams,
|
|
TypeHierarchySubtypesParams,
|
|
TypeHierarchySupertypesParams,
|
|
WorkspaceSymbolParams,
|
|
)
|
|
|
|
|
|
def _doc(uri: str) -> TextDocumentIdentifier:
|
|
return TextDocumentIdentifier(uri=uri)
|
|
|
|
|
|
async def _wait_for_index(client, uri, timeout=30):
|
|
"""Trigger compilation via a hover request, then poll workspace/symbol until
|
|
indexing is ready (symbols appear)."""
|
|
from lsprotocol.types import HoverParams
|
|
|
|
# Send a hover request to trigger ensure_compiled() → compilation → indexing
|
|
await client.text_document_hover_async(
|
|
HoverParams(
|
|
text_document=_doc(uri),
|
|
position=Position(line=0, character=0),
|
|
)
|
|
)
|
|
|
|
for _ in range(timeout):
|
|
result = await client.workspace_symbol_async(WorkspaceSymbolParams(query="add"))
|
|
if result and any(s.name == "add" for s in result):
|
|
return True
|
|
await asyncio.sleep(1)
|
|
return False
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# GoToDefinition
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.workspace("index_features")
|
|
async def test_goto_definition(client, workspace):
|
|
"""Test GoToDefinition navigates from a call site to the function definition."""
|
|
uri, _ = await client.open_and_wait(workspace / "main.cpp")
|
|
assert await _wait_for_index(client, uri), "Index not ready after 30s"
|
|
|
|
# 'add' call on line 24 (0-indexed), column 12
|
|
result = await client.text_document_definition_async(
|
|
DefinitionParams(
|
|
text_document=_doc(uri),
|
|
position=Position(line=24, character=12),
|
|
)
|
|
)
|
|
assert result is not None
|
|
locs = result if isinstance(result, list) else [result]
|
|
assert len(locs) > 0, f"GoToDefinition returned empty list, result={result}"
|
|
# Definition should point to line 18 where 'int add(...)' is declared
|
|
assert any(loc.range.start.line == 18 for loc in locs), (
|
|
f"Expected line 18, got locations:"
|
|
f" {[(loc.uri, loc.range.start.line, loc.range.start.character) for loc in locs]}"
|
|
)
|
|
|
|
client.text_document_did_close(DidCloseTextDocumentParams(text_document=_doc(uri)))
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# FindReferences
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.workspace("index_features")
|
|
async def test_find_references(client, workspace):
|
|
"""Test FindReferences returns all usages of global_var."""
|
|
uri, _ = await client.open_and_wait(workspace / "main.cpp")
|
|
assert await _wait_for_index(client, uri), "Index not ready after 30s"
|
|
|
|
# global_var definition on line 30 (0-indexed), column 4
|
|
result = await client.text_document_references_async(
|
|
ReferenceParams(
|
|
text_document=_doc(uri),
|
|
position=Position(line=30, character=4),
|
|
context=ReferenceContext(include_declaration=True),
|
|
)
|
|
)
|
|
assert result is not None, "FindReferences returned None"
|
|
# global_var is declared on line 30 and used on lines 33 and 37
|
|
assert len(result) >= 3, (
|
|
f"Expected >=3 refs, got {len(result)}:"
|
|
f" {[(r.uri, r.range.start.line) for r in result]}"
|
|
)
|
|
|
|
client.text_document_did_close(DidCloseTextDocumentParams(text_document=_doc(uri)))
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# CallHierarchy
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.workspace("index_features")
|
|
async def test_call_hierarchy_prepare(client, workspace):
|
|
"""Test prepareCallHierarchy returns a CallHierarchyItem for 'add'."""
|
|
uri, _ = await client.open_and_wait(workspace / "main.cpp")
|
|
assert await _wait_for_index(client, uri), "Index not ready after 30s"
|
|
|
|
# 'add' definition at line 18 (0-indexed), column 4
|
|
result = await client.text_document_prepare_call_hierarchy_async(
|
|
CallHierarchyPrepareParams(
|
|
text_document=_doc(uri),
|
|
position=Position(line=18, character=4),
|
|
)
|
|
)
|
|
assert result is not None, "prepareCallHierarchy returned None"
|
|
assert len(result) > 0, f"prepareCallHierarchy returned empty, result={result}"
|
|
assert result[0].name == "add", f"Expected 'add', got '{result[0].name}'"
|
|
|
|
client.text_document_did_close(DidCloseTextDocumentParams(text_document=_doc(uri)))
|
|
|
|
|
|
@pytest.mark.workspace("index_features")
|
|
async def test_call_hierarchy_incoming(client, workspace):
|
|
"""Test incomingCalls shows compute() calls add()."""
|
|
uri, _ = await client.open_and_wait(workspace / "main.cpp")
|
|
assert await _wait_for_index(client, uri), "Index not ready after 30s"
|
|
|
|
# Prepare call hierarchy for 'add' at line 18 (0-indexed), column 4
|
|
items = await client.text_document_prepare_call_hierarchy_async(
|
|
CallHierarchyPrepareParams(
|
|
text_document=_doc(uri),
|
|
position=Position(line=18, character=4),
|
|
)
|
|
)
|
|
assert items and len(items) > 0, f"prepareCallHierarchy returned {items}"
|
|
|
|
incoming = await client.call_hierarchy_incoming_calls_async(
|
|
CallHierarchyIncomingCallsParams(item=items[0])
|
|
)
|
|
assert incoming is not None, "incomingCalls returned None"
|
|
caller_names = [call.from_.name for call in incoming]
|
|
assert "compute" in caller_names, (
|
|
f"Expected 'compute' in callers, got {caller_names}"
|
|
)
|
|
|
|
client.text_document_did_close(DidCloseTextDocumentParams(text_document=_doc(uri)))
|
|
|
|
|
|
@pytest.mark.workspace("index_features")
|
|
async def test_call_hierarchy_outgoing(client, workspace):
|
|
"""Test outgoingCalls shows compute() calls add()."""
|
|
uri, _ = await client.open_and_wait(workspace / "main.cpp")
|
|
assert await _wait_for_index(client, uri), "Index not ready after 30s"
|
|
|
|
# Prepare call hierarchy for 'compute' at line 23 (0-indexed), column 4
|
|
items = await client.text_document_prepare_call_hierarchy_async(
|
|
CallHierarchyPrepareParams(
|
|
text_document=_doc(uri),
|
|
position=Position(line=23, character=4),
|
|
)
|
|
)
|
|
assert items and len(items) > 0, f"prepareCallHierarchy returned {items}"
|
|
|
|
outgoing = await client.call_hierarchy_outgoing_calls_async(
|
|
CallHierarchyOutgoingCallsParams(item=items[0])
|
|
)
|
|
assert outgoing is not None, "outgoingCalls returned None"
|
|
callee_names = [call.to.name for call in outgoing]
|
|
assert "add" in callee_names, f"Expected 'add' in callees, got {callee_names}"
|
|
|
|
client.text_document_did_close(DidCloseTextDocumentParams(text_document=_doc(uri)))
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# TypeHierarchy
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.workspace("index_features")
|
|
async def test_type_hierarchy_prepare(client, workspace):
|
|
"""Test prepareTypeHierarchy returns a TypeHierarchyItem for 'Dog'."""
|
|
uri, _ = await client.open_and_wait(workspace / "main.cpp")
|
|
assert await _wait_for_index(client, uri), "Index not ready after 30s"
|
|
|
|
# 'Dog' at line 8 (0-indexed), column 7
|
|
result = await client.text_document_prepare_type_hierarchy_async(
|
|
TypeHierarchyPrepareParams(
|
|
text_document=_doc(uri),
|
|
position=Position(line=8, character=7),
|
|
)
|
|
)
|
|
assert result is not None, "prepareTypeHierarchy returned None"
|
|
assert len(result) > 0, f"prepareTypeHierarchy returned empty"
|
|
assert result[0].name == "Dog", f"Expected 'Dog', got '{result[0].name}'"
|
|
|
|
client.text_document_did_close(DidCloseTextDocumentParams(text_document=_doc(uri)))
|
|
|
|
|
|
@pytest.mark.workspace("index_features")
|
|
async def test_type_hierarchy_supertypes(client, workspace):
|
|
"""Test supertypes of Dog includes Animal."""
|
|
uri, _ = await client.open_and_wait(workspace / "main.cpp")
|
|
assert await _wait_for_index(client, uri), "Index not ready after 30s"
|
|
|
|
# 'Dog' at line 8 (0-indexed), column 7
|
|
items = await client.text_document_prepare_type_hierarchy_async(
|
|
TypeHierarchyPrepareParams(
|
|
text_document=_doc(uri),
|
|
position=Position(line=8, character=7),
|
|
)
|
|
)
|
|
assert items and len(items) > 0, f"prepareTypeHierarchy returned {items}"
|
|
|
|
supertypes = await client.type_hierarchy_supertypes_async(
|
|
TypeHierarchySupertypesParams(item=items[0])
|
|
)
|
|
assert supertypes is not None, "supertypes returned None"
|
|
supertype_names = [t.name for t in supertypes]
|
|
assert "Animal" in supertype_names, (
|
|
f"Expected 'Animal' in supertypes, got {supertype_names}"
|
|
)
|
|
|
|
client.text_document_did_close(DidCloseTextDocumentParams(text_document=_doc(uri)))
|
|
|
|
|
|
@pytest.mark.workspace("index_features")
|
|
async def test_type_hierarchy_subtypes(client, workspace):
|
|
"""Test subtypes of Animal includes Dog and Cat."""
|
|
uri, _ = await client.open_and_wait(workspace / "main.cpp")
|
|
assert await _wait_for_index(client, uri), "Index not ready after 30s"
|
|
|
|
# 'Animal' at line 1, column 7
|
|
items = await client.text_document_prepare_type_hierarchy_async(
|
|
TypeHierarchyPrepareParams(
|
|
text_document=_doc(uri),
|
|
position=Position(line=1, character=7),
|
|
)
|
|
)
|
|
assert items and len(items) > 0, f"prepareTypeHierarchy returned {items}"
|
|
|
|
subtypes = await client.type_hierarchy_subtypes_async(
|
|
TypeHierarchySubtypesParams(item=items[0])
|
|
)
|
|
assert subtypes is not None, "subtypes returned None"
|
|
subtype_names = [t.name for t in subtypes]
|
|
assert "Dog" in subtype_names, f"Expected 'Dog' in subtypes, got {subtype_names}"
|
|
assert "Cat" in subtype_names, f"Expected 'Cat' in subtypes, got {subtype_names}"
|
|
|
|
client.text_document_did_close(DidCloseTextDocumentParams(text_document=_doc(uri)))
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# WorkspaceSymbol
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.workspace("index_features")
|
|
async def test_workspace_symbol(client, workspace):
|
|
"""Test workspace/symbol finds symbols by query string."""
|
|
uri, _ = await client.open_and_wait(workspace / "main.cpp")
|
|
assert await _wait_for_index(client, uri), "Index not ready after 30s"
|
|
|
|
result = await client.workspace_symbol_async(WorkspaceSymbolParams(query="add"))
|
|
assert result is not None
|
|
names = [s.name for s in result]
|
|
assert "add" in names
|
|
|
|
client.text_document_did_close(DidCloseTextDocumentParams(text_document=_doc(uri)))
|
|
|
|
|
|
@pytest.mark.workspace("index_features")
|
|
async def test_workspace_symbol_class(client, workspace):
|
|
"""Test workspace/symbol finds class symbols."""
|
|
uri, _ = await client.open_and_wait(workspace / "main.cpp")
|
|
assert await _wait_for_index(client, uri), "Index not ready after 30s"
|
|
|
|
result = await client.workspace_symbol_async(WorkspaceSymbolParams(query="Animal"))
|
|
assert result is not None
|
|
names = [s.name for s in result]
|
|
assert "Animal" in names
|
|
|
|
client.text_document_did_close(DidCloseTextDocumentParams(text_document=_doc(uri)))
|