Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Testing

Code that uses ddust is tested as code that uses integers is: a decimal is a value, with no clock, no allocator and no global state, so a test that builds its decimals from literals is deterministic. Three features draw a test’s decimals instead.

Property Tests

With proptest, a decimal implements proptest’s Arbitrary: any steps, at any scale its scale type takes, from 0 to 38 decimals for Dynamic. A rounding mode is Arbitrary too, any of the nine:

mod properties {
    use ddust::D64;
    use proptest::prelude::*;

    proptest! {
        #[test]
        fn a_sum_is_the_same_either_way(a: D64<2>, b: D64<2>) {
            prop_assert_eq!(a.checked_add(b), b.checked_add(a));
        }
    }
}

Any steps means the ends of the range too, so a property that adds two values meets an overflow, and is written in the checked_* forms, or over a narrower strategy of its own.

Fuzzing

With arbitrary, a decimal implements arbitrary’s Arbitrary, the trait cargo-fuzz and libFuzzer targets draw their inputs through: a fuzz target takes decimals from the fuzzer’s bytes as it takes integers. The feature needs std, as arbitrary does:

fn a_fuzzer_draws_any_decimal_from_its_bytes() {
    use arbitrary::Unstructured;
    use ddust::{D64, Decimal, Dynamic};

    let mut input = Unstructured::new(b"the bytes a fuzzer gives, any of them");
    let (a, b): (D64<2>, D64<2>) = input.arbitrary().expect("sixteen bytes");
    assert_eq!(a.checked_add(b), b.checked_add(a));
    let carried: Decimal<i64, Dynamic> = input.arbitrary().expect("steps, and a scale");
    assert!(carried.decimals() <= 38);
}

Random Values

With rand-09, a decimal implements rand 0.9’s SampleUniform, through ddust::rand_09::UniformDecimal, so it is drawn uniformly from a range as an integer is: its steps from the bounds’ steps. Seeded, the draws are the same on every run:

fn a_seeded_draw_is_the_same_on_every_run() {
    use ddust::{D64, dec};
    use rand::rngs::StdRng;
    use rand::{Rng, SeedableRng};

    let draw = |seed| {
        let mut rng = StdRng::seed_from_u64(seed);
        let price: D64<2> = rng.random_range(dec!(99.00)..=dec!(101.00));
        price
    };
    assert_eq!(draw(7), draw(7), "one seed, one price");
    assert!((dec!(99)..=dec!(101)).contains(&draw(7)));
}

Overflow in Tests

Cargo’s test profile has overflow checks on, so an operator past the range panics in a test even where it wraps in the release build. A test of what happens past the range calls the checked_*, saturating_*, wrapping_* or overflowing_* method, which does the same in every profile, and a test of the operator itself is gated on the build’s checks, as Arithmetic and Overflow shows.