1use std::fmt;
6use std::time::Duration;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum TestStatus {
11 Passed,
12 Failed,
13 Ignored,
14}
15
16#[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#[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 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 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}