1mod compute;
2mod report;
3
4pub use report::{
5 FloatFieldSummary, LatencyStats, RequestSummary, ResponseStatsReport, RunMeta, RunReport,
6 SamplingInfo, StageReport,
7};
8
9use std::time::Instant;
10
11use crate::command::run::{RunMode, RunStats};
12
13use compute::{
14 error_rate, latency_stats, per_stage_reports, response_stats_report, status_code_map,
15 throughput,
16};
17
18pub struct RunReportParams<'a> {
31 pub stats: &'a RunStats,
32 pub reservoir_size: usize,
34 pub run_start: Instant,
48}
49
50impl RunReport {
53 pub fn from_params(params: RunReportParams<'_>) -> Self {
60 let RunReportParams {
61 stats,
62 reservoir_size,
63 run_start: _,
64 } = params;
65
66 let total = stats.total_requests;
67 let failed = stats.total_failures;
68 let ok = total.saturating_sub(failed);
69
70 let mode_str = match stats.mode {
71 RunMode::Fixed => "fixed".to_string(),
72 RunMode::Curve => "curve".to_string(),
73 };
74
75 let run = RunMeta {
76 mode: mode_str,
77 elapsed_ms: stats.elapsed.as_secs_f64() * 1000.0,
78 curve_duration_ms: stats.curve_duration.map(|d| d.as_secs_f64() * 1000.0),
79 template_generation_ms: stats.template_duration.map(|d| d.as_secs_f64() * 1000.0),
80 };
81
82 let requests = RequestSummary {
83 total,
84 ok,
85 failed,
86 error_rate: error_rate(total, failed),
87 throughput_rps: throughput(total, stats.elapsed),
88 };
89
90 let latency = latency_stats(&stats.results);
91 let status_codes = status_code_map(&stats.results);
92
93 let sampling = SamplingInfo {
94 sampled: stats.min_sample_rate < 1.0,
95 final_sample_rate: stats.sample_rate,
96 min_sample_rate: stats.min_sample_rate,
97 reservoir_size,
98 results_collected: stats.results.len(),
99 };
100
101 let response_stats = stats.response_stats.as_ref().map(response_stats_report);
102
103 let curve_stages = match stats.mode {
104 RunMode::Curve => {
105 None
114 }
115 RunMode::Fixed => None,
116 };
117
118 RunReport {
119 version: 1,
120 run,
121 requests,
122 latency,
123 status_codes,
124 sampling,
125 response_stats,
126 curve_stages,
127 thresholds: None,
128 }
129 }
130
131 pub fn from_params_with_curve(
136 params: RunReportParams<'_>,
137 stages: &[crate::load_curve::Stage],
138 ) -> Self {
139 let mut report = Self::from_params(RunReportParams {
140 stats: params.stats,
141 reservoir_size: params.reservoir_size,
142 run_start: params.run_start,
143 });
144
145 if report.run.curve_duration_ms.is_some() {
146 report.curve_stages = Some(per_stage_reports(
147 ¶ms.stats.results,
148 stages,
149 params.run_start,
150 ));
151 }
152
153 report
154 }
155}
156
157#[cfg(test)]
160mod tests {
161 use std::time::{Duration, Instant};
162
163 use crate::command::run::{RunMode, RunStats};
164 use crate::http::RequestResult;
165 use crate::load_curve::{LoadCurve, RampType, Stage};
166 use crate::output::{RunReport, RunReportParams};
167
168 fn make_run_stats(
169 mode: RunMode,
170 results: Vec<RequestResult>,
171 total_requests: usize,
172 total_failures: usize,
173 sample_rate: f64,
174 min_sample_rate: f64,
175 ) -> RunStats {
176 RunStats {
177 elapsed: Duration::from_secs(5),
178 template_duration: None,
179 response_stats: None,
180 results,
181 mode,
182 curve_duration: if mode == RunMode::Curve {
183 Some(Duration::from_secs(5))
184 } else {
185 None
186 },
187 curve_stages: None,
188 total_requests,
189 total_failures,
190 sample_rate,
191 min_sample_rate,
192 }
193 }
194
195 fn make_result(duration_ms: u64, success: bool, status: Option<u16>) -> RequestResult {
196 RequestResult::new(Duration::from_millis(duration_ms), success, status, None)
197 }
198
199 #[test]
202 fn run_report_fixed_mode_no_response_stats() {
203 let stats = make_run_stats(
204 RunMode::Fixed,
205 vec![make_result(10, true, Some(200))],
206 100,
207 5,
208 1.0,
209 1.0,
210 );
211 let report = RunReport::from_params(RunReportParams {
212 stats: &stats,
213 reservoir_size: 100_000,
214 run_start: Instant::now(),
215 });
216
217 assert_eq!(report.version, 1);
218 assert_eq!(report.run.mode, "fixed");
219 assert!(report.curve_stages.is_none());
220 assert!(report.response_stats.is_none());
221 assert!(report.run.curve_duration_ms.is_none());
222 }
223
224 #[test]
227 fn run_report_curve_mode_stages_populated() {
228 let past_start = Instant::now();
234
235 let results = vec![
241 make_result(10, true, Some(200)),
242 make_result(20, true, Some(200)),
243 make_result(30, false, Some(503)),
244 make_result(40, true, Some(200)),
245 ];
246
247 let stats = make_run_stats(RunMode::Curve, results, 4, 1, 1.0, 1.0);
248
249 let stages = vec![
250 Stage {
251 duration: Duration::from_secs(2),
252 target_vus: 50,
253 ramp: RampType::Linear,
254 },
255 Stage {
256 duration: Duration::from_secs(2),
257 target_vus: 100,
258 ramp: RampType::Linear,
259 },
260 ];
261
262 let curve = LoadCurve {
263 stages: stages.clone(),
264 };
265 let _ = curve; let report = RunReport::from_params_with_curve(
268 RunReportParams {
269 stats: &stats,
270 reservoir_size: 100_000,
271 run_start: past_start,
272 },
273 &stages,
274 );
275
276 assert_eq!(report.version, 1);
277 assert_eq!(report.run.mode, "curve");
278 let stage_reports = report.curve_stages.expect("curve_stages must be Some");
279 assert_eq!(stage_reports.len(), 2);
280 assert_eq!(stage_reports[0].index, 0);
281 assert_eq!(stage_reports[1].index, 1);
282 assert_eq!(stage_reports[0].target_vus, 50);
283 assert_eq!(stage_reports[1].target_vus, 100);
284 }
285
286 #[test]
289 fn run_report_sampling_fields_accurate_when_sampled() {
290 let stats = make_run_stats(
291 RunMode::Fixed,
292 vec![make_result(10, true, Some(200))],
293 10000,
294 50,
295 0.5,
296 0.25,
297 );
298 let report = RunReport::from_params(RunReportParams {
299 stats: &stats,
300 reservoir_size: 50_000,
301 run_start: Instant::now(),
302 });
303
304 assert!(
305 report.sampling.sampled,
306 "sampled must be true when min_sample_rate < 1.0"
307 );
308 assert_eq!(report.sampling.final_sample_rate, 0.5);
309 assert_eq!(report.sampling.min_sample_rate, 0.25);
310 assert_eq!(report.sampling.reservoir_size, 50_000);
311 }
312
313 #[test]
314 fn run_report_sampling_fields_accurate_when_not_sampled() {
315 let stats = make_run_stats(
316 RunMode::Fixed,
317 vec![make_result(10, true, Some(200))],
318 100,
319 0,
320 1.0,
321 1.0,
322 );
323 let report = RunReport::from_params(RunReportParams {
324 stats: &stats,
325 reservoir_size: 100_000,
326 run_start: Instant::now(),
327 });
328
329 assert!(
330 !report.sampling.sampled,
331 "sampled must be false when min_sample_rate == 1.0"
332 );
333 }
334
335 #[test]
338 fn run_report_serializes_to_valid_json() {
339 let stats = make_run_stats(
340 RunMode::Fixed,
341 vec![
342 make_result(10, true, Some(200)),
343 make_result(20, true, Some(200)),
344 make_result(15, false, None),
345 ],
346 3,
347 1,
348 1.0,
349 1.0,
350 );
351 let report = RunReport::from_params(RunReportParams {
352 stats: &stats,
353 reservoir_size: 100_000,
354 run_start: Instant::now(),
355 });
356
357 let json = serde_json::to_string(&report).expect("serialization must succeed");
358 let parsed: serde_json::Value =
359 serde_json::from_str(&json).expect("output must be valid JSON");
360
361 assert_eq!(parsed["version"], 1);
362 assert_eq!(parsed["run"]["mode"], "fixed");
363 assert!(parsed["requests"]["total"].is_number());
364 assert!(parsed["latency"]["p50_ms"].is_number());
365 assert!(parsed["sampling"]["sampled"].is_boolean());
366 }
367}