use std::fmt;
use crate::types::{Ecosystem, PackageManager};
#[derive(Debug)]
pub(crate) enum ResolveError {
NoSignalsFound {
ecosystem: Ecosystem,
soft: bool,
},
DevEnginesFailHard {
pm: PackageManager,
reason: DevEnginesFailReason,
},
MismatchPolicyError {
declared: PackageManager,
field: &'static str,
lockfile: PackageManager,
},
InvalidOverride {
value: String,
reason: &'static str,
},
PmOverrideNotDetected {
pm: PackageManager,
origin: super::types::OverrideOrigin,
detected: Vec<PackageManager>,
},
ConflictingFailurePolicy {
source: &'static str,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum DevEnginesFailReason {
BinaryMissing,
VersionMismatch {
declared: String,
actual: String,
},
}
impl fmt::Display for ResolveError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::NoSignalsFound { ecosystem, soft } => {
let suffix = if *soft { "" } else { " (--fallback=error)" };
write!(
f,
"no {} package manager detected{suffix}. Checked: lockfiles, manifest \
(packageManager + devEngines), PATH. Pin one with `--pm <name>`, set \
`RUNNER_PM=<name>`, add it to runner.toml, or install a supported PM.",
ecosystem.label(),
)
}
Self::DevEnginesFailHard { pm, reason } => match reason {
DevEnginesFailReason::BinaryMissing => write!(
f,
"devEngines.packageManager declares {} but it was not found on PATH \
(onFail=error)",
pm.label(),
),
DevEnginesFailReason::VersionMismatch { declared, actual } => write!(
f,
"devEngines.packageManager requires {} {declared} but the installed version \
is {actual} (onFail=error)",
pm.label(),
),
},
Self::MismatchPolicyError {
declared,
field,
lockfile,
} => write!(
f,
"{field} declares {} but the lockfile reflects {} (--on-mismatch=error)",
declared.label(),
lockfile.label(),
),
Self::InvalidOverride { value, reason } => {
write!(f, "invalid override value {value:?}: {reason}")
}
Self::PmOverrideNotDetected {
pm,
origin,
detected,
} => {
let detected = if detected.is_empty() {
"none".to_string()
} else {
detected
.iter()
.map(|pm| pm.label())
.collect::<Vec<_>>()
.join(", ")
};
write!(
f,
"cannot install with {} {}: not a detected package manager in this project \
(detected: {detected}). Install {} or drop the override.",
pm.label(),
origin.describe_pm_source(),
pm.label(),
)
}
Self::ConflictingFailurePolicy { source } => write!(
f,
"`keep_going` and `kill_on_fail` are mutually exclusive but both were set ({source}). \
Unset one of `--keep-going` / `RUNNER_KEEP_GOING` / `[chain].keep_going` or \
`--kill-on-fail` / `RUNNER_KILL_ON_FAIL` / `[chain].kill_on_fail` to pick a policy.",
),
}
}
}
impl std::error::Error for ResolveError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pm_override_not_detected_display_names_source_and_detected() {
let err = ResolveError::PmOverrideNotDetected {
pm: PackageManager::Pnpm,
origin: super::super::types::OverrideOrigin::EnvVar,
detected: vec![PackageManager::Npm, PackageManager::Cargo],
};
let msg = format!("{err}");
assert!(msg.contains("pnpm"), "msg: {msg}");
assert!(msg.contains("RUNNER_PM"), "msg: {msg}");
assert!(msg.contains("npm, cargo"), "msg: {msg}");
}
#[test]
fn pm_override_not_detected_display_handles_empty_detected() {
let err = ResolveError::PmOverrideNotDetected {
pm: PackageManager::Pnpm,
origin: super::super::types::OverrideOrigin::CliFlag,
detected: Vec::new(),
};
let msg = format!("{err}");
assert!(msg.contains("detected: none"), "msg: {msg}");
assert!(msg.contains("--pm"), "msg: {msg}");
}
#[test]
fn conflicting_failure_policy_display_includes_source() {
let err = ResolveError::ConflictingFailurePolicy { source: "env vars" };
let msg = format!("{err}");
assert!(msg.contains("keep_going"), "msg: {msg}");
assert!(msg.contains("kill_on_fail"), "msg: {msg}");
assert!(msg.contains("env vars"), "msg: {msg}");
}
}