Skip to main content

lmn_core/output/
mod.rs

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
16// ── RunReportParams ───────────────────────────────────────────────────────────
17
18/// Parameters required to build a `RunReport` from a completed `RunStats`.
19pub struct RunReportParams<'a> {
20    pub stats: &'a RunStats,
21}
22
23// ── RunReport ─────────────────────────────────────────────────────────────────
24
25impl RunReport {
26    /// Constructs a `RunReport` from a completed `RunStats`.
27    ///
28    /// All derived metrics (percentiles, throughput, error rate, status code map,
29    /// response stats, per-stage breakdowns) are computed here via the compute
30    /// module functions.
31    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// ── Tests ─────────────────────────────────────────────────────────────────────
87
88#[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    // ── run_report_fixed_mode_no_response_stats ────────────────────────────────
120
121    #[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    // ── run_report_curve_mode_stages_populated ────────────────────────────────
134
135    #[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        // Override curve_stats with the actual stages
152        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; // just to ensure it compiles
170
171        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    // ── run_report_serializes_to_valid_json ───────────────────────────────────
184
185    #[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        // No sampling field in v2
199        assert!(parsed["sampling"].is_null());
200    }
201
202    // ── run_report_error_rate_computed_correctly ──────────────────────────────
203
204    #[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        // 5 failures out of 100
209        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}