watchctl 0.3.0

Process supervisor with wait, watch, and retry phases
use crate::error::{Error, Result};
use std::time::Duration;

pub fn parse_duration(s: &str) -> Result<Duration> {
    let s = s.trim();
    if s.is_empty() {
        return Err(Error::InvalidDuration(s.to_string()));
    }

    let (num_str, suffix) = if let Some(n) = s.strip_suffix("ms") {
        (n, "ms")
    } else if let Some(n) = s.strip_suffix('s') {
        (n, "s")
    } else if let Some(n) = s.strip_suffix('m') {
        (n, "m")
    } else if let Some(n) = s.strip_suffix('h') {
        (n, "h")
    } else {
        return Err(Error::InvalidDuration(format!(
            "{s}: missing time suffix (ms, s, m, h)"
        )));
    };

    let num: u64 = num_str
        .parse()
        .map_err(|_| Error::InvalidDuration(format!("{s}: invalid number")))?;

    let duration = match suffix {
        "ms" => Duration::from_millis(num),
        "s" => Duration::from_secs(num),
        "m" => Duration::from_secs(
            num.checked_mul(60)
                .ok_or_else(|| Error::InvalidDuration(format!("{s}: value too large")))?,
        ),
        "h" => Duration::from_secs(
            num.checked_mul(3600)
                .ok_or_else(|| Error::InvalidDuration(format!("{s}: value too large")))?,
        ),
        _ => unreachable!(),
    };

    Ok(duration)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parse_milliseconds() {
        assert_eq!(parse_duration("500ms").unwrap(), Duration::from_millis(500));
        assert_eq!(parse_duration("0ms").unwrap(), Duration::from_millis(0));
    }

    #[test]
    fn parse_seconds() {
        assert_eq!(parse_duration("30s").unwrap(), Duration::from_secs(30));
        assert_eq!(parse_duration("1s").unwrap(), Duration::from_secs(1));
    }

    #[test]
    fn parse_minutes() {
        assert_eq!(parse_duration("5m").unwrap(), Duration::from_secs(300));
    }

    #[test]
    fn parse_hours() {
        assert_eq!(parse_duration("1h").unwrap(), Duration::from_secs(3600));
        assert_eq!(parse_duration("2h").unwrap(), Duration::from_secs(7200));
    }

    #[test]
    fn parse_with_whitespace() {
        assert_eq!(parse_duration("  30s  ").unwrap(), Duration::from_secs(30));
    }

    #[test]
    fn parse_invalid() {
        assert!(parse_duration("").is_err());
        assert!(parse_duration("30").is_err());
        assert!(parse_duration("abc").is_err());
        assert!(parse_duration("30x").is_err());
        assert!(parse_duration("-5s").is_err());
    }

    #[test]
    fn parse_minutes_overflow() {
        let err = parse_duration("18446744073709551615m")
            .expect_err("minute conversion should detect overflow");
        match err {
            Error::InvalidDuration(message) => {
                assert!(message.contains("value too large"));
            }
            other => panic!("unexpected error: {other}"),
        }
    }

    #[test]
    fn parse_hours_overflow() {
        let err = parse_duration("18446744073709551615h")
            .expect_err("hour conversion should detect overflow");
        match err {
            Error::InvalidDuration(message) => {
                assert!(message.contains("value too large"));
            }
            other => panic!("unexpected error: {other}"),
        }
    }
}