## 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 -->
509 lines
14 KiB
C++
509 lines
14 KiB
C++
#include <string>
|
|
#include <vector>
|
|
|
|
#include "test/test.h"
|
|
#include "server/protocol.h"
|
|
#include "server/worker_test_helpers.h"
|
|
|
|
#include "kota/codec/json/json.h"
|
|
|
|
namespace clice::testing {
|
|
|
|
namespace {
|
|
|
|
TEST_SUITE(StatefulWorker) {
|
|
|
|
TEST_CASE(SpawnAndExit) {
|
|
WorkerHandle w;
|
|
ASSERT_TRUE(w.spawn("stateful-worker"));
|
|
|
|
w.peer->close_output();
|
|
w.loop.schedule(w.peer->run());
|
|
w.loop.run();
|
|
}
|
|
|
|
TEST_CASE(CompileRequest) {
|
|
TempDir tmp;
|
|
tmp.touch("compile_test.cpp", "int main() { return 0; }\n");
|
|
auto src = tmp.path("compile_test.cpp");
|
|
|
|
WorkerHandle w;
|
|
ASSERT_TRUE(w.spawn("stateful-worker"));
|
|
|
|
bool test_done = false;
|
|
|
|
w.run([&]() -> kota::task<> {
|
|
worker::CompileParams params;
|
|
params.path = src;
|
|
params.version = 1;
|
|
params.text = "int main() { return 0; }\n";
|
|
params.directory = "/tmp";
|
|
params.arguments = make_args(src);
|
|
params.pch = {"", 0};
|
|
params.pcms = {};
|
|
|
|
auto result = co_await w.peer->send_request(params);
|
|
CO_ASSERT_TRUE(result.has_value());
|
|
EXPECT_EQ(result.value().version, 1);
|
|
test_done = true;
|
|
w.peer->close_output();
|
|
});
|
|
|
|
ASSERT_TRUE(test_done);
|
|
}
|
|
|
|
TEST_CASE(HoverWithoutCompile) {
|
|
WorkerHandle w;
|
|
ASSERT_TRUE(w.spawn("stateful-worker"));
|
|
|
|
bool test_done = false;
|
|
|
|
w.run([&]() -> kota::task<> {
|
|
// Hover on a file that hasn't been compiled should return null.
|
|
worker::QueryParams params;
|
|
params.kind = worker::QueryKind::Hover;
|
|
params.path = "/tmp/nonexistent.cpp";
|
|
params.offset = 0;
|
|
|
|
auto result = co_await w.peer->send_request(params);
|
|
CO_ASSERT_TRUE(result.has_value());
|
|
// Should be "null" RawValue since document doesn't exist.
|
|
EXPECT_EQ(result.value().data, std::string("null"));
|
|
test_done = true;
|
|
w.peer->close_output();
|
|
});
|
|
|
|
ASSERT_TRUE(test_done);
|
|
}
|
|
|
|
TEST_CASE(CompileThenHover) {
|
|
std::string text = "int foo() { return 42; }\nint main() { return foo(); }\n";
|
|
TempDir tmp;
|
|
tmp.touch("hover_test.cpp", text);
|
|
auto src = tmp.path("hover_test.cpp");
|
|
|
|
WorkerHandle w;
|
|
ASSERT_TRUE(w.spawn("stateful-worker"));
|
|
|
|
bool test_done = false;
|
|
|
|
w.run([&]() -> kota::task<> {
|
|
// First compile
|
|
worker::CompileParams cp;
|
|
cp.path = src;
|
|
cp.version = 1;
|
|
cp.text = text;
|
|
cp.directory = "/tmp";
|
|
cp.arguments = make_args(src);
|
|
|
|
auto compile_result = co_await w.peer->send_request(cp);
|
|
CO_ASSERT_TRUE(compile_result.has_value());
|
|
|
|
// After successful compilation, hover should return info.
|
|
// "int foo() { return 42; }\n" is 25 chars, then char 22 on line 1 = offset 47
|
|
worker::QueryParams hp;
|
|
hp.kind = worker::QueryKind::Hover;
|
|
hp.path = src;
|
|
hp.offset = 47; // position of 'foo' in 'return foo();'
|
|
|
|
auto hover_result = co_await w.peer->send_request(hp);
|
|
EXPECT_TRUE(hover_result.has_value());
|
|
// Should return non-null hover info for 'foo'.
|
|
EXPECT_NE(hover_result.value().data, std::string("null"));
|
|
|
|
test_done = true;
|
|
w.peer->close_output();
|
|
});
|
|
|
|
ASSERT_TRUE(test_done);
|
|
}
|
|
|
|
TEST_CASE(DocumentUpdate) {
|
|
TempDir tmp;
|
|
tmp.touch("update_test.cpp", "int x = 1;\n");
|
|
auto src = tmp.path("update_test.cpp");
|
|
|
|
WorkerHandle w;
|
|
ASSERT_TRUE(w.spawn("stateful-worker"));
|
|
|
|
bool test_done = false;
|
|
|
|
w.run([&]() -> kota::task<> {
|
|
// Compile first
|
|
worker::CompileParams cp;
|
|
cp.path = src;
|
|
cp.version = 1;
|
|
cp.text = "int x = 1;\n";
|
|
cp.directory = "/tmp";
|
|
cp.arguments = make_args(src);
|
|
|
|
auto r1 = co_await w.peer->send_request(cp);
|
|
CO_ASSERT_TRUE(r1.has_value());
|
|
|
|
// Send document update notification (marks doc dirty, text comes
|
|
// with next Compile request).
|
|
worker::DocumentUpdateParams up;
|
|
up.path = src;
|
|
up.version = 2;
|
|
w.peer->send_notification(up);
|
|
|
|
// After update, hover still returns stale AST results (not null).
|
|
worker::QueryParams hp;
|
|
hp.kind = worker::QueryKind::Hover;
|
|
hp.path = src;
|
|
hp.offset = 4;
|
|
|
|
auto hover_result = co_await w.peer->send_request(hp);
|
|
EXPECT_TRUE(hover_result.has_value());
|
|
|
|
test_done = true;
|
|
w.peer->close_output();
|
|
});
|
|
|
|
ASSERT_TRUE(test_done);
|
|
}
|
|
|
|
TEST_CASE(CodeActionReturnsEmpty) {
|
|
WorkerHandle w;
|
|
ASSERT_TRUE(w.spawn("stateful-worker"));
|
|
|
|
bool test_done = false;
|
|
|
|
w.run([&]() -> kota::task<> {
|
|
worker::QueryParams params;
|
|
params.kind = worker::QueryKind::CodeAction;
|
|
params.path = "/tmp/test.cpp";
|
|
|
|
auto result = co_await w.peer->send_request(params);
|
|
CO_ASSERT_TRUE(result.has_value());
|
|
// Should return empty array "[]" (TODO stub)
|
|
EXPECT_EQ(result.value().data, std::string("[]"));
|
|
test_done = true;
|
|
w.peer->close_output();
|
|
});
|
|
|
|
ASSERT_TRUE(test_done);
|
|
}
|
|
|
|
TEST_CASE(GoToDefinitionReturnsEmpty) {
|
|
WorkerHandle w;
|
|
ASSERT_TRUE(w.spawn("stateful-worker"));
|
|
|
|
bool test_done = false;
|
|
|
|
w.run([&]() -> kota::task<> {
|
|
worker::QueryParams params;
|
|
params.kind = worker::QueryKind::GoToDefinition;
|
|
params.path = "/tmp/test.cpp";
|
|
params.offset = 0;
|
|
|
|
auto result = co_await w.peer->send_request(params);
|
|
CO_ASSERT_TRUE(result.has_value());
|
|
// Should return empty array "[]" (TODO stub)
|
|
EXPECT_EQ(result.value().data, std::string("[]"));
|
|
test_done = true;
|
|
w.peer->close_output();
|
|
});
|
|
|
|
ASSERT_TRUE(test_done);
|
|
}
|
|
|
|
TEST_CASE(SemanticTokensWithoutCompile) {
|
|
WorkerHandle w;
|
|
ASSERT_TRUE(w.spawn("stateful-worker"));
|
|
|
|
bool test_done = false;
|
|
|
|
w.run([&]() -> kota::task<> {
|
|
worker::QueryParams params;
|
|
params.kind = worker::QueryKind::SemanticTokens;
|
|
params.path = "/tmp/nonexistent.cpp";
|
|
|
|
auto result = co_await w.peer->send_request(params);
|
|
CO_ASSERT_TRUE(result.has_value());
|
|
EXPECT_EQ(result.value().data, std::string("null"));
|
|
test_done = true;
|
|
w.peer->close_output();
|
|
});
|
|
|
|
ASSERT_TRUE(test_done);
|
|
}
|
|
|
|
TEST_CASE(FoldingRangeWithoutCompile) {
|
|
WorkerHandle w;
|
|
ASSERT_TRUE(w.spawn("stateful-worker"));
|
|
|
|
bool test_done = false;
|
|
|
|
w.run([&]() -> kota::task<> {
|
|
worker::QueryParams params;
|
|
params.kind = worker::QueryKind::FoldingRange;
|
|
params.path = "/tmp/nonexistent.cpp";
|
|
|
|
auto result = co_await w.peer->send_request(params);
|
|
CO_ASSERT_TRUE(result.has_value());
|
|
EXPECT_EQ(result.value().data, std::string("null"));
|
|
test_done = true;
|
|
w.peer->close_output();
|
|
});
|
|
|
|
ASSERT_TRUE(test_done);
|
|
}
|
|
|
|
TEST_CASE(DocumentSymbolWithoutCompile) {
|
|
WorkerHandle w;
|
|
ASSERT_TRUE(w.spawn("stateful-worker"));
|
|
|
|
bool test_done = false;
|
|
|
|
w.run([&]() -> kota::task<> {
|
|
worker::QueryParams params;
|
|
params.kind = worker::QueryKind::DocumentSymbol;
|
|
params.path = "/tmp/nonexistent.cpp";
|
|
|
|
auto result = co_await w.peer->send_request(params);
|
|
CO_ASSERT_TRUE(result.has_value());
|
|
EXPECT_EQ(result.value().data, std::string("null"));
|
|
test_done = true;
|
|
w.peer->close_output();
|
|
});
|
|
|
|
ASSERT_TRUE(test_done);
|
|
}
|
|
|
|
TEST_CASE(DocumentLinkWithoutCompile) {
|
|
WorkerHandle w;
|
|
ASSERT_TRUE(w.spawn("stateful-worker"));
|
|
|
|
bool test_done = false;
|
|
|
|
w.run([&]() -> kota::task<> {
|
|
worker::QueryParams params;
|
|
params.kind = worker::QueryKind::DocumentLink;
|
|
params.path = "/tmp/nonexistent.cpp";
|
|
|
|
auto result = co_await w.peer->send_request(params);
|
|
CO_ASSERT_TRUE(result.has_value());
|
|
EXPECT_EQ(result.value().data, std::string("null"));
|
|
test_done = true;
|
|
w.peer->close_output();
|
|
});
|
|
|
|
ASSERT_TRUE(test_done);
|
|
}
|
|
|
|
TEST_CASE(InlayHintsWithoutCompile) {
|
|
WorkerHandle w;
|
|
ASSERT_TRUE(w.spawn("stateful-worker"));
|
|
|
|
bool test_done = false;
|
|
|
|
w.run([&]() -> kota::task<> {
|
|
worker::QueryParams params;
|
|
params.kind = worker::QueryKind::InlayHints;
|
|
params.path = "/tmp/nonexistent.cpp";
|
|
|
|
auto result = co_await w.peer->send_request(params);
|
|
CO_ASSERT_TRUE(result.has_value());
|
|
EXPECT_EQ(result.value().data, std::string("null"));
|
|
test_done = true;
|
|
w.peer->close_output();
|
|
});
|
|
|
|
ASSERT_TRUE(test_done);
|
|
}
|
|
|
|
TEST_CASE(MultipleSequentialRequests) {
|
|
TempDir tmp;
|
|
tmp.touch("seq_test.cpp",
|
|
"int foo(int x) {\n"
|
|
" return x + 1;\n"
|
|
"}\n"
|
|
"int main() {\n"
|
|
" return foo(0);\n"
|
|
"}\n");
|
|
auto src = tmp.path("seq_test.cpp");
|
|
|
|
WorkerHandle w;
|
|
ASSERT_TRUE(w.spawn("stateful-worker"));
|
|
|
|
bool test_done = false;
|
|
|
|
w.run([&]() -> kota::task<> {
|
|
// Compile first so feature requests return real data.
|
|
worker::CompileParams cp;
|
|
cp.path = src;
|
|
cp.version = 1;
|
|
cp.text = "int foo(int x) {\n return x + 1;\n}\nint main() {\n return foo(0);\n}\n";
|
|
cp.directory = "/tmp";
|
|
cp.arguments = make_args(src);
|
|
|
|
auto cr = co_await w.peer->send_request(cp);
|
|
CO_ASSERT_TRUE(cr.has_value());
|
|
|
|
// Now send multiple different feature requests sequentially.
|
|
worker::QueryParams hp;
|
|
hp.kind = worker::QueryKind::Hover;
|
|
hp.path = src;
|
|
hp.offset = 4; // 'foo' on line 0
|
|
auto r1 = co_await w.peer->send_request(hp);
|
|
EXPECT_TRUE(r1.has_value());
|
|
|
|
worker::QueryParams cap;
|
|
cap.kind = worker::QueryKind::CodeAction;
|
|
cap.path = src;
|
|
auto r2 = co_await w.peer->send_request(cap);
|
|
EXPECT_TRUE(r2.has_value());
|
|
|
|
// 'foo' in 'return foo(0);' at line 4, char 11
|
|
// lines: "int foo(int x) {\n"=17, " return x + 1;\n"=18, "}\n"=2, "int main() {\n"=14
|
|
// offset = 17+18+2+14+11 = 62
|
|
worker::QueryParams gdp;
|
|
gdp.kind = worker::QueryKind::GoToDefinition;
|
|
gdp.path = src;
|
|
gdp.offset = 62;
|
|
auto r3 = co_await w.peer->send_request(gdp);
|
|
EXPECT_TRUE(r3.has_value());
|
|
|
|
worker::QueryParams stp;
|
|
stp.kind = worker::QueryKind::SemanticTokens;
|
|
stp.path = src;
|
|
auto r4 = co_await w.peer->send_request(stp);
|
|
EXPECT_TRUE(r4.has_value());
|
|
|
|
worker::QueryParams frp;
|
|
frp.kind = worker::QueryKind::FoldingRange;
|
|
frp.path = src;
|
|
auto r5 = co_await w.peer->send_request(frp);
|
|
EXPECT_TRUE(r5.has_value());
|
|
|
|
test_done = true;
|
|
w.peer->close_output();
|
|
});
|
|
|
|
ASSERT_TRUE(test_done);
|
|
}
|
|
|
|
TEST_CASE(MultipleDocuments) {
|
|
TempDir tmp;
|
|
std::vector<std::string> paths;
|
|
std::vector<std::string> texts;
|
|
for(int i = 0; i < 3; i++) {
|
|
auto name = "multi_" + std::to_string(i) + ".cpp";
|
|
auto text = "int var_" + std::to_string(i) + " = " + std::to_string(i) + ";\n";
|
|
tmp.touch(name, text);
|
|
paths.push_back(tmp.path(name));
|
|
texts.push_back(text);
|
|
}
|
|
|
|
WorkerHandle w;
|
|
ASSERT_TRUE(w.spawn("stateful-worker"));
|
|
|
|
bool test_done = false;
|
|
|
|
w.run([&]() -> kota::task<> {
|
|
// Compile 3 different documents.
|
|
for(int i = 0; i < 3; i++) {
|
|
worker::CompileParams cp;
|
|
cp.path = paths[i];
|
|
cp.version = 1;
|
|
cp.text = texts[i];
|
|
cp.directory = "/tmp";
|
|
cp.arguments = make_args(paths[i]);
|
|
|
|
auto result = co_await w.peer->send_request(cp);
|
|
EXPECT_TRUE(result.has_value());
|
|
}
|
|
|
|
// Hover on each document after compilation.
|
|
for(int i = 0; i < 3; i++) {
|
|
worker::QueryParams hp;
|
|
hp.kind = worker::QueryKind::Hover;
|
|
hp.path = paths[i];
|
|
hp.offset = 4; // 'var_N'
|
|
|
|
auto result = co_await w.peer->send_request(hp);
|
|
EXPECT_TRUE(result.has_value());
|
|
}
|
|
|
|
test_done = true;
|
|
w.peer->close_output();
|
|
});
|
|
|
|
ASSERT_TRUE(test_done);
|
|
}
|
|
|
|
TEST_CASE(EvictNotification) {
|
|
WorkerHandle w;
|
|
ASSERT_TRUE(w.spawn("stateful-worker"));
|
|
|
|
bool test_done = false;
|
|
|
|
w.run([&]() -> kota::task<> {
|
|
// Send an evict notification — worker should remove the document without crashing.
|
|
worker::EvictParams ep;
|
|
ep.path = "/tmp/evict_test.cpp";
|
|
w.peer->send_notification(ep);
|
|
|
|
// Hover on the evicted document should return null (document doesn't exist).
|
|
worker::QueryParams hp;
|
|
hp.kind = worker::QueryKind::Hover;
|
|
hp.path = "/tmp/evict_test.cpp";
|
|
hp.offset = 0;
|
|
|
|
auto result = co_await w.peer->send_request(hp);
|
|
CO_ASSERT_TRUE(result.has_value());
|
|
EXPECT_EQ(result.value().data, std::string("null"));
|
|
|
|
test_done = true;
|
|
w.peer->close_output();
|
|
});
|
|
|
|
ASSERT_TRUE(test_done);
|
|
}
|
|
|
|
TEST_CASE(SpawnWithMemoryLimit) {
|
|
TempDir tmp;
|
|
tmp.touch("memlimit_test.cpp", "int memlimit_var = 42;\n");
|
|
auto src = tmp.path("memlimit_test.cpp");
|
|
|
|
WorkerHandle w;
|
|
// Spawn with a specific memory limit to test the CLI flag is accepted.
|
|
ASSERT_TRUE(w.spawn("stateful-worker", 2ULL * 1024 * 1024 * 1024));
|
|
|
|
bool test_done = false;
|
|
|
|
w.run([&]() -> kota::task<> {
|
|
// Compile first.
|
|
worker::CompileParams cp;
|
|
cp.path = src;
|
|
cp.version = 1;
|
|
cp.text = "int memlimit_var = 42;\n";
|
|
cp.directory = "/tmp";
|
|
cp.arguments = make_args(src);
|
|
|
|
auto cr = co_await w.peer->send_request(cp);
|
|
EXPECT_TRUE(cr.has_value());
|
|
|
|
// Feature request should work after compilation.
|
|
worker::QueryParams hp;
|
|
hp.kind = worker::QueryKind::Hover;
|
|
hp.path = src;
|
|
hp.offset = 4; // 'memlimit_var'
|
|
|
|
auto result = co_await w.peer->send_request(hp);
|
|
EXPECT_TRUE(result.has_value());
|
|
|
|
test_done = true;
|
|
w.peer->close_output();
|
|
});
|
|
|
|
ASSERT_TRUE(test_done);
|
|
}
|
|
|
|
}; // TEST_SUITE(StatefulWorker)
|
|
|
|
} // namespace
|
|
|
|
} // namespace clice::testing
|