Skip to main content

mockforge_bench/
reporter.rs

1//! Result reporting and formatting
2
3use crate::executor::K6Results;
4use crate::parallel_executor::AggregatedResults;
5use colored::*;
6
7/// Terminal reporter for bench results
8pub struct TerminalReporter;
9
10impl TerminalReporter {
11    /// Print a summary of the bench results.
12    ///
13    /// `cps_mode` is `true` when the bench was invoked with `--cps`. In that
14    /// mode each request opens a fresh TCP/TLS connection, so we print an
15    /// explicit "Connection Rate" line alongside RPS — Srikanth's round-5
16    /// reply on Issue #79: "CPS without RPS Command is Working but Client
17    /// dont report CPS Counts".
18    pub fn print_summary(results: &K6Results, duration_secs: u64) {
19        Self::print_summary_with_mode(results, duration_secs, false);
20    }
21
22    /// Like [`print_summary`] but lets the caller opt into the `--cps` view.
23    ///
24    /// Issue #79 round 6 — the connection-count lines now render unconditionally
25    /// whenever k6 reported `http_req_connecting` samples (i.e. it actually
26    /// opened TCP sockets), so non-`--cps` runs also surface client-side
27    /// connection counts. Without `--cps` k6 reuses sockets, so the count
28    /// equals "distinct connections opened", which is what Srikanth wanted
29    /// alongside RPS.
30    pub fn print_summary_with_mode(results: &K6Results, duration_secs: u64, cps_mode: bool) {
31        println!("\n{}", "=".repeat(60).bright_green());
32        println!("{}", "Load Test Complete! ✓".bright_green().bold());
33        println!("{}\n", "=".repeat(60).bright_green());
34
35        println!("{}", "Summary:".bold());
36        println!("  Total Requests:       {}", results.total_requests.to_string().cyan());
37        println!(
38            "  Successful:           {} ({}%)",
39            (results.total_requests - results.failed_requests).to_string().green(),
40            format!("{:.2}", results.success_rate()).green()
41        );
42        println!(
43            "  Failed:               {} ({}%)",
44            results.failed_requests.to_string().red(),
45            format!("{:.2}", results.error_rate()).red()
46        );
47
48        println!("\n{}", "Response Times:".bold());
49        println!("  Min:                  {}ms", format!("{:.2}", results.min_duration_ms).cyan());
50        println!("  Avg:                  {}ms", format!("{:.2}", results.avg_duration_ms).cyan());
51        println!("  Med:                  {}ms", format!("{:.2}", results.med_duration_ms).cyan());
52        println!("  p90:                  {}ms", format!("{:.2}", results.p90_duration_ms).cyan());
53        println!("  p95:                  {}ms", format!("{:.2}", results.p95_duration_ms).cyan());
54        println!("  p99:                  {}ms", format!("{:.2}", results.p99_duration_ms).cyan());
55        println!("  Max:                  {}ms", format!("{:.2}", results.max_duration_ms).cyan());
56
57        println!("\n{}", "Throughput:".bold());
58        if results.rps > 0.0 {
59            println!("  RPS:                  {} req/s", format!("{:.1}", results.rps).cyan());
60        } else {
61            println!(
62                "  RPS:                  {} req/s",
63                format!("{:.1}", results.total_requests as f64 / duration_secs as f64).cyan()
64            );
65        }
66        if results.vus_max > 0 {
67            println!("  Max VUs:              {}", results.vus_max.to_string().cyan());
68        }
69
70        // Issue #79 (round 5) — Connections-per-second report. When the user
71        // passed `--cps`, k6 ran with `noConnectionReuse: true` so each
72        // request opened a new TCP/TLS connection. CPS therefore equals the
73        // request rate; show it explicitly, plus connect/handshake timing.
74        if cps_mode {
75            let cps = if results.rps > 0.0 {
76                results.rps
77            } else {
78                results.total_requests as f64 / duration_secs.max(1) as f64
79            };
80            println!("  CPS:                  {} conn/s (--cps)", format!("{:.1}", cps).cyan());
81            println!("  Total Connections:    {}", results.total_requests.to_string().cyan());
82        }
83
84        // Issue #79 round 6 — Always surface client-side connection counts
85        // when k6 actually opened sockets. Helpful even without `--cps`
86        // because it lets you compare "k6 opened N connections" against
87        // the server's `connections_total_opened` and detect whether
88        // your proxy is keeping the upstream pool warm. `tcp_connect_samples`
89        // is k6's `http_req_connecting.count`, which is the number of
90        // request iterations that had to wait for a new TCP socket — i.e.
91        // a fresh connection.
92        if results.tcp_connect_samples > 0 {
93            if !cps_mode {
94                // Surface the count in non-CPS mode too — Srikanth's "open
95                // connection on the client" ask.
96                println!(
97                    "  Connections opened:   {} ({} conn/s avg)",
98                    results.tcp_connect_samples.to_string().cyan(),
99                    format!(
100                        "{:.1}",
101                        results.tcp_connect_samples as f64 / duration_secs.max(1) as f64
102                    )
103                    .cyan(),
104                );
105            }
106            println!(
107                "  TCP connect:          avg {:.2}ms, max {:.2}ms",
108                results.tcp_connect_avg_ms, results.tcp_connect_max_ms,
109            );
110        }
111        if results.tls_handshake_samples > 0 {
112            println!(
113                "  TLS handshake:        avg {:.2}ms, max {:.2}ms",
114                results.tls_handshake_avg_ms, results.tls_handshake_max_ms,
115            );
116        }
117        // Peak concurrent VUs — the upper bound on simultaneously-open
118        // connections from the client. For HTTP/1.1 each VU holds at
119        // most one socket; for HTTP/2 with multiplexing this is the
120        // bound on streams, not sockets. Surface it as an open-connection
121        // ceiling so users can sanity-check against the server's
122        // `connections_open` gauge.
123        if results.vus_max > 0 && (cps_mode || results.tcp_connect_samples > 0) {
124            println!(
125                "  Peak concurrent VUs:  {} (max open conns from client side)",
126                results.vus_max.to_string().cyan(),
127            );
128        }
129
130        // Issue #79 — server-injected chaos signals (latency / jitter / faults)
131        // observed from MockForge response headers. Surfaces the slice of
132        // total wire time that came from the chaos middleware vs the system
133        // under test.
134        if results.server_injected_latency_samples > 0
135            || results.server_injected_jitter_samples > 0
136            || results.server_reported_faults > 0
137        {
138            println!("\n{}", "Server-Injected (chaos):".bold());
139            if results.server_injected_latency_samples > 0 {
140                println!(
141                    "  Latency samples:      {} (avg {:.2}ms, max {:.2}ms)",
142                    results.server_injected_latency_samples.to_string().cyan(),
143                    results.server_injected_latency_avg_ms,
144                    results.server_injected_latency_max_ms,
145                );
146            }
147            if results.server_injected_jitter_samples > 0 {
148                println!(
149                    "  Jitter samples:       {} (avg {:.2}ms)",
150                    results.server_injected_jitter_samples.to_string().cyan(),
151                    results.server_injected_jitter_avg_ms,
152                );
153            }
154            if results.server_reported_faults > 0 {
155                println!(
156                    "  Fault-marked resps:   {}",
157                    results.server_reported_faults.to_string().cyan(),
158                );
159            }
160        }
161
162        println!("\n{}", "=".repeat(60).bright_green());
163    }
164
165    /// Print header information
166    pub fn print_header(
167        spec_file: &str,
168        target: &str,
169        num_operations: usize,
170        scenario: &str,
171        duration_secs: u64,
172    ) {
173        println!("\n{}\n", "MockForge Bench - Load Testing Mode".bright_green().bold());
174        println!("{}", "─".repeat(60).bright_black());
175
176        println!("{}: {}", "Specification".bold(), spec_file.cyan());
177        println!("{}: {}", "Target".bold(), target.cyan());
178        println!("{}: {} endpoints", "Operations".bold(), num_operations.to_string().cyan());
179        println!("{}: {}", "Scenario".bold(), scenario.cyan());
180        println!("{}: {}s", "Duration".bold(), duration_secs.to_string().cyan());
181
182        println!("{}\n", "─".repeat(60).bright_black());
183    }
184
185    /// Print progress message
186    pub fn print_progress(message: &str) {
187        println!("{} {}", "→".bright_green().bold(), message);
188    }
189
190    /// Print error message
191    pub fn print_error(message: &str) {
192        eprintln!("{} {}", "✗".bright_red().bold(), message.red());
193    }
194
195    /// Print success message
196    pub fn print_success(message: &str) {
197        println!("{} {}", "✓".bright_green().bold(), message.green());
198    }
199
200    /// Print warning message
201    pub fn print_warning(message: &str) {
202        println!("{} {}", "⚠".bright_yellow().bold(), message.yellow());
203    }
204
205    /// Print multi-target summary
206    pub fn print_multi_target_summary(results: &AggregatedResults) {
207        println!("\n{}", "=".repeat(60).bright_green());
208        println!("{}", "Multi-Target Load Test Complete! ✓".bright_green().bold());
209        println!("{}\n", "=".repeat(60).bright_green());
210
211        println!("{}", "Overall Summary:".bold());
212        println!("  Total Targets:        {}", results.total_targets.to_string().cyan());
213        println!(
214            "  Successful:           {} ({}%)",
215            results.successful_targets.to_string().green(),
216            format!(
217                "{:.1}",
218                (results.successful_targets as f64 / results.total_targets as f64) * 100.0
219            )
220            .green()
221        );
222        println!(
223            "  Failed:               {} ({}%)",
224            results.failed_targets.to_string().red(),
225            format!(
226                "{:.1}",
227                (results.failed_targets as f64 / results.total_targets as f64) * 100.0
228            )
229            .red()
230        );
231
232        println!("\n{}", "Aggregated Metrics:".bold());
233        println!(
234            "  Total Requests:       {}",
235            results.aggregated_metrics.total_requests.to_string().cyan()
236        );
237        println!(
238            "  Failed Requests:      {} ({}%)",
239            results.aggregated_metrics.total_failed_requests.to_string().red(),
240            format!("{:.2}", results.aggregated_metrics.error_rate).red()
241        );
242        println!(
243            "  Total RPS:            {} req/s",
244            format!("{:.1}", results.aggregated_metrics.total_rps).cyan()
245        );
246        println!(
247            "  Avg RPS/target:       {} req/s",
248            format!("{:.1}", results.aggregated_metrics.avg_rps).cyan()
249        );
250        println!(
251            "  Total VUs:            {}",
252            results.aggregated_metrics.total_vus_max.to_string().cyan()
253        );
254        println!(
255            "  Avg Response Time:    {}ms",
256            format!("{:.2}", results.aggregated_metrics.avg_duration_ms).cyan()
257        );
258        println!(
259            "  p95 Response Time:    {}ms",
260            format!("{:.2}", results.aggregated_metrics.p95_duration_ms).cyan()
261        );
262        println!(
263            "  p99 Response Time:    {}ms",
264            format!("{:.2}", results.aggregated_metrics.p99_duration_ms).cyan()
265        );
266
267        // Show per-target summary
268        let print_target = |result: &crate::parallel_executor::TargetResult| {
269            let status = if result.success {
270                "✓".bright_green()
271            } else {
272                "✗".bright_red()
273            };
274            println!("  {} {}", status, result.target_url.cyan());
275            if result.success {
276                println!(
277                    "      Requests: {}  RPS: {}  VUs: {}",
278                    result.results.total_requests.to_string().white(),
279                    format!("{:.1}", result.results.rps).white(),
280                    result.results.vus_max.to_string().white(),
281                );
282                println!(
283                    "      Latency: min={:.1}ms avg={:.1}ms med={:.1}ms p90={:.1}ms p95={:.1}ms p99={:.1}ms max={:.1}ms",
284                    result.results.min_duration_ms,
285                    result.results.avg_duration_ms,
286                    result.results.med_duration_ms,
287                    result.results.p90_duration_ms,
288                    result.results.p95_duration_ms,
289                    result.results.p99_duration_ms,
290                    result.results.max_duration_ms,
291                );
292            }
293            if let Some(error) = &result.error {
294                println!("      Error: {}", error.red());
295            }
296        };
297
298        if results.total_targets <= 20 {
299            println!("\n{}", "Per-Target Results:".bold());
300            for result in &results.target_results {
301                print_target(result);
302            }
303        } else {
304            // Show top 10 and bottom 10
305            println!("\n{}", "Top 10 Targets (by requests):".bold());
306            let mut sorted_results = results.target_results.clone();
307            sorted_results.sort_by_key(|r| r.results.total_requests);
308            sorted_results.reverse();
309
310            for result in sorted_results.iter().take(10) {
311                print_target(result);
312            }
313
314            println!("\n{}", "Bottom 10 Targets:".bold());
315            for result in sorted_results.iter().rev().take(10) {
316                print_target(result);
317            }
318        }
319
320        println!("\n{}", "=".repeat(60).bright_green());
321    }
322}
323
324#[cfg(test)]
325mod tests {
326    use super::*;
327
328    #[test]
329    fn test_terminal_reporter_creation() {
330        let _reporter = TerminalReporter;
331    }
332}