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 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 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
156fn 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
169pub 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
176pub 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
195pub 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
269pub 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}