#include #include #include #include #include #include #include #include #include #include #include #include #include "fold/detect/clone.hpp" #include "fold/frontend/ast_visitor.hpp" #include "fold/frontend/ndjson_keys.hpp" #include "fold/report/finding.hpp" #include "fold/report/sarif.hpp" namespace { // Materialize a compile_commands.json next to the fixtures at test time so the // absolute paths inside the DB resolve correctly on the host machine. Returns // the directory containing the generated DB. std::string writeCompileDb(const std::string& fixtures_dir) { namespace fs = std::filesystem; fs::path dir = fs::path(fixtures_dir); nlohmann::json db = nlohmann::json::array(); for (const auto& name : {"clones/exact.cpp", "clones/renamed.cpp", "clones/near_miss.cpp", "clones/not_a_clone.cpp"}) { fs::path file = dir / name; db.push_back({ {"directory", dir.string()}, {"command", std::string("clang++ -std=c++20 -c ") + file.string()}, {"file", file.string()}, }); } fs::path out = dir / "compile_commands.json"; std::ofstream(out) << db.dump(2); return dir.string(); } } // namespace TEST_CASE("scan emits clusters for exact / renamed / near_miss but not for not_a_clone", "[e2e]") { // Catch2 wires command-line args through its singleton; for our test we // accept the fixtures path via env or fall back to the canonical location. const char* env = std::getenv("FOLD_FIXTURES_DIR"); std::string fixtures = env ? env : "../fixtures"; if (!std::filesystem::exists(fixtures + "/clones/exact.cpp")) { // Allow running from the meson build dir. fixtures = std::filesystem::current_path().string() + "/../fixtures"; } REQUIRE(std::filesystem::exists(fixtures + "/clones/exact.cpp")); auto dir = writeCompileDb(fixtures); std::string err; auto db = clang::tooling::CompilationDatabase::autoDetectFromDirectory(dir, err); REQUIRE(db); auto files = db->getAllFiles(); clang::tooling::ClangTool tool(*db, files); auto summaries = fold::collectFunctionSummaries(tool); REQUIRE(summaries.size() >= 8); // 4 fixture files * 2 funcs each fold::CloneDetectorOptions opts; opts.min_body_stmts = 1; auto findings = fold::detectClones(summaries, opts); // Expect three clusters: exact/exact, renamed/renamed, near_miss/near_miss. REQUIRE(findings.size() == 3); // SARIF should be valid JSON with a single run and three results. std::ostringstream sarif_buf; fold::writeSarif(sarif_buf, findings); auto sarif = nlohmann::json::parse(sarif_buf.str()); REQUIRE(sarif["version"] == "2.1.0"); REQUIRE(sarif["runs"].size() == 1); REQUIRE(sarif["runs"][0]["results"].size() == 3); } TEST_CASE("SARIF keeps stable rule metadata for structural findings", "[sarif]") { fold::Finding clone; clone.id = "clone-id"; clone.kind = fold::FindingKind::Clone; clone.locations = {{"src/report/sarif.cpp", 1, 1, "clone"}}; clone.message = "clone"; clone.evidence.backend = "clang"; fold::Finding complexity; complexity.id = "complexity-id"; complexity.kind = fold::FindingKind::HighComplexity; complexity.locations = {{"src/report/sarif.cpp", 2, 1, "complexity"}}; complexity.message = "complexity"; std::ostringstream output; fold::writeSarif(output, {clone, complexity}); const auto sarif = nlohmann::json::parse(output.str()); const auto& driver = sarif["runs"][0]["tool"]["driver"]; const auto& results = sarif["runs"][0]["results"]; // fold lives only on git.trevon.dev; SARIF consumers follow this link. REQUIRE(driver["informationUri"] == "https://git.trevon.dev/trevon/fold"); REQUIRE(results[0]["ruleId"] == "fold.clone"); REQUIRE(results[0]["properties"]["backend"] == "clang"); REQUIRE(results[1]["ruleId"] == "fold.high-complexity"); const auto clone_rule = std::find_if(driver["rules"].begin(), driver["rules"].end(), [](const auto& rule) { return rule["id"] == "fold.clone"; }); REQUIRE(clone_rule != driver["rules"].end()); REQUIRE((*clone_rule)["shortDescription"]["text"] == "Duplicate function (syntactic)"); } TEST_CASE("filtered SARIF writer preserves an existing report-visible set", "[sarif]") { fold::Finding finding; finding.id = "already-filtered"; finding.kind = fold::FindingKind::ProductionReachability; finding.message = "already filtered"; std::ostringstream output; fold::writeFilteredSarif(output, {finding}); const auto sarif = nlohmann::json::parse(output.str()); REQUIRE(sarif["runs"][0]["results"].size() == 1); REQUIRE(sarif["runs"][0]["results"][0]["properties"]["id"] == "already-filtered"); } TEST_CASE("SARIF expands canonical reachability witnesses into compatibility fields", "[sarif]") { fold::Finding finding; finding.id = "reachability-witness"; finding.kind = fold::FindingKind::ProductionReachability; finding.message = "reachable under another configuration"; finding.evidence.quality = fold::QualityEvidence{}; finding.evidence.reachability = fold::ReachabilityEvidence{.witness = {"main", "feature"}}; std::ostringstream output; fold::writeFilteredSarif(output, {finding}); const auto properties = nlohmann::json::parse(output.str())["runs"][0]["results"][0]["properties"]; REQUIRE(properties["pathTrace"] == nlohmann::json::array({"main", "feature"})); REQUIRE(properties["qualityEvidence"]["member_ids"] == nlohmann::json::array({"main", "feature"})); REQUIRE(properties["reachabilityEvidence"]["witness"] == nlohmann::json::array({"main", "feature"})); } TEST_CASE("SARIF declares every emitted foreign and security rule", "[sarif]") { std::vector findings; for (const auto [kind, id, line] : {std::tuple{fold::FindingKind::FormatString, "format", 10}, std::tuple{fold::FindingKind::SwiftUnsafeCast, "swift", 11}, std::tuple{fold::FindingKind::UnknownForeign, "foreign", 12}}) { fold::Finding finding; finding.id = id; finding.kind = kind; finding.locations = {{"src/report/sarif.cpp", line, 1, id}}; finding.message = id; findings.push_back(finding); } std::ostringstream output; fold::writeSarif(output, findings); const auto sarif = nlohmann::json::parse(output.str()); const auto& driver_rules = sarif["runs"][0]["tool"]["driver"]["rules"]; for (const auto& result : sarif["runs"][0]["results"]) { const auto rule = std::find_if(driver_rules.begin(), driver_rules.end(), [&](const auto& candidate) { return candidate["id"] == result["ruleId"]; }); REQUIRE(rule != driver_rules.end()); } } TEST_CASE("frontend NDJSON finding envelope key remains stable", "[frontend][wire-format]") { REQUIRE(std::string_view(fold::frontend::ndjson_keys::kKindFinding) == "finding"); }