## Summary Three pre-existing bugs cause worker processes to crash with SEGV or SIGABRT. On the main branch these crashes are silent (workers die, requests fail fast with "transport closed", tests still pass because null responses are accepted). However when combined with #432's worker respawn mechanism, the crash-respawn-crash cycle on low-core CI machines causes request timeouts and smoke test hangs. ### Fixes - **compilation.cpp**: `ProxyAction::CreateASTConsumer` now checks for null before passing to `MultiplexConsumer`. When the wrapped action's `CreateASTConsumer` fails (e.g. missing system headers during PCH generation), this previously caused a null pointer dereference, SEGV, ASAN kills the stateless worker. - **compilation_unit.cpp**: `file_path()` returns empty `StringRef` on invalid `FileID` instead of asserting. The assert fired when `IncludeGraph::from()` called `file_path(interested_file())` on an AST compiled with synthesized default commands (no compile_commands.json, clang++ -std=c++20 fallback, no system headers, invalid main file ID), SIGABRT, stateful worker crash. - **compiler.cpp**: `ensure_pch` now creates the PCH cache directory before sending the build request. Previously, when `load_workspace()` exited early (no compile_commands.json), the cache subdirectories were never created, causing every PCH write to fail with "No such file or directory". - **master_server.cpp/h**: `load_workspace()` changed from `kota::task<>` to plain `void` -- it contains only synchronous filesystem operations and no co_await, so the coroutine wrapper was unnecessary. Called directly instead of via `loop.schedule()`. ## Test plan - [x] Verified zero SEGV/SIGABRT/assertion crashes in worker stderr after fix - [x] rapid_edit.jsonl smoke test passes 3/3 runs consistently (34s each) - [x] Behavior matches main branch (both return 134 responses, 0 pending) - [x] Debug build with ASAN (detect_leaks=0) -- clean run, no sanitizer reports <!-- codesmith:footer --> --- <a href="https://app.blacksmith.sh/clice-io/codesmith/clice/pr/435"><picture><source media="(prefers-color-scheme: dark)" srcset="https://pr-comments-assets.blacksmith.sh/codesmith/view-in-codesmith-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://pr-comments-assets.blacksmith.sh/codesmith/view-in-codesmith-light.svg"><img alt="View in Codesmith" src="https://pr-comments-assets.blacksmith.sh/codesmith/view-in-codesmith-dark.svg"></picture></a> <sup>Codesmith can help with this PR — just tag <code>@codesmith</code> or enable autofix.</sup> - [ ] Autofix CI and bot reviews <!-- /codesmith:footer --> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Improved error handling for AST consumer creation with null checks and a clear failure path. * Safer file-path access that returns empty for invalid identifiers instead of asserting. * PCH cache handling now validates cache configuration, attempts directory creation, logs warnings, and aborts PCH builds on failure. * **Refactor** * Workspace loading changed from asynchronous to synchronous execution. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
286 lines
8.1 KiB
C++
286 lines
8.1 KiB
C++
#include <string>
|
|
#include <vector>
|
|
|
|
#include "test/test.h"
|
|
#include "server/protocol.h"
|
|
#include "server/worker_test_helpers.h"
|
|
|
|
#include "kota/codec/bincode/bincode.h"
|
|
|
|
namespace clice::testing {
|
|
|
|
namespace {
|
|
|
|
// ============================================================================
|
|
// Bincode Serialization Tests
|
|
// ============================================================================
|
|
|
|
TEST_SUITE(BincodeRoundTrip) {
|
|
|
|
TEST_CASE(CompileParamsRoundTrip) {
|
|
namespace bincode = kota::codec::bincode;
|
|
|
|
worker::CompileParams params;
|
|
params.path = "/tmp/test.cpp";
|
|
params.version = 1;
|
|
params.text = "int main() { return 0; }";
|
|
params.directory = "/tmp";
|
|
params.arguments = {"clang++", "-c", "test.cpp"};
|
|
params.pch = {"", 0};
|
|
params.pcms = {};
|
|
|
|
auto bytes = bincode::to_bytes(params);
|
|
ASSERT_TRUE(bytes.has_value());
|
|
|
|
worker::CompileParams result;
|
|
auto status =
|
|
bincode::from_bytes(std::span<const std::byte>(bytes->data(), bytes->size()), result);
|
|
ASSERT_TRUE(status.has_value());
|
|
|
|
EXPECT_EQ(result.path, params.path);
|
|
EXPECT_EQ(result.version, params.version);
|
|
EXPECT_EQ(result.text, params.text);
|
|
EXPECT_EQ(result.directory, params.directory);
|
|
EXPECT_EQ(result.arguments.size(), params.arguments.size());
|
|
}
|
|
|
|
TEST_CASE(CompileResultRoundTrip) {
|
|
namespace bincode = kota::codec::bincode;
|
|
|
|
worker::CompileResult result;
|
|
result.version = 1;
|
|
result.diagnostics = {}; // empty
|
|
result.memory_usage = 0;
|
|
|
|
auto bytes = bincode::to_bytes(result);
|
|
ASSERT_TRUE(bytes.has_value());
|
|
|
|
worker::CompileResult decoded;
|
|
auto status =
|
|
bincode::from_bytes(std::span<const std::byte>(bytes->data(), bytes->size()), decoded);
|
|
ASSERT_TRUE(status.has_value());
|
|
EXPECT_EQ(decoded.version, result.version);
|
|
}
|
|
|
|
}; // TEST_SUITE(BincodeRoundTrip)
|
|
|
|
// ============================================================================
|
|
// StatelessWorker Tests
|
|
// ============================================================================
|
|
|
|
TEST_SUITE(StatelessWorker) {
|
|
|
|
TEST_CASE(SpawnAndExit) {
|
|
WorkerHandle w;
|
|
ASSERT_TRUE(w.spawn("stateless-worker"));
|
|
|
|
// Close stdin pipe to signal worker to exit.
|
|
w.peer->close_output();
|
|
w.loop.schedule(w.peer->run());
|
|
w.loop.run();
|
|
}
|
|
|
|
TEST_CASE(BuildPCHRequest) {
|
|
TempDir tmp;
|
|
tmp.touch("test_pch.h", "#pragma once\nint pch_global = 42;\n");
|
|
auto hdr = tmp.path("test_pch.h");
|
|
|
|
WorkerHandle w;
|
|
ASSERT_TRUE(w.spawn("stateless-worker"));
|
|
|
|
bool test_done = false;
|
|
|
|
w.run([&]() -> kota::task<> {
|
|
worker::BuildParams params;
|
|
params.kind = worker::BuildKind::BuildPCH;
|
|
params.file = hdr;
|
|
params.directory = "/tmp";
|
|
params.arguments =
|
|
{"clang++", "-resource-dir", std::string(resource_dir()), "-x", "c++-header", hdr};
|
|
params.text = "#pragma once\nint pch_global = 42;\n";
|
|
params.output_path = tmp.path("test_pch.pch");
|
|
|
|
auto result = co_await w.peer->send_request(params);
|
|
EXPECT_TRUE(result.has_value());
|
|
if(!result.has_value()) {
|
|
w.peer->close_output();
|
|
co_return;
|
|
}
|
|
EXPECT_TRUE(result.value().success);
|
|
EXPECT_FALSE(result.value().output_path.empty());
|
|
test_done = true;
|
|
w.peer->close_output();
|
|
});
|
|
|
|
ASSERT_TRUE(test_done);
|
|
}
|
|
|
|
TEST_CASE(IndexRequest) {
|
|
TempDir tmp;
|
|
tmp.touch("test_index.cpp", "int indexed_var = 1;\n");
|
|
auto src = tmp.path("test_index.cpp");
|
|
|
|
WorkerHandle w;
|
|
ASSERT_TRUE(w.spawn("stateless-worker"));
|
|
|
|
bool test_done = false;
|
|
|
|
w.run([&]() -> kota::task<> {
|
|
worker::BuildParams params;
|
|
params.kind = worker::BuildKind::Index;
|
|
params.file = src;
|
|
params.directory = "/tmp";
|
|
params.arguments = make_args(src);
|
|
|
|
auto result = co_await w.peer->send_request(params);
|
|
EXPECT_TRUE(result.has_value());
|
|
test_done = true;
|
|
w.peer->close_output();
|
|
});
|
|
|
|
ASSERT_TRUE(test_done);
|
|
}
|
|
|
|
}; // TEST_SUITE(StatelessWorker)
|
|
|
|
// ============================================================================
|
|
// StatelessWorker Extended Tests
|
|
// ============================================================================
|
|
|
|
TEST_SUITE(StatelessWorkerExtended) {
|
|
|
|
TEST_CASE(BuildPCMRequest) {
|
|
TempDir tmp;
|
|
tmp.touch("test_module.cppm",
|
|
"export module test_module;\nexport int module_func() { return 1; }\n");
|
|
auto src = tmp.path("test_module.cppm");
|
|
|
|
WorkerHandle w;
|
|
ASSERT_TRUE(w.spawn("stateless-worker"));
|
|
|
|
bool test_done = false;
|
|
|
|
w.run([&]() -> kota::task<> {
|
|
worker::BuildParams params;
|
|
params.kind = worker::BuildKind::BuildPCM;
|
|
params.file = src;
|
|
params.directory = "/tmp";
|
|
params.arguments = {"clang++",
|
|
"-resource-dir",
|
|
std::string(resource_dir()),
|
|
"-std=c++20",
|
|
"--precompile",
|
|
src};
|
|
params.module_name = "test_module";
|
|
params.output_path = tmp.path("test_module.pcm");
|
|
|
|
auto result = co_await w.peer->send_request(params);
|
|
EXPECT_TRUE(result.has_value());
|
|
test_done = true;
|
|
w.peer->close_output();
|
|
});
|
|
|
|
ASSERT_TRUE(test_done);
|
|
}
|
|
|
|
TEST_CASE(CompletionRequest) {
|
|
std::string text = "int foo = 1;\nint bar = fo";
|
|
TempDir tmp;
|
|
tmp.touch("completion_test.cpp", text);
|
|
auto src = tmp.path("completion_test.cpp");
|
|
|
|
WorkerHandle w;
|
|
ASSERT_TRUE(w.spawn("stateless-worker"));
|
|
|
|
bool test_done = false;
|
|
|
|
w.run([&]() -> kota::task<> {
|
|
worker::BuildParams params;
|
|
params.kind = worker::BuildKind::Completion;
|
|
params.file = src;
|
|
params.version = 1;
|
|
params.text = text;
|
|
params.directory = "/tmp";
|
|
params.arguments = make_args(src);
|
|
params.offset = 25; // after "fo" in "int bar = fo" (13 + 12)
|
|
|
|
auto result = co_await w.peer->send_request(params);
|
|
EXPECT_TRUE(result.has_value());
|
|
test_done = true;
|
|
w.peer->close_output();
|
|
});
|
|
|
|
ASSERT_TRUE(test_done);
|
|
}
|
|
|
|
TEST_CASE(SignatureHelpRequest) {
|
|
std::string text = "void foo(int a, int b) {}\nint main() { foo(";
|
|
TempDir tmp;
|
|
tmp.touch("sighelp_test.cpp", text);
|
|
auto src = tmp.path("sighelp_test.cpp");
|
|
|
|
WorkerHandle w;
|
|
ASSERT_TRUE(w.spawn("stateless-worker"));
|
|
|
|
bool test_done = false;
|
|
|
|
w.run([&]() -> kota::task<> {
|
|
worker::BuildParams params;
|
|
params.kind = worker::BuildKind::SignatureHelp;
|
|
params.file = src;
|
|
params.version = 1;
|
|
params.text = text;
|
|
params.directory = "/tmp";
|
|
params.arguments = make_args(src);
|
|
params.offset = 45; // after "foo(" (26 + 19)
|
|
|
|
auto result = co_await w.peer->send_request(params);
|
|
EXPECT_TRUE(result.has_value());
|
|
// Should return signature help for foo(int a, int b).
|
|
test_done = true;
|
|
w.peer->close_output();
|
|
});
|
|
|
|
ASSERT_TRUE(test_done);
|
|
}
|
|
|
|
TEST_CASE(MultipleStatelessRequests) {
|
|
TempDir tmp;
|
|
std::vector<std::string> paths;
|
|
for(int i = 0; i < 3; i++) {
|
|
auto name = "multi_index_" + std::to_string(i) + ".cpp";
|
|
auto text = "int idx_var_" + std::to_string(i) + " = " + std::to_string(i) + ";\n";
|
|
tmp.touch(name, text);
|
|
paths.push_back(tmp.path(name));
|
|
}
|
|
|
|
WorkerHandle w;
|
|
ASSERT_TRUE(w.spawn("stateless-worker"));
|
|
|
|
bool test_done = false;
|
|
|
|
w.run([&]() -> kota::task<> {
|
|
// Send multiple index requests to test stateless worker handles them sequentially.
|
|
for(int i = 0; i < 3; i++) {
|
|
worker::BuildParams params;
|
|
params.kind = worker::BuildKind::Index;
|
|
params.file = paths[i];
|
|
params.directory = "/tmp";
|
|
params.arguments = make_args(paths[i]);
|
|
|
|
auto result = co_await w.peer->send_request(params);
|
|
EXPECT_TRUE(result.has_value());
|
|
}
|
|
test_done = true;
|
|
w.peer->close_output();
|
|
});
|
|
|
|
ASSERT_TRUE(test_done);
|
|
}
|
|
|
|
}; // TEST_SUITE(StatelessWorkerExtended)
|
|
|
|
} // namespace
|
|
|
|
} // namespace clice::testing
|