use std::time::Duration;
const MICROS_PER_SEC: f64 = 1_000_000.0;
const MICROS_PER_MS: f64 = 1_000.0;
const MICROS_PER_MIN: f64 = 60.0 * MICROS_PER_SEC;
const MICROS_PER_HOUR: f64 = 60.0 * MICROS_PER_MIN;
const MAX_F64_MICROS: f64 = 9_223_372_036_854_775_000.0;
const FMT_DUR_SECS_BOUNDARY_MS: u128 = 1_000;
#[must_use]
pub fn format_error_chain(err: &dyn std::error::Error) -> String {
let mut out = err.to_string();
let mut cur = err.source();
while let Some(src) = cur {
out.push_str(": ");
out.push_str(&src.to_string());
cur = src.source();
}
sanitize_for_terminal(&out)
}
#[must_use]
pub fn sanitize_for_terminal(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
match c {
'\t' | '\n' => out.push(c),
c if (c as u32) < 0x20 || c == '\x7f' => out.push('\u{fffd}'),
_ => out.push(c),
}
}
out
}
#[must_use]
pub fn redact_in(text: &str, secret: &str) -> String {
if secret.is_empty() {
return text.to_owned();
}
text.replace(secret, "***")
}
#[must_use]
pub fn duration_ms(d: Duration) -> u64 {
u64::try_from(d.as_millis()).unwrap_or(u64::MAX)
}
#[must_use]
pub fn fmt_dur(d: Duration) -> String {
let ms = d.as_millis();
if ms < FMT_DUR_SECS_BOUNDARY_MS {
format!("{ms}ms")
} else {
format!("{:.1}s", d.as_secs_f64())
}
}
pub fn parse_duration(s: &str) -> Result<Duration, String> {
let s = s.trim();
let (num, unit) = s
.find(|c: char| !c.is_ascii_digit() && c != '.')
.map_or((s, ""), |i| s.split_at(i));
let n: f64 = num
.parse()
.map_err(|e| format!("invalid number `{num}`: {e}"))?;
if !n.is_finite() || n < 0.0 {
return Err(format!(
"invalid duration `{s}`: must be finite and non-negative"
));
}
let mult_us: f64 = match unit.trim() {
"" | "s" => MICROS_PER_SEC,
"ms" => MICROS_PER_MS,
"us" | "µs" => 1.0,
"m" => MICROS_PER_MIN,
"h" => MICROS_PER_HOUR,
other => return Err(format!("unknown unit `{other}` (use ms, s, m, h)")),
};
#[allow(
clippy::cast_sign_loss,
clippy::cast_possible_truncation,
clippy::cast_precision_loss
)]
let micros = (n * mult_us).clamp(0.0, MAX_F64_MICROS) as u64;
Ok(Duration::from_micros(micros))
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn parses_seconds_default() {
assert_eq!(parse_duration("30").unwrap(), Duration::from_secs(30));
assert_eq!(parse_duration("30s").unwrap(), Duration::from_secs(30));
}
#[test]
fn parses_units() {
assert_eq!(parse_duration("100ms").unwrap(), Duration::from_millis(100));
assert_eq!(parse_duration("500us").unwrap(), Duration::from_micros(500));
assert_eq!(parse_duration("1.5s").unwrap(), Duration::from_millis(1500));
assert_eq!(parse_duration("2m").unwrap(), Duration::from_secs(120));
assert_eq!(parse_duration("1h").unwrap(), Duration::from_secs(3600));
}
#[test]
fn rejects_invalid() {
assert!(parse_duration("inf").is_err());
assert!(parse_duration("NaN").is_err());
assert!(parse_duration("-5s").is_err());
assert!(parse_duration("5x").is_err());
}
#[test]
fn fmt_dur_human() {
assert_eq!(fmt_dur(Duration::from_millis(42)), "42ms");
assert_eq!(fmt_dur(Duration::from_millis(1500)), "1.5s");
}
#[test]
fn sanitize_strips_escape_and_control() {
let evil = "ok\x1b[31mred\x1b[0m\x07\rrest";
let safe = sanitize_for_terminal(evil);
assert!(!safe.contains('\x1b'));
assert!(!safe.contains('\x07'));
assert!(!safe.contains('\r'));
assert!(safe.contains("ok"));
}
#[test]
fn sanitize_keeps_tab_and_newline() {
assert_eq!(sanitize_for_terminal("a\tb\nc"), "a\tb\nc");
}
#[test]
fn redact_replaces_secret() {
assert_eq!(redact_in("user:hunter2@host", "hunter2"), "user:***@host");
assert_eq!(redact_in("none", ""), "none");
}
#[test]
fn cli_defaults_match_runner_constants() {
use crate::RunnerConfig;
assert_eq!(
parse_duration("30s").unwrap(),
RunnerConfig::DEFAULT_OVERALL_TIMEOUT
);
assert_eq!(
parse_duration("100ms").unwrap(),
RunnerConfig::DEFAULT_INITIAL_INTERVAL
);
assert_eq!(
parse_duration("2s").unwrap(),
RunnerConfig::DEFAULT_MAX_INTERVAL
);
assert_eq!(
parse_duration("0s").unwrap(),
RunnerConfig::DEFAULT_INITIAL_DELAY
);
assert_eq!(
parse_duration("5s").unwrap(),
RunnerConfig::DEFAULT_ATTEMPT_TIMEOUT
);
}
}