Skip to main content

probador/
output.rs

1//! Output formatting and progress reporting
2
3use console::{style, Style, Term};
4use indicatif::{ProgressBar, ProgressStyle};
5use serde::{Deserialize, Serialize};
6use std::time::Duration;
7
8/// Output format for test results
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
10pub enum OutputFormat {
11    /// Human-readable text
12    #[default]
13    Text,
14    /// JSON output
15    Json,
16    /// TAP (Test Anything Protocol)
17    Tap,
18}
19
20/// Progress reporter for test execution
21#[derive(Debug)]
22pub struct ProgressReporter {
23    term: Term,
24    progress_bar: Option<ProgressBar>,
25    /// Whether to use colors
26    pub use_color: bool,
27    /// Quiet mode
28    pub quiet: bool,
29}
30
31impl Default for ProgressReporter {
32    fn default() -> Self {
33        Self::new(true, false)
34    }
35}
36
37impl ProgressReporter {
38    /// Create a new progress reporter
39    #[must_use]
40    pub fn new(use_color: bool, quiet: bool) -> Self {
41        Self {
42            term: Term::stderr(),
43            progress_bar: None,
44            use_color,
45            quiet,
46        }
47    }
48
49    /// Start a progress bar for multiple tests
50    pub fn start_progress(&mut self, total: u64, message: &str) {
51        if self.quiet {
52            return;
53        }
54
55        let pb = ProgressBar::new(total);
56        pb.set_style(
57            ProgressStyle::default_bar()
58                .template("{spinner:.green} [{bar:40.cyan/blue}] {pos}/{len} {msg}")
59                .unwrap_or_else(|_| ProgressStyle::default_bar())
60                .progress_chars("=>-"),
61        );
62        pb.set_message(message.to_string());
63        self.progress_bar = Some(pb);
64    }
65
66    /// Increment progress
67    pub fn increment(&self, delta: u64) {
68        if let Some(ref pb) = self.progress_bar {
69            pb.inc(delta);
70        }
71    }
72
73    /// Update progress message
74    pub fn set_message(&self, message: &str) {
75        if let Some(ref pb) = self.progress_bar {
76            pb.set_message(message.to_string());
77        }
78    }
79
80    /// Finish progress bar
81    pub fn finish(&self) {
82        if let Some(ref pb) = self.progress_bar {
83            pb.finish_with_message("Done");
84        }
85    }
86
87    /// Print a success message
88    pub fn success(&self, message: &str) {
89        if self.quiet {
90            return;
91        }
92
93        let prefix = if self.use_color {
94            style("✓").green().bold().to_string()
95        } else {
96            "PASS".to_string()
97        };
98
99        let _ = self.term.write_line(&format!("{prefix} {message}"));
100    }
101
102    /// Print a failure message
103    pub fn failure(&self, message: &str) {
104        // Always print failures, even in quiet mode
105        let prefix = if self.use_color {
106            style("✗").red().bold().to_string()
107        } else {
108            "FAIL".to_string()
109        };
110
111        let _ = self.term.write_line(&format!("{prefix} {message}"));
112    }
113
114    /// Print a warning message
115    pub fn warning(&self, message: &str) {
116        if self.quiet {
117            return;
118        }
119
120        let prefix = if self.use_color {
121            style("⚠").yellow().bold().to_string()
122        } else {
123            "WARN".to_string()
124        };
125
126        let _ = self.term.write_line(&format!("{prefix} {message}"));
127    }
128
129    /// Print an info message
130    pub fn info(&self, message: &str) {
131        if self.quiet {
132            return;
133        }
134
135        let prefix = if self.use_color {
136            style("ℹ").blue().bold().to_string()
137        } else {
138            "INFO".to_string()
139        };
140
141        let _ = self.term.write_line(&format!("{prefix} {message}"));
142    }
143
144    /// Print a section header
145    pub fn header(&self, title: &str) {
146        if self.quiet {
147            return;
148        }
149
150        let styled = if self.use_color {
151            style(title).bold().underlined().to_string()
152        } else {
153            format!("=== {title} ===")
154        };
155
156        let _ = self.term.write_line("");
157        let _ = self.term.write_line(&styled);
158    }
159
160    /// Print test summary
161    pub fn summary(&self, passed: usize, failed: usize, skipped: usize, duration: Duration) {
162        if self.quiet && failed == 0 {
163            return;
164        }
165
166        let _ = self.term.write_line("");
167
168        let total = passed + failed + skipped;
169        let duration_secs = duration.as_secs_f64();
170
171        if self.use_color {
172            let passed_style = Style::new().green().bold();
173            let failed_style = Style::new().red().bold();
174            let skipped_style = Style::new().yellow();
175
176            let status = if failed > 0 {
177                failed_style.apply_to("FAILED")
178            } else {
179                passed_style.apply_to("PASSED")
180            };
181
182            let _ = self.term.write_line(&format!(
183                "{} {} tests in {:.2}s ({} passed, {} failed, {} skipped)",
184                status,
185                total,
186                duration_secs,
187                passed_style.apply_to(passed),
188                if failed > 0 {
189                    failed_style.apply_to(failed).to_string()
190                } else {
191                    failed.to_string()
192                },
193                skipped_style.apply_to(skipped)
194            ));
195        } else {
196            let status = if failed > 0 { "FAILED" } else { "PASSED" };
197            let _ = self.term.write_line(&format!(
198                "{status} {total} tests in {duration_secs:.2}s ({passed} passed, {failed} failed, {skipped} skipped)"
199            ));
200        }
201    }
202}
203
204#[cfg(test)]
205#[allow(clippy::unwrap_used, clippy::expect_used)]
206mod tests {
207    use super::*;
208
209    mod output_format_tests {
210        use super::*;
211
212        #[test]
213        fn test_default_format() {
214            let format = OutputFormat::default();
215            assert_eq!(format, OutputFormat::Text);
216        }
217
218        #[test]
219        fn test_format_variants() {
220            let _ = OutputFormat::Text;
221            let _ = OutputFormat::Json;
222            let _ = OutputFormat::Tap;
223        }
224    }
225
226    mod progress_reporter_tests {
227        use super::*;
228
229        #[test]
230        fn test_new_reporter() {
231            let reporter = ProgressReporter::new(true, false);
232            assert!(reporter.use_color);
233            assert!(!reporter.quiet);
234        }
235
236        #[test]
237        fn test_default_reporter() {
238            let reporter = ProgressReporter::default();
239            assert!(reporter.use_color);
240            assert!(!reporter.quiet);
241        }
242
243        #[test]
244        fn test_quiet_reporter() {
245            let reporter = ProgressReporter::new(false, true);
246            assert!(reporter.quiet);
247        }
248
249        #[test]
250        fn test_success_message() {
251            let reporter = ProgressReporter::new(false, false);
252            reporter.success("Test passed");
253            // No panic = success
254        }
255
256        #[test]
257        fn test_failure_message() {
258            let reporter = ProgressReporter::new(false, false);
259            reporter.failure("Test failed");
260            // No panic = success
261        }
262
263        #[test]
264        fn test_warning_message() {
265            let reporter = ProgressReporter::new(false, false);
266            reporter.warning("Test warning");
267            // No panic = success
268        }
269
270        #[test]
271        fn test_info_message() {
272            let reporter = ProgressReporter::new(false, false);
273            reporter.info("Test info");
274            // No panic = success
275        }
276
277        #[test]
278        fn test_header() {
279            let reporter = ProgressReporter::new(false, false);
280            reporter.header("Test Header");
281            // No panic = success
282        }
283
284        #[test]
285        fn test_summary_passed() {
286            let reporter = ProgressReporter::new(false, false);
287            reporter.summary(10, 0, 2, Duration::from_secs(5));
288            // No panic = success
289        }
290
291        #[test]
292        fn test_summary_failed() {
293            let reporter = ProgressReporter::new(false, false);
294            reporter.summary(8, 2, 0, Duration::from_secs(3));
295            // No panic = success
296        }
297
298        #[test]
299        fn test_progress_bar() {
300            let mut reporter = ProgressReporter::new(false, false);
301            reporter.start_progress(10, "Running tests");
302            reporter.increment(1);
303            reporter.set_message("test_1");
304            reporter.increment(1);
305            reporter.finish();
306            // No panic = success
307        }
308
309        #[test]
310        fn test_quiet_mode_suppresses_output() {
311            let mut reporter = ProgressReporter::new(false, true);
312            reporter.start_progress(10, "Running tests");
313            reporter.success("hidden");
314            reporter.warning("hidden");
315            reporter.info("hidden");
316            reporter.header("hidden");
317            // Failure is still printed
318            reporter.failure("shown");
319            // No panic = success
320        }
321
322        #[test]
323        fn test_color_mode_messages() {
324            let reporter = ProgressReporter::new(true, false);
325            reporter.success("Pass with color");
326            reporter.failure("Fail with color");
327            reporter.warning("Warn with color");
328            reporter.info("Info with color");
329            reporter.header("Header with color");
330        }
331
332        #[test]
333        fn test_summary_all_skipped() {
334            let reporter = ProgressReporter::new(false, false);
335            reporter.summary(0, 0, 5, Duration::from_secs(1));
336        }
337
338        #[test]
339        fn test_summary_mixed() {
340            let reporter = ProgressReporter::new(true, false);
341            reporter.summary(5, 3, 2, Duration::from_millis(500));
342        }
343
344        #[test]
345        fn test_progress_without_start() {
346            let reporter = ProgressReporter::new(false, false);
347            // These should not panic even without start_progress
348            reporter.increment(1);
349            reporter.set_message("test");
350            reporter.finish();
351        }
352
353        #[test]
354        fn test_debug() {
355            let reporter = ProgressReporter::new(true, false);
356            let debug = format!("{reporter:?}");
357            assert!(debug.contains("ProgressReporter"));
358        }
359    }
360
361    mod output_format_additional_tests {
362        use super::*;
363
364        #[test]
365        fn test_clone() {
366            let format = OutputFormat::Json;
367            let cloned = format;
368            assert_eq!(format, cloned);
369        }
370
371        #[test]
372        fn test_debug() {
373            let debug = format!("{:?}", OutputFormat::Text);
374            assert!(debug.contains("Text"));
375        }
376
377        #[test]
378        fn test_serialize() {
379            let format = OutputFormat::Json;
380            let json = serde_json::to_string(&format).unwrap();
381            assert!(json.contains("Json"));
382        }
383    }
384}