Skip to main content

windjammer_runtime/
test_output.rs

1//! Enhanced test output utilities
2//!
3//! Provides better formatting and diagnostics for test results.
4
5use std::fmt;
6use std::time::Duration;
7
8/// Test result status
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum TestStatus {
11    Passed,
12    Failed,
13    Ignored,
14}
15
16/// Individual test result
17#[derive(Debug, Clone)]
18pub struct TestResult {
19    pub name: String,
20    pub status: TestStatus,
21    pub duration: Duration,
22    pub error: Option<String>,
23}
24
25impl TestResult {
26    pub fn new(name: String, status: TestStatus, duration: Duration) -> Self {
27        Self {
28            name,
29            status,
30            duration,
31            error: None,
32        }
33    }
34
35    pub fn with_error(mut self, error: String) -> Self {
36        self.error = Some(error);
37        self
38    }
39}
40
41/// Test suite summary
42#[derive(Debug, Clone)]
43pub struct TestSummary {
44    pub results: Vec<TestResult>,
45    pub total_duration: Duration,
46}
47
48impl TestSummary {
49    pub fn new() -> Self {
50        Self {
51            results: Vec::new(),
52            total_duration: Duration::ZERO,
53        }
54    }
55
56    pub fn add_result(&mut self, result: TestResult) {
57        self.total_duration += result.duration;
58        self.results.push(result);
59    }
60
61    pub fn passed_count(&self) -> usize {
62        self.results
63            .iter()
64            .filter(|r| r.status == TestStatus::Passed)
65            .count()
66    }
67
68    pub fn failed_count(&self) -> usize {
69        self.results
70            .iter()
71            .filter(|r| r.status == TestStatus::Failed)
72            .count()
73    }
74
75    pub fn ignored_count(&self) -> usize {
76        self.results
77            .iter()
78            .filter(|r| r.status == TestStatus::Ignored)
79            .count()
80    }
81
82    pub fn total_count(&self) -> usize {
83        self.results.len()
84    }
85
86    /// Format test results in standard format
87    pub fn format_standard(&self) -> String {
88        let mut output = String::new();
89        output.push_str(&format!("Running {} tests...\n\n", self.total_count()));
90
91        for result in &self.results {
92            let symbol = match result.status {
93                TestStatus::Passed => "✓",
94                TestStatus::Failed => "✗",
95                TestStatus::Ignored => "⊘",
96            };
97
98            output.push_str(&format!(
99                "{} {} ({:?})\n",
100                symbol, result.name, result.duration
101            ));
102
103            if let Some(error) = &result.error {
104                output.push_str(&format!("  {}\n\n", error));
105            }
106        }
107
108        output.push_str("\nTest Results:\n");
109        output.push_str(&format!(
110            "  Passed: {}/{} ({:.1}%)\n",
111            self.passed_count(),
112            self.total_count(),
113            (self.passed_count() as f64 / self.total_count() as f64) * 100.0
114        ));
115        output.push_str(&format!("  Failed: {}\n", self.failed_count()));
116        output.push_str(&format!("  Ignored: {}\n", self.ignored_count()));
117        output.push_str(&format!("  Total time: {:?}\n", self.total_duration));
118
119        output
120    }
121
122    /// Format test results in verbose format
123    pub fn format_verbose(&self) -> String {
124        let mut output = String::new();
125        output.push_str(&format!("Running {} tests...\n\n", self.total_count()));
126
127        for result in &self.results {
128            let status = match result.status {
129                TestStatus::Passed => "[PASS]",
130                TestStatus::Failed => "[FAIL]",
131                TestStatus::Ignored => "[SKIP]",
132            };
133
134            output.push_str(&format!(
135                "{} {} ({:?})\n",
136                status, result.name, result.duration
137            ));
138
139            if let Some(error) = &result.error {
140                output.push_str(&format!("  ↳ {}\n", error));
141            }
142
143            output.push('\n');
144        }
145
146        output.push_str("────────────────────────────────────────\n");
147        output.push_str(&format!("Total: {} tests\n", self.total_count()));
148        output.push_str(&format!(
149            "Passed: {} ({:.1}%)\n",
150            self.passed_count(),
151            (self.passed_count() as f64 / self.total_count() as f64) * 100.0
152        ));
153        output.push_str(&format!("Failed: {}\n", self.failed_count()));
154        output.push_str(&format!("Ignored: {}\n", self.ignored_count()));
155        output.push_str(&format!("Duration: {:?}\n", self.total_duration));
156
157        output
158    }
159}
160
161impl Default for TestSummary {
162    fn default() -> Self {
163        Self::new()
164    }
165}
166
167impl fmt::Display for TestSummary {
168    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
169        write!(f, "{}", self.format_standard())
170    }
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176
177    #[test]
178    fn test_result_creation() {
179        let result = TestResult::new(
180            "test_foo".to_string(),
181            TestStatus::Passed,
182            Duration::from_millis(10),
183        );
184
185        assert_eq!(result.name, "test_foo");
186        assert_eq!(result.status, TestStatus::Passed);
187        assert_eq!(result.duration, Duration::from_millis(10));
188        assert!(result.error.is_none());
189    }
190
191    #[test]
192    fn test_result_with_error() {
193        let result = TestResult::new(
194            "test_foo".to_string(),
195            TestStatus::Failed,
196            Duration::from_millis(10),
197        )
198        .with_error("assertion failed".to_string());
199
200        assert!(result.error.is_some());
201        assert_eq!(result.error.unwrap(), "assertion failed");
202    }
203
204    #[test]
205    fn test_summary_counts() {
206        let mut summary = TestSummary::new();
207
208        summary.add_result(TestResult::new(
209            "test1".to_string(),
210            TestStatus::Passed,
211            Duration::from_millis(5),
212        ));
213        summary.add_result(TestResult::new(
214            "test2".to_string(),
215            TestStatus::Failed,
216            Duration::from_millis(10),
217        ));
218        summary.add_result(TestResult::new(
219            "test3".to_string(),
220            TestStatus::Ignored,
221            Duration::from_millis(0),
222        ));
223
224        assert_eq!(summary.total_count(), 3);
225        assert_eq!(summary.passed_count(), 1);
226        assert_eq!(summary.failed_count(), 1);
227        assert_eq!(summary.ignored_count(), 1);
228        assert_eq!(summary.total_duration, Duration::from_millis(15));
229    }
230
231    #[test]
232    fn test_format_standard() {
233        let mut summary = TestSummary::new();
234        summary.add_result(TestResult::new(
235            "test_foo".to_string(),
236            TestStatus::Passed,
237            Duration::from_millis(5),
238        ));
239        summary.add_result(
240            TestResult::new(
241                "test_bar".to_string(),
242                TestStatus::Failed,
243                Duration::from_millis(10),
244            )
245            .with_error("assertion failed".to_string()),
246        );
247
248        let output = summary.format_standard();
249        assert!(output.contains("✓ test_foo"));
250        assert!(output.contains("✗ test_bar"));
251        assert!(output.contains("assertion failed"));
252        assert!(output.contains("Passed: 1/2"));
253        assert!(output.contains("Failed: 1"));
254    }
255
256    #[test]
257    fn test_format_verbose() {
258        let mut summary = TestSummary::new();
259        summary.add_result(TestResult::new(
260            "test_foo".to_string(),
261            TestStatus::Passed,
262            Duration::from_millis(5),
263        ));
264
265        let output = summary.format_verbose();
266        assert!(output.contains("[PASS] test_foo"));
267        assert!(output.contains("Total: 1 tests"));
268    }
269}