1use crate::executor::K6Results;
4use crate::parallel_executor::AggregatedResults;
5use colored::*;
6
7pub struct TerminalReporter;
9
10impl TerminalReporter {
11 pub fn print_summary(results: &K6Results, duration_secs: u64) {
19 Self::print_summary_with_mode(results, duration_secs, false);
20 }
21
22 pub fn print_summary_with_mode(results: &K6Results, duration_secs: u64, cps_mode: bool) {
31 Self::print_summary_full(results, duration_secs, cps_mode, None);
32 }
33
34 pub fn print_summary_full(
43 results: &K6Results,
44 duration_secs: u64,
45 cps_mode: bool,
46 num_operations: Option<u32>,
47 ) {
48 println!("\n{}", "=".repeat(60).bright_green());
49 println!("{}", "Load Test Complete! ✓".bright_green().bold());
50 println!("{}\n", "=".repeat(60).bright_green());
51
52 println!("{}", "Summary:".bold());
53 println!(" Total Requests: {}", results.total_requests.to_string().cyan());
54 println!(
55 " Successful: {} ({}%)",
56 (results.total_requests - results.failed_requests).to_string().green(),
57 format!("{:.2}", results.success_rate()).green()
58 );
59 println!(
60 " Failed: {} ({}%)",
61 results.failed_requests.to_string().red(),
62 format!("{:.2}", results.error_rate()).red()
63 );
64
65 println!("\n{}", "Response Times:".bold());
66 println!(" Min: {}ms", format!("{:.2}", results.min_duration_ms).cyan());
67 println!(" Avg: {}ms", format!("{:.2}", results.avg_duration_ms).cyan());
68 println!(" Med: {}ms", format!("{:.2}", results.med_duration_ms).cyan());
69 println!(" p90: {}ms", format!("{:.2}", results.p90_duration_ms).cyan());
70 println!(" p95: {}ms", format!("{:.2}", results.p95_duration_ms).cyan());
71 println!(" p99: {}ms", format!("{:.2}", results.p99_duration_ms).cyan());
72 println!(" Max: {}ms", format!("{:.2}", results.max_duration_ms).cyan());
73
74 println!("\n{}", "Throughput:".bold());
75 if results.rps > 0.0 {
76 println!(" RPS: {} req/s", format!("{:.1}", results.rps).cyan());
77 } else {
78 println!(
79 " RPS: {} req/s",
80 format!("{:.1}", results.total_requests as f64 / duration_secs as f64).cyan()
81 );
82 }
83 if results.vus_max > 0 {
84 println!(" Max VUs: {}", results.vus_max.to_string().cyan());
85 }
86
87 if cps_mode {
92 let cps = if results.rps > 0.0 {
93 results.rps
94 } else {
95 results.total_requests as f64 / duration_secs.max(1) as f64
96 };
97 println!(" CPS: {} conn/s (--cps)", format!("{:.1}", cps).cyan());
98 println!(" Total Connections: {}", results.total_requests.to_string().cyan());
99 }
100
101 if results.tcp_connect_samples > 0 && !cps_mode {
114 println!(
117 " Connections opened: {} ({} conn/s avg)",
118 results.tcp_connect_samples.to_string().cyan(),
119 format!("{:.1}", results.tcp_connect_samples as f64 / duration_secs.max(1) as f64)
120 .cyan(),
121 );
122
123 if results.vus_max > 0 {
130 let reuse_ratio = results.tcp_connect_samples as f64 / results.vus_max as f64;
131 if reuse_ratio > 5.0 {
132 println!(
133 " {}: {:.0}× more sockets opened than concurrent VUs — \
134 the target is closing connections (proxy pool disabled, \
135 `Connection: close`, or short upstream idle timeout).",
136 "Connection reuse NOT detected".yellow().bold(),
137 reuse_ratio,
138 );
139 }
140 }
141 }
142 if results.tcp_connect_avg_ms > 0.0 || results.tcp_connect_max_ms > 0.0 {
147 println!(
148 " TCP connect: avg {:.2}ms, max {:.2}ms",
149 results.tcp_connect_avg_ms, results.tcp_connect_max_ms,
150 );
151 }
152 if results.tls_handshake_avg_ms > 0.0 || results.tls_handshake_max_ms > 0.0 {
153 println!(
154 " TLS handshake: avg {:.2}ms, max {:.2}ms",
155 results.tls_handshake_avg_ms, results.tls_handshake_max_ms,
156 );
157 }
158 if results.vus_max > 0
165 && (cps_mode || results.tcp_connect_samples > 0 || results.tcp_connect_avg_ms > 0.0)
166 {
167 println!(
168 " Peak concurrent VUs: {} (max open conns from client side)",
169 results.vus_max.to_string().cyan(),
170 );
171 }
172
173 if results.iterations_completed > 0 {
179 if let Some(num_ops) = num_operations {
180 let expected_reqs_per_iter = num_ops as u64;
181 let full_iter_reqs =
182 results.iterations_completed.saturating_mul(expected_reqs_per_iter);
183 let partial_iter_reqs = results.total_requests.saturating_sub(full_iter_reqs);
184 println!(
185 " Iterations: {} complete × {} ops = {} ops fully exercised",
186 results.iterations_completed.to_string().cyan(),
187 num_ops.to_string().cyan(),
188 full_iter_reqs.to_string().cyan(),
189 );
190 if partial_iter_reqs > 0 && num_ops > 1 {
191 println!(
192 " {} extra request(s) from a partially-completed \
193 iteration — duration ended mid-walk; not every op was hit on the last pass.",
194 partial_iter_reqs.to_string().yellow(),
195 );
196 }
197 } else {
198 println!(
199 " Iterations: {} complete",
200 results.iterations_completed.to_string().cyan(),
201 );
202 }
203 }
204
205 if results.server_injected_latency_samples > 0
210 || results.server_injected_jitter_samples > 0
211 || results.server_reported_faults > 0
212 {
213 println!("\n{}", "Server-Injected (chaos):".bold());
214 if results.server_injected_latency_samples > 0 {
215 println!(
216 " Latency samples: {} (avg {:.2}ms, max {:.2}ms)",
217 results.server_injected_latency_samples.to_string().cyan(),
218 results.server_injected_latency_avg_ms,
219 results.server_injected_latency_max_ms,
220 );
221 }
222 if results.server_injected_jitter_samples > 0 {
223 println!(
224 " Jitter samples: {} (avg {:.2}ms)",
225 results.server_injected_jitter_samples.to_string().cyan(),
226 results.server_injected_jitter_avg_ms,
227 );
228 }
229 if results.server_reported_faults > 0 {
230 println!(
231 " Fault-marked resps: {}",
232 results.server_reported_faults.to_string().cyan(),
233 );
234 }
235 }
236
237 println!("\n{}", "=".repeat(60).bright_green());
238 }
239
240 pub fn print_header(
242 spec_file: &str,
243 target: &str,
244 num_operations: usize,
245 scenario: &str,
246 duration_secs: u64,
247 ) {
248 println!("\n{}\n", "MockForge Bench - Load Testing Mode".bright_green().bold());
249 println!("{}", "─".repeat(60).bright_black());
250
251 println!("{}: {}", "Specification".bold(), spec_file.cyan());
252 println!("{}: {}", "Target".bold(), target.cyan());
253 println!("{}: {} endpoints", "Operations".bold(), num_operations.to_string().cyan());
254 println!("{}: {}", "Scenario".bold(), scenario.cyan());
255 println!("{}: {}s", "Duration".bold(), duration_secs.to_string().cyan());
256
257 println!("{}\n", "─".repeat(60).bright_black());
258 }
259
260 pub fn print_progress(message: &str) {
262 println!("{} {}", "→".bright_green().bold(), message);
263 }
264
265 pub fn print_error(message: &str) {
267 eprintln!("{} {}", "✗".bright_red().bold(), message.red());
268 }
269
270 pub fn print_success(message: &str) {
272 println!("{} {}", "✓".bright_green().bold(), message.green());
273 }
274
275 pub fn print_warning(message: &str) {
277 println!("{} {}", "⚠".bright_yellow().bold(), message.yellow());
278 }
279
280 pub fn print_multi_target_summary(results: &AggregatedResults) {
282 println!("\n{}", "=".repeat(60).bright_green());
283 println!("{}", "Multi-Target Load Test Complete! ✓".bright_green().bold());
284 println!("{}\n", "=".repeat(60).bright_green());
285
286 println!("{}", "Overall Summary:".bold());
287 println!(" Total Targets: {}", results.total_targets.to_string().cyan());
288 println!(
289 " Successful: {} ({}%)",
290 results.successful_targets.to_string().green(),
291 format!(
292 "{:.1}",
293 (results.successful_targets as f64 / results.total_targets as f64) * 100.0
294 )
295 .green()
296 );
297 println!(
298 " Failed: {} ({}%)",
299 results.failed_targets.to_string().red(),
300 format!(
301 "{:.1}",
302 (results.failed_targets as f64 / results.total_targets as f64) * 100.0
303 )
304 .red()
305 );
306
307 println!("\n{}", "Aggregated Metrics:".bold());
308 println!(
309 " Total Requests: {}",
310 results.aggregated_metrics.total_requests.to_string().cyan()
311 );
312 println!(
313 " Failed Requests: {} ({}%)",
314 results.aggregated_metrics.total_failed_requests.to_string().red(),
315 format!("{:.2}", results.aggregated_metrics.error_rate).red()
316 );
317 println!(
318 " Total RPS: {} req/s",
319 format!("{:.1}", results.aggregated_metrics.total_rps).cyan()
320 );
321 println!(
322 " Avg RPS/target: {} req/s",
323 format!("{:.1}", results.aggregated_metrics.avg_rps).cyan()
324 );
325 println!(
326 " Total VUs: {}",
327 results.aggregated_metrics.total_vus_max.to_string().cyan()
328 );
329 println!(
330 " Avg Response Time: {}ms",
331 format!("{:.2}", results.aggregated_metrics.avg_duration_ms).cyan()
332 );
333 println!(
334 " p95 Response Time: {}ms",
335 format!("{:.2}", results.aggregated_metrics.p95_duration_ms).cyan()
336 );
337 println!(
338 " p99 Response Time: {}ms",
339 format!("{:.2}", results.aggregated_metrics.p99_duration_ms).cyan()
340 );
341
342 if results.aggregated_metrics.total_connections_opened > 0 {
347 println!(
348 " Total Connections: {}",
349 results.aggregated_metrics.total_connections_opened.to_string().cyan()
350 );
351 if results.aggregated_metrics.total_vus_max > 0 {
352 let reuse_ratio = results.aggregated_metrics.total_connections_opened as f64
353 / results.aggregated_metrics.total_vus_max as f64;
354 if reuse_ratio > 5.0 {
355 println!(
356 " {}: {:.0}× more sockets opened than concurrent VUs across all targets — \
357 at least one target is closing connections (proxy pool disabled, \
358 `Connection: close`, or short upstream idle timeout).",
359 "Connection reuse NOT detected".yellow().bold(),
360 reuse_ratio,
361 );
362 }
363 }
364 }
365 if results.aggregated_metrics.total_iterations_completed > 0 {
366 println!(
367 " Total Iterations: {} complete (sum across all targets)",
368 results.aggregated_metrics.total_iterations_completed.to_string().cyan()
369 );
370 }
371
372 let print_target = |result: &crate::parallel_executor::TargetResult| {
374 let status = if result.success {
375 "✓".bright_green()
376 } else {
377 "✗".bright_red()
378 };
379 println!(" {} {}", status, result.target_url.cyan());
380 if result.success {
381 println!(
382 " Requests: {} RPS: {} VUs: {}",
383 result.results.total_requests.to_string().white(),
384 format!("{:.1}", result.results.rps).white(),
385 result.results.vus_max.to_string().white(),
386 );
387 println!(
388 " Latency: min={:.1}ms avg={:.1}ms med={:.1}ms p90={:.1}ms p95={:.1}ms p99={:.1}ms max={:.1}ms",
389 result.results.min_duration_ms,
390 result.results.avg_duration_ms,
391 result.results.med_duration_ms,
392 result.results.p90_duration_ms,
393 result.results.p95_duration_ms,
394 result.results.p99_duration_ms,
395 result.results.max_duration_ms,
396 );
397 if result.results.tcp_connect_samples > 0 || result.results.iterations_completed > 0
400 {
401 println!(
402 " Connections: {} Iterations: {}",
403 result.results.tcp_connect_samples.to_string().white(),
404 result.results.iterations_completed.to_string().white(),
405 );
406 }
407 }
408 if let Some(error) = &result.error {
409 println!(" Error: {}", error.red());
410 }
411 };
412
413 if results.total_targets <= 20 {
414 println!("\n{}", "Per-Target Results:".bold());
415 for result in &results.target_results {
416 print_target(result);
417 }
418 } else {
419 println!("\n{}", "Top 10 Targets (by requests):".bold());
421 let mut sorted_results = results.target_results.clone();
422 sorted_results.sort_by_key(|r| r.results.total_requests);
423 sorted_results.reverse();
424
425 for result in sorted_results.iter().take(10) {
426 print_target(result);
427 }
428
429 println!("\n{}", "Bottom 10 Targets:".bold());
430 for result in sorted_results.iter().rev().take(10) {
431 print_target(result);
432 }
433 }
434
435 println!("\n{}", "=".repeat(60).bright_green());
436 }
437}
438
439#[cfg(test)]
440mod tests {
441 use super::*;
442
443 #[test]
444 fn test_terminal_reporter_creation() {
445 let _reporter = TerminalReporter;
446 }
447}