strest 0.1.10

Blazing-fast async HTTP load tester in Rust - lock-free design, real-time stats, distributed runs, and optional chart exports for high-load API testing.
Documentation
use std::time::Duration;

use crate::args::{LoadProfile, PositiveU64, PositiveUsize, Scenario, ScenarioStep, TesterArgs};
use crate::error::{AppError, AppResult, DistributedError, WireValueField};
use crate::metrics::MetricsRange;

use super::protocol::{WireArgs, WireLoadProfile, WireLoadStage, WireScenario, WireScenarioStep};
use super::utils::duration_to_ms;

pub(super) fn build_wire_args(args: &TesterArgs) -> WireArgs {
    WireArgs {
        method: args.method,
        protocol: args.protocol,
        load_mode: args.load_mode,
        url: args.url.clone(),
        headers: args.headers.clone(),
        data: args.data.clone(),
        target_duration: args.target_duration.get(),
        expected_status_code: args.expected_status_code,
        request_timeout_ms: duration_to_ms(args.request_timeout),
        charts_path: args.charts_path.clone(),
        no_charts: true,
        verbose: args.verbose,
        tmp_path: args.tmp_path.clone(),
        keep_tmp: args.keep_tmp,
        warmup_ms: args.warmup.map(duration_to_ms),
        export_csv: None,
        export_json: None,
        log_shards: args.log_shards.get(),
        no_ui: true,
        summary: true,
        proxy_url: args.proxy_url.clone(),
        max_tasks: args.max_tasks.get(),
        spawn_rate_per_tick: args.spawn_rate_per_tick.get(),
        tick_interval: args.tick_interval.get(),
        rate_limit: args.rate_limit.map(u64::from),
        load_profile: args.load_profile.as_ref().map(to_wire_load_profile),
        metrics_range: args.metrics_range.as_ref().map(|range| {
            let start = *range.0.start();
            let end = *range.0.end();
            (start, end)
        }),
        metrics_max: 1,
        scenario: args.scenario.as_ref().map(to_wire_scenario),
        tls_min: args.tls_min,
        tls_max: args.tls_max,
        http2: args.http2,
        http3: args.http3,
        alpn: args.alpn.clone(),
        stream_summaries: args.distributed_stream_summaries,
        stream_interval_ms: args.distributed_stream_interval_ms.map(u64::from),
    }
}

pub(super) fn apply_wire_args(args: &mut TesterArgs, wire: WireArgs) -> AppResult<()> {
    args.method = wire.method;
    args.protocol = wire.protocol;
    args.load_mode = wire.load_mode;
    args.url = wire.url;
    args.headers = wire.headers;
    args.data = wire.data;
    args.target_duration = PositiveU64::try_from(wire.target_duration).map_err(|err| {
        AppError::distributed(DistributedError::WireValueTooSmall {
            field: WireValueField::TargetDuration,
            source: err,
        })
    })?;
    args.expected_status_code = wire.expected_status_code;
    args.request_timeout = Duration::from_millis(wire.request_timeout_ms);
    args.charts_path = wire.charts_path;
    args.no_charts = wire.no_charts;
    args.verbose = wire.verbose;
    args.tmp_path = wire.tmp_path;
    args.keep_tmp = wire.keep_tmp;
    args.warmup = wire.warmup_ms.map(Duration::from_millis);
    args.export_csv = wire.export_csv;
    args.export_json = wire.export_json;
    args.log_shards = PositiveUsize::try_from(wire.log_shards).map_err(|err| {
        AppError::distributed(DistributedError::WireValueTooSmall {
            field: WireValueField::LogShards,
            source: err,
        })
    })?;
    args.no_ui = wire.no_ui;
    args.summary = wire.summary;
    args.proxy_url = wire.proxy_url;
    args.max_tasks = PositiveUsize::try_from(wire.max_tasks).map_err(|err| {
        AppError::distributed(DistributedError::WireValueTooSmall {
            field: WireValueField::MaxTasks,
            source: err,
        })
    })?;
    args.spawn_rate_per_tick =
        PositiveUsize::try_from(wire.spawn_rate_per_tick).map_err(|err| {
            AppError::distributed(DistributedError::WireValueTooSmall {
                field: WireValueField::SpawnRatePerTick,
                source: err,
            })
        })?;
    args.tick_interval = PositiveU64::try_from(wire.tick_interval).map_err(|err| {
        AppError::distributed(DistributedError::WireValueTooSmall {
            field: WireValueField::TickInterval,
            source: err,
        })
    })?;
    args.rate_limit = match wire.rate_limit {
        Some(value) => Some(PositiveU64::try_from(value).map_err(|err| {
            AppError::distributed(DistributedError::WireValueTooSmall {
                field: WireValueField::RateLimit,
                source: err,
            })
        })?),
        None => None,
    };
    args.load_profile = wire.load_profile.map(from_wire_load_profile);
    args.metrics_range = wire
        .metrics_range
        .map(|(start, end)| MetricsRange(start..=end));
    args.metrics_max = PositiveUsize::try_from(wire.metrics_max).map_err(|err| {
        AppError::distributed(DistributedError::WireValueTooSmall {
            field: WireValueField::MetricsMax,
            source: err,
        })
    })?;
    args.scenario = wire.scenario.map(from_wire_scenario);
    args.tls_min = wire.tls_min;
    args.tls_max = wire.tls_max;
    args.http2 = wire.http2;
    args.http3 = wire.http3;
    args.alpn = wire.alpn;
    args.distributed_stream_summaries = wire.stream_summaries;
    args.distributed_stream_interval_ms = match wire.stream_interval_ms {
        Some(value) => Some(PositiveU64::try_from(value).map_err(|err| {
            AppError::distributed(DistributedError::WireValueTooSmall {
                field: WireValueField::StreamIntervalMs,
                source: err,
            })
        })?),
        None => None,
    };
    Ok(())
}

pub(super) fn to_wire_load_profile(profile: &LoadProfile) -> WireLoadProfile {
    WireLoadProfile {
        initial_rpm: profile.initial_rpm,
        stages: profile
            .stages
            .iter()
            .map(|stage| WireLoadStage {
                duration_secs: stage.duration.as_secs(),
                target_rpm: stage.target_rpm,
            })
            .collect(),
    }
}

pub(super) fn from_wire_load_profile(profile: WireLoadProfile) -> LoadProfile {
    LoadProfile {
        initial_rpm: profile.initial_rpm,
        stages: profile
            .stages
            .into_iter()
            .map(|stage| crate::args::LoadStage {
                duration: Duration::from_secs(stage.duration_secs.max(1)),
                target_rpm: stage.target_rpm,
            })
            .collect(),
    }
}

pub(super) fn to_wire_scenario(scenario: &Scenario) -> WireScenario {
    WireScenario {
        base_url: scenario.base_url.clone(),
        vars: scenario.vars.clone(),
        steps: scenario
            .steps
            .iter()
            .map(|step| WireScenarioStep {
                name: step.name.clone(),
                method: step.method,
                url: step.url.clone(),
                path: step.path.clone(),
                headers: step.headers.clone(),
                body: step.body.clone(),
                assert_status: step.assert_status,
                assert_body_contains: step.assert_body_contains.clone(),
                think_time_ms: step.think_time.map(duration_to_ms),
                vars: step.vars.clone(),
            })
            .collect(),
    }
}

pub(super) fn from_wire_scenario(scenario: WireScenario) -> Scenario {
    Scenario {
        base_url: scenario.base_url,
        vars: scenario.vars,
        steps: scenario
            .steps
            .into_iter()
            .map(|step| ScenarioStep {
                name: step.name,
                method: step.method,
                url: step.url,
                path: step.path,
                headers: step.headers,
                body: step.body,
                assert_status: step.assert_status,
                assert_body_contains: step.assert_body_contains,
                think_time: step.think_time_ms.map(Duration::from_millis),
                vars: step.vars,
            })
            .collect(),
    }
}