fold/tests/unit/test_complexity.cpp

116 lines
3.7 KiB
C++

#include <clang/Frontend/ASTUnit.h>
#include <clang/Tooling/Tooling.h>
#include <catch2/catch_test_macros.hpp>
#include <memory>
#include <string>
#include <vector>
#include "fold/detect/complexity.hpp"
#include "fold/report/finding.hpp"
namespace {
std::unique_ptr<clang::ASTUnit> parse(const std::string& code) {
return clang::tooling::buildASTFromCodeWithArgs(code, {"-std=c++20"}, "input.cpp");
}
std::vector<fold::Finding> run(const std::string& code, int threshold) {
auto unit = parse(code);
REQUIRE(unit);
return fold::detectComplexityInContext(unit->getASTContext(), threshold);
}
} // namespace
TEST_CASE("complexity: a branchy function over threshold is flagged", "[complexity]") {
// Six independent decision points -> cyclomatic complexity 7.
auto findings = run(R"(
int f(int x) {
int r = 0;
if (x > 0) r++;
if (x > 1) r++;
if (x > 2) r++;
if (x > 3) r++;
if (x > 4) r++;
if (x > 5) r++;
return r;
}
)",
5);
REQUIRE_FALSE(findings.empty());
REQUIRE(findings[0].kind == fold::FindingKind::HighComplexity);
REQUIRE(findings[0].confidence == fold::Confidence::Syntactic);
// McCabe remains the compatibility gate and message score, while
// independent dimensions provide explainable ranking evidence.
REQUIRE(findings[0].message.find("complexity 7") != std::string::npos);
REQUIRE(findings[0].evidence.quality.has_value());
const auto& quality = *findings[0].evidence.quality;
REQUIRE(quality.classification == "complexity-hotspot");
REQUIRE(quality.metrics.size() == 6);
CHECK(quality.metrics[0].name == "mccabe");
CHECK(quality.metrics[0].value == 7.0);
CHECK(quality.rank_reasons.front() == "mccabe");
}
TEST_CASE("complexity: dimensions separate nesting, state, boolean, loops, and switches",
"[complexity][dimensions]") {
auto findings = run(R"(
int dimensions(int x) {
int result = 0;
for (int i = 0; i < x; ++i) {
if (i > 1 && i < 4) {
switch (i) {
case 2: result = 1; break;
default: result = 2; break;
}
}
}
return result;
}
)",
1);
REQUIRE(findings.size() == 1);
REQUIRE(findings[0].evidence.quality.has_value());
const auto& metrics = findings[0].evidence.quality->metrics;
const auto metric = [&metrics](const std::string& name) {
for (const auto& item : metrics) {
if (item.name == name)
return item.value;
}
return -1.0;
};
CHECK(metric("max_control_nesting") == 3.0);
CHECK(metric("state_mutations") == 3.0);
CHECK(metric("boolean_operators") == 1.0);
CHECK(metric("loops") == 1.0);
CHECK(metric("switch_cases") == 2.0);
}
TEST_CASE("complexity: a straight-line function is not flagged", "[complexity]") {
auto findings = run(R"(
int g(int x) {
int y = x + 1;
return y;
}
)",
5);
REQUIRE(findings.empty());
}
TEST_CASE("complexity: threshold boundary is inclusive", "[complexity]") {
// Three ifs -> complexity 4.
const std::string code = R"(
int h(int x) {
int r = 0;
if (x > 0) r++;
if (x > 1) r++;
if (x > 2) r++;
return r;
}
)";
// At threshold 4 the function fires (complexity == threshold).
REQUIRE_FALSE(run(code, 4).empty());
// At threshold 5 it does not (complexity < threshold).
REQUIRE(run(code, 5).empty());
}