use crate::cli_commands::load_data_file;
use crate::config_file::PartialConfig;
use tropel_core::config::*;
pub(crate) fn apply_overlay(
config: &mut JobConfig,
overlay: PartialConfig,
cli_reporters: &[String],
cli_insecure: bool,
cli_load_profile_explicit: bool,
iteration_data_is_empty: bool,
) {
if config.input_type.is_none() {
config.input_type = overlay.input_type.clone();
}
if !cli_load_profile_explicit {
if let Some(exec) = overlay.execution {
tracing::info!("Using execution from config file / K6_* env: {:?}", exec);
config.execution = exec;
config.execution_explicit = true;
}
}
if config.execution_segment.is_none() {
config.execution_segment = overlay.execution_segment.clone();
}
if config.execution_segment_sequence.is_none() {
config.execution_segment_sequence = overlay.execution_segment_sequence.clone();
}
if config.control_port.is_none() {
config.control_port = overlay.control_port;
}
for (k, v) in &overlay.env {
config.env.entry(k.clone()).or_insert_with(|| v.clone());
}
for (k, v) in &overlay.thresholds {
config
.thresholds
.entry(k.clone())
.or_insert_with(|| v.clone());
}
if let Some(out) = &overlay.output {
let cli_reporters_default = cli_reporters.len() == 1 && cli_reporters[0] == "stdout";
if cli_reporters_default && !out.reporters.is_empty() {
config.output.reporters = out.reporters.clone();
}
if config.output.output_file.is_none() {
config.output.output_file = out.output_file.clone();
}
if config.output.prometheus_remote_write_url.is_none() {
config.output.prometheus_remote_write_url = out.prometheus_remote_write_url.clone();
}
if config.output.otlp_endpoint.is_none() {
config.output.otlp_endpoint = out.otlp_endpoint.clone();
}
if config.output.summary_export.is_none() {
config.output.summary_export = out.summary_export.clone();
}
if config.output.json_stream.is_none() {
config.output.json_stream = out.json_stream.clone();
}
if config.output.statsd_addr.is_none() {
config.output.statsd_addr = out.statsd_addr.clone();
}
if config.output.influxdb_addr.is_none() {
config.output.influxdb_addr = out.influxdb_addr.clone();
}
config.output.summary = out.summary;
config.output.trends = out.trends;
}
if let Some(scenarios) = &overlay.scenarios {
if !scenarios.is_empty() {
config.scenarios = scenarios.clone();
}
}
if let Some(http) = &overlay.http {
config.http = http.clone();
}
if let Some(tls) = &overlay.tls {
let mut merged = tls.clone();
if cli_insecure {
merged.insecure_skip_verify = true;
}
config.tls = merged;
}
if !overlay.globals.is_empty() {
config.globals.extend(overlay.globals.clone());
}
if !overlay.collection_vars.is_empty() {
config
.collection_vars
.extend(overlay.collection_vars.clone());
}
if overlay.data_file.is_some() && !overlay.iteration_data.is_empty() {
tracing::warn!(
"Config file sets both data_file and iteration_data — inline iteration_data wins"
);
} else if iteration_data_is_empty {
if let Some(data_path) = overlay.data_file {
match load_data_file(&std::path::PathBuf::from(&data_path)) {
Ok(data) => config.iteration_data = data,
Err(e) => {
tracing::warn!(
"Failed to load config-file data_file '{}': {}",
data_path,
e
);
}
}
}
}
if !overlay.iteration_data.is_empty() {
config.iteration_data = overlay.iteration_data.clone();
}
if !overlay.extensions.is_empty() {
config.extensions.extend(overlay.extensions.clone());
}
}
pub(crate) fn merge_partial(base: PartialConfig, file: PartialConfig) -> PartialConfig {
PartialConfig {
input_type: file.input_type.or(base.input_type),
execution: file.execution.or(base.execution),
scenarios: file.scenarios.or(base.scenarios),
env: base.env.into_iter().chain(file.env).collect(),
globals: base.globals.into_iter().chain(file.globals).collect(),
collection_vars: base
.collection_vars
.into_iter()
.chain(file.collection_vars)
.collect(),
data_file: file.data_file.or(base.data_file),
iteration_data: if file.iteration_data.is_empty() {
base.iteration_data
} else {
file.iteration_data
},
thresholds: base.thresholds.into_iter().chain(file.thresholds).collect(),
output: file.output.or(base.output),
http: file.http.or(base.http),
tls: file.tls.or(base.tls),
extensions: base.extensions.into_iter().chain(file.extensions).collect(),
execution_segment: file.execution_segment.or(base.execution_segment),
execution_segment_sequence: file
.execution_segment_sequence
.or(base.execution_segment_sequence),
control_port: file.control_port.or(base.control_port),
}
}
#[cfg(test)]
mod overlay_tests {
use super::*;
fn base_config() -> JobConfig {
JobConfig {
input: "test.js".to_string(),
execution: ExecutionConfig::ConstantVus {
vus: 1,
duration: "30s".to_string(),
graceful_stop: None,
think_time: ThinkTimeConfig::default(),
},
..Default::default()
}
}
#[test]
fn test_overlay_execution_when_no_cli_load_flags() {
let mut cfg = base_config();
let overlay = PartialConfig {
execution: Some(ExecutionConfig::SharedIterations {
iterations: 50,
max_duration: None,
vus: 5,
graceful_stop: None,
think_time: ThinkTimeConfig::default(),
}),
..Default::default()
};
apply_overlay(
&mut cfg,
overlay,
&["stdout".to_string()],
false,
false,
true,
);
match cfg.execution {
ExecutionConfig::SharedIterations {
iterations, vus, ..
} => {
assert_eq!(iterations, 50);
assert_eq!(vus, 5);
}
other => panic!("expected SharedIterations, got {other:?}"),
}
assert!(cfg.execution_explicit);
}
#[test]
fn test_overlay_execution_ignored_when_cli_flags_explicit() {
let mut cfg = base_config();
let overlay = PartialConfig {
execution: Some(ExecutionConfig::SharedIterations {
iterations: 999,
max_duration: None,
vus: 5,
graceful_stop: None,
think_time: ThinkTimeConfig::default(),
}),
..Default::default()
};
apply_overlay(
&mut cfg,
overlay,
&["stdout".to_string()],
false,
true,
true,
);
match cfg.execution {
ExecutionConfig::ConstantVus { vus, .. } => assert_eq!(vus, 1),
other => panic!("expected ConstantVus, got {other:?}"),
}
assert!(!cfg.execution_explicit);
}
#[test]
fn test_overlay_reporters_replace_default_stdout() {
let mut cfg = base_config();
let overlay = PartialConfig {
output: Some(OutputConfig {
reporters: vec!["json".to_string()],
..Default::default()
}),
..Default::default()
};
apply_overlay(
&mut cfg,
overlay,
&["stdout".to_string()],
false,
false,
true,
);
assert_eq!(cfg.output.reporters, vec!["json".to_string()]);
}
#[test]
fn test_overlay_reporters_do_not_override_cli_flag() {
let mut cfg = base_config();
cfg.output.reporters = vec!["csv".to_string()];
let overlay = PartialConfig {
output: Some(OutputConfig {
reporters: vec!["json".to_string()],
..Default::default()
}),
..Default::default()
};
apply_overlay(&mut cfg, overlay, &["csv".to_string()], false, false, true);
assert_eq!(cfg.output.reporters, vec!["csv".to_string()]);
}
#[test]
fn test_overlay_env_thresholds_fill_not_override() {
let mut cfg = base_config();
cfg.env.insert("CLI_ONLY".to_string(), "1".to_string());
let overlay = PartialConfig {
env: [
("CLI_ONLY".to_string(), "overridden".to_string()),
("NEW".to_string(), "2".to_string()),
]
.into_iter()
.collect(),
..Default::default()
};
apply_overlay(
&mut cfg,
overlay,
&["stdout".to_string()],
false,
false,
true,
);
assert_eq!(cfg.env.get("CLI_ONLY").map(|s| s.as_str()), Some("1"));
assert_eq!(cfg.env.get("NEW").map(|s| s.as_str()), Some("2"));
}
#[test]
fn test_overlay_tls_insecure_cli_wins() {
let mut cfg = base_config();
let overlay = PartialConfig {
tls: Some(TlsConfig {
insecure_skip_verify: false,
..Default::default()
}),
..Default::default()
};
apply_overlay(
&mut cfg,
overlay,
&["stdout".to_string()],
true,
false,
true,
);
assert!(cfg.tls.insecure_skip_verify);
}
#[test]
fn test_merge_partial_file_wins_over_env() {
let base = PartialConfig {
env: [("K".to_string(), "base".to_string())]
.into_iter()
.collect(),
execution: Some(ExecutionConfig::ConstantVus {
vus: 1,
duration: "10s".to_string(),
graceful_stop: None,
think_time: ThinkTimeConfig::default(),
}),
..Default::default()
};
let file = PartialConfig {
env: [("K".to_string(), "file".to_string())]
.into_iter()
.collect(),
..Default::default()
};
let merged = merge_partial(base, file);
assert_eq!(merged.env.get("K").map(|s| s.as_str()), Some("file"));
assert!(merged.execution.is_some());
}
}