Unit + integration coverage for the session's work: incremental project graph/index, pcode encoder, unreachable/dead-branch, equiv/cross-TU, div-by-zero/sign-conversion/int-truncation, use-after-free, quality-precision, scan-router profile, source TU runner, detector plan, and the eval fixtures.
419 lines
19 KiB
C++
419 lines
19 KiB
C++
#include <clang/Tooling/CompilationDatabase.h>
|
|
|
|
#include <algorithm>
|
|
#include <catch2/catch_test_macros.hpp>
|
|
#include <chrono>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <nlohmann/json.hpp>
|
|
#include <string>
|
|
#include <tuple>
|
|
#include <vector>
|
|
|
|
#include "fold/cli/project_graph_orchestrator.hpp"
|
|
#include "fold/cli/source_call_graph.hpp"
|
|
#include "fold/config/configuration_profile.hpp"
|
|
#include "fold/frontend/comp_db.hpp"
|
|
#include "fold/index/store.hpp"
|
|
#include "fold/ir/call_graph.hpp"
|
|
|
|
namespace {
|
|
|
|
struct TempDir {
|
|
std::filesystem::path path;
|
|
|
|
TempDir() {
|
|
const auto stamp = std::chrono::steady_clock::now().time_since_epoch().count();
|
|
path = std::filesystem::temp_directory_path() /
|
|
("fold-incremental-project-graph-" + std::to_string(stamp));
|
|
std::filesystem::create_directories(path);
|
|
}
|
|
|
|
~TempDir() { std::filesystem::remove_all(path); }
|
|
};
|
|
|
|
void writeFile(const std::filesystem::path& path, const std::string& content) {
|
|
std::ofstream output(path);
|
|
REQUIRE(output.good());
|
|
output << content;
|
|
}
|
|
|
|
void writeCompileDb(const std::filesystem::path& directory,
|
|
const std::vector<std::filesystem::path>& files,
|
|
const std::string& extra_flags = {},
|
|
const std::filesystem::path& command_directory = {}) {
|
|
nlohmann::json commands = nlohmann::json::array();
|
|
for (const auto& file : files) {
|
|
nlohmann::json arguments = {"clang++", "-std=c++20"};
|
|
if (!extra_flags.empty())
|
|
arguments.push_back(extra_flags);
|
|
arguments.push_back("-c");
|
|
arguments.push_back(file.string());
|
|
commands.push_back(
|
|
{{"directory", (command_directory.empty() ? directory : command_directory).string()},
|
|
{"file", file.string()},
|
|
{"arguments", arguments}});
|
|
}
|
|
writeFile(directory / "compile_commands.json", commands.dump(2));
|
|
}
|
|
|
|
std::unique_ptr<clang::tooling::CompilationDatabase> loadDb(const std::filesystem::path& path) {
|
|
std::string error;
|
|
auto database = fold::loadCompilationDatabase(path.string(), error);
|
|
INFO(error);
|
|
REQUIRE(database);
|
|
return database;
|
|
}
|
|
|
|
struct GraphSnapshot {
|
|
std::vector<std::tuple<std::string, std::string, std::string, std::string, unsigned, bool, bool,
|
|
std::vector<std::string>, std::vector<std::string>>>
|
|
nodes;
|
|
std::vector<std::tuple<std::string, std::string, int, double>> edges;
|
|
|
|
bool operator==(const GraphSnapshot&) const = default;
|
|
};
|
|
|
|
GraphSnapshot snapshot(const fold::ir::ProjectCallGraph& graph) {
|
|
GraphSnapshot result;
|
|
for (const auto& node : graph.nodes()) {
|
|
result.nodes.emplace_back(
|
|
node.id, node.qualified_name, node.source_file, node.body_hash, node.scc_id,
|
|
node.is_defined, node.escapes,
|
|
std::vector<std::string>(node.callees.begin(), node.callees.end()),
|
|
std::vector<std::string>(node.callers.begin(), node.callers.end()));
|
|
}
|
|
for (const auto& edge : graph.edges()) {
|
|
result.edges.emplace_back(edge.caller_id, edge.callee_id, static_cast<int>(edge.kind),
|
|
edge.confidence);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
fold::ir::ProjectCallGraph exhaustive(clang::tooling::CompilationDatabase& database,
|
|
const std::vector<std::string>& files) {
|
|
fold::ir::ProjectCallGraph graph;
|
|
fold::cli::buildProjectCallGraph(database, files, graph);
|
|
return graph;
|
|
}
|
|
|
|
void checkMatchesExhaustive(clang::tooling::CompilationDatabase& database,
|
|
const std::vector<std::string>& files,
|
|
const fold::ir::ProjectCallGraph& incremental) {
|
|
const auto expected = exhaustive(database, files);
|
|
CHECK(snapshot(incremental) == snapshot(expected));
|
|
}
|
|
|
|
std::size_t countNamed(const fold::ir::ProjectCallGraph& graph, const std::string& name) {
|
|
const auto nodes = graph.nodes();
|
|
return static_cast<std::size_t>(
|
|
std::count_if(nodes.begin(), nodes.end(),
|
|
[&name](const auto& node) { return node.qualified_name == name; }));
|
|
}
|
|
|
|
std::size_t countNamePrefix(const fold::ir::ProjectCallGraph& graph, const std::string& prefix) {
|
|
const auto nodes = graph.nodes();
|
|
return static_cast<std::size_t>(
|
|
std::count_if(nodes.begin(), nodes.end(), [&prefix](const auto& node) {
|
|
return node.qualified_name.starts_with(prefix);
|
|
}));
|
|
}
|
|
|
|
} // namespace
|
|
|
|
TEST_CASE("warm project graph reuses unchanged TU contributions without parsing",
|
|
"[project-graph][incremental][warm]") {
|
|
TempDir directory;
|
|
const auto header = directory.path / "shared.hpp";
|
|
const auto caller = directory.path / "caller.cpp";
|
|
const auto callee = directory.path / "callee.cpp";
|
|
writeFile(header, "static int header_local(){ return 3; }\n");
|
|
writeFile(caller, "#include \"shared.hpp\"\n"
|
|
"int cross_tu(int); int overloaded(int); double overloaded(double);\n"
|
|
"int entry(){ return cross_tu(1) + overloaded(1) + header_local(); }\n");
|
|
writeFile(callee, "#include \"shared.hpp\"\n"
|
|
"int cross_tu(int x){ return x + header_local(); }\n"
|
|
"int overloaded(int x){ return x; }\n"
|
|
"double overloaded(double x){ return x; }\n");
|
|
writeCompileDb(directory.path, {caller, callee});
|
|
auto database = loadDb(directory.path);
|
|
const std::vector<std::string> files{caller.string(), callee.string()};
|
|
fold::index::ProjectIndex index((directory.path / "project.sqlite").string());
|
|
REQUIRE(index.isOpen());
|
|
|
|
fold::ir::ProjectCallGraph cold_graph;
|
|
const auto cold = fold::cli::assembleProjectCallGraph(*database, files, &index, cold_graph);
|
|
REQUIRE_FALSE(cold.exhaustive_fallback);
|
|
CHECK(cold.parsed_tus == 2);
|
|
checkMatchesExhaustive(*database, files, cold_graph);
|
|
|
|
fold::ir::ProjectCallGraph warm_graph;
|
|
const auto warm = fold::cli::assembleProjectCallGraph(*database, files, &index, warm_graph);
|
|
REQUIRE_FALSE(warm.exhaustive_fallback);
|
|
CHECK(warm.parsed_tus == 0);
|
|
CHECK(warm.reused_tus == 2);
|
|
CHECK(countNamed(warm_graph, "overloaded") == 2);
|
|
CHECK(countNamePrefix(warm_graph, "header_local@") == 2);
|
|
checkMatchesExhaustive(*database, files, warm_graph);
|
|
}
|
|
|
|
TEST_CASE("reachability-only assembly reuses unresolved contributions without proving closure",
|
|
"[project-graph][incremental][production-reachability]") {
|
|
TempDir directory;
|
|
const auto source = directory.path / "indirect.cpp";
|
|
writeFile(source, "using Callback=void(*)(); static void target(){} "
|
|
"static void invoke(Callback callback){ callback(); } "
|
|
"int main(){ invoke(target); return 0; }\n");
|
|
writeCompileDb(directory.path, {source});
|
|
auto database = loadDb(directory.path);
|
|
const std::vector<std::string> files = {source.string()};
|
|
fold::index::ProjectIndex index((directory.path / "index.sqlite").string());
|
|
REQUIRE(index.isOpen());
|
|
|
|
fold::ir::ProjectCallGraph graph;
|
|
fold::cli::ProjectGraphEvidence cold_evidence;
|
|
const auto cold = fold::cli::assembleProjectCallGraph(*database, files, &index, graph, nullptr,
|
|
&cold_evidence, true);
|
|
CHECK_FALSE(cold.exhaustive_fallback);
|
|
CHECK(cold.parsed_tus == 1);
|
|
CHECK(cold_evidence.complete);
|
|
CHECK_FALSE(cold_evidence.roots.closed_world);
|
|
CHECK(cold_evidence.roots.has_unresolved_dynamic_calls);
|
|
|
|
fold::cli::ProjectGraphEvidence warm_evidence;
|
|
const auto warm = fold::cli::assembleProjectCallGraph(*database, files, &index, graph, nullptr,
|
|
&warm_evidence, true);
|
|
CHECK_FALSE(warm.exhaustive_fallback);
|
|
CHECK(warm.parsed_tus == 0);
|
|
CHECK(warm.reused_tus == 1);
|
|
CHECK(warm_evidence.complete);
|
|
CHECK_FALSE(warm_evidence.roots.closed_world);
|
|
}
|
|
|
|
TEST_CASE("production callback roots survive cross-TU declaration and definition merging",
|
|
"[project-graph][incremental][production-roots]") {
|
|
TempDir directory;
|
|
const auto callback = directory.path / "callback.cpp";
|
|
const auto entry = directory.path / "entry.cpp";
|
|
writeFile(callback, "void callback() {}\n");
|
|
writeFile(entry, "void register_callback(void (*)()); void callback(); "
|
|
"int main(){ register_callback(callback); return 0; }\n");
|
|
writeCompileDb(directory.path, {callback, entry});
|
|
auto database = loadDb(directory.path);
|
|
const std::vector<std::string> files = {callback.string(), entry.string()};
|
|
fold::index::ProjectIndex index((directory.path / "index.sqlite").string());
|
|
REQUIRE(index.isOpen());
|
|
|
|
fold::ir::ProjectCallGraph graph;
|
|
const auto cold = fold::cli::assembleProjectCallGraph(*database, files, &index, graph);
|
|
REQUIRE_FALSE(cold.exhaustive_fallback);
|
|
CHECK(cold.production_roots == 2);
|
|
CHECK(cold.root_inventory_closed_world);
|
|
checkMatchesExhaustive(*database, files, graph);
|
|
|
|
const auto loaded = index.loadTranslationUnitContributions();
|
|
REQUIRE(loaded.status == fold::index::ContributionLoadStatus::Ok);
|
|
std::vector<fold::analysis::ProductionRootRecord> roots;
|
|
for (const auto& contribution : loaded.contributions)
|
|
roots.insert(roots.end(), contribution.production_roots.begin(),
|
|
contribution.production_roots.end());
|
|
const auto callback_root = std::ranges::find_if(roots, [](const auto& root) {
|
|
return root.qualified_name == "callback" &&
|
|
root.kind == fold::analysis::ProductionRootKind::RegisteredCallback;
|
|
});
|
|
REQUIRE(callback_root != roots.end());
|
|
const auto* callback_node = graph.getNode(callback_root->function_usr);
|
|
REQUIRE(callback_node != nullptr);
|
|
CHECK(callback_node->is_defined);
|
|
CHECK(callback_node->escapes);
|
|
|
|
const auto warm = fold::cli::assembleProjectCallGraph(*database, files, &index, graph);
|
|
CHECK(warm.parsed_tus == 0);
|
|
CHECK(warm.reused_tus == 2);
|
|
CHECK(warm.production_roots == 2);
|
|
}
|
|
|
|
TEST_CASE("configuration profiles persist projected callsite guards and invalidate reuse",
|
|
"[project-graph][incremental][configuration]") {
|
|
TempDir directory;
|
|
const auto source = directory.path / "guarded.cpp";
|
|
writeFile(source, "bool feature; int runtime; void enabled(){} void disabled(){} "
|
|
"void entry(){ if (feature) enabled(); else disabled(); "
|
|
"if (feature + runtime) enabled(); if (runtime) disabled(); }\n");
|
|
writeCompileDb(directory.path, {source});
|
|
auto database = loadDb(directory.path);
|
|
const std::vector<std::string> files = {source.string()};
|
|
fold::index::ProjectIndex index((directory.path / "index.sqlite").string());
|
|
REQUIRE(index.isOpen());
|
|
|
|
const auto disabled = fold::config::parseConfigurationProfileText(
|
|
R"({"version":1,"entrypoints":["entry"],"assumptions":[{"kind":"global","selector":"feature","value":false}]})");
|
|
REQUIRE(disabled);
|
|
fold::ir::ProjectCallGraph graph;
|
|
fold::cli::ProjectGraphEvidence cold_evidence;
|
|
const auto cold = fold::cli::assembleProjectCallGraph(*database, files, &index, graph,
|
|
&*disabled.profile, &cold_evidence);
|
|
REQUIRE_FALSE(cold.exhaustive_fallback);
|
|
CHECK(cold.parsed_tus == 1);
|
|
CHECK(cold.guarded_callsites == 4);
|
|
CHECK(cold.projected_guards == 2);
|
|
CHECK(cold.unconditional_guards == 1);
|
|
CHECK(cold.unknown_guards == 1);
|
|
CHECK(cold.production_roots == 1);
|
|
CHECK(cold.test_roots == 0);
|
|
CHECK(cold.conditional_roots == 0);
|
|
CHECK(cold.unresolved_explicit_roots == 0);
|
|
CHECK(cold.root_inventory_closed_world);
|
|
CHECK(cold_evidence.complete);
|
|
CHECK(cold_evidence.roots.closed_world);
|
|
CHECK(cold_evidence.guarded_calls.size() == 4);
|
|
checkMatchesExhaustive(*database, files, graph);
|
|
|
|
const auto loaded = index.loadTranslationUnitContributions();
|
|
REQUIRE(loaded.status == fold::index::ContributionLoadStatus::Ok);
|
|
REQUIRE(loaded.contributions.size() == 1);
|
|
REQUIRE(loaded.contributions.front().guarded_calls.size() == 4);
|
|
REQUIRE(loaded.contributions.front().production_roots.size() == 1);
|
|
CHECK(loaded.contributions.front().production_roots.front().kind ==
|
|
fold::analysis::ProductionRootKind::Explicit);
|
|
std::size_t projected = 0;
|
|
std::size_t unconditional = 0;
|
|
std::size_t unknown = 0;
|
|
for (const auto& callsite : loaded.contributions.front().guarded_calls) {
|
|
const auto serialized = fold::ir::configGuardToJson(callsite.guard).dump();
|
|
CHECK(serialized.find("feature") == std::string::npos);
|
|
projected += callsite.guard.status == fold::ir::ConfigGuardStatus::Projected;
|
|
unconditional += callsite.guard.status == fold::ir::ConfigGuardStatus::Unconditional;
|
|
unknown += callsite.guard.status == fold::ir::ConfigGuardStatus::Unknown;
|
|
}
|
|
CHECK(projected == 2);
|
|
CHECK(unconditional == 1);
|
|
CHECK(unknown == 1);
|
|
|
|
fold::cli::ProjectGraphEvidence warm_evidence;
|
|
const auto warm = fold::cli::assembleProjectCallGraph(*database, files, &index, graph,
|
|
&*disabled.profile, &warm_evidence);
|
|
CHECK(warm.parsed_tus == 0);
|
|
CHECK(warm.reused_tus == 1);
|
|
CHECK(warm_evidence.complete);
|
|
CHECK(warm_evidence.guarded_calls == cold_evidence.guarded_calls);
|
|
CHECK(warm_evidence.roots.production_roots == cold_evidence.roots.production_roots);
|
|
|
|
const auto enabled = fold::config::parseConfigurationProfileText(
|
|
R"({"version":1,"entrypoints":["entry"],"assumptions":[{"kind":"global","selector":"feature","value":true}]})");
|
|
REQUIRE(enabled);
|
|
const auto changed =
|
|
fold::cli::assembleProjectCallGraph(*database, files, &index, graph, &*enabled.profile);
|
|
CHECK(changed.parsed_tus == 1);
|
|
CHECK(changed.reused_tus == 0);
|
|
checkMatchesExhaustive(*database, files, graph);
|
|
}
|
|
|
|
TEST_CASE("incremental project graph matches exhaustive after edits dependencies flags and removal",
|
|
"[project-graph][incremental][invalidate]") {
|
|
TempDir directory;
|
|
const auto header = directory.path / "value.hpp";
|
|
const auto caller = directory.path / "caller.cpp";
|
|
const auto callee = directory.path / "callee.cpp";
|
|
writeFile(header, "#define HEADER_VALUE 1\n");
|
|
writeFile(caller, "#include \"value.hpp\"\nint callee(int); "
|
|
"int caller(){ return callee(HEADER_VALUE); }\n");
|
|
writeFile(callee, "#include \"value.hpp\"\nint callee(int x){ return x + HEADER_VALUE; }\n");
|
|
writeCompileDb(directory.path, {caller, callee}, "-include" + header.string());
|
|
auto database = loadDb(directory.path);
|
|
std::vector<std::string> files{caller.string(), callee.string()};
|
|
fold::index::ProjectIndex index((directory.path / "project.sqlite").string());
|
|
|
|
fold::ir::ProjectCallGraph graph;
|
|
REQUIRE(fold::cli::assembleProjectCallGraph(*database, files, &index, graph).parsed_tus == 2);
|
|
|
|
writeFile(callee,
|
|
"#include \"value.hpp\"\nint callee(int x){ return x + HEADER_VALUE + 1; }\n");
|
|
auto edited = fold::cli::assembleProjectCallGraph(*database, files, &index, graph);
|
|
CHECK(edited.parsed_tus == 1);
|
|
CHECK(edited.dirty_tus == 1);
|
|
checkMatchesExhaustive(*database, files, graph);
|
|
|
|
writeFile(header, "#define HEADER_VALUE 2\n");
|
|
auto header_changed = fold::cli::assembleProjectCallGraph(*database, files, &index, graph);
|
|
CHECK(header_changed.parsed_tus == 2);
|
|
CHECK(header_changed.dirty_tus == 2);
|
|
checkMatchesExhaustive(*database, files, graph);
|
|
|
|
writeCompileDb(directory.path, {caller, callee}, "-DGRAPH_FLAG=1");
|
|
database = loadDb(directory.path);
|
|
auto flags_changed = fold::cli::assembleProjectCallGraph(*database, files, &index, graph);
|
|
CHECK(flags_changed.parsed_tus == 2);
|
|
CHECK(flags_changed.dirty_tus == 2);
|
|
checkMatchesExhaustive(*database, files, graph);
|
|
|
|
files = {caller.string()};
|
|
auto removed = fold::cli::assembleProjectCallGraph(*database, files, &index, graph);
|
|
CHECK(removed.parsed_tus == 0);
|
|
CHECK(removed.removed_tus == 1);
|
|
checkMatchesExhaustive(*database, files, graph);
|
|
const auto loaded = index.loadTranslationUnitContributions();
|
|
REQUIRE(loaded.status == fold::index::ContributionLoadStatus::Ok);
|
|
REQUIRE(loaded.contributions.size() == 1);
|
|
CHECK(loaded.contributions.front().fingerprint.source_path ==
|
|
std::filesystem::weakly_canonical(caller).string());
|
|
}
|
|
|
|
TEST_CASE("compile-command working directory invalidates TU contribution reuse",
|
|
"[project-graph][incremental][invalidate]") {
|
|
TempDir directory;
|
|
const auto first_working_directory = directory.path / "first";
|
|
const auto second_working_directory = directory.path / "second";
|
|
std::filesystem::create_directories(first_working_directory);
|
|
std::filesystem::create_directories(second_working_directory);
|
|
const auto source = directory.path / "entry.cpp";
|
|
writeFile(source, "int entry(){ return 1; }\n");
|
|
const std::vector<std::filesystem::path> source_paths{source};
|
|
const std::vector<std::string> files{source.string()};
|
|
|
|
writeCompileDb(directory.path, source_paths, {}, first_working_directory);
|
|
auto database = loadDb(directory.path);
|
|
fold::index::ProjectIndex index((directory.path / "project.sqlite").string());
|
|
fold::ir::ProjectCallGraph graph;
|
|
REQUIRE(fold::cli::assembleProjectCallGraph(*database, files, &index, graph).parsed_tus == 1);
|
|
|
|
writeCompileDb(directory.path, source_paths, {}, second_working_directory);
|
|
database = loadDb(directory.path);
|
|
const auto changed = fold::cli::assembleProjectCallGraph(*database, files, &index, graph);
|
|
CHECK(changed.dirty_tus == 1);
|
|
CHECK(changed.parsed_tus == 1);
|
|
}
|
|
|
|
TEST_CASE("unresolved virtual function-pointer evidence resolves in contribution merge",
|
|
"[project-graph][incremental][resolution]") {
|
|
TempDir directory;
|
|
const auto source = directory.path / "dynamic.cpp";
|
|
writeFile(source, "struct Base { virtual int run(); };\n"
|
|
"struct Derived : Base { int run() override { return 1; } };\n"
|
|
"int virtual_call(Base* value){ return value->run(); }\n"
|
|
"int indirect(int (*function)(double)){ return function(1.0); }\n");
|
|
writeCompileDb(directory.path, {source});
|
|
auto database = loadDb(directory.path);
|
|
const std::vector<std::string> files{source.string()};
|
|
fold::index::ProjectIndex index((directory.path / "project.sqlite").string());
|
|
|
|
fold::ir::ProjectCallGraph cold_graph;
|
|
const auto cold = fold::cli::assembleProjectCallGraph(*database, files, &index, cold_graph);
|
|
REQUIRE_FALSE(cold.exhaustive_fallback);
|
|
CHECK(cold.parsed_tus == 1);
|
|
CHECK(cold_graph.edgeCount(fold::ir::CallGraphEdgeKind::Conservative) >= 1);
|
|
CHECK(cold_graph.edgeCount(fold::ir::CallGraphEdgeKind::Unknown) >= 1);
|
|
checkMatchesExhaustive(*database, files, cold_graph);
|
|
|
|
// The warm run reuses the stored contribution: unresolved calls are
|
|
// resolved from the persisted cross-TU resolution facts instead of
|
|
// re-parsing every TU through the exhaustive fallback.
|
|
fold::ir::ProjectCallGraph warm_graph;
|
|
const auto warm = fold::cli::assembleProjectCallGraph(*database, files, &index, warm_graph);
|
|
REQUIRE_FALSE(warm.exhaustive_fallback);
|
|
CHECK(warm.parsed_tus == 0);
|
|
CHECK(warm.reused_tus == 1);
|
|
CHECK(warm_graph.edgeCount(fold::ir::CallGraphEdgeKind::Conservative) >= 1);
|
|
CHECK(warm_graph.edgeCount(fold::ir::CallGraphEdgeKind::Unknown) >= 1);
|
|
checkMatchesExhaustive(*database, files, warm_graph);
|
|
}
|