clnrm_core/formatting/
human.rs

1//! Human-Readable Formatter
2//!
3//! Generates colored terminal output for test results.
4//! Default formatter for interactive terminal use.
5
6use crate::error::Result;
7use crate::formatting::formatter::{Formatter, FormatterType};
8use crate::formatting::test_result::{TestStatus, TestSuite};
9
10/// Human-readable formatter with ANSI color support
11#[derive(Debug, Default)]
12pub struct HumanFormatter {
13    /// Whether to use colors in output
14    use_colors: bool,
15}
16
17impl HumanFormatter {
18    /// Create a new human formatter with color support
19    pub fn new() -> Self {
20        Self::with_colors(true)
21    }
22
23    /// Create a new human formatter with optional color support
24    pub fn with_colors(use_colors: bool) -> Self {
25        Self { use_colors }
26    }
27
28    /// Format a status indicator
29    fn format_status(&self, status: &TestStatus) -> String {
30        let (symbol, color) = match status {
31            TestStatus::Passed => ("✓", "\x1b[32m"),  // Green
32            TestStatus::Failed => ("✗", "\x1b[31m"),  // Red
33            TestStatus::Skipped => ("⊘", "\x1b[33m"), // Yellow
34            TestStatus::Unknown => ("?", "\x1b[90m"), // Gray
35        };
36
37        if self.use_colors {
38            format!("{}{}\x1b[0m", color, symbol)
39        } else {
40            symbol.to_string()
41        }
42    }
43
44    /// Format a test name with color
45    fn format_test_name(&self, name: &str, passed: bool) -> String {
46        if self.use_colors {
47            if passed {
48                format!("\x1b[32m{}\x1b[0m", name)
49            } else {
50                format!("\x1b[31m{}\x1b[0m", name)
51            }
52        } else {
53            name.to_string()
54        }
55    }
56
57    /// Format duration in milliseconds
58    fn format_duration(&self, duration_ms: f64) -> String {
59        if duration_ms < 1.0 {
60            "<1ms".to_string()
61        } else if duration_ms < 1000.0 {
62            format!("{}ms", duration_ms as u64)
63        } else {
64            format!("{:.2}s", duration_ms / 1000.0)
65        }
66    }
67
68    /// Format summary line
69    fn format_summary(&self, suite: &TestSuite) -> String {
70        let total = suite.total_count();
71        let passed = suite.passed_count();
72        let failed = suite.failed_count();
73        let skipped = suite.skipped_count();
74
75        let status_text = if suite.is_success() {
76            if self.use_colors {
77                "\x1b[32mPASSED\x1b[0m"
78            } else {
79                "PASSED"
80            }
81        } else if self.use_colors {
82            "\x1b[31mFAILED\x1b[0m"
83        } else {
84            "FAILED"
85        };
86
87        let mut parts = vec![format!("{} tests", total), format!("{} passed", passed)];
88
89        if failed > 0 {
90            parts.push(format!("{} failed", failed));
91        }
92
93        if skipped > 0 {
94            parts.push(format!("{} skipped", skipped));
95        }
96
97        format!("{}: {}", status_text, parts.join(", "))
98    }
99}
100
101impl Formatter for HumanFormatter {
102    fn format(&self, suite: &TestSuite) -> Result<String> {
103        let mut output = Vec::new();
104
105        // Header
106        output.push(format!("Test Suite: {}", suite.name));
107        output.push(String::from(""));
108
109        // Individual test results
110        for result in &suite.results {
111            let status = self.format_status(&result.status);
112            let name = self.format_test_name(&result.name, result.is_passed());
113
114            let mut line = format!("  {} {}", status, name);
115
116            // Add duration if available
117            if let Some(duration) = result.duration {
118                let duration_str = self.format_duration(duration.as_secs_f64() * 1000.0);
119                line.push_str(&format!(" ({})", duration_str));
120            }
121
122            output.push(line);
123
124            // Add error message for failures
125            if let Some(error) = &result.error {
126                let error_lines: Vec<&str> = error.lines().collect();
127                for error_line in error_lines {
128                    output.push(format!("      {}", error_line));
129                }
130            }
131
132            // Add stdout if present
133            if let Some(stdout) = &result.stdout {
134                output.push(String::from("      stdout:"));
135                for stdout_line in stdout.lines() {
136                    output.push(format!("        {}", stdout_line));
137                }
138            }
139
140            // Add stderr if present
141            if let Some(stderr) = &result.stderr {
142                output.push(String::from("      stderr:"));
143                for stderr_line in stderr.lines() {
144                    output.push(format!("        {}", stderr_line));
145                }
146            }
147        }
148
149        // Summary
150        output.push(String::from(""));
151        output.push("─".repeat(60));
152        output.push(self.format_summary(suite));
153
154        // Duration
155        if let Some(duration) = suite.duration {
156            let duration_str = self.format_duration(duration.as_secs_f64() * 1000.0);
157            output.push(format!("Duration: {}", duration_str));
158        }
159
160        output.push(String::from(""));
161
162        Ok(output.join("\n"))
163    }
164
165    fn name(&self) -> &'static str {
166        "human"
167    }
168
169    fn formatter_type(&self) -> FormatterType {
170        FormatterType::Human
171    }
172}
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177    use crate::formatting::test_result::TestResult;
178    use std::time::Duration;
179
180    #[test]
181    fn test_human_formatter_empty_suite() -> Result<()> {
182        // Arrange
183        let formatter = HumanFormatter::with_colors(false);
184        let suite = TestSuite::new("empty_suite");
185
186        // Act
187        let output = formatter.format(&suite)?;
188
189        // Assert
190        assert!(output.contains("Test Suite: empty_suite"));
191        // Empty suite is NOT considered success (requires at least 1 test)
192        assert!(output.contains("FAILED") || output.contains("0 tests"));
193        assert!(output.contains("0 tests"));
194
195        Ok(())
196    }
197
198    #[test]
199    fn test_human_formatter_all_passed() -> Result<()> {
200        // Arrange
201        let formatter = HumanFormatter::with_colors(false);
202        let suite = TestSuite::new("passing_suite")
203            .add_result(TestResult::passed("test1"))
204            .add_result(TestResult::passed("test2"));
205
206        // Act
207        let output = formatter.format(&suite)?;
208
209        // Assert
210        assert!(output.contains("Test Suite: passing_suite"));
211        assert!(output.contains("✓ test1"));
212        assert!(output.contains("✓ test2"));
213        assert!(output.contains("PASSED"));
214        assert!(output.contains("2 tests"));
215        assert!(output.contains("2 passed"));
216
217        Ok(())
218    }
219
220    #[test]
221    fn test_human_formatter_with_failures() -> Result<()> {
222        // Arrange
223        let formatter = HumanFormatter::with_colors(false);
224        let suite = TestSuite::new("failing_suite")
225            .add_result(TestResult::passed("test1"))
226            .add_result(TestResult::failed(
227                "test2",
228                "assertion failed: expected 2, got 1",
229            ));
230
231        // Act
232        let output = formatter.format(&suite)?;
233
234        // Assert
235        assert!(output.contains("Test Suite: failing_suite"));
236        assert!(output.contains("✓ test1"));
237        assert!(output.contains("✗ test2"));
238        assert!(output.contains("assertion failed: expected 2, got 1"));
239        assert!(output.contains("FAILED"));
240        assert!(output.contains("2 tests"));
241        assert!(output.contains("1 passed"));
242        assert!(output.contains("1 failed"));
243
244        Ok(())
245    }
246
247    #[test]
248    fn test_human_formatter_with_skipped() -> Result<()> {
249        // Arrange
250        let formatter = HumanFormatter::with_colors(false);
251        let suite = TestSuite::new("suite_with_skipped")
252            .add_result(TestResult::passed("test1"))
253            .add_result(TestResult::skipped("test2"));
254
255        // Act
256        let output = formatter.format(&suite)?;
257
258        // Assert
259        assert!(output.contains("✓ test1"));
260        assert!(output.contains("⊘ test2"));
261        assert!(output.contains("1 passed"));
262        assert!(output.contains("1 skipped"));
263
264        Ok(())
265    }
266
267    #[test]
268    fn test_human_formatter_with_duration() -> Result<()> {
269        // Arrange
270        let formatter = HumanFormatter::with_colors(false);
271        let suite = TestSuite::new("suite_with_duration")
272            .add_result(TestResult::passed("fast_test").with_duration(Duration::from_millis(50)))
273            .add_result(TestResult::passed("slow_test").with_duration(Duration::from_millis(1500)));
274
275        // Act
276        let output = formatter.format(&suite)?;
277
278        // Assert
279        assert!(output.contains("fast_test (50ms)"));
280        assert!(output.contains("slow_test (1.50s)"));
281
282        Ok(())
283    }
284
285    #[test]
286    fn test_human_formatter_with_stdout_stderr() -> Result<()> {
287        // Arrange
288        let formatter = HumanFormatter::with_colors(false);
289        let suite = TestSuite::new("suite_with_output").add_result(
290            TestResult::passed("test_with_output")
291                .with_stdout("stdout line 1\nstdout line 2")
292                .with_stderr("stderr line 1"),
293        );
294
295        // Act
296        let output = formatter.format(&suite)?;
297
298        // Assert
299        assert!(output.contains("stdout:"));
300        assert!(output.contains("stdout line 1"));
301        assert!(output.contains("stdout line 2"));
302        assert!(output.contains("stderr:"));
303        assert!(output.contains("stderr line 1"));
304
305        Ok(())
306    }
307
308    #[test]
309    fn test_human_formatter_name() {
310        // Arrange
311        let formatter = HumanFormatter::new();
312
313        // Act & Assert
314        assert_eq!(formatter.name(), "human");
315    }
316
317    #[test]
318    fn test_human_formatter_type() {
319        // Arrange
320        let formatter = HumanFormatter::new();
321
322        // Act & Assert
323        assert_eq!(formatter.formatter_type(), FormatterType::Human);
324    }
325
326    #[test]
327    fn test_format_duration_less_than_1ms() {
328        // Arrange
329        let formatter = HumanFormatter::new();
330
331        // Act
332        let result = formatter.format_duration(0.5);
333
334        // Assert
335        assert_eq!(result, "<1ms");
336    }
337
338    #[test]
339    fn test_format_duration_milliseconds() {
340        // Arrange
341        let formatter = HumanFormatter::new();
342
343        // Act
344        let result = formatter.format_duration(150.0);
345
346        // Assert
347        assert_eq!(result, "150ms");
348    }
349
350    #[test]
351    fn test_format_duration_seconds() {
352        // Arrange
353        let formatter = HumanFormatter::new();
354
355        // Act
356        let result = formatter.format_duration(1500.0);
357
358        // Assert
359        assert_eq!(result, "1.50s");
360    }
361}