use super::check_name::CheckName;
use crate::command::CommandReport;
use crate::config::Level;
use crate::output::lines::Line as OutputLine;
use serde::Serialize;
pub type Finding = crate::diagnostic::Diagnostic<CheckName>;
pub type Report = crate::diagnostic::Report<CheckName>;
const NO_ISSUES: &str = "No audit findings";
impl CommandReport for Report {
fn render(&self) -> Vec<OutputLine> {
if self.diagnostics.is_empty() {
return vec![OutputLine::Summary {
text: NO_ISSUES.to_owned(),
}];
}
let mut lines: Vec<OutputLine> = self
.diagnostics
.iter()
.map(|finding| OutputLine::LintDiag {
level: finding.level,
workflow: finding.workflow.as_ref().map(ToString::to_string),
line: finding.line,
rule: finding.rule.to_string(),
message: finding.message.clone(),
})
.collect();
lines.push(OutputLine::Blank);
lines.push(OutputLine::Summary {
text: self.summary(NO_ISSUES),
});
lines
}
fn exit_code(&self) -> i32 {
Self::exit_code(self)
}
}
#[derive(Serialize)]
struct JsonFinding<'find> {
check: CheckName,
level: Level,
message: &'find str,
}
#[derive(Serialize)]
struct JsonReport<'rep> {
findings: Vec<JsonFinding<'rep>>,
error_count: usize,
warning_count: usize,
}
impl Report {
#[must_use]
pub fn to_json(&self) -> String {
let document = JsonReport {
findings: self
.diagnostics
.iter()
.map(|finding| JsonFinding {
check: finding.rule,
level: finding.level,
message: &finding.message,
})
.collect(),
error_count: self.error_count,
warning_count: self.warning_count,
};
serde_json::to_string_pretty(&document).unwrap_or_else(|_| "{}".to_owned())
}
}
#[cfg(test)]
#[expect(
clippy::unwrap_used,
clippy::indexing_slicing,
reason = "tests use unwrap, indexing, and other patterns freely"
)]
mod tests {
use super::{CheckName, CommandReport, Finding, Level, OutputLine, Report};
use std::str::FromStr as _;
fn warning() -> Finding {
Finding::new(
CheckName::MutableRef,
Level::Warn,
"actions/checkout is pinned to branch main",
)
}
#[test]
fn clean_report_renders_a_summary_only() {
let lines = Report::default().render();
assert_eq!(lines.len(), 1);
assert!(matches!(&lines[0], OutputLine::Summary { text } if text == "No audit findings"));
}
#[test]
fn findings_render_with_check_name_and_summary() {
let report = Report::from_diagnostics(vec![warning()]);
let lines = report.render();
assert!(matches!(
&lines[0],
OutputLine::LintDiag { rule, level, .. }
if rule == "mutable-ref" && *level == Level::Warn
));
assert!(matches!(&lines[2], OutputLine::Summary { text } if text == "1 warning"));
}
#[test]
fn warning_only_report_exits_zero() {
let report = Report::from_diagnostics(vec![warning()]);
assert_eq!(<Report as CommandReport>::exit_code(&report), 0);
}
#[test]
fn error_report_exits_one() {
let finding = Finding::new(CheckName::MutableRef, Level::Error, "boom");
let report = Report::from_diagnostics(vec![finding]);
assert_eq!(<Report as CommandReport>::exit_code(&report), 1);
}
#[test]
fn json_carries_the_contracted_field_names() {
let report = Report::from_diagnostics(vec![warning()]);
let value: serde_json::Value = serde_json::from_str(&report.to_json()).unwrap();
assert_eq!(value["findings"][0]["check"], "mutable-ref");
assert_eq!(value["findings"][0]["level"], "warn");
assert_eq!(
value["findings"][0]["message"],
"actions/checkout is pinned to branch main"
);
assert_eq!(value["error_count"], 0);
assert_eq!(value["warning_count"], 1);
}
#[test]
fn json_findings_is_an_empty_array_when_clean() {
let value: serde_json::Value = serde_json::from_str(&Report::default().to_json()).unwrap();
assert_eq!(value["findings"], serde_json::json!([]));
assert_eq!(value["error_count"], 0);
assert_eq!(value["warning_count"], 0);
}
#[test]
fn check_name_round_trips_through_its_literal_string() {
for (check, name) in [
(CheckName::MutableRef, "mutable-ref"),
(CheckName::KnownVulnerability, "known-vulnerability"),
(CheckName::ArchivedAction, "archived-action"),
] {
assert_eq!(check.as_str(), name);
assert_eq!(check.to_string(), name);
assert_eq!(CheckName::from_str(name), Ok(check));
assert!(CheckName::ALL.contains(&check));
}
CheckName::from_str("mutable_ref").unwrap_err();
CheckName::from_str("known_vulnerability").unwrap_err();
CheckName::from_str("archived_action").unwrap_err();
}
#[test]
fn every_check_is_reachable_by_name() {
for &check in CheckName::ALL {
assert_eq!(CheckName::from_str(check.as_str()), Ok(check));
}
}
#[test]
fn no_audit_check_is_configurable_as_a_lint_rule() {
for &check in CheckName::ALL {
let as_lint_rule = crate::diagnostic::RuleName::from_str(check.as_str());
assert!(
as_lint_rule.is_err(),
"audit check `{check}` must not be accepted as a lint rule name"
);
}
}
}