Files
clice/tests/unit/server/module_worker_tests.cpp
ykiko 6d3b6acc82 feat: initial CompileGraph integration into MasterServer (#376)
## Summary

Initial integration of `CompileGraph` (#375) into `MasterServer`,
enabling basic end-to-end C++20 module support: on-demand PCM building,
dependency-ordered compilation, cascade invalidation on save, and
diagnostic integration.

This is a **first-pass implementation** — the core pipeline works, but
there are known areas for follow-up:

- PCM files go to system temp dir instead of `.clice/cache/`; no disk
cleanup on invalidation
- `run_build_drain` scans imports itself rather than delegating fully to
CompileGraph
- No incremental/partial rebuild (full PCM rebuild on any change)
- Cycle detection is tested at unit level but integration-level coverage
is minimal

## Changes

### Module dependency compilation (`master_server.cpp`)

Before sending a file to the stateful worker, `run_build_drain` now:

1. Scans imports via `scan_precise()` to discover module dependencies
2. Compiles each dep through `compile_graph->compile()`, which
recursively builds transitive PCMs
3. Handles implementation units — `module M;` implicitly needs the
interface PCM
4. Passes all built PCMs to the stateful worker, excluding the file's
own PCM
5. Skips compile on dep failure and resets `build_running` /
`drain_scheduled`
6. Re-lookups iterators after `co_await` to avoid use-after-invalidation

### Cascade invalidation (`didSave` / `didClose`)

- `didSave`: calls `compile_graph->update()` to mark transitive
dependents dirty, removes stale PCM paths, schedules rebuilds for open
dirtied files
- `didClose`: cancels in-flight compilations for the closed file

### Other fixes in this PR

- Debounce timers switched to `shared_ptr` to prevent use-after-free
when `didClose` destroys the timer mid-wait
- `fill_compile_args` returns `bool`; callers handle empty CDB
gracefully
- Adapt all `PositionMapper` call sites to the new `optional` return API
from eventide

## Test plan

- [x] 25 C++ unit tests for CompileGraph (cycles, partial failure,
cancel, update, empty graph)
- [x] 24 C++ integration tests with real clang PCM compilation
- [x] 3 worker-level module tests (BuildPCM, PCM-dependent compile,
multi-module)
- [x] 26 Python LSP integration tests (single module through circular
deps, hover, error diagnostics)
- [x] 371 unit tests + 54 integration tests pass

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-29 20:05:58 +08:00

276 lines
8.7 KiB
C++

#include <string>
#include <vector>
#include "test/test.h"
#include "server/protocol.h"
#include "server/worker_test_helpers.h"
namespace clice::testing {
namespace {
namespace et = eventide;
// ============================================================================
// End-to-end module compilation through real workers:
// 1. Stateless worker builds PCM for module interface
// 2. Stateful worker compiles a file that imports the module using the PCM
// This tests the same pipeline as MasterServer.run_build_drain().
// ============================================================================
TEST_SUITE(ModuleWorker) {
TEST_CASE(BuildPCMThenCompileWithImport) {
// Module interface: produces PCM.
TempFile iface(
"mod_iface.cppm",
"export module Hello;\n" R"(export const char* hello() { return "world"; })" "\n");
// Consumer: imports the module.
TempFile consumer("consumer.cpp", "import Hello;\n" "int main() { return hello()[0]; }\n");
// --- Phase 1: Build PCM via stateless worker ---
WorkerHandle sl;
ASSERT_TRUE(sl.spawn("stateless-worker"));
std::string pcm_path;
bool phase1_done = false;
sl.run([&]() -> et::task<> {
worker::BuildPCMParams params;
params.file = iface.path;
params.directory = "/tmp";
params.arguments = {"clang++",
"-resource-dir",
std::string(resource_dir()),
"-std=c++20",
"--precompile",
iface.path};
params.module_name = "Hello";
auto result = co_await sl.peer->send_request(params);
CO_ASSERT_TRUE(result.has_value());
CO_ASSERT_TRUE(result.value().success);
pcm_path = result.value().pcm_path;
EXPECT_FALSE(pcm_path.empty());
phase1_done = true;
sl.peer->close_output();
});
ASSERT_TRUE(phase1_done);
ASSERT_FALSE(pcm_path.empty());
// --- Phase 2: Compile consumer with the PCM via stateful worker ---
WorkerHandle sf;
ASSERT_TRUE(sf.spawn("stateful-worker"));
bool phase2_done = false;
sf.run([&]() -> et::task<> {
worker::CompileParams params;
params.path = consumer.path;
params.version = 1;
params.text = "import Hello;\n" "int main() { return hello()[0]; }\n";
params.directory = "/tmp";
params.arguments = {"clang++",
"-resource-dir",
std::string(resource_dir()),
"-std=c++20",
"-fsyntax-only",
consumer.path};
// Pass the PCM — same as MasterServer fills CompileParams.pcms.
params.pcms = {
{"Hello", pcm_path}
};
auto result = co_await sf.peer->send_request(params);
CO_ASSERT_TRUE(result.has_value());
EXPECT_EQ(result.value().version, 1);
phase2_done = true;
sf.peer->close_output();
});
ASSERT_TRUE(phase2_done);
// Cleanup PCM temp file.
std::remove(pcm_path.c_str());
}
TEST_CASE(BuildPCMChainThenCompile) {
// Module A: no deps.
TempFile mod_a("chain_a.cppm", "export module A;\n" "export int val_a() { return 1; }\n");
// Module B: imports A.
TempFile mod_b("chain_b.cppm",
"export module B;\n"
"import A;\n"
"export int val_b() { return val_a() + 1; }\n");
// Consumer: imports B (transitively needs A).
TempFile consumer("chain_consumer.cpp", "import B;\n" "int main() { return val_b(); }\n");
WorkerHandle sl;
ASSERT_TRUE(sl.spawn("stateless-worker"));
std::string pcm_a, pcm_b;
bool pcm_done = false;
sl.run([&]() -> et::task<> {
// Build PCM for A first.
{
worker::BuildPCMParams params;
params.file = mod_a.path;
params.directory = "/tmp";
params.arguments = {"clang++",
"-resource-dir",
std::string(resource_dir()),
"-std=c++20",
"--precompile",
mod_a.path};
params.module_name = "A";
auto result = co_await sl.peer->send_request(params);
CO_ASSERT_TRUE(result.has_value() && result.value().success);
pcm_a = result.value().pcm_path;
}
// Build PCM for B, passing A's PCM (transitive dep).
{
worker::BuildPCMParams params;
params.file = mod_b.path;
params.directory = "/tmp";
params.arguments = {"clang++",
"-resource-dir",
std::string(resource_dir()),
"-std=c++20",
"--precompile",
mod_b.path};
params.module_name = "B";
params.pcms = {
{"A", pcm_a}
};
auto result = co_await sl.peer->send_request(params);
CO_ASSERT_TRUE(result.has_value() && result.value().success);
pcm_b = result.value().pcm_path;
}
pcm_done = true;
sl.peer->close_output();
});
ASSERT_TRUE(pcm_done);
// Compile consumer with BOTH PCMs via stateful worker.
WorkerHandle sf;
ASSERT_TRUE(sf.spawn("stateful-worker"));
bool compile_done = false;
sf.run([&]() -> et::task<> {
worker::CompileParams params;
params.path = consumer.path;
params.version = 1;
params.text = "import B;\n" "int main() { return val_b(); }\n";
params.directory = "/tmp";
params.arguments = {"clang++",
"-resource-dir",
std::string(resource_dir()),
"-std=c++20",
"-fsyntax-only",
consumer.path};
// Clang needs ALL transitive PCMs.
params.pcms = {
{"A", pcm_a},
{"B", pcm_b}
};
auto result = co_await sf.peer->send_request(params);
CO_ASSERT_TRUE(result.has_value());
EXPECT_EQ(result.value().version, 1);
compile_done = true;
sf.peer->close_output();
});
ASSERT_TRUE(compile_done);
std::remove(pcm_a.c_str());
std::remove(pcm_b.c_str());
}
TEST_CASE(ModuleImplementationUnitWithWorker) {
// Module interface.
TempFile iface("impl_iface.cppm", "export module Calc;\n" "export int add(int a, int b);\n");
// Module implementation unit (no export).
TempFile impl("impl_unit.cpp", "module Calc;\n" "int add(int a, int b) { return a + b; }\n");
// Build PCM for interface.
WorkerHandle sl;
ASSERT_TRUE(sl.spawn("stateless-worker"));
std::string pcm_path;
bool pcm_done = false;
sl.run([&]() -> et::task<> {
worker::BuildPCMParams params;
params.file = iface.path;
params.directory = "/tmp";
params.arguments = {"clang++",
"-resource-dir",
std::string(resource_dir()),
"-std=c++20",
"--precompile",
iface.path};
params.module_name = "Calc";
auto result = co_await sl.peer->send_request(params);
CO_ASSERT_TRUE(result.has_value() && result.value().success);
pcm_path = result.value().pcm_path;
pcm_done = true;
sl.peer->close_output();
});
ASSERT_TRUE(pcm_done);
// Compile implementation unit with the PCM via stateful worker.
WorkerHandle sf;
ASSERT_TRUE(sf.spawn("stateful-worker"));
bool compile_done = false;
sf.run([&]() -> et::task<> {
worker::CompileParams params;
params.path = impl.path;
params.version = 1;
params.text = "module Calc;\n" "int add(int a, int b) { return a + b; }\n";
params.directory = "/tmp";
params.arguments = {"clang++",
"-resource-dir",
std::string(resource_dir()),
"-std=c++20",
"-fsyntax-only",
impl.path};
params.pcms = {
{"Calc", pcm_path}
};
auto result = co_await sf.peer->send_request(params);
CO_ASSERT_TRUE(result.has_value());
EXPECT_EQ(result.value().version, 1);
compile_done = true;
sf.peer->close_output();
});
ASSERT_TRUE(compile_done);
std::remove(pcm_path.c_str());
}
}; // TEST_SUITE(ModuleWorker)
} // namespace
} // namespace clice::testing