#![forbid(unsafe_code)]
#![doc = include_str!("../RUST.md")]
use std::collections::HashSet;
use std::error::Error;
use std::fmt::{self, Write as _};
use std::sync::Arc;
#[derive(Debug, PartialEq, Eq)]
pub struct TestResult {
pub passed: Box<[String]>,
pub failed: Box<[String]>,
pub total: usize,
pub failure_count: usize,
pub ok: bool,
pub report: String,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TestError {
SuiteAlreadyReported,
TestAlreadyActive,
DuplicateDescription,
NoActiveTest,
ActiveTestNotDone,
}
impl fmt::Display for TestError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(match self {
Self::SuiteAlreadyReported => "this vanilla-test suite has already reported",
Self::TestAlreadyActive => "a test is already active; call done() first",
Self::DuplicateDescription => "test descriptions must be unique",
Self::NoActiveTest => "there is no active test; call expects() first",
Self::ActiveTestNotDone => {
"the active test is not complete; call done() before report()"
}
})
}
}
impl Error for TestError {}
#[derive(Clone, Copy)]
enum Decision {
Passed,
Failed,
}
struct Case {
number: usize,
description: Arc<str>,
}
struct ActiveCase {
case: Case,
decision: Option<Decision>,
}
#[derive(Default)]
pub struct VanillaTest {
active: Option<ActiveCase>,
descriptions: HashSet<Arc<str>>,
passed: Vec<Case>,
failed: Vec<Case>,
result: Option<TestResult>,
}
impl VanillaTest {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn expects(&mut self, description: &str) -> Result<(), TestError> {
if self.result.is_some() {
return Err(TestError::SuiteAlreadyReported);
}
if self.active.is_some() {
return Err(TestError::TestAlreadyActive);
}
let description: Arc<str> = Arc::from(description);
if !self.descriptions.insert(Arc::clone(&description)) {
return Err(TestError::DuplicateDescription);
}
self.active = Some(ActiveCase {
case: Case {
number: self.passed.len() + self.failed.len() + 1,
description,
},
decision: None,
});
Ok(())
}
pub fn pass(&mut self) -> Result<(), TestError> {
self.decide(Decision::Passed)
}
pub fn fail(&mut self) -> Result<(), TestError> {
self.decide(Decision::Failed)
}
pub fn done(&mut self) -> Result<(), TestError> {
let active = self.active.take().ok_or(TestError::NoActiveTest)?;
match active.decision.unwrap_or(Decision::Failed) {
Decision::Passed => self.passed.push(active.case),
Decision::Failed => self.failed.push(active.case),
}
Ok(())
}
pub fn report(&mut self) -> Result<&TestResult, TestError> {
if self.result.is_none() {
if self.active.is_some() {
return Err(TestError::ActiveTestNotDone);
}
self.descriptions = HashSet::new();
let passed = render_cases(std::mem::take(&mut self.passed));
let failed = render_cases(std::mem::take(&mut self.failed));
let total = passed.len() + failed.len();
let failure_count = failed.len();
let report = render_report(&passed, &failed);
self.result = Some(TestResult {
passed,
failed,
total,
failure_count,
ok: failure_count == 0,
report,
});
}
let Some(result) = self.result.as_ref() else {
unreachable!("the result is initialized above")
};
Ok(result)
}
fn decide(&mut self, decision: Decision) -> Result<(), TestError> {
let active = self.active.as_mut().ok_or(TestError::NoActiveTest)?;
if active.decision.is_none() {
active.decision = Some(decision);
}
Ok(())
}
}
fn render_cases(cases: Vec<Case>) -> Box<[String]> {
cases
.into_iter()
.map(|case| format!("{}) .expects {}", case.number, case.description))
.collect()
}
fn render_report(passed: &[String], failed: &[String]) -> String {
let mut report = String::new();
write!(
report,
"\n\nResult : {}\n\nTest Total : {}\nPassed : {}\nFailed : {}\n\nFAILED TESTS :\n",
if failed.is_empty() {
"PASSED"
} else {
"FAILED"
},
passed.len() + failed.len(),
passed.len(),
failed.len()
)
.expect("writing to a String cannot fail");
for test in failed {
writeln!(report, "{test}").expect("writing to a String cannot fail");
}
report.push_str("\nPASSED TESTS :\n");
for test in passed {
writeln!(report, "{test}").expect("writing to a String cannot fail");
}
report
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_report_is_passing_cached_and_final() {
let mut test = VanillaTest::new();
let first = test.report().unwrap();
assert!(first.ok);
assert_eq!(first.total, 0);
assert_eq!(first.failure_count, 0);
assert!(first.passed.is_empty());
assert!(first.failed.is_empty());
assert!(first.report.contains("Result : PASSED"));
let first = first as *const TestResult;
assert_eq!(test.report().unwrap() as *const TestResult, first);
assert_eq!(
test.expects("too late"),
Err(TestError::SuiteAlreadyReported)
);
}
#[test]
fn lifecycle_preserves_first_decisions_order_and_report() {
let mut test = VanillaTest::new();
test.expects("first passes").unwrap();
test.pass().unwrap();
test.fail().unwrap();
test.done().unwrap();
test.expects("second fails").unwrap();
test.fail().unwrap();
test.pass().unwrap();
test.done().unwrap();
test.expects("undecided fails").unwrap();
test.done().unwrap();
let result = test.report().unwrap();
assert_eq!(result.total, 3);
assert_eq!(result.failure_count, 2);
assert!(!result.ok);
assert_eq!(&*result.passed, ["1) .expects first passes"]);
assert_eq!(
&*result.failed,
["2) .expects second fails", "3) .expects undecided fails"]
);
assert_eq!(
result.report,
"\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"
);
}
#[test]
fn invalid_transitions_are_typed_and_do_not_advance_numbering() {
let mut test = VanillaTest::new();
assert_eq!(test.pass(), Err(TestError::NoActiveTest));
assert_eq!(test.fail(), Err(TestError::NoActiveTest));
assert_eq!(test.done(), Err(TestError::NoActiveTest));
test.expects("unique").unwrap();
assert_eq!(test.expects("blocked"), Err(TestError::TestAlreadyActive));
assert_eq!(test.report(), Err(TestError::ActiveTestNotDone));
test.pass().unwrap();
test.done().unwrap();
assert_eq!(test.expects("unique"), Err(TestError::DuplicateDescription));
test.expects("next").unwrap();
test.done().unwrap();
let result = test.report().unwrap();
assert_eq!(&*result.failed, ["2) .expects next"]);
}
#[test]
fn suites_isolate_unicode_descriptions_and_outcomes() {
let mut first = VanillaTest::new();
let mut second = VanillaTest::new();
first.expects("same ✓").unwrap();
first.pass().unwrap();
first.done().unwrap();
second.expects("same ✓").unwrap();
second.fail().unwrap();
second.done().unwrap();
assert!(first.report().unwrap().ok);
let result = second.report().unwrap();
assert!(!result.ok);
assert_eq!(&*result.failed, ["1) .expects same ✓"]);
}
#[test]
fn error_messages_are_stable() {
let cases = [
(
TestError::SuiteAlreadyReported,
"this vanilla-test suite has already reported",
),
(
TestError::TestAlreadyActive,
"a test is already active; call done() first",
),
(
TestError::DuplicateDescription,
"test descriptions must be unique",
),
(
TestError::NoActiveTest,
"there is no active test; call expects() first",
),
(
TestError::ActiveTestNotDone,
"the active test is not complete; call done() before report()",
),
];
for (error, message) in cases {
assert_eq!(error.to_string(), message);
}
}
#[test]
fn public_types_are_safe_to_move_between_threads() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<VanillaTest>();
assert_send_sync::<TestResult>();
assert_send_sync::<TestError>();
}
}