#include #include #include #include #include #include #include #include #include "fold/cli/source_call_graph.hpp" #include "fold/index/store.hpp" namespace { fold::index::TranslationUnitGraphContribution contribution(std::string source, std::string command_hash = "command-v1", std::string header_hash = "header-v1") { return { .fingerprint = {.source_path = source, .compile_command_hash = command_hash, .source_hash = "source-v1", .toolchain_hash = "clang-v1", .dependencies = {{source, "source-v1", fold::index::DependencyKind::Source}, {"shared.hpp", header_hash, fold::index::DependencyKind::Header}}}, .functions = {{"usr:entry:" + source, "entry", source, "entry-v1", true, false, 1, {}, {}}, {"usr:shared", "shared", "shared.hpp", "body-v1", true, false, 0, {}, {}}}, .edges = {{"usr:entry:" + source, "usr:shared", fold::index::EdgeKind::Conservative, 0.75}}, .unresolved_calls = {{"callsite:" + source, "usr:entry:" + source, fold::index::UnresolvedCallKind::Virtual, "void(Base::*)()", "usr:base", {"family:a", "family:b"}}}, .guarded_calls = {{"guarded:" + source, "usr:entry:" + source, "usr:shared", fold::index::EdgeKind::Direct, fold::ir::ConfigGuard::projected( fold::ir::ConfigGuardExpr::boolSymbol(std::string(64, 'a')))}}, .production_roots = {{"usr:entry:" + source, "entry", source, fold::analysis::ProductionRootKind::Main, source, 1, 1, true, false}}, }; } std::filesystem::path temporaryDatabase() { const auto stamp = std::chrono::steady_clock::now().time_since_epoch().count(); return std::filesystem::temp_directory_path() / ("fold-project-index-" + std::to_string(stamp) + ".sqlite"); } void executeSql(const std::filesystem::path& path, const char* sql) { sqlite3* database = nullptr; REQUIRE(sqlite3_open(path.string().c_str(), &database) == SQLITE_OK); char* error = nullptr; const int status = sqlite3_exec(database, sql, nullptr, nullptr, &error); INFO(std::string(error == nullptr ? "" : error)); CHECK(status == SQLITE_OK); sqlite3_free(error); sqlite3_close(database); } } // namespace TEST_CASE("project index round-trips versioned TU contributions", "[project-index][tu-contribution]") { fold::index::ProjectIndex index(":memory:"); REQUIRE(index.isOpen()); const auto expected = contribution("a.cpp"); REQUIRE(index.replaceTranslationUnitContribution(expected)); REQUIRE(index.replaceTranslationUnitContribution(expected)); const auto loaded = index.loadTranslationUnitContributions(); REQUIRE(loaded.status == fold::index::ContributionLoadStatus::Ok); REQUIRE(loaded.contributions.size() == 1); CHECK(loaded.contributions.front() == expected); } TEST_CASE("project index TU replacement tracks source header and flag changes atomically", "[project-index][tu-contribution][replace]") { fold::index::ProjectIndex index(":memory:"); REQUIRE(index.isOpen()); REQUIRE(index.replaceTranslationUnitContribution(contribution("a.cpp"))); auto changed = contribution("a.cpp", "command-with-DENABLE", "header-v2"); changed.fingerprint.source_hash = "source-v2"; changed.functions.erase(changed.functions.begin()); changed.edges.clear(); changed.unresolved_calls.clear(); changed.production_roots.clear(); REQUIRE(index.replaceTranslationUnitContribution(changed)); auto loaded = index.loadTranslationUnitContributions(); REQUIRE(loaded.contributions.size() == 1); CHECK(loaded.contributions.front() == changed); auto invalid = changed; invalid.functions.push_back(invalid.functions.front()); CHECK_FALSE(index.replaceTranslationUnitContribution(invalid)); loaded = index.loadTranslationUnitContributions(); REQUIRE(loaded.contributions.size() == 1); CHECK(loaded.contributions.front() == changed); } TEST_CASE("project index preserves shared header ownership across TU removal", "[project-index][tu-contribution][shared-header]") { fold::index::ProjectIndex index(":memory:"); REQUIRE(index.replaceTranslationUnitContribution(contribution("a.cpp"))); auto second = contribution("b.cpp", "command-v2", "header-v2"); const auto shared = std::find_if(second.functions.begin(), second.functions.end(), [](const auto& function) { return function.usr == "usr:shared"; }); REQUIRE(shared != second.functions.end()); shared->body_hash = "body-v2"; REQUIRE(index.replaceTranslationUnitContribution(second)); auto loaded = index.loadTranslationUnitContributions(); REQUIRE(loaded.contributions.size() == 2); for (const auto& item : loaded.contributions) { CHECK(std::any_of(item.functions.begin(), item.functions.end(), [](const auto& function) { return function.usr == "usr:shared"; })); } REQUIRE(index.removeTranslationUnitContribution("a.cpp")); loaded = index.loadTranslationUnitContributions(); REQUIRE(loaded.contributions.size() == 1); CHECK(loaded.contributions.front().fingerprint.source_path == "b.cpp"); const auto remaining_shared = std::find_if(loaded.contributions.front().functions.begin(), loaded.contributions.front().functions.end(), [](const auto& function) { return function.usr == "usr:shared"; }); REQUIRE(remaining_shared != loaded.contributions.front().functions.end()); CHECK(remaining_shared->body_hash == "body-v2"); } TEST_CASE("project index self-heals missing contribution schema metadata", "[project-index][tu-contribution][schema]") { const auto path = temporaryDatabase(); { fold::index::ProjectIndex index(path.string()); REQUIRE(index.replaceTranslationUnitContribution(contribution("a.cpp"))); } executeSql(path, "DELETE FROM graph_contribution_meta WHERE key='schema_version';"); { fold::index::ProjectIndex index(path.string()); REQUIRE(index.isOpen()); // Stale graph tables are dropped and recreated at the current schema, // so the index reopens writable instead of failing closed forever. CHECK(index.replaceTranslationUnitContribution(contribution("b.cpp"))); CHECK(index.loadTranslationUnitContributions().status == fold::index::ContributionLoadStatus::Ok); } std::filesystem::remove(path); } TEST_CASE("project index self-heals future contribution schemas", "[project-index][tu-contribution][schema]") { const auto path = temporaryDatabase(); { fold::index::ProjectIndex index(path.string()); REQUIRE(index.isOpen()); REQUIRE(index.replaceTranslationUnitContribution(contribution("a.cpp"))); } executeSql(path, "UPDATE graph_contribution_meta SET value='999' " "WHERE key='schema_version';"); { fold::index::ProjectIndex index(path.string()); REQUIRE(index.isOpen()); // A future schema invalidates every stored contribution; the cache // resets itself to the current schema instead of refusing all writes. CHECK(index.replaceTranslationUnitContribution(contribution("b.cpp"))); const auto loaded = index.loadTranslationUnitContributions(); CHECK(loaded.status == fold::index::ContributionLoadStatus::Ok); CHECK_FALSE(loaded.contributions.empty()); } std::filesystem::remove(path); } TEST_CASE("project index rejects malformed contribution rows without partial loads", "[project-index][tu-contribution][corrupt]") { const auto path = temporaryDatabase(); { fold::index::ProjectIndex index(path.string()); REQUIRE(index.replaceTranslationUnitContribution(contribution("a.cpp"))); } executeSql(path, "UPDATE graph_edges SET kind=99;"); { fold::index::ProjectIndex index(path.string()); const auto loaded = index.loadTranslationUnitContributions(); CHECK(loaded.status == fold::index::ContributionLoadStatus::Corrupt); CHECK(loaded.contributions.empty()); } std::filesystem::remove(path); } TEST_CASE("project index rejects malformed persisted production roots", "[project-index][tu-contribution][roots][corrupt]") { const auto path = temporaryDatabase(); { fold::index::ProjectIndex index(path.string()); REQUIRE(index.replaceTranslationUnitContribution(contribution("a.cpp"))); } executeSql(path, "UPDATE graph_production_roots SET production=2;"); { fold::index::ProjectIndex index(path.string()); const auto loaded = index.loadTranslationUnitContributions(); CHECK(loaded.status == fold::index::ContributionLoadStatus::Corrupt); CHECK(loaded.contributions.empty()); } std::filesystem::remove(path); } TEST_CASE("project index rejects malformed persisted configuration guards", "[project-index][tu-contribution][guard][corrupt]") { const auto path = temporaryDatabase(); { fold::index::ProjectIndex index(path.string()); REQUIRE(index.replaceTranslationUnitContribution(contribution("a.cpp"))); } executeSql(path, "UPDATE graph_guarded_calls SET guard_json='{\"status\":\"projected\"}';"); { fold::index::ProjectIndex index(path.string()); const auto loaded = index.loadTranslationUnitContributions(); CHECK(loaded.status == fold::index::ContributionLoadStatus::Corrupt); CHECK(loaded.contributions.empty()); } std::filesystem::remove(path); } TEST_CASE("project index persists USR nodes and typed uncertain edges", "[project-index]") { const auto path = temporaryDatabase(); { fold::index::ProjectIndex index(path.string()); REQUIRE(index.isOpen()); const auto stats = index.replaceTranslationUnit( "a.cpp", {{"usr:a", "a", "a.cpp", "hash-a", true, false, {}, {}, {}}, {"usr:b", "b", "a.cpp", "hash-b", true, true, 7, std::string{}, std::vector{}}}, {{"usr:a", "usr:b", fold::index::EdgeKind::Direct, 1.0}, {"usr:b", "usr:external", fold::index::EdgeKind::Unknown, 0.0}}); CHECK(stats.changed_functions == 2); CHECK(stats.reused_functions == 0); } { fold::index::ProjectIndex reopened(path.string()); REQUIRE(reopened.isOpen()); REQUIRE(reopened.functions().size() == 2); const auto edges = reopened.outgoingEdges("usr:b"); REQUIRE(edges.size() == 1); CHECK(edges[0].kind == fold::index::EdgeKind::Unknown); CHECK(edges[0].confidence == 0.0); CHECK(reopened.functions()[1].escapes); CHECK(reopened.functions()[1].scc_id == 7); } std::filesystem::remove(path); } TEST_CASE("project index round-trips unified graphs and reports warm reuse", "[project-index][graph]") { fold::index::ProjectIndex index(":memory:"); REQUIRE(index.isOpen()); fold::ir::ProjectCallGraph cold; cold.addFunction("usr:caller", "caller", "caller.cpp", true, "body-caller"); cold.addFunction("usr:callee", "callee", "callee.cpp", true, "body-callee", true); cold.addCallEdge("usr:caller", "usr:callee", fold::ir::CallGraphEdgeKind::Conservative, 0.5); cold.computeSCCs(); const std::vector> inputs = {{"file:caller.cpp", "hash-1"}, {"file:callee.cpp", "hash-2"}}; const auto first = index.replaceProjectGraph(cold, inputs); CHECK(first.changed_functions == 2); CHECK(first.reused_functions == 0); fold::ir::ProjectCallGraph loaded; REQUIRE(index.loadProjectGraph(loaded)); REQUIRE(index.matchesInputFingerprints(inputs)); CHECK(loaded.nodeCount() == 2); CHECK(loaded.edgeCount(fold::ir::CallGraphEdgeKind::Conservative) == 1); CHECK(loaded.getNode("usr:callee")->escapes); fold::ir::ProjectCallGraph reordered; reordered.addFunction("usr:callee", "callee", "callee.cpp", true, "body-callee", true); reordered.addFunction("usr:caller", "caller", "caller.cpp", true, "body-caller"); reordered.addCallEdge("usr:caller", "usr:callee", fold::ir::CallGraphEdgeKind::Conservative, 0.5); reordered.computeSCCs(); const auto warm = index.replaceProjectGraph(reordered, inputs); CHECK(warm.reused_functions == 2); CHECK(warm.changed_functions == 0); CHECK(warm.changed_usrs.empty()); fold::ir::ProjectCallGraph changed; changed.addFunction("usr:caller", "caller", "caller.cpp", true, "body-caller"); changed.addFunction("usr:callee", "callee", "callee.cpp", true, "body-callee-v2", true); changed.addCallEdge("usr:caller", "usr:callee", fold::ir::CallGraphEdgeKind::Conservative, 0.5); changed.computeSCCs(); const auto update = index.replaceProjectGraph(changed); CHECK(update.reused_functions == 1); CHECK(update.changed_functions == 1); CHECK(update.invalidated_dependents == 1); CHECK(update.changed_usrs == std::vector{"usr:callee"}); CHECK(update.invalidated_usrs == std::vector{"usr:caller"}); fold::ir::ProjectCallGraph removed; removed.addFunction("usr:caller", "caller", "caller.cpp", true, "body-caller"); removed.computeSCCs(); const auto removal = index.replaceProjectGraph(removed); CHECK(removal.removed_functions == 1); CHECK(index.functions().size() == 1); } TEST_CASE("project index corruption fails closed", "[project-index][corrupt]") { const auto path = temporaryDatabase(); { std::ofstream output(path, std::ios::binary); output << "not a sqlite database"; } fold::index::ProjectIndex index(path.string()); CHECK_FALSE(index.isOpen()); CHECK_FALSE(index.error().empty()); std::filesystem::remove(path); } TEST_CASE("project index replacement reports reuse and invalidates reverse dependents", "[project-index][incremental]") { fold::index::ProjectIndex index(":memory:"); REQUIRE(index.isOpen()); index.replaceTranslationUnit("a.cpp", {{"usr:a", "a", "a.cpp", "hash-a", true, false, {}, {}, {}}}, {{"usr:a", "usr:b", fold::index::EdgeKind::Conservative, 0.5}}); index.replaceTranslationUnit("b.cpp", {{"usr:b", "b", "b.cpp", "hash-b", true, false, {}, {}, {}}}, {{"usr:b", "usr:c", fold::index::EdgeKind::Unknown, 0.0}}); index.replaceTranslationUnit("c.cpp", {{"usr:c", "c", "c.cpp", "hash-c", true, false, {}, {}, {}}}, {}); CHECK(index.reverseDependents({"usr:c"}) == std::vector{"usr:a", "usr:b"}); const auto unchanged = index.replaceTranslationUnit( "a.cpp", {{"usr:a", "a", "a.cpp", "hash-a", true, false, {}, {}, {}}}, {{"usr:a", "usr:b", fold::index::EdgeKind::Conservative, 0.5}}); CHECK(unchanged.reused_functions == 1); CHECK(unchanged.changed_functions == 0); const auto changed = index.replaceTranslationUnit( "a.cpp", {{"usr:a", "a", "a.cpp", "hash-a2", true, false, {}, {}, {}}, {"usr:new", "new_function", "a.cpp", "hash-new", true, false, {}, {}, {}}}, {}); CHECK(changed.changed_functions == 2); CHECK(changed.reused_functions == 0); CHECK(changed.removed_functions == 0); CHECK(index.outgoingEdges("usr:a").empty()); } TEST_CASE("project index chunks reverse-dependents past sqlite compound-select limit", "[project-index][incremental][large]") { // A fresh or large incremental scan changes more than 500 USRs; the // recursive-CTE seed list then exceeds SQLite's 500-term compound SELECT // cap (SQLITE_MAX_COMPOUND_SELECT) and the update must still succeed with // the exact transitive dependents. fold::index::ProjectIndex index(":memory:"); REQUIRE(index.isOpen()); // reverseDependents walks reverse edges (callers of changed functions), so // build a fan-in closure: g_i -> f_i and h_i -> g_i. Changing the 600 f_* // bodies seeds 600 USRs and must invalidate exactly {g_*, h_*}. constexpr int kChanged = 600; constexpr int kFunctions = 3 * kChanged; const auto build_closure = [&](bool mutate_functions) { fold::ir::ProjectCallGraph graph; const auto usr = [](const char* prefix, int i) { return std::string("usr:") + prefix + std::to_string(i); }; const auto file_of = [](int i) { return i < kChanged ? "a.cpp" : "b.cpp"; }; for (int i = 0; i < kChanged; ++i) { const std::string body = mutate_functions ? "body2-" + std::to_string(i) : "body-" + std::to_string(i); graph.addFunction(usr("f", i), "f" + std::to_string(i), file_of(i), true, body); graph.addFunction(usr("g", i), "g" + std::to_string(i), file_of(i), true, "body-g-" + std::to_string(i)); graph.addFunction(usr("h", i), "h" + std::to_string(i), file_of(i), true, "body-h-" + std::to_string(i)); graph.addCallEdge(usr("g", i), usr("f", i), fold::ir::CallGraphEdgeKind::Direct, 1.0); graph.addCallEdge(usr("h", i), usr("g", i), fold::ir::CallGraphEdgeKind::Direct, 1.0); } graph.computeSCCs(); return graph; }; const std::vector> inputs = {{"file:a.cpp", "hash-1"}, {"file:b.cpp", "hash-2"}}; const auto cold = index.replaceProjectGraph(build_closure(false), inputs); CHECK(cold.changed_functions == kFunctions); CHECK(index.error().empty()); // Every function is a seed, so nothing can be a non-seed dependent. CHECK(cold.invalidated_usrs.empty()); // 600 changed functions -> 600 CTE seed terms, previously > the 500-term // compound-SELECT cap (prepare failed, error_ set, invalidated_usrs empty). const auto update = index.replaceProjectGraph(build_closure(true)); CHECK(index.error().empty()); CHECK(update.changed_functions == kChanged); CHECK(update.reused_functions == 2 * kChanged); // Dependents of {f0..f599} = {g0..g599} U {h0..h599} (multi-hop closure). REQUIRE(update.invalidated_usrs.size() == 2 * kChanged); CHECK(update.invalidated_usrs.front() == "usr:g0"); CHECK(update.invalidated_usrs.back() == "usr:h99"); CHECK(std::is_sorted(update.invalidated_usrs.begin(), update.invalidated_usrs.end())); CHECK(index.error().empty()); // Warm reuse round-trips identically and stays error-free. const auto warm = index.replaceProjectGraph(build_closure(true), inputs); CHECK(index.error().empty()); CHECK(warm.reused_functions == kFunctions); CHECK(warm.changed_functions == 0); CHECK(warm.invalidated_usrs.empty()); fold::ir::ProjectCallGraph loaded; REQUIRE(index.loadProjectGraph(loaded)); CHECK(loaded.nodeCount() == kFunctions); } TEST_CASE("contribution merge resolves unresolved calls cross-TU like the exhaustive builder", "[project-index][merge][resolution]") { using fold::index::EdgeKind; using fold::index::TranslationUnitGraphContribution; using fold::index::UnresolvedCallKind; const auto base = [](std::string usr, std::string name) { return fold::index::FunctionRecord{usr, name, "a.cpp", "body-" + usr, true, false, 0, "void (int)", std::vector{}}; }; const std::string family_key = "Base::run|void (int)"; TranslationUnitGraphContribution a; a.fingerprint = {.source_path = "a.cpp", .compile_command_hash = "cc-a", .source_hash = "sh-a", .toolchain_hash = "tc", .configuration_profile_hash = fold::index::kNoConfigurationProfileHash, .schema_version = fold::index::kGraphContributionSchemaVersion, .dependencies = {{"a.cpp", "sh-a", fold::index::DependencyKind::Source}}}; auto caller_record = base("usr:caller", "caller"); caller_record.canonical_signature = "void (void)"; a.functions.push_back(caller_record); a.unresolved_calls.push_back({.callsite_id = "a.cpp:1:1", .caller_usr = "usr:caller", .kind = UnresolvedCallKind::Virtual, .canonical_signature = {}, .direct_target_usr = "usr:Base::run", .override_family_keys = {family_key}}); a.unresolved_calls.push_back({.callsite_id = "a.cpp:2:1", .caller_usr = "usr:caller", .kind = UnresolvedCallKind::Indirect, .canonical_signature = "void (int)", .direct_target_usr = {}, .override_family_keys = {}}); TranslationUnitGraphContribution b; b.fingerprint = {.source_path = "b.cpp", .compile_command_hash = "cc-b", .source_hash = "sh-b", .toolchain_hash = "tc", .configuration_profile_hash = fold::index::kNoConfigurationProfileHash, .schema_version = fold::index::kGraphContributionSchemaVersion, .dependencies = {{"b.cpp", "sh-b", fold::index::DependencyKind::Source}}}; auto base_run = base("usr:Base::run", "Base::run"); base_run.override_family_keys = {family_key}; auto derived_run = base("usr:Derived::run", "Derived::run"); derived_run.override_family_keys = {family_key}; auto handler = base("usr:handler", "handler"); handler.canonical_signature = "void (int)"; b.functions = {base_run, derived_run, handler}; // Strict merge (the non-reachability path that previously forced the // exhaustive fallback): resolution matches both family targets and the // signature target; nothing is refused. fold::ir::ProjectCallGraph graph; std::string reason; REQUIRE(fold::cli::mergeTranslationUnitGraphContributions({a, b}, graph, reason, /*allow_unresolved_calls=*/false)); CHECK(reason.empty()); CHECK(graph.edgeCount(fold::ir::CallGraphEdgeKind::Conservative) == 3); CHECK(graph.edgeCount(fold::ir::CallGraphEdgeKind::Unknown) == 0); const auto callees = graph.getCalleeIds("usr:caller"); REQUIRE(callees.size() == 3); CHECK(std::find(callees.begin(), callees.end(), "usr:Base::run") != callees.end()); CHECK(std::find(callees.begin(), callees.end(), "usr:Derived::run") != callees.end()); CHECK(std::find(callees.begin(), callees.end(), "usr:handler") != callees.end()); // A genuinely unresolvable indirect call degrades to an Unknown synthetic // edge, exactly like the exhaustive builder. TranslationUnitGraphContribution lone; lone.fingerprint = {.source_path = "c.cpp", .compile_command_hash = "cc-c", .source_hash = "sh-c", .toolchain_hash = "tc", .configuration_profile_hash = fold::index::kNoConfigurationProfileHash, .schema_version = fold::index::kGraphContributionSchemaVersion, .dependencies = {{"c.cpp", "sh-c", fold::index::DependencyKind::Source}}}; lone.functions.push_back(base("usr:lone", "lone")); lone.unresolved_calls.push_back({.callsite_id = "c.cpp:1:1", .caller_usr = "usr:lone", .kind = UnresolvedCallKind::Indirect, .canonical_signature = "int (void)", .direct_target_usr = {}, .override_family_keys = {}}); fold::ir::ProjectCallGraph partial; REQUIRE(fold::cli::mergeTranslationUnitGraphContributions({lone}, partial, reason, /*allow_unresolved_calls=*/false)); CHECK(partial.edgeCount(fold::ir::CallGraphEdgeKind::Unknown) == 1); CHECK(partial.unresolvedNodeCount() == 1); // allow_unresolved_calls=true (reachability-only path) keeps dropping // unresolved evidence without synthetic nodes. fold::ir::ProjectCallGraph loose; REQUIRE(fold::cli::mergeTranslationUnitGraphContributions({lone}, loose, reason, /*allow_unresolved_calls=*/true)); CHECK(loose.edgeCount(fold::ir::CallGraphEdgeKind::Unknown) == 0); CHECK(loose.edgeCount() == 0); } TEST_CASE("contribution merge tolerates same-USR spelling variants like the exhaustive builder", "[project-index][merge][usr-conflict]") { using fold::index::TranslationUnitGraphContribution; // Real-world case (fold optimization loop iteration 3): libc++ inline // template/inline functions appear in multiple TU contributions with the // same semantic USR but spelling-variant qualified names (std:: vs // std::__1) and sometimes an empty body hash (TU-dependent instantiation). // ProjectCallGraph::addFunction — the exhaustive builder this merge must // mirror — merges those duplicates; the merge must not refuse evidence // the fallback tolerates. const auto make_contribution = [](const std::string& path, const std::string& qualified_name, const std::string& body_hash) { TranslationUnitGraphContribution c; c.fingerprint = { .source_path = path, .compile_command_hash = "cc-" + path, .source_hash = "sh-" + path, .toolchain_hash = "tc", .configuration_profile_hash = fold::index::kNoConfigurationProfileHash, .schema_version = fold::index::kGraphContributionSchemaVersion, .dependencies = {{path, "sh-" + path, fold::index::DependencyKind::Source}}}; c.functions.push_back({"usr:libcpp-fn", qualified_name, "/sdk/usr/include/c++/v1/header.h", body_hash, true, false, 0, {}, {}}); c.functions.push_back( {"usr:caller", "caller", path, "body-caller", true, false, 0, {}, {}}); return c; }; auto a = make_contribution("a.cpp", "std::__1::operator<<", "7249926610571155715"); auto b = make_contribution("b.cpp", "std::operator<<", "7249926610571155715"); auto c = make_contribution("c.cpp", "std::operator<<", ""); fold::ir::ProjectCallGraph graph; std::string reason; REQUIRE(fold::cli::mergeTranslationUnitGraphContributions({a, b, c}, graph, reason, /*allow_unresolved_calls=*/false)); CHECK(reason.empty()); const auto* merged = graph.getNode("usr:libcpp-fn"); REQUIRE(merged != nullptr); CHECK(merged->is_defined); CHECK(!merged->body_hash.empty()); } TEST_CASE("contribution store round-trips cross-TU resolution facts", "[project-index][tu-contribution][resolution]") { const auto path = temporaryDatabase(); { fold::index::ProjectIndex index(path.string()); REQUIRE(index.isOpen()); fold::index::TranslationUnitGraphContribution contribution; contribution.fingerprint = {.source_path = "a.cpp", .compile_command_hash = "cc-a", .source_hash = "sh-a", .toolchain_hash = "tc", .configuration_profile_hash = fold::index::kNoConfigurationProfileHash, .schema_version = fold::index::kGraphContributionSchemaVersion, .dependencies = {}}; contribution.functions.push_back({"usr:Base::run", "Base::run", "a.cpp", "body", true, false, 0, "void (int)", {"Base::run|void (int)", "Base::run|void (long)"}}); REQUIRE(index.replaceTranslationUnitContribution(contribution)); } { fold::index::ProjectIndex index(path.string()); REQUIRE(index.isOpen()); const auto loaded = index.loadTranslationUnitContributions(); REQUIRE(loaded.status == fold::index::ContributionLoadStatus::Ok); REQUIRE(loaded.contributions.size() == 1); REQUIRE(loaded.contributions[0].functions.size() == 1); const auto& function = loaded.contributions[0].functions[0]; CHECK(function.canonical_signature == "void (int)"); CHECK(function.override_family_keys == std::vector{"Base::run|void (int)", "Base::run|void (long)"}); } std::filesystem::remove(path); }