fold/tests/unit/test_cross_tu_cache_provenance.cpp

165 lines
7.2 KiB
C++

// 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 <algorithm>
#include <catch2/catch_test_macros.hpp>
#include <string>
#include <vector>
#include "fold/detect/cross_tu_cache.hpp"
#include "fold/detect/cross_tu_cache_io.hpp"
namespace {
fold::detect::CalleeProperties propsWith(std::initializer_list<unsigned> 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<fold::detect::CrossTUCacheEntry>& 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<unsigned>{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<unsigned>{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<unsigned>{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<unsigned>{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<std::string>{"/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());
}