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
6 changes: 6 additions & 0 deletions docs/architecture/ard/ARCHITECTURE_RISKS_HEXAGONAL_PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,12 @@ Phase 0 artifacts (implemented):
Exit criteria:
- `entry` calls use-case commands, not raw `TesterArgs` (except adapter boundary).

Phase 1 artifacts (implemented):
- Domain run model: `src/domain/run.rs`
- Application commands per mode: `src/application/commands.rs`
- CLI anti-corruption mapper: `src/adapters/cli/mapper.rs`
- Entry plan command wiring: `src/entry/plan/types.rs`, `src/entry/plan/build.rs`, `src/entry/plan/execute.rs`

### Phase 2: Config decoupling (1-2 weeks)
1. Replace `config::apply_config(&mut TesterArgs, ...)` with `ConfigOverrides` builder.
2. Merge order defined in one place: `CLI > Config > Preset defaults`.
Expand Down
168 changes: 168 additions & 0 deletions src/adapters/cli/mapper.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
use std::collections::BTreeMap;

use crate::application::commands::{
AgentRunCommand, ControllerRunCommand, LocalRunCommand, ReplayRunCommand, ServiceCommand,
};
use crate::args::{
LoadMode as CliLoadMode, Protocol as CliProtocol, Scenario as CliScenario, TesterArgs,
};
use crate::config::types::ScenarioConfig;
use crate::domain::run::{LoadMode, ProtocolKind, RunConfig, Scenario};
use crate::error::{AppError, AppResult, ValidationError};

pub(crate) fn to_local_run_command(mut args: TesterArgs) -> AppResult<LocalRunCommand> {
if args.url.is_none() && args.scenario.is_none() {
tracing::error!("Missing URL (set --url or provide in config).");
return Err(AppError::validation(ValidationError::MissingUrl));
}
args.distributed_stream_summaries = false;
let run_config = to_run_config(&args);
Ok(LocalRunCommand::new(run_config, args))
}

pub(crate) fn to_replay_run_command(args: TesterArgs) -> ReplayRunCommand {
let run_config = to_run_config(&args);
ReplayRunCommand::new(run_config, args)
}

pub(crate) const fn to_service_command(args: TesterArgs) -> ServiceCommand {
ServiceCommand::new(args)
}

pub(crate) fn to_controller_run_command(
args: TesterArgs,
scenarios: Option<BTreeMap<String, ScenarioConfig>>,
) -> ControllerRunCommand {
let run_config = to_run_config(&args);
ControllerRunCommand::new(run_config, args, scenarios)
}

pub(crate) fn to_agent_run_command(args: TesterArgs) -> AgentRunCommand {
let run_config = to_run_config(&args);
AgentRunCommand::new(run_config, args)
}

fn to_run_config(args: &TesterArgs) -> RunConfig {
RunConfig {
protocol: map_protocol(args.protocol),
load_mode: map_load_mode(args.load_mode),
target_url: args.url.clone(),
scenario: args.scenario.as_ref().map(map_scenario),
}
}

const fn map_protocol(protocol: CliProtocol) -> ProtocolKind {
match protocol {
CliProtocol::Http => ProtocolKind::Http,
CliProtocol::GrpcUnary => ProtocolKind::GrpcUnary,
CliProtocol::GrpcStreaming => ProtocolKind::GrpcStreaming,
CliProtocol::Websocket => ProtocolKind::Websocket,
CliProtocol::Tcp => ProtocolKind::Tcp,
CliProtocol::Udp => ProtocolKind::Udp,
CliProtocol::Quic => ProtocolKind::Quic,
CliProtocol::Mqtt => ProtocolKind::Mqtt,
CliProtocol::Enet => ProtocolKind::Enet,
CliProtocol::Kcp => ProtocolKind::Kcp,
CliProtocol::Raknet => ProtocolKind::Raknet,
}
}

const fn map_load_mode(load_mode: CliLoadMode) -> LoadMode {
match load_mode {
CliLoadMode::Arrival => LoadMode::Arrival,
CliLoadMode::Step => LoadMode::Step,
CliLoadMode::Ramp => LoadMode::Ramp,
CliLoadMode::Jitter => LoadMode::Jitter,
CliLoadMode::Burst => LoadMode::Burst,
CliLoadMode::Soak => LoadMode::Soak,
}
}

fn map_scenario(scenario: &CliScenario) -> Scenario {
Scenario {
base_url: scenario.base_url.clone(),
vars_count: scenario.vars.len(),
step_count: scenario.steps.len(),
}
}

#[cfg(test)]
mod tests {
use clap::Parser;

use crate::args::TesterArgs;
use crate::domain::run::{LoadMode, ProtocolKind};
use crate::error::{AppError, ValidationError};

use super::to_local_run_command;

#[test]
fn local_mapper_requires_url_or_scenario() {
let args_result = TesterArgs::try_parse_from(["strest"]);
assert!(
args_result.is_ok(),
"Expected CLI parsing to succeed without URL for mapper validation"
);
let args = if let Ok(args) = args_result {
args
} else {
return;
};

let mapped = to_local_run_command(args);
assert!(
mapped.is_err(),
"Expected local command mapper to reject missing URL and scenario"
);
let err = if let Err(err) = mapped {
err
} else {
return;
};
assert!(matches!(
err,
AppError::Validation(ValidationError::MissingUrl)
));
}

#[test]
fn local_mapper_builds_domain_run_config() {
let args_result = TesterArgs::try_parse_from([
"strest",
"--url",
"grpc://127.0.0.1:50051/test.Service/Method",
"--protocol",
"grpc-unary",
"--load-mode",
"arrival",
]);
assert!(
args_result.is_ok(),
"Expected CLI parsing to succeed for local mapper test"
);
let args = if let Ok(args) = args_result {
args
} else {
return;
};

let mapped = to_local_run_command(args);
assert!(
mapped.is_ok(),
"Expected local command mapping to succeed for valid arguments"
);
let command = if let Ok(command) = mapped {
command
} else {
return;
};

assert_eq!(command.run_config().protocol, ProtocolKind::GrpcUnary);
assert_eq!(command.run_config().load_mode, LoadMode::Arrival);
assert_eq!(
command.run_config().target_url.as_deref(),
Some("grpc://127.0.0.1:50051/test.Service/Method")
);
assert_eq!(command.run_config().scenario_step_count(), 0);
}
}
1 change: 1 addition & 0 deletions src/adapters/cli/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub(crate) mod mapper;
1 change: 1 addition & 0 deletions src/adapters/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub(crate) mod cli;
143 changes: 143 additions & 0 deletions src/application/commands.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
use std::collections::BTreeMap;

use crate::args::TesterArgs;
use crate::config::types::ScenarioConfig;
use crate::domain::run::RunConfig;

#[derive(Debug)]
pub(crate) struct LocalRunCommand {
run_config: RunConfig,
args: TesterArgs,
}

impl LocalRunCommand {
#[must_use]
pub(crate) const fn new(run_config: RunConfig, args: TesterArgs) -> Self {
Self { run_config, args }
}

#[must_use]
pub(crate) const fn run_config(&self) -> &RunConfig {
&self.run_config
}

#[must_use]
pub(crate) const fn no_color(&self) -> bool {
self.args.no_color
}

#[must_use]
pub(crate) fn into_args(self) -> TesterArgs {
self.args
}
}

#[derive(Debug)]
pub(crate) struct ReplayRunCommand {
run_config: RunConfig,
args: TesterArgs,
}

impl ReplayRunCommand {
#[must_use]
pub(crate) const fn new(run_config: RunConfig, args: TesterArgs) -> Self {
Self { run_config, args }
}

#[must_use]
pub(crate) const fn run_config(&self) -> &RunConfig {
&self.run_config
}

#[must_use]
pub(crate) const fn no_color(&self) -> bool {
self.args.no_color
}

#[must_use]
pub(crate) const fn as_args(&self) -> &TesterArgs {
&self.args
}
}

#[derive(Debug)]
pub(crate) struct ServiceCommand {
args: TesterArgs,
}

impl ServiceCommand {
#[must_use]
pub(crate) const fn new(args: TesterArgs) -> Self {
Self { args }
}

#[must_use]
pub(crate) const fn as_args(&self) -> &TesterArgs {
&self.args
}
}

#[derive(Debug)]
pub(crate) struct ControllerRunCommand {
run_config: RunConfig,
args: TesterArgs,
scenarios: Option<BTreeMap<String, ScenarioConfig>>,
}

impl ControllerRunCommand {
#[must_use]
pub(crate) const fn new(
run_config: RunConfig,
args: TesterArgs,
scenarios: Option<BTreeMap<String, ScenarioConfig>>,
) -> Self {
Self {
run_config,
args,
scenarios,
}
}

#[must_use]
pub(crate) const fn run_config(&self) -> &RunConfig {
&self.run_config
}

#[must_use]
pub(crate) const fn no_color(&self) -> bool {
self.args.no_color
}

#[must_use]
pub(crate) fn into_parts(self) -> (TesterArgs, Option<BTreeMap<String, ScenarioConfig>>) {
(self.args, self.scenarios)
}
}

#[derive(Debug)]
pub(crate) struct AgentRunCommand {
run_config: RunConfig,
args: TesterArgs,
}

impl AgentRunCommand {
#[must_use]
pub(crate) const fn new(run_config: RunConfig, args: TesterArgs) -> Self {
Self { run_config, args }
}

#[must_use]
pub(crate) const fn run_config(&self) -> &RunConfig {
&self.run_config
}

#[must_use]
pub(crate) const fn no_color(&self) -> bool {
self.args.no_color
}

#[must_use]
pub(crate) fn into_args(self) -> TesterArgs {
self.args
}
}
1 change: 1 addition & 0 deletions src/application/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub(crate) mod commands;
1 change: 1 addition & 0 deletions src/domain/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub(crate) mod run;
Loading