pbt-tests 0.4.22

Property-based testing with `derive` macros, aware of mutual induction & instantiability.
Documentation
use {
    core::convert::Infallible,
    pbt::{
        Pbt,
        sigma::{Predicate, Sigma},
    },
};

#[non_exhaustive]
#[derive(Clone, Debug, Eq, PartialEq, Pbt)]
pub enum Foo {
    Bar,
    Baz { a: u64, b: u64, c: Vec<Foo> },
}

#[derive(Clone, Debug, Eq, PartialEq, Pbt)]
pub enum PartiallyInstantiable {
    Instantiable,
    Uninstantiable(Infallible),
}

#[derive(Clone, Debug, Eq, PartialEq, Pbt)]
pub enum Uninhabited {}

pub type NonAnswer = Sigma<u8, NotTheAnswer>;

pub enum NotTheAnswer {}

impl Foo {
    #[inline]
    #[must_use]
    pub fn bus_factor(&self) -> usize {
        match *self {
            Self::Bar => 0,
            Self::Baz { ref c, .. } => c.len(),
        }
    }
}

impl Predicate<u8> for NotTheAnswer {
    type Error = String;

    #[inline]
    fn check(candidate: &u8) -> Result<(), Self::Error> {
        if *candidate == 42 {
            Err(format!(
                "The Answer to the Ultimate Question of Life, the Universe, and Everything is {candidate}",
            ))
        } else {
            Ok(())
        }
    }
}

#[cfg(test)]
mod test {
    use {super::*, pbt::search, pretty_assertions::assert_eq};

    const N_CASES: usize = 1_000;

    #[derive(Clone, Debug, Eq, PartialEq, Pbt)]
    enum ShapedNever {}

    #[derive(Clone, Debug, Eq, PartialEq, Pbt)]
    enum ShapedPayload<T> {
        Empty,
        Bits(Vec<T>),
    }

    #[derive(Clone, Debug, Eq, PartialEq, Pbt)]
    struct ShapedGenericWrapper<T> {
        item: T,
    }

    #[derive(Clone, Debug, Eq, PartialEq, Pbt)]
    struct ShapedUsesGenericWrapper {
        wrapper: ShapedGenericWrapper<bool>,
    }

    type ShapedPayloadAlias = ShapedPayload<bool>;

    #[derive(Clone, Debug, Eq, PartialEq, Pbt)]
    struct ShapedAliasField {
        payload: ShapedPayloadAlias,
    }

    #[derive(Clone, Debug, Eq, PartialEq, Pbt)]
    struct ShapedOpaqueForeignPath {
        set: std::collections::BTreeSet<u8>,
    }

    #[derive(Clone, Debug, Eq, PartialEq, Pbt)]
    struct ShapedProjection<T> {
        item: <Vec<T> as IntoIterator>::Item,
    }

    #[derive(Clone, Debug, Eq, PartialEq, Pbt)]
    enum ShapedTree {
        Leaf,
        Branch {
            left: Box<Self>,
            value: usize,
            flags: Vec<bool>,
            right: Box<Self>,
        },
    }

    #[test]
    fn instantiability_logic() {
        search::assert_eq(N_CASES, |pi: &PartiallyInstantiable| {
            (pi.clone(), PartiallyInstantiable::Instantiable)
        });
    }

    #[test]
    fn search_and_minimize() {
        let maybe_witness: Option<Foo> =
            search::witness(N_CASES, |foo: &Foo| foo.bus_factor() >= 3);
        assert_eq!(
            maybe_witness,
            Some(Foo::Baz {
                a: 0,
                b: 0,
                c: vec![Foo::Bar, Foo::Bar, Foo::Bar],
            }),
        );
    }

    #[test]
    fn sigma() {
        search::assert(N_CASES, |u: &NonAnswer| **u != 42);
    }

    #[test]
    fn empty_enum_is_supported() {
        let maybe_witness: Option<Uninhabited> = search::witness(N_CASES, |_| true);
        assert_eq!(maybe_witness, None);
    }

    #[test]
    fn shaped_empty_enums_have_vacuous_eliminators() {
        let absurd: fn(&ShapedNever) -> usize = |never| never.elim(());
        let _ = absurd;
    }

    #[test]
    fn shaped_generic_fields_do_not_need_recursion() {
        let empty = <ShapedPayload<bool> as ShapedPayloadShaped>::empty(ShapedPayloadEmpty);
        let empty_len = empty.elim(
            (),
            |(), ShapedPayloadEmpty| 0_usize,
            |(), ShapedPayloadBits(bits)| bits.len(),
        );
        let payload =
            <ShapedPayload<bool> as ShapedPayloadShaped>::bits(ShapedPayloadBits(vec![true]));
        let len = payload.elim(
            (),
            |(), ShapedPayloadEmpty| 0_usize,
            |(), ShapedPayloadBits(bits)| bits.len(),
        );

        assert_eq!(empty_len, 0);
        assert_eq!(len, 1);
    }

    #[test]
    fn shaped_custom_generic_field_paths_are_opaque_slots() {
        let value = ShapedUsesGenericWrapper {
            wrapper: ShapedGenericWrapper { item: true },
        };
        let selected = value.elim((), |(), ShapedUsesGenericWrapperShape { wrapper }| {
            wrapper.item
        });

        assert!(selected);
    }

    #[test]
    fn shaped_qualified_projection_fields_are_opaque_slots() {
        let value = ShapedProjection::<u8> { item: 9 };
        let selected = value.elim((), |(), ShapedProjectionShape { item }| *item);

        assert_eq!(selected, 9);
    }

    #[test]
    fn shaped_unknown_paths_are_opaque_slots() {
        let alias = ShapedAliasField {
            payload: ShapedPayload::Empty,
        };
        let is_empty = alias.elim((), |(), ShapedAliasFieldShape { payload }| {
            matches!(*payload, ShapedPayload::Empty)
        });
        let foreign = ShapedOpaqueForeignPath {
            set: [7_u8].into_iter().collect(),
        };
        let contains = foreign.elim((), |(), ShapedOpaqueForeignPathShape { set }| {
            set.contains(&7)
        });

        assert!(is_empty);
        assert!(contains);
    }

    #[test]
    fn shaped_macro_generates_field_wise_eliminator() {
        let tree = <ShapedTree as ShapedTreeShaped>::branch(ShapedTreeBranch {
            left: Box::new(ShapedTree::Leaf),
            value: 7,
            flags: vec![true, false],
            right: Box::new(ShapedTree::Leaf),
        });

        let selected = tree.elim(
            5,
            |_, ShapedTreeLeaf| 0,
            |state, branch| {
                assert!(matches!(**branch.left, ShapedTree::Leaf));
                assert!(matches!(**branch.right, ShapedTree::Leaf));
                assert_eq!(branch.flags.as_slice(), &[true, false]);
                state + *branch.value
            },
        );

        assert_eq!(selected, 12);
    }
}