cruise 0.1.45

YAML-driven coding agent workflow orchestrator
Documentation
use std::time::Duration;

use crate::error::{CruiseError, Result};

/// Parse a timeout string like `"30"`, `"5m"`, `"1h"` into a [`Duration`].
///
/// # Errors
///
/// Returns [`CruiseError::InvalidStepConfig`] if the string is empty, zero, or
/// contains non-numeric/garbage characters.
pub fn parse_timeout(s: &str) -> Result<Duration> {
    if s.is_empty() {
        return Err(CruiseError::InvalidStepConfig(
            "timeout must be positive".to_string(),
        ));
    }

    let (num_part, multiplier) = match s.as_bytes()[s.len() - 1] {
        b'h' => (&s[..s.len() - 1], 3600u64),
        b'm' => (&s[..s.len() - 1], 60u64),
        _ => (s, 1u64),
    };

    let value: u64 = num_part
        .parse()
        .map_err(|_| CruiseError::InvalidStepConfig(format!("invalid timeout: '{s}'")))?;

    if value == 0 {
        return Err(CruiseError::InvalidStepConfig(
            "timeout must be positive".to_string(),
        ));
    }

    Ok(Duration::from_secs(value * multiplier))
}

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

    #[test]
    fn parse_timeout_plain_digits() {
        let result = parse_timeout("30").unwrap_or_else(|e| panic!("{e:?}"));
        assert_eq!(result, Duration::from_secs(30));
    }

    #[test]
    fn parse_timeout_minutes() {
        let result = parse_timeout("5m").unwrap_or_else(|e| panic!("{e:?}"));
        assert_eq!(result, Duration::from_mins(5));
    }

    #[test]
    fn parse_timeout_hours() {
        let result = parse_timeout("1h").unwrap_or_else(|e| panic!("{e:?}"));
        assert_eq!(result, Duration::from_hours(1));
    }

    #[test]
    fn parse_timeout_zero_is_error() {
        let result = parse_timeout("0");
        assert!(result.is_err(), "expected Err for '0'");
        let msg = err_string(result);
        assert!(
            msg.contains("positive"),
            "error should mention positive, got: {msg}"
        );
    }

    #[test]
    fn parse_timeout_empty_is_error() {
        let result = parse_timeout("");
        assert!(result.is_err(), "expected Err for empty string");
        let msg = err_string(result);
        assert!(
            msg.contains("positive"),
            "error should mention positive, got: {msg}"
        );
    }

    #[test]
    fn parse_timeout_rejects_5min() {
        let result = parse_timeout("5min");
        assert!(result.is_err(), "expected Err for '5min'");
    }

    #[test]
    fn parse_timeout_rejects_negative() {
        let result = parse_timeout("-5");
        assert!(result.is_err(), "expected Err for '-5'");
    }

    #[test]
    fn parse_timeout_rejects_abc() {
        let result = parse_timeout("abc");
        assert!(result.is_err(), "expected Err for 'abc'");
    }

    #[test]
    fn parse_timeout_large_value() {
        let result = parse_timeout("24h").unwrap_or_else(|e| panic!("{e:?}"));
        assert_eq!(result, Duration::from_hours(24));
    }

    #[test]
    fn parse_timeout_single_minute() {
        let result = parse_timeout("1m").unwrap_or_else(|e| panic!("{e:?}"));
        assert_eq!(result, Duration::from_mins(1));
    }
}