Skip to content

Commit 742e79d

Browse files
fix: resolve working_directory relative to config file, not CWD
When running `codspeed run` with config targets, the `working-directory` option is now resolved relative to the config file's directory instead of the current working directory. When no `working-directory` is set, it defaults to the config file's directory. For `codspeed run -- command` and `codspeed exec`, the config's `working-directory` is explicitly ignored — only the `--working-directory` CLI flag is used. A warning is emitted if `--working-directory` is passed in config targets mode.
1 parent fccfc51 commit 742e79d

7 files changed

Lines changed: 467 additions & 105 deletions

File tree

Cargo.lock

Lines changed: 83 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ test-with = { version = "0.15", default-features = false, features = [] }
8484
rstest = { version = "0.25.0", default-features = false }
8585
rstest_reuse = "0.7.0"
8686
shell-quote = "0.7.2"
87+
assert_cmd = "2.0.16"
88+
predicates = "3.1.4"
8789

8890
[workspace]
8991
members = [

src/cli/exec/mod.rs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,6 @@ impl ExecArgs {
4040
/// CLI arguments take precedence over config values.
4141
pub fn merge_with_project_config(mut self, project_config: Option<&ProjectConfig>) -> Self {
4242
if let Some(project_config) = project_config {
43-
// Merge shared args
44-
self.shared =
45-
ConfigMerger::merge_shared_args(&self.shared, project_config.options.as_ref());
46-
// Merge walltime args
4743
self.walltime_args = ConfigMerger::merge_walltime_options(
4844
&self.walltime_args,
4945
project_config

src/cli/run/mod.rs

Lines changed: 39 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@ use crate::executor;
55
use crate::executor::config::{self, OrchestratorConfig, RepositoryOverride};
66
use crate::instruments::Instruments;
77
use crate::prelude::*;
8-
use crate::project_config::merger::ConfigMerger;
9-
use crate::project_config::{DiscoveredProjectConfig, ProjectConfig};
8+
use crate::project_config::DiscoveredProjectConfig;
109
use crate::run_environment::interfaces::RepositoryProvider;
1110
use crate::upload::poll_results::PollResultsOptions;
1211
use clap::{Args, ValueEnum};
@@ -40,19 +39,6 @@ pub struct RunArgs {
4039
pub command: Vec<String>,
4140
}
4241

43-
impl RunArgs {
44-
/// Merge CLI args with project config if available
45-
///
46-
/// CLI arguments take precedence over config values.
47-
pub fn merge_with_project_config(mut self, project_config: Option<&ProjectConfig>) -> Self {
48-
if let Some(project_config) = project_config {
49-
self.shared =
50-
ConfigMerger::merge_shared_args(&self.shared, project_config.options.as_ref());
51-
}
52-
self
53-
}
54-
}
55-
5642
#[derive(ValueEnum, Clone, Debug, PartialEq)]
5743
pub enum MessageFormat {
5844
Json,
@@ -158,7 +144,6 @@ pub async fn run(
158144
let output_json = args.message_format == Some(MessageFormat::Json);
159145
let project_config = discovered_config.map(|d| &d.config);
160146

161-
let args = args.merge_with_project_config(project_config);
162147
let run_target = if args.command.is_empty() {
163148
// No command provided - check for targets in project config
164149
let targets = project_config
@@ -183,6 +168,8 @@ pub async fn run(
183168

184169
match run_target {
185170
RunTarget::SingleCommand(args) => {
171+
// SingleCommand: working_directory comes from --working-directory CLI flag only.
172+
// Config file's working-directory is NOT used.
186173
let command = args.command.join(" ");
187174
let config = build_orchestrator_config(
188175
args,
@@ -192,6 +179,7 @@ pub async fn run(
192179
}],
193180
PollResultsOptions::for_run(output_json),
194181
)?;
182+
195183
let orchestrator =
196184
executor::Orchestrator::new(config, codspeed_config, api_client).await?;
197185

@@ -208,10 +196,44 @@ pub async fn run(
208196
targets,
209197
default_walltime,
210198
} => {
199+
// ConfigTargets: working_directory is resolved relative to config file dir.
200+
// If --working-directory CLI flag is passed, ignore it with a warning.
201+
if args.shared.working_directory.is_some() {
202+
// Intentionally using eprintln! because logger has not been initialized yet.
203+
eprintln!(
204+
"Warning: The --working-directory flag is ignored when running targets from the config file. \
205+
Use the `working-directory` option in the config file instead."
206+
);
207+
}
208+
209+
// Resolve working_directory relative to config file directory
210+
let resolved_working_directory =
211+
if let Some(config_dir) = discovered_config.and_then(|d| d.config_dir()) {
212+
let root_wd = project_config
213+
.and_then(|c| c.options.as_ref())
214+
.and_then(|o| o.working_directory.as_ref());
215+
216+
match root_wd {
217+
Some(wd) => {
218+
let wd_path = Path::new(wd);
219+
if wd_path.is_absolute() {
220+
Some(wd.clone())
221+
} else {
222+
Some(config_dir.join(wd).to_string_lossy().into_owned())
223+
}
224+
}
225+
None => Some(config_dir.to_string_lossy().into_owned()),
226+
}
227+
} else {
228+
None
229+
};
230+
211231
let benchmark_targets =
212232
super::exec::multi_targets::build_benchmark_targets(targets, default_walltime)?;
213-
let config =
233+
let mut config =
214234
build_orchestrator_config(args, benchmark_targets, PollResultsOptions::for_exec())?;
235+
config.working_directory = resolved_working_directory;
236+
215237
super::exec::execute_config(config, api_client, codspeed_config, setup_cache_dir)
216238
.await?;
217239
}

src/project_config/discover.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ const CONFIG_FILENAMES: &[&str] = &[
1313

1414
/// A project configuration paired with the path it was loaded from.
1515
#[derive(Debug)]
16-
#[allow(dead_code)]
1716
pub struct DiscoveredProjectConfig {
1817
pub config: ProjectConfig,
1918
pub path: PathBuf,
@@ -64,7 +63,6 @@ impl DiscoveredProjectConfig {
6463
}
6564

6665
/// Returns the directory containing the config file.
67-
#[allow(dead_code)]
6866
pub fn config_dir(&self) -> Option<PathBuf> {
6967
let canonical_path = self
7068
.path

src/project_config/merger.rs

Lines changed: 1 addition & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
use crate::cli::ExecAndRunSharedArgs;
21
use exec_harness::walltime::WalltimeExecutionArgs;
32

4-
use super::{ProjectOptions, WalltimeOptions};
3+
use super::WalltimeOptions;
54

65
/// Handles merging of CLI arguments with project configuration
76
///
@@ -35,20 +34,6 @@ impl ConfigMerger {
3534
}
3635
}
3736

38-
/// Merge shared args with project config options
39-
///
40-
/// CLI arguments take precedence over config values.
41-
/// Note: Some fields like upload_url, token, repository are CLI-only and not in config.
42-
pub fn merge_shared_args(
43-
cli: &ExecAndRunSharedArgs,
44-
_config_opts: Option<&ProjectOptions>,
45-
) -> ExecAndRunSharedArgs {
46-
// Note: working_directory is NOT merged here because config paths need to be
47-
// resolved relative to the config file directory. This resolution is handled
48-
// by the caller (e.g., `codspeed run`) which has access to the config file path.
49-
cli.clone()
50-
}
51-
5237
/// Helper to merge Option values with precedence: CLI > config > None
5338
fn merge_option<T: Clone>(cli_value: &Option<T>, config_value: Option<&T>) -> Option<T> {
5439
cli_value.clone().or_else(|| config_value.cloned())
@@ -58,31 +43,6 @@ impl ConfigMerger {
5843
#[cfg(test)]
5944
mod tests {
6045
use super::*;
61-
use crate::cli::PerfRunArgs;
62-
use crate::runner_mode::RunnerMode;
63-
64-
fn make_cli(working_directory: Option<&str>) -> ExecAndRunSharedArgs {
65-
ExecAndRunSharedArgs {
66-
upload_url: None,
67-
token: None,
68-
repository: None,
69-
provider: None,
70-
working_directory: working_directory.map(|s| s.to_string()),
71-
mode: vec![RunnerMode::Walltime],
72-
simulation_tool: None,
73-
profile_folder: None,
74-
skip_upload: false,
75-
skip_run: false,
76-
skip_setup: false,
77-
allow_empty: false,
78-
go_runner_version: None,
79-
show_full_output: false,
80-
perf_run_args: PerfRunArgs {
81-
enable_perf: true,
82-
perf_unwinding_mode: None,
83-
},
84-
}
85-
}
8646

8747
#[test]
8848
fn test_merge_walltime_all_from_cli() {
@@ -161,47 +121,6 @@ mod tests {
161121
assert_eq!(merged.min_rounds, None);
162122
}
163123

164-
#[test]
165-
fn test_merge_shared_args_working_directory_from_cli() {
166-
let cli = make_cli(Some("./cli-dir"));
167-
let config = ProjectOptions {
168-
walltime: None,
169-
working_directory: Some("./config-dir".to_string()),
170-
};
171-
172-
let merged = ConfigMerger::merge_shared_args(&cli, Some(&config));
173-
174-
// CLI working_directory should win
175-
assert_eq!(merged.working_directory, Some("./cli-dir".to_string()));
176-
}
177-
178-
#[test]
179-
fn test_merge_shared_args_working_directory_not_merged_from_config() {
180-
let cli = make_cli(None);
181-
let config = ProjectOptions {
182-
walltime: None,
183-
working_directory: Some("./config-dir".to_string()),
184-
};
185-
186-
let merged = ConfigMerger::merge_shared_args(&cli, Some(&config));
187-
188-
// Config working_directory is NOT merged — resolution is handled by the caller
189-
// relative to the config file directory.
190-
assert_eq!(merged.working_directory, None);
191-
// Mode stays as CLI value
192-
assert_eq!(merged.mode, vec![RunnerMode::Walltime]);
193-
}
194-
195-
#[test]
196-
fn test_merge_shared_args_no_config() {
197-
let cli = make_cli(Some("./dir"));
198-
199-
let merged = ConfigMerger::merge_shared_args(&cli, None);
200-
201-
// Should be identical to CLI
202-
assert_eq!(merged.working_directory, Some("./dir".to_string()));
203-
}
204-
205124
#[test]
206125
fn test_merge_option_helper() {
207126
// CLI value wins

0 commit comments

Comments
 (0)