litmus 0.4.1

a macro-free BDD test harness.
Documentation
use std::collections::HashSet;
use std::marker::PhantomData;

use litmus::elements::Background;
use litmus::elements::Feature;
use litmus::elements::Rule;
use litmus::elements::Scenario;
use litmus::prelude::*;

pub fn main() -> std::process::ExitCode {
    type World = RepositoryWorld<usize, InMemoryRepositoryWorldHandle<usize>>;

    litmus::run(World::default())
}

pub trait Repository<T> {
    fn add(&mut self, item: T);
    fn remove(&mut self, item: &T);
    fn clear(&mut self);

    fn contains(&self, item: &T) -> bool;
}

pub struct RepositoryWorld<T, Handle: RepositoryWorldHandle<T>> {
    pub repository: Handle::Repository,
}

impl<T, Handle: RepositoryWorldHandle<T>> Default for RepositoryWorld<T, Handle> {
    fn default() -> Self {
        Self {
            repository: Handle::default(),
        }
    }
}

pub trait RepositoryWorldHandle<T> {
    type Repository: Repository<T>;

    fn default() -> Self::Repository;
}

pub struct InMemoryRepository<T>(HashSet<T>);

impl<T> Default for InMemoryRepository<T> {
    fn default() -> Self {
        Self(HashSet::new())
    }
}

impl<T> Repository<T> for InMemoryRepository<T>
where
    T: Eq + std::hash::Hash,
{
    fn add(&mut self, item: T) {
        self.0.insert(item);
    }

    fn remove(&mut self, item: &T) {
        self.0.remove(item);
    }

    fn clear(&mut self) {
        self.0.clear();
    }

    fn contains(&self, item: &T) -> bool {
        self.0.contains(item)
    }
}

pub struct InMemoryRepositoryWorldHandle<T>(PhantomData<T>);

impl<T> RepositoryWorldHandle<T> for InMemoryRepositoryWorldHandle<T>
where
    T: Eq + std::hash::Hash,
{
    type Repository = InMemoryRepository<T>;

    fn default() -> Self::Repository {
        Self::Repository::default()
    }
}

impl From<RepositoryWorld<usize, InMemoryRepositoryWorldHandle<usize>>> for Vec<libtest::Trial> {
    #[rustfmt::skip]
    #[allow(clippy::unit_arg)]
    fn from(_: RepositoryWorld<usize, InMemoryRepositoryWorldHandle<usize>>) -> Self {
        type World = RepositoryWorld<usize, InMemoryRepositoryWorldHandle<usize>>;

        Feature::<World>::named("Number Repository")
            .ignored(false)

            .background(|ctx| Background::from(ctx)

                .named("No-op background")
                .ignored(true)
                .given("foo", |_| ok())
                .and("bar", |_| ok())
                .but("baz", |_| ok()))

            .rule(|ctx| Rule::from(ctx)

                .named("Big numbers should work as well")
                .ignored(true)
                
                .background(|ctx| Background::from(ctx)

                    .named("Populate repository with big numbers")
                    .given("a repository with task `MAX`", |world| world.repository.add(usize::MAX).ok())
                    .and("a repository with task `MAX - 1`", |world| world.repository.add(usize::MAX - 1).ok())
                    .and("a repository with task `MAX - 2`", |world| world.repository.add(usize::MAX - 2).ok()))

                .scenario(|ctx| Scenario::from(ctx)

                    .given("a populated repository", |_| ok())
                    .when("doing nothing", |_| ok())
                    .then("the repository should contain all populated numbers", |world| {
                        world.repository.contains(&usize::MAX)
                            .expect(true, "expected `MAX` to be present, found absent")?;
                        world.repository.contains(&(usize::MAX - 1))
                            .expect(true, "expected `MAX - 1` to be present, found absent")?;
                        world.repository.contains(&(usize::MAX - 2))
                            .expect(true, "expected `MAX - 2` to be present, found absent")?;
                        Ok(())
                    })))

            .scenario(|ctx| Scenario::from(ctx)

                .given("an empty repository", |_| ok())
                .when("adding 0", |world| world.repository.add(0).ok())
                .then("the repository should contain 0", |world| world.repository.contains(&0)

                    .expect(true, "expected 0 to be present, found absent")))

            .scenario(|ctx| Scenario::from(ctx)

                .given("a repository containing 0", |world| world.repository.add(0).ok())
                .and("1", |world| world.repository.add(1).ok())
                .when("removing 0", |world| world.repository.remove(&0).ok())
                .but("not removing 1", |_| ok())
                .then("the repository should contain 1", |world| world.repository.contains(&1)

                    .expect(true, "expected 1 to be present, found absent"))
                .but("not 0", |world| world.repository.contains(&0)

                    .expect(false, "expected 0 to be absent, found present")))

            .into()
    }
}