fold/tests/unit/test_detector_plan.cpp
trvon fd4a68dfff
test: add coverage for project graph, pcode encoder, and detector behaviors
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.
2026-08-16 16:46:07 -06:00

145 lines
6.1 KiB
C++

#include <catch2/catch_test_macros.hpp>
#include <set>
#include <string>
#include <vector>
#include "fold/cli/detector_plan.hpp"
#include "fold/cli/scan_options.hpp"
#include "fold/cli/scan_runner.hpp"
namespace {
std::set<std::string> enabledNames(const std::vector<std::string>& requested) {
const auto plan = fold::cli::makeSourceDetectorPlan(requested);
const auto names = plan.enabledProfileNames();
return {names.begin(), names.end()};
}
bool contains(const std::set<std::string>& names, const char* name) {
return names.find(name) != names.end();
}
} // namespace
TEST_CASE("source detector descriptors are complete and uniquely named", "[detector-plan]") {
using Category = fold::cli::SourceDetectorCategory;
std::set<std::string> names;
std::size_t default_count = 0;
std::size_t security_count = 0;
std::size_t quality_count = 0;
std::size_t redundancy_count = 0;
std::size_t composite_security_count = 0;
for (const auto& descriptor : fold::cli::sourceDetectorDescriptors()) {
REQUIRE(descriptor.name != nullptr);
REQUIRE(descriptor.enabled != nullptr);
REQUIRE(names.insert(descriptor.name).second);
default_count += descriptor.default_enabled ? 1U : 0U;
security_count += descriptor.category == Category::Security ? 1U : 0U;
quality_count += descriptor.category == Category::Quality ? 1U : 0U;
redundancy_count += descriptor.category == Category::Redundancy ? 1U : 0U;
composite_security_count += descriptor.composite_security_walk ? 1U : 0U;
}
REQUIRE(names.size() == fold::cli::sourceDetectorDescriptors().size);
REQUIRE(default_count == 5);
REQUIRE(security_count == 17);
REQUIRE(quality_count == 8);
REQUIRE(redundancy_count == 7);
// composite_security_walk is orthogonal to the Security category: a subset
// of Security detectors run in the shared symbolic walk.
REQUIRE(composite_security_count == 9);
REQUIRE(security_count + quality_count + redundancy_count ==
fold::cli::sourceDetectorDescriptors().size);
}
TEST_CASE("source detector plan preserves default and group selection", "[detector-plan]") {
const auto defaults = enabledNames({});
REQUIRE(defaults == std::set<std::string>{"always-condition", "class-dup", "clone", "dead",
"dead-statement", "equiv"});
const auto redundancy = enabledNames({"redundancy"});
REQUIRE(contains(redundancy, "clone"));
REQUIRE(contains(redundancy, "dead"));
REQUIRE(contains(redundancy, "equiv"));
REQUIRE(contains(redundancy, "class-dup"));
REQUIRE(contains(redundancy, "always-condition"));
REQUIRE(contains(redundancy, "unreachable"));
REQUIRE(contains(redundancy, "redundant-store"));
REQUIRE_FALSE(contains(redundancy, "div-by-zero"));
const auto security = enabledNames({"security"});
REQUIRE(contains(security, "div-by-zero"));
REQUIRE(contains(security, "null-deref"));
REQUIRE(contains(security, "int-overflow"));
REQUIRE(contains(security, "int-truncation"));
REQUIRE(contains(security, "sign-conversion"));
REQUIRE(contains(security, "oob"));
REQUIRE(contains(security, "taint-track"));
REQUIRE(contains(security, "sql-injection"));
REQUIRE(contains(security, "uninit-read"));
REQUIRE(contains(security, "memory-leak"));
// The security group is now the full Security category — including the
// standalone passes the old security_group flag omitted.
REQUIRE(contains(security, "double-free"));
REQUIRE(contains(security, "format-string"));
REQUIRE(contains(security, "use-after-free"));
REQUIRE(contains(security, "weak-crypto"));
REQUIRE(contains(security, "deprecated-banned"));
REQUIRE_FALSE(contains(security, "clone"));
const auto quality = enabledNames({"quality"});
REQUIRE(contains(quality, "dead-statement"));
REQUIRE(contains(quality, "complexity"));
REQUIRE(contains(quality, "unused-function"));
REQUIRE(contains(quality, "production-reachability"));
REQUIRE(contains(quality, "use-after-move"));
REQUIRE(contains(quality, "iterator-invalidation"));
REQUIRE_FALSE(contains(quality, "null-deref"));
REQUIRE_FALSE(contains(quality, "clone"));
const auto dead_code = enabledNames({"dead-code"});
REQUIRE(contains(dead_code, "dead"));
REQUIRE(contains(dead_code, "dead-statement"));
REQUIRE(contains(dead_code, "unused-function"));
REQUIRE(contains(dead_code, "production-reachability"));
REQUIRE(contains(dead_code, "always-condition"));
REQUIRE(contains(dead_code, "unreachable"));
REQUIRE_FALSE(contains(dead_code, "clone"));
}
TEST_CASE("source detector descriptors drive profiles and composite counts", "[detector-plan]") {
auto plan = fold::cli::makeSourceDetectorPlan({"div-by-zero", "double-free", "format-string"});
REQUIRE(plan.div_by_zero);
REQUIRE(plan.double_free);
REQUIRE(plan.format_string);
REQUIRE(plan.enabledSecurityDetectorCount() == 2);
const auto names = plan.enabledProfileNames();
REQUIRE(names == std::vector<std::string>{"div-by-zero", "format-string", "double-free"});
REQUIRE(fold::cli::detectorEnabled({"all"}, "future-detector"));
REQUIRE(fold::cli::detectorEnabled({"future-detector"}, "future-detector"));
REQUIRE_FALSE(fold::cli::detectorEnabled({}, "future-detector"));
}
TEST_CASE("language IR safety detector selection gates Rust and Swift symbolic passes",
"[detector-plan][language-ir]") {
auto div_only = fold::cli::requestedLanguageIrSafetyDetectors({"div-by-zero"});
REQUIRE(div_only.any());
REQUIRE_FALSE(div_only.null_deref);
REQUIRE(div_only.div_by_zero);
REQUIRE_FALSE(div_only.int_overflow);
REQUIRE_FALSE(div_only.oob);
auto overflow_only = fold::cli::requestedLanguageIrSafetyDetectors({"int-overflow"});
REQUIRE(overflow_only.any());
REQUIRE(overflow_only.int_overflow);
auto oob_alias = fold::cli::requestedLanguageIrSafetyDetectors({"oob-access"});
REQUIRE(oob_alias.any());
REQUIRE(oob_alias.oob);
auto unrelated = fold::cli::requestedLanguageIrSafetyDetectors({"clone"});
REQUIRE_FALSE(unrelated.any());
}