diff --git a/src/benchmark/executor.rs b/src/benchmark/executor.rs index 03e2b6a..915b735 100644 --- a/src/benchmark/executor.rs +++ b/src/benchmark/executor.rs @@ -79,23 +79,41 @@ fn run_command_and_measure_common( let result = execute_and_measure(command) .with_context(|| format!("Failed to run command '{command_name}'"))?; - if command_failure_action == CmdFailureAction::RaiseError && !result.status.success() { - let when = match iteration { - BenchmarkIteration::NonBenchmarkRun => "a non-benchmark run".to_string(), - BenchmarkIteration::Warmup(0) => "the first warmup run".to_string(), - BenchmarkIteration::Warmup(i) => format!("warmup iteration {i}"), - BenchmarkIteration::Benchmark(0) => "the first benchmark run".to_string(), - BenchmarkIteration::Benchmark(i) => format!("benchmark iteration {i}"), - }; - bail!( - "{cause} in {when}. Use the '-i'/'--ignore-failure' option if you want to ignore this. \ - Alternatively, use the '--show-output' option to debug what went wrong.", - cause=result.status.code().map_or( - "The process has been terminated by a signal".into(), - |c| format!("Command terminated with non-zero exit code {c}") + if !result.status.success() { + use crate::util::exit_code::extract_exit_code; - ), - ); + let should_fail = match command_failure_action { + CmdFailureAction::RaiseError => true, + CmdFailureAction::IgnoreAllFailures => false, + CmdFailureAction::IgnoreSpecificFailures(ref codes) => { + // Only fail if the exit code is not in the list of codes to ignore + if let Some(exit_code) = extract_exit_code(result.status) { + !codes.contains(&exit_code) + } else { + // If we can't extract an exit code, treat it as a failure + true + } + } + }; + + if should_fail { + let when = match iteration { + BenchmarkIteration::NonBenchmarkRun => "a non-benchmark run".to_string(), + BenchmarkIteration::Warmup(0) => "the first warmup run".to_string(), + BenchmarkIteration::Warmup(i) => format!("warmup iteration {i}"), + BenchmarkIteration::Benchmark(0) => "the first benchmark run".to_string(), + BenchmarkIteration::Benchmark(i) => format!("benchmark iteration {i}"), + }; + bail!( + "{cause} in {when}. Use the '-i'/'--ignore-failure' option if you want to ignore this. \ + Alternatively, use the '--show-output' option to debug what went wrong.", + cause=result.status.code().map_or( + "The process has been terminated by a signal".into(), + |c| format!("Command terminated with non-zero exit code {c}") + + ), + ); + } } Ok(result) @@ -122,7 +140,7 @@ impl Executor for RawExecutor<'_> { let result = run_command_and_measure_common( command.get_command()?, iteration, - command_failure_action.unwrap_or(self.options.command_failure_action), + command_failure_action.unwrap_or_else(|| self.options.command_failure_action.clone()), &self.options.command_input_policy, output_policy, &command.get_command_line(), @@ -187,7 +205,7 @@ impl Executor for ShellExecutor<'_> { let mut result = run_command_and_measure_common( command_builder, iteration, - command_failure_action.unwrap_or(self.options.command_failure_action), + command_failure_action.unwrap_or_else(|| self.options.command_failure_action.clone()), &self.options.command_input_policy, output_policy, &command.get_command_line(), diff --git a/src/cli.rs b/src/cli.rs index eceabcb..b12f6d3 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -226,9 +226,15 @@ fn build_command() -> Command { .arg( Arg::new("ignore-failure") .long("ignore-failure") - .action(ArgAction::SetTrue) + .action(ArgAction::Set) + .value_name("MODE") + .num_args(0..=1) + .default_missing_value("all-non-zero") + .require_equals(true) .short('i') - .help("Ignore non-zero exit codes of the benchmarked programs."), + .help("Ignore failures of the benchmarked programs. Without a value or with \ + 'all-non-zero', all non-zero exit codes are ignored. You can also provide \ + a comma-separated list of exit codes to ignore (e.g., --ignore-failure=1,2)."), ) .arg( Arg::new("style") diff --git a/src/options.rs b/src/options.rs index 1d5f2d5..7c83da1 100644 --- a/src/options.rs +++ b/src/options.rs @@ -67,13 +67,16 @@ impl Shell { } /// Action to take when an executed command fails. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum CmdFailureAction { /// Exit with an error message RaiseError, - /// Simply ignore the non-zero exit code - Ignore, + /// Ignore all non-zero exit codes + IgnoreAllFailures, + + /// Ignore specific exit codes + IgnoreSpecificFailures(Vec), } /// Output style type option @@ -415,8 +418,21 @@ impl Options { } }; - if matches.get_flag("ignore-failure") { - options.command_failure_action = CmdFailureAction::Ignore; + if let Some(mode) = matches.get_one::("ignore-failure") { + options.command_failure_action = match mode.as_str() { + "all-non-zero" | "" => CmdFailureAction::IgnoreAllFailures, + codes => { + let exit_codes: Result, _> = codes + .split(',') + .map(|s| { + s.trim() + .parse::() + .map_err(|e| OptionsError::IntParsingError("ignore-failure", e)) + }) + .collect(); + CmdFailureAction::IgnoreSpecificFailures(exit_codes?) + } + }; } options.time_unit = match matches.get_one::("time-unit").map(|s| s.as_str()) { diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index 4672808..2de3a50 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -277,6 +277,78 @@ fn can_run_failing_commands_with_ignore_failure_option() { .success(); } +#[cfg(unix)] +#[test] +fn can_ignore_specific_exit_codes() { + // Test that specifying exit code 1 ignores it + hyperfine() + .arg("--runs=1") + .arg("--ignore-failure=1") + .arg("exit 1") + .assert() + .success(); + + // Test that other exit codes still fail + hyperfine() + .arg("--runs=1") + .arg("--ignore-failure=1") + .arg("exit 2") + .assert() + .failure() + .stderr(predicate::str::contains( + "Command terminated with non-zero exit code 2", + )); +} + +#[cfg(unix)] +#[test] +fn can_ignore_multiple_exit_codes() { + // Test that all specified exit codes are ignored + hyperfine() + .arg("--runs=1") + .arg("--ignore-failure=1,2,3") + .arg("exit 1") + .assert() + .success(); + + hyperfine() + .arg("--runs=1") + .arg("--ignore-failure=1,2,3") + .arg("exit 2") + .assert() + .success(); + + hyperfine() + .arg("--runs=1") + .arg("--ignore-failure=1,2,3") + .arg("exit 3") + .assert() + .success(); + + // Test that other exit codes still fail + hyperfine() + .arg("--runs=1") + .arg("--ignore-failure=1,2,3") + .arg("exit 4") + .assert() + .failure() + .stderr(predicate::str::contains( + "Command terminated with non-zero exit code 4", + )); +} + +#[cfg(unix)] +#[test] +fn ignore_failure_with_all_non_zero() { + // Test explicit "all-non-zero" mode + hyperfine() + .arg("--runs=1") + .arg("--ignore-failure=all-non-zero") + .arg("exit 5") + .assert() + .success(); +} + #[test] fn shows_output_of_benchmarked_command() { hyperfine()