Skip to main content

testx/output/
mod.rs

1use std::io::Write;
2use std::time::Duration;
3
4use colored::Colorize;
5
6use crate::adapters::util::xml_escape;
7use crate::adapters::{TestRunResult, TestStatus};
8use crate::detection::DetectedProject;
9
10pub fn print_detection(detected: &DetectedProject) {
11    println!(
12        "  {} {} ({}) [confidence: {:.0}%]",
13        "▸".bold(),
14        detected.detection.language.bold(),
15        detected.detection.framework.dimmed(),
16        detected.detection.confidence * 100.0,
17    );
18}
19
20pub fn print_header(adapter_name: &str, detected: &DetectedProject) {
21    println!();
22    println!(
23        "{} {} {}",
24        "testx".bold().cyan(),
25        "·".dimmed(),
26        format!("{} ({})", adapter_name, detected.detection.framework).white(),
27    );
28    println!("{}", "─".repeat(60).dimmed());
29}
30
31pub fn print_results(result: &TestRunResult) {
32    for suite in &result.suites {
33        println!();
34        let suite_icon = if suite.is_passed() {
35            "✓".green()
36        } else {
37            "✗".red()
38        };
39        println!("  {} {}", suite_icon, suite.name.bold().underline());
40
41        for test in &suite.tests {
42            let (icon, name_colored) = match test.status {
43                TestStatus::Passed => ("✓".green(), test.name.green()),
44                TestStatus::Failed => ("✗".red(), test.name.red()),
45                TestStatus::Skipped => ("○".yellow(), test.name.yellow()),
46            };
47
48            let duration_str = format_duration(test.duration);
49            if test.duration.as_millis() > 0 {
50                println!("    {} {} {}", icon, name_colored, duration_str.dimmed());
51            } else {
52                println!("    {} {}", icon, name_colored);
53            }
54
55            // Print error details if present
56            if let Some(err) = &test.error {
57                println!("      {} {}", "→".red(), err.message.red());
58                if let Some(loc) = &err.location {
59                    println!("        {}", loc.dimmed());
60                }
61            }
62        }
63    }
64
65    println!();
66    println!("{}", "─".repeat(60).dimmed());
67
68    // Print failure summary if there are failures
69    if !result.is_success() {
70        print_failure_summary(result);
71    }
72
73    print_summary(result);
74}
75
76fn print_failure_summary(result: &TestRunResult) {
77    let mut has_failures = false;
78    for suite in &result.suites {
79        let failures = suite.failures();
80        if failures.is_empty() {
81            continue;
82        }
83        if !has_failures {
84            println!("  {} {}", "✗".red().bold(), "Failures:".red().bold());
85            has_failures = true;
86        }
87        for tc in failures {
88            println!(
89                "    {} {} :: {}",
90                "→".red(),
91                suite.name.dimmed(),
92                tc.name.red()
93            );
94            if let Some(err) = &tc.error {
95                println!("      {}", err.message.dimmed());
96            }
97        }
98    }
99    if has_failures {
100        println!();
101    }
102}
103
104fn print_summary(result: &TestRunResult) {
105    let total = result.total_tests();
106    let passed = result.total_passed();
107    let failed = result.total_failed();
108    let skipped = result.total_skipped();
109
110    let status_line = if result.is_success() {
111        "PASS".green().bold()
112    } else {
113        "FAIL".red().bold()
114    };
115
116    let mut parts = Vec::new();
117    if passed > 0 {
118        parts.push(format!("{} passed", passed).green().to_string());
119    }
120    if failed > 0 {
121        parts.push(format!("{} failed", failed).red().to_string());
122    }
123    if skipped > 0 {
124        parts.push(format!("{} skipped", skipped).yellow().to_string());
125    }
126
127    println!(
128        "  {} {} ({} total) in {}",
129        status_line,
130        parts.join(", "),
131        total,
132        format_duration(result.duration),
133    );
134    println!();
135}
136
137pub fn print_slowest_tests(result: &TestRunResult, count: usize) {
138    let slowest = result.slowest_tests(count);
139    if slowest.is_empty() || slowest.iter().all(|(_, tc)| tc.duration.as_millis() == 0) {
140        return;
141    }
142
143    println!("  {} {}", "⏱".dimmed(), "Slowest tests:".dimmed());
144    for (_suite, tc) in slowest {
145        if tc.duration.as_millis() > 0 {
146            println!(
147                "    {} {}",
148                format_duration(tc.duration).yellow(),
149                tc.name.dimmed(),
150            );
151        }
152    }
153    println!();
154}
155
156/// Format a Duration as human-readable
157fn format_duration(d: Duration) -> String {
158    let ms = d.as_millis();
159    if ms == 0 {
160        return String::new();
161    }
162    if ms < 1000 {
163        format!("{}ms", ms)
164    } else {
165        format!("{:.2}s", d.as_secs_f64())
166    }
167}
168
169/// Print results as JSON to stdout
170pub fn print_json(result: &TestRunResult) {
171    let json = serde_json::to_string_pretty(result).unwrap_or_else(|_| "{}".into());
172    let mut stdout = std::io::stdout().lock();
173    let _ = writeln!(stdout, "{}", json);
174}
175
176/// Print raw output from the test runner (useful for debugging failures)
177pub fn print_raw_output(stdout: &str, stderr: &str) {
178    let stdout = stdout.trim();
179    let stderr = stderr.trim();
180    if stdout.is_empty() && stderr.is_empty() {
181        return;
182    }
183    println!("  {} {}", "▾".dimmed(), "Raw output:".dimmed());
184    println!("{}", "─".repeat(60).dimmed());
185    if !stdout.is_empty() {
186        println!("{}", stdout);
187    }
188    if !stderr.is_empty() {
189        println!("{}", stderr);
190    }
191    println!("{}", "─".repeat(60).dimmed());
192    println!();
193}
194
195/// Print results as JUnit XML (compatible with CI tools)
196pub fn print_junit_xml(result: &TestRunResult) {
197    use std::io::Write;
198
199    let mut buf = String::new();
200    buf.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
201    buf.push_str(&format!(
202        "<testsuites tests=\"{}\" failures=\"{}\" time=\"{:.3}\">\n",
203        result.total_tests(),
204        result.total_failed(),
205        result.duration.as_secs_f64(),
206    ));
207
208    for suite in &result.suites {
209        buf.push_str(&format!(
210            "  <testsuite name=\"{}\" tests=\"{}\" failures=\"{}\" skipped=\"{}\">\n",
211            xml_escape(&suite.name),
212            suite.tests.len(),
213            suite.failed(),
214            suite.skipped(),
215        ));
216
217        for test in &suite.tests {
218            let time = format!("{:.3}", test.duration.as_secs_f64());
219            match test.status {
220                TestStatus::Passed => {
221                    buf.push_str(&format!(
222                        "    <testcase name=\"{}\" classname=\"{}\" time=\"{}\"/>\n",
223                        xml_escape(&test.name),
224                        xml_escape(&suite.name),
225                        time,
226                    ));
227                }
228                TestStatus::Failed => {
229                    buf.push_str(&format!(
230                        "    <testcase name=\"{}\" classname=\"{}\" time=\"{}\">\n",
231                        xml_escape(&test.name),
232                        xml_escape(&suite.name),
233                        time,
234                    ));
235                    let msg = test
236                        .error
237                        .as_ref()
238                        .map(|e| e.message.as_str())
239                        .unwrap_or("Test failed");
240                    buf.push_str(&format!(
241                        "      <failure message=\"{}\" type=\"AssertionError\">{}</failure>\n",
242                        xml_escape(msg),
243                        xml_escape(msg),
244                    ));
245                    buf.push_str("    </testcase>\n");
246                }
247                TestStatus::Skipped => {
248                    buf.push_str(&format!(
249                        "    <testcase name=\"{}\" classname=\"{}\" time=\"{}\">\n",
250                        xml_escape(&test.name),
251                        xml_escape(&suite.name),
252                        time,
253                    ));
254                    buf.push_str("      <skipped/>\n");
255                    buf.push_str("    </testcase>\n");
256                }
257            }
258        }
259
260        buf.push_str("  </testsuite>\n");
261    }
262
263    buf.push_str("</testsuites>\n");
264
265    let mut stdout = std::io::stdout().lock();
266    let _ = stdout.write_all(buf.as_bytes());
267}
268
269/// Print results in TAP (Test Anything Protocol) format
270pub fn print_tap(result: &TestRunResult) {
271    use std::io::Write;
272
273    let total = result.total_tests();
274    let mut stdout = std::io::stdout().lock();
275
276    macro_rules! tap_write {
277        ($($arg:tt)*) => {
278            if writeln!(stdout, $($arg)*).is_err() {
279                return;
280            }
281        }
282    }
283
284    tap_write!("TAP version 13");
285    tap_write!("1..{total}");
286
287    let mut n = 0;
288    for suite in &result.suites {
289        for test in &suite.tests {
290            n += 1;
291            let full_name = format!("{} - {}", suite.name, test.name);
292            match test.status {
293                TestStatus::Passed => {
294                    tap_write!("ok {n} {full_name}");
295                }
296                TestStatus::Failed => {
297                    tap_write!("not ok {n} {full_name}");
298                    if let Some(err) = &test.error {
299                        tap_write!("  ---");
300                        tap_write!("  message: {}", err.message);
301                        if let Some(loc) = &err.location {
302                            tap_write!("  at: {loc}");
303                        }
304                        tap_write!("  ...");
305                    }
306                }
307                TestStatus::Skipped => {
308                    tap_write!("ok {n} {full_name} # SKIP");
309                }
310            }
311        }
312    }
313}