use crate::report::{CheckReport, FailOnPolicy};
use crate::update::{Outcome, SkipReason, UpdatePlan};
pub const EXIT_CLEAN: i32 = 0;
pub const EXIT_POLICY_VIOLATION: i32 = 1;
pub const EXIT_EXECUTION_ERROR: i32 = 2;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExecutionOutcome {
Clean,
Failed,
}
impl ExecutionOutcome {
#[must_use]
pub fn from_had_execution_error(had_execution_error: bool) -> Self {
if had_execution_error {
Self::Failed
} else {
Self::Clean
}
}
}
#[must_use]
pub fn exit_code(
report: &CheckReport,
policy: &FailOnPolicy,
had_execution_error: ExecutionOutcome,
) -> i32 {
if policy.matches(&report.findings) {
return EXIT_POLICY_VIOLATION;
}
if had_execution_error == ExecutionOutcome::Failed {
return EXIT_EXECUTION_ERROR;
}
EXIT_CLEAN
}
#[must_use]
pub fn update_exit_code(plan: &UpdatePlan) -> i32 {
let has_unresolved_item = plan.items.iter().any(|item| {
!matches!(
item.outcome,
Outcome::Applied { .. }
| Outcome::Skipped {
reason: SkipReason::NotRequested
| SkipReason::IgnoreRule
| SkipReason::WithinFreshnessCooldown
| SkipReason::OverlapsAnotherEdit,
..
}
)
});
if has_unresolved_item {
EXIT_POLICY_VIOLATION
} else {
EXIT_CLEAN
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::report::{Category, CheckFinding};
use deps_core::EcosystemId;
use deps_core::diagnostic::Severity;
use deps_core::position::Range;
use std::path::PathBuf;
fn finding(category: Category) -> CheckFinding {
CheckFinding {
ecosystem: EcosystemId::Cargo,
manifest_path: PathBuf::from("Cargo.toml"),
dependency_name: Some("serde".to_string()),
requirement: None,
category,
code: None,
advisory_url: None,
advisory_severity: None,
severity: Severity::Warning,
range: Range::default(),
message: "test".to_string(),
}
}
#[test]
fn test_exit_code_clean_report_is_zero() {
let report = CheckReport::default();
let policy = FailOnPolicy::default_categories();
assert_eq!(
exit_code(&report, &policy, ExecutionOutcome::Clean),
EXIT_CLEAN
);
}
#[test]
fn test_exit_code_policy_violation_is_one() {
let report = CheckReport {
findings: vec![finding(Category::Vulnerable)],
};
let policy = FailOnPolicy::default_categories();
assert_eq!(
exit_code(&report, &policy, ExecutionOutcome::Clean),
EXIT_POLICY_VIOLATION
);
}
#[test]
fn test_exit_code_non_failing_category_is_zero() {
let report = CheckReport {
findings: vec![finding(Category::Outdated)],
};
let policy = FailOnPolicy::default_categories();
assert_eq!(
exit_code(&report, &policy, ExecutionOutcome::Clean),
EXIT_CLEAN
);
}
#[test]
fn test_exit_code_execution_error_is_two() {
let report = CheckReport::default();
let policy = FailOnPolicy::default_categories();
assert_eq!(
exit_code(&report, &policy, ExecutionOutcome::Failed),
EXIT_EXECUTION_ERROR
);
}
#[test]
fn test_exit_code_execution_error_takes_precedence_over_clean_policy() {
let report = CheckReport {
findings: vec![finding(Category::Outdated)],
};
let policy = FailOnPolicy::default_categories();
assert_eq!(
exit_code(&report, &policy, ExecutionOutcome::Failed),
EXIT_EXECUTION_ERROR
);
}
#[test]
fn test_exit_code_policy_violation_takes_precedence_over_execution_error() {
let report = CheckReport {
findings: vec![finding(Category::Vulnerable)],
};
let policy = FailOnPolicy::default_categories();
assert_eq!(
exit_code(&report, &policy, ExecutionOutcome::Failed),
EXIT_POLICY_VIOLATION
);
}
use crate::update::{PlannedUpdateItem, UnfixableReason};
use deps_core::edit::{ManifestEdit, UnplannableReason};
fn update_item(outcome: Outcome) -> PlannedUpdateItem {
PlannedUpdateItem {
name: "serde".to_string(),
current: crate::update::CurrentVersion::Resolved(deps_core::ConcreteVersion::from(
"1.0.0",
)),
outcome,
advisory_ids: Vec::new(),
ignore_rule_overridden: false,
gossip_excluded_version: None,
cooldown_fallback: None,
}
}
fn applied() -> Outcome {
Outcome::Applied {
edit: ManifestEdit {
range: Range::default(),
new_text: "1.2.0".to_string(),
},
target: deps_core::ConcreteVersion::from("1.2.0"),
}
}
#[test]
fn test_update_exit_code_empty_plan_is_clean() {
assert_eq!(update_exit_code(&UpdatePlan::default()), EXIT_CLEAN);
}
#[test]
fn test_update_exit_code_all_applied_is_clean() {
let plan = UpdatePlan {
items: vec![update_item(applied()), update_item(applied())],
};
assert_eq!(update_exit_code(&plan), EXIT_CLEAN);
}
#[test]
fn test_update_exit_code_ignore_rule_skip_alone_is_clean() {
let plan = UpdatePlan {
items: vec![
update_item(applied()),
update_item(Outcome::Skipped {
reason: SkipReason::IgnoreRule,
target: None,
}),
],
};
assert_eq!(update_exit_code(&plan), EXIT_CLEAN);
}
#[test]
fn test_update_exit_code_not_requested_skip_alone_is_clean() {
let plan = UpdatePlan {
items: vec![
update_item(applied()),
update_item(Outcome::Skipped {
reason: SkipReason::NotRequested,
target: None,
}),
],
};
assert_eq!(update_exit_code(&plan), EXIT_CLEAN);
}
#[test]
fn test_update_exit_code_not_safely_editable_skip_is_policy_violation() {
let plan = UpdatePlan {
items: vec![update_item(Outcome::Skipped {
reason: SkipReason::NotSafelyEditable(UnplannableReason::NonLiteralSpan),
target: None,
})],
};
assert_eq!(update_exit_code(&plan), EXIT_POLICY_VIOLATION);
}
#[test]
fn test_update_exit_code_requires_lockfile_update_is_policy_violation() {
let plan = UpdatePlan {
items: vec![update_item(Outcome::RequiresLockfileUpdate {
target: deps_core::ConcreteVersion::from("1.2.0"),
})],
};
assert_eq!(update_exit_code(&plan), EXIT_POLICY_VIOLATION);
}
#[test]
fn test_update_exit_code_unfixable_is_policy_violation() {
let plan = UpdatePlan {
items: vec![update_item(Outcome::Unfixable(
UnfixableReason::FetchFailedOrAbsent,
))],
};
assert_eq!(update_exit_code(&plan), EXIT_POLICY_VIOLATION);
}
#[test]
fn test_update_exit_code_within_freshness_cooldown_skip_alone_is_clean() {
let plan = UpdatePlan {
items: vec![
update_item(applied()),
update_item(Outcome::Skipped {
reason: SkipReason::WithinFreshnessCooldown,
target: None,
}),
],
};
assert_eq!(update_exit_code(&plan), EXIT_CLEAN);
}
#[test]
fn test_update_exit_code_overlaps_another_edit_skip_alone_is_clean() {
let plan = UpdatePlan {
items: vec![
update_item(applied()),
update_item(Outcome::Skipped {
reason: SkipReason::OverlapsAnotherEdit,
target: None,
}),
],
};
assert_eq!(update_exit_code(&plan), EXIT_CLEAN);
}
#[test]
fn test_update_exit_code_mixed_applied_and_operator_skips_is_clean() {
let plan = UpdatePlan {
items: vec![
update_item(applied()),
update_item(Outcome::Skipped {
reason: SkipReason::NotRequested,
target: None,
}),
update_item(Outcome::Skipped {
reason: SkipReason::IgnoreRule,
target: None,
}),
],
};
assert_eq!(update_exit_code(&plan), EXIT_CLEAN);
}
}