1mod compute;
2mod report;
3
4pub use report::{
5 FloatFieldSummary, LatencyStats, RequestSummary, ResponseStatsReport, RunMeta, RunReport,
6 ScenarioReport, ScenarioStepReport, StageReport,
7};
8
9use crate::execution::{RunMode, RunStats};
10
11use compute::{
12 latency_stats, per_stage_reports, request_summary, response_stats_report, scenario_reports,
13 status_code_map,
14};
15
16pub struct RunReportParams<'a> {
20 pub stats: &'a RunStats,
21}
22
23impl RunReport {
26 pub fn from_params(params: RunReportParams<'_>) -> Self {
32 let RunReportParams { stats } = params;
33
34 let total = stats.total_requests as usize;
35 let failed = stats.total_failures as usize;
36 let mode_str = match stats.mode {
37 RunMode::Fixed => "fixed".to_string(),
38 RunMode::Curve => "curve".to_string(),
39 };
40
41 let run = RunMeta {
42 mode: mode_str,
43 elapsed_ms: stats.elapsed.as_secs_f64() * 1000.0,
44 curve_duration_ms: stats
45 .curve_stats
46 .as_ref()
47 .map(|cs| cs.duration.as_secs_f64() * 1000.0),
48 template_generation_ms: stats
49 .template_stats
50 .as_ref()
51 .map(|ts| ts.generation_duration.as_secs_f64() * 1000.0),
52 };
53
54 let skipped = stats.total_skipped as usize;
55 let requests = request_summary(total, failed, skipped, stats.elapsed);
56
57 let latency = latency_stats(&stats.latency);
58 let status_codes = status_code_map(&stats.status_codes);
59 let response_stats = stats.response_stats.as_ref().map(response_stats_report);
60
61 let curve_stages = stats
62 .curve_stats
63 .as_ref()
64 .map(|cs| per_stage_reports(&cs.stages, &cs.stage_stats));
65 let scenarios = stats
66 .scenario_stats
67 .as_ref()
68 .map(|scenarios| scenario_reports(scenarios, stats.elapsed));
69
70 RunReport {
71 version: 2,
72 run,
73 requests,
74 latency,
75 status_codes,
76 response_stats,
77 curve_stages,
78 scenarios,
79 thresholds: None,
80 }
81 }
82}
83
84#[cfg(test)]
87mod tests {
88 use std::time::Duration;
89
90 use crate::execution::{CurveStats, RunMode, RunStats, StageStats};
91 use crate::histogram::{LatencyHistogram, StatusCodeHistogram};
92 use crate::load_curve::{LoadCurve, RampType, Stage};
93 use crate::output::{RunReport, RunReportParams};
94
95 fn make_run_stats(mode: RunMode, total_requests: u64, total_failures: u64) -> RunStats {
96 RunStats {
97 elapsed: Duration::from_secs(5),
98 mode,
99 latency: LatencyHistogram::new(),
100 status_codes: StatusCodeHistogram::new(),
101 total_requests,
102 total_failures,
103 total_skipped: 0,
104 template_stats: None,
105 response_stats: None,
106 curve_stats: if mode == RunMode::Curve {
107 Some(CurveStats {
108 duration: Duration::from_secs(5),
109 stages: vec![],
110 stage_stats: vec![],
111 })
112 } else {
113 None
114 },
115 scenario_stats: None,
116 }
117 }
118
119 #[test]
122 fn run_report_fixed_mode_no_response_stats() {
123 let stats = make_run_stats(RunMode::Fixed, 100, 5);
124 let report = RunReport::from_params(RunReportParams { stats: &stats });
125
126 assert_eq!(report.version, 2);
127 assert_eq!(report.run.mode, "fixed");
128 assert!(report.curve_stages.is_none());
129 assert!(report.scenarios.is_none());
130 assert!(report.response_stats.is_none());
131 assert!(report.run.curve_duration_ms.is_none());
132 }
133
134 #[test]
137 fn run_report_curve_mode_stages_populated() {
138 let stages = vec![
139 Stage {
140 duration: Duration::from_secs(2),
141 target_vus: 50,
142 ramp: RampType::Linear,
143 },
144 Stage {
145 duration: Duration::from_secs(2),
146 target_vus: 100,
147 ramp: RampType::Linear,
148 },
149 ];
150
151 let mut stats = make_run_stats(RunMode::Curve, 4, 1);
152 stats.curve_stats = Some(CurveStats {
154 duration: Duration::from_secs(4),
155 stages: stages.clone(),
156 stage_stats: stages
157 .iter()
158 .map(|_| StageStats {
159 latency: LatencyHistogram::new(),
160 status_codes: StatusCodeHistogram::new(),
161 total_requests: 2,
162 total_failures: 0,
163 })
164 .collect(),
165 });
166
167 let curve = LoadCurve {
168 stages: stages.clone(),
169 };
170 let _ = curve; let report = RunReport::from_params(RunReportParams { stats: &stats });
173
174 assert_eq!(report.version, 2);
175 assert_eq!(report.run.mode, "curve");
176 let stage_reports = report.curve_stages.expect("curve_stages must be Some");
177 assert_eq!(stage_reports.len(), 2);
178 assert_eq!(stage_reports[0].index, 0);
179 assert_eq!(stage_reports[1].index, 1);
180 assert_eq!(stage_reports[0].target_vus, 50);
181 assert_eq!(stage_reports[1].target_vus, 100);
182 }
183
184 #[test]
187 fn run_report_serializes_to_valid_json() {
188 let stats = make_run_stats(RunMode::Fixed, 3, 1);
189 let report = RunReport::from_params(RunReportParams { stats: &stats });
190
191 let json = serde_json::to_string(&report).expect("serialization must succeed");
192 let parsed: serde_json::Value =
193 serde_json::from_str(&json).expect("output must be valid JSON");
194
195 assert_eq!(parsed["version"], 2);
196 assert_eq!(parsed["run"]["mode"], "fixed");
197 assert!(parsed["requests"]["total"].is_number());
198 assert!(parsed["latency"]["p50_ms"].is_number());
199 assert!(parsed["scenarios"].is_null());
200 assert!(parsed["sampling"].is_null());
202 }
203
204 #[test]
207 fn run_report_error_rate_computed_correctly() {
208 let stats = make_run_stats(RunMode::Fixed, 100, 5);
209 let report = RunReport::from_params(RunReportParams { stats: &stats });
210 assert!((report.requests.error_rate - 0.05).abs() < 1e-9);
212 assert_eq!(report.requests.total, 100);
213 assert_eq!(report.requests.failed, 5);
214 assert_eq!(report.requests.ok, 95);
215 }
216}