diff --git a/Cargo.toml b/Cargo.toml index 9493325..5ce3dc0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "contrafact" -version = "0.2.0-rc.0" +version = "0.2.0-rc.1" authors = ["Michael Dougherty "] repository = "https://github.com/maackle/contrafact-rs/" license = "MIT" @@ -11,19 +11,18 @@ edition = "2021" [dependencies] arbitrary = {version = "1.0", features = ["derive"]} +either = "1.5" derive_more = "0.99" itertools = "0.10" -tracing = "0.1" num = "0.4.0" -anyhow = "1.0.40" +tracing = "0.1" # utils once_cell = { version = "1.5", optional = true } rand = { version = "0.7", optional = true } -# lens -lens-rs = { version = "0.3", optional = true } -either = "1.5" +# # lens +# lens-rs = { version = "0.3", optional = true } [dev-dependencies] either = "1.5" @@ -34,7 +33,7 @@ default = ["utils"] utils = ["once_cell", "rand"] -optics = ["lens-rs"] +# optics = ["lens-rs"] [package.metadata.inwelling] lens-rs_generator = true diff --git a/TODO.md b/TODO.md index 67678a0..d998569 100644 --- a/TODO.md +++ b/TODO.md @@ -1,4 +1,2 @@ -- [ ] Fix generator fns -- [ ] Make Fact a builder which takes a LambdaFact-style function, and can provide a label or label modifier - [ ] Take some methods off of the trait (satisfy, build) \ No newline at end of file diff --git a/src/check.rs b/src/check.rs index 80c9a5e..05a482c 100644 --- a/src/check.rs +++ b/src/check.rs @@ -91,6 +91,11 @@ impl Check { } } + /// If Failures, return all failures joined together in a single string + pub fn result_joined(self) -> ContrafactResult> { + self.result().map(|r| r.map_err(|es| es.join(";"))) + } + /// Create a single-error failure if predicate is false, otherwise pass /// /// ``` @@ -118,7 +123,8 @@ impl Check { Ok(_) => Self::pass(), Err(MutationError::Check(err)) => Self::fail(err), Err(MutationError::Arbitrary(err)) => Self::Error(err.to_string()), - Err(MutationError::Exception(err)) => Self::Error(format!("{:?}", err)), + Err(MutationError::Internal(err)) => Self::Error(format!("{:?}", err)), + Err(MutationError::User(err)) => Self::Error(format!("{:?}", err)), } } diff --git a/src/error.rs b/src/error.rs index 9e05622..8917ddf 100644 --- a/src/error.rs +++ b/src/error.rs @@ -29,8 +29,13 @@ pub enum MutationError { /// arbitrary failed to produce new data, which means we can't go on #[from] Arbitrary(arbitrary::Error), + + /// Contrafact experienced a problem + #[from] + Internal(ContrafactError), + /// There was some other bug in the Fact implementation - Exception(String), + User(String), } impl PartialEq for MutationError { @@ -38,7 +43,8 @@ impl PartialEq for MutationError { match (self, other) { (Self::Check(s), Self::Check(o)) => s == o, (Self::Arbitrary(s), Self::Arbitrary(o)) => s.to_string() == o.to_string(), - (Self::Exception(s), Self::Exception(o)) => s == o, + (Self::Internal(s), Self::Internal(o)) => s == o, + (Self::User(s), Self::User(o)) => s == o, _ => false, } } diff --git a/src/fact.rs b/src/fact.rs index 3858ac8..02d3182 100644 --- a/src/fact.rs +++ b/src/fact.rs @@ -3,32 +3,33 @@ use either::Either; use crate::*; -// TODO: we can remove the Clone requirement if: -// - make `Mutate` track list of errors so that it can know if a mutation occurred. -// - make `mutate()` take a mut ref -// - make `check()` take a mut ref -// then `check()` can know if a mutation occurred -// -/// The trait bounds for the subject of a Fact -pub trait Bounds<'a>: std::fmt::Debug + Clone + Send + Sync + PartialEq + Arbitrary<'a> {} -impl<'a, T> Bounds<'a> for T where - T: std::fmt::Debug + Clone + Send + Sync + PartialEq + Arbitrary<'a> +/// The trait bounds for the target of a Fact +pub trait Target<'a>: + 'a + std::fmt::Debug + Clone + Send + Sync + PartialEq + Arbitrary<'a> +{ +} +impl<'a, T> Target<'a> for T where + T: 'a + std::fmt::Debug + Clone + Send + Sync + PartialEq + Arbitrary<'a> { } -// /// Type alias for a boxed Fact. Implements [`Fact`] itself. -// pub type BoxFact<'a, T> = Box>; - -// pub trait Facts>: Fact<'static, T> {} -// impl, F: Facts> Fact<'static, T> for F {} +/// The trait bounds for the State of a Fact +pub trait State: std::fmt::Debug + Clone + Send + Sync {} +impl State for T where T: std::fmt::Debug + Clone + Send + Sync {} /// A declarative representation of a constraint on some data, which can be /// used to both make an assertion (check) or to mold some arbitrary existing /// data into a shape which passes that same assertion (mutate) -pub trait Fact<'a, T>: Send + Sync + Clone +pub trait Fact<'a, T>: Send + Sync + Clone + std::fmt::Debug where - T: Bounds<'a>, + T: Target<'a>, { + /// Change the label + fn labeled(self, label: impl ToString) -> Self; + + /// Get the label of this fact + fn label(&self) -> String; + /// Assert that the constraint is satisfied for given data. /// /// If the mutation function is written properly, we get a check for free @@ -36,14 +37,15 @@ where /// some reason unreasonable, a check function can be written by hand, but /// care must be taken to make sure it perfectly lines up with the mutation function. #[tracing::instrument(fields(fact_impl = "Fact"), skip(self))] - fn check(mut self, obj: &T) -> Check { - check_raw(&mut self, obj) + fn check(mut self, t: &T) -> Check { + let mut g = Generator::checker(); + Check::from_mutation(self.mutate(&mut g, t.clone())) } - /// Apply a mutation which moves the obj closer to satisfying the overall + /// Apply a mutation which moves the t closer to satisfying the overall /// constraint. // #[tracing::instrument(skip(self, g))] - fn mutate(&mut self, g: &mut Generator<'a>, obj: T) -> Mutation; + fn mutate(&mut self, g: &mut Generator<'a>, t: T) -> Mutation; /// Make this many attempts to satisfy a constraint before giving up and panicking. /// @@ -59,15 +61,14 @@ where /// Mutate a value such that it satisfies the constraint. /// If the constraint cannot be satisfied, panic. #[tracing::instrument(fields(fact_impl = "Fact"), skip(self, g))] - fn satisfy(&mut self, g: &mut Generator<'a>, obj: T) -> ContrafactResult { + fn satisfy(&mut self, g: &mut Generator<'a>, t: T) -> ContrafactResult { tracing::trace!("satisfy"); let mut last_failure: Vec = vec![]; - let mut next = obj.clone(); + let mut next = t.clone(); for _i in 0..self.satisfy_attempts() { let mut m = self.clone(); - let mut c = self.clone(); next = m.mutate(g, next).unwrap(); - if let Err(errs) = check_raw(&mut c, &next).result()? { + if let Err(errs) = self.clone().check(&next).result()? { last_failure = errs; } else { *self = m; @@ -83,8 +84,8 @@ where #[tracing::instrument(fields(fact_impl = "Fact"), skip(self, g))] /// Build a new value such that it satisfies the constraint fn build_fallible(mut self, g: &mut Generator<'a>) -> ContrafactResult { - let obj = T::arbitrary(g).unwrap(); - self.satisfy(g, obj) + let t = T::arbitrary(g).unwrap(); + self.satisfy(g, t) } /// Build a new value such that it satisfies the constraint, panicking on error @@ -96,40 +97,44 @@ where impl<'a, T, F1, F2> Fact<'a, T> for Either where - T: Bounds<'a>, + T: Target<'a>, F1: Fact<'a, T> + ?Sized, F2: Fact<'a, T> + ?Sized, { #[tracing::instrument(fields(fact_impl = "Either"), skip(self, g))] - fn mutate(&mut self, g: &mut Generator<'a>, obj: T) -> Mutation { + fn mutate(&mut self, g: &mut Generator<'a>, t: T) -> Mutation { match self { - Either::Left(f) => f.mutate(g, obj), - Either::Right(f) => f.mutate(g, obj), + Either::Left(f) => f.mutate(g, t), + Either::Right(f) => f.mutate(g, t), } } -} -#[tracing::instrument(skip(fact))] -pub(crate) fn check_raw<'a, T, F: Fact<'a, T>>(fact: &mut F, obj: &T) -> Check -where - T: Bounds<'a> + ?Sized, - F: Fact<'a, T> + ?Sized, -{ - let mut g = Generator::checker(); - Check::from_mutation(fact.mutate(&mut g, obj.clone())) + fn label(&self) -> String { + match self { + Either::Left(f) => f.label(), + Either::Right(f) => f.label(), + } + } + + fn labeled(self, label: impl ToString) -> Self { + match self { + Either::Left(f) => Either::Left(f.labeled(label)), + Either::Right(f) => Either::Right(f.labeled(label)), + } + } } #[tracing::instrument(skip(facts))] -fn collect_checks<'a, T, F>(facts: Vec, obj: &T) -> Check +fn collect_checks<'a, T, F>(facts: Vec, t: &T) -> Check where - T: Bounds<'a>, + T: Target<'a>, F: Fact<'a, T>, { let checks = facts .into_iter() .enumerate() .map(|(i, f)| { - Ok(f.check(obj) + Ok(f.check(t) .failures()? .iter() .map(|e| format!("fact {}: {}", i, e)) diff --git a/src/facts.rs b/src/facts.rs index a263960..f2bfdd1 100644 --- a/src/facts.rs +++ b/src/facts.rs @@ -5,9 +5,7 @@ mod constant; mod eq; mod in_range; mod in_slice; -mod lambda; mod lens; -mod mapped; mod not; mod or; mod prism; @@ -16,126 +14,23 @@ mod seq; pub use consecutive_int::{consecutive_int, consecutive_int_}; pub use constant::{always, never}; -pub use eq::{eq, eq_, ne, ne_}; -pub use in_range::{in_range, in_range_}; +pub use eq::{eq, ne}; +pub use in_range::in_range; pub use in_slice::{in_slice, in_slice_}; -pub use not::{not, not_}; +pub use not::not; pub use or::or; pub use same::{different, same}; pub use and::and; -pub use brute::{brute, brute_fallible}; -pub use lambda::lambda; -pub use lens::{lens, LensFact}; -pub use mapped::{mapped, mapped_fallible}; -pub use prism::{prism, PrismFact}; +pub use brute::brute; +pub use lens::{lens1, lens2}; +pub use prism::prism; pub use seq::{vec, vec_len, vec_of_length}; -#[cfg(feature = "optics")] -mod optical; -#[cfg(feature = "optics")] -pub use optical::*; +// Optical facts are experimental and currently not supported +// #[cfg(feature = "optics")] +// mod optical; +// #[cfg(feature = "optics")] +// pub use optical::*; -pub(crate) use eq::EqOp; - -use crate::fact::check_raw; use crate::*; -use std::marker::PhantomData; - -#[cfg(test)] -mod tests { - use super::*; - use crate::utils; - - #[test] - fn test_eq() { - observability::test_run().ok(); - let mut g = utils::random_generator(); - - let eq1 = vec(eq("must be 1", 1)); - - let ones = eq1.clone().build(&mut g); - eq1.clone().check(&ones).unwrap(); - - assert!(ones.iter().all(|x| *x == 1)); - } - - #[test] - fn test_or() { - observability::test_run().ok(); - let mut g = utils::random_generator(); - - let eq1 = eq("must be 1", 1); - let eq2 = eq("must be 2", 2); - let either = or("can be 1 or 2", eq1, eq2); - - let ones = vec(either.clone()).build(&mut g); - vec(either.clone()).check(&ones).unwrap(); - assert!(ones.iter().all(|x| *x == 1 || *x == 2)); - - assert_eq!(either.check(&3).result().unwrap().unwrap_err().len(), 1); - } - - #[test] - fn test_not() { - observability::test_run().ok(); - let mut g = utils::random_generator(); - - let eq1 = eq("must be 1", 1); - let not1 = vec(not_(eq1)); - - let nums = not1.clone().build(&mut g); - not1.clone().check(&nums).unwrap(); - - assert!(nums.iter().all(|x| *x != 1)); - } - - #[test] - fn test_same() { - observability::test_run().ok(); - let mut g = utils::random_generator(); - - { - let f = vec(same::()); - let nums = f.clone().build(&mut g); - f.clone().check(&nums).unwrap(); - assert!(nums.iter().all(|(a, b)| a == b)); - } - { - let f = vec(different::()); - let nums = f.clone().build(&mut g); - f.clone().check(&nums).unwrap(); - assert!(nums.iter().all(|(a, b)| a != b)); - } - } - - #[test] - fn test_in_range() { - observability::test_run().ok(); - let mut g = utils::random_generator(); - - let positive1 = in_range("must be positive", 1..=i32::MAX); - let positive2 = in_range("must be positive", 1..); - let smallish = in_range("must be small in magnitude", -10..100); - let over9000 = in_range("must be over 9000", 9001..); - let under9000 = in_range("must be under 9000 (and no less than zero)", ..9000u32); - - let nonpositive1 = vec(not_(positive1)); - let nonpositive2 = vec(not_(positive2)); - - let smallish_nums = smallish.clone().build(&mut g); - let over9000_nums = over9000.clone().build(&mut g); - let under9000_nums = under9000.clone().build(&mut g); - let nonpositive1_nums = nonpositive1.clone().build(&mut g); - let nonpositive2_nums = nonpositive2.clone().build(&mut g); - - dbg!(&under9000_nums); - - smallish.clone().check(&smallish_nums).unwrap(); - over9000.clone().check(&over9000_nums).unwrap(); - under9000.clone().check(&under9000_nums).unwrap(); - nonpositive1.clone().check(&nonpositive1_nums).unwrap(); - nonpositive2.clone().check(&nonpositive2_nums).unwrap(); - assert!(nonpositive1_nums.iter().all(|x| *x <= 0)); - } -} diff --git a/src/facts/and.rs b/src/facts/and.rs index 375417c..a6f609a 100644 --- a/src/facts/and.rs +++ b/src/facts/and.rs @@ -1,58 +1,13 @@ -use std::marker::PhantomData; - use crate::*; /// A Fact which applies two other facts. -pub fn and<'a, F1, F2, T>(a: F1, b: F2) -> AndFact<'a, F1, F2, T> -where - T: Bounds<'a>, - F1: Fact<'a, T>, - F2: Fact<'a, T>, -{ - AndFact::new(a, b) -} - -/// A fact which applies two facts. -/// This is the primary way to build up a complex fact from simpler facts. -/// The [`facts!`] macro is a more convenient way to compose more than two facts -/// together using [`AndFact`]. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct AndFact<'a, F1, F2, T> -where - F1: Fact<'a, T>, - F2: Fact<'a, T>, - T: ?Sized + Bounds<'a>, -{ - pub(crate) a: F1, - pub(crate) b: F2, - _phantom: PhantomData<&'a T>, -} - -impl<'a, F1, F2, T> AndFact<'a, F1, F2, T> -where - F1: Fact<'a, T>, - F2: Fact<'a, T>, - T: ?Sized + Bounds<'a>, -{ - /// Constructor - pub fn new(a: F1, b: F2) -> Self { - Self { - a, - b, - _phantom: PhantomData, - } - } -} - -impl<'a, F1, F2, T> Fact<'a, T> for AndFact<'a, F1, F2, T> +pub fn and<'a, T>(a: impl Fact<'a, T>, b: impl Fact<'a, T>) -> impl Fact<'a, T> where - F1: Fact<'a, T> + Fact<'a, T>, - F2: Fact<'a, T> + Fact<'a, T>, - T: Bounds<'a>, + T: Target<'a>, { - fn mutate(&mut self, g: &mut Generator<'a>, obj: T) -> Mutation { - let obj = self.a.mutate(g, obj)?; - let obj = self.b.mutate(g, obj)?; - Ok(obj) - } + lambda("and", (a, b), |g, (a, b), t| { + let t = a.mutate(g, t)?; + let t = b.mutate(g, t)?; + Ok(t) + }) } diff --git a/src/facts/brute.rs b/src/facts/brute.rs index 9cdc5e2..9f4077f 100644 --- a/src/facts/brute.rs +++ b/src/facts/brute.rs @@ -1,18 +1,4 @@ -use std::sync::Arc; - -use crate::{fact::Bounds, Fact, BRUTE_ITERATION_LIMIT}; - -use crate::{Generator, Mutation}; - -/// A version of [`brute`] whose closure returns a Result -pub fn brute_fallible<'a, T, F, S>(reason: S, f: F) -> BruteFact<'a, T> -where - S: ToString, - T: Bounds<'a>, - F: 'a + Send + Sync + Fn(&T) -> Mutation, -{ - BruteFact::::new(reason.to_string(), f) -} +use crate::*; /// A constraint defined only by a predicate closure. Mutation occurs by brute /// force, randomly trying values until one matches the constraint. @@ -30,7 +16,8 @@ where /// ALSO **NOTE**: It is usually best to place this constraint at the beginning /// of a chain when doing mutation, because if the closure specifies a weak /// constraint, the mutation may drastically alter the data, potentially undoing -/// constraints that were met by previous mutations. +/// constraints that were met by previous mutations. It's also probably not a +/// good idea to combine two different brute facts /// /// There is a fixed iteration limit, beyond which this will panic. /// @@ -45,53 +32,39 @@ where /// let mut g = utils::random_generator(); /// assert!(div_by(3).build(&mut g) % 3 == 0); /// ``` -pub fn brute<'a, T, F, S>(reason: S, f: F) -> BruteFact<'a, T> +pub fn brute<'a, T, F>(label: impl ToString, f: F) -> Lambda<'a, (), T> where - S: ToString, - T: Bounds<'a>, + T: Target<'a>, F: 'a + Send + Sync + Fn(&T) -> bool, { - BruteFact::::new(reason.to_string(), move |x| Ok(f(x))) + let label = label.to_string(); + // TODO figure out this label stuff + let label2 = label.clone(); + brute_labeled(move |v| Ok(f(v).then_some(()).ok_or_else(|| label.clone()))).labeled(label2) } -type BruteFn<'a, T> = Arc Mutation>; - -/// A brute-force fact. Use [`brute()`] to construct. -#[derive(Clone)] -pub struct BruteFact<'a, T> { - label: String, - f: BruteFn<'a, T>, -} - -impl<'a, T> Fact<'a, T> for BruteFact<'a, T> +/// A version of [`brute`] which allows the closure to return the reason for failure +pub fn brute_labeled<'a, T, F>(f: F) -> Lambda<'a, (), T> where - T: Bounds<'a>, + T: Target<'a>, + F: 'a + Send + Sync + Fn(&T) -> ContrafactResult, { - #[tracing::instrument(fields(fact = "brute"), skip(self, g))] - fn mutate(&mut self, g: &mut Generator<'a>, mut obj: T) -> Mutation { - tracing::trace!("brute"); + lambda_unit("brute_labeled", move |g, mut t| { + let mut last_reason = "".to_string(); for _ in 0..=BRUTE_ITERATION_LIMIT { - if (self.f)(&obj)? { - return Ok(obj); + if let Err(reason) = f(&t)? { + last_reason = reason.clone(); + t = g.arbitrary(|| reason)?; + } else { + return Ok(t); } - obj = g.arbitrary(&self.label)?; } panic!( - "Exceeded iteration limit of {} while attempting to meet a BruteFact. Context: {}", - BRUTE_ITERATION_LIMIT, self.label + "Exceeded iteration limit of {} while attempting to meet a BruteFact. Last failure reason: {}", + BRUTE_ITERATION_LIMIT, last_reason ); - } + }) } -impl<'a, T> BruteFact<'a, T> { - pub(crate) fn new Mutation>( - reason: String, - f: F, - ) -> Self { - Self { - label: reason, - f: Arc::new(f), - } - } -} +type BruteResult = Result<(), String>; diff --git a/src/facts/consecutive_int.rs b/src/facts/consecutive_int.rs index d3ceca7..156b654 100644 --- a/src/facts/consecutive_int.rs +++ b/src/facts/consecutive_int.rs @@ -1,43 +1,26 @@ use super::*; /// Specifies that a value should be increasing by 1 at every check/mutation -pub fn consecutive_int(context: S, initial: T) -> ConsecutiveIntFact +pub fn consecutive_int<'a, S>(context: impl ToString, initial: S) -> Lambda<'a, S, S> where - S: ToString, - T: std::fmt::Debug + PartialEq + num::PrimInt, + S: Target<'a> + std::fmt::Debug + PartialEq + num::PrimInt, { - ConsecutiveIntFact { - context: context.to_string(), - counter: initial, - } + let context = context.to_string(); + lambda("consecutive_int", initial, move |g, counter, mut t| { + if t != *counter { + g.fail(&context)?; + t = counter.clone(); + } + *counter = counter.checked_add(&S::from(1).unwrap()).unwrap(); + Ok(t) + }) } /// Specifies that a value should be increasing by 1 at every check/mutation, /// with no context given -pub fn consecutive_int_(initial: T) -> ConsecutiveIntFact +pub fn consecutive_int_<'a, T>(initial: T) -> Lambda<'a, T, T> where - T: std::fmt::Debug + PartialEq + num::PrimInt, + T: Target<'a> + PartialEq + num::PrimInt, { consecutive_int("consecutive_int", initial) } - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ConsecutiveIntFact { - context: String, - counter: T, -} - -impl<'a, T> Fact<'a, T> for ConsecutiveIntFact -where - T: Bounds<'a> + num::PrimInt, -{ - #[tracing::instrument(fields(fact = "consecutive_int"), skip(self, g))] - fn mutate(&mut self, g: &mut Generator<'a>, mut obj: T) -> Mutation { - if obj != self.counter { - g.fail(&self.context)?; - obj = self.counter.clone(); - } - self.counter = self.counter.checked_add(&T::from(1).unwrap()).unwrap(); - Ok(obj) - } -} diff --git a/src/facts/constant.rs b/src/facts/constant.rs index 424e8bc..fca0e76 100644 --- a/src/facts/constant.rs +++ b/src/facts/constant.rs @@ -1,27 +1,15 @@ use super::*; /// A constraint which is always met -pub fn always() -> ConstantFact { - ConstantFact(true, "always".to_string()) +pub fn always<'a, T: Target<'a>>() -> Lambda<'a, (), T> { + lambda_unit("always", |_, t| Ok(t)) } /// A constraint which is never met -pub fn never(context: S) -> ConstantFact { - ConstantFact(false, context.to_string()) -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ConstantFact(bool, String); - -impl<'a, T> Fact<'a, T> for ConstantFact -where - T: Bounds<'a> + PartialEq + Clone, -{ - #[tracing::instrument(fields(fact = "bool"), skip(self, g))] - fn mutate(&mut self, g: &mut Generator<'_>, obj: T) -> Mutation { - if !self.0 { - g.fail("never() encountered.")?; - } - Ok(obj) - } +pub fn never<'a, T: Target<'a>>(context: impl ToString) -> Lambda<'a, (), T> { + let context = context.to_string(); + lambda_unit("never", move |g, t: T| { + g.fail(context.clone())?; + Ok(t) + }) } diff --git a/src/facts/eq.rs b/src/facts/eq.rs index 0f57e53..0da38a5 100644 --- a/src/facts/eq.rs +++ b/src/facts/eq.rs @@ -1,90 +1,38 @@ use super::*; /// Specifies an equality constraint -pub fn eq(context: S, constant: T) -> EqFact +pub fn eq<'a, T>(constant: T) -> Lambda<'a, (), T> where - S: ToString, - T: std::fmt::Debug + PartialEq, + T: Target<'a> + PartialEq + Clone, { - EqFact { - context: context.to_string(), - constant, - op: EqOp::Equal, - _phantom: PhantomData, - } -} - -/// Specifies an equality constraint with no context -pub fn eq_(constant: T) -> EqFact -where - T: std::fmt::Debug + PartialEq, -{ - eq("eq", constant) + let label = format!("eq({:?})", constant); + lambda_unit(label, move |g, mut t| { + if t != constant { + g.fail(format!("expected {:?} == {:?}", t, constant))?; + t = constant.clone(); + } + Ok(t) + }) } /// Specifies an inequality constraint -pub fn ne(context: S, constant: T) -> EqFact +pub fn ne<'a, S, T>(constant: T) -> Lambda<'a, (), T> where S: ToString, - T: std::fmt::Debug + PartialEq, + T: Target<'a> + PartialEq, { - EqFact { - context: context.to_string(), - constant, - op: EqOp::NotEqual, - _phantom: PhantomData, - } + not(eq(constant)).labeled("ne") } -/// Specifies an inequality constraint with no context -pub fn ne_(constant: T) -> EqFact -where - T: std::fmt::Debug + PartialEq, -{ - ne("ne", constant) -} +#[test] +fn test_eq() { + observability::test_run().ok(); + let mut g = utils::random_generator(); -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct EqFact { - context: String, - op: EqOp, - constant: T, - _phantom: PhantomData, -} + let eq1 = vec(eq(1)); -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum EqOp { - Equal, - NotEqual, -} + let ones = eq1.clone().build(&mut g); + eq1.clone().check(&ones).unwrap(); -impl<'a, T> Fact<'a, T> for EqFact -where - T: Bounds<'a> + PartialEq + Clone, -{ - #[tracing::instrument(fields(fact = "eq"), skip(self, g))] - fn mutate(&mut self, g: &mut Generator<'a>, mut obj: T) -> Mutation { - let constant = self.constant.clone(); - match self.op { - EqOp::Equal => { - if obj != constant { - g.fail(format!( - "{}: expected {:?} == {:?}", - self.context, obj, constant - ))?; - obj = constant; - } - } - EqOp::NotEqual => loop { - if obj != constant { - break; - } - obj = g.arbitrary(format!( - "{}: expected {:?} != {:?}", - self.context, obj, constant - ))?; - }, - } - Ok(obj) - } + assert!(ones.iter().all(|x| *x == 1)); } diff --git a/src/facts/in_range.rs b/src/facts/in_range.rs index c3b4e3a..dbd7f0d 100644 --- a/src/facts/in_range.rs +++ b/src/facts/in_range.rs @@ -3,88 +3,28 @@ use std::ops::{Bound, RangeBounds}; use super::*; /// Specifies a range constraint -pub fn in_range(context: S, range: R) -> InRangeFact +pub fn in_range<'a, R, T>(context: impl ToString, range: R) -> Lambda<'a, (), T> where - S: ToString, - R: RangeBounds + std::fmt::Debug, - T: PartialEq + R: 'a + Send + Sync + RangeBounds + std::fmt::Debug, + T: Target<'a> + PartialOrd + Ord - + Clone - + std::fmt::Debug + num::traits::Euclid + std::ops::Add + std::ops::Sub + num::Bounded + num::One, { - InRangeFact { - context: context.to_string(), - range, - phantom: PhantomData, - } -} - -/// Specifies a range constraint -pub fn in_range_(range: R) -> InRangeFact -where - R: RangeBounds + std::fmt::Debug, - T: PartialEq - + PartialOrd - + Ord - + Clone - + std::fmt::Debug - + num::traits::Euclid - + std::ops::Add - + std::ops::Sub - + num::Bounded - + num::One, -{ - in_range("in_range", range) -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct InRangeFact -where - R: RangeBounds + std::fmt::Debug, - T: PartialEq - + PartialOrd - + Ord - + Clone - + std::fmt::Debug - + num::traits::Euclid - + std::ops::Add - + std::ops::Sub - + num::Bounded - + num::One, -{ - context: String, - range: R, - phantom: PhantomData, -} - -impl<'a, R, T> Fact<'a, T> for InRangeFact -where - R: Send + Sync + RangeBounds + std::fmt::Debug + Clone, - T: Bounds<'a> - + PartialEq - + PartialOrd - + Ord - + Clone - + std::fmt::Debug - + num::traits::Euclid - + std::ops::Add - + std::ops::Sub - + num::Bounded - + num::One, -{ - fn mutate(&mut self, g: &mut Generator<'a>, mut obj: T) -> Mutation { - if !self.range.contains(&obj) { - let rand = g.arbitrary(format!( - "{}: expected {:?} to be contained in {:?}", - self.context, obj, self.range - ))?; - obj = match (self.range.start_bound(), self.range.end_bound()) { + let context = context.to_string(); + lambda_unit("in_range", move |g, mut t| { + if !range.contains(&t) { + let rand = g.arbitrary(|| { + format!( + "{}: expected {:?} to be contained in {:?}", + context, t, range + ) + })?; + t = match (range.start_bound(), range.end_bound()) { (Bound::Unbounded, Bound::Unbounded) => rand, (Bound::Included(a), Bound::Included(b)) if b.clone() - a.clone() >= T::one() => { a.clone() + rand.rem_euclid(&(b.clone() - a.clone())) @@ -101,9 +41,39 @@ where (Bound::Included(a), Bound::Unbounded) => { a.clone() + rand.rem_euclid(&(T::max_value() - a.clone())) } - _ => panic!("Range not yet supported, sorry! {:?}", self.range), + _ => panic!("Range not yet supported, sorry! {:?}", range), }; } - Ok(obj) - } + Ok(t) + }) +} + +#[test] +fn test_in_range() { + observability::test_run().ok(); + let mut g = utils::random_generator(); + + let positive1 = in_range("must be positive", 1..=i32::MAX); + let positive2 = in_range("must be positive", 1..); + let smallish = in_range("must be small in magnitude", -10..100); + let over9000 = in_range("must be over 9000", 9001..); + let under9000 = in_range("must be under 9000 (and no less than zero)", ..9000u32); + + let nonpositive1 = vec(not(positive1)); + let nonpositive2 = vec(not(positive2)); + + let smallish_nums = smallish.clone().build(&mut g); + let over9000_nums = over9000.clone().build(&mut g); + let under9000_nums = under9000.clone().build(&mut g); + let nonpositive1_nums = nonpositive1.clone().build(&mut g); + let nonpositive2_nums = nonpositive2.clone().build(&mut g); + + dbg!(&under9000_nums); + + smallish.clone().check(&smallish_nums).unwrap(); + over9000.clone().check(&over9000_nums).unwrap(); + under9000.clone().check(&under9000_nums).unwrap(); + nonpositive1.clone().check(&nonpositive1_nums).unwrap(); + nonpositive2.clone().check(&nonpositive2_nums).unwrap(); + assert!(nonpositive1_nums.iter().all(|x| *x <= 0)); } diff --git a/src/facts/in_slice.rs b/src/facts/in_slice.rs index 92cd417..e989f57 100644 --- a/src/facts/in_slice.rs +++ b/src/facts/in_slice.rs @@ -1,52 +1,30 @@ use super::*; /// Specifies a membership constraint -pub fn in_slice<'a, S, T>(context: S, slice: &'a [T]) -> InSliceFact<'a, T> +pub fn in_slice<'a, T>(context: impl ToString, slice: &'a [T]) -> LambdaUnit<'a, T> where - S: ToString, - T: 'a + PartialEq + std::fmt::Debug + Clone, + T: Target<'a> + PartialEq + Clone, { - InSliceFact { - context: context.to_string(), - slice, - } + let context = context.to_string(); + lambda_unit("in_slice", move |g, t| { + Ok(if !slice.contains(&t) { + let reason = || { + format!( + "{}: expected {:?} to be contained in {:?}", + context, t, slice + ) + }; + g.choose(slice, reason)?.to_owned() + } else { + t + }) + }) } /// Specifies a membership constraint -pub fn in_slice_<'a, T>(slice: &'a [T]) -> InSliceFact<'a, T> +pub fn in_slice_<'a, T>(slice: &'a [T]) -> LambdaUnit<'a, T> where - T: 'a + PartialEq + std::fmt::Debug + Clone, + T: Target<'a> + PartialEq + Clone, { in_slice("in_slice", slice) } - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct InSliceFact<'a, T> -where - T: 'a + PartialEq + std::fmt::Debug + Clone, - // I: Iterator, -{ - context: String, - slice: &'a [T], -} - -impl<'a, 'b: 'a, T> Fact<'a, T> for InSliceFact<'b, T> -where - T: 'b + Bounds<'a> + Clone, - // I: Iterator, -{ - fn mutate(&mut self, g: &mut Generator<'a>, obj: T) -> Mutation { - Ok(if !self.slice.contains(&obj) { - g.choose( - self.slice, - format!( - "{}: expected {:?} to be contained in {:?}", - self.context, obj, self.slice - ), - )? - .to_owned() - } else { - obj - }) - } -} diff --git a/src/facts/lambda.rs b/src/facts/lambda.rs deleted file mode 100644 index deebec4..0000000 --- a/src/facts/lambda.rs +++ /dev/null @@ -1,79 +0,0 @@ -use std::marker::PhantomData; - -use crate::*; - -/// Create a fact from a bare function which specifies the mutation. -/// Can be quicker to experiment with ideas this way than to have to directly implement -/// the [`Fact`] trait -/// -/// ``` -/// use contrafact::*; -/// let mut g = utils::random_generator(); -/// -/// let mut fact = vec_of_length( -/// 4, -/// lambda(2, move |g, s, mut v| { -/// g.set(&mut v, s, "value is not geometrically increasing by 2")?; -/// *s *= 2; -/// Ok(v) -/// }), -/// ); -/// -/// let list = fact.clone().build(&mut g); -/// fact.check(&list).unwrap(); -/// assert_eq!(list, vec![2, 4, 8, 16]); -/// ``` -pub fn lambda<'a, T, S, F>(state: S, f: F) -> LambdaFact<'a, T, S, F> -where - S: Clone + Send + Sync, - T: Bounds<'a>, - F: Clone + Send + Sync + FnMut(&mut Generator, &mut S, T) -> Mutation, -{ - LambdaFact { - state, - fun: f, - _phantom: PhantomData, - } -} - -#[derive(Clone)] -pub struct LambdaFact<'a, T, S, F> -where - S: Clone + Send + Sync, - T: Bounds<'a>, - F: Clone + Send + Sync + FnMut(&mut Generator, &mut S, T) -> Mutation, -{ - state: S, - fun: F, - _phantom: PhantomData<&'a T>, -} - -impl<'a, T, S, F> Fact<'a, T> for LambdaFact<'a, T, S, F> -where - S: Clone + Send + Sync, - T: Bounds<'a>, - F: Clone + Send + Sync + FnMut(&mut Generator, &mut S, T) -> Mutation, -{ - fn mutate(&mut self, g: &mut Generator<'a>, obj: T) -> Mutation { - (self.fun)(g, &mut self.state, obj) - } -} - -#[test] -fn test_lambda_fact() { - use crate::facts::*; - let mut g = utils::random_generator(); - - let fact = vec_of_length( - 4, - lambda(2, move |g, s, mut v| { - g.set(&mut v, s, "value is not geometrically increasing by 2")?; - *s *= 2; - Ok(v) - }), - ); - - let list = fact.clone().build(&mut g); - fact.check(&list).unwrap(); - assert_eq!(list, vec![2, 4, 8, 16]); -} diff --git a/src/facts/lens.rs b/src/facts/lens.rs index cd926f4..77758c7 100644 --- a/src/facts/lens.rs +++ b/src/facts/lens.rs @@ -1,8 +1,7 @@ -use std::{marker::PhantomData, sync::Arc}; - use crate::*; -/// Lifts a Fact about a subset of some data into a Fact about the superset. +/// Lifts a Fact about a subset of some data into a Fact about the superset, +/// using a single function to specify a getter/setter pair. /// /// In other words, if type `O` contains a `T`, and you have a `Fact<'a, T>`, /// `LensFact` lets you lift that fact into a `Fact<'a, O>`. @@ -24,7 +23,7 @@ use crate::*; /// y: u32, /// } /// -/// let mut fact = lens("S::x", |s: &mut S| &mut s.x, eq("must be 1", 1)); +/// let mut fact = lens1("S::x", |s: &mut S| &mut s.x, eq(1)); /// /// assert!(fact.clone().check(&S {x: 1, y: 333}).is_ok()); /// assert!(fact.clone().check(&S {x: 2, y: 333}).is_err()); @@ -35,84 +34,51 @@ use crate::*; /// ``` // // TODO: can rewrite this in terms of PrismFact for DRYness -pub fn lens<'a, O, T, F, L, S>(label: S, lens: L, inner_fact: F) -> LensFact<'a, O, T, F> +pub fn lens1<'a, O, T, L>( + label: impl ToString, + accessor: L, + inner_fact: impl Fact<'a, T>, +) -> impl Fact<'a, O> where - O: Bounds<'a>, - T: Bounds<'a> + Clone, - S: ToString, - F: Fact<'a, T>, + O: Target<'a>, + T: Target<'a>, L: 'a + Clone + Send + Sync + Fn(&mut O) -> &mut T, { - let lens2 = lens.clone(); - let getter = move |mut o| lens(&mut o).clone(); + let accessor2 = accessor.clone(); + let getter = move |mut o| accessor(&mut o).clone(); let setter = move |mut o, t: T| { - let r = lens2(&mut o); + let r = accessor2(&mut o); *r = t; o }; - LensFact::new(label.to_string(), getter, setter, inner_fact) -} - -/// A fact which uses a lens to apply another fact. Use [`lens()`] to construct. -#[derive(Clone)] -pub struct LensFact<'a, O, T, F> -where - T: Bounds<'a>, - O: Bounds<'a>, - F: Fact<'a, T>, -{ - label: String, - - getter: Arc T>, - setter: Arc O>, - - /// The inner_fact about the inner substructure - inner_fact: F, - - __phantom: PhantomData<&'a F>, + lens2(label, getter, setter, inner_fact).labeled("lens1") } -impl<'a, O, T, F> LensFact<'a, O, T, F> -where - T: Bounds<'a>, - O: Bounds<'a>, - F: Fact<'a, T>, -{ - /// Constructor. Supply a lens and an existing Fact to create a new Fact. - pub fn new(label: L, getter: G, setter: S, inner_fact: F) -> Self - where - T: Bounds<'a>, - O: Bounds<'a>, - F: Fact<'a, T>, - L: ToString, - G: 'a + Send + Sync + Fn(O) -> T, - S: 'a + Send + Sync + Fn(O, T) -> O, - { - Self { - label: label.to_string(), - getter: Arc::new(getter), - setter: Arc::new(setter), - inner_fact, - __phantom: PhantomData, - } - } -} - -impl<'a, O, T, F> Fact<'a, O> for LensFact<'a, O, T, F> +/// Lifts a Fact about a subset of some data into a Fact about the superset, using +/// explicit getter and setter functions. +/// +/// This is a more general version of [`lens1`]. This can be useful particularly +/// when the setter requires modifications other than replacing the item specified +/// by the getter, for instance if your data contains some kind of digest of the data +/// being focused on, then the digest must also be recomputed when the focus is modified. +pub fn lens2<'a, O, T>( + label: impl ToString, + getter: impl 'a + Clone + Send + Sync + Fn(O) -> T, + setter: impl 'a + Clone + Send + Sync + Fn(O, T) -> O, + inner_fact: impl Fact<'a, T>, +) -> impl Fact<'a, O> where - T: Bounds<'a>, - O: Bounds<'a> + Clone, - F: Fact<'a, T>, + O: Target<'a>, + T: Target<'a>, { - #[tracing::instrument(fields(fact = "lens"), skip(self, g))] - fn mutate(&mut self, g: &mut Generator<'a>, obj: O) -> Mutation { - let t = (self.getter)(obj.clone()); - let t = self - .inner_fact + let label = label.to_string(); + lambda("lens", inner_fact, move |g, fact, o: O| { + let t = getter(o.clone()); + let t = fact .mutate(g, t) - .map_check_err(|err| format!("lens({}) > {}", self.label, err))?; - Ok((self.setter)(obj, t)) - } + .map_check_err(|err| format!("lens1({}) > {}", label, err))?; + Ok(setter(o, t)) + }) } #[cfg(test)] @@ -132,7 +98,7 @@ mod tests { observability::test_run().ok(); let mut g = utils::random_generator(); - let f = || vec(lens("S::x", |s: &mut S| &mut s.x, eq("must be 1", 1))); + let f = || vec(lens1("S::x", |s: &mut S| &mut s.x, eq(1))); let ones = f().build(&mut g); f().check(&ones).unwrap(); diff --git a/src/facts/mapped.rs b/src/facts/mapped.rs deleted file mode 100644 index 2717199..0000000 --- a/src/facts/mapped.rs +++ /dev/null @@ -1,151 +0,0 @@ -use std::sync::Arc; - -use crate::{fact::Bounds, *}; - -/// A version of [`mapped`] whose closure returns a Result -pub fn mapped_fallible<'a, T, F, O, S>(reason: S, f: F) -> MappedFact<'a, T, O> -where - S: ToString, - T: Bounds<'a>, - O: Fact<'a, T>, - F: 'a + Send + Sync + Fn(&T) -> Mutation, -{ - MappedFact::new(reason.to_string(), f) -} - -/// A fact which is defined based on the data to which it is applied. It maps -/// the data into a fact to be applied. -/// -/// This can be useful for "piecewise" functions, where the -/// constraint is fundamentally different depending on the shape of the data, -/// or when wanting to set some subset of data to match some other subset of -/// data, without caring what the value actually is, and without having to -/// explicitly construct the value. -/// -/// **NOTE**: since the returned Facts are generated brand-new on-the-fly, -/// these Facts must be stateless. State changes cannot be carried over to -/// subsequent calls when running over a sequence. -/// (TODO: add `StatelessFact` trait to give type-level protection here.) -/// -/// ``` -/// use contrafact::*; -/// -/// // This contrived fact reads: -/// // "if the number is greater than 9000, -/// // ensure that it's also divisible by 9, -/// // and otherwise, ensure that it's divisible by 10" -/// let mut fact = mapped("reason", |n: &u32| { -/// if *n > 9000 { -/// facts![ brute("divisible by 9", |n| *n % 9 == 0) ] -/// } else { -/// facts![ brute("divisible by 10", |n| *n % 10 == 0) ] -/// } -/// }); -/// -/// assert!(fact.clone().check(&50).is_ok()); -/// assert!(fact.clone().check(&99).is_err()); -/// assert!(fact.clone().check(&9009).is_ok()); -/// assert!(fact.clone().check(&9010).is_err()); -/// ``` -pub fn mapped<'a, T, F, O, S>(reason: S, f: F) -> MappedFact<'a, T, O> -where - S: ToString, - T: Bounds<'a>, - O: Fact<'a, T>, - F: 'a + Send + Sync + Fn(&T) -> O, -{ - MappedFact::new(reason.to_string(), move |x| Ok(f(x))) -} - -/// A fact which is mapped from the data to be checked/mutated. -/// Use [`mapped`] to construct. -#[derive(Clone)] -pub struct MappedFact<'a, T, O> { - reason: String, - f: Arc Mutation>, -} - -impl<'a, T, O> Fact<'a, T> for MappedFact<'a, T, O> -where - T: Bounds<'a>, - O: Fact<'a, T>, -{ - #[tracing::instrument(fields(fact = "mapped"), skip(self, g))] - fn mutate(&mut self, g: &mut Generator<'a>, obj: T) -> Mutation { - (self.f)(&obj)? - .mutate(g, obj) - .map_check_err(|err| format!("mapped({}) > {}", self.reason, err)) - } -} - -impl<'a, T, O> MappedFact<'a, T, O> { - pub(crate) fn new Mutation>(reason: String, f: F) -> Self { - Self { - reason, - f: Arc::new(f), - } - } -} - -#[test] -fn test_mapped_fact() { - use crate::facts::*; - - type T = (u8, u8); - - let numbers = vec![(1, 11), (2, 22), (3, 33), (4, 44)]; - - // This fact says: - // if the first element of the tuple is even, - // then the second element must be divisible by 3; - // and if the first element is odd, - // then the second element must be divisible by 4. - let divisibility_fact = || { - mapped("reason", |t: &T| { - lens( - "T.1", - |(_, n)| n, - if t.0 % 2 == 0 { - brute("divisible by 3", |n: &u8| n % 3 == 0) - } else { - brute("divisible by 4", |n: &u8| n % 4 == 0) - }, - ) - }) - }; - - // assert that there was a failure - vec(divisibility_fact()) - .check(&numbers) - .result() - .unwrap() - .unwrap_err(); - - // TODO: return all errors in the seq, not just the first - // assert_eq!( - // dbg!(vec(divisibility_fact()) - // .check(&numbers) - // .result() - // .unwrap() - // .unwrap_err()), - // vec![ - // "item 0: mapped(reason) > lens(T.1) > divisible by 4".to_string(), - // "item 1: mapped(reason) > lens(T.1) > divisible by 3".to_string(), - // "item 2: mapped(reason) > lens(T.1) > divisible by 4".to_string(), - // "item 3: mapped(reason) > lens(T.1) > divisible by 3".to_string(), - // ] - // ); - - let mut g = utils::random_generator(); - - let composite_fact = || { - vec(facts![ - lens("T.0", |(i, _)| i, consecutive_int("increasing", 0)), - divisibility_fact(), - ]) - }; - - let built = composite_fact().build(&mut g); - dbg!(&built); - composite_fact().check(&built).unwrap(); -} diff --git a/src/facts/not.rs b/src/facts/not.rs index ff4dd2c..b71696f 100644 --- a/src/facts/not.rs +++ b/src/facts/not.rs @@ -4,47 +4,36 @@ use super::*; // TODO: `not` in particular would really benefit from Facts having accessible // labels, since currently you can only get context about why a `not` fact passed, // not why it fails. -pub fn not<'a, F, S, T>(context: S, fact: F) -> NotFact<'a, F, T> +pub fn not<'a, T>(fact: LambdaUnit<'a, T>) -> LambdaUnit<'a, T> where - S: ToString, - F: Fact<'a, T>, - T: Bounds<'a>, + T: Target<'a>, { - NotFact { - context: context.to_string(), - fact, - _phantom: PhantomData, - } + lambda_unit("not", move |g, t| { + let label = format!("not({:?})", fact); + let fact = fact.clone(); + brute(label, move |o| fact.clone().check(o).is_err()).mutate(g, t) + }) } -/// Negates a fact, with no context given -pub fn not_<'a, F, T>(fact: F) -> NotFact<'a, F, T> -where - F: Fact<'a, T>, - T: Bounds<'a>, -{ - not("not", fact) -} +// /// Negates a fact, with no context given +// pub fn not<'a, F, T>(fact: F) -> Fact<'a, (), T> +// where +// F: 'a + Fact<'a, T>, +// T: Bounds<'a>, +// { +// not("not", fact) +// } -#[derive(Debug, Clone)] -pub struct NotFact<'a, F, T> -where - F: Fact<'a, T>, - T: Bounds<'a>, -{ - context: String, - fact: F, - _phantom: PhantomData<&'a T>, -} +#[test] +fn test_not() { + observability::test_run().ok(); + let mut g = utils::random_generator(); -impl<'a, F, T> Fact<'a, T> for NotFact<'a, F, T> -where - F: Fact<'a, T> + 'a, - T: Bounds<'a>, -{ - fn mutate(&mut self, g: &mut Generator<'a>, obj: T) -> Mutation { - let label = format!("not({})", self.context.clone()); - let fact = self.fact.clone(); - brute(label, move |o| fact.clone().check(o).is_err()).mutate(g, obj) - } + let eq1 = eq(1); + let not1 = vec(not(eq1)); + + let nums = not1.clone().build(&mut g); + not1.clone().check(&nums).unwrap(); + + assert!(nums.iter().all(|x| *x != 1)); } diff --git a/src/facts/optical.rs b/src/facts/optical.rs index 18cd12b..e9e2bb3 100644 --- a/src/facts/optical.rs +++ b/src/facts/optical.rs @@ -3,7 +3,7 @@ use std::marker::PhantomData; use arbitrary::Unstructured; use lens_rs::*; -use crate::{fact::Bounds, Fact, *}; +use crate::{fact::Target, Fact, *}; pub fn optical<'a, Src, Img, Optics, F, L>( label: L, @@ -11,8 +11,8 @@ pub fn optical<'a, Src, Img, Optics, F, L>( inner_fact: F, ) -> OpticalFact<'a, Src, Img, Optics, F> where - Src: Bounds<'a> + Lens, - Img: Bounds<'a> + Clone, + Src: Target<'a> + Lens, + Img: Target<'a>, Optics: Clone + std::fmt::Debug, F: Fact<'a, Img>, L: ToString, @@ -20,12 +20,12 @@ where OpticalFact::new(label.to_string(), optics, inner_fact) } -/// A fact which uses a lens to apply another fact. Use [`lens()`] to construct. +/// A fact which uses a lens to apply another fact. Use [`lens1()`] to construct. #[derive(Clone)] pub struct OpticalFact<'a, Src, Img, Optics, F> where - Src: Bounds<'a> + Lens, - Img: Bounds<'a> + Clone, + Src: Target<'a> + Lens, + Img: Target<'a>, Optics: Clone + std::fmt::Debug, F: Fact<'a, Img>, { @@ -41,8 +41,8 @@ where impl<'a, Src, Img, Optics, F> OpticalFact<'a, Src, Img, Optics, F> where - Src: Bounds<'a> + Lens, - Img: Bounds<'a> + Clone, + Src: Target<'a> + Lens, + Img: Target<'a>, Optics: Clone + std::fmt::Debug, F: Fact<'a, Img>, { @@ -58,15 +58,15 @@ where impl<'a, Src, Img, Optics, F> Fact<'a, Src> for OpticalFact<'a, Src, Img, Optics, F> where - Src: Bounds<'a> + Lens, - Img: Bounds<'a> + Clone, + Src: Target<'a> + Lens, + Img: Target<'a>, Optics: Clone + std::fmt::Debug, F: Fact<'a, Img>, { // TODO: remove #[tracing::instrument(skip(self))] - fn check(&mut self, obj: &Src) -> Check { - let imgs = obj.traverse_ref(self.optics.clone()); + fn check(&mut self, t: &Src) -> Check { + let imgs = t.traverse_ref(self.optics.clone()); imgs.iter() .enumerate() .flat_map(|(i, img)| { @@ -78,40 +78,40 @@ where self.inner_fact .check(img) - .map(|err| format!("lens({}){{{:?}}} > {}", label, self.optics.clone(), err)) + .map(|err| format!("lens1({}){{{:?}}} > {}", label, self.optics.clone(), err)) }) .collect::>() .into() } - fn mutate(&mut self, mut obj: Src, g: &mut Generator<'a>) -> Src { - for img in obj.traverse_mut(self.optics.clone()) { + fn mutate(&mut self, mut t: Src, g: &mut Generator<'a>) -> Src { + for img in t.traverse_mut(self.optics.clone()) { *img = self.inner_fact.mutate(img.clone(), g); } - obj + t } #[tracing::instrument(skip(self))] - fn advance(&mut self, obj: &Src) { - for img in obj.traverse_ref(self.optics.clone()) { + fn advance(&mut self, t: &Src) { + for img in t.traverse_ref(self.optics.clone()) { self.inner_fact.advance(img); } } } #[test] -fn test_lens() { +fn test_lens1() { let x = (1u8, (2u8, (3u8, 4u8))); let mut fact = OpticalFact { label: "".into(), optics: optics!(_1._1._1), - inner_fact: eq_(3), + inner_fact: eq(3), __phantom: PhantomData::<&((u8, (u8, (u8, u8))), u8)>, }; assert_eq!(fact.check(&x).errors().len(), 1); - fact.inner_fact = eq_(4); + fact.inner_fact = eq(4); assert!(fact.check(&x).is_ok()); } diff --git a/src/facts/or.rs b/src/facts/or.rs index 8d2693b..f42299a 100644 --- a/src/facts/or.rs +++ b/src/facts/or.rs @@ -1,64 +1,48 @@ use super::*; /// Combines two constraints so that either one may be satisfied -pub fn or<'a, A, T, S, Item>(context: S, a: A, b: T) -> OrFact<'a, A, T, Item> +pub fn or<'a, T>(a: impl Fact<'a, T>, b: impl Fact<'a, T>) -> impl Fact<'a, T> where - S: ToString, - A: Fact<'a, Item>, - T: Fact<'a, Item>, - Item: Bounds<'a>, + T: Target<'a>, { - OrFact { - context: context.to_string(), - a, - b, - _phantom: PhantomData, - } -} - -/// Fact that combines two `Fact`s, returning the OR of the results. -/// -/// This is created by the `or` function. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct OrFact<'a, M1, M2, Item> -where - M1: Fact<'a, Item>, - M2: Fact<'a, Item>, - Item: ?Sized + Bounds<'a>, -{ - context: String, - pub(crate) a: M1, - pub(crate) b: M2, - _phantom: PhantomData<&'a Item>, -} - -impl<'a, P1, P2, T> Fact<'a, T> for OrFact<'a, P1, P2, T> -where - P1: Fact<'a, T> + Fact<'a, T>, - P2: Fact<'a, T> + Fact<'a, T>, - T: Bounds<'a>, -{ - fn mutate(&mut self, g: &mut Generator<'a>, obj: T) -> Mutation { + lambda("or", (a, b), |g, (a, b), t| { use rand::{thread_rng, Rng}; - let a = check_raw(&mut self.a, &obj).is_ok(); - let b = check_raw(&mut self.b, &obj).is_ok(); - match (a, b) { - (true, _) => Ok(obj), - (_, true) => Ok(obj), + let a_ok = a.clone().check(&t).is_ok(); + let b_ok = b.clone().check(&t).is_ok(); + match (a_ok, b_ok) { + (true, _) => Ok(t), + (_, true) => Ok(t), (false, false) => { g.fail(format!( - "expected either one of the following conditions to be met: - condition 1: {:#?} - condition 2: {:#?}", + "expected either one of the following conditions to be met: {:?} OR {:?}", a, b ))?; if thread_rng().gen::() { - self.a.mutate(g, obj) + a.mutate(g, t) } else { - self.b.mutate(g, obj) + b.mutate(g, t) } } } - } + }) +} + +#[test] +fn test_or() { + observability::test_run().ok(); + let mut g = utils::random_generator(); + + let eq1 = eq(1); + let eq2 = eq(2); + let either = or(eq1, eq2); + + let ones = vec(either.clone()).build(&mut g); + vec(either.clone()).check(&ones).unwrap(); + assert!(ones.iter().all(|x| *x == 1 || *x == 2)); + + assert_eq!( + dbg!(either.check(&3)).result().unwrap().unwrap_err().len(), + 1 + ); } diff --git a/src/facts/prism.rs b/src/facts/prism.rs index 123dee1..8c0f982 100644 --- a/src/facts/prism.rs +++ b/src/facts/prism.rs @@ -1,5 +1,3 @@ -use std::{marker::PhantomData, sync::Arc}; - use crate::*; /// Lifts a Fact about some *optional* subset of data into a Fact about the @@ -42,7 +40,7 @@ use crate::*; /// } /// } /// -/// let mut fact = prism("E::x", E::x, eq("must be 1", 1)); +/// let mut fact = prism("E::x", E::x, eq(1)); /// /// assert!(fact.clone().check(&E::X(1)).is_ok()); /// assert!(fact.clone().check(&E::X(2)).is_err()); @@ -59,85 +57,25 @@ use crate::*; /// The `prism` closure is a rather lazy way to provide a prism in the /// traditional optics sense. We may consider using a true lens library for /// this in the future. -pub fn prism<'a, O, T, F, P, S>(label: S, prism: P, inner_fact: F) -> PrismFact<'a, O, T, F> +pub fn prism<'a, O, T, P>( + label: impl ToString, + prism: P, + inner_fact: impl Fact<'a, T>, +) -> impl Fact<'a, O> where - O: Bounds<'a>, - S: ToString, - T: Bounds<'a> + Clone, - F: Fact<'a, T>, + O: Target<'a>, + T: Target<'a>, P: 'a + Send + Sync + Fn(&mut O) -> Option<&mut T>, { - // let getter = |o| prism(&mut o).cloned(); - // let setter = |o, t| { - // let some = if let Some(i) = prism(&mut o) { - // *i = t; - // true - // } else { - // false - // }; - // some.then_some(o) - // }; - PrismFact::new(label.to_string(), prism, inner_fact) -} - -/// A fact which uses a prism to apply another fact. Use [`prism()`] to construct. -#[derive(Clone)] -pub struct PrismFact<'a, O, T, F> -where - T: Bounds<'a>, - O: Bounds<'a>, - F: Fact<'a, T>, -{ - label: String, - // getter: Arc Option>, - // setter: Arc Option>, - prism: Arc Option<&mut T>>, - inner_fact: F, - __phantom: PhantomData<&'a F>, -} - -impl<'a, O, T, F> PrismFact<'a, O, T, F> -where - T: Bounds<'a>, - O: Bounds<'a>, - F: Fact<'a, T>, -{ - /// Constructor. Supply a prism and an existing Fact to create a new Fact. - pub fn new

(label: String, prism: P, /*getter: G, setter: S,*/ inner_fact: F) -> Self - where - T: Bounds<'a>, - O: Bounds<'a>, - F: Fact<'a, T>, - P: 'a + Send + Sync + Fn(&mut O) -> Option<&mut T>, - // G: Fn(O) -> Option, - // S: Fn(O, T) -> Option, - { - Self { - label, - inner_fact, - prism: Arc::new(prism), - // getter: Arc::new(getter), - // setter: Arc::new(setter), - __phantom: PhantomData, - } - } -} - -impl<'a, O, T, F> Fact<'a, O> for PrismFact<'a, O, T, F> -where - T: Bounds<'a> + Clone, - O: Bounds<'a>, - F: Fact<'a, T>, -{ - fn mutate(&mut self, g: &mut Generator<'a>, mut obj: O) -> Mutation { - if let Some(t) = (self.prism)(&mut obj) { - *t = self - .inner_fact + let label = label.to_string(); + lambda("prism", inner_fact, move |g, fact, mut t| { + if let Some(t) = prism(&mut t) { + *t = fact .mutate(g, t.clone()) - .map_check_err(|err| format!("prism({}) > {}", self.label, err))?; + .map_check_err(|err| format!("prism({}) > {}", label, err))?; } - Ok(obj) - } + Ok(t) + }) } #[cfg(test)] @@ -174,8 +112,8 @@ mod tests { let f = || { facts::vec(facts![ - prism("E::x", E::x, facts::eq("must be 1", 1)), - prism("E::y", E::y, facts::eq("must be 2", 2)), + prism("E::x", E::x, facts::eq(1)), + prism("E::y", E::y, facts::eq(2)), ]) }; diff --git a/src/facts/same.rs b/src/facts/same.rs index 4fc2c1f..ea19405 100644 --- a/src/facts/same.rs +++ b/src/facts/same.rs @@ -1,56 +1,50 @@ -use super::*; +use super::{brute::brute_labeled, *}; /// Specifies an equality constraint between two items in a tuple -pub fn same() -> SameFact +pub fn same<'a, T>() -> LambdaUnit<'a, (T, T)> where - T: std::fmt::Debug + PartialEq, + T: Target<'a> + PartialEq, { - SameFact { - op: EqOp::Equal, - _phantom: PhantomData, - } + lambda_unit("same", |g, mut t: (T, T)| { + let o = t.clone(); + let reason = move || format!("must be same: expected {:?} == {:?}", o.0.clone(), o.1); + g.set(&mut t.0, &t.1, reason)?; + Ok(t) + }) } /// Specifies an inequality constraint between two items in a tuple -pub fn different() -> SameFact +pub fn different<'a, T>() -> LambdaUnit<'a, (T, T)> where - T: std::fmt::Debug + PartialEq, + T: Target<'a> + PartialEq, { - SameFact { - op: EqOp::NotEqual, - _phantom: PhantomData, - } + brute_labeled(|(a, b)| { + if a == b { + Ok(Err(format!( + "must be different: expected {:?} != {:?}", + a, b + ))) + } else { + Ok(Ok(())) + } + }) } -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SameFact { - op: EqOp, - _phantom: PhantomData, -} +#[test] +fn test_same() { + observability::test_run().ok(); + let mut g = utils::random_generator(); -impl<'a, T> Fact<'a, (T, T)> for SameFact -where - T: Bounds<'a> + PartialEq + Clone, -{ - #[tracing::instrument(fields(fact = "same"), skip(self, g))] - fn mutate(&mut self, g: &mut Generator<'a>, mut obj: (T, T)) -> Mutation<(T, T)> { - match self.op { - EqOp::Equal => { - if obj.0 != obj.1 { - g.fail(format!("must be same: expected {:?} == {:?}", obj.0, obj.1))?; - obj.0 = obj.1.clone(); - } - } - EqOp::NotEqual => loop { - if obj.0 != obj.1 { - break; - } - obj.0 = g.arbitrary(format!( - "must be different: expected {:?} != {:?}", - obj.0, obj.1 - ))?; - }, - } - Ok(obj) + { + let f = vec(same::()); + let nums = f.clone().build(&mut g); + f.clone().check(&nums).unwrap(); + assert!(nums.iter().all(|(a, b)| a == b)); + } + { + let f = vec(different::()); + let nums = f.clone().build(&mut g); + f.clone().check(&nums).unwrap(); + assert!(nums.iter().all(|(a, b)| a != b)); } } diff --git a/src/facts/seq.rs b/src/facts/seq.rs index 41d1b60..d5440e4 100644 --- a/src/facts/seq.rs +++ b/src/facts/seq.rs @@ -5,8 +5,6 @@ //! of internally inconsistent facts, then the facts must be "rolled back" for the next //! `satisfy()` attempt. -use std::marker::PhantomData; - use crate::*; use super::and; @@ -20,7 +18,7 @@ use super::and; /// let mut g = utils::random_generator(); /// /// // `consecutive_int` -/// let fact = facts::vec(facts::eq_(1)); +/// let fact = facts::vec(facts::eq(1)); /// let list = fact.clone().satisfy(&mut g, vec![0; 5]).unwrap(); /// assert_eq!(list, vec![1, 1, 1, 1, 1]); /// ``` @@ -37,124 +35,54 @@ use super::and; /// let list = fact.clone().satisfy(&mut g, vec![0; 5]).unwrap(); /// assert_eq!(list, vec![0, 1, 2, 3, 4]); /// ``` -pub fn vec<'a, T, F>(inner_fact: F) -> VecFact<'a, T, F> -where - T: Bounds<'a> + Clone, - F: Fact<'a, T>, -{ - VecFact::new(inner_fact) -} - -/// Checks that a Vec is of a given length -pub fn vec_len<'a, T>(len: usize) -> SeqLenFact<'a, T> -where - T: Bounds<'a> + Clone + 'a, -{ - SeqLenFact::new(len) -} - -/// Combines a LenFact with a VecFact to ensure that the vector is of a given length -pub fn vec_of_length<'a, T, F>(len: usize, inner_fact: F) -> impl Fact<'a, Vec> -where - T: Bounds<'a> + Clone + 'a, - F: Fact<'a, T> + 'a, -{ - and(vec_len(len), vec(inner_fact)) -} - -/// A fact which uses a seq to apply another fact. Use [`vec()`] to construct. -#[derive(Clone)] -pub struct VecFact<'a, T, F> -where - T: Bounds<'a>, - F: Fact<'a, T>, -{ - /// The inner_fact about the inner substructure - inner_fact: F, - - __phantom: PhantomData<&'a T>, -} - -impl<'a, T, F> VecFact<'a, T, F> +pub fn vec<'a, T>(inner_fact: impl Fact<'a, T>) -> impl Fact<'a, Vec> where - T: Bounds<'a>, - F: Fact<'a, T>, + T: Target<'a> + Clone, { - /// Constructor. Supply a seq and an existing Fact to create a new Fact. - pub fn new(inner_fact: F) -> Self - where - T: Bounds<'a>, - F: Fact<'a, T>, - { - Self { - inner_fact, - __phantom: PhantomData, - } - } -} - -impl<'a, T, F> Fact<'a, Vec> for VecFact<'a, T, F> -where - T: Bounds<'a>, - F: Fact<'a, T>, -{ - #[tracing::instrument(fields(fact = "seq"), skip(self, g))] - fn mutate(&mut self, g: &mut Generator<'a>, obj: Vec) -> Mutation> { - tracing::trace!(""); - obj.into_iter() + lambda("vec", inner_fact, |g, f, t: Vec| { + t.into_iter() .enumerate() .map(|(i, o)| { - self.inner_fact - .mutate(g, o) + f.mutate(g, o) .map_check_err(|e| format!("seq[{}]: {}", i, e)) }) .collect::, _>>() - } -} - -/// A fact which uses a seq to apply another fact. Use [`vec()`] to construct. -#[derive(Clone)] -pub struct SeqLenFact<'a, T> -where - T: Bounds<'a>, -{ - len: usize, - __phantom: PhantomData<&'a T>, + }) } -impl<'a, T> SeqLenFact<'a, T> +/// Checks that a Vec is of a given length +pub fn vec_len<'a, T>(len: usize) -> LambdaUnit<'a, Vec> where - T: Bounds<'a>, + T: Target<'a> + Clone + 'a, { - /// Constructor. Supply a seq and an existing Fact to create a new Fact. - pub fn new(len: usize) -> Self - where - T: Bounds<'a>, - { - Self { - len, - __phantom: PhantomData, + lambda_unit("vec_len", move |g, mut t: Vec| { + if t.len() > len { + g.fail(format!( + "vec should be of length {} but is actually of length {}", + len, + t.len() + ))?; + t = t[0..len].to_vec(); } - } + while t.len() < len { + t.push(g.arbitrary(|| { + format!( + "vec should be of length {} but is actually of length {}", + len, + t.len() + ) + })?) + } + Ok(t) + }) } -impl<'a, T> Fact<'a, Vec> for SeqLenFact<'a, T> +/// Combines a LenFact with a VecFact to ensure that the vector is of a given length +pub fn vec_of_length<'a, T>(len: usize, inner_fact: impl Fact<'a, T>) -> impl Fact<'a, Vec> where - T: Bounds<'a>, + T: Target<'a> + 'a, { - #[tracing::instrument(fields(fact = "len"), skip(self, g))] - fn mutate(&mut self, g: &mut Generator<'a>, mut obj: Vec) -> Mutation> { - tracing::trace!(""); - - if obj.len() > self.len { - g.fail("LenFact: vec was too long")?; - obj = obj[0..self.len].to_vec(); - } - while obj.len() < self.len { - obj.push(g.arbitrary("LenFact: vec was too short")?) - } - Ok(obj) - } + and(vec_len(len), vec(inner_fact)) } #[cfg(test)] @@ -180,7 +108,7 @@ mod tests { let f = facts![ brute("len must be >= 3", |v: &Vec<_>| v.len() >= 3), - vec(eq("must be 1", 1)), + vec(eq(1)), ]; let ones = f.clone().build(&mut g); f.check(&ones).unwrap(); @@ -221,12 +149,12 @@ mod tests { let piecewise = move || { let count = Arc::new(AtomicU8::new(0)); - lambda((), move |g, (), mut obj| { + lambda("piecewise", (), move |g, (), mut t| { let c = count.fetch_add(1, Ordering::SeqCst); if c < 3 { - g.set(&mut obj, &999, "i'm being difficult, haha")?; + g.set(&mut t, &999, || "i'm being difficult, haha")?; } - Ok(obj) + Ok(t) }) }; @@ -241,7 +169,7 @@ mod tests { // Assert that piecewise() messes everything up during the // first 3 mutations, and cooperates afterwards { - let mut f = facts!(eq_(0), piecewise()); + let mut f = facts!(eq(0), piecewise()); for _ in 0..3 { let val = f.mutate(&mut g, 0).unwrap(); assert!(f.clone().check(&val).is_err()); diff --git a/src/generator.rs b/src/generator.rs index 0208551..8295363 100644 --- a/src/generator.rs +++ b/src/generator.rs @@ -66,15 +66,15 @@ impl<'a> Generator<'a> { /// When running a Check, fail immediately with this error if the existing value doesn't match. /// During mutation, set the value so that it does match. - pub fn set( + pub fn set( &self, source: &mut T, target: &T, - err: impl ToString, + err: impl FnOnce() -> S, ) -> Mutation<()> { if source != target { if self.check { - return Err(MutationError::Check(err.to_string())); + return Err(MutationError::Check(err().to_string())); } else { *source = target.clone(); } @@ -83,24 +83,27 @@ impl<'a> Generator<'a> { } /// Generate arbitrary data in mutation mode, or produce an error in check mode - pub fn arbitrary>(&mut self, err: impl ToString) -> Mutation { + pub fn arbitrary, S: ToString>( + &mut self, + err: impl FnOnce() -> S, + ) -> Mutation { self.with(err, |u| u.arbitrary()) } /// Choose between specified items in mutation mode, or produce an error in check mode. - pub fn choose>( + pub fn choose, S: ToString>( &mut self, choices: &'a [T], - err: impl ToString, + err: impl FnOnce() -> S, ) -> Mutation<&T> { if choices.is_empty() { - return Err(MutationError::Exception("Empty choices".to_string())).into(); + return Err(MutationError::User("Empty choices".to_string())).into(); } if choices.len() == 1 { return Ok(&choices[0]).into(); } if !self.check && self.arb.is_empty() { - return Err(MutationError::Exception("Ran out of entropy".to_string())).into(); + return Err(MutationError::User("Ran out of entropy".to_string())).into(); } self.with(err, |u| u.choose(choices)) } @@ -111,29 +114,34 @@ impl<'a> Generator<'a> { /// size of a collection to build, that's exactly what I will be doing with /// this, because I'm not sure exactly what the contrafact story is for /// size hints (which is what arbitrary would be using instead). - pub fn int_in_range(&mut self, range: RangeInclusive, err: impl ToString) -> Mutation + pub fn int_in_range( + &mut self, + range: RangeInclusive, + err: impl FnOnce() -> S, + ) -> Mutation where T: Arbitrary<'a> + PartialOrd + Copy + Int, + S: ToString, { if range.start() > range.end() { - return Err(MutationError::Exception("Invalid range".to_string())).into(); + return Err(MutationError::User("Invalid range".to_string())).into(); } else if range.start() == range.end() { return Ok(*range.start()).into(); } if !self.check && self.arb.is_empty() { - return Err(MutationError::Exception("Ran out of entropy".to_string())).into(); + return Err(MutationError::User("Ran out of entropy".to_string())).into(); } self.with(err, |u| u.int_in_range(range)) } /// Call the specified Arbitrary function in mutation mode, or produce an error in check mode. - pub fn with( + pub fn with( &mut self, - err: impl ToString, + err: impl FnOnce() -> S, f: impl FnOnce(&mut Unstructured<'a>) -> Result, ) -> Mutation { if self.check { - Err(MutationError::Check(err.to_string())).into() + Err(MutationError::Check(err().to_string())).into() } else { f(&mut self.arb).map_err(Into::into) } @@ -151,8 +159,8 @@ pub mod test { pub fn test_generator_int_in_range_invalid_range() { let mut gen = crate::generator::Generator::from(&[0, 1, 2, 3, 4, 5][..]); assert_eq!( - gen.int_in_range(5..=4, "error"), - Err(MutationError::Exception("Invalid range".to_string())) + gen.int_in_range(5..=4, || "error"), + Err(MutationError::User("Invalid range".to_string())) ); } @@ -161,7 +169,7 @@ pub mod test { pub fn test_generator_int_in_range_valid_range_single_option() { let mut gen = crate::generator::Generator::from(&[0][..]); for _ in 0..10 { - assert_eq!(gen.int_in_range(5..=5, "error").unwrap(), 5); + assert_eq!(gen.int_in_range(5..=5, || "error").unwrap(), 5); } // The generator has not had any entropy consumed by this point. assert_eq!(gen.len(), 1); @@ -172,13 +180,13 @@ pub mod test { pub fn test_generator_int_in_range_valid_range_multiple_options() { let mut gen = crate::generator::Generator::from(&[0, 1, 2, 3, 4, 5][..]); for i in 0..6 { - assert_eq!(gen.int_in_range(0..=3, "error").unwrap(), i % 4); + assert_eq!(gen.int_in_range(0..=3, || "error").unwrap(), i % 4); } // The generator has no entropy remaining at this point. assert_eq!(gen.len(), 0); assert_eq!( - gen.int_in_range(0..=3, "error"), - Err(MutationError::Exception("Ran out of entropy".to_string())) + gen.int_in_range(0..=3, || "error"), + Err(MutationError::User("Ran out of entropy".to_string())) ); } @@ -188,8 +196,8 @@ pub mod test { let mut gen = crate::generator::Generator::from(&[0, 1, 2, 3, 4, 5][..]); let choices: [usize; 0] = []; assert_eq!( - gen.choose(&choices, "error"), - Err(MutationError::Exception("Empty choices".to_string())) + gen.choose(&choices, || "error"), + Err(MutationError::User("Empty choices".to_string())) ); } @@ -198,7 +206,7 @@ pub mod test { pub fn test_generator_one_choices() { let mut gen = crate::generator::Generator::from(&[0, 1, 2, 3, 4, 5][..]); let choices = [0]; - assert_eq!(gen.choose(&choices, "error").unwrap(), &0); + assert_eq!(gen.choose(&choices, || "error").unwrap(), &0); } /// Test that a generator can be used to choose between two values. @@ -206,17 +214,17 @@ pub mod test { pub fn test_generator_choose_two_values() { let mut gen = crate::generator::Generator::from(&[0, 1, 2, 3, 4, 5][..]); let choices = [0, 1]; - assert_eq!(gen.choose(&choices, "error").unwrap(), &0); - assert_eq!(gen.choose(&choices, "error").unwrap(), &1); - assert_eq!(gen.choose(&choices, "error").unwrap(), &0); - assert_eq!(gen.choose(&choices, "error").unwrap(), &1); - assert_eq!(gen.choose(&choices, "error").unwrap(), &0); - assert_eq!(gen.choose(&choices, "error").unwrap(), &1); + assert_eq!(gen.choose(&choices, || "error").unwrap(), &0); + assert_eq!(gen.choose(&choices, || "error").unwrap(), &1); + assert_eq!(gen.choose(&choices, || "error").unwrap(), &0); + assert_eq!(gen.choose(&choices, || "error").unwrap(), &1); + assert_eq!(gen.choose(&choices, || "error").unwrap(), &0); + assert_eq!(gen.choose(&choices, || "error").unwrap(), &1); // This is the only case where we can't choose a value, because we have 2 choices and 6 bytes. assert_eq!( - gen.choose(&choices, "error"), - Err(MutationError::Exception("Ran out of entropy".to_string())) + gen.choose(&choices, || "error"), + Err(MutationError::User("Ran out of entropy".to_string())) ); } @@ -225,17 +233,17 @@ pub mod test { pub fn test_generator_choose_three_values() { let mut gen = crate::generator::Generator::from(&[0, 1, 2, 3, 4, 5][..]); let choices = [0, 1, 2]; - assert_eq!(gen.choose(&choices, "error").unwrap(), &0); - assert_eq!(gen.choose(&choices, "error").unwrap(), &1); - assert_eq!(gen.choose(&choices, "error").unwrap(), &2); - assert_eq!(gen.choose(&choices, "error").unwrap(), &0); - assert_eq!(gen.choose(&choices, "error").unwrap(), &1); - assert_eq!(gen.choose(&choices, "error").unwrap(), &2); + assert_eq!(gen.choose(&choices, || "error").unwrap(), &0); + assert_eq!(gen.choose(&choices, || "error").unwrap(), &1); + assert_eq!(gen.choose(&choices, || "error").unwrap(), &2); + assert_eq!(gen.choose(&choices, || "error").unwrap(), &0); + assert_eq!(gen.choose(&choices, || "error").unwrap(), &1); + assert_eq!(gen.choose(&choices, || "error").unwrap(), &2); // This is the only case where we can't choose a value, because we have 3 choices and 6 bytes. assert_eq!( - gen.choose(&choices, "error"), - Err(MutationError::Exception("Ran out of entropy".to_string())) + gen.choose(&choices, || "error"), + Err(MutationError::User("Ran out of entropy".to_string())) ); } @@ -252,17 +260,17 @@ pub mod test { let mut gen = crate::generator::Generator::from(&u_data[..]); let choices = [0, 1, 2]; - assert_eq!(gen.choose(&choices, "error").unwrap(), &1); - assert_eq!(gen.choose(&choices, "error").unwrap(), &2); - assert_eq!(gen.choose(&choices, "error").unwrap(), &2); - assert_eq!(gen.choose(&choices, "error").unwrap(), &1); - assert_eq!(gen.choose(&choices, "error").unwrap(), &0); - assert_eq!(gen.choose(&choices, "error").unwrap(), &0); + assert_eq!(gen.choose(&choices, || "error").unwrap(), &1); + assert_eq!(gen.choose(&choices, || "error").unwrap(), &2); + assert_eq!(gen.choose(&choices, || "error").unwrap(), &2); + assert_eq!(gen.choose(&choices, || "error").unwrap(), &1); + assert_eq!(gen.choose(&choices, || "error").unwrap(), &0); + assert_eq!(gen.choose(&choices, || "error").unwrap(), &0); // This is the only case where we can't choose a value, because we have 3 choices and 6 bytes. assert_eq!( - gen.choose(&choices, "error"), - Err(MutationError::Exception("Ran out of entropy".to_string())) + gen.choose(&choices, || "error"), + Err(MutationError::User("Ran out of entropy".to_string())) ); } @@ -272,7 +280,7 @@ pub mod test { let mut gen = crate::generator::Generator::from(&[0][..]); let choices = [0]; for _ in 0..10 { - assert_eq!(gen.choose(&choices, "error").unwrap(), &0); + assert_eq!(gen.choose(&choices, || "error").unwrap(), &0); } // The generator has not had any entropy consumed by this point. diff --git a/src/lambda.rs b/src/lambda.rs new file mode 100644 index 0000000..c97656c --- /dev/null +++ b/src/lambda.rs @@ -0,0 +1,132 @@ +use std::{fmt::Debug, marker::PhantomData, sync::Arc}; + +use crate::*; + +/// Create a fact from a bare function which specifies the mutation. +/// Can be quicker to experiment with ideas this way than to have to directly implement +/// the [`Fact`] trait +/// +/// ``` +/// use contrafact::*; +/// let mut g = utils::random_generator(); +/// +/// let mut fact = vec_of_length( +/// 4, +/// lambda("geometric series", 2, move |g, s, mut v| { +/// g.set(&mut v, s, || "value is not geometrically increasing by 2")?; +/// *s *= 2; +/// Ok(v) +/// }), +/// ); +/// +/// let list = fact.clone().build(&mut g); +/// fact.check(&list).unwrap(); +/// assert_eq!(list, vec![2, 4, 8, 16]); +/// ``` +pub fn lambda<'a, S, T>( + label: impl ToString, + state: S, + f: impl 'a + Send + Sync + Fn(&mut Generator<'a>, &mut S, T) -> Mutation, +) -> Lambda<'a, S, T> +where + S: State, + T: Target<'a>, +{ + Lambda { + label: label.to_string(), + state, + fun: Arc::new(f), + _phantom: PhantomData, + } +} + +/// Create a lambda with unit state +pub fn lambda_unit<'a, T>( + label: impl ToString, + f: impl 'a + Send + Sync + Fn(&mut Generator<'a>, T) -> Mutation, +) -> Lambda<'a, (), T> +where + T: Target<'a>, +{ + lambda(label, (), move |g, (), t| f(g, t)) +} + +pub type LambdaFn<'a, S, T> = + Arc, &mut S, T) -> Mutation>; + +#[derive(Clone)] +pub struct Lambda<'a, S, T> +where + S: State, + T: Target<'a>, +{ + state: S, + fun: LambdaFn<'a, S, T>, + label: String, + _phantom: PhantomData<&'a T>, +} + +/// A Lambda with unit state +pub type LambdaUnit<'a, T> = Lambda<'a, (), T>; + +impl<'a, S, T> std::fmt::Debug for Lambda<'a, S, T> +where + S: State + Debug, + T: Target<'a>, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Lambda") + .field("label", &self.label) + .field("state", &self.state) + .finish() + } +} + +impl<'a, S, T> Fact<'a, T> for Lambda<'a, S, T> +where + S: State + Debug, + T: Target<'a>, +{ + fn mutate(&mut self, g: &mut Generator<'a>, t: T) -> Mutation { + (self.fun)(g, &mut self.state, t) + } + + fn label(&self) -> String { + self.label.clone() + } + + fn labeled(mut self, label: impl ToString) -> Self { + self.label = label.to_string(); + self + } +} + +#[test] +fn test_lambda_fact() { + use crate::facts::*; + let mut g = utils::random_generator(); + + let geom = |k, s| { + lambda("geom", s, move |g, s, mut v| { + g.set(&mut v, s, || { + format!("value is not geometrically increasing by {k} starting from {s}") + })?; + *s *= k; + Ok(v) + }) + }; + + let fact = |k, s| vec_of_length(4, geom(k, s)); + + { + let list = fact(2, 2).build(&mut g); + assert_eq!(list, vec![2, 4, 8, 16]); + fact(2, 2).check(&list).unwrap(); + } + + { + let list = fact(3, 4).build(&mut g); + assert_eq!(list, vec![4, 12, 36, 108]); + fact(3, 4).check(&list).unwrap(); + } +} diff --git a/src/lib.rs b/src/lib.rs index b93ed71..b1f262b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,7 +11,7 @@ //! meets the constraint, or to generate new instances of `S` which meet the constraint. //! //! ``` -//! use contrafact::{Fact, facts::{eq, lens}}; +//! use contrafact::{Fact, facts::{eq, lens1}}; //! use arbitrary::{Arbitrary, Unstructured}; //! //! #[derive(Debug, Clone, PartialEq, Arbitrary)] @@ -20,7 +20,7 @@ //! y: u32, //! } //! -//! let mut fact = lens("S::x", |s: &mut S| &mut s.x, eq("must be 1", 1)); +//! let mut fact = lens1("S::x", |s: &mut S| &mut s.x, eq(1)); //! //! assert!(fact.clone().check(&S {x: 1, y: 333}).is_ok()); //! assert!(fact.clone().check(&S {x: 2, y: 333}).is_err()); @@ -61,10 +61,10 @@ mod check; mod error; mod fact; -mod generator; - /// Some built-in implementations of some useful facts pub mod facts; +mod generator; +mod lambda; pub use facts::*; #[cfg(feature = "utils")] @@ -74,8 +74,11 @@ pub use arbitrary; pub use check::Check; pub use error::*; -pub use fact::{Bounds, Fact}; +pub use fact::{Fact, State, Target}; pub use generator::*; +pub use lambda::{lambda, lambda_unit}; + +pub(crate) use lambda::{Lambda, LambdaUnit}; pub use either; @@ -94,8 +97,8 @@ pub(crate) const SATISFY_ATTEMPTS: usize = 100; /// ``` /// use contrafact::*; /// -/// let eq1 = eq_(1); -/// let not2 = not_(eq_(2)); +/// let eq1 = eq(1); +/// let not2 = not(eq(2)); /// let mut fact = facts![eq1, not2]; /// assert!(fact.check(&1).is_ok()); /// ``` @@ -105,7 +108,7 @@ macro_rules! facts { ( $fact:expr $(,)?) => { $fact }; ( $fact_0:expr, $fact_1:expr $( , $fact_n:expr )* $(,)? ) => {{ - facts![ + $crate::facts![ $crate::facts::and($fact_0, $fact_1), $( $fact_n , )* ] diff --git a/tests/chainlink.rs b/tests/chainlink.rs index 82f2449..f124bcb 100644 --- a/tests/chainlink.rs +++ b/tests/chainlink.rs @@ -25,12 +25,8 @@ struct Wrapper { /// consecutive `prev` values starting with 0. fn chain_fact<'a>(author: String) -> impl Fact<'a, Link> { facts![ - lens( - "Link::author", - |o: &mut Link| &mut o.author, - eq("same author", author), - ), - lens( + lens1("Link::author", |o: &mut Link| &mut o.author, eq(author),), + lens1( "Link::prev", |o: &mut Link| &mut o.prev, consecutive_int("increasing prev", 0), @@ -42,12 +38,12 @@ fn chain_fact<'a>(author: String) -> impl Fact<'a, Link> { /// of the wrapper is in the given set. fn wrapper_fact<'a>(author: String, valid_colors: &'a [Color]) -> impl Fact<'a, Wrapper> { facts![ - lens( + lens1( "Wrapper::color", |o: &mut Wrapper| &mut o.color, in_slice("valid color", valid_colors), ), - lens( + lens1( "Wrapper::link", |o: &mut Wrapper| &mut o.link, chain_fact(author), diff --git a/tests/complex.rs b/tests/complex.rs index edbca62..1742128 100644 --- a/tests/complex.rs +++ b/tests/complex.rs @@ -116,6 +116,7 @@ struct Rho { /// Some struct needed to set the values of a Sigma whenever its Alpha changes. /// Analogous to Holochain's Keystore (MetaLairClient). +#[derive(Clone)] struct AlphaSigner; impl AlphaSigner { @@ -130,11 +131,11 @@ impl AlphaSigner { #[allow(unused)] fn alpha_fact() -> impl Fact<'static, Alpha> { - facts![lens("Alpha::id", |a: &mut Alpha| a.id(), id_fact(None))] + facts![lens1("Alpha::id", |a: &mut Alpha| a.id(), id_fact(None))] } fn beta_fact() -> impl Fact<'static, Beta> { - facts![lens("Beta::id", |a: &mut Beta| &mut a.id, id_fact(None))] + facts![lens1("Beta::id", |a: &mut Beta| &mut a.id, id_fact(None))] } /// Just a pair of an Alpha with optional Beta. @@ -157,7 +158,7 @@ fn id_fact(id: Option) -> impl Fact<'static, Id> { let le = brute("< u32::MAX", |id: &Id| *id < Id::MAX / 2); if let Some(id) = id { - Either::Left(facts![le, eq("id", id)]) + Either::Left(facts![le, eq(id)]) } else { Either::Right(facts![le]) } @@ -167,13 +168,13 @@ fn id_fact(id: Option) -> impl Fact<'static, Id> { /// - All Ids should match each other. If there is a Beta, its id should match too. fn pi_fact(id: Id) -> impl Fact<'static, Pi> { let alpha_fact = facts![ - lens("Alpha::id", |a: &mut Alpha| a.id(), id_fact(Some(id))), - // lens("Alpha::data", |a: &mut Alpha| a.data(), eq("data", data)), + lens1("Alpha::id", |a: &mut Alpha| a.id(), id_fact(Some(id))), + // lens1("Alpha::data", |a: &mut Alpha| a.data(), eq(data)), ]; - let beta_fact = lens("Beta::id", |b: &mut Beta| &mut b.id, id_fact(Some(id))); + let beta_fact = lens1("Beta::id", |b: &mut Beta| &mut b.id, id_fact(Some(id))); facts![ pi_beta_match(), - lens("Pi::alpha", |o: &mut Pi| &mut o.0, alpha_fact), + lens1("Pi::alpha", |o: &mut Pi| &mut o.0, alpha_fact), prism("Pi::beta", |o: &mut Pi| o.1.as_mut(), beta_fact), ] } @@ -184,7 +185,7 @@ fn pi_fact(id: Id) -> impl Fact<'static, Pi> { /// - and, the the Betas of the Alpha and the Omega should match. /// - all data must be set as specified fn omega_fact(id: Id) -> impl Fact<'static, Omega> { - let omega_pi = LensFact::new( + let omega_pi = lens2( "Omega -> Pi", |o| match o { Omega::AlphaBeta { alpha, beta, .. } => Pi(alpha, Some(beta)), @@ -202,13 +203,13 @@ fn omega_fact(id: Id) -> impl Fact<'static, Omega> { facts![ omega_pi, - lens("Omega::id", |o: &mut Omega| o.id_mut(), id_fact(Some(id))), + lens1("Omega::id", |o: &mut Omega| o.id_mut(), id_fact(Some(id))), ] } #[allow(unused)] fn sigma_fact() -> impl Fact<'static, Sigma> { - let id2_fact = LensFact::new( + let id2_fact = lens2( "Sigma::id is correct", |mut s: Sigma| (s.id2, *(s.alpha.id()) * 2), |mut s, (_, id2)| { @@ -217,7 +218,7 @@ fn sigma_fact() -> impl Fact<'static, Sigma> { }, same(), ); - let sig_fact = LensFact::new( + let sig_fact = lens2( "Sigma::sig is correct", |mut s: Sigma| (s.sig, s.alpha.id().to_string()), |mut s, (_, sig)| { @@ -227,7 +228,7 @@ fn sigma_fact() -> impl Fact<'static, Sigma> { same(), ); facts![ - lens("Sigma::id", |o: &mut Sigma| o.alpha.id(), id_fact(None)), + lens1("Sigma::id", |o: &mut Sigma| o.alpha.id(), id_fact(None)), id2_fact ] } @@ -235,7 +236,7 @@ fn sigma_fact() -> impl Fact<'static, Sigma> { /// The inner Sigma is correct wrt to signature /// XXX: this is a little wonky, probably room for improvement. fn rho_fact(id: Id, signer: AlphaSigner) -> impl Fact<'static, Rho> { - let rho_pi = LensFact::new( + let rho_pi = lens2( "Rho -> Pi", |rho: Rho| Pi(rho.sigma.alpha, rho.beta), move |mut rho, Pi(a, b)| { @@ -249,7 +250,7 @@ fn rho_fact(id: Id, signer: AlphaSigner) -> impl Fact<'static, Rho> { #[cfg(not(feature = "optics"))] { facts![ - lens("Rho -> Sigma", |rho: &mut Rho| &mut rho.sigma, sigma_fact()), + lens1("Rho -> Sigma", |rho: &mut Rho| &mut rho.sigma, sigma_fact()), rho_pi ] }