uninum 0.1.1

A robust, ergonomic unified number type for Rust with automatic overflow handling, type promotion, and cross-type consistency.
Documentation
//! Property-based stress tests for uninum
//!
//! These exhaustive tests verify Number behavior under extreme conditions
//! Run with: cargo test --test stress_proptest -- --ignored

use std::convert::TryFrom;

use proptest::prelude::*;
use uninum::Number;

// Exhaustive test configuration
fn stress_config() -> ProptestConfig {
    ProptestConfig::with_cases(10000)
}

// Strategy for generating extreme numeric values
fn extreme_number_strategy() -> impl Strategy<Value = Number> {
    prop_oneof![
        // Extreme u64 values
        Just(Number::from(u64::MAX)),
        Just(Number::from(u64::MAX - 1)),
        Just(Number::from(u64::MAX / 2)),
        (u64::MAX - 1000..u64::MAX).prop_map(Number::from),
        // Extreme i64 values
        Just(Number::from(i64::MAX)),
        Just(Number::from(i64::MIN)),
        Just(Number::from(i64::MAX - 1)),
        Just(Number::from(i64::MIN + 1)),
        (i64::MIN..i64::MIN + 1000).prop_map(Number::from),
        (i64::MAX - 1000..i64::MAX).prop_map(Number::from),
        // Extreme f64 values
        Just(Number::from(f64::MAX)),
        Just(Number::from(f64::MIN)),
        Just(Number::from(f64::MIN_POSITIVE)),
        Just(Number::from(f64::EPSILON)),
        Just(Number::from(-f64::EPSILON)),
        (1e100f64..f64::MAX / 2.0).prop_map(Number::from),
        (f64::MIN / 2.0..-1e100f64).prop_map(Number::from),
    ]
}

proptest! {
    #![proptest_config(stress_config())]

    #[test]
    #[ignore]
    fn prop_exhaustive_arithmetic_overflow(
        a in any::<i64>(),
        b in any::<i64>()
    ) {
        let na = Number::from(a);
        let nb = Number::from(b);

        // Test addition
        let result = na.clone() + nb.clone();
        match a.checked_add(b) {
            Some(expected) => assert_eq!(result, Number::from(expected)),
            None => {
                // Overflow should promote to a wider representation
                // Accept Decimal when the `decimal` feature is enabled, or F64 otherwise.
                #[cfg(feature = "decimal")]
                {
                    assert!(
                        result.try_get_decimal().is_some() || result.try_get_f64().is_some(),
                        "Expected promotion to Decimal or F64 on overflow, got {result:?}"
                    );
                }
                #[cfg(not(feature = "decimal"))]
                {
                    assert!(result.try_get_f64().is_some());
                }
            }
        }

        // Test multiplication
        let result = na.clone() * nb.clone();
        match a.checked_mul(b) {
            Some(expected) => assert_eq!(result, Number::from(expected)),
            None => {
                // Overflow should promote to a wider representation
                #[cfg(feature = "decimal")]
                {
                    assert!(
                        result.try_get_decimal().is_some() || result.try_get_f64().is_some(),
                        "Expected promotion to Decimal or F64 on overflow, got {result:?}"
                    );
                }
                #[cfg(not(feature = "decimal"))]
                {
                    assert!(result.try_get_f64().is_some());
                }
            }
        }
    }

    #[test]
    #[ignore]
    fn prop_extreme_value_operations(
        val in extreme_number_strategy(),
        op in 0..4u8
    ) {
        match op {
            0 => {
                // Test negation
                let neg = -val.clone();
                let double_neg = -neg;
                // Double negation should return to original (except for edge cases)
                if !val.is_nan() {
                    assert_eq!(val, double_neg);
                }
            }
            1 => {
                // Test squaring
                let squared = val.clone() * val.clone();
                assert!(!squared.is_negative() || squared.is_nan());
            }
            2 => {
                // Test division by small number
                let small = Number::from(0.0001);
                let _result = val / small;
                // Just ensure it doesn't panic
            }
            3 => {
                // Test comparison with zero
                let zero = Number::from(0);
                let _cmp = val.partial_cmp(&zero);
                // Just ensure it doesn't panic
            }
            _ => unreachable!()
        }
    }

    #[test]
    #[ignore]
    fn prop_massive_string_parsing(
        digits in 100..1000usize,
        has_decimal in any::<bool>(),
        is_negative in any::<bool>()
    ) {
        let mut s = String::new();
        if is_negative {
            s.push('-');
        }

        // Generate a very long number string
        for i in 0..digits {
            s.push_str(&(i % 10).to_string());
            if has_decimal && i == digits / 2 {
                s.push('.');
            }
        }

        // Try to parse it
        let result = Number::try_from(s.as_str());
        prop_assert!(result.is_ok(), "Failed to parse: {}", s);
    }

    #[test]
    #[ignore]
    fn prop_extreme_conversions(val in any::<u64>()) {
        // Test all conversion paths
        let n = Number::from(val);

        // Test conversions
        if let Some(v) = n.try_get_u64() {
            assert_eq!(v, val);
        } else {
            panic!("Expected U64 variant");
        }

        // To f64 and back
        let f = if let Some(v) = n.try_get_u64() {
            v as f64
        } else {
            unreachable!();
        };
        let back = Number::from(f);

        // For large u64 values, we might lose precision
        if val < (1u64 << 53) {
            if let Some(v) = back.try_get_u64() {
                assert_eq!(v, val);
            } else if back.try_get_f64().is_some() {
                // Acceptable for large values
            } else {
                panic!("Unexpected conversion result");
            }
        }
    }

    #[test]
    #[ignore]
    fn prop_bitwise_exhaustive(
        a in any::<u64>(),
        b in any::<u64>(),
        shift in 0..128u8
    ) {
        let na = Number::from(a);
        let nb = Number::from(b);

        // Test bitwise operations only work on integer types
        // The Number type doesn't implement bitwise operations directly
        // We'll test the underlying integer conversions
        if let (Some(a_u64), Some(b_u64)) = (na.try_get_u64(), nb.try_get_u64()) {
            assert_eq!(a_u64 & b_u64, a & b);
            assert_eq!(a_u64 | b_u64, a | b);
            assert_eq!(a_u64 ^ b_u64, a ^ b);
            assert_eq!(!a_u64, !a);

            if shift < 64 {
                if let Some(expected) = a.checked_shl(shift as u32) {
                    assert_eq!(a_u64.checked_shl(shift as u32), Some(expected));
                }
                if let Some(expected) = a.checked_shr(shift as u32) {
                    assert_eq!(a_u64.checked_shr(shift as u32), Some(expected));
                }
            }
        }
    }

    #[test]
    #[ignore]
    fn prop_decimal_precision_stress(
        integer in -999_999_999i64..=999_999_999,
        decimal in 0u64..1_000_000_000,
    ) {
        #[cfg(not(feature = "decimal"))]
        {
            let _ = (integer, decimal);
        }
        #[cfg(feature = "decimal")]
        {
            let float_str = format!("{integer}.{decimal:09}");
            let parsed = Number::try_from(float_str.as_str()).unwrap();

            // Should preserve as decimal
            assert!(parsed.try_get_decimal().is_some());

            // Round-trip through string should preserve value
            let back_str = parsed.to_string();
            let reparsed = Number::try_from(back_str.as_str()).unwrap();

            // Due to decimal representation, values should match
            assert_eq!(parsed, reparsed);
        }
    }

    #[test]
    #[ignore]
    fn prop_mixed_type_arithmetic_exhaustive(
        a_type in 0..5u8,
        b_type in 0..5u8,
        val_a in 0..1000i64,
        val_b in 0..1000i64,
    ) {
        let a = match a_type {
            0 => Number::from(val_a as u64),
            1 => Number::from(val_a),
            2 => Number::from(val_a as f64),
            3 => Number::from(val_a as u32),
            4 => Number::from(val_a as i32),
            _ => unreachable!()
        };

        let b = match b_type {
            0 => Number::from(val_b as u64),
            1 => Number::from(val_b),
            2 => Number::from(val_b as f64),
            3 => Number::from(val_b as u32),
            4 => Number::from(val_b as i32),
            _ => unreachable!()
        };

        // Test all operations
        let _add = &a + &b;
        let _sub = &a - &b;
        let _mul = &a * &b;
        if !b.is_zero() {
            let _div = &a / &b;
            let _rem = &a % &b;
        }

        // Test comparisons
        let _lt = a < b;
        let _eq = a == b;
        let _gt = a > b;
    }

    #[test]
    #[ignore]
    fn prop_extreme_pow_operations(
        base in -100f64..100f64,
        exp in -100f64..100f64,
    ) {
        let n_base = Number::from(base);
        let n_exp = Number::from(exp);

        // Test pow - might produce infinity or very large/small numbers
        let result = n_base.pow(&n_exp);

        // Just ensure it doesn't panic and produces a valid Number
        if let Some(f) = result.try_get_f64() {
            // Result should be a valid f64 (including NaN or infinity per IEEE 754)
            assert!(f.is_finite() || f.is_infinite() || f.is_nan());
        }
        // Other number types are also valid, no further checks needed
    }
}