mod compute;
mod report;
pub use report::{
FloatFieldSummary, LatencyStats, RequestSummary, ResponseStatsReport, RunMeta, RunReport,
SamplingInfo, StageReport,
};
use std::time::Instant;
use crate::command::run::{RunMode, RunStats};
use compute::{
error_rate, latency_stats, per_stage_reports, response_stats_report, status_code_map,
throughput,
};
pub struct RunReportParams<'a> {
pub stats: &'a RunStats,
pub reservoir_size: usize,
pub run_start: Instant,
}
impl RunReport {
pub fn from_params(params: RunReportParams<'_>) -> Self {
let RunReportParams {
stats,
reservoir_size,
run_start: _,
} = params;
let total = stats.total_requests;
let failed = stats.total_failures;
let ok = total.saturating_sub(failed);
let mode_str = match stats.mode {
RunMode::Fixed => "fixed".to_string(),
RunMode::Curve => "curve".to_string(),
};
let run = RunMeta {
mode: mode_str,
elapsed_ms: stats.elapsed.as_secs_f64() * 1000.0,
curve_duration_ms: stats.curve_duration.map(|d| d.as_secs_f64() * 1000.0),
template_generation_ms: stats.template_duration.map(|d| d.as_secs_f64() * 1000.0),
};
let requests = RequestSummary {
total,
ok,
failed,
error_rate: error_rate(total, failed),
throughput_rps: throughput(total, stats.elapsed),
};
let latency = latency_stats(&stats.results);
let status_codes = status_code_map(&stats.results);
let sampling = SamplingInfo {
sampled: stats.min_sample_rate < 1.0,
final_sample_rate: stats.sample_rate,
min_sample_rate: stats.min_sample_rate,
reservoir_size,
results_collected: stats.results.len(),
};
let response_stats = stats.response_stats.as_ref().map(response_stats_report);
let curve_stages = match stats.mode {
RunMode::Curve => {
None
}
RunMode::Fixed => None,
};
RunReport {
version: 1,
run,
requests,
latency,
status_codes,
sampling,
response_stats,
curve_stages,
thresholds: None,
}
}
pub fn from_params_with_curve(
params: RunReportParams<'_>,
stages: &[crate::load_curve::Stage],
) -> Self {
let mut report = Self::from_params(RunReportParams {
stats: params.stats,
reservoir_size: params.reservoir_size,
run_start: params.run_start,
});
if report.run.curve_duration_ms.is_some() {
report.curve_stages = Some(per_stage_reports(
¶ms.stats.results,
stages,
params.run_start,
));
}
report
}
}
#[cfg(test)]
mod tests {
use std::time::{Duration, Instant};
use crate::command::run::{RunMode, RunStats};
use crate::http::RequestResult;
use crate::load_curve::{LoadCurve, RampType, Stage};
use crate::output::{RunReport, RunReportParams};
fn make_run_stats(
mode: RunMode,
results: Vec<RequestResult>,
total_requests: usize,
total_failures: usize,
sample_rate: f64,
min_sample_rate: f64,
) -> RunStats {
RunStats {
elapsed: Duration::from_secs(5),
template_duration: None,
response_stats: None,
results,
mode,
curve_duration: if mode == RunMode::Curve {
Some(Duration::from_secs(5))
} else {
None
},
curve_stages: None,
total_requests,
total_failures,
sample_rate,
min_sample_rate,
}
}
fn make_result(duration_ms: u64, success: bool, status: Option<u16>) -> RequestResult {
RequestResult::new(Duration::from_millis(duration_ms), success, status, None)
}
#[test]
fn run_report_fixed_mode_no_response_stats() {
let stats = make_run_stats(
RunMode::Fixed,
vec![make_result(10, true, Some(200))],
100,
5,
1.0,
1.0,
);
let report = RunReport::from_params(RunReportParams {
stats: &stats,
reservoir_size: 100_000,
run_start: Instant::now(),
});
assert_eq!(report.version, 1);
assert_eq!(report.run.mode, "fixed");
assert!(report.curve_stages.is_none());
assert!(report.response_stats.is_none());
assert!(report.run.curve_duration_ms.is_none());
}
#[test]
fn run_report_curve_mode_stages_populated() {
let past_start = Instant::now();
let results = vec![
make_result(10, true, Some(200)),
make_result(20, true, Some(200)),
make_result(30, false, Some(503)),
make_result(40, true, Some(200)),
];
let stats = make_run_stats(RunMode::Curve, results, 4, 1, 1.0, 1.0);
let stages = vec![
Stage {
duration: Duration::from_secs(2),
target_vus: 50,
ramp: RampType::Linear,
},
Stage {
duration: Duration::from_secs(2),
target_vus: 100,
ramp: RampType::Linear,
},
];
let curve = LoadCurve {
stages: stages.clone(),
};
let _ = curve;
let report = RunReport::from_params_with_curve(
RunReportParams {
stats: &stats,
reservoir_size: 100_000,
run_start: past_start,
},
&stages,
);
assert_eq!(report.version, 1);
assert_eq!(report.run.mode, "curve");
let stage_reports = report.curve_stages.expect("curve_stages must be Some");
assert_eq!(stage_reports.len(), 2);
assert_eq!(stage_reports[0].index, 0);
assert_eq!(stage_reports[1].index, 1);
assert_eq!(stage_reports[0].target_vus, 50);
assert_eq!(stage_reports[1].target_vus, 100);
}
#[test]
fn run_report_sampling_fields_accurate_when_sampled() {
let stats = make_run_stats(
RunMode::Fixed,
vec![make_result(10, true, Some(200))],
10000,
50,
0.5,
0.25,
);
let report = RunReport::from_params(RunReportParams {
stats: &stats,
reservoir_size: 50_000,
run_start: Instant::now(),
});
assert!(
report.sampling.sampled,
"sampled must be true when min_sample_rate < 1.0"
);
assert_eq!(report.sampling.final_sample_rate, 0.5);
assert_eq!(report.sampling.min_sample_rate, 0.25);
assert_eq!(report.sampling.reservoir_size, 50_000);
}
#[test]
fn run_report_sampling_fields_accurate_when_not_sampled() {
let stats = make_run_stats(
RunMode::Fixed,
vec![make_result(10, true, Some(200))],
100,
0,
1.0,
1.0,
);
let report = RunReport::from_params(RunReportParams {
stats: &stats,
reservoir_size: 100_000,
run_start: Instant::now(),
});
assert!(
!report.sampling.sampled,
"sampled must be false when min_sample_rate == 1.0"
);
}
#[test]
fn run_report_serializes_to_valid_json() {
let stats = make_run_stats(
RunMode::Fixed,
vec![
make_result(10, true, Some(200)),
make_result(20, true, Some(200)),
make_result(15, false, None),
],
3,
1,
1.0,
1.0,
);
let report = RunReport::from_params(RunReportParams {
stats: &stats,
reservoir_size: 100_000,
run_start: Instant::now(),
});
let json = serde_json::to_string(&report).expect("serialization must succeed");
let parsed: serde_json::Value =
serde_json::from_str(&json).expect("output must be valid JSON");
assert_eq!(parsed["version"], 1);
assert_eq!(parsed["run"]["mode"], "fixed");
assert!(parsed["requests"]["total"].is_number());
assert!(parsed["latency"]["p50_ms"].is_number());
assert!(parsed["sampling"]["sampled"].is_boolean());
}
}