use super::*;
fn summary(advisory: usize, warning: usize, error: usize) -> Summary {
Summary {
advisory,
warning,
error,
total: advisory + warning + error,
}
}
#[test]
pub(crate) fn gate_evaluate_is_pure_over_severity_counts() {
let empty = Gate::default();
assert!(!empty.evaluate(&summary(50, 9, 0)).fails);
let error_gate = Gate {
error: Some(0),
warning: Some(10),
advisory: Some(50),
..Gate::default()
};
let tripped = error_gate.evaluate(&summary(5, 3, 1));
assert!(tripped.fails);
assert!(tripped.message.contains("error 1/0 (over)"));
assert!(tripped.message.contains("warning 3/10"));
assert!(tripped.message.starts_with("Quality gate trip:"));
assert!(!error_gate.evaluate(&summary(50, 10, 0)).fails);
let total_gate = Gate {
total: Some(2),
..Gate::default()
};
assert!(total_gate.evaluate(&summary(2, 1, 0)).fails);
assert!(!total_gate.evaluate(&summary(1, 1, 0)).fails);
let warn_gate = Gate {
error: Some(0),
on_match: GateOnMatch::Warn,
..Gate::default()
};
let warned = warn_gate.evaluate(&summary(0, 0, 1));
assert!(!warned.fails);
assert!(warned.message.starts_with("Quality gate warn:"));
assert!(!error_gate.evaluate(&summary(0, 0, 0)).fails);
}
#[test]
pub(crate) fn gate_drives_classify_precedence_over_fail_on() {
let report = sample_report_with(
vec![test_finding(
"sensitive-data.api-key-pattern",
"src/lib.rs",
1,
Severity::Error,
Pillar::SensitiveData,
)],
Vec::new(),
);
let fail_gate = Gate {
error: Some(0),
..Gate::default()
};
assert_eq!(
RunOutcome::classify(&report, FailThreshold::None, Some(&fail_gate)),
RunOutcome::ThresholdHit,
);
let warn_gate = Gate {
error: Some(0),
on_match: GateOnMatch::Warn,
..Gate::default()
};
assert_eq!(
RunOutcome::classify(&report, FailThreshold::None, Some(&warn_gate)),
RunOutcome::Success,
);
assert_eq!(
RunOutcome::classify(&report, FailThreshold::None, None),
RunOutcome::Success,
);
assert_eq!(
RunOutcome::classify(&report, FailThreshold::Error, None),
RunOutcome::ThresholdHit,
);
}
#[test]
pub(crate) fn gate_block_parses_strictly() {
let _guard = analysis_lock();
let dir = tempdir().expect("tempdir");
fs::write(dir.path().join("README.md"), "# Fixture\n").expect("readme");
fs::write(dir.path().join("sample.rs"), "pub fn ready() {}\n").expect("fixture");
let analyse = |dir: &Path| {
run_project_analysis(
dir,
AnalysisOptions {
paths: vec![PathBuf::from("sample.rs")],
no_config: false,
no_baseline: true,
..default_test_options()
},
)
};
write_config(
dir.path(),
"gate:\n total: 200\n severity:\n error: 0\n warning: 10\n onMatch: warn\n",
);
assert!(analyse(dir.path()).is_ok());
write_config(dir.path(), "gate: {}\n");
assert!(analyse(dir.path()).is_ok());
write_config(dir.path(), "gate:\n severity:\n severtiy: 0\n");
let err = analyse(dir.path()).expect_err("typo rejected");
assert!(err.contains("severtiy"), "{err}");
assert!(err.contains("gate.severity"), "{err}");
write_config(dir.path(), "gate:\n severity:\n error: -1\n");
let err = analyse(dir.path()).expect_err("negative rejected");
assert!(err.contains("gate.severity.error"), "{err}");
write_config(dir.path(), "gate:\n onMatch: boom\n");
let err = analyse(dir.path()).expect_err("bad onMatch rejected");
assert!(err.contains("gate.onMatch"), "{err}");
write_config(dir.path(), "gate:\n scope: all\n");
assert!(analyse(dir.path()).is_ok());
write_config(dir.path(), "gate:\n scope: bogus\n");
let err = analyse(dir.path()).expect_err("bad scope rejected");
assert!(err.contains("gate.scope"), "{err}");
}
fn test_baseline_report(generated: bool) -> BaselineReport {
BaselineReport {
path: "gruff-baseline.json".to_string(),
source: "explicit".to_string(),
suppressed: 0,
new_count: 0,
unchanged_count: 0,
absent_count: 0,
generated,
}
}
#[test]
pub(crate) fn gate_scope_new_requires_applied_baseline() {
let new_gate = Gate {
error: Some(0),
scope: GateScope::New,
..Gate::default()
};
let mut report = sample_report_with(Vec::new(), Vec::new());
let error = new_gate
.scope_precondition_error(&report)
.expect("scope: new without a baseline is a config error");
assert!(error.contains("baseline"), "{error}");
report.baseline = Some(test_baseline_report(true));
assert!(new_gate.scope_precondition_error(&report).is_some());
report.baseline = Some(test_baseline_report(false));
assert!(new_gate.scope_precondition_error(&report).is_none());
let no_baseline = sample_report_with(Vec::new(), Vec::new());
let all_gate = Gate {
scope: GateScope::All,
..Gate::default()
};
assert!(all_gate.scope_precondition_error(&no_baseline).is_none());
assert!(Gate::default()
.scope_precondition_error(&no_baseline)
.is_none());
}
#[test]
pub(crate) fn gate_scope_all_counts_pre_baseline_findings() {
let mut report = sample_report_with(Vec::new(), Vec::new());
report.summary = summary(0, 0, 0);
report.all_findings_summary = Some(summary(0, 0, 1));
let cap = |scope| Gate {
error: Some(0),
scope,
..Gate::default()
};
assert!(!cap(GateScope::Current).evaluate_report(&report).fails);
assert!(!cap(GateScope::New).evaluate_report(&report).fails);
assert!(cap(GateScope::All).evaluate_report(&report).fails);
report.all_findings_summary = None;
assert!(!cap(GateScope::All).evaluate_report(&report).fails);
}
#[test]
pub(crate) fn gate_config_error_diagnostic_is_exit_2() {
let report = sample_report_with(
Vec::new(),
vec![RunDiagnostic {
diagnostic_type: "gate-config-error".to_string(),
message: "needs a baseline".to_string(),
file_path: None,
line: None,
}],
);
assert_eq!(
RunOutcome::classify(&report, FailThreshold::None, Some(&Gate::default())),
RunOutcome::DiagnosticsFailed,
);
}
#[test]
pub(crate) fn shared_analysis_does_not_force_gate_diagnostics_on_summary_consumers() {
let _guard = analysis_lock();
let dir = tempdir().expect("tempdir");
fs::create_dir_all(dir.path().join("src")).expect("src dir");
fs::write(dir.path().join("README.md"), "# Fixture\n").expect("readme");
fs::write(
dir.path().join("src/lib.rs"),
"/// Ready.\npub fn ready() {}\n",
)
.expect("source");
write_config(dir.path(), "gate:\n scope: new\n");
let options = AnalysisOptions {
paths: vec![PathBuf::from(".")],
no_config: false,
no_baseline: false,
..default_test_options()
};
let config = load_config(dir.path(), &options).expect("config loads");
let mut report =
run_analysis_in_project(dir.path(), &options, &config).expect("analysis succeeds");
assert!(
!report
.diagnostics
.iter()
.any(|diagnostic| diagnostic.diagnostic_type == "gate-config-error"),
"shared analysis must not inject gate diagnostics before a command opts in"
);
assert_eq!(
RunOutcome::classify(&report, FailThreshold::None, None),
RunOutcome::Success,
"summary-style classification ignores gates and should stay successful"
);
apply_gate_diagnostic(&mut report, config.gate.as_ref());
assert_eq!(
RunOutcome::classify(&report, FailThreshold::None, config.gate.as_ref()),
RunOutcome::DiagnosticsFailed,
"analyse/report opt into the missing-baseline gate precondition"
);
}
#[test]
pub(crate) fn fail_on_new_forces_fail_over_warn_gate() {
let mut config = crate::config::Config::default();
config.gate = Some(Gate {
on_match: GateOnMatch::Warn,
..Gate::default()
});
crate::apply_fail_on_new(&mut config);
let gate = config.gate.expect("gate present after --fail-on-new");
assert_eq!(gate.scope, GateScope::New);
assert_eq!(gate.error, Some(0));
assert_eq!(
gate.on_match,
GateOnMatch::Fail,
"--fail-on-new must override a warn-only gate so new findings fail the build"
);
}