1#![forbid(unsafe_code)]
2#![doc = include_str!("../RUST.md")]
3
4use std::collections::HashSet;
5use std::error::Error;
6use std::fmt::{self, Write as _};
7use std::sync::Arc;
8
9#[derive(Debug, PartialEq, Eq)]
11pub struct TestResult {
12 pub passed: Box<[String]>,
14 pub failed: Box<[String]>,
16 pub total: usize,
18 pub failure_count: usize,
20 pub ok: bool,
22 pub report: String,
24}
25
26#[derive(Clone, Copy, Debug, PartialEq, Eq)]
28pub enum TestError {
29 SuiteAlreadyReported,
31 TestAlreadyActive,
33 DuplicateDescription,
35 NoActiveTest,
37 ActiveTestNotDone,
39}
40
41impl fmt::Display for TestError {
42 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
43 formatter.write_str(match self {
44 Self::SuiteAlreadyReported => "this vanilla-test suite has already reported",
45 Self::TestAlreadyActive => "a test is already active; call done() first",
46 Self::DuplicateDescription => "test descriptions must be unique",
47 Self::NoActiveTest => "there is no active test; call expects() first",
48 Self::ActiveTestNotDone => {
49 "the active test is not complete; call done() before report()"
50 }
51 })
52 }
53}
54
55impl Error for TestError {}
56
57#[derive(Clone, Copy)]
58enum Decision {
59 Passed,
60 Failed,
61}
62
63struct Case {
64 number: usize,
65 description: Arc<str>,
66}
67
68struct ActiveCase {
69 case: Case,
70 decision: Option<Decision>,
71}
72
73#[derive(Default)]
79pub struct VanillaTest {
80 active: Option<ActiveCase>,
81 descriptions: HashSet<Arc<str>>,
82 passed: Vec<Case>,
83 failed: Vec<Case>,
84 result: Option<TestResult>,
85}
86
87impl VanillaTest {
88 #[must_use]
90 pub fn new() -> Self {
91 Self::default()
92 }
93
94 pub fn expects(&mut self, description: &str) -> Result<(), TestError> {
96 if self.result.is_some() {
97 return Err(TestError::SuiteAlreadyReported);
98 }
99 if self.active.is_some() {
100 return Err(TestError::TestAlreadyActive);
101 }
102 let description: Arc<str> = Arc::from(description);
103 if !self.descriptions.insert(Arc::clone(&description)) {
104 return Err(TestError::DuplicateDescription);
105 }
106 self.active = Some(ActiveCase {
107 case: Case {
108 number: self.passed.len() + self.failed.len() + 1,
109 description,
110 },
111 decision: None,
112 });
113 Ok(())
114 }
115
116 pub fn pass(&mut self) -> Result<(), TestError> {
118 self.decide(Decision::Passed)
119 }
120
121 pub fn fail(&mut self) -> Result<(), TestError> {
123 self.decide(Decision::Failed)
124 }
125
126 pub fn done(&mut self) -> Result<(), TestError> {
128 let active = self.active.take().ok_or(TestError::NoActiveTest)?;
129 match active.decision.unwrap_or(Decision::Failed) {
130 Decision::Passed => self.passed.push(active.case),
131 Decision::Failed => self.failed.push(active.case),
132 }
133 Ok(())
134 }
135
136 pub fn report(&mut self) -> Result<&TestResult, TestError> {
140 if self.result.is_none() {
141 if self.active.is_some() {
142 return Err(TestError::ActiveTestNotDone);
143 }
144
145 self.descriptions = HashSet::new();
146 let passed = render_cases(std::mem::take(&mut self.passed));
147 let failed = render_cases(std::mem::take(&mut self.failed));
148 let total = passed.len() + failed.len();
149 let failure_count = failed.len();
150 let report = render_report(&passed, &failed);
151 self.result = Some(TestResult {
152 passed,
153 failed,
154 total,
155 failure_count,
156 ok: failure_count == 0,
157 report,
158 });
159 }
160
161 let Some(result) = self.result.as_ref() else {
162 unreachable!("the result is initialized above")
163 };
164 Ok(result)
165 }
166
167 fn decide(&mut self, decision: Decision) -> Result<(), TestError> {
168 let active = self.active.as_mut().ok_or(TestError::NoActiveTest)?;
169 if active.decision.is_none() {
170 active.decision = Some(decision);
171 }
172 Ok(())
173 }
174}
175
176fn render_cases(cases: Vec<Case>) -> Box<[String]> {
177 cases
178 .into_iter()
179 .map(|case| format!("{}) .expects {}", case.number, case.description))
180 .collect()
181}
182
183fn render_report(passed: &[String], failed: &[String]) -> String {
184 let mut report = String::new();
185 write!(
186 report,
187 "\n\nResult : {}\n\nTest Total : {}\nPassed : {}\nFailed : {}\n\nFAILED TESTS :\n",
188 if failed.is_empty() {
189 "PASSED"
190 } else {
191 "FAILED"
192 },
193 passed.len() + failed.len(),
194 passed.len(),
195 failed.len()
196 )
197 .expect("writing to a String cannot fail");
198
199 for test in failed {
200 writeln!(report, "{test}").expect("writing to a String cannot fail");
201 }
202 report.push_str("\nPASSED TESTS :\n");
203 for test in passed {
204 writeln!(report, "{test}").expect("writing to a String cannot fail");
205 }
206 report
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212
213 #[test]
214 fn empty_report_is_passing_cached_and_final() {
215 let mut test = VanillaTest::new();
216 let first = test.report().unwrap();
217 assert!(first.ok);
218 assert_eq!(first.total, 0);
219 assert_eq!(first.failure_count, 0);
220 assert!(first.passed.is_empty());
221 assert!(first.failed.is_empty());
222 assert!(first.report.contains("Result : PASSED"));
223 let first = first as *const TestResult;
224
225 assert_eq!(test.report().unwrap() as *const TestResult, first);
226 assert_eq!(
227 test.expects("too late"),
228 Err(TestError::SuiteAlreadyReported)
229 );
230 }
231
232 #[test]
233 fn lifecycle_preserves_first_decisions_order_and_report() {
234 let mut test = VanillaTest::new();
235
236 test.expects("first passes").unwrap();
237 test.pass().unwrap();
238 test.fail().unwrap();
239 test.done().unwrap();
240
241 test.expects("second fails").unwrap();
242 test.fail().unwrap();
243 test.pass().unwrap();
244 test.done().unwrap();
245
246 test.expects("undecided fails").unwrap();
247 test.done().unwrap();
248
249 let result = test.report().unwrap();
250 assert_eq!(result.total, 3);
251 assert_eq!(result.failure_count, 2);
252 assert!(!result.ok);
253 assert_eq!(&*result.passed, ["1) .expects first passes"]);
254 assert_eq!(
255 &*result.failed,
256 ["2) .expects second fails", "3) .expects undecided fails"]
257 );
258 assert_eq!(
259 result.report,
260 "\n\nResult : FAILED\n\nTest Total : 3\nPassed : 1\nFailed : 2\n\nFAILED TESTS :\n2) .expects second fails\n3) .expects undecided fails\n\nPASSED TESTS :\n1) .expects first passes\n"
261 );
262 }
263
264 #[test]
265 fn invalid_transitions_are_typed_and_do_not_advance_numbering() {
266 let mut test = VanillaTest::new();
267 assert_eq!(test.pass(), Err(TestError::NoActiveTest));
268 assert_eq!(test.fail(), Err(TestError::NoActiveTest));
269 assert_eq!(test.done(), Err(TestError::NoActiveTest));
270
271 test.expects("unique").unwrap();
272 assert_eq!(test.expects("blocked"), Err(TestError::TestAlreadyActive));
273 assert_eq!(test.report(), Err(TestError::ActiveTestNotDone));
274 test.pass().unwrap();
275 test.done().unwrap();
276
277 assert_eq!(test.expects("unique"), Err(TestError::DuplicateDescription));
278 test.expects("next").unwrap();
279 test.done().unwrap();
280 let result = test.report().unwrap();
281 assert_eq!(&*result.failed, ["2) .expects next"]);
282 }
283
284 #[test]
285 fn suites_isolate_unicode_descriptions_and_outcomes() {
286 let mut first = VanillaTest::new();
287 let mut second = VanillaTest::new();
288
289 first.expects("same ✓").unwrap();
290 first.pass().unwrap();
291 first.done().unwrap();
292 second.expects("same ✓").unwrap();
293 second.fail().unwrap();
294 second.done().unwrap();
295
296 assert!(first.report().unwrap().ok);
297 let result = second.report().unwrap();
298 assert!(!result.ok);
299 assert_eq!(&*result.failed, ["1) .expects same ✓"]);
300 }
301
302 #[test]
303 fn error_messages_are_stable() {
304 let cases = [
305 (
306 TestError::SuiteAlreadyReported,
307 "this vanilla-test suite has already reported",
308 ),
309 (
310 TestError::TestAlreadyActive,
311 "a test is already active; call done() first",
312 ),
313 (
314 TestError::DuplicateDescription,
315 "test descriptions must be unique",
316 ),
317 (
318 TestError::NoActiveTest,
319 "there is no active test; call expects() first",
320 ),
321 (
322 TestError::ActiveTestNotDone,
323 "the active test is not complete; call done() before report()",
324 ),
325 ];
326
327 for (error, message) in cases {
328 assert_eq!(error.to_string(), message);
329 }
330 }
331
332 #[test]
333 fn public_types_are_safe_to_move_between_threads() {
334 fn assert_send_sync<T: Send + Sync>() {}
335 assert_send_sync::<VanillaTest>();
336 assert_send_sync::<TestResult>();
337 assert_send_sync::<TestError>();
338 }
339}