feat: add include resolver, dependency graph, BFS scanner (#368)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
606
tests/unit/syntax/dependency_graph_tests.cpp
Normal file
606
tests/unit/syntax/dependency_graph_tests.cpp
Normal file
@@ -0,0 +1,606 @@
|
||||
#include "test/temp_dir.h"
|
||||
#include "test/test.h"
|
||||
#include "command/command.h"
|
||||
#include "support/path_pool.h"
|
||||
#include "syntax/dependency_graph.h"
|
||||
|
||||
namespace clice::testing {
|
||||
namespace {
|
||||
|
||||
TEST_SUITE(DependencyGraph) {
|
||||
|
||||
// ============================================================================
|
||||
// Module mapping tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_CASE(LookupModuleEmpty) {
|
||||
clice::DependencyGraph graph;
|
||||
EXPECT_TRUE(graph.lookup_module("foo.bar").empty());
|
||||
}
|
||||
|
||||
TEST_CASE(AddAndLookupModule) {
|
||||
clice::DependencyGraph graph;
|
||||
graph.add_module("foo.bar", 42);
|
||||
|
||||
auto result = graph.lookup_module("foo.bar");
|
||||
ASSERT_EQ(result.size(), 1u);
|
||||
EXPECT_EQ(result[0], 42u);
|
||||
}
|
||||
|
||||
TEST_CASE(DuplicateModuleDedup) {
|
||||
clice::DependencyGraph graph;
|
||||
// Same module name, same path_id — should dedup.
|
||||
graph.add_module("foo", 10);
|
||||
graph.add_module("foo", 10);
|
||||
ASSERT_EQ(graph.lookup_module("foo").size(), 1u);
|
||||
|
||||
// Same module name, different path_id — multiple candidates.
|
||||
graph.add_module("foo", 20);
|
||||
auto result = graph.lookup_module("foo");
|
||||
ASSERT_EQ(result.size(), 2u);
|
||||
EXPECT_EQ(result[0], 10u);
|
||||
EXPECT_EQ(result[1], 20u);
|
||||
}
|
||||
|
||||
TEST_CASE(MultipleModules) {
|
||||
clice::DependencyGraph graph;
|
||||
graph.add_module("mod.a", 1);
|
||||
graph.add_module("mod.b", 2);
|
||||
graph.add_module("mod.c:part", 3);
|
||||
|
||||
ASSERT_EQ(graph.lookup_module("mod.a").size(), 1u);
|
||||
EXPECT_EQ(graph.lookup_module("mod.a")[0], 1u);
|
||||
ASSERT_EQ(graph.lookup_module("mod.b").size(), 1u);
|
||||
EXPECT_EQ(graph.lookup_module("mod.b")[0], 2u);
|
||||
ASSERT_EQ(graph.lookup_module("mod.c:part").size(), 1u);
|
||||
EXPECT_EQ(graph.lookup_module("mod.c:part")[0], 3u);
|
||||
EXPECT_TRUE(graph.lookup_module("mod.d").empty());
|
||||
}
|
||||
|
||||
TEST_CASE(ModuleCount) {
|
||||
clice::DependencyGraph graph;
|
||||
EXPECT_EQ(graph.module_count(), 0u);
|
||||
|
||||
graph.add_module("a", 1);
|
||||
EXPECT_EQ(graph.module_count(), 1u);
|
||||
|
||||
graph.add_module("b", 2);
|
||||
EXPECT_EQ(graph.module_count(), 2u);
|
||||
|
||||
// Second candidate for "a" doesn't increase module name count.
|
||||
graph.add_module("a", 3);
|
||||
EXPECT_EQ(graph.module_count(), 2u);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Include edge tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_CASE(EmptyGraphIncludes) {
|
||||
clice::DependencyGraph graph;
|
||||
auto includes = graph.get_includes(0, 0);
|
||||
EXPECT_TRUE(includes.empty());
|
||||
}
|
||||
|
||||
TEST_CASE(SetAndGetIncludes) {
|
||||
clice::DependencyGraph graph;
|
||||
llvm::SmallVector<std::uint32_t> ids = {10, 20, 30};
|
||||
graph.set_includes(1, 0, ids);
|
||||
|
||||
auto result = graph.get_includes(1, 0);
|
||||
ASSERT_EQ(result.size(), 3u);
|
||||
EXPECT_EQ(result[0], 10u);
|
||||
EXPECT_EQ(result[1], 20u);
|
||||
EXPECT_EQ(result[2], 30u);
|
||||
}
|
||||
|
||||
TEST_CASE(IncludesPerConfig) {
|
||||
clice::DependencyGraph graph;
|
||||
|
||||
// Same file, different configs.
|
||||
graph.set_includes(1, 0, {10, 20});
|
||||
graph.set_includes(1, 1, {20, 30});
|
||||
|
||||
auto config0 = graph.get_includes(1, 0);
|
||||
ASSERT_EQ(config0.size(), 2u);
|
||||
EXPECT_EQ(config0[0], 10u);
|
||||
EXPECT_EQ(config0[1], 20u);
|
||||
|
||||
auto config1 = graph.get_includes(1, 1);
|
||||
ASSERT_EQ(config1.size(), 2u);
|
||||
EXPECT_EQ(config1[0], 20u);
|
||||
EXPECT_EQ(config1[1], 30u);
|
||||
}
|
||||
|
||||
TEST_CASE(GetAllIncludesUnion) {
|
||||
clice::DependencyGraph graph;
|
||||
|
||||
graph.set_includes(1, 0, {10, 20});
|
||||
graph.set_includes(1, 1, {20, 30});
|
||||
|
||||
auto all = graph.get_all_includes(1);
|
||||
// Union of {10, 20} and {20, 30} = {10, 20, 30}.
|
||||
ASSERT_EQ(all.size(), 3u);
|
||||
}
|
||||
|
||||
TEST_CASE(ConditionalFlag) {
|
||||
clice::DependencyGraph graph;
|
||||
|
||||
constexpr auto FLAG = clice::DependencyGraph::CONDITIONAL_FLAG;
|
||||
constexpr auto MASK = clice::DependencyGraph::PATH_ID_MASK;
|
||||
|
||||
// PathID 5 unconditional, PathID 7 conditional.
|
||||
llvm::SmallVector<std::uint32_t> ids = {5, 7 | FLAG};
|
||||
graph.set_includes(1, 0, ids);
|
||||
|
||||
auto result = graph.get_includes(1, 0);
|
||||
ASSERT_EQ(result.size(), 2u);
|
||||
|
||||
// First: unconditional.
|
||||
EXPECT_EQ(result[0] & MASK, 5u);
|
||||
EXPECT_EQ(result[0] & FLAG, 0u);
|
||||
|
||||
// Second: conditional.
|
||||
EXPECT_EQ(result[1] & MASK, 7u);
|
||||
EXPECT_NE(result[1] & FLAG, 0u);
|
||||
}
|
||||
|
||||
TEST_CASE(FileCount) {
|
||||
clice::DependencyGraph graph;
|
||||
EXPECT_EQ(graph.file_count(), 0u);
|
||||
|
||||
graph.set_includes(1, 0, {10});
|
||||
EXPECT_EQ(graph.file_count(), 1u);
|
||||
|
||||
// Same file, different config.
|
||||
graph.set_includes(1, 1, {20});
|
||||
EXPECT_EQ(graph.file_count(), 1u);
|
||||
|
||||
// Different file.
|
||||
graph.set_includes(2, 0, {30});
|
||||
EXPECT_EQ(graph.file_count(), 2u);
|
||||
}
|
||||
|
||||
TEST_CASE(EdgeCount) {
|
||||
clice::DependencyGraph graph;
|
||||
EXPECT_EQ(graph.edge_count(), 0u);
|
||||
|
||||
graph.set_includes(1, 0, {10, 20});
|
||||
EXPECT_EQ(graph.edge_count(), 2u);
|
||||
|
||||
graph.set_includes(2, 0, {30});
|
||||
EXPECT_EQ(graph.edge_count(), 3u);
|
||||
}
|
||||
|
||||
TEST_CASE(EmptyIncludes) {
|
||||
clice::DependencyGraph graph;
|
||||
graph.set_includes(1, 0, {});
|
||||
|
||||
auto result = graph.get_includes(1, 0);
|
||||
EXPECT_TRUE(result.empty());
|
||||
EXPECT_EQ(graph.file_count(), 1u);
|
||||
EXPECT_EQ(graph.edge_count(), 0u);
|
||||
}
|
||||
|
||||
}; // TEST_SUITE(DependencyGraph)
|
||||
|
||||
// ============================================================================
|
||||
// scan_dependency_graph() integration tests
|
||||
// ============================================================================
|
||||
|
||||
/// Write a compile_commands.json into the temp dir and load it into the given CDB.
|
||||
void write_cdb(TempDir& tmp, CompilationDatabase& cdb, llvm::StringRef json_content) {
|
||||
tmp.touch("compile_commands.json", json_content);
|
||||
cdb.load(tmp.path("compile_commands.json"));
|
||||
}
|
||||
|
||||
/// Helper: build a compile_commands.json array from entries.
|
||||
/// Uses "arguments" array form to avoid platform-specific tokenization issues
|
||||
/// (e.g. TokenizeGNUCommandLine treating backslashes as escape characters).
|
||||
struct CDBEntry {
|
||||
llvm::StringRef dir;
|
||||
std::string file;
|
||||
std::vector<std::string> extra_args;
|
||||
};
|
||||
|
||||
/// Escape backslashes and quotes for JSON string values.
|
||||
std::string json_escape(llvm::StringRef s) {
|
||||
std::string result;
|
||||
result.reserve(s.size());
|
||||
for(char c: s) {
|
||||
if(c == '\\' || c == '"') {
|
||||
result += '\\';
|
||||
}
|
||||
result += c;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string build_cdb_json(llvm::ArrayRef<CDBEntry> entries) {
|
||||
std::string json = "[\n";
|
||||
for(std::size_t i = 0; i < entries.size(); ++i) {
|
||||
auto& e = entries[i];
|
||||
if(i > 0) {
|
||||
json += ",\n";
|
||||
}
|
||||
json += R"( {"directory": ")";
|
||||
json += json_escape(e.dir);
|
||||
json += R"(", "file": ")";
|
||||
json += json_escape(e.file);
|
||||
json += R"(", "arguments": ["clang++", "-std=c++20")";
|
||||
for(auto& arg: e.extra_args) {
|
||||
json += R"(, ")";
|
||||
json += json_escape(arg);
|
||||
json += R"(")";
|
||||
}
|
||||
json += R"(, ")";
|
||||
json += json_escape(e.file);
|
||||
json += R"("]})";
|
||||
}
|
||||
json += "\n]";
|
||||
return json;
|
||||
}
|
||||
|
||||
TEST_SUITE(ScanDependencyGraph) {
|
||||
|
||||
TEST_CASE(EmptyCDB) {
|
||||
CompilationDatabase cdb;
|
||||
PathPool pool;
|
||||
DependencyGraph graph;
|
||||
|
||||
scan_dependency_graph(cdb, pool, graph);
|
||||
|
||||
EXPECT_EQ(graph.file_count(), 0u);
|
||||
EXPECT_EQ(graph.module_count(), 0u);
|
||||
EXPECT_EQ(graph.edge_count(), 0u);
|
||||
}
|
||||
|
||||
TEST_CASE(SingleFileNoIncludes) {
|
||||
TempDir tmp;
|
||||
tmp.touch("src/main.cpp", R"(int main() { return 0; })");
|
||||
|
||||
CompilationDatabase cdb;
|
||||
PathPool pool;
|
||||
DependencyGraph graph;
|
||||
|
||||
auto json = build_cdb_json({
|
||||
{tmp.root, tmp.path("src/main.cpp"), {}}
|
||||
});
|
||||
write_cdb(tmp, cdb, json);
|
||||
scan_dependency_graph(cdb, pool, graph);
|
||||
|
||||
EXPECT_EQ(graph.file_count(), 1u);
|
||||
EXPECT_EQ(graph.edge_count(), 0u);
|
||||
EXPECT_EQ(graph.module_count(), 0u);
|
||||
}
|
||||
|
||||
TEST_CASE(SingleFileWithInclude) {
|
||||
TempDir tmp;
|
||||
tmp.touch("include/header.h", R"(int x = 1;)");
|
||||
tmp.touch("src/main.cpp", R"(
|
||||
#include "header.h"
|
||||
int main() { return x; }
|
||||
)");
|
||||
|
||||
CompilationDatabase cdb;
|
||||
PathPool pool;
|
||||
DependencyGraph graph;
|
||||
|
||||
auto json = build_cdb_json({
|
||||
{tmp.root, tmp.path("src/main.cpp"), {"-I", tmp.path("include")}}
|
||||
});
|
||||
write_cdb(tmp, cdb, json);
|
||||
scan_dependency_graph(cdb, pool, graph);
|
||||
|
||||
EXPECT_GE(graph.file_count(), 1u);
|
||||
EXPECT_GE(graph.edge_count(), 1u);
|
||||
}
|
||||
|
||||
TEST_CASE(TransitiveIncludes) {
|
||||
TempDir tmp;
|
||||
tmp.touch("inc/a.h", R"(#include "b.h")");
|
||||
tmp.touch("inc/b.h", R"(#include "c.h")");
|
||||
tmp.touch("inc/c.h", R"(int c = 3;)");
|
||||
tmp.touch("src/main.cpp", R"(
|
||||
#include "a.h"
|
||||
int main() {}
|
||||
)");
|
||||
|
||||
CompilationDatabase cdb;
|
||||
PathPool pool;
|
||||
DependencyGraph graph;
|
||||
|
||||
auto json = build_cdb_json({
|
||||
{tmp.root, tmp.path("src/main.cpp"), {"-I", tmp.path("inc")}}
|
||||
});
|
||||
write_cdb(tmp, cdb, json);
|
||||
scan_dependency_graph(cdb, pool, graph);
|
||||
|
||||
// main->a, a->b, b->c across 4 waves.
|
||||
EXPECT_GE(graph.file_count(), 3u);
|
||||
EXPECT_GE(graph.edge_count(), 3u);
|
||||
}
|
||||
|
||||
TEST_CASE(MultipleSourceFiles) {
|
||||
TempDir tmp;
|
||||
tmp.touch("inc/shared.h", R"(int shared = 1;)");
|
||||
tmp.touch("src/a.cpp", R"(
|
||||
#include "shared.h"
|
||||
void a() {}
|
||||
)");
|
||||
tmp.touch("src/b.cpp", R"(
|
||||
#include "shared.h"
|
||||
void b() {}
|
||||
)");
|
||||
|
||||
CompilationDatabase cdb;
|
||||
PathPool pool;
|
||||
DependencyGraph graph;
|
||||
|
||||
std::vector<std::string> inc = {"-I", tmp.path("inc")};
|
||||
auto json = build_cdb_json({
|
||||
{tmp.root, tmp.path("src/a.cpp"), inc},
|
||||
{tmp.root, tmp.path("src/b.cpp"), inc},
|
||||
});
|
||||
write_cdb(tmp, cdb, json);
|
||||
scan_dependency_graph(cdb, pool, graph);
|
||||
|
||||
EXPECT_GE(graph.file_count(), 2u);
|
||||
EXPECT_GE(graph.edge_count(), 2u);
|
||||
}
|
||||
|
||||
TEST_CASE(ConditionalIncludes) {
|
||||
TempDir tmp;
|
||||
tmp.touch("inc/always.h", R"(// always)");
|
||||
tmp.touch("inc/maybe.h", R"(// maybe)");
|
||||
tmp.touch("src/main.cpp", R"(
|
||||
#include "always.h"
|
||||
#ifdef FOO
|
||||
#include "maybe.h"
|
||||
#endif
|
||||
)");
|
||||
|
||||
CompilationDatabase cdb;
|
||||
PathPool pool;
|
||||
DependencyGraph graph;
|
||||
|
||||
auto json = build_cdb_json({
|
||||
{tmp.root, tmp.path("src/main.cpp"), {"-I", tmp.path("inc")}}
|
||||
});
|
||||
write_cdb(tmp, cdb, json);
|
||||
scan_dependency_graph(cdb, pool, graph);
|
||||
|
||||
// Both headers discovered (over-approximate).
|
||||
EXPECT_GE(graph.edge_count(), 2u);
|
||||
|
||||
// Verify conditional flag.
|
||||
bool found_unconditional = false;
|
||||
bool found_conditional = false;
|
||||
auto includes = graph.get_includes(pool.cache[tmp.path("src/main.cpp")], 0);
|
||||
for(auto id: includes) {
|
||||
if(id & DependencyGraph::CONDITIONAL_FLAG) {
|
||||
found_conditional = true;
|
||||
} else {
|
||||
found_unconditional = true;
|
||||
}
|
||||
}
|
||||
EXPECT_TRUE(found_unconditional);
|
||||
EXPECT_TRUE(found_conditional);
|
||||
}
|
||||
|
||||
TEST_CASE(ModuleExtraction) {
|
||||
TempDir tmp;
|
||||
tmp.touch("src/mymod.cpp", R"(
|
||||
export module my.module;
|
||||
export int foo() { return 42; }
|
||||
)");
|
||||
|
||||
CompilationDatabase cdb;
|
||||
PathPool pool;
|
||||
DependencyGraph graph;
|
||||
|
||||
auto json = build_cdb_json({
|
||||
{tmp.root, tmp.path("src/mymod.cpp"), {}}
|
||||
});
|
||||
write_cdb(tmp, cdb, json);
|
||||
scan_dependency_graph(cdb, pool, graph);
|
||||
|
||||
auto result = graph.lookup_module("my.module");
|
||||
ASSERT_EQ(result.size(), 1u);
|
||||
|
||||
auto path = pool.resolve(result[0]);
|
||||
EXPECT_TRUE(llvm::sys::fs::equivalent(path, tmp.path("src/mymod.cpp")));
|
||||
}
|
||||
|
||||
TEST_CASE(ModulePartition) {
|
||||
TempDir tmp;
|
||||
tmp.touch("src/mod.cpp", R"(
|
||||
export module my.mod:part;
|
||||
void impl() {}
|
||||
)");
|
||||
|
||||
CompilationDatabase cdb;
|
||||
PathPool pool;
|
||||
DependencyGraph graph;
|
||||
|
||||
auto json = build_cdb_json({
|
||||
{tmp.root, tmp.path("src/mod.cpp"), {}}
|
||||
});
|
||||
write_cdb(tmp, cdb, json);
|
||||
scan_dependency_graph(cdb, pool, graph);
|
||||
|
||||
ASSERT_EQ(graph.lookup_module("my.mod:part").size(), 1u);
|
||||
}
|
||||
|
||||
TEST_CASE(DiamondIncludes) {
|
||||
TempDir tmp;
|
||||
tmp.touch("inc/common.h", R"(int common = 1;)");
|
||||
tmp.touch("inc/a.h", R"(
|
||||
#include "common.h"
|
||||
int a = 1;
|
||||
)");
|
||||
tmp.touch("inc/b.h", R"(
|
||||
#include "common.h"
|
||||
int b = 1;
|
||||
)");
|
||||
tmp.touch("src/main.cpp", R"(
|
||||
#include "a.h"
|
||||
#include "b.h"
|
||||
int main() {}
|
||||
)");
|
||||
|
||||
CompilationDatabase cdb;
|
||||
PathPool pool;
|
||||
DependencyGraph graph;
|
||||
|
||||
auto json = build_cdb_json({
|
||||
{tmp.root, tmp.path("src/main.cpp"), {"-I", tmp.path("inc")}}
|
||||
});
|
||||
write_cdb(tmp, cdb, json);
|
||||
scan_dependency_graph(cdb, pool, graph);
|
||||
|
||||
// main->a, main->b, a->common, b->common.
|
||||
EXPECT_GE(graph.edge_count(), 4u);
|
||||
EXPECT_GE(graph.file_count(), 3u);
|
||||
}
|
||||
|
||||
TEST_CASE(AngledVsQuoted) {
|
||||
TempDir tmp;
|
||||
tmp.touch("quoted/header.h", R"(int q = 1;)");
|
||||
tmp.touch("angled/header.h", R"(int a = 1;)");
|
||||
tmp.touch("src/main.cpp", R"(
|
||||
#include "header.h"
|
||||
#include <header.h>
|
||||
int main() {}
|
||||
)");
|
||||
|
||||
CompilationDatabase cdb;
|
||||
PathPool pool;
|
||||
DependencyGraph graph;
|
||||
|
||||
auto json = build_cdb_json({
|
||||
{tmp.root,
|
||||
tmp.path("src/main.cpp"),
|
||||
{"-iquote", tmp.path("quoted"), "-I", tmp.path("angled")}}
|
||||
});
|
||||
write_cdb(tmp, cdb, json);
|
||||
scan_dependency_graph(cdb, pool, graph);
|
||||
|
||||
EXPECT_GE(graph.edge_count(), 2u);
|
||||
}
|
||||
|
||||
TEST_CASE(MissingInclude) {
|
||||
TempDir tmp;
|
||||
tmp.touch("src/main.cpp", R"(
|
||||
#include "nonexistent.h"
|
||||
int main() {}
|
||||
)");
|
||||
|
||||
CompilationDatabase cdb;
|
||||
PathPool pool;
|
||||
DependencyGraph graph;
|
||||
|
||||
auto json = build_cdb_json({
|
||||
{tmp.root, tmp.path("src/main.cpp"), {}}
|
||||
});
|
||||
write_cdb(tmp, cdb, json);
|
||||
scan_dependency_graph(cdb, pool, graph);
|
||||
|
||||
EXPECT_EQ(graph.file_count(), 1u);
|
||||
EXPECT_EQ(graph.edge_count(), 0u);
|
||||
}
|
||||
|
||||
TEST_CASE(MultipleModules) {
|
||||
TempDir tmp;
|
||||
tmp.touch("src/mod_a.cpp", R"(
|
||||
export module mod.a;
|
||||
void a() {}
|
||||
)");
|
||||
tmp.touch("src/mod_b.cpp", R"(
|
||||
export module mod.b;
|
||||
void b() {}
|
||||
)");
|
||||
tmp.touch("src/impl.cpp", R"(
|
||||
module mod.a;
|
||||
void a_impl() {}
|
||||
)");
|
||||
|
||||
CompilationDatabase cdb;
|
||||
PathPool pool;
|
||||
DependencyGraph graph;
|
||||
|
||||
auto json = build_cdb_json({
|
||||
{tmp.root, tmp.path("src/mod_a.cpp"), {}},
|
||||
{tmp.root, tmp.path("src/mod_b.cpp"), {}},
|
||||
{tmp.root, tmp.path("src/impl.cpp"), {}},
|
||||
});
|
||||
write_cdb(tmp, cdb, json);
|
||||
scan_dependency_graph(cdb, pool, graph);
|
||||
|
||||
EXPECT_EQ(graph.module_count(), 2u);
|
||||
ASSERT_FALSE(graph.lookup_module("mod.a").empty());
|
||||
ASSERT_FALSE(graph.lookup_module("mod.b").empty());
|
||||
}
|
||||
|
||||
TEST_CASE(DeepIncludeChain) {
|
||||
TempDir tmp;
|
||||
tmp.touch("inc/h4.h", R"(int h4 = 4;)");
|
||||
tmp.touch("inc/h3.h", R"(#include "h4.h")");
|
||||
tmp.touch("inc/h2.h", R"(#include "h3.h")");
|
||||
tmp.touch("inc/h1.h", R"(#include "h2.h")");
|
||||
tmp.touch("inc/h0.h", R"(#include "h1.h")");
|
||||
tmp.touch("src/main.cpp", R"(
|
||||
#include "h0.h"
|
||||
int main() {}
|
||||
)");
|
||||
|
||||
CompilationDatabase cdb;
|
||||
PathPool pool;
|
||||
DependencyGraph graph;
|
||||
|
||||
auto json = build_cdb_json({
|
||||
{tmp.root, tmp.path("src/main.cpp"), {"-I", tmp.path("inc")}}
|
||||
});
|
||||
write_cdb(tmp, cdb, json);
|
||||
scan_dependency_graph(cdb, pool, graph);
|
||||
|
||||
// main->h0->h1->h2->h3->h4 across 5 waves.
|
||||
EXPECT_GE(graph.edge_count(), 5u);
|
||||
EXPECT_GE(graph.file_count(), 5u);
|
||||
}
|
||||
|
||||
TEST_CASE(ModuleWithIncludes) {
|
||||
TempDir tmp;
|
||||
tmp.touch("inc/util.h", R"(int util = 1;)");
|
||||
tmp.touch("src/mymod.cpp", R"(
|
||||
module;
|
||||
#include "util.h"
|
||||
export module my.lib;
|
||||
export int value() { return util; }
|
||||
)");
|
||||
|
||||
CompilationDatabase cdb;
|
||||
PathPool pool;
|
||||
DependencyGraph graph;
|
||||
|
||||
auto json = build_cdb_json({
|
||||
{tmp.root, tmp.path("src/mymod.cpp"), {"-I", tmp.path("inc")}}
|
||||
});
|
||||
write_cdb(tmp, cdb, json);
|
||||
scan_dependency_graph(cdb, pool, graph);
|
||||
|
||||
ASSERT_FALSE(graph.lookup_module("my.lib").empty());
|
||||
EXPECT_GE(graph.edge_count(), 1u);
|
||||
}
|
||||
|
||||
// TODO: add tests for:
|
||||
// - Circular includes (A→B→A) to verify BFS terminates correctly
|
||||
// - ScanCache warm runs (pass ScanCache* to scan_dependency_graph twice)
|
||||
// - get_all_includes flag merge: same header conditional in one config,
|
||||
// unconditional in another — unconditional should win
|
||||
// - set_includes overwrite: calling twice with same (path_id, config_id)
|
||||
|
||||
}; // TEST_SUITE(ScanDependencyGraph)
|
||||
|
||||
} // namespace
|
||||
} // namespace clice::testing
|
||||
356
tests/unit/syntax/include_resolver_tests.cpp
Normal file
356
tests/unit/syntax/include_resolver_tests.cpp
Normal file
@@ -0,0 +1,356 @@
|
||||
#include "test/temp_dir.h"
|
||||
#include "test/test.h"
|
||||
#include "syntax/include_resolver.h"
|
||||
#include "syntax/scan.h"
|
||||
|
||||
namespace clice::testing {
|
||||
namespace {
|
||||
|
||||
// ============================================================================
|
||||
// scan() — is_angled and is_include_next fields
|
||||
// ============================================================================
|
||||
|
||||
TEST_SUITE(IncludeResolver) {
|
||||
|
||||
TEST_CASE(ScanAngledVsQuoted) {
|
||||
auto result = scan(R"(
|
||||
#include <vector>
|
||||
#include "local.h"
|
||||
)");
|
||||
|
||||
ASSERT_EQ(result.includes.size(), 2u);
|
||||
EXPECT_EQ(result.includes[0].path, "vector");
|
||||
EXPECT_TRUE(result.includes[0].is_angled);
|
||||
EXPECT_FALSE(result.includes[0].is_include_next);
|
||||
|
||||
EXPECT_EQ(result.includes[1].path, "local.h");
|
||||
EXPECT_FALSE(result.includes[1].is_angled);
|
||||
EXPECT_FALSE(result.includes[1].is_include_next);
|
||||
}
|
||||
|
||||
TEST_CASE(ScanIncludeNext) {
|
||||
auto result = scan(R"(
|
||||
#include_next <stdlib.h>
|
||||
)");
|
||||
|
||||
ASSERT_EQ(result.includes.size(), 1u);
|
||||
EXPECT_EQ(result.includes[0].path, "stdlib.h");
|
||||
EXPECT_TRUE(result.includes[0].is_angled);
|
||||
EXPECT_TRUE(result.includes[0].is_include_next);
|
||||
}
|
||||
|
||||
TEST_CASE(ScanMixedDirectives) {
|
||||
auto result = scan(R"(
|
||||
#include <system.h>
|
||||
#include "quoted.h"
|
||||
#ifdef FOO
|
||||
#include <conditional_angled.h>
|
||||
#include "conditional_quoted.h"
|
||||
#endif
|
||||
#include_next "next_quoted.h"
|
||||
)");
|
||||
|
||||
ASSERT_EQ(result.includes.size(), 5u);
|
||||
|
||||
EXPECT_TRUE(result.includes[0].is_angled);
|
||||
EXPECT_FALSE(result.includes[0].conditional);
|
||||
|
||||
EXPECT_FALSE(result.includes[1].is_angled);
|
||||
EXPECT_FALSE(result.includes[1].conditional);
|
||||
|
||||
EXPECT_TRUE(result.includes[2].is_angled);
|
||||
EXPECT_TRUE(result.includes[2].conditional);
|
||||
|
||||
EXPECT_FALSE(result.includes[3].is_angled);
|
||||
EXPECT_TRUE(result.includes[3].conditional);
|
||||
|
||||
EXPECT_FALSE(result.includes[4].is_angled);
|
||||
EXPECT_TRUE(result.includes[4].is_include_next);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// resolve_include() — tests with real filesystem
|
||||
// ============================================================================
|
||||
|
||||
TEST_CASE(ResolveAbsolutePath) {
|
||||
TempDir tmp;
|
||||
tmp.touch("header.h");
|
||||
|
||||
auto abs_path = tmp.path("header.h");
|
||||
SearchConfig config;
|
||||
DirListingCache dir_cache;
|
||||
|
||||
auto result = resolve_include(abs_path, false, "", false, 0, config, dir_cache);
|
||||
|
||||
ASSERT_TRUE(result.has_value());
|
||||
EXPECT_TRUE(llvm::sys::fs::equivalent(result->path, abs_path));
|
||||
}
|
||||
|
||||
TEST_CASE(ResolveQuotedIncludeFromIncluderDir) {
|
||||
TempDir tmp;
|
||||
tmp.touch("src/main.cpp");
|
||||
tmp.touch("src/local.h");
|
||||
|
||||
SearchConfig config;
|
||||
config.dirs.push_back({tmp.path("include")});
|
||||
config.angled_start_idx = 0;
|
||||
|
||||
DirListingCache dir_cache;
|
||||
|
||||
auto result = resolve_include("local.h", false, tmp.path("src"), false, 0, config, dir_cache);
|
||||
|
||||
ASSERT_TRUE(result.has_value());
|
||||
EXPECT_TRUE(llvm::sys::fs::equivalent(result->path, tmp.path("src/local.h")));
|
||||
}
|
||||
|
||||
TEST_CASE(ResolveAngledIncludeFromSearchDirs) {
|
||||
TempDir tmp;
|
||||
tmp.touch("include/sys/types.h");
|
||||
|
||||
SearchConfig config;
|
||||
config.dirs.push_back({tmp.path("include")});
|
||||
config.angled_start_idx = 0;
|
||||
|
||||
DirListingCache dir_cache;
|
||||
|
||||
auto result = resolve_include("sys/types.h", true, "", false, 0, config, dir_cache);
|
||||
|
||||
ASSERT_TRUE(result.has_value());
|
||||
EXPECT_TRUE(llvm::sys::fs::equivalent(result->path, tmp.path("include/sys/types.h")));
|
||||
}
|
||||
|
||||
TEST_CASE(ResolveAngledSkipsQuotedDirs) {
|
||||
TempDir tmp;
|
||||
tmp.touch("quoted/header.h", "// quoted");
|
||||
tmp.touch("angled/header.h", "// angled");
|
||||
|
||||
SearchConfig config;
|
||||
config.dirs.push_back({tmp.path("quoted")}); // index 0 — quoted only
|
||||
config.dirs.push_back({tmp.path("angled")}); // index 1 — angled starts
|
||||
config.angled_start_idx = 1;
|
||||
|
||||
DirListingCache dir_cache;
|
||||
|
||||
auto result = resolve_include("header.h", true, "", false, 0, config, dir_cache);
|
||||
|
||||
ASSERT_TRUE(result.has_value());
|
||||
// Angled include should skip quoted dir and find in angled dir.
|
||||
EXPECT_TRUE(llvm::sys::fs::equivalent(result->path, tmp.path("angled/header.h")));
|
||||
EXPECT_EQ(result->found_dir_idx, 1u);
|
||||
}
|
||||
|
||||
TEST_CASE(ResolveIncludeNext) {
|
||||
TempDir tmp;
|
||||
tmp.touch("dir1/stdlib.h", "// first");
|
||||
tmp.touch("dir2/stdlib.h", "// second");
|
||||
|
||||
SearchConfig config;
|
||||
config.dirs.push_back({tmp.path("dir1")}); // index 0
|
||||
config.dirs.push_back({tmp.path("dir2")}); // index 1
|
||||
config.angled_start_idx = 0;
|
||||
|
||||
DirListingCache dir_cache;
|
||||
|
||||
// Simulate #include_next from a file found at dir index 0.
|
||||
auto result = resolve_include("stdlib.h", true, "", true, 0, config, dir_cache);
|
||||
|
||||
ASSERT_TRUE(result.has_value());
|
||||
// Should skip dir1 (found_dir_idx=0) and find in dir2.
|
||||
EXPECT_TRUE(llvm::sys::fs::equivalent(result->path, tmp.path("dir2/stdlib.h")));
|
||||
EXPECT_EQ(result->found_dir_idx, 1u);
|
||||
}
|
||||
|
||||
TEST_CASE(ResolveNotFound) {
|
||||
TempDir tmp;
|
||||
|
||||
SearchConfig config;
|
||||
config.dirs.push_back({tmp.path("include")});
|
||||
config.angled_start_idx = 0;
|
||||
|
||||
DirListingCache dir_cache;
|
||||
|
||||
auto result =
|
||||
resolve_include("nonexistent.h", false, tmp.path("src"), false, 0, config, dir_cache);
|
||||
|
||||
EXPECT_FALSE(result.has_value());
|
||||
}
|
||||
|
||||
TEST_CASE(ResolveStatCacheHits) {
|
||||
TempDir tmp;
|
||||
tmp.touch("include/cached.h");
|
||||
|
||||
SearchConfig config;
|
||||
config.dirs.push_back({tmp.path("include")});
|
||||
config.angled_start_idx = 0;
|
||||
|
||||
DirListingCache dir_cache;
|
||||
|
||||
// First resolution — populates cache.
|
||||
auto result1 = resolve_include("cached.h", true, "", false, 0, config, dir_cache);
|
||||
|
||||
ASSERT_TRUE(result1.has_value());
|
||||
|
||||
// Second resolution — should use cache (no filesystem I/O needed).
|
||||
auto result2 = resolve_include("cached.h", true, "", false, 0, config, dir_cache);
|
||||
|
||||
ASSERT_TRUE(result2.has_value());
|
||||
EXPECT_EQ(result1->path, result2->path);
|
||||
}
|
||||
|
||||
TEST_CASE(ResolveQuotedFallsBackToSearchDirs) {
|
||||
TempDir tmp;
|
||||
// Header not in includer dir, but in search dir.
|
||||
tmp.touch("include/fallback.h");
|
||||
|
||||
SearchConfig config;
|
||||
config.dirs.push_back({tmp.path("include")});
|
||||
config.angled_start_idx = 0;
|
||||
|
||||
DirListingCache dir_cache;
|
||||
|
||||
auto result =
|
||||
resolve_include("fallback.h", false, tmp.path("src"), false, 0, config, dir_cache);
|
||||
|
||||
ASSERT_TRUE(result.has_value());
|
||||
EXPECT_TRUE(llvm::sys::fs::equivalent(result->path, tmp.path("include/fallback.h")));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Three-tier search directory tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_CASE(AngledSkipsQuotedDirs) {
|
||||
TempDir tmp;
|
||||
tmp.touch("iquote/header.h", "// iquote");
|
||||
tmp.touch("idir/header.h", "// I dir");
|
||||
tmp.touch("sys/header.h", "// system");
|
||||
|
||||
// Layout: [iquote | idir | sys]
|
||||
SearchConfig config;
|
||||
config.dirs.push_back({tmp.path("iquote")}); // 0: Quoted
|
||||
config.dirs.push_back({tmp.path("idir")}); // 1: Angled
|
||||
config.dirs.push_back({tmp.path("sys")}); // 2: System
|
||||
config.angled_start_idx = 1;
|
||||
config.system_start_idx = 2;
|
||||
|
||||
DirListingCache dir_cache;
|
||||
|
||||
// <header.h> should skip iquote, find in idir (Angled before System).
|
||||
auto result = resolve_include("header.h", true, "", false, 0, config, dir_cache);
|
||||
ASSERT_TRUE(result.has_value());
|
||||
EXPECT_TRUE(llvm::sys::fs::equivalent(result->path, tmp.path("idir/header.h")));
|
||||
EXPECT_EQ(result->found_dir_idx, 1u);
|
||||
}
|
||||
|
||||
TEST_CASE(AngledMissesQuotedOnly) {
|
||||
TempDir tmp;
|
||||
tmp.touch("iquote/only_here.h");
|
||||
|
||||
// Layout: [iquote | (no angled) | (no system)]
|
||||
SearchConfig config;
|
||||
config.dirs.push_back({tmp.path("iquote")});
|
||||
config.angled_start_idx = 1;
|
||||
config.system_start_idx = 1;
|
||||
|
||||
DirListingCache dir_cache;
|
||||
|
||||
// <only_here.h> should NOT find it — only in quoted dir.
|
||||
auto result = resolve_include("only_here.h", true, "", false, 0, config, dir_cache);
|
||||
EXPECT_FALSE(result.has_value());
|
||||
}
|
||||
|
||||
TEST_CASE(QuotedSearchesAllDirs) {
|
||||
TempDir tmp;
|
||||
tmp.touch("sys/deep.h", "// system");
|
||||
|
||||
// Layout: [iquote | idir | sys]
|
||||
SearchConfig config;
|
||||
config.dirs.push_back({tmp.path("iquote")});
|
||||
config.dirs.push_back({tmp.path("idir")});
|
||||
config.dirs.push_back({tmp.path("sys")});
|
||||
config.angled_start_idx = 1;
|
||||
config.system_start_idx = 2;
|
||||
|
||||
DirListingCache dir_cache;
|
||||
|
||||
// "deep.h" is only in system dir, but quoted search goes through all.
|
||||
auto result = resolve_include("deep.h", false, "", false, 0, config, dir_cache);
|
||||
ASSERT_TRUE(result.has_value());
|
||||
EXPECT_TRUE(llvm::sys::fs::equivalent(result->path, tmp.path("sys/deep.h")));
|
||||
}
|
||||
|
||||
TEST_CASE(AngledBeforeSystem) {
|
||||
TempDir tmp;
|
||||
tmp.touch("idir/priority.h", "// angled");
|
||||
tmp.touch("sys/priority.h", "// system");
|
||||
|
||||
SearchConfig config;
|
||||
config.dirs.push_back({tmp.path("idir")}); // 0: Angled
|
||||
config.dirs.push_back({tmp.path("sys")}); // 1: System
|
||||
config.angled_start_idx = 0;
|
||||
config.system_start_idx = 1;
|
||||
|
||||
DirListingCache dir_cache;
|
||||
|
||||
// <priority.h> should find in Angled (index 0) before System (index 1).
|
||||
auto result = resolve_include("priority.h", true, "", false, 0, config, dir_cache);
|
||||
ASSERT_TRUE(result.has_value());
|
||||
EXPECT_TRUE(llvm::sys::fs::equivalent(result->path, tmp.path("idir/priority.h")));
|
||||
EXPECT_EQ(result->found_dir_idx, 0u);
|
||||
}
|
||||
|
||||
TEST_CASE(AfterSearchedLast) {
|
||||
TempDir tmp;
|
||||
tmp.touch("after/fallback.h", "// after");
|
||||
|
||||
// Layout: [| /angled | /sys | /after]
|
||||
SearchConfig config;
|
||||
config.dirs.push_back({tmp.path("angled")});
|
||||
config.dirs.push_back({tmp.path("sys")});
|
||||
config.dirs.push_back({tmp.path("after")});
|
||||
config.angled_start_idx = 0;
|
||||
config.system_start_idx = 1;
|
||||
config.after_start_idx = 2;
|
||||
|
||||
DirListingCache dir_cache;
|
||||
|
||||
// <fallback.h> not in angled or sys, found in after.
|
||||
auto result = resolve_include("fallback.h", true, "", false, 0, config, dir_cache);
|
||||
ASSERT_TRUE(result.has_value());
|
||||
EXPECT_TRUE(llvm::sys::fs::equivalent(result->path, tmp.path("after/fallback.h")));
|
||||
EXPECT_EQ(result->found_dir_idx, 2u);
|
||||
}
|
||||
|
||||
TEST_CASE(IncludeNextPropagatesIdx) {
|
||||
TempDir tmp;
|
||||
tmp.touch("dir0/limits.h", "// local");
|
||||
tmp.touch("dir1/limits.h", "// system1");
|
||||
tmp.touch("dir2/limits.h", "// system2");
|
||||
|
||||
SearchConfig config;
|
||||
config.dirs.push_back({tmp.path("dir0")});
|
||||
config.dirs.push_back({tmp.path("dir1")});
|
||||
config.dirs.push_back({tmp.path("dir2")});
|
||||
config.angled_start_idx = 0;
|
||||
config.system_start_idx = 1;
|
||||
|
||||
DirListingCache dir_cache;
|
||||
|
||||
// File found at dir1 (index 1) does #include_next <limits.h>
|
||||
auto result = resolve_include("limits.h", true, "", true, 1, config, dir_cache);
|
||||
ASSERT_TRUE(result.has_value());
|
||||
// Should skip dirs 0-1, find in dir2.
|
||||
EXPECT_TRUE(llvm::sys::fs::equivalent(result->path, tmp.path("dir2/limits.h")));
|
||||
EXPECT_EQ(result->found_dir_idx, 2u);
|
||||
}
|
||||
|
||||
// TODO: add tests for:
|
||||
// - #include_next crossing segment boundaries (angled→system)
|
||||
// - #include_next at last search dir (should return nullopt)
|
||||
// - Relative paths with .. components ("../sibling/header.h")
|
||||
// - ResolvedSearchConfig overload (the production hot path)
|
||||
|
||||
}; // TEST_SUITE(IncludeResolver)
|
||||
|
||||
} // namespace
|
||||
} // namespace clice::testing
|
||||
@@ -6,6 +6,8 @@ namespace {
|
||||
|
||||
TEST_SUITE(Scan) {
|
||||
|
||||
// === scan() tests ===
|
||||
|
||||
TEST_CASE(BasicIncludes) {
|
||||
auto result = scan(R"(
|
||||
#include <vector>
|
||||
@@ -15,8 +17,10 @@ int x = 1;
|
||||
|
||||
ASSERT_EQ(result.includes.size(), 2u);
|
||||
EXPECT_EQ(result.includes[0].path, "vector");
|
||||
EXPECT_TRUE(result.includes[0].is_angled);
|
||||
EXPECT_FALSE(result.includes[0].conditional);
|
||||
EXPECT_EQ(result.includes[1].path, "foo/bar.h");
|
||||
EXPECT_FALSE(result.includes[1].is_angled);
|
||||
EXPECT_FALSE(result.includes[1].conditional);
|
||||
EXPECT_TRUE(result.module_name.empty());
|
||||
}
|
||||
@@ -71,6 +75,7 @@ export module my.module;
|
||||
EXPECT_FALSE(result.need_preprocess);
|
||||
ASSERT_EQ(result.includes.size(), 1u);
|
||||
EXPECT_EQ(result.includes[0].path, "header.h");
|
||||
EXPECT_TRUE(result.includes[0].is_angled);
|
||||
}
|
||||
|
||||
TEST_CASE(ModulePartition) {
|
||||
@@ -128,6 +133,8 @@ int main() {
|
||||
|
||||
EXPECT_TRUE(result.includes.empty());
|
||||
EXPECT_TRUE(result.module_name.empty());
|
||||
EXPECT_FALSE(result.is_interface_unit);
|
||||
EXPECT_FALSE(result.need_preprocess);
|
||||
}
|
||||
|
||||
// === scan_precise() tests ===
|
||||
|
||||
Reference in New Issue
Block a user