mod compute;
mod report;
pub use report::{
FloatFieldSummary, LatencyStats, RequestSummary, ResponseStatsReport, RunMeta, RunReport,
ScenarioReport, ScenarioStepReport, StageReport,
};
use crate::execution::{RunMode, RunStats};
use compute::{
latency_stats, per_stage_reports, request_summary, response_stats_report, scenario_reports,
status_code_map,
};
pub struct RunReportParams<'a> {
pub stats: &'a RunStats,
}
impl RunReport {
pub fn from_params(params: RunReportParams<'_>) -> Self {
let RunReportParams { stats } = params;
let total = stats.total_requests as usize;
let failed = stats.total_failures as usize;
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_stats
.as_ref()
.map(|cs| cs.duration.as_secs_f64() * 1000.0),
template_generation_ms: stats
.template_stats
.as_ref()
.map(|ts| ts.generation_duration.as_secs_f64() * 1000.0),
};
let skipped = stats.total_skipped as usize;
let requests = request_summary(total, failed, skipped, stats.elapsed);
let latency = latency_stats(&stats.latency);
let status_codes = status_code_map(&stats.status_codes);
let response_stats = stats.response_stats.as_ref().map(response_stats_report);
let curve_stages = stats
.curve_stats
.as_ref()
.map(|cs| per_stage_reports(&cs.stages, &cs.stage_stats));
let scenarios = stats
.scenario_stats
.as_ref()
.map(|scenarios| scenario_reports(scenarios, stats.elapsed));
RunReport {
version: 2,
run,
requests,
latency,
status_codes,
response_stats,
curve_stages,
scenarios,
thresholds: None,
}
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use crate::execution::{CurveStats, RunMode, RunStats, StageStats};
use crate::histogram::{LatencyHistogram, StatusCodeHistogram};
use crate::load_curve::{LoadCurve, RampType, Stage};
use crate::output::{RunReport, RunReportParams};
fn make_run_stats(mode: RunMode, total_requests: u64, total_failures: u64) -> RunStats {
RunStats {
elapsed: Duration::from_secs(5),
mode,
latency: LatencyHistogram::new(),
status_codes: StatusCodeHistogram::new(),
total_requests,
total_failures,
total_skipped: 0,
template_stats: None,
response_stats: None,
curve_stats: if mode == RunMode::Curve {
Some(CurveStats {
duration: Duration::from_secs(5),
stages: vec![],
stage_stats: vec![],
})
} else {
None
},
scenario_stats: None,
}
}
#[test]
fn run_report_fixed_mode_no_response_stats() {
let stats = make_run_stats(RunMode::Fixed, 100, 5);
let report = RunReport::from_params(RunReportParams { stats: &stats });
assert_eq!(report.version, 2);
assert_eq!(report.run.mode, "fixed");
assert!(report.curve_stages.is_none());
assert!(report.scenarios.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 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 mut stats = make_run_stats(RunMode::Curve, 4, 1);
stats.curve_stats = Some(CurveStats {
duration: Duration::from_secs(4),
stages: stages.clone(),
stage_stats: stages
.iter()
.map(|_| StageStats {
latency: LatencyHistogram::new(),
status_codes: StatusCodeHistogram::new(),
total_requests: 2,
total_failures: 0,
})
.collect(),
});
let curve = LoadCurve {
stages: stages.clone(),
};
let _ = curve;
let report = RunReport::from_params(RunReportParams { stats: &stats });
assert_eq!(report.version, 2);
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_serializes_to_valid_json() {
let stats = make_run_stats(RunMode::Fixed, 3, 1);
let report = RunReport::from_params(RunReportParams { stats: &stats });
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"], 2);
assert_eq!(parsed["run"]["mode"], "fixed");
assert!(parsed["requests"]["total"].is_number());
assert!(parsed["latency"]["p50_ms"].is_number());
assert!(parsed["scenarios"].is_null());
assert!(parsed["sampling"].is_null());
}
#[test]
fn run_report_error_rate_computed_correctly() {
let stats = make_run_stats(RunMode::Fixed, 100, 5);
let report = RunReport::from_params(RunReportParams { stats: &stats });
assert!((report.requests.error_rate - 0.05).abs() < 1e-9);
assert_eq!(report.requests.total, 100);
assert_eq!(report.requests.failed, 5);
assert_eq!(report.requests.ok, 95);
}
}