use super::*;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub(crate) struct Gate {
pub(crate) total: Option<u64>,
pub(crate) error: Option<u64>,
pub(crate) warning: Option<u64>,
pub(crate) advisory: Option<u64>,
pub(crate) on_match: GateOnMatch,
pub(crate) scope: GateScope,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub(crate) enum GateOnMatch {
#[default]
Fail,
Warn,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub(crate) enum GateScope {
#[default]
Current,
New,
All,
}
pub(crate) struct GateEvaluation {
pub(crate) fails: bool,
pub(crate) message: String,
}
impl Gate {
pub(crate) fn evaluate(&self, summary: &Summary) -> GateEvaluation {
let dimensions = [
("error", summary.error as u64, self.error),
("warning", summary.warning as u64, self.warning),
("advisory", summary.advisory as u64, self.advisory),
("total", summary.total as u64, self.total),
];
let mut breached = false;
let mut parts = Vec::new();
for (name, count, cap) in dimensions {
let (part, over) = format_gate_dimension(name, count, cap);
breached |= over;
parts.push(part);
}
let decision = match (breached, self.on_match) {
(false, _) => "pass",
(true, GateOnMatch::Fail) => "trip",
(true, GateOnMatch::Warn) => "warn",
};
GateEvaluation {
fails: breached && self.on_match == GateOnMatch::Fail,
message: format!("Quality gate {decision}: {}.", parts.join(", ")),
}
}
pub(crate) fn evaluate_report(&self, report: &AnalysisReport) -> GateEvaluation {
self.evaluate(self.gated_summary(report))
}
fn gated_summary<'a>(&self, report: &'a AnalysisReport) -> &'a Summary {
match self.scope {
GateScope::All => report
.all_findings_summary
.as_ref()
.unwrap_or(&report.summary),
GateScope::Current | GateScope::New => &report.summary,
}
}
pub(crate) fn scope_precondition_error(&self, report: &AnalysisReport) -> Option<String> {
(self.scope == GateScope::New && !is_baseline_applied(report)).then(|| {
"`gate.scope: new` (and `--fail-on-new`) needs a baseline to define what is \
\"new\"; pass `--baseline <path>`, keep a `gruff-baseline.json`, or drop \
`scope: new`"
.to_string()
})
}
pub(crate) fn diagnostic(&self, report: &AnalysisReport) -> RunDiagnostic {
let base = self.evaluate_report(report).message;
let message = match report
.baseline
.as_ref()
.filter(|baseline| !baseline.generated)
{
Some(baseline) => format!(
"{base} (baseline: {} new, {} unchanged)",
baseline.new_count, baseline.unchanged_count
),
None => base,
};
RunDiagnostic {
diagnostic_type: "gate".to_string(),
message,
file_path: None,
line: None,
}
}
}
fn is_baseline_applied(report: &AnalysisReport) -> bool {
report
.baseline
.as_ref()
.is_some_and(|baseline| !baseline.generated)
}
fn format_gate_dimension(name: &str, count: u64, cap: Option<u64>) -> (String, bool) {
match cap {
Some(cap) => {
let over = count > cap;
let marker = if over { " (over)" } else { "" };
(format!("{name} {count}/{cap}{marker}"), over)
}
None => (format!("{name} {count}"), false),
}
}