Skip to content

Commit c3b4fe7

Browse files
committed
feat: provide much better debugging experience
1 parent 29524ff commit c3b4fe7

4 files changed

Lines changed: 283 additions & 4 deletions

File tree

src/cli/commands.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ pub enum Commands {
3333
help = "Set variables in key=value format (comma-separated: key1=val1,key2=val2)"
3434
)]
3535
var: Option<String>,
36+
37+
#[arg(long, default_value = "false", help = "Disable failure summary display")]
38+
no_fail_summary: bool,
3639
},
3740

3841
Validate {

src/cli/runner.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ pub fn run(opts: Opts) {
1616
file,
1717
debug: debug_enabled,
1818
var,
19+
no_fail_summary,
1920
} => {
2021
if debug_enabled {
2122
debug::enable_debug();
@@ -27,6 +28,7 @@ pub fn run(opts: Opts) {
2728
disable_color,
2829
file,
2930
var,
31+
no_fail_summary,
3032
));
3133
}
3234
Commands::Validate { file, var } => {
@@ -54,7 +56,8 @@ pub async fn run_tests(
5456
disable_color: bool,
5557
file: Option<String>,
5658
var: Option<String>,
59+
no_fail_summary: bool,
5760
) {
5861
let mut runner = TestRunner::new(disable_color);
59-
runner.execute_tests(filter, verbose, file, var).await;
62+
runner.execute_tests(filter, verbose, file, var, no_fail_summary).await;
6063
}

src/core/runner.rs

Lines changed: 139 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ pub struct TestResult {
2525
pub response_body: Option<Value>,
2626
pub headers: HashMap<String, String>,
2727
pub messages: Vec<String>,
28+
pub method: String,
29+
pub endpoint: String,
2830
}
2931

3032
struct TestExecutionContext<'a> {
@@ -40,6 +42,7 @@ pub struct TestRunner {
4042
pub variables: HashMap<String, String>,
4143
pub results: Vec<TestResult>,
4244
pub disable_color: bool,
45+
pub no_fail_summary: bool,
4346
}
4447

4548
impl TestRunner {
@@ -49,6 +52,7 @@ impl TestRunner {
4952
variables: HashMap::new(),
5053
results: Vec::new(),
5154
disable_color,
55+
no_fail_summary: false,
5256
}
5357
}
5458

@@ -73,6 +77,8 @@ impl TestRunner {
7377
response_body: result.body,
7478
headers: result.headers,
7579
messages: result.errors,
80+
method: test.method.clone(),
81+
endpoint: test.endpoint.clone(),
7682
}
7783
}
7884

@@ -109,6 +115,8 @@ impl TestRunner {
109115
response_body: None,
110116
headers: HashMap::new(),
111117
messages: error_messages.clone(),
118+
method: test.method.clone(),
119+
endpoint: test.endpoint.clone(),
112120
}
113121
} else {
114122
self.execute_test(test, client, test_file_dir).await
@@ -149,8 +157,10 @@ impl TestRunner {
149157
verbose: bool,
150158
file: Option<String>,
151159
var: Option<String>,
160+
no_fail_summary: bool,
152161
) {
153162
load_env_files();
163+
self.no_fail_summary = no_fail_summary;
154164

155165
// Parse CLI variables and add them to the runner's variables
156166
let cli_variables = crate::cli::Commands::parse_variables(var);
@@ -339,16 +349,45 @@ impl TestRunner {
339349
self.results.push(result);
340350
}
341351

342-
self.display_summary(*skipped, context.total);
352+
self.display_compact_results(*skipped, context.total, context.verbose);
353+
if !self.no_fail_summary && !context.verbose {
354+
self.display_failure_details();
355+
}
343356
}
344357

345-
fn display_summary(&self, skipped: usize, total: usize) {
358+
fn display_compact_results(&self, skipped: usize, total: usize, verbose: bool) {
359+
// Only display compact results in non-verbose mode
360+
if verbose {
361+
return;
362+
}
363+
346364
if !self.disable_color {
347365
println!("\n{}", "━".repeat(get_terminal_width()).blue());
366+
println!("\nResults:");
367+
368+
for result in &self.results {
369+
let status_indicator = if result.success {
370+
"✓".green()
371+
} else {
372+
"✗".red()
373+
};
374+
375+
if result.success {
376+
println!(" {} {}", status_indicator, result.name);
377+
} else {
378+
println!(" {} {} (expected {}, got {})",
379+
status_indicator,
380+
result.name,
381+
result.expected_status,
382+
result.actual_status.to_string().red()
383+
);
384+
}
385+
}
386+
387+
println!();
348388
let success_count = self.results.iter().filter(|r| r.success).count();
349389
let fail_count = self.results.len() - success_count;
350390

351-
println!("\nSummary:");
352391
if success_count > 0 {
353392
print!("{} passed", format!("{success_count} tests").green());
354393
}
@@ -365,6 +404,103 @@ impl TestRunner {
365404
print!("{} skipped", format!("{skipped} tests").yellow());
366405
}
367406
println!(" (total: {total})");
407+
} else {
408+
println!("\nResults:");
409+
for result in &self.results {
410+
if result.success {
411+
println!(" ✓ {}", result.name);
412+
} else {
413+
println!(" ✗ {} (expected {}, got {})",
414+
result.name, result.expected_status, result.actual_status);
415+
}
416+
}
417+
418+
let success_count = self.results.iter().filter(|r| r.success).count();
419+
let fail_count = self.results.len() - success_count;
420+
421+
print!("\n{success_count} tests passed");
422+
if fail_count > 0 {
423+
print!(", {fail_count} tests failed");
424+
}
425+
if skipped > 0 {
426+
print!(", {skipped} tests skipped");
427+
}
428+
println!(" (total: {total})");
429+
}
430+
}
431+
432+
fn display_failure_details(&self) {
433+
let failed_results: Vec<_> = self.results.iter().filter(|r| !r.success).collect();
434+
435+
if failed_results.is_empty() {
436+
return;
437+
}
438+
439+
if !self.disable_color {
440+
println!("\n{}", "━".repeat(get_terminal_width()).blue());
441+
println!("\n{}", "Failures:".red().bold());
442+
} else {
443+
println!("\nFailures:");
444+
}
445+
446+
for (i, result) in failed_results.iter().enumerate() {
447+
if i > 0 {
448+
println!();
449+
}
450+
451+
if !self.disable_color {
452+
println!("---");
453+
println!("Test: {}", result.name.bold());
454+
println!("Endpoint: {} {}",
455+
result.method.yellow(),
456+
result.endpoint.yellow()
457+
);
458+
println!("Status: {} (expected {})",
459+
result.actual_status.to_string().red(),
460+
result.expected_status.to_string().bold()
461+
);
462+
} else {
463+
println!("---");
464+
println!("Test: {}", result.name);
465+
println!("Endpoint: {} {}", result.method, result.endpoint);
466+
println!("Status: {} (expected {})", result.actual_status, result.expected_status);
467+
}
468+
469+
if !result.messages.is_empty() {
470+
println!("Messages:");
471+
for msg in &result.messages {
472+
if !self.disable_color {
473+
println!(" - {}", msg.red());
474+
} else {
475+
println!(" - {msg}");
476+
}
477+
}
478+
}
479+
480+
if let Some(body) = &result.response_body {
481+
println!("Response Body:");
482+
let body_str = self.format_response_body(body);
483+
println!("{body_str}");
484+
} else {
485+
println!("Response Body: <none>");
486+
}
487+
}
488+
}
489+
490+
pub const MAX_SUMMARY_BODY_BYTES: usize = 8192;
491+
492+
pub fn format_response_body(&self, body: &Value) -> String {
493+
let formatted = if body.is_object() || body.is_array() {
494+
serde_json::to_string_pretty(body).unwrap_or_else(|_| body.to_string())
495+
} else {
496+
body.to_string()
497+
};
498+
499+
if formatted.len() > Self::MAX_SUMMARY_BODY_BYTES {
500+
let truncated = &formatted[..Self::MAX_SUMMARY_BODY_BYTES];
501+
format!("{truncated}\n... (truncated)")
502+
} else {
503+
formatted
368504
}
369505
}
370506
}

tests/failure_summary_tests.rs

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
use std::collections::HashMap;
2+
use serde_json::json;
3+
use catalyst::core::runner::{TestRunner, TestResult};
4+
5+
#[tokio::test]
6+
async fn test_failure_summary_display() {
7+
let mut runner = TestRunner::new(false); // colored output enabled
8+
runner.no_fail_summary = false; // failure summary enabled
9+
10+
// Add some test results with failures
11+
runner.results.push(TestResult {
12+
name: "Test 1 - Success".to_string(),
13+
success: true,
14+
expected_status: 200,
15+
actual_status: 200,
16+
response_body: Some(json!({"message": "success"})),
17+
headers: HashMap::new(),
18+
messages: vec![],
19+
method: "GET".to_string(),
20+
endpoint: "/api/users".to_string(),
21+
});
22+
23+
runner.results.push(TestResult {
24+
name: "Test 2 - Failure".to_string(),
25+
success: false,
26+
expected_status: 200,
27+
actual_status: 404,
28+
response_body: Some(json!({"error": "Not found", "code": 404})),
29+
headers: HashMap::new(),
30+
messages: vec!["Status mismatch".to_string(), "Body validation failed".to_string()],
31+
method: "POST".to_string(),
32+
endpoint: "/api/users/123".to_string(),
33+
});
34+
35+
runner.results.push(TestResult {
36+
name: "Test 3 - Success".to_string(),
37+
success: true,
38+
expected_status: 201,
39+
actual_status: 201,
40+
response_body: Some(json!({"id": 123, "created": true})),
41+
headers: HashMap::new(),
42+
messages: vec![],
43+
method: "POST".to_string(),
44+
endpoint: "/api/users".to_string(),
45+
});
46+
47+
// Verify that we have the expected mix of results
48+
let success_count = runner.results.iter().filter(|r| r.success).count();
49+
let fail_count = runner.results.iter().filter(|r| !r.success).count();
50+
51+
assert_eq!(success_count, 2);
52+
assert_eq!(fail_count, 1);
53+
54+
// Test the format_response_body method
55+
let json_body = json!({"error": "Not found", "code": 404});
56+
let formatted = runner.format_response_body(&json_body);
57+
assert!(formatted.contains("\"error\": \"Not found\""));
58+
assert!(formatted.contains("\"code\": 404"));
59+
60+
// Test truncation with large body
61+
let large_json = json!({
62+
"data": "x".repeat(10000),
63+
"message": "This is a large response"
64+
});
65+
let truncated = runner.format_response_body(&large_json);
66+
assert!(truncated.contains("... (truncated)"));
67+
assert!(truncated.len() <= TestRunner::MAX_SUMMARY_BODY_BYTES + 20); // Allow for truncation message
68+
}
69+
70+
#[tokio::test]
71+
async fn test_no_fail_summary_flag() {
72+
let mut runner = TestRunner::new(false);
73+
runner.no_fail_summary = true; // failure summary disabled
74+
75+
// Add a failing test result
76+
runner.results.push(TestResult {
77+
name: "Failing Test".to_string(),
78+
success: false,
79+
expected_status: 200,
80+
actual_status: 500,
81+
response_body: Some(json!({"error": "Internal server error"})),
82+
headers: HashMap::new(),
83+
messages: vec!["Server error occurred".to_string()],
84+
method: "GET".to_string(),
85+
endpoint: "/api/status".to_string(),
86+
});
87+
88+
// When no_fail_summary is true, failure details should not be displayed
89+
// This is tested indirectly by checking the flag value
90+
assert!(runner.no_fail_summary);
91+
}
92+
93+
#[tokio::test]
94+
async fn test_body_truncation_logic() {
95+
let runner = TestRunner::new(false);
96+
97+
// Test normal sized body
98+
let normal_body = json!({"message": "Hello world"});
99+
let formatted_normal = runner.format_response_body(&normal_body);
100+
assert!(!formatted_normal.contains("... (truncated)"));
101+
102+
// Test large body that exceeds MAX_SUMMARY_BODY_BYTES
103+
let large_data = "x".repeat(TestRunner::MAX_SUMMARY_BODY_BYTES + 1000);
104+
let large_body = json!({"data": large_data});
105+
let formatted_large = runner.format_response_body(&large_body);
106+
assert!(formatted_large.contains("... (truncated)"));
107+
assert!(formatted_large.len() <= TestRunner::MAX_SUMMARY_BODY_BYTES + 50);
108+
109+
// Test non-JSON body (string)
110+
let string_body = json!("This is just a string response");
111+
let formatted_string = runner.format_response_body(&string_body);
112+
assert_eq!(formatted_string, "\"This is just a string response\"");
113+
}
114+
115+
#[tokio::test]
116+
async fn test_test_result_structure() {
117+
// Test that TestResult properly captures method and endpoint
118+
let result = TestResult {
119+
name: "Test API call".to_string(),
120+
success: true,
121+
expected_status: 200,
122+
actual_status: 200,
123+
response_body: Some(json!({"status": "ok"})),
124+
headers: HashMap::new(),
125+
messages: vec![],
126+
method: "GET".to_string(),
127+
endpoint: "/api/status".to_string(),
128+
};
129+
130+
assert_eq!(result.name, "Test API call");
131+
assert!(result.success);
132+
assert_eq!(result.expected_status, 200);
133+
assert_eq!(result.actual_status, 200);
134+
assert_eq!(result.method, "GET");
135+
assert_eq!(result.endpoint, "/api/status");
136+
assert!(result.response_body.is_some());
137+
}

0 commit comments

Comments
 (0)