#include #include #include #include #include #include #include #include #include #include #include #include #include "fold/cli/scan_options.hpp" #include "fold/cli/scan_runner.hpp" #include "fold/cli/subprocess_fanout.hpp" #include "fold/cli/tu_quarantine.hpp" #include "fold/frontend/comp_db.hpp" #include "fold/report/finding.hpp" #include "fold/report/triage.hpp" namespace { class ScopedWorkerBinary { public: explicit ScopedWorkerBinary(const std::string& path) { if (const char* current = std::getenv("FOLD_WORKER_BINARY")) previous_ = current; ::setenv("FOLD_WORKER_BINARY", path.c_str(), 1); } ~ScopedWorkerBinary() { if (previous_.empty()) ::unsetenv("FOLD_WORKER_BINARY"); else ::setenv("FOLD_WORKER_BINARY", previous_.c_str(), 1); } private: std::string previous_; }; std::string locateFixtures() { const char* env = std::getenv("FOLD_FIXTURES_DIR"); std::string fixtures = env ? env : "../fixtures"; if (!std::filesystem::exists(fixtures + "/clones/exact.cpp")) { fixtures = std::filesystem::current_path().string() + "/../fixtures"; } REQUIRE(std::filesystem::exists(fixtures + "/clones/exact.cpp")); return fixtures; } void writeCompileDb(const std::string& fixtures) { namespace fs = std::filesystem; nlohmann::json db = nlohmann::json::array(); for (const auto& f : {"clones/exact.cpp", "clones/renamed.cpp", "clones/near_miss.cpp"}) { fs::path file = fs::path(fixtures) / f; db.push_back({ {"directory", fixtures}, {"command", std::string("clang++ -std=c++20 -c ") + file.string()}, {"file", file.string()}, }); } std::ofstream(fs::path(fixtures) / "compile_commands.json") << db.dump(); } std::filesystem::path writeQualityCompileDb(const std::string& fixtures) { namespace fs = std::filesystem; const fs::path output = fs::temp_directory_path() / "fold-subprocess-quality"; fs::create_directories(output); nlohmann::json db = nlohmann::json::array(); for (const auto& f : {"quality/structural_debt.cpp", "quality/structural_clean.cpp"}) { fs::path file = fs::path(fixtures) / f; db.push_back({ {"directory", fixtures}, {"command", std::string("clang++ -std=c++20 -c ") + file.string()}, {"file", file.string()}, }); } std::ofstream(output / "compile_commands.json") << db.dump(); return output; } std::string locateFoldBinary() { if (const char* env = std::getenv("FOLD_WORKER_BINARY")) { if (*env && std::filesystem::exists(env)) { return std::filesystem::canonical(env).string(); } } auto cwd = std::filesystem::current_path(); for (const auto& rel : {"../../src/cli/fold", "../../../src/cli/fold", "../src/cli/fold"}) { auto candidate = cwd / rel; if (std::filesystem::exists(candidate)) { return std::filesystem::canonical(candidate).string(); } } return ""; } } // namespace TEST_CASE("SubprocessFanout matches serial output on fixtures", "[fanout][e2e]") { auto fixtures = locateFixtures(); writeCompileDb(fixtures); std::string err; auto db = fold::loadCompilationDatabase(fixtures, err); REQUIRE(db); auto files = db->getAllFiles(); REQUIRE(files.size() == 3); fold::cli::ScanOptions opts; opts.compile_db = fixtures; opts.detectors = {"clone"}; opts.min_body_stmts = 1; // Serial reference. auto serial = fold::cli::runDetectorsOnShard(*db, opts, files, /*include_clone_clustering=*/true); // Parallel via subprocess fan-out. std::string fold_bin = locateFoldBinary(); if (fold_bin.empty()) { WARN("could not locate fold binary; skipping fan-out comparison"); return; } ::setenv("FOLD_WORKER_BINARY", fold_bin.c_str(), 1); fold::cli::SubprocessFanout fanout; auto parallel = fanout.runSync(opts, files, /*n_workers=*/2); REQUIRE(parallel.size() == serial.findings.size()); // Sort both and compare ids — clone finding IDs encode the cluster key // so identical inputs must yield identical IDs. auto idsOf = [](auto&& v) { std::vector ids; ids.reserve(v.size()); for (auto& f : v) ids.push_back(f.id); std::sort(ids.begin(), ids.end()); return ids; }; REQUIRE(idsOf(parallel) == idsOf(serial.findings)); } TEST_CASE("SubprocessFanout single worker == serial path", "[fanout][unit]") { auto fixtures = locateFixtures(); writeCompileDb(fixtures); std::string err; auto db = fold::loadCompilationDatabase(fixtures, err); REQUIRE(db); auto files = db->getAllFiles(); fold::cli::ScanOptions opts; opts.compile_db = fixtures; opts.detectors = {"clone"}; opts.min_body_stmts = 1; auto serial = fold::cli::runDetectorsOnShard(*db, opts, files, /*include_clone_clustering=*/true); std::string fold_bin = locateFoldBinary(); if (fold_bin.empty()) { WARN("could not locate fold binary; skipping fan-out comparison"); return; } ::setenv("FOLD_WORKER_BINARY", fold_bin.c_str(), 1); fold::cli::SubprocessFanout fanout; auto parallel = fanout.runSync(opts, files, /*n_workers=*/1); REQUIRE(parallel.size() == serial.findings.size()); } TEST_CASE("quarantined TUs are coverage gaps in serial and fan-out scans", "[fanout][quarantine][coverage]") { // A TU skipped because a prior run quarantined it was not analysed. The // serial path dropped it from the attempted set (a complete scan with it // missing); the fan-out path reported it as frontend-failed. auto fixtures = locateFixtures(); writeCompileDb(fixtures); std::string err; auto db = fold::loadCompilationDatabase(fixtures, err); REQUIRE(db); auto files = db->getAllFiles(); REQUIRE(files.size() == 3); const auto unique = std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()); const auto quarantine = std::filesystem::temp_directory_path() / ("fold-quarantine-coverage-" + unique); fold::cli::appendQuarantine(quarantine.string(), files[0], SIGSEGV); fold::cli::ScanOptions opts; opts.compile_db = fixtures; opts.detectors = {"clone"}; opts.min_body_stmts = 1; opts.quarantine_dir = quarantine.string(); auto quarantinedGap = [&](const fold::ScanCoverage& coverage) { CHECK(coverage.tus_attempted == 3); CHECK(coverage.tus_analyzed == 2); REQUIRE(coverage.gaps.size() == 1); CHECK(fold::cli::normalizeTuPath(coverage.gaps[0].file) == fold::cli::normalizeTuPath(files[0])); CHECK(coverage.gaps[0].reason == "quarantined"); }; SECTION("serial") { auto serial = fold::cli::runDetectorsOnShard(*db, opts, files, /*include_clone_clustering=*/true); quarantinedGap(serial.coverage); } SECTION("fan-out") { std::string fold_bin = locateFoldBinary(); if (fold_bin.empty()) { WARN("could not locate fold binary; skipping fan-out coverage"); return; } ScopedWorkerBinary worker(fold_bin); fold::cli::SubprocessFanout fanout; (void)fanout.runSync(opts, files, /*n_workers=*/2); quarantinedGap(fanout.coverage()); } std::error_code error; std::filesystem::remove_all(quarantine, error); } TEST_CASE("SubprocessFanout isolates sequential frontend failures and preserves findings", "[fanout][recovery]") { const char* helper = std::getenv("FOLD_TEST_FANOUT_FAILURE_WORKER"); REQUIRE(helper != nullptr); REQUIRE(std::filesystem::exists(helper)); ScopedWorkerBinary worker(helper); const auto unique = std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()); const auto root = std::filesystem::temp_directory_path() / ("fold-fanout-recovery-" + unique); const auto quarantine = root / "quarantine"; std::filesystem::create_directories(root); const std::vector files = { (root / "before.cpp").string(), (root / "missing_header.cpp").string(), (root / "crash.cpp").string(), (root / "after.cpp").string(), }; fold::cli::ScanOptions opts; opts.compile_db = (root / "compile_commands.json").string(); opts.quarantine_dir = quarantine.string(); opts.detectors = {"complexity"}; fold::cli::SubprocessFanout fanout; const auto findings = fanout.runSync(opts, files, 1); REQUIRE(findings.size() == 2); std::vector finding_ids; for (const auto& finding : findings) finding_ids.push_back(finding.id); std::sort(finding_ids.begin(), finding_ids.end()); REQUIRE(finding_ids == std::vector{"fanout-test:after.cpp", "fanout-test:before.cpp"}); const auto quarantined = fold::cli::loadQuarantine(quarantine.string()); REQUIRE(quarantined.contains(fold::cli::normalizeTuPath(files[1]))); REQUIRE(quarantined.contains(fold::cli::normalizeTuPath(files[2]))); std::ifstream input(quarantine / "quarantine.txt"); const std::string records((std::istreambuf_iterator(input)), std::istreambuf_iterator()); REQUIRE(records.find("missing_header.cpp\tworker-exit:3") != std::string::npos); REQUIRE(records.find("crash.cpp\tsignal:" + std::to_string(SIGSEGV)) != std::string::npos); std::error_code error; std::filesystem::remove_all(root, error); } TEST_CASE("SubprocessFanout kills a stalled worker and preserves surrounding TU findings", "[fanout][recovery][timeout]") { const char* helper = std::getenv("FOLD_TEST_FANOUT_FAILURE_WORKER"); REQUIRE(helper != nullptr); REQUIRE(std::filesystem::exists(helper)); ScopedWorkerBinary worker(helper); const auto unique = std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()); const auto root = std::filesystem::temp_directory_path() / ("fold-fanout-timeout-" + unique); const auto quarantine = root / "quarantine"; std::filesystem::create_directories(root); const std::vector files = { (root / "before.cpp").string(), (root / "hang.cpp").string(), (root / "after.cpp").string(), }; fold::cli::ScanOptions opts; opts.compile_db = (root / "compile_commands.json").string(); opts.quarantine_dir = quarantine.string(); opts.detectors = {"complexity"}; opts.per_tu_timeout_seconds = 1; fold::cli::SubprocessFanout fanout; const auto started = std::chrono::steady_clock::now(); const auto findings = fanout.runSync(opts, files, 1); const auto elapsed = std::chrono::steady_clock::now() - started; REQUIRE(elapsed < std::chrono::seconds(5)); REQUIRE(findings.size() == 2); std::vector finding_ids; for (const auto& finding : findings) finding_ids.push_back(finding.id); std::sort(finding_ids.begin(), finding_ids.end()); REQUIRE(finding_ids == std::vector{"fanout-test:after.cpp", "fanout-test:before.cpp"}); std::ifstream input(quarantine / "quarantine.txt"); const std::string records((std::istreambuf_iterator(input)), std::istreambuf_iterator()); REQUIRE(records.find("hang.cpp\ttimeout:1") != std::string::npos); std::error_code error; std::filesystem::remove_all(root, error); } TEST_CASE("SubprocessFanout propagates worker startup failures", "[fanout][recovery]") { REQUIRE(std::filesystem::exists("/usr/bin/false")); ScopedWorkerBinary worker("/usr/bin/false"); fold::cli::ScanOptions opts; opts.compile_db = "/missing/compile-db"; opts.detectors = {"complexity"}; fold::cli::SubprocessFanout fanout; REQUIRE_THROWS(fanout.runSync(opts, {"input.cpp"}, 1)); } TEST_CASE("SubprocessFanout preserves structural quality settings and evidence", "[fanout][quality]") { auto fixtures = locateFixtures(); auto compile_db_dir = writeQualityCompileDb(fixtures); std::string err; auto db = fold::loadCompilationDatabase(compile_db_dir.string(), err); REQUIRE(db); auto files = db->getAllFiles(); REQUIRE(files.size() == 2); fold::cli::ScanOptions opts; opts.compile_db = compile_db_dir.string(); opts.detectors = {"clone", "equiv", "class-dup", "complexity", "dead-statement", "unused-function"}; opts.min_body_stmts = 2; opts.complexity_threshold = 5; auto serial = fold::cli::runDetectorsOnShard(*db, opts, files, /*include_clone_clustering=*/true); fold::triageFindings(serial.findings, opts); std::string fold_bin = locateFoldBinary(); if (fold_bin.empty()) { WARN("could not locate fold binary; skipping quality fan-out comparison"); return; } ::setenv("FOLD_WORKER_BINARY", fold_bin.c_str(), 1); fold::cli::SubprocessFanout fanout; auto parallel = fanout.runSync(opts, files, /*n_workers=*/2); using FindingView = std::tuple; auto views = [](const std::vector& findings) { std::map out; for (const auto& finding : findings) { out.emplace(finding.id, FindingView{finding.message, finding.score, finding.evidence.has_cross_tu_corroboration}); } return out; }; REQUIRE(views(parallel) == views(serial.findings)); REQUIRE(std::any_of(parallel.begin(), parallel.end(), [](const fold::Finding& finding) { return finding.kind == fold::FindingKind::HighComplexity; })); }