Skip to main content

testx/output/
mod.rs

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