clnrm_core/formatting/
test_result.rs

1//! Test Result Structures
2//!
3//! Core data structures for representing test execution results.
4//! Used by all formatters to generate output.
5
6use std::time::Duration;
7
8/// Status of a test execution
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub enum TestStatus {
11    /// Test passed successfully
12    Passed,
13    /// Test failed with error
14    Failed,
15    /// Test was skipped
16    Skipped,
17    /// Test status is unknown or pending
18    Unknown,
19}
20
21/// Individual test result
22#[derive(Debug, Clone)]
23pub struct TestResult {
24    /// Test name
25    pub name: String,
26    /// Test status
27    pub status: TestStatus,
28    /// Test duration
29    pub duration: Option<Duration>,
30    /// Error message (if failed)
31    pub error: Option<String>,
32    /// Standard output captured during test
33    pub stdout: Option<String>,
34    /// Standard error captured during test
35    pub stderr: Option<String>,
36    /// Test metadata
37    pub metadata: std::collections::HashMap<String, String>,
38}
39
40impl TestResult {
41    /// Create a new passed test result
42    pub fn passed(name: impl Into<String>) -> Self {
43        Self {
44            name: name.into(),
45            status: TestStatus::Passed,
46            duration: None,
47            error: None,
48            stdout: None,
49            stderr: None,
50            metadata: std::collections::HashMap::new(),
51        }
52    }
53
54    /// Create a new failed test result
55    pub fn failed(name: impl Into<String>, error: impl Into<String>) -> Self {
56        Self {
57            name: name.into(),
58            status: TestStatus::Failed,
59            duration: None,
60            error: Some(error.into()),
61            stdout: None,
62            stderr: None,
63            metadata: std::collections::HashMap::new(),
64        }
65    }
66
67    /// Create a new skipped test result
68    pub fn skipped(name: impl Into<String>) -> Self {
69        Self {
70            name: name.into(),
71            status: TestStatus::Skipped,
72            duration: None,
73            error: None,
74            stdout: None,
75            stderr: None,
76            metadata: std::collections::HashMap::new(),
77        }
78    }
79
80    /// Set test duration
81    pub fn with_duration(mut self, duration: Duration) -> Self {
82        self.duration = Some(duration);
83        self
84    }
85
86    /// Set stdout
87    pub fn with_stdout(mut self, stdout: impl Into<String>) -> Self {
88        self.stdout = Some(stdout.into());
89        self
90    }
91
92    /// Set stderr
93    pub fn with_stderr(mut self, stderr: impl Into<String>) -> Self {
94        self.stderr = Some(stderr.into());
95        self
96    }
97
98    /// Add metadata
99    pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
100        self.metadata.insert(key.into(), value.into());
101        self
102    }
103
104    /// Check if test passed
105    pub fn is_passed(&self) -> bool {
106        self.status == TestStatus::Passed
107    }
108
109    /// Check if test failed
110    pub fn is_failed(&self) -> bool {
111        self.status == TestStatus::Failed
112    }
113
114    /// Check if test was skipped
115    pub fn is_skipped(&self) -> bool {
116        self.status == TestStatus::Skipped
117    }
118}
119
120/// Test suite containing multiple test results
121#[derive(Debug, Clone)]
122pub struct TestSuite {
123    /// Suite name
124    pub name: String,
125    /// Test results
126    pub results: Vec<TestResult>,
127    /// Suite duration
128    pub duration: Option<Duration>,
129    /// Suite metadata
130    pub metadata: std::collections::HashMap<String, String>,
131}
132
133impl TestSuite {
134    /// Create a new empty test suite
135    pub fn new(name: impl Into<String>) -> Self {
136        Self {
137            name: name.into(),
138            results: Vec::new(),
139            duration: None,
140            metadata: std::collections::HashMap::new(),
141        }
142    }
143
144    /// Add a test result
145    pub fn add_result(mut self, result: TestResult) -> Self {
146        self.results.push(result);
147        self
148    }
149
150    /// Set suite duration
151    pub fn with_duration(mut self, duration: Duration) -> Self {
152        self.duration = Some(duration);
153        self
154    }
155
156    /// Add metadata
157    pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
158        self.metadata.insert(key.into(), value.into());
159        self
160    }
161
162    /// Get count of passed tests
163    pub fn passed_count(&self) -> usize {
164        self.results.iter().filter(|r| r.is_passed()).count()
165    }
166
167    /// Get count of failed tests
168    pub fn failed_count(&self) -> usize {
169        self.results.iter().filter(|r| r.is_failed()).count()
170    }
171
172    /// Get count of skipped tests
173    pub fn skipped_count(&self) -> usize {
174        self.results.iter().filter(|r| r.is_skipped()).count()
175    }
176
177    /// Get total test count
178    pub fn total_count(&self) -> usize {
179        self.results.len()
180    }
181
182    /// Check if all tests passed
183    pub fn is_success(&self) -> bool {
184        self.failed_count() == 0 && self.total_count() > 0
185    }
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191
192    #[test]
193    fn test_test_result_passed() {
194        // Arrange & Act
195        let result = TestResult::passed("test_name");
196
197        // Assert
198        assert_eq!(result.name, "test_name");
199        assert_eq!(result.status, TestStatus::Passed);
200        assert!(result.is_passed());
201        assert!(!result.is_failed());
202        assert!(!result.is_skipped());
203    }
204
205    #[test]
206    fn test_test_result_failed() {
207        // Arrange & Act
208        let result = TestResult::failed("test_name", "error message");
209
210        // Assert
211        assert_eq!(result.name, "test_name");
212        assert_eq!(result.status, TestStatus::Failed);
213        assert_eq!(result.error, Some("error message".to_string()));
214        assert!(!result.is_passed());
215        assert!(result.is_failed());
216        assert!(!result.is_skipped());
217    }
218
219    #[test]
220    fn test_test_result_skipped() {
221        // Arrange & Act
222        let result = TestResult::skipped("test_name");
223
224        // Assert
225        assert_eq!(result.name, "test_name");
226        assert_eq!(result.status, TestStatus::Skipped);
227        assert!(!result.is_passed());
228        assert!(!result.is_failed());
229        assert!(result.is_skipped());
230    }
231
232    #[test]
233    fn test_test_result_with_duration() {
234        // Arrange & Act
235        let result = TestResult::passed("test_name").with_duration(Duration::from_millis(100));
236
237        // Assert
238        assert_eq!(result.duration, Some(Duration::from_millis(100)));
239    }
240
241    #[test]
242    fn test_test_result_with_stdout() {
243        // Arrange & Act
244        let result = TestResult::passed("test_name").with_stdout("output");
245
246        // Assert
247        assert_eq!(result.stdout, Some("output".to_string()));
248    }
249
250    #[test]
251    fn test_test_result_with_metadata() {
252        // Arrange & Act
253        let result = TestResult::passed("test_name").with_metadata("key", "value");
254
255        // Assert
256        assert_eq!(result.metadata.get("key"), Some(&"value".to_string()));
257    }
258
259    #[test]
260    fn test_test_suite_new() {
261        // Arrange & Act
262        let suite = TestSuite::new("suite_name");
263
264        // Assert
265        assert_eq!(suite.name, "suite_name");
266        assert_eq!(suite.total_count(), 0);
267    }
268
269    #[test]
270    fn test_test_suite_add_result() {
271        // Arrange
272        let suite = TestSuite::new("suite_name");
273
274        // Act
275        let suite = suite.add_result(TestResult::passed("test1"));
276
277        // Assert
278        assert_eq!(suite.total_count(), 1);
279        assert_eq!(suite.passed_count(), 1);
280    }
281
282    #[test]
283    fn test_test_suite_counts() {
284        // Arrange
285        let suite = TestSuite::new("suite_name")
286            .add_result(TestResult::passed("test1"))
287            .add_result(TestResult::passed("test2"))
288            .add_result(TestResult::failed("test3", "error"))
289            .add_result(TestResult::skipped("test4"));
290
291        // Act & Assert
292        assert_eq!(suite.total_count(), 4);
293        assert_eq!(suite.passed_count(), 2);
294        assert_eq!(suite.failed_count(), 1);
295        assert_eq!(suite.skipped_count(), 1);
296    }
297
298    #[test]
299    fn test_test_suite_is_success_with_all_passed() {
300        // Arrange
301        let suite = TestSuite::new("suite_name")
302            .add_result(TestResult::passed("test1"))
303            .add_result(TestResult::passed("test2"));
304
305        // Act & Assert
306        assert!(suite.is_success());
307    }
308
309    #[test]
310    fn test_test_suite_is_success_with_failures() {
311        // Arrange
312        let suite = TestSuite::new("suite_name")
313            .add_result(TestResult::passed("test1"))
314            .add_result(TestResult::failed("test2", "error"));
315
316        // Act & Assert
317        assert!(!suite.is_success());
318    }
319
320    #[test]
321    fn test_test_suite_is_success_empty() {
322        // Arrange
323        let suite = TestSuite::new("suite_name");
324
325        // Act & Assert
326        assert!(!suite.is_success());
327    }
328}