Skip to main content

vanilla_test/
lib.rs

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/// An immutable summary produced by [`VanillaTest::report`].
10#[derive(Debug, PartialEq, Eq)]
11pub struct TestResult {
12    /// Numbered descriptions that passed, in test order.
13    pub passed: Box<[String]>,
14    /// Numbered descriptions that failed, in test order.
15    pub failed: Box<[String]>,
16    /// Total number of completed tests.
17    pub total: usize,
18    /// Number of failed tests.
19    pub failure_count: usize,
20    /// `true` exactly when `failure_count` is zero.
21    pub ok: bool,
22    /// Plain-text report containing the summary and both outcome lists.
23    pub report: String,
24}
25
26/// A lifecycle error that leaves the suite unchanged.
27#[derive(Clone, Copy, Debug, PartialEq, Eq)]
28pub enum TestError {
29    /// A reported suite cannot start another test.
30    SuiteAlreadyReported,
31    /// Only one test can be active at a time.
32    TestAlreadyActive,
33    /// Descriptions are exact, case-sensitive, and unique within a suite.
34    DuplicateDescription,
35    /// A decision or completion was requested without an active test.
36    NoActiveTest,
37    /// The active test must be completed before reporting.
38    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/// A single-use, sequential test suite.
74///
75/// The normal lifecycle is `expects` → `pass` or `fail` → `done`, repeated as
76/// needed, followed by one `report`. Calling `done` without a decision records
77/// a failure. Repeated decisions preserve the first outcome.
78#[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    /// Creates an empty suite.
89    #[must_use]
90    pub fn new() -> Self {
91        Self::default()
92    }
93
94    /// Starts one uniquely described test.
95    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    /// Records the active test as passed. The first decision wins.
117    pub fn pass(&mut self) -> Result<(), TestError> {
118        self.decide(Decision::Passed)
119    }
120
121    /// Records the active test as failed. The first decision wins.
122    pub fn fail(&mut self) -> Result<(), TestError> {
123        self.decide(Decision::Failed)
124    }
125
126    /// Completes the active test, failing it if no decision was recorded.
127    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    /// Seals the suite and returns its cached immutable result.
137    ///
138    /// Repeated calls return the same result. Reporting an empty suite succeeds.
139    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}