fold lives only on git.trevon.dev, but every SARIF report advertised https://github.com/trvon/fold as the tool's information URI, so code scanning UIs and SARIF viewers linked readers to a repository fold no longer uses. The driver now points at https://git.trevon.dev/trevon/fold. test_scan_e2e asserts the URI; it failed before this change.
178 lines
7.1 KiB
C++
178 lines
7.1 KiB
C++
#include <clang/Tooling/CompilationDatabase.h>
|
|
#include <clang/Tooling/Tooling.h>
|
|
|
|
#include <catch2/catch_test_macros.hpp>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <memory>
|
|
#include <nlohmann/json.hpp>
|
|
#include <sstream>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <tuple>
|
|
#include <vector>
|
|
|
|
#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<fold::Finding> 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");
|
|
}
|