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