use anyhow::{Result, anyhow};
use crate::config::Output;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum DeadBudget {
Count(usize),
Ratio(f64),
}
impl Default for DeadBudget {
fn default() -> Self {
DeadBudget::Count(0)
}
}
impl DeadBudget {
pub fn is_exceeded(self, dead: usize, total: usize) -> bool {
match self {
DeadBudget::Count(max) => dead > max,
DeadBudget::Ratio(r) => total > 0 && (dead as f64 / total as f64) > r,
}
}
pub fn is_set(self) -> bool {
!matches!(self, DeadBudget::Count(0))
}
}
pub fn resolve(
cli_max_dead: Option<usize>,
cli_max_dead_ratio: Option<f64>,
output: &Output,
) -> Result<DeadBudget> {
let (max_dead, max_dead_ratio, src) = if cli_max_dead.is_some() || cli_max_dead_ratio.is_some()
{
(
cli_max_dead,
cli_max_dead_ratio,
"--max-dead / --max-dead-ratio",
)
} else {
(
output.max_dead,
output.max_dead_ratio,
"[output] max_dead / max_dead_ratio",
)
};
match (max_dead, max_dead_ratio) {
(Some(_), Some(_)) => Err(anyhow!(
"{src}: set only one of an absolute budget or a ratio, not both"
)),
(Some(n), None) => Ok(DeadBudget::Count(n)),
(None, Some(r)) => {
if !(0.0..=1.0).contains(&r) {
return Err(anyhow!(
"{src}: max_dead_ratio must be between 0.0 and 1.0 (got {r})"
));
}
Ok(DeadBudget::Ratio(r))
}
(None, None) => Ok(DeadBudget::default()),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn output(max_dead: Option<usize>, max_dead_ratio: Option<f64>) -> Output {
Output {
max_dead,
max_dead_ratio,
..Output::default()
}
}
#[test]
fn cli_overrides_config() {
let b = resolve(Some(100), None, &output(None, Some(0.5))).unwrap();
assert_eq!(b, DeadBudget::Count(100));
let b = resolve(None, None, &output(Some(42), None)).unwrap();
assert_eq!(b, DeadBudget::Count(42));
let b = resolve(None, None, &output(None, None)).unwrap();
assert_eq!(b, DeadBudget::default());
}
#[test]
fn validation_rejects_bad_input() {
assert!(resolve(Some(1), Some(0.1), &output(None, None)).is_err());
assert!(resolve(None, None, &output(Some(1), Some(0.1))).is_err());
assert!(resolve(None, Some(1.5), &output(None, None)).is_err());
assert!(resolve(None, Some(-0.1), &output(None, None)).is_err());
assert_eq!(
resolve(None, Some(0.0), &output(None, None)).unwrap(),
DeadBudget::Ratio(0.0)
);
assert_eq!(
resolve(None, Some(1.0), &output(None, None)).unwrap(),
DeadBudget::Ratio(1.0)
);
}
#[test]
fn is_exceeded_count_and_ratio() {
assert!(DeadBudget::Count(2).is_exceeded(3, 10));
assert!(!DeadBudget::Count(2).is_exceeded(2, 10));
assert!(DeadBudget::Ratio(0.4).is_exceeded(5, 10));
assert!(!DeadBudget::Ratio(0.5).is_exceeded(5, 10));
assert!(!DeadBudget::Ratio(0.0).is_exceeded(0, 0));
}
#[test]
fn is_set_only_for_non_default() {
assert!(!DeadBudget::default().is_set());
assert!(!DeadBudget::Count(0).is_set());
assert!(DeadBudget::Count(1).is_set());
assert!(DeadBudget::Ratio(0.1).is_set());
}
}