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    pub fn print_summary_with_mode(results: &K6Results, duration_secs: u64, cps_mode: bool) {
24        println!("\n{}", "=".repeat(60).bright_green());
25        println!("{}", "Load Test Complete! ✓".bright_green().bold());
26        println!("{}\n", "=".repeat(60).bright_green());
27
28        println!("{}", "Summary:".bold());
29        println!("  Total Requests:       {}", results.total_requests.to_string().cyan());
30        println!(
31            "  Successful:           {} ({}%)",
32            (results.total_requests - results.failed_requests).to_string().green(),
33            format!("{:.2}", results.success_rate()).green()
34        );
35        println!(
36            "  Failed:               {} ({}%)",
37            results.failed_requests.to_string().red(),
38            format!("{:.2}", results.error_rate()).red()
39        );
40
41        println!("\n{}", "Response Times:".bold());
42        println!("  Min:                  {}ms", format!("{:.2}", results.min_duration_ms).cyan());
43        println!("  Avg:                  {}ms", format!("{:.2}", results.avg_duration_ms).cyan());
44        println!("  Med:                  {}ms", format!("{:.2}", results.med_duration_ms).cyan());
45        println!("  p90:                  {}ms", format!("{:.2}", results.p90_duration_ms).cyan());
46        println!("  p95:                  {}ms", format!("{:.2}", results.p95_duration_ms).cyan());
47        println!("  p99:                  {}ms", format!("{:.2}", results.p99_duration_ms).cyan());
48        println!("  Max:                  {}ms", format!("{:.2}", results.max_duration_ms).cyan());
49
50        println!("\n{}", "Throughput:".bold());
51        if results.rps > 0.0 {
52            println!("  RPS:                  {} req/s", format!("{:.1}", results.rps).cyan());
53        } else {
54            println!(
55                "  RPS:                  {} req/s",
56                format!("{:.1}", results.total_requests as f64 / duration_secs as f64).cyan()
57            );
58        }
59        if results.vus_max > 0 {
60            println!("  Max VUs:              {}", results.vus_max.to_string().cyan());
61        }
62
63        // Issue #79 (round 5) — Connections-per-second report. When the user
64        // passed `--cps`, k6 ran with `noConnectionReuse: true` so each
65        // request opened a new TCP/TLS connection. CPS therefore equals the
66        // request rate; show it explicitly, plus connect/handshake timing.
67        if cps_mode {
68            let cps = if results.rps > 0.0 {
69                results.rps
70            } else {
71                results.total_requests as f64 / duration_secs.max(1) as f64
72            };
73            println!("  CPS:                  {} conn/s (--cps)", format!("{:.1}", cps).cyan());
74            println!("  Total Connections:    {}", results.total_requests.to_string().cyan());
75            if results.tcp_connect_samples > 0 {
76                println!(
77                    "  TCP connect:          avg {:.2}ms, max {:.2}ms",
78                    results.tcp_connect_avg_ms, results.tcp_connect_max_ms,
79                );
80            }
81            if results.tls_handshake_samples > 0 {
82                println!(
83                    "  TLS handshake:        avg {:.2}ms, max {:.2}ms",
84                    results.tls_handshake_avg_ms, results.tls_handshake_max_ms,
85                );
86            }
87        }
88
89        // Issue #79 — server-injected chaos signals (latency / jitter / faults)
90        // observed from MockForge response headers. Surfaces the slice of
91        // total wire time that came from the chaos middleware vs the system
92        // under test.
93        if results.server_injected_latency_samples > 0
94            || results.server_injected_jitter_samples > 0
95            || results.server_reported_faults > 0
96        {
97            println!("\n{}", "Server-Injected (chaos):".bold());
98            if results.server_injected_latency_samples > 0 {
99                println!(
100                    "  Latency samples:      {} (avg {:.2}ms, max {:.2}ms)",
101                    results.server_injected_latency_samples.to_string().cyan(),
102                    results.server_injected_latency_avg_ms,
103                    results.server_injected_latency_max_ms,
104                );
105            }
106            if results.server_injected_jitter_samples > 0 {
107                println!(
108                    "  Jitter samples:       {} (avg {:.2}ms)",
109                    results.server_injected_jitter_samples.to_string().cyan(),
110                    results.server_injected_jitter_avg_ms,
111                );
112            }
113            if results.server_reported_faults > 0 {
114                println!(
115                    "  Fault-marked resps:   {}",
116                    results.server_reported_faults.to_string().cyan(),
117                );
118            }
119        }
120
121        println!("\n{}", "=".repeat(60).bright_green());
122    }
123
124    /// Print header information
125    pub fn print_header(
126        spec_file: &str,
127        target: &str,
128        num_operations: usize,
129        scenario: &str,
130        duration_secs: u64,
131    ) {
132        println!("\n{}\n", "MockForge Bench - Load Testing Mode".bright_green().bold());
133        println!("{}", "─".repeat(60).bright_black());
134
135        println!("{}: {}", "Specification".bold(), spec_file.cyan());
136        println!("{}: {}", "Target".bold(), target.cyan());
137        println!("{}: {} endpoints", "Operations".bold(), num_operations.to_string().cyan());
138        println!("{}: {}", "Scenario".bold(), scenario.cyan());
139        println!("{}: {}s", "Duration".bold(), duration_secs.to_string().cyan());
140
141        println!("{}\n", "─".repeat(60).bright_black());
142    }
143
144    /// Print progress message
145    pub fn print_progress(message: &str) {
146        println!("{} {}", "→".bright_green().bold(), message);
147    }
148
149    /// Print error message
150    pub fn print_error(message: &str) {
151        eprintln!("{} {}", "✗".bright_red().bold(), message.red());
152    }
153
154    /// Print success message
155    pub fn print_success(message: &str) {
156        println!("{} {}", "✓".bright_green().bold(), message.green());
157    }
158
159    /// Print warning message
160    pub fn print_warning(message: &str) {
161        println!("{} {}", "⚠".bright_yellow().bold(), message.yellow());
162    }
163
164    /// Print multi-target summary
165    pub fn print_multi_target_summary(results: &AggregatedResults) {
166        println!("\n{}", "=".repeat(60).bright_green());
167        println!("{}", "Multi-Target Load Test Complete! ✓".bright_green().bold());
168        println!("{}\n", "=".repeat(60).bright_green());
169
170        println!("{}", "Overall Summary:".bold());
171        println!("  Total Targets:        {}", results.total_targets.to_string().cyan());
172        println!(
173            "  Successful:           {} ({}%)",
174            results.successful_targets.to_string().green(),
175            format!(
176                "{:.1}",
177                (results.successful_targets as f64 / results.total_targets as f64) * 100.0
178            )
179            .green()
180        );
181        println!(
182            "  Failed:               {} ({}%)",
183            results.failed_targets.to_string().red(),
184            format!(
185                "{:.1}",
186                (results.failed_targets as f64 / results.total_targets as f64) * 100.0
187            )
188            .red()
189        );
190
191        println!("\n{}", "Aggregated Metrics:".bold());
192        println!(
193            "  Total Requests:       {}",
194            results.aggregated_metrics.total_requests.to_string().cyan()
195        );
196        println!(
197            "  Failed Requests:      {} ({}%)",
198            results.aggregated_metrics.total_failed_requests.to_string().red(),
199            format!("{:.2}", results.aggregated_metrics.error_rate).red()
200        );
201        println!(
202            "  Total RPS:            {} req/s",
203            format!("{:.1}", results.aggregated_metrics.total_rps).cyan()
204        );
205        println!(
206            "  Avg RPS/target:       {} req/s",
207            format!("{:.1}", results.aggregated_metrics.avg_rps).cyan()
208        );
209        println!(
210            "  Total VUs:            {}",
211            results.aggregated_metrics.total_vus_max.to_string().cyan()
212        );
213        println!(
214            "  Avg Response Time:    {}ms",
215            format!("{:.2}", results.aggregated_metrics.avg_duration_ms).cyan()
216        );
217        println!(
218            "  p95 Response Time:    {}ms",
219            format!("{:.2}", results.aggregated_metrics.p95_duration_ms).cyan()
220        );
221        println!(
222            "  p99 Response Time:    {}ms",
223            format!("{:.2}", results.aggregated_metrics.p99_duration_ms).cyan()
224        );
225
226        // Show per-target summary
227        let print_target = |result: &crate::parallel_executor::TargetResult| {
228            let status = if result.success {
229                "✓".bright_green()
230            } else {
231                "✗".bright_red()
232            };
233            println!("  {} {}", status, result.target_url.cyan());
234            if result.success {
235                println!(
236                    "      Requests: {}  RPS: {}  VUs: {}",
237                    result.results.total_requests.to_string().white(),
238                    format!("{:.1}", result.results.rps).white(),
239                    result.results.vus_max.to_string().white(),
240                );
241                println!(
242                    "      Latency: min={:.1}ms avg={:.1}ms med={:.1}ms p90={:.1}ms p95={:.1}ms p99={:.1}ms max={:.1}ms",
243                    result.results.min_duration_ms,
244                    result.results.avg_duration_ms,
245                    result.results.med_duration_ms,
246                    result.results.p90_duration_ms,
247                    result.results.p95_duration_ms,
248                    result.results.p99_duration_ms,
249                    result.results.max_duration_ms,
250                );
251            }
252            if let Some(error) = &result.error {
253                println!("      Error: {}", error.red());
254            }
255        };
256
257        if results.total_targets <= 20 {
258            println!("\n{}", "Per-Target Results:".bold());
259            for result in &results.target_results {
260                print_target(result);
261            }
262        } else {
263            // Show top 10 and bottom 10
264            println!("\n{}", "Top 10 Targets (by requests):".bold());
265            let mut sorted_results = results.target_results.clone();
266            sorted_results.sort_by_key(|r| r.results.total_requests);
267            sorted_results.reverse();
268
269            for result in sorted_results.iter().take(10) {
270                print_target(result);
271            }
272
273            println!("\n{}", "Bottom 10 Targets:".bold());
274            for result in sorted_results.iter().rev().take(10) {
275                print_target(result);
276            }
277        }
278
279        println!("\n{}", "=".repeat(60).bright_green());
280    }
281}
282
283#[cfg(test)]
284mod tests {
285    use super::*;
286
287    #[test]
288    fn test_terminal_reporter_creation() {
289        let _reporter = TerminalReporter;
290    }
291}