Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 27 additions & 9 deletions src/config/apply.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,46 @@ mod section_runtime;
mod section_tail;
mod util;

use std::collections::BTreeMap;

use clap::ArgMatches;

use crate::args::TesterArgs;
use crate::config::types::ScenarioConfig;
use crate::error::{AppError, AppResult, ConfigError};

use super::types::ConfigFile;
use scenario::ScenarioDefaults;

/// Applies configuration values to CLI arguments.
/// Builds config-driven overrides over preset arguments.
///
/// # Errors
///
/// Returns an error when config values are invalid or conflict with CLI options.
pub fn apply_config(
args: &mut TesterArgs,
preset_args: TesterArgs,
matches: &ArgMatches,
config: &ConfigFile,
) -> AppResult<()> {
validate_config_conflicts(config)?;
section_basic::apply_basic_config(args, matches, config)?;
section_runtime::apply_runtime_config(args, matches, config)?;
section_tail::apply_tail_config(args, matches, config)?;
Ok(())
mut config: ConfigFile,
) -> AppResult<(TesterArgs, Option<BTreeMap<String, ScenarioConfig>>)> {
validate_config_conflicts(&config)?;

// Merge order is centralized here and intentionally explicit:
// command-line values in `preset_args` win, config fills missing values,
// and untouched fields keep preset defaults.
let mut effective_args = preset_args;
section_basic::apply_basic_config(&mut effective_args, matches, &config)?;
section_runtime::apply_runtime_config(&mut effective_args, matches, &config)?;
let scenario_defaults = ScenarioDefaults::new(
effective_args.url.clone(),
effective_args.method,
effective_args.data.clone(),
effective_args.headers.clone(),
);
section_tail::apply_tail_config(&mut effective_args, matches, &config, &scenario_defaults)?;

let scenario_registry = config.scenarios.take();

Ok((effective_args, scenario_registry))
}

fn validate_config_conflicts(config: &ConfigFile) -> AppResult<()> {
Expand Down
43 changes: 37 additions & 6 deletions src/config/apply/scenario.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,37 @@
use crate::args::{Scenario, ScenarioStep, TesterArgs, parse_header};
use crate::args::{HttpMethod, Scenario, ScenarioStep, parse_header};
use crate::error::{AppError, AppResult, ConfigError};

use super::super::types::{SCENARIO_SCHEMA_VERSION, ScenarioConfig};

pub(crate) fn parse_scenario(config: &ScenarioConfig, args: &TesterArgs) -> AppResult<Scenario> {
#[derive(Debug, Clone)]
pub(crate) struct ScenarioDefaults {
pub(crate) base_url: Option<String>,
pub(crate) method: HttpMethod,
pub(crate) body: String,
pub(crate) headers: Vec<(String, String)>,
}

impl ScenarioDefaults {
#[must_use]
pub(crate) const fn new(
base_url: Option<String>,
method: HttpMethod,
body: String,
headers: Vec<(String, String)>,
) -> Self {
Self {
base_url,
method,
body,
headers,
}
}
}

pub(crate) fn parse_scenario(
config: &ScenarioConfig,
defaults: &ScenarioDefaults,
) -> AppResult<Scenario> {
if let Some(schema_version) = config.schema_version
&& schema_version != SCENARIO_SCHEMA_VERSION
{
Expand All @@ -16,9 +44,12 @@ pub(crate) fn parse_scenario(config: &ScenarioConfig, args: &TesterArgs) -> AppR
return Err(AppError::config(ConfigError::ScenarioMissingSteps));
}

let base_url = config.base_url.clone().or_else(|| args.url.clone());
let default_method = config.method.unwrap_or(args.method);
let default_body = config.data.clone().unwrap_or_else(|| args.data.clone());
let base_url = config
.base_url
.clone()
.or_else(|| defaults.base_url.clone());
let default_method = config.method.unwrap_or(defaults.method);
let default_body = config.data.clone().unwrap_or_else(|| defaults.body.clone());

let default_headers = if let Some(headers) = config.headers.as_ref() {
let mut parsed = Vec::with_capacity(headers.len());
Expand All @@ -30,7 +61,7 @@ pub(crate) fn parse_scenario(config: &ScenarioConfig, args: &TesterArgs) -> AppR
}
parsed
} else {
args.headers.clone()
defaults.headers.clone()
};

let vars = config.vars.clone().unwrap_or_default();
Expand Down
5 changes: 3 additions & 2 deletions src/config/apply/section_tail.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@ use crate::error::AppResult;

use super::super::types::ConfigFile;
use super::distributed::apply_distributed_config;
use super::scenario::parse_scenario;
use super::scenario::{ScenarioDefaults, parse_scenario};
use super::util::{ensure_positive_u64, ensure_positive_usize, is_cli};

pub(super) fn apply_tail_config(
args: &mut TesterArgs,
matches: &ArgMatches,
config: &ConfigFile,
scenario_defaults: &ScenarioDefaults,
) -> AppResult<()> {
if !is_cli(matches, "metrics_max")
&& let Some(max) = config.metrics_max
Expand Down Expand Up @@ -56,7 +57,7 @@ pub(super) fn apply_tail_config(
}

if let Some(scenario) = config.scenario.as_ref() {
args.scenario = Some(parse_scenario(scenario, args)?);
args.scenario = Some(parse_scenario(scenario, scenario_defaults)?);
}

if let Some(sinks) = config.sinks.as_ref() {
Expand Down
36 changes: 18 additions & 18 deletions src/config/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,10 +147,10 @@ aws_sigv4 = "aws:amz:us-east-1:service"

let cmd = TesterArgs::command();
let matches = cmd.get_matches_from(["strest"]);
let mut args = TesterArgs::from_arg_matches(&matches)
let args = TesterArgs::from_arg_matches(&matches)
.map_err(|err| AppError::config(format!("parse args failed: {}", err)))?;

apply_config(&mut args, &matches, &config)?;
let args = apply_config(args, &matches, config)?.0;

if args.proxy_url.as_deref() != Some("http://127.0.0.1:8080") {
return Err(AppError::config("Unexpected proxy_url"));
Expand Down Expand Up @@ -264,10 +264,10 @@ fn apply_config_rejects_ipv4_ipv6_conflict() -> AppResult<()> {

let cmd = TesterArgs::command();
let matches = cmd.get_matches_from(["strest"]);
let mut args = TesterArgs::from_arg_matches(&matches)
let args = TesterArgs::from_arg_matches(&matches)
.map_err(|err| AppError::config(format!("parse args failed: {}", err)))?;

if apply_config(&mut args, &matches, &config).is_ok() {
if apply_config(args, &matches, config).is_ok() {
return Err(AppError::config("Expected ipv4/ipv6 conflict error"));
}

Expand All @@ -284,10 +284,10 @@ fn apply_config_rejects_conflicting_body_sources() -> AppResult<()> {

let cmd = TesterArgs::command();
let matches = cmd.get_matches_from(["strest"]);
let mut args = TesterArgs::from_arg_matches(&matches)
let args = TesterArgs::from_arg_matches(&matches)
.map_err(|err| AppError::config(format!("parse args failed: {}", err)))?;

if apply_config(&mut args, &matches, &config).is_ok() {
if apply_config(args, &matches, config).is_ok() {
return Err(AppError::config("Expected conflict error"));
}

Expand All @@ -304,10 +304,10 @@ fn apply_config_respects_cli_overrides() -> AppResult<()> {

let cmd = TesterArgs::command();
let matches = cmd.get_matches_from(["strest", "--url", "http://from-cli", "--no-charts"]);
let mut args = TesterArgs::from_arg_matches(&matches)
let args = TesterArgs::from_arg_matches(&matches)
.map_err(|err| AppError::config(format!("parse args failed: {}", err)))?;

apply_config(&mut args, &matches, &config)?;
let args = apply_config(args, &matches, config)?.0;

if args.url.as_deref() != Some("http://from-cli") {
return Err(AppError::config("Expected CLI url to win"));
Expand Down Expand Up @@ -337,10 +337,10 @@ fn apply_config_load_profile_rate_to_rpm() -> AppResult<()> {

let cmd = TesterArgs::command();
let matches = cmd.get_matches_from(["strest"]);
let mut args = TesterArgs::from_arg_matches(&matches)
let args = TesterArgs::from_arg_matches(&matches)
.map_err(|err| AppError::config(format!("parse args failed: {}", err)))?;

apply_config(&mut args, &matches, &config)?;
let args = apply_config(args, &matches, config)?.0;

let load = match args.load_profile {
Some(load) => load,
Expand Down Expand Up @@ -380,10 +380,10 @@ fn apply_config_rejects_load_and_rate_conflict() -> AppResult<()> {

let cmd = TesterArgs::command();
let matches = cmd.get_matches_from(["strest"]);
let mut args = TesterArgs::from_arg_matches(&matches)
let args = TesterArgs::from_arg_matches(&matches)
.map_err(|err| AppError::config(format!("parse args failed: {}", err)))?;

let result = apply_config(&mut args, &matches, &config);
let result = apply_config(args, &matches, config);
if result.is_err() {
Ok(())
} else {
Expand Down Expand Up @@ -425,10 +425,10 @@ fn apply_config_sets_warmup_and_tls() -> AppResult<()> {

let cmd = TesterArgs::command();
let matches = cmd.get_matches_from(["strest"]);
let mut args = TesterArgs::from_arg_matches(&matches)
let args = TesterArgs::from_arg_matches(&matches)
.map_err(|err| AppError::config(format!("parse args failed: {}", err)))?;

apply_config(&mut args, &matches, &config)?;
let args = apply_config(args, &matches, config)?.0;

if args.warmup != Some(Duration::from_secs(10)) {
return Err(AppError::config("Expected warmup to be 10s"));
Expand Down Expand Up @@ -472,10 +472,10 @@ fn apply_config_parses_scenario() -> AppResult<()> {

let cmd = TesterArgs::command();
let matches = cmd.get_matches_from(["strest"]);
let mut args = TesterArgs::from_arg_matches(&matches)
let args = TesterArgs::from_arg_matches(&matches)
.map_err(|err| AppError::config(format!("parse args failed: {}", err)))?;

apply_config(&mut args, &matches, &config)?;
let args = apply_config(args, &matches, config)?.0;

let scenario = match args.scenario {
Some(scenario) => scenario,
Expand Down Expand Up @@ -530,10 +530,10 @@ fn apply_config_sets_distributed_fields() -> AppResult<()> {

let cmd = TesterArgs::command();
let matches = cmd.get_matches_from(["strest"]);
let mut args = TesterArgs::from_arg_matches(&matches)
let args = TesterArgs::from_arg_matches(&matches)
.map_err(|err| AppError::config(format!("parse args failed: {}", err)))?;

apply_config(&mut args, &matches, &config)?;
let args = apply_config(args, &matches, config)?.0;

if args.agent_join.as_deref() != Some("127.0.0.1:9009") {
return Err(AppError::config("Unexpected agent_join"));
Expand Down
16 changes: 11 additions & 5 deletions src/distributed/controller/manual/run_lifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use tokio::sync::watch;
use tokio::time::{Instant, MissedTickBehavior};

use crate::args::{Scenario, TesterArgs};
use crate::config::apply::scenario::parse_scenario;
use crate::config::apply::scenario::{ScenarioDefaults, parse_scenario};
use crate::ui::{model::UiData, render::setup_render_ui};

use super::super::control::{ControlError, ControlStartRequest};
Expand Down Expand Up @@ -161,9 +161,15 @@ pub(super) fn resolve_scenario_for_run(
request: &ControlStartRequest,
scenario_state: &mut ScenarioState,
) -> Result<Option<Scenario>, ControlError> {
let scenario_defaults = ScenarioDefaults::new(
args.url.clone(),
args.method,
args.data.clone(),
args.headers.clone(),
);
if let Some(config) = request.scenario.as_ref() {
let scenario =
parse_scenario(config, args).map_err(|err| ControlError::new(400, err.to_string()))?;
let scenario = parse_scenario(config, &scenario_defaults)
.map_err(|err| ControlError::new(400, err.to_string()))?;
if let Some(name) = request.scenario_name.as_ref() {
scenario_state.named.insert(name.clone(), config.clone());
}
Expand All @@ -175,8 +181,8 @@ pub(super) fn resolve_scenario_for_run(
.named
.get(name)
.ok_or_else(|| ControlError::new(404, "Scenario not found."))?;
let scenario =
parse_scenario(config, args).map_err(|err| ControlError::new(400, err.to_string()))?;
let scenario = parse_scenario(config, &scenario_defaults)
.map_err(|err| ControlError::new(400, err.to_string()))?;
return Ok(Some(scenario));
}

Expand Down
17 changes: 8 additions & 9 deletions src/entry/plan/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ pub(crate) fn build_plan(mut args: TesterArgs, matches: &ArgMatches) -> AppResul
return Ok(RunPlan::Replay(to_replay_run_command(args)));
}

let scenario_registry = apply_config(&mut args, matches)?;
let (mut args, scenario_registry) = apply_config(args, matches)?;

apply_output_aliases(&mut args)?;
validate_db_logging(&args)?;
Expand Down Expand Up @@ -131,16 +131,15 @@ pub(crate) fn build_plan(mut args: TesterArgs, matches: &ArgMatches) -> AppResul
}

fn apply_config(
args: &mut TesterArgs,
args: TesterArgs,
matches: &ArgMatches,
) -> AppResult<Option<BTreeMap<String, ScenarioConfig>>> {
let mut scenario_registry = None;
let mut loaded_config = crate::config::load_config(args.config.as_deref())?;
if let Some(config) = loaded_config.as_mut() {
scenario_registry = config.scenarios.take();
crate::config::apply_config(args, matches, config)?;
) -> AppResult<(TesterArgs, Option<BTreeMap<String, ScenarioConfig>>)> {
let loaded_config = crate::config::load_config(args.config.as_deref())?;
if let Some(config) = loaded_config {
let overrides = crate::config::apply_config(args, matches, config)?;
return Ok(overrides);
}
Ok(scenario_registry)
Ok((args, None))
}

fn apply_output_aliases(args: &mut TesterArgs) -> AppResult<()> {
Expand Down
22 changes: 14 additions & 8 deletions src/fuzzing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use crate::args::{
PositiveU64, PositiveUsize, Scenario, ScenarioStep, TesterArgs, TlsVersion, parse_header,
parsers::parse_duration_arg,
};
use crate::config::apply::scenario::parse_scenario;
use crate::config::apply::scenario::{ScenarioDefaults, parse_scenario};
use crate::config::types::{ConfigFile, LoadConfig, ScenarioConfig};
use crate::config::{apply_config, parse_duration_value};
use crate::error::{AppError, AppResult, ValidationError};
Expand Down Expand Up @@ -78,7 +78,7 @@ pub fn render_template_input(input: &str, vars: &BTreeMap<String, String>) -> St
/// Returns an error when parsing or validation fails.
pub fn apply_config_from_toml(input: &str) -> AppResult<()> {
let config: ConfigFile = toml::from_str(input)?;
apply_config_to_defaults(&config)
apply_config_to_defaults(config)
}

/// Parses JSON config and applies it to defaults.
Expand All @@ -88,7 +88,7 @@ pub fn apply_config_from_toml(input: &str) -> AppResult<()> {
/// Returns an error when parsing or validation fails.
pub fn apply_config_from_json(input: &[u8]) -> AppResult<()> {
let config: ConfigFile = serde_json::from_slice(input)?;
apply_config_to_defaults(&config)
apply_config_to_defaults(config)
}

/// Parses a positive u64 string value.
Expand Down Expand Up @@ -166,7 +166,7 @@ pub fn apply_load_config_input(load: LoadConfig) -> AppResult<()> {
load: Some(load),
..ConfigFile::default()
};
apply_config_to_defaults(&config)
apply_config_to_defaults(config)
}

/// Parses a scenario config using default arguments.
Expand All @@ -177,7 +177,13 @@ pub fn apply_load_config_input(load: LoadConfig) -> AppResult<()> {
pub fn parse_scenario_config_input(config: &ScenarioConfig) -> AppResult<()> {
BASE_MATCHES.with(|matches| {
let args = TesterArgs::from_arg_matches(matches)?;
parse_scenario(config, &args).map(|_| ())
let defaults = ScenarioDefaults::new(
args.url.clone(),
args.method,
args.data.clone(),
args.headers.clone(),
);
parse_scenario(config, &defaults).map(|_| ())
})
}

Expand Down Expand Up @@ -217,9 +223,9 @@ pub fn load_config_file_input(path: &std::path::Path) -> AppResult<()> {
crate::config::load_config_file(path).map(|_| ())
}

fn apply_config_to_defaults(config: &ConfigFile) -> AppResult<()> {
fn apply_config_to_defaults(config: ConfigFile) -> AppResult<()> {
BASE_MATCHES.with(|matches| {
let mut args = TesterArgs::from_arg_matches(matches)?;
apply_config(&mut args, matches, config)
let args = TesterArgs::from_arg_matches(matches)?;
apply_config(args, matches, config).map(|_| ())
})
}
Loading