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.