Skip to content

Commit 51cffa2

Browse files
committed
feat(cli): add --ignore-failure to exec command
Adds -i / --ignore-failure on the exec command, inspired by hyperfine. When set, a non-zero exit from the benchmarked process is logged as a warning and execution continues, instead of aborting. Reworked per review: the flag now lives in the exec-harness crate (where the user command is directly executed), not at the executor level. Scoped to codspeed exec only, not codspeed run. Closes #242
1 parent be3c3b2 commit 51cffa2

8 files changed

Lines changed: 62 additions & 5 deletions

File tree

crates/exec-harness/src/analysis/mod.rs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,14 @@ pub fn perform(commands: Vec<BenchmarkCommand>) -> Result<()> {
2727
let status = status.context("Failed to execute command")?;
2828

2929
if !status.success() {
30-
bail!("Command exited with non-zero status: {status}");
30+
if benchmark_cmd.ignore_failure {
31+
warn!(
32+
"Command exited with non-zero status: {status}; \
33+
continuing because --ignore-failure is set"
34+
);
35+
} else {
36+
bail!("Command exited with non-zero status: {status}");
37+
}
3138
}
3239

3340
hooks.set_executed_benchmark(&name_and_uri.uri).unwrap();
@@ -68,7 +75,14 @@ pub fn perform_with_valgrind(commands: Vec<BenchmarkCommand>) -> Result<()> {
6875
bail_if_command_spawned_subprocesses_under_valgrind(child.id())?;
6976

7077
if !status.success() {
71-
bail!("Command exited with non-zero status: {status}");
78+
if benchmark_cmd.ignore_failure {
79+
warn!(
80+
"Command exited with non-zero status: {status}; \
81+
continuing because --ignore-failure is set"
82+
);
83+
} else {
84+
bail!("Command exited with non-zero status: {status}");
85+
}
7286
}
7387
}
7488

crates/exec-harness/src/lib.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ pub struct BenchmarkCommand {
3636
/// Walltime execution options (flattened into the JSON object)
3737
#[serde(default)]
3838
pub walltime_args: walltime::WalltimeExecutionArgs,
39+
40+
/// When true, a non-zero exit from the command is logged as a warning
41+
/// instead of aborting execution.
42+
#[serde(default)]
43+
pub ignore_failure: bool,
3944
}
4045

4146
/// Read and parse benchmark commands from stdin as JSON

crates/exec-harness/src/main.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@ struct Args {
2020
#[arg(short, long, global = true, env = "CODSPEED_RUNNER_MODE", hide = true)]
2121
measurement_mode: Option<MeasurementMode>,
2222

23+
/// Allow the benchmarked command to exit with a non-zero status code.
24+
///
25+
/// When set, a non-zero exit from the benchmarked process is logged as a
26+
/// warning and measurement continues, instead of aborting.
27+
#[arg(short = 'i', long, default_value = "false")]
28+
ignore_failure: bool,
29+
2330
#[command(flatten)]
2431
walltime_args: WalltimeExecutionArgs,
2532

@@ -51,6 +58,7 @@ fn main() -> Result<()> {
5158
command: args.command,
5259
name: args.name,
5360
walltime_args: args.walltime_args,
61+
ignore_failure: args.ignore_failure,
5462
}],
5563
};
5664

crates/exec-harness/src/walltime/benchmark_loop.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ pub fn run_rounds(
1010
bench_uri: String,
1111
command: Vec<String>,
1212
config: &ExecutionOptions,
13+
ignore_failure: bool,
1314
) -> Result<Vec<u128>> {
1415
let warmup_time_ns = config.warmup_time_ns;
1516
let hooks = InstrumentHooks::instance(INTEGRATION_NAME, INTEGRATION_VERSION);
@@ -27,7 +28,14 @@ pub fn run_rounds(
2728
let bench_round_end_ts_ns = InstrumentHooks::current_timestamp();
2829

2930
if !status.success() {
30-
bail!("Command exited with non-zero status: {status}");
31+
if ignore_failure {
32+
warn!(
33+
"Command exited with non-zero status: {status}; \
34+
continuing because --ignore-failure is set"
35+
);
36+
} else {
37+
bail!("Command exited with non-zero status: {status}");
38+
}
3139
}
3240

3341
Ok((bench_round_start_ts_ns, bench_round_end_ts_ns))

crates/exec-harness/src/walltime/mod.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,12 @@ pub fn perform(commands: Vec<BenchmarkCommand>) -> Result<()> {
2828
..
2929
} = name_and_uri;
3030

31-
let times_per_round_ns =
32-
benchmark_loop::run_rounds(bench_uri.clone(), cmd.command, &execution_options)?;
31+
let times_per_round_ns = benchmark_loop::run_rounds(
32+
bench_uri.clone(),
33+
cmd.command,
34+
&execution_options,
35+
cmd.ignore_failure,
36+
)?;
3337

3438
// Collect walltime results
3539
let max_time_ns = times_per_round_ns.iter().copied().max();

src/cli/exec/mod.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,14 @@ pub struct ExecArgs {
3030
#[arg(long)]
3131
pub name: Option<String>,
3232

33+
/// Allow the benchmarked command to exit with a non-zero status code.
34+
///
35+
/// When set, a non-zero exit from the benchmarked process is logged as a
36+
/// warning and measurement continues, instead of aborting. Mirrors
37+
/// hyperfine's `-i / --ignore-failure`.
38+
#[arg(short = 'i', long, default_value = "false")]
39+
pub ignore_failure: bool,
40+
3341
/// The command to execute with the exec harness
3442
pub command: Vec<String>,
3543
}
@@ -106,6 +114,7 @@ pub async fn run(
106114
command: merged_args.command.clone(),
107115
name: merged_args.name.clone(),
108116
walltime_args: merged_args.walltime_args.clone(),
117+
ignore_failure: merged_args.ignore_failure,
109118
};
110119
let config = build_orchestrator_config(
111120
merged_args,

src/cli/exec/multi_targets.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ pub fn build_benchmark_targets(
5959
command,
6060
name: target.name.clone(),
6161
walltime_args,
62+
ignore_failure: false,
6263
})
6364
}
6465
TargetCommand::Entrypoint { entrypoint } => Ok(BenchmarkTarget::Entrypoint {
@@ -80,10 +81,12 @@ pub fn build_exec_targets_pipe_command(
8081
command,
8182
name,
8283
walltime_args,
84+
ignore_failure,
8385
} => Ok(BenchmarkCommand {
8486
command: command.clone(),
8587
name: name.clone(),
8688
walltime_args: walltime_args.clone(),
89+
ignore_failure: *ignore_failure,
8790
}),
8891
crate::executor::config::BenchmarkTarget::Entrypoint { .. } => {
8992
bail!("Entrypoint targets cannot be used with exec-harness pipe command")

src/executor/config.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ pub enum BenchmarkTarget {
2222
command: Vec<String>,
2323
name: Option<String>,
2424
walltime_args: exec_harness::walltime::WalltimeExecutionArgs,
25+
/// When true, a non-zero exit from the command is tolerated.
26+
ignore_failure: bool,
2527
},
2628
/// A command with built-in harness (e.g. `pytest --codspeed src`)
2729
Entrypoint {
@@ -286,11 +288,13 @@ mod tests {
286288
command: vec!["exec1".into()],
287289
name: None,
288290
walltime_args: Default::default(),
291+
ignore_failure: false,
289292
},
290293
BenchmarkTarget::Exec {
291294
command: vec!["exec2".into()],
292295
name: None,
293296
walltime_args: Default::default(),
297+
ignore_failure: false,
294298
},
295299
],
296300
modes: vec![RunnerMode::Simulation],
@@ -305,6 +309,7 @@ mod tests {
305309
command: vec!["exec1".into()],
306310
name: None,
307311
walltime_args: Default::default(),
312+
ignore_failure: false,
308313
},
309314
BenchmarkTarget::Entrypoint {
310315
command: "cmd".into(),
@@ -336,6 +341,7 @@ mod tests {
336341
command: vec!["exec1".into()],
337342
name: None,
338343
walltime_args: Default::default(),
344+
ignore_failure: false,
339345
},
340346
BenchmarkTarget::Entrypoint {
341347
command: "cmd".into(),

0 commit comments

Comments
 (0)