From c4f262e1c772cd37e465923e20802682663fee1a Mon Sep 17 00:00:00 2001 From: not-matthias Date: Thu, 23 Jul 2026 17:48:15 +0200 Subject: [PATCH 1/2] fix(exec): reject empty command before executor setup `codspeed exec` with no command, and `codspeed run` with an empty or whitespace-only `exec` target, built a BenchmarkTarget with an empty command that only failed deep inside exec-harness ("Empty command in stdin input"), surfaced as the cryptic "failed to execute memory tracker process: exit status: 1" after a full memtrack probe attach. Enforce a non-empty command at each origin: mark the exec positional clap-required, and bail in build_benchmark_targets when a config exec command parses to zero words. --- src/cli/exec/mod.rs | 76 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/src/cli/exec/mod.rs b/src/cli/exec/mod.rs index 89952255..3be07531 100644 --- a/src/cli/exec/mod.rs +++ b/src/cli/exec/mod.rs @@ -30,6 +30,7 @@ pub struct ExecArgs { pub name: Option, /// The command to execute with the exec harness + #[arg(required = true)] pub command: Vec, } @@ -125,6 +126,8 @@ pub async fn execute_config( api_client: &mut CodSpeedAPIClient, setup_cache_dir: Option<&Path>, ) -> Result<()> { + ensure_exec_commands_runnable(&config.targets)?; + let orchestrator = executor::Orchestrator::new(config, api_client).await?; if !orchestrator.is_local() { @@ -137,3 +140,76 @@ pub async fn execute_config( Ok(()) } + +/// Rejects exec targets whose executable (first token) is missing or empty before +/// the orchestrator does any setup. An empty executable otherwise surfaces only as an +/// opaque failure deep inside exec-harness. Empty *arguments* after a real executable +/// stay valid. +fn ensure_exec_commands_runnable(targets: &[executor::BenchmarkTarget]) -> Result<()> { + for target in targets { + let executor::BenchmarkTarget::Exec { command, name, .. } = target else { + continue; + }; + if command.first().is_none_or(|exe| exe.is_empty()) { + let label = name.as_deref().unwrap_or(""); + bail!( + "Empty command for exec benchmark target `{label}`. Provide a program to run \ + (e.g. `codspeed exec -- [args...]`, or set a non-empty `exec` in the config)." + ); + } + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use crate::cli::Cli; + use crate::executor; + use clap::Parser; + + #[test] + fn exec_requires_a_command() { + // `codspeed exec` with no command must be rejected at parse time instead of + // proceeding into executor setup with an empty command. + assert!(Cli::try_parse_from(["codspeed", "exec"]).is_err()); + } + + #[test] + fn exec_accepts_a_command() { + assert!(Cli::try_parse_from(["codspeed", "exec", "echo", "hello"]).is_ok()); + } + + fn exec_target(command: &[&str], name: Option<&str>) -> executor::BenchmarkTarget { + executor::BenchmarkTarget::Exec { + command: command.iter().map(|s| s.to_string()).collect(), + name: name.map(str::to_string), + walltime_args: Default::default(), + } + } + + #[test] + fn rejects_missing_or_empty_executable() { + // Empty vec (`exec: ""` / whitespace) and an empty first token + // (`codspeed exec ''` / `exec: "''"`) are both invalid. + assert!(super::ensure_exec_commands_runnable(&[exec_target(&[], Some("a"))]).is_err()); + let err = super::ensure_exec_commands_runnable(&[exec_target(&[""], Some("bench"))]) + .unwrap_err() + .to_string(); + assert!(err.contains("bench"), "{err}"); + } + + #[test] + fn accepts_runnable_command_with_empty_argument() { + // An empty *argument* after a real executable stays valid (e.g. `grep '' file`). + assert!(super::ensure_exec_commands_runnable(&[exec_target(&["grep", ""], None)]).is_ok()); + } + + #[test] + fn ignores_entrypoint_targets() { + let target = executor::BenchmarkTarget::Entrypoint { + command: String::new(), + name: None, + }; + assert!(super::ensure_exec_commands_runnable(&[target]).is_ok()); + } +} From 825e87c86075f118c5fc1651877ba92c9b06deba Mon Sep 17 00:00:00 2001 From: not-matthias Date: Fri, 24 Jul 2026 15:42:52 +0200 Subject: [PATCH 2/2] fix(exec): reject whitespace-only executable token --- src/cli/exec/mod.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/cli/exec/mod.rs b/src/cli/exec/mod.rs index 3be07531..01bdd6da 100644 --- a/src/cli/exec/mod.rs +++ b/src/cli/exec/mod.rs @@ -141,8 +141,8 @@ pub async fn execute_config( Ok(()) } -/// Rejects exec targets whose executable (first token) is missing or empty before -/// the orchestrator does any setup. An empty executable otherwise surfaces only as an +/// Rejects exec targets whose executable (first token) is missing or blank before +/// the orchestrator does any setup. A blank executable otherwise surfaces only as an /// opaque failure deep inside exec-harness. Empty *arguments* after a real executable /// stay valid. fn ensure_exec_commands_runnable(targets: &[executor::BenchmarkTarget]) -> Result<()> { @@ -150,7 +150,7 @@ fn ensure_exec_commands_runnable(targets: &[executor::BenchmarkTarget]) -> Resul let executor::BenchmarkTarget::Exec { command, name, .. } = target else { continue; }; - if command.first().is_none_or(|exe| exe.is_empty()) { + if command.first().is_none_or(|exe| exe.trim().is_empty()) { let label = name.as_deref().unwrap_or(""); bail!( "Empty command for exec benchmark target `{label}`. Provide a program to run \ @@ -189,9 +189,10 @@ mod tests { #[test] fn rejects_missing_or_empty_executable() { - // Empty vec (`exec: ""` / whitespace) and an empty first token - // (`codspeed exec ''` / `exec: "''"`) are both invalid. + // Empty vec (`exec: ""`), an empty first token (`codspeed exec ''` / `exec: "''"`), + // and a whitespace-only token (`codspeed exec " "`) are all invalid. assert!(super::ensure_exec_commands_runnable(&[exec_target(&[], Some("a"))]).is_err()); + assert!(super::ensure_exec_commands_runnable(&[exec_target(&[" "], Some("a"))]).is_err()); let err = super::ensure_exec_commands_runnable(&[exec_target(&[""], Some("bench"))]) .unwrap_err() .to_string();