From 907de26344b896dc674a19995daf33cfb738aad8 Mon Sep 17 00:00:00 2001 From: Vincent <93332837+VXNCXNX@users.noreply.github.com> Date: Fri, 2 Oct 2026 11:56:27 +0200 Subject: [PATCH] Fix panic on --runs 0 (#923) * fix: reject --runs/--max-runs 0, clamp --min-runs to 1 * Validate positive run counts with clap --------- Co-authored-by: VXNCXNX Co-authored-by: David Peter --- src/cli.rs | 4 ++++ src/options.rs | 21 +++++++-------------- tests/integration_tests.rs | 26 ++++++++++++++++++++++++++ 3 files changed, 37 insertions(+), 14 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 5c58761..992b82c 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -41,6 +41,7 @@ fn build_command() -> Command { .short('w') .value_name("NUM") .action(ArgAction::Set) + .value_parser(clap::value_parser!(u64)) .help( "Perform NUM warmup runs before the actual benchmark. This can be used \ to fill (disk) caches for I/O-heavy programs.", @@ -52,6 +53,7 @@ fn build_command() -> Command { .short('m') .action(ArgAction::Set) .value_name("NUM") + .value_parser(clap::value_parser!(u64).range(1..)) .help("Perform at least NUM runs for each command (default: 10)."), ) .arg( @@ -60,6 +62,7 @@ fn build_command() -> Command { .short('M') .action(ArgAction::Set) .value_name("NUM") + .value_parser(clap::value_parser!(u64).range(1..)) .help("Perform at most NUM runs for each command. By default, there is no limit."), ) .arg( @@ -69,6 +72,7 @@ fn build_command() -> Command { .short('r') .action(ArgAction::Set) .value_name("NUM") + .value_parser(clap::value_parser!(u64).range(1..)) .help("Perform exactly NUM runs for each command. If this option is not specified, \ hyperfine automatically determines the number of runs."), ) diff --git a/src/options.rs b/src/options.rs index 7c83da1..92d3823 100644 --- a/src/options.rs +++ b/src/options.rs @@ -275,22 +275,15 @@ impl Default for Options { impl Options { pub fn from_cli_arguments<'a>(matches: &ArgMatches) -> Result> { let mut options = Self::default(); - let param_to_u64 = |param| { - matches - .get_one::(param) - .map(|n| { - n.parse::() - .map_err(|e| OptionsError::IntParsingError(param, e)) - }) - .transpose() - }; + options.warmup_count = matches + .get_one::("warmup") + .copied() + .unwrap_or(options.warmup_count); - options.warmup_count = param_to_u64("warmup")?.unwrap_or(options.warmup_count); + let mut min_runs = matches.get_one::("min-runs").copied(); + let mut max_runs = matches.get_one::("max-runs").copied(); - let mut min_runs = param_to_u64("min-runs")?; - let mut max_runs = param_to_u64("max-runs")?; - - if let Some(runs) = param_to_u64("runs")? { + if let Some(&runs) = matches.get_one::("runs") { min_runs = Some(runs); max_runs = Some(runs); } diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index 54e3844..1141827 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -74,6 +74,32 @@ fn exits_quietly_when_stdout_is_closed() { } } +#[test] +fn fails_with_zero_runs() { + for option in ["--runs", "--min-runs", "--max-runs"] { + hyperfine() + .arg(option) + .arg("0") + .arg("echo dummy benchmark") + .assert() + .code(2) + .stderr(predicate::str::contains(format!( + "invalid value '0' for '{option} '" + ))); + } +} + +#[test] +fn min_runs_of_one_still_performs_one_run() { + hyperfine_debug() + .arg("--min-runs=1") + .arg("--warmup=0") + .arg("sleep 4") + .assert() + .success() + .stdout(predicate::str::contains("Time (abs ≡)")); +} + #[test] fn can_run_commands_without_a_shell() { hyperfine()