use proptest::prelude::*;
use renew_fixed::{Fixed, saturations};
fn modest() -> impl Strategy<Value = Fixed> {
(-(1i64 << 34)..(1i64 << 34)).prop_map(Fixed::from_bits)
}
fn any_fixed() -> impl Strategy<Value = Fixed> {
any::<i64>().prop_map(Fixed::from_bits)
}
proptest! {
#![proptest_config(ProptestConfig { cases: 4096, ..ProptestConfig::default() })]
#[test]
fn multiplication_is_symmetric_under_negation(a in modest(), b in modest()) {
prop_assert_eq!((-a).saturating_mul(b), -(a.saturating_mul(b)));
}
#[test]
fn multiplication_commutes(a in modest(), b in modest()) {
prop_assert_eq!(a.saturating_mul(b), b.saturating_mul(a));
}
#[test]
fn multiplication_associates_to_within_the_rounding_error(
a in modest(), b in modest(), c in modest()
) {
let left = a.saturating_mul(b).saturating_mul(c);
let right = a.saturating_mul(b.saturating_mul(c));
let difference = (left.to_bits() - right.to_bits()).abs();
let tolerance = 1 + (a.to_bits().abs() >> 16) + (c.to_bits().abs() >> 16);
prop_assert!(
difference <= tolerance,
"difference {difference} exceeds tolerance {tolerance}"
);
}
#[test]
fn addition_is_associative_and_commutative(a in modest(), b in modest(), c in modest()) {
prop_assert_eq!(a + b, b + a);
prop_assert_eq!((a + b) + c, a + (b + c));
}
#[test]
fn ordering_follows_the_raw_pattern(a in any_fixed(), b in any_fixed()) {
prop_assert_eq!(a.cmp(&b), a.to_bits().cmp(&b.to_bits()));
}
#[test]
fn arithmetic_never_escapes_the_range(a in any_fixed(), b in any_fixed()) {
for value in [a + b, a - b, a.saturating_mul(b)] {
prop_assert!(value >= Fixed::MIN && value <= Fixed::MAX);
}
}
#[test]
fn sqrt_is_floor_exact(value in 0i64..=i64::MAX) {
let x = Fixed::from_bits(value);
let root = u128::from(x.sqrt().to_bits().unsigned_abs());
#[expect(
clippy::cast_sign_loss,
reason = "the strategy generates non-negative values only"
)]
let shifted = (value as u128) << 16;
prop_assert!(root * root <= shifted, "root too large");
prop_assert!(shifted < (root + 1) * (root + 1), "root too small");
}
#[test]
fn sqrt_of_a_negative_is_none(value in i64::MIN..0i64) {
prop_assert_eq!(Fixed::from_bits(value).checked_sqrt(), None);
}
#[test]
fn whole_numbers_are_exact(value in -100_000i32..100_000) {
prop_assert_eq!(Fixed::from_int(value).trunc_int(), i64::from(value));
prop_assert_eq!(Fixed::from_int(value).fract(), Fixed::ZERO);
}
#[test]
fn dividing_by_a_factor_recovers_the_other(a in modest(), b in modest()) {
prop_assume!(b.to_bits().abs() > (1 << 16));
let product = a.saturating_mul(b);
let recovered = product.saturating_div(b);
let difference = (recovered.to_bits() - a.to_bits()).abs();
prop_assert!(difference <= 2, "recovered {recovered:?} from {a:?}, off by {difference}");
}
#[test]
fn the_counter_is_silent_when_nothing_saturates(a in modest(), b in modest()) {
let before = saturations();
let _ = a + b;
let _ = a.saturating_mul(b);
prop_assert_eq!(saturations(), before);
}
}
#[test]
fn the_named_edges_behave() {
const GRAVITY: Fixed = Fixed::from_ratio(981, 100);
const BIRD_X: Fixed = Fixed::from_int(40);
let before = saturations();
assert_eq!(Fixed::MAX + Fixed::ONE, Fixed::MAX);
assert_eq!(Fixed::MIN - Fixed::ONE, Fixed::MIN);
assert_eq!(saturations().0, before.0 + 2);
assert_eq!(
Fixed::ONE.saturating_mul(Fixed::from_int(7)),
Fixed::from_int(7)
);
assert_eq!(Fixed::ZERO.saturating_mul(Fixed::MAX), Fixed::ZERO);
assert_eq!(Fixed::ZERO.sqrt(), Fixed::ZERO);
let gravity = Fixed::from_ratio(981, 100);
assert_eq!(gravity.trunc_int(), 9);
assert_eq!(gravity.to_bits(), 642_908);
assert_eq!(GRAVITY, gravity);
assert_eq!(BIRD_X.trunc_int(), 40);
}