// Pin the provenance contract on CrossTUCache. Phase 2 of the // cross-TU cache persistence design: insertFromTU(tu_path, ...) // records which TU contributed each entry so the per-TU save path // can write exactly those entries (and only those) to each TU's // on-disk snapshot. The existing flat `insert(name, props)` API // remains for backward compatibility but does not track provenance. #include #include #include #include #include "fold/detect/cross_tu_cache.hpp" #include "fold/detect/cross_tu_cache_io.hpp" namespace { fold::detect::CalleeProperties propsWith(std::initializer_list derefed, bool returns_null = false) { fold::detect::CalleeProperties p; p.derefed_params.insert(derefed.begin(), derefed.end()); p.returns_null = returns_null; return p; } bool hasEntry(const std::vector& vec, const std::string& name) { return std::any_of(vec.begin(), vec.end(), [&](const auto& e) { return e.name == name; }); } } // namespace TEST_CASE("CrossTUCache provenance: insertFromTU populates the flat lookup", "[cross-tu-cache][provenance][insert]") { using namespace fold::detect; CrossTUCache::instance().clear(); CrossTUCache::instance().insertFromTU("/abs/a.c", "foo", propsWith({0, 1})); const auto* p = CrossTUCache::instance().lookup("foo"); REQUIRE(p != nullptr); REQUIRE(p->derefed_params.count(0) == 1); REQUIRE(p->derefed_params.count(1) == 1); CrossTUCache::instance().clear(); } TEST_CASE("CrossTUCache provenance: entriesForTU returns only this TU's contributions", "[cross-tu-cache][provenance][per-tu]") { using namespace fold::detect; CrossTUCache::instance().clear(); CrossTUCache::instance().insertFromTU("/abs/a.c", "alpha", propsWith({0})); CrossTUCache::instance().insertFromTU("/abs/a.c", "beta", propsWith({1})); CrossTUCache::instance().insertFromTU("/abs/b.c", "gamma", propsWith({2})); auto a_entries = CrossTUCache::instance().entriesForTU("/abs/a.c"); auto b_entries = CrossTUCache::instance().entriesForTU("/abs/b.c"); REQUIRE(a_entries.size() == 2); REQUIRE(hasEntry(a_entries, "alpha")); REQUIRE(hasEntry(a_entries, "beta")); REQUIRE(b_entries.size() == 1); REQUIRE(hasEntry(b_entries, "gamma")); CrossTUCache::instance().clear(); } TEST_CASE("CrossTUCache provenance: same name from two TUs records in both provenance lists", "[cross-tu-cache][provenance][collision]") { using namespace fold::detect; CrossTUCache::instance().clear(); // Static fn `helper` defined separately in TU-A and TU-B: collision // at root qualified name is real in curl. Both TUs' caches must list // it so re-priming either TU restores its own contribution. CrossTUCache::instance().insertFromTU("/abs/a.c", "helper", propsWith({0})); CrossTUCache::instance().insertFromTU("/abs/b.c", "helper", propsWith({1}, true)); auto a = CrossTUCache::instance().entriesForTU("/abs/a.c"); auto b = CrossTUCache::instance().entriesForTU("/abs/b.c"); REQUIRE(a.size() == 1); REQUIRE(a[0].name == "helper"); REQUIRE(a[0].props.derefed_params == std::set{0}); REQUIRE_FALSE(a[0].props.returns_null); REQUIRE(b.size() == 1); REQUIRE(b[0].name == "helper"); REQUIRE(b[0].props.derefed_params == std::set{1}); REQUIRE(b[0].props.returns_null); CrossTUCache::instance().clear(); } TEST_CASE("CrossTUCache provenance: flat lookup merges across TUs", "[cross-tu-cache][provenance][merge]") { using namespace fold::detect; // Even though provenance is tracked per-TU, the flat lookup() still // returns the merged view across TUs — detectors querying a callee's // properties see the union of what's been observed everywhere. CrossTUCache::instance().clear(); CrossTUCache::instance().insertFromTU("/abs/a.c", "helper", propsWith({0})); CrossTUCache::instance().insertFromTU("/abs/b.c", "helper", propsWith({1}, true)); const auto* p = CrossTUCache::instance().lookup("helper"); REQUIRE(p != nullptr); REQUIRE(p->derefed_params == std::set{0, 1}); REQUIRE(p->returns_null); CrossTUCache::instance().clear(); } TEST_CASE("CrossTUCache provenance: same TU + same name calls merge within the TU", "[cross-tu-cache][provenance][dedupe]") { using namespace fold::detect; CrossTUCache::instance().clear(); CrossTUCache::instance().insertFromTU("/abs/a.c", "foo", propsWith({0})); CrossTUCache::instance().insertFromTU("/abs/a.c", "foo", propsWith({1})); auto a = CrossTUCache::instance().entriesForTU("/abs/a.c"); REQUIRE(a.size() == 1); REQUIRE(a[0].name == "foo"); REQUIRE(a[0].props.derefed_params == std::set{0, 1}); CrossTUCache::instance().clear(); } TEST_CASE("CrossTUCache provenance: trackedTUs returns the contributing-TU set", "[cross-tu-cache][provenance][tracked]") { using namespace fold::detect; CrossTUCache::instance().clear(); CrossTUCache::instance().insertFromTU("/abs/a.c", "x", {}); CrossTUCache::instance().insertFromTU("/abs/b.c", "y", {}); CrossTUCache::instance().insertFromTU("/abs/a.c", "z", {}); // dup TU auto tus = CrossTUCache::instance().trackedTUs(); std::sort(tus.begin(), tus.end()); REQUIRE(tus == std::vector{"/abs/a.c", "/abs/b.c"}); CrossTUCache::instance().clear(); } TEST_CASE("CrossTUCache provenance: clear wipes both flat lookup and provenance", "[cross-tu-cache][provenance][clear]") { using namespace fold::detect; CrossTUCache::instance().insertFromTU("/abs/a.c", "foo", propsWith({0})); CrossTUCache::instance().clear(); REQUIRE(CrossTUCache::instance().lookup("foo") == nullptr); REQUIRE(CrossTUCache::instance().entriesForTU("/abs/a.c").empty()); REQUIRE(CrossTUCache::instance().trackedTUs().empty()); } TEST_CASE("CrossTUCache provenance: entriesForTU on unknown TU returns empty", "[cross-tu-cache][provenance][miss]") { using namespace fold::detect; CrossTUCache::instance().clear(); auto entries = CrossTUCache::instance().entriesForTU("/abs/never-primed.c"); REQUIRE(entries.empty()); } TEST_CASE("CrossTUCache provenance: legacy insert() does not register TU provenance", "[cross-tu-cache][provenance][legacy]") { using namespace fold::detect; CrossTUCache::instance().clear(); CrossTUCache::instance().insert("legacy_fn", propsWith({0})); // Flat lookup works. REQUIRE(CrossTUCache::instance().lookup("legacy_fn") != nullptr); // But no TU is tracked. REQUIRE(CrossTUCache::instance().trackedTUs().empty()); CrossTUCache::instance().clear(); } TEST_CASE("CrossTUCache provenance: insertFromTU ignores empty tu_path or empty name", "[cross-tu-cache][provenance][validation]") { using namespace fold::detect; CrossTUCache::instance().clear(); CrossTUCache::instance().insertFromTU("", "foo", propsWith({0})); CrossTUCache::instance().insertFromTU("/abs/a.c", "", propsWith({0})); REQUIRE(CrossTUCache::instance().lookup("foo") == nullptr); REQUIRE(CrossTUCache::instance().trackedTUs().empty()); }