proptest-rs / proptest

Hypothesis-like property testing for Rust
Apache License 2.0
1.63k stars 152 forks source link

Zero Examples of `impl Arbitrary` ? #445

Open bionicles opened 2 months ago

bionicles commented 2 months ago

Hey, I admit I'm a noob, but i just wanted to make a simple toy example of proptest but it looks like everything is either macros or derive, functions accepting ranges as arguments, what's the simple straightforward way to create a BoxedStrategy from a prop_map? Should i be using Arbitrary from the arbitrary crate instead of from the proptest crate?

use chrono::TimeZone;
use proptest::prelude::*;
use proptest::strategy::{BoxedStrategy, Just, Strategy};
// suppose this is a separate crate and you can't implement our traits on it
// how do you add a new plugin custom datatype easily
mod coordinate_library {
    use chrono::{DateTime, FixedOffset};
    use uom::si::angle::degree;
    use uom::si::f64::{Angle, Length};
    use uom::si::length::meter;

    #[derive(Debug)]
    pub struct SpaceTimeCoordinate {
        pub latitude: Angle,
        pub longitude: Angle,
        pub elevation: Length,
        pub datetime: DateTime<FixedOffset>,
    }

    impl SpaceTimeCoordinate {
        pub fn new(lat: f64, lng: f64, elev: f64, dt: DateTime<FixedOffset>) -> Self {
            SpaceTimeCoordinate {
                latitude: Angle::new::<degree>(lat),
                longitude: Angle::new::<degree>(lng),
                elevation: Length::new::<meter>(elev),
                datetime: dt,
            }
        }
    }
}

use chrono::{DateTime, FixedOffset, Utc};
// Import the external library type.
use coordinate_library::SpaceTimeCoordinate;

use uom::si::angle::degree;
use uom::si::f64::{Angle, Length};
use uom::si::length::meter;

// compile failure due to orphan rules prevents direct implementation
// impl CustomDType for SpaceTimeCoordinate {} // error

#[derive(Debug)]
// Define a newtype wrapper for the external library type.
pub struct Coordinate(pub SpaceTimeCoordinate);

impl Arbitrary for Coordinate {
    type Parameters = ();
    type Strategy = BoxedStrategy<Self>;

    fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
        let timestamp = any::<i64>().prop_map(|seconds| {
            let tz = FixedOffset::east_opt(0).unwrap();
            tz.timestamp_opt(seconds, 0).unwrap()
        });
        let latitude = any::<f64>().prop_map(|value| Angle::new::<degree>(value));
        let longitude = any::<f64>().prop_map(|value| Angle::new::<degree>(value));
        let elevation = any::<f64>().prop_map(|value| Length::new::<meter>(value));

        (latitude, longitude, elevation, timestamp)
            .prop_map(|(latitude, longitude, elevation, datetime)| {
                Box::new(Coordinate(SpaceTimeCoordinate {
                    latitude,
                    longitude,
                    elevation,
                    datetime,
                }))
            })
            .into_boxed_strategy()
    }
}

you could improve the docs a ton by putting complete example of how to impl Arbitrary somewhere, even the reference docs for the trait itself do not have a code block example of how to implement that, and it seems pretty specific about how we ought to code this, anyway, thanks for all the fuzz testing, i feel dumb right now

bionicles commented 2 months ago

(tried without the box, tried Ok(box), etc etc) , edited to add some context but tried not to put too much. Anyway, just pestering about docs / examples, user error, carry on

bionicles commented 2 months ago

Strategy::boxed worked, here is an example impl proptest::Arbitrary which compiles and works

// In your library's module (e.g., mod.rs)
// suppose you want to enable a new data type in your sql tables, like GIS / coordinates
use chrono::{DateTime, FixedOffset, TimeZone};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use uom::si::angle::degree;
use uom::si::f64::{Angle, Length};
use uom::si::length::meter;

// suppose this is a separate crate and you can't implement our traits on it
// how do you add a new plugin custom datatype easily
mod coordinate_library {
    use chrono::{DateTime, FixedOffset};
    use uom::si::angle::degree;
    use uom::si::f64::{Angle, Length};
    use uom::si::length::meter;

    #[derive(Debug)]
    pub struct SpaceTimeCoordinate {
        pub latitude: Angle,
        pub longitude: Angle,
        pub elevation: Length,
        pub datetime: DateTime<FixedOffset>,
    }

    impl SpaceTimeCoordinate {
        pub fn new(lat: f64, lng: f64, elev: f64, dt: DateTime<FixedOffset>) -> Self {
            SpaceTimeCoordinate {
                latitude: Angle::new::<degree>(lat),
                longitude: Angle::new::<degree>(lng),
                elevation: Length::new::<meter>(elev),
                datetime: dt,
            }
        }
    }
}

// Import the external library type.
use coordinate_library::SpaceTimeCoordinate;

// compile failure due to orphan rules prevents direct implementation
// impl CustomDType for SpaceTimeCoordinate {} // error

#[derive(Debug)]
// Define a newtype wrapper for the external library type.
pub struct Coordinate(pub SpaceTimeCoordinate);

use proptest::prelude::*;
use proptest::test_runner::TestRunner;
// use proptest::strategy::{BoxedStrategy, Just, Strategy}; // redundant with "prelude"

const HOUR: i32 = 3600;

impl Arbitrary for Coordinate {
    type Parameters = ();
    type Strategy = BoxedStrategy<Self>;

    fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
        let timestamp = any::<i32>().prop_map(|seconds| {
            let seconds_i64 = seconds as i64;
            let tz = FixedOffset::east_opt(5 * HOUR).unwrap();
            println!("seconds={seconds:#?}");
            let dt: DateTime<FixedOffset> = tz.timestamp_opt(seconds_i64, 0).unwrap();
            dt
        });
        let latitude = any::<f32>().prop_map(|x| Angle::new::<degree>(x as f64));
        let longitude = any::<f32>().prop_map(|x| Angle::new::<degree>(x as f64));
        let elevation = any::<f32>().prop_map(|x| Length::new::<meter>(x as f64));

        let pipeline = (latitude, longitude, elevation, timestamp).prop_map(
            |(latitude, longitude, elevation, datetime)| {
                Coordinate(SpaceTimeCoordinate {
                    latitude,
                    longitude,
                    elevation,
                    datetime,
                })
            },
        );
        Strategy::boxed(pipeline)
    }
}

const MAX_LOOPS: usize = 12;
fn main() {
    let config = Default::default();
    let mut runner = TestRunner::new(config);
    let strat = Coordinate::arbitrary_with(());
    for iteration in 0..MAX_LOOPS {
        match strat.new_tree(&mut runner) {
            Ok(tree) => {
                let value = tree.current();
                println!("{iteration:#?} {value:#?}")
            }
            Err(reason) => {
                eprintln!("{reason:#?}")
            }
        }
    }
}

image

gonna keep this open since it's a docs issue edited to use f32 for more satisfying diversity of timestamps and less huge angles

matthew-russo commented 2 months ago

Hi sorry for the delay -- glad you ended up figuring it out. Will see what we can do to improve the docs/guides