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}