use super::*;
#[test]
pub(crate) fn summary_json_pillar_shape_includes_canonical_fields_with_penalty() {
let mut findings: Vec<Finding> = (0..200)
.map(|index| {
test_finding(
"docs.todo-density",
&format!("src/many_{index}.rs"),
1,
Severity::Advisory,
Pillar::Documentation,
)
})
.collect();
findings.push(test_finding(
"complexity.cyclomatic",
"src/complex.rs",
1,
Severity::Error,
Pillar::Complexity,
));
let report = sample_report_with(findings, Vec::new());
let decoded: Value =
serde_json::from_str(&crate::summary::render(&report, 5, SummaryFormat::Json, 1))
.expect("summary json");
assert_eq!(decoded["schemaVersion"], "gruff.summary.v2");
let pillars = decoded["pillars"].as_array().expect("pillars array");
let find_pillar = |slug: &'static str| {
pillars
.iter()
.find(|pillar| pillar["pillar"] == slug)
.unwrap_or_else(|| panic!("{slug} pillar present"))
};
let documentation = find_pillar("documentation");
let fields: BTreeSet<&str> = documentation
.as_object()
.expect("pillar object")
.keys()
.map(String::as_str)
.collect();
let expected: BTreeSet<&str> = [
"advisory",
"applicable",
"error",
"findings",
"grade",
"penalty",
"pillar",
"score",
"warning",
]
.into_iter()
.collect();
assert_eq!(
fields, expected,
"JSON pillar must expose 9 canonical fields"
);
assert_eq!(documentation["score"].as_f64(), Some(0.0));
assert_eq!(documentation["penalty"].as_f64(), Some(300.0));
assert_eq!(documentation["grade"], "F");
assert!(documentation["applicable"].is_boolean());
let complexity = find_pillar("complexity");
assert_eq!(complexity["penalty"].as_f64(), Some(8.0));
assert_eq!(complexity["score"].as_f64(), Some(92.0));
assert!(complexity["applicable"].is_boolean());
let security = find_pillar("security");
assert_eq!(security["penalty"].as_f64(), Some(0.0));
assert_eq!(security["score"].as_f64(), Some(100.0));
assert!(security["applicable"].is_boolean());
}
#[test]
pub(crate) fn non_score_pillars_are_inapplicable_and_excluded_from_composite() {
let waste = test_finding(
"custom.waste",
"src/wasteful.rs",
1,
Severity::Error,
Pillar::Waste,
);
let report = sample_report_with(vec![waste], Vec::new());
assert_eq!(report.score.composite, 100.0);
let decoded: Value =
serde_json::from_str(&crate::summary::render(&report, 5, SummaryFormat::Json, 1))
.expect("summary json");
let waste_pillar = decoded["pillars"]
.as_array()
.expect("pillars array")
.iter()
.find(|pillar| pillar["pillar"] == "waste")
.expect("waste pillar present");
assert_eq!(waste_pillar["applicable"], false);
assert_eq!(waste_pillar["penalty"].as_f64(), Some(8.0));
}
#[test]
pub(crate) fn pillar_ties_sort_by_canonical_label_not_enum_order() {
let findings = vec![
test_finding(
"size.function-length",
"src/big.rs",
1,
Severity::Warning,
Pillar::Size,
),
test_finding(
"complexity.cyclomatic",
"src/complex.rs",
1,
Severity::Warning,
Pillar::Complexity,
),
];
let report = sample_report_with(findings, Vec::new());
let markdown = render_report(&report, OutputFormat::Markdown);
let complexity_pos = markdown.find("| complexity |").expect("complexity row");
let size_pos = markdown.find("| size |").expect("size row");
assert!(
complexity_pos < size_pos,
"tied pillars must sort by kebab-case label (complexity < size):\n{markdown}"
);
}
#[test]
pub(crate) fn html_pillars_section_matches_canonical_contract() {
let findings = vec![
test_finding(
"complexity.cyclomatic",
"src/complex.rs",
1,
Severity::Warning,
Pillar::Complexity,
),
test_finding(
"complexity.cyclomatic",
"src/complex.rs",
2,
Severity::Advisory,
Pillar::Complexity,
),
test_finding(
"complexity.cyclomatic",
"src/complex.rs",
3,
Severity::Error,
Pillar::Complexity,
),
test_finding(
"naming.snake_case",
"src/named.rs",
1,
Severity::Advisory,
Pillar::Naming,
),
test_finding(
"docs.missing",
"src/docs.rs",
1,
Severity::Advisory,
Pillar::Documentation,
),
test_finding(
"docs.missing",
"src/docs.rs",
2,
Severity::Warning,
Pillar::Documentation,
),
];
let report = sample_report_with(findings, Vec::new());
let html = render_report(&report, OutputFormat::Html);
assert!(
html.contains("<table class=\"pillar-list\">"),
"missing canonical pillar-list table"
);
for header in [
"<th scope=\"col\">pillar</th>",
"<th scope=\"col\" class=\"num\">grade</th>",
"<th scope=\"col\" class=\"num\">score</th>",
"<th scope=\"col\" class=\"num\">findings</th>",
"<th scope=\"col\" class=\"num\">advisory</th>",
"<th scope=\"col\" class=\"num\">warning</th>",
"<th scope=\"col\" class=\"num\">error</th>",
] {
assert!(
html.contains(header),
"missing pillar table header {header:?}"
);
}
for pillar in ["complexity", "documentation", "naming"] {
let marker = format!("<td class=\"pillar-name\">{pillar}</td>");
assert!(
html.contains(&marker),
"missing pillar-name cell for {pillar}"
);
}
for stale in [
"<div class=\"pillar\">",
"class=\"pillar-grid\"",
"<span class=\"key\">",
"<div class=\"breakdown\">",
">advisories<",
">warnings<",
">errors<",
] {
assert!(
!html.contains(stale),
"stale card-grid markup found: {stale}"
);
}
assert!(
html.contains("<span class=\"grade-pill "),
"pillar table should render grades inside .grade-pill"
);
let complexity_pos = html
.find("<td class=\"pillar-name\">complexity</td>")
.expect("complexity row");
let documentation_pos = html
.find("<td class=\"pillar-name\">documentation</td>")
.expect("documentation row");
let naming_pos = html
.find("<td class=\"pillar-name\">naming</td>")
.expect("naming row");
assert!(
complexity_pos < documentation_pos,
"complexity (3 findings) should come before documentation (2)"
);
assert!(
documentation_pos < naming_pos,
"documentation (2 findings) should come before naming (1)"
);
assert!(
html.contains(">86.50<"),
"expected complexity score 86.50 in HTML, html = {html}"
);
assert!(
html.contains("<td class=\"num note\">1</td>"),
"expected non-zero advisory cell to carry .note tier class"
);
assert!(
html.contains("<td class=\"num warn\">1</td>"),
"expected non-zero warning cell to carry .warn tier class"
);
assert!(
html.contains("<td class=\"num fail\">1</td>"),
"expected non-zero error cell to carry .fail tier class"
);
assert!(
html.contains("<td class=\"num\">0</td>"),
"expected zero-count cells to stay neutral (no tier class)"
);
}
#[test]
pub(crate) fn markdown_pillars_section_matches_canonical_contract() {
let findings = vec![
test_finding(
"complexity.cyclomatic",
"src/complex.rs",
1,
Severity::Warning,
Pillar::Complexity,
),
test_finding(
"complexity.cyclomatic",
"src/complex.rs",
2,
Severity::Advisory,
Pillar::Complexity,
),
test_finding(
"complexity.cyclomatic",
"src/complex.rs",
3,
Severity::Error,
Pillar::Complexity,
),
test_finding(
"naming.snake_case",
"src/named.rs",
1,
Severity::Advisory,
Pillar::Naming,
),
test_finding(
"docs.missing",
"src/docs.rs",
1,
Severity::Advisory,
Pillar::Documentation,
),
test_finding(
"docs.missing",
"src/docs.rs",
2,
Severity::Warning,
Pillar::Documentation,
),
];
let report = sample_report_with(findings, Vec::new());
let markdown = render_report(&report, OutputFormat::Markdown);
assert!(
markdown.contains("\n## Pillars\n"),
"missing canonical `## Pillars` heading"
);
assert!(
markdown.contains("| Pillar | Grade | Score | Findings | Advisory | Warning | Error |"),
"missing canonical pillar table header in markdown:\n{markdown}"
);
assert!(
markdown.contains("| --- | --- | ---: | ---: | ---: | ---: | ---: |"),
"missing canonical pillar table separator in markdown:\n{markdown}"
);
assert!(
markdown.contains(" 86.50 "),
"expected complexity score 86.50 in markdown:\n{markdown}"
);
let complexity_pos = markdown
.find("| complexity |")
.expect("complexity row in markdown");
let documentation_pos = markdown
.find("| documentation |")
.expect("documentation row in markdown");
let naming_pos = markdown.find("| naming |").expect("naming row in markdown");
assert!(
complexity_pos < documentation_pos,
"complexity (3 findings) must precede documentation (2)"
);
assert!(
documentation_pos < naming_pos,
"documentation (2 findings) must precede naming (1)"
);
let score_pos = markdown.find("Score: **").expect("score header");
let pillars_pos = markdown.find("## Pillars").expect("pillars heading");
assert!(
score_pos < pillars_pos,
"Pillars section must follow the score header"
);
let first_finding_pos = markdown
.find("\n- `")
.expect("seeded fixture must produce a bulleted finding line");
assert!(
pillars_pos < first_finding_pos,
"Pillars section must precede the findings list"
);
assert!(
markdown.contains("| complexity | B | 86.50 | 3 | 1 | 1 | 1 |"),
"complexity row should expose the 7 canonical cells exactly:\n{markdown}"
);
assert!(
markdown.contains("| naming | A | 98.50 | 1 | 1 | 0 | 0 |"),
"naming row should carry zero counts for warning and error:\n{markdown}"
);
}