309 lines
13 KiB
C++
309 lines
13 KiB
C++
// Round-trip and edge-case tests for CrossTUCache on-disk
|
|
// serialization. Phase 1 of cross-TU cache persistence; the
|
|
// serializer must round-trip every CalleeProperties field and
|
|
// reject mismatched schema versions / corrupt files gracefully.
|
|
|
|
#include <unistd.h>
|
|
|
|
#include <catch2/catch_test_macros.hpp>
|
|
#include <cstdint>
|
|
#include <cstdio>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include "fold/detect/cross_tu_cache.hpp"
|
|
#include "fold/detect/cross_tu_cache_io.hpp"
|
|
|
|
namespace {
|
|
|
|
std::filesystem::path makeTempFile(const std::string& tag) {
|
|
auto p = std::filesystem::temp_directory_path() /
|
|
("fold-cross-tu-cache-test-" + tag + "-" + std::to_string(::getpid()) + ".json");
|
|
std::filesystem::remove(p);
|
|
return p;
|
|
}
|
|
|
|
fold::detect::CalleeProperties makeProps(std::initializer_list<unsigned> derefed,
|
|
std::initializer_list<unsigned> divisor,
|
|
std::initializer_list<unsigned> bounds,
|
|
std::initializer_list<unsigned> arithmetic,
|
|
bool returns_null) {
|
|
fold::detect::CalleeProperties p;
|
|
p.derefed_params.insert(derefed.begin(), derefed.end());
|
|
p.divisor_params.insert(divisor.begin(), divisor.end());
|
|
p.bounds_params.insert(bounds.begin(), bounds.end());
|
|
p.arithmetic_params.insert(arithmetic.begin(), arithmetic.end());
|
|
p.returns_null = returns_null;
|
|
return p;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
TEST_CASE("CrossTUCache I/O: round-trip preserves every field", "[cross-tu-cache][io][roundtrip]") {
|
|
using namespace fold::detect;
|
|
CrossTUCacheSnapshot snap;
|
|
snap.tu_path = "/abs/path/to/altsvc.c";
|
|
snap.source_mtime_ns = 1747938472123456789LL;
|
|
snap.flags_hash = "ab12cd34ef56";
|
|
snap.entries.push_back({"Curl_altsvc_init", makeProps({0, 1}, {}, {}, {2}, false)});
|
|
snap.entries.push_back({"Curl_altsvc_cleanup", makeProps({0}, {}, {}, {}, true)});
|
|
snap.entries.push_back({"Curl_altsvc_parse_line", makeProps({1, 2}, {3}, {4}, {}, false)});
|
|
|
|
auto path = makeTempFile("roundtrip");
|
|
REQUIRE(saveSnapshot(path, snap));
|
|
auto loaded = loadSnapshot(path);
|
|
REQUIRE(loaded.has_value());
|
|
|
|
REQUIRE(loaded->tu_path == snap.tu_path);
|
|
REQUIRE(loaded->source_mtime_ns == snap.source_mtime_ns);
|
|
REQUIRE(loaded->flags_hash == snap.flags_hash);
|
|
REQUIRE(loaded->entries.size() == snap.entries.size());
|
|
for (size_t i = 0; i < snap.entries.size(); ++i) {
|
|
REQUIRE(loaded->entries[i].name == snap.entries[i].name);
|
|
REQUIRE(loaded->entries[i].props.derefed_params == snap.entries[i].props.derefed_params);
|
|
REQUIRE(loaded->entries[i].props.divisor_params == snap.entries[i].props.divisor_params);
|
|
REQUIRE(loaded->entries[i].props.bounds_params == snap.entries[i].props.bounds_params);
|
|
REQUIRE(loaded->entries[i].props.arithmetic_params ==
|
|
snap.entries[i].props.arithmetic_params);
|
|
REQUIRE(loaded->entries[i].props.returns_null == snap.entries[i].props.returns_null);
|
|
}
|
|
std::filesystem::remove(path);
|
|
}
|
|
|
|
TEST_CASE("CrossTUCache I/O: empty entries vector round-trips", "[cross-tu-cache][io][edge]") {
|
|
using namespace fold::detect;
|
|
CrossTUCacheSnapshot snap{"/abs/empty.c", 0, "0", {}};
|
|
auto path = makeTempFile("empty");
|
|
REQUIRE(saveSnapshot(path, snap));
|
|
auto loaded = loadSnapshot(path);
|
|
REQUIRE(loaded.has_value());
|
|
REQUIRE(loaded->entries.empty());
|
|
std::filesystem::remove(path);
|
|
}
|
|
|
|
TEST_CASE("CrossTUCache I/O: load returns nullopt on missing file", "[cross-tu-cache][io][miss]") {
|
|
auto missing =
|
|
std::filesystem::temp_directory_path() / "fold-cross-tu-cache-does-not-exist.json";
|
|
std::filesystem::remove(missing);
|
|
auto loaded = fold::detect::loadSnapshot(missing);
|
|
REQUIRE_FALSE(loaded.has_value());
|
|
}
|
|
|
|
TEST_CASE("CrossTUCache I/O: load returns nullopt on malformed JSON",
|
|
"[cross-tu-cache][io][malformed]") {
|
|
auto path = makeTempFile("malformed");
|
|
{
|
|
std::ofstream out(path);
|
|
out << "{not even valid json,,,";
|
|
}
|
|
auto loaded = fold::detect::loadSnapshot(path);
|
|
REQUIRE_FALSE(loaded.has_value());
|
|
std::filesystem::remove(path);
|
|
}
|
|
|
|
TEST_CASE("CrossTUCache I/O: load returns nullopt on schema version mismatch",
|
|
"[cross-tu-cache][io][version]") {
|
|
auto path = makeTempFile("version");
|
|
{
|
|
std::ofstream out(path);
|
|
// version 999 is in the future; loader must reject without crashing.
|
|
out << R"({"version": 999, "tu_path": "/x.c", "source_mtime_ns": 0, "flags_hash": "h", "entries": []})";
|
|
}
|
|
auto loaded = fold::detect::loadSnapshot(path);
|
|
REQUIRE_FALSE(loaded.has_value());
|
|
std::filesystem::remove(path);
|
|
}
|
|
|
|
TEST_CASE("CrossTUCache I/O: load returns nullopt on missing required field",
|
|
"[cross-tu-cache][io][partial]") {
|
|
auto path = makeTempFile("partial");
|
|
{
|
|
std::ofstream out(path);
|
|
// Missing source_mtime_ns — loader must reject, not silently default.
|
|
out << R"({"version": 1, "tu_path": "/x.c", "flags_hash": "h", "entries": []})";
|
|
}
|
|
auto loaded = fold::detect::loadSnapshot(path);
|
|
REQUIRE_FALSE(loaded.has_value());
|
|
std::filesystem::remove(path);
|
|
}
|
|
|
|
TEST_CASE("CrossTUCache I/O: save creates parent directory if missing",
|
|
"[cross-tu-cache][io][mkdir]") {
|
|
using namespace fold::detect;
|
|
auto nested = std::filesystem::temp_directory_path() /
|
|
("fold-cross-tu-cache-nested-" + std::to_string(::getpid())) / "sub" /
|
|
"deeper.json";
|
|
std::filesystem::remove_all(nested.parent_path().parent_path());
|
|
|
|
CrossTUCacheSnapshot snap{"/x.c", 1, "h", {{"foo", {}}}};
|
|
REQUIRE(saveSnapshot(nested, snap));
|
|
REQUIRE(std::filesystem::exists(nested));
|
|
auto loaded = loadSnapshot(nested);
|
|
REQUIRE(loaded.has_value());
|
|
REQUIRE(loaded->entries.size() == 1);
|
|
|
|
std::filesystem::remove_all(nested.parent_path().parent_path());
|
|
}
|
|
|
|
TEST_CASE("CrossTUCache I/O: canonicalizeFlagsHash is order-stable for -I/-isystem",
|
|
"[cross-tu-cache][io][flags-hash]") {
|
|
using namespace fold::detect;
|
|
// Two argv differing only in -I/-isystem order must hash identically;
|
|
// the same with a real semantic change must NOT.
|
|
std::vector<std::string> a{"-DFOO", "-I/a", "-I/b", "-isystem", "/sys/x", "-O2"};
|
|
std::vector<std::string> b{"-DFOO", "-I/b", "-I/a", "-isystem", "/sys/x", "-O2"};
|
|
std::vector<std::string> c{"-DFOO", "-I/a", "-I/b", "-isystem", "/sys/y", "-O2"};
|
|
|
|
auto ha = canonicalizeFlagsHash(a);
|
|
auto hb = canonicalizeFlagsHash(b);
|
|
auto hc = canonicalizeFlagsHash(c);
|
|
|
|
REQUIRE_FALSE(ha.empty());
|
|
REQUIRE(ha == hb);
|
|
REQUIRE(ha != hc);
|
|
}
|
|
|
|
TEST_CASE("CrossTUCache I/O: canonicalizeFlagsHash strips output-only flags",
|
|
"[cross-tu-cache][io][flags-hash]") {
|
|
using namespace fold::detect;
|
|
// -o, -MD, -MF, -MMD, -MT affect build output only, not semantics;
|
|
// they must not change the hash.
|
|
std::vector<std::string> with_out{"-DFOO", "-I/a", "-o", "x.o", "-MD",
|
|
"-MF", "x.d", "-MMD", "-MT", "x.o"};
|
|
std::vector<std::string> without{"-DFOO", "-I/a"};
|
|
REQUIRE(canonicalizeFlagsHash(with_out) == canonicalizeFlagsHash(without));
|
|
}
|
|
|
|
TEST_CASE("CrossTUCache I/O: canonicalizeFlagsHash distinguishes -D macros",
|
|
"[cross-tu-cache][io][flags-hash]") {
|
|
using namespace fold::detect;
|
|
std::vector<std::string> a{"-DFOO=1", "-I/a"};
|
|
std::vector<std::string> b{"-DFOO=2", "-I/a"};
|
|
REQUIRE(canonicalizeFlagsHash(a) != canonicalizeFlagsHash(b));
|
|
}
|
|
|
|
TEST_CASE("CrossTUCache I/O: defaultCrossTUCacheDir uses XDG_CACHE_HOME when set",
|
|
"[cross-tu-cache][io][cache-dir]") {
|
|
using namespace fold::detect;
|
|
auto sentinel = std::filesystem::temp_directory_path() /
|
|
("fold-xdg-sentinel-" + std::to_string(::getpid()));
|
|
::setenv("XDG_CACHE_HOME", sentinel.string().c_str(), 1);
|
|
auto dir = defaultCrossTUCacheDir("/abs/path/to/compile_commands.json");
|
|
REQUIRE_FALSE(dir.empty());
|
|
REQUIRE(dir.string().find(sentinel.string()) == 0);
|
|
REQUIRE(dir.string().find("/fold/cross_tu/") != std::string::npos);
|
|
::unsetenv("XDG_CACHE_HOME");
|
|
}
|
|
|
|
TEST_CASE("CrossTUCache I/O: defaultCrossTUCacheDir is stable for same db path",
|
|
"[cross-tu-cache][io][cache-dir]") {
|
|
using namespace fold::detect;
|
|
auto a = defaultCrossTUCacheDir("/abs/path/to/compile_commands.json");
|
|
auto b = defaultCrossTUCacheDir("/abs/path/to/compile_commands.json");
|
|
auto c = defaultCrossTUCacheDir("/other/path/compile_commands.json");
|
|
REQUIRE(a == b);
|
|
REQUIRE(a != c);
|
|
}
|
|
|
|
TEST_CASE("CrossTUCache I/O: tuCacheFile returns <cache_dir>/<hash>.json",
|
|
"[cross-tu-cache][io][cache-dir]") {
|
|
using namespace fold::detect;
|
|
auto base = std::filesystem::path("/tmp/some-cache");
|
|
auto a = tuCacheFile(base, "/abs/altsvc.c");
|
|
auto b = tuCacheFile(base, "/abs/altsvc.c");
|
|
auto c = tuCacheFile(base, "/abs/urlapi.c");
|
|
REQUIRE(a == b);
|
|
REQUIRE(a != c);
|
|
REQUIRE(a.parent_path() == base);
|
|
REQUIRE(a.extension() == ".json");
|
|
}
|
|
|
|
TEST_CASE("CrossTUCache I/O: tuCacheFile on empty cache_dir returns empty",
|
|
"[cross-tu-cache][io][cache-dir]") {
|
|
REQUIRE(fold::detect::tuCacheFile({}, "/abs/x.c").empty());
|
|
}
|
|
|
|
TEST_CASE("CrossTUCache I/O: fileMtimeNs returns >0 for existing file",
|
|
"[cross-tu-cache][io][mtime]") {
|
|
auto path = std::filesystem::temp_directory_path() /
|
|
("fold-mtime-" + std::to_string(::getpid()) + ".txt");
|
|
{
|
|
std::ofstream out(path);
|
|
out << "x";
|
|
}
|
|
auto t = fold::detect::fileMtimeNs(path.string());
|
|
REQUIRE(t > 0);
|
|
std::filesystem::remove(path);
|
|
}
|
|
|
|
TEST_CASE("CrossTUCache I/O: fileMtimeNs returns 0 for missing file",
|
|
"[cross-tu-cache][io][mtime]") {
|
|
auto missing = std::filesystem::temp_directory_path() / "fold-mtime-does-not-exist.txt";
|
|
std::filesystem::remove(missing);
|
|
REQUIRE(fold::detect::fileMtimeNs(missing.string()) == 0);
|
|
}
|
|
|
|
TEST_CASE("CrossTUCache I/O v2: call-graph snapshot round-trips",
|
|
"[cross-tu-cache][io][callgraph]") {
|
|
using namespace fold::detect;
|
|
CrossTUCacheSnapshot snap;
|
|
snap.tu_path = "/abs/altsvc.c";
|
|
snap.source_mtime_ns = 100;
|
|
snap.flags_hash = "h";
|
|
snap.call_graph.functions.push_back(
|
|
{"Curl_altsvc_init",
|
|
"/abs/altsvc.c",
|
|
true,
|
|
{{"malloc", "/sys/stdlib.h", false}, {"Curl_strdup", "/abs/strdup.c", true}}});
|
|
snap.call_graph.functions.push_back(
|
|
{"Curl_altsvc_cleanup", "/abs/altsvc.c", true, {{"free", "/sys/stdlib.h", false}}});
|
|
|
|
auto path = makeTempFile("callgraph-roundtrip");
|
|
REQUIRE(saveSnapshot(path, snap));
|
|
auto loaded = loadSnapshot(path);
|
|
REQUIRE(loaded.has_value());
|
|
|
|
REQUIRE(loaded->call_graph.functions.size() == 2);
|
|
REQUIRE(loaded->call_graph.functions[0].name == "Curl_altsvc_init");
|
|
REQUIRE(loaded->call_graph.functions[0].source_file == "/abs/altsvc.c");
|
|
REQUIRE(loaded->call_graph.functions[0].is_defined);
|
|
REQUIRE(loaded->call_graph.functions[0].callees.size() == 2);
|
|
REQUIRE(loaded->call_graph.functions[0].callees[0].name == "malloc");
|
|
REQUIRE_FALSE(loaded->call_graph.functions[0].callees[0].is_defined);
|
|
REQUIRE(loaded->call_graph.functions[0].callees[1].name == "Curl_strdup");
|
|
REQUIRE(loaded->call_graph.functions[0].callees[1].is_defined);
|
|
REQUIRE(loaded->call_graph.functions[1].name == "Curl_altsvc_cleanup");
|
|
REQUIRE(loaded->call_graph.functions[1].callees.size() == 1);
|
|
std::filesystem::remove(path);
|
|
}
|
|
|
|
TEST_CASE("CrossTUCache I/O v2: empty call-graph round-trips", "[cross-tu-cache][io][callgraph]") {
|
|
using namespace fold::detect;
|
|
CrossTUCacheSnapshot snap;
|
|
snap.tu_path = "/abs/header-only.c";
|
|
snap.source_mtime_ns = 1;
|
|
snap.flags_hash = "h";
|
|
// entries empty, call_graph empty
|
|
auto path = makeTempFile("callgraph-empty");
|
|
REQUIRE(saveSnapshot(path, snap));
|
|
auto loaded = loadSnapshot(path);
|
|
REQUIRE(loaded.has_value());
|
|
REQUIRE(loaded->call_graph.functions.empty());
|
|
std::filesystem::remove(path);
|
|
}
|
|
|
|
TEST_CASE("CrossTUCache I/O v2: v1 schema is rejected (now requires v2)",
|
|
"[cross-tu-cache][io][callgraph][version]") {
|
|
// v1 files written by Phase 3 v1 lack the call_graph block. v2 loader
|
|
// must reject them so they get re-primed and re-saved with v2 shape.
|
|
auto path = makeTempFile("v1-reject");
|
|
{
|
|
std::ofstream out(path);
|
|
out << R"({"version": 1, "tu_path": "/x.c", "source_mtime_ns": 0, "flags_hash": "h", "entries": []})";
|
|
}
|
|
auto loaded = fold::detect::loadSnapshot(path);
|
|
REQUIRE_FALSE(loaded.has_value());
|
|
std::filesystem::remove(path);
|
|
}
|