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 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 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
155fn 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
168pub 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
175pub 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
194pub 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('&', "&")
270 .replace('<', "<")
271 .replace('>', ">")
272 .replace('"', """)
273 .replace('\'', "'")
274}
275
276pub 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}