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    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
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 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// ── Tests ─────────────────────────────────────────────────────────────────────
85
86#[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    // ── 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.scenarios.is_none());
130        assert!(report.response_stats.is_none());
131        assert!(report.run.curve_duration_ms.is_none());
132    }
133
134    // ── run_report_curve_mode_stages_populated ────────────────────────────────
135
136    #[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        // Override curve_stats with the actual stages
153        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; // just to ensure it compiles
171
172        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    // ── run_report_serializes_to_valid_json ───────────────────────────────────
185
186    #[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        // No sampling field in v2
201        assert!(parsed["sampling"].is_null());
202    }
203
204    // ── run_report_error_rate_computed_correctly ──────────────────────────────
205
206    #[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        // 5 failures out of 100
211        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}