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 if num_operations == 0 {
260 println!("{}: {}", "Operations".bold(), "(analyzing spec…)".bright_black());
261 } else {
262 println!("{}: {} endpoints", "Operations".bold(), num_operations.to_string().cyan());
263 }
264 println!("{}: {}", "Scenario".bold(), scenario.cyan());
265 println!("{}: {}s", "Duration".bold(), duration_secs.to_string().cyan());
266
267 println!("{}\n", "─".repeat(60).bright_black());
268 }
269
270 pub fn print_progress(message: &str) {
272 println!("{} {}", "→".bright_green().bold(), message);
273 }
274
275 pub fn print_error(message: &str) {
277 eprintln!("{} {}", "✗".bright_red().bold(), message.red());
278 }
279
280 pub fn print_success(message: &str) {
282 println!("{} {}", "✓".bright_green().bold(), message.green());
283 }
284
285 pub fn print_warning(message: &str) {
287 println!("{} {}", "⚠".bright_yellow().bold(), message.yellow());
288 }
289
290 pub fn print_multi_target_summary(results: &AggregatedResults) {
292 println!("\n{}", "=".repeat(60).bright_green());
293 println!("{}", "Multi-Target Load Test Complete! ✓".bright_green().bold());
294 println!("{}\n", "=".repeat(60).bright_green());
295
296 println!("{}", "Overall Summary:".bold());
297 println!(" Total Targets: {}", results.total_targets.to_string().cyan());
298 println!(
299 " Successful: {} ({}%)",
300 results.successful_targets.to_string().green(),
301 format!(
302 "{:.1}",
303 (results.successful_targets as f64 / results.total_targets as f64) * 100.0
304 )
305 .green()
306 );
307 println!(
308 " Failed: {} ({}%)",
309 results.failed_targets.to_string().red(),
310 format!(
311 "{:.1}",
312 (results.failed_targets as f64 / results.total_targets as f64) * 100.0
313 )
314 .red()
315 );
316
317 println!("\n{}", "Aggregated Metrics:".bold());
318 println!(
319 " Total Requests: {}",
320 results.aggregated_metrics.total_requests.to_string().cyan()
321 );
322 println!(
323 " Failed Requests: {} ({}%)",
324 results.aggregated_metrics.total_failed_requests.to_string().red(),
325 format!("{:.2}", results.aggregated_metrics.error_rate).red()
326 );
327 println!(
328 " Total RPS: {} req/s",
329 format!("{:.1}", results.aggregated_metrics.total_rps).cyan()
330 );
331 println!(
332 " Avg RPS/target: {} req/s",
333 format!("{:.1}", results.aggregated_metrics.avg_rps).cyan()
334 );
335 println!(
336 " Total VUs: {}",
337 results.aggregated_metrics.total_vus_max.to_string().cyan()
338 );
339 println!(
340 " Avg Response Time: {}ms",
341 format!("{:.2}", results.aggregated_metrics.avg_duration_ms).cyan()
342 );
343 println!(
344 " p95 Response Time: {}ms",
345 format!("{:.2}", results.aggregated_metrics.p95_duration_ms).cyan()
346 );
347 println!(
348 " p99 Response Time: {}ms",
349 format!("{:.2}", results.aggregated_metrics.p99_duration_ms).cyan()
350 );
351
352 if results.aggregated_metrics.total_connections_opened > 0 {
357 println!(
358 " Total Connections: {}",
359 results.aggregated_metrics.total_connections_opened.to_string().cyan()
360 );
361 if results.aggregated_metrics.total_vus_max > 0 {
362 let reuse_ratio = results.aggregated_metrics.total_connections_opened as f64
363 / results.aggregated_metrics.total_vus_max as f64;
364 if reuse_ratio > 5.0 {
365 println!(
366 " {}: {:.0}× more sockets opened than concurrent VUs across all targets — \
367 at least one target is closing connections (proxy pool disabled, \
368 `Connection: close`, or short upstream idle timeout).",
369 "Connection reuse NOT detected".yellow().bold(),
370 reuse_ratio,
371 );
372 }
373 }
374 }
375 if results.aggregated_metrics.total_iterations_completed > 0 {
376 println!(
377 " Total Iterations: {} complete (sum across all targets)",
378 results.aggregated_metrics.total_iterations_completed.to_string().cyan()
379 );
380 }
381
382 let print_target = |result: &crate::parallel_executor::TargetResult| {
384 let status = if result.success {
385 "✓".bright_green()
386 } else {
387 "✗".bright_red()
388 };
389 println!(" {} {}", status, result.target_url.cyan());
390 if result.success {
391 println!(
392 " Requests: {} RPS: {} VUs: {}",
393 result.results.total_requests.to_string().white(),
394 format!("{:.1}", result.results.rps).white(),
395 result.results.vus_max.to_string().white(),
396 );
397 println!(
398 " Latency: min={:.1}ms avg={:.1}ms med={:.1}ms p90={:.1}ms p95={:.1}ms p99={:.1}ms max={:.1}ms",
399 result.results.min_duration_ms,
400 result.results.avg_duration_ms,
401 result.results.med_duration_ms,
402 result.results.p90_duration_ms,
403 result.results.p95_duration_ms,
404 result.results.p99_duration_ms,
405 result.results.max_duration_ms,
406 );
407 if result.results.tcp_connect_samples > 0 || result.results.iterations_completed > 0
410 {
411 println!(
412 " Connections: {} Iterations: {}",
413 result.results.tcp_connect_samples.to_string().white(),
414 result.results.iterations_completed.to_string().white(),
415 );
416 }
417 }
418 if let Some(error) = &result.error {
419 println!(" Error: {}", error.red());
420 }
421 };
422
423 if results.total_targets <= 20 {
424 println!("\n{}", "Per-Target Results:".bold());
425 for result in &results.target_results {
426 print_target(result);
427 }
428 } else {
429 println!("\n{}", "Top 10 Targets (by requests):".bold());
431 let mut sorted_results = results.target_results.clone();
432 sorted_results.sort_by_key(|r| r.results.total_requests);
433 sorted_results.reverse();
434
435 for result in sorted_results.iter().take(10) {
436 print_target(result);
437 }
438
439 println!("\n{}", "Bottom 10 Targets:".bold());
440 for result in sorted_results.iter().rev().take(10) {
441 print_target(result);
442 }
443 }
444
445 println!("\n{}", "=".repeat(60).bright_green());
446 }
447}
448
449#[cfg(test)]
450mod tests {
451 use super::*;
452
453 #[test]
454 fn test_terminal_reporter_creation() {
455 let _reporter = TerminalReporter;
456 }
457}