use fallow_output::{GateName, GateOutcome, GateOutcomes, GateStatus};
pub const fn status_of(failed: bool) -> GateStatus {
if failed {
GateStatus::Fail
} else {
GateStatus::Pass
}
}
pub fn regression_outcome(
outcome: Option<&crate::regression::RegressionOutcome>,
enforced: bool,
) -> Option<GateOutcome> {
#[expect(
clippy::cast_precision_loss,
reason = "issue counts never approach the f64 integer limit"
)]
fn delta(baseline: usize, current: usize) -> f64 {
current as f64 - baseline as f64
}
let outcome = outcome?;
Some(match outcome {
crate::regression::RegressionOutcome::Pass {
baseline_total,
current_total,
} => GateOutcome {
status: GateStatus::Pass,
enforced,
observed: Some(delta(*baseline_total, *current_total)),
threshold: None,
threshold_label: None,
},
crate::regression::RegressionOutcome::Exceeded {
baseline_total,
current_total,
tolerance,
..
} => GateOutcome {
status: GateStatus::Fail,
enforced,
observed: Some(delta(*baseline_total, *current_total)),
threshold: Some(tolerance.allowed_delta(*baseline_total)),
threshold_label: Some(tolerance.label()),
},
crate::regression::RegressionOutcome::Skipped { .. } => {
GateOutcome::new(GateStatus::Skipped, enforced)
}
})
}
pub const fn stale_baseline_outcome(
staleness: Option<&fallow_output::BaselineStaleness>,
fail_on_stale_baseline: bool,
) -> Option<GateOutcome> {
let Some(staleness) = staleness else {
return None;
};
if staleness.change_scoped && !staleness.unrecognised_format {
return Some(GateOutcome::new(GateStatus::Skipped, false));
}
Some(GateOutcome::new(
status_of(staleness.gate_trips),
fail_on_stale_baseline,
))
}
pub fn type_aware_outcome(
require: fallow_config::TypeAwareRequire,
meta: Option<&fallow_types::envelope::TypeAwareMeta>,
) -> Option<GateOutcome> {
if require != fallow_config::TypeAwareRequire::Complete {
return None;
}
let incomplete = crate::report::ci::required_type_aware_incomplete(meta);
Some(GateOutcome::new(status_of(incomplete), true))
}
pub const fn error_severity_outcome(has_error_severity: bool) -> GateOutcome {
GateOutcome::new(status_of(has_error_severity), true)
}
pub fn duplication_threshold_outcome(
threshold: f64,
duplication_percentage: f64,
enforced: bool,
) -> Option<GateOutcome> {
if threshold <= 0.0 {
return None;
}
Some(GateOutcome::measured(
status_of(crate::dupes::exceeds_threshold(
threshold,
duplication_percentage,
)),
enforced,
duplication_percentage,
threshold,
))
}
pub struct CheckGateInputs<'a> {
pub fail_on_issues: bool,
pub has_error_severity: bool,
pub regression: Option<&'a crate::regression::RegressionOutcome>,
pub baseline_staleness: Option<&'a fallow_output::BaselineStaleness>,
pub fail_on_stale_baseline: bool,
pub type_aware_require: fallow_config::TypeAwareRequire,
pub type_aware_meta: Option<&'a fallow_types::envelope::TypeAwareMeta>,
}
pub fn check_gate_outcomes(input: &CheckGateInputs<'_>) -> Option<GateOutcomes> {
let mut gates = GateOutcomes::new();
gates.insert_if(
GateName::Regression,
regression_outcome(input.regression, true),
);
gates.insert_if(
GateName::StaleBaseline,
stale_baseline_outcome(input.baseline_staleness, input.fail_on_stale_baseline),
);
gates.insert_if(
GateName::TypeAwareRequire,
type_aware_outcome(input.type_aware_require, input.type_aware_meta),
);
if input.fail_on_issues || !gates.is_empty() {
gates.insert(
GateName::ErrorSeverityFindings,
error_severity_outcome(input.has_error_severity),
);
}
gates.into_option()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_run_that_arms_nothing_emits_no_object() {
let gates = check_gate_outcomes(&CheckGateInputs {
fail_on_issues: false,
has_error_severity: true,
regression: None,
baseline_staleness: None,
fail_on_stale_baseline: false,
type_aware_require: fallow_config::TypeAwareRequire::BestEffort,
type_aware_meta: None,
});
assert!(
gates.is_none(),
"an ungated run stays byte-identical, even one the severity rule fails"
);
}
#[test]
fn the_object_always_carries_the_rule_that_decides_the_exit_code() {
let gates = check_gate_outcomes(&CheckGateInputs {
fail_on_issues: false,
has_error_severity: true,
regression: Some(&crate::regression::RegressionOutcome::Pass {
baseline_total: 1,
current_total: 1,
}),
baseline_staleness: None,
fail_on_stale_baseline: false,
type_aware_require: fallow_config::TypeAwareRequire::BestEffort,
type_aware_meta: None,
})
.expect("the regression gate armed the object");
assert_eq!(
gates.get(GateName::Regression).expect("armed").status,
GateStatus::Pass
);
let outcome = gates
.get(GateName::ErrorSeverityFindings)
.expect("the default exit rule joins the object");
assert_eq!(outcome.status, GateStatus::Fail);
assert!(outcome.enforced);
}
#[test]
fn a_published_baseline_verdict_is_unenforced_without_the_flag() {
let staleness = fallow_output::BaselineStaleness {
baseline_entries: 8,
matched_entries: 3,
stale_entries: 5,
current_findings: 0,
change_scoped: false,
stale: false,
warning: fallow_output::BaselineStalenessAdvisory::None,
gate_trips: true,
moved_entries: 0,
unrecognised_format: false,
scope_reasons: fallow_output::BaselineScopeReasons::empty(),
};
let unarmed = stale_baseline_outcome(Some(&staleness), false).expect("verdict published");
assert_eq!(unarmed.status, GateStatus::Fail);
assert!(!unarmed.enforced);
assert!(!unarmed.fails_run());
let armed = stale_baseline_outcome(Some(&staleness), true).expect("verdict published");
assert!(armed.fails_run());
}
#[test]
fn a_change_scoped_baseline_run_stands_down() {
let staleness = fallow_output::BaselineStaleness {
baseline_entries: 8,
matched_entries: 0,
stale_entries: 8,
current_findings: 0,
change_scoped: true,
stale: false,
warning: fallow_output::BaselineStalenessAdvisory::None,
gate_trips: false,
moved_entries: 0,
unrecognised_format: false,
scope_reasons: fallow_output::BaselineScopeReasons::empty()
.with(fallow_output::ScopeReason::Production),
};
let outcome = stale_baseline_outcome(Some(&staleness), true).expect("verdict published");
assert_eq!(outcome.status, GateStatus::Skipped);
assert!(!outcome.fails_run());
}
#[test]
fn a_zero_threshold_arms_no_duplication_gate() {
assert!(duplication_threshold_outcome(0.0, 100.0, true).is_none());
let outcome = duplication_threshold_outcome(5.0, 100.0, true).expect("gate armed");
assert_eq!(outcome.status, GateStatus::Fail);
assert_eq!(outcome.observed, Some(100.0));
assert_eq!(outcome.threshold, Some(5.0));
}
#[test]
fn a_skipped_regression_comparison_is_not_a_pass() {
let outcome = regression_outcome(
Some(&crate::regression::RegressionOutcome::Skipped {
reason: "changed-since",
}),
true,
)
.expect("comparison ran");
assert_eq!(outcome.status, GateStatus::Skipped);
}
}