use super::*;
#[test]
pub(crate) fn report_renderers_escape_and_preserve_contracts() {
let report = sample_report();
let json_output = render_report(&report, OutputFormat::Json);
let decoded: Value = serde_json::from_str(&json_output).expect("json report");
assert_eq!(decoded["schemaVersion"], "gruff.analysis.v2");
assert_eq!(decoded["findings"][0]["ruleId"], "security.process-command");
assert_eq!(decoded["findings"][0]["file"], "src/lib.rs");
assert_eq!(decoded["findings"][0]["filePath"], "src/lib.rs");
assert_eq!(decoded["score"]["topOffenders"][0]["file"], "src/lib.rs");
assert_eq!(
decoded["score"]["topOffenders"][0]["filePath"],
"src/lib.rs"
);
let sarif: Value =
serde_json::from_str(&render_report(&report, OutputFormat::Sarif)).expect("sarif report");
assert_eq!(OutputFormat::Sarif.as_str(), "sarif");
assert_eq!(sarif["version"], "2.1.0");
assert_eq!(sarif["runs"][0]["tool"]["driver"]["name"], "gruff-rs");
assert_eq!(
sarif["runs"][0]["properties"]["gruffSchemaVersion"],
"gruff.analysis.v2"
);
let sarif_rules = sarif["runs"][0]["tool"]["driver"]["rules"]
.as_array()
.expect("sarif rules");
let sarif_rule_ids: Vec<&str> = sarif_rules
.iter()
.map(|rule| rule["id"].as_str().expect("sarif rule id"))
.collect();
let mut sorted_rule_ids = sarif_rule_ids.clone();
sorted_rule_ids.sort_unstable();
assert_eq!(sarif_rule_ids, sorted_rule_ids);
let rule_index = sarif_rule_ids
.iter()
.position(|rule_id| *rule_id == "security.process-command")
.expect("security rule in sarif driver");
let sarif_result = &sarif["runs"][0]["results"][0];
assert_eq!(sarif_result["ruleId"], "security.process-command");
assert_eq!(sarif_result["ruleIndex"].as_u64(), Some(rule_index as u64));
assert_eq!(sarif_result["level"], "warning");
assert_eq!(
sarif_result["message"]["text"],
"Use <escaped> command & args"
);
assert_eq!(
sarif_result["locations"][0]["physicalLocation"]["artifactLocation"]["uri"],
"src/lib.rs"
);
assert_eq!(
sarif_result["locations"][0]["physicalLocation"]["region"]["startLine"],
7
);
assert_eq!(
sarif_result["partialFingerprints"]["gruffFingerprint"].as_str(),
Some(report.findings[0].fingerprint.as_str())
);
let text = render_report(&report, OutputFormat::Text);
assert!(text.contains("gruff-rs"));
assert!(text.contains("security.process-command"));
let markdown = render_report(&report, OutputFormat::Markdown);
assert!(markdown.starts_with("# gruff-rs report"));
assert!(markdown.contains("`security.process-command`"));
let github = render_report(&report, OutputFormat::Github);
assert!(github.starts_with("::warning file=src/lib.rs,line=7"));
let html = render_report(&report, OutputFormat::Html);
assert!(html.contains("Use <escaped> command & args"));
assert!(!html.contains("Use <escaped> command & args"));
let hotspot: Value =
serde_json::from_str(&render_report(&report, OutputFormat::Hotspot)).expect("hotspot json");
assert_eq!(hotspot["schemaVersion"], "gruff.hotspot.v1");
assert_eq!(hotspot["files"][0]["filePath"], "src/lib.rs");
}
#[test]
pub(crate) fn text_renderers_surface_ignored_paths_and_baseline_guidance() {
let mut report = sample_report();
report.paths.ignored_paths = vec!["target".to_string(), "node_modules".to_string()];
let text = render_report(&report, OutputFormat::Text);
assert!(text.contains("ignored: 2"));
assert!(text.contains("pass --include-ignored"));
let summary = crate::summary::render(&report, 10, SummaryFormat::Text, 1);
assert!(summary.contains("ignored: 2"));
assert!(summary.contains("pass --include-ignored"));
assert!(summary.contains("gruff-rs analyse --generate-baseline"));
report.baseline = Some(BaselineReport {
path: "gruff-baseline.json".to_string(),
source: "default".to_string(),
suppressed: 1,
new_count: 2,
unchanged_count: 1,
absent_count: 3,
generated: false,
});
let baseline_summary = crate::summary::render(&report, 10, SummaryFormat::Text, 1);
assert!(baseline_summary.contains("baseline: 2 new, 1 unchanged, 3 resolved"));
assert!(baseline_summary.contains("gruff-rs analyse --no-baseline"));
assert!(!baseline_summary.contains("gruff-rs analyse --generate-baseline"));
}
#[test]
pub(crate) fn github_renderer_escapes_annotation_properties() {
let report = sample_report_with(
vec![Finding::new(FindingDescriptor {
rule_id: "custom.rule:id".to_string(),
message: "Message with 100% and\nnewline".to_string(),
file_path: "src/weird,path:100%.rs".to_string(),
line: Some(3),
severity: Severity::Warning,
pillar: Pillar::Documentation,
confidence: Confidence::High,
symbol: None,
remediation: None,
metadata: json!({}),
})],
Vec::new(),
);
let github = render_report(&report, OutputFormat::Github);
assert!(github.starts_with(
"::warning file=src/weird%2Cpath%3A100%25.rs,line=3,title=custom.rule%3Aid::"
));
assert!(github.ends_with("Message with 100%25 and%0Anewline"));
}
#[test]
pub(crate) fn report_json_keeps_deterministic_finding_order() {
let _guard = analysis_lock();
let dir = tempdir().expect("tempdir");
fs::write(dir.path().join("README.md"), "# Fixture\n").expect("readme write");
fs::write(dir.path().join("b.rs"), "pub fn process() {}\n").expect("b write");
fs::write(dir.path().join("a.rs"), "pub fn process() {}\n").expect("a write");
let report = run_project_analysis(
dir.path(),
AnalysisOptions {
paths: vec![PathBuf::from("b.rs"), PathBuf::from("a.rs")],
no_config: true,
no_baseline: true,
..default_test_options()
},
)
.expect("analysis succeeds");
let ordered_paths: Vec<&str> = report
.findings
.iter()
.map(|finding| finding.file_path.as_str())
.collect();
assert_eq!(ordered_paths, vec!["a.rs", "a.rs", "b.rs", "b.rs"]);
}
#[test]
pub(crate) fn summary_top_file_limit_is_not_capped_by_score_report() {
let findings: Vec<Finding> = (0..12)
.map(|index| {
test_finding(
"docs.todo-density",
&format!("src/file_{index}.rs"),
1,
Severity::Advisory,
Pillar::Documentation,
)
})
.collect();
let report = sample_report_with(findings, Vec::new());
let json_output = crate::summary::render(&report, 12, SummaryFormat::Json, 1);
let decoded: Value = serde_json::from_str(&json_output).expect("summary json");
assert_eq!(decoded["topFiles"].as_array().expect("top files").len(), 12);
}