Skip to main content

deps_cli/
exit.rs

1//! Exit-code mapping (FR-011, FR-012): 0 clean, 1 policy violation, 2 execution error.
2
3use crate::report::{CheckReport, FailOnPolicy};
4
5/// The process exited cleanly: no finding matched the `--fail-on` policy.
6pub const EXIT_CLEAN: i32 = 0;
7/// At least one finding matched the `--fail-on` policy.
8pub const EXIT_POLICY_VIOLATION: i32 = 1;
9/// A registry required by a non-offline run was unreachable, or another execution error
10/// occurred (a malformed `deps.toml`, an unreadable explicitly-given manifest path, ...).
11pub const EXIT_EXECUTION_ERROR: i32 = 2;
12
13/// Computes the process exit code for a completed `check` run.
14///
15/// A real policy violation (T021) always reports as `1`, even when `had_execution_error` is
16/// also set: a genuine `--fail-on` hit is the more actionable signal, and an unrelated
17/// registry/parse error elsewhere in the run must not hide it behind a less specific `2`
18/// (spec 062 review S3 — the original precedence order lost this signal, e.g. one malformed
19/// manifest anywhere in a monorepo turning a real `--fail-on vulnerable` hit from `1` into
20/// `2`). `had_execution_error` only takes precedence over an otherwise-*clean* policy result:
21/// a run that could not reach a registry it needed, or failed to parse a manifest, produced
22/// an incomplete report, so its exit code must never claim a clean `0`, even when nothing
23/// already-resolved happened to violate `policy`.
24///
25/// # Examples
26///
27/// ```
28/// use deps_cli::exit::{EXIT_CLEAN, EXIT_EXECUTION_ERROR, EXIT_POLICY_VIOLATION, exit_code};
29/// use deps_cli::report::{CheckReport, FailOnPolicy};
30///
31/// let clean = CheckReport::default();
32/// let policy = FailOnPolicy::default_categories();
33/// assert_eq!(exit_code(&clean, &policy, false), EXIT_CLEAN);
34/// assert_eq!(exit_code(&clean, &policy, true), EXIT_EXECUTION_ERROR);
35/// ```
36#[must_use]
37pub fn exit_code(report: &CheckReport, policy: &FailOnPolicy, had_execution_error: bool) -> i32 {
38    if policy.matches(&report.findings) {
39        return EXIT_POLICY_VIOLATION;
40    }
41    if had_execution_error {
42        return EXIT_EXECUTION_ERROR;
43    }
44    EXIT_CLEAN
45}
46
47#[cfg(test)]
48mod tests {
49    use super::*;
50    use crate::report::{Category, CheckFinding};
51    use deps_core::EcosystemId;
52    use deps_core::diagnostic::Severity;
53    use deps_core::position::Range;
54    use std::path::PathBuf;
55
56    fn finding(category: Category) -> CheckFinding {
57        CheckFinding {
58            ecosystem: EcosystemId::Cargo,
59            manifest_path: PathBuf::from("Cargo.toml"),
60            dependency_name: Some("serde".to_string()),
61            requirement: None,
62            category,
63            code: None,
64            advisory_url: None,
65            advisory_severity: None,
66            severity: Severity::Warning,
67            range: Range::default(),
68            message: "test".to_string(),
69        }
70    }
71
72    #[test]
73    fn test_exit_code_clean_report_is_zero() {
74        let report = CheckReport::default();
75        let policy = FailOnPolicy::default_categories();
76        assert_eq!(exit_code(&report, &policy, false), EXIT_CLEAN);
77    }
78
79    #[test]
80    fn test_exit_code_policy_violation_is_one() {
81        let report = CheckReport {
82            findings: vec![finding(Category::Vulnerable)],
83        };
84        let policy = FailOnPolicy::default_categories();
85        assert_eq!(exit_code(&report, &policy, false), EXIT_POLICY_VIOLATION);
86    }
87
88    #[test]
89    fn test_exit_code_non_failing_category_is_zero() {
90        let report = CheckReport {
91            findings: vec![finding(Category::Outdated)],
92        };
93        let policy = FailOnPolicy::default_categories();
94        assert_eq!(exit_code(&report, &policy, false), EXIT_CLEAN);
95    }
96
97    #[test]
98    fn test_exit_code_execution_error_is_two() {
99        let report = CheckReport::default();
100        let policy = FailOnPolicy::default_categories();
101        assert_eq!(exit_code(&report, &policy, true), EXIT_EXECUTION_ERROR);
102    }
103
104    #[test]
105    fn test_exit_code_execution_error_takes_precedence_over_clean_policy() {
106        let report = CheckReport {
107            findings: vec![finding(Category::Outdated)],
108        };
109        let policy = FailOnPolicy::default_categories();
110        assert_eq!(exit_code(&report, &policy, true), EXIT_EXECUTION_ERROR);
111    }
112
113    /// Regression test for S3 (spec 062 review): a real policy violation must win over an
114    /// unrelated execution error, not be masked by it.
115    #[test]
116    fn test_exit_code_policy_violation_takes_precedence_over_execution_error() {
117        let report = CheckReport {
118            findings: vec![finding(Category::Vulnerable)],
119        };
120        let policy = FailOnPolicy::default_categories();
121        assert_eq!(exit_code(&report, &policy, true), EXIT_POLICY_VIOLATION);
122    }
123}