Files
clice/tests/integration/test_pch.py
ykiko e24eff6c16 refactor: pull-based compilation for document lifecycle (#385)
## 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>
2026-04-04 02:35:17 +08:00

106 lines
4.2 KiB
Python

"""Integration tests for PCH (precompiled header) functionality in MasterServer."""
import asyncio
import pytest
from lsprotocol.types import (
CompletionParams,
DidChangeTextDocumentParams,
DidCloseTextDocumentParams,
HoverParams,
Position,
TextDocumentContentChangeWholeDocument,
TextDocumentIdentifier,
VersionedTextDocumentIdentifier,
)
def _doc(uri: str) -> TextDocumentIdentifier:
return TextDocumentIdentifier(uri=uri)
@pytest.mark.workspace("pch_test")
async def test_pch_diagnostics_on_open(client, workspace):
"""Opening a file with #include should trigger PCH build and return clean diagnostics."""
uri, _ = await client.open_and_wait(workspace / "main.cpp")
assert uri in client.diagnostics
# main.cpp is well-formed, so diagnostics list should be empty (no errors).
diags = client.diagnostics[uri]
assert len(diags) == 0, f"Expected no diagnostics, got: {diags}"
client.text_document_did_close(DidCloseTextDocumentParams(text_document=_doc(uri)))
@pytest.mark.workspace("pch_test")
async def test_pch_body_edit_triggers_recompile(client, workspace):
"""Editing only the body (not the preamble) should trigger recompilation."""
uri, content = await client.open_and_wait(workspace / "main.cpp")
# Edit only the function body — preamble (#include "common.h") unchanged.
new_content = content.replace("return result;", "return result + 1;")
client.text_document_did_change(
DidChangeTextDocumentParams(
text_document=VersionedTextDocumentIdentifier(uri=uri, version=1),
content_changes=[TextDocumentContentChangeWholeDocument(text=new_content)],
)
)
# Send hover to trigger recompilation via pull-based model.
event = client.wait_for_diagnostics(uri)
await client.text_document_hover_async(
HoverParams(text_document=_doc(uri), position=Position(line=0, character=0))
)
await asyncio.wait_for(event.wait(), timeout=30.0)
assert uri in client.diagnostics
client.text_document_did_close(DidCloseTextDocumentParams(text_document=_doc(uri)))
@pytest.mark.workspace("pch_test")
async def test_no_pch_for_no_includes(client, workspace):
"""A file with no #include directives should compile without PCH."""
uri, _ = await client.open_and_wait(workspace / "no_includes.cpp")
assert uri in client.diagnostics
diags = client.diagnostics[uri]
assert len(diags) == 0, f"Expected no diagnostics, got: {diags}"
client.text_document_did_close(DidCloseTextDocumentParams(text_document=_doc(uri)))
@pytest.mark.workspace("pch_test")
async def test_hover_on_local_symbol(client, workspace):
"""Hover on a locally defined symbol should work when PCH is active."""
uri, _ = await client.open_and_wait(workspace / "main.cpp")
# Hover over "add" on line 2 (0-indexed): "int add(int a, int b) {"
result = await client.text_document_hover_async(
HoverParams(text_document=_doc(uri), position=Position(line=2, character=4))
)
assert result is not None
client.text_document_did_close(DidCloseTextDocumentParams(text_document=_doc(uri)))
@pytest.mark.workspace("pch_test")
async def test_completion_with_pch(client, workspace):
"""Completion should see symbols from PCH headers."""
uri, content = await client.open_and_wait(workspace / "main.cpp")
# Add a line that starts typing "Poi" to trigger completion for Point.
new_content = content + "\nPoi"
lines = new_content.split("\n")
last_line = len(lines) - 1
client.text_document_did_change(
DidChangeTextDocumentParams(
text_document=VersionedTextDocumentIdentifier(uri=uri, version=1),
content_changes=[TextDocumentContentChangeWholeDocument(text=new_content)],
)
)
# The completion request itself triggers compilation via ensure_compiled().
result = await client.text_document_completion_async(
CompletionParams(
text_document=_doc(uri),
position=Position(line=last_line, character=3),
)
)
# Completion should return results.
assert result is not None
client.text_document_did_close(DidCloseTextDocumentParams(text_document=_doc(uri)))