From 940ba9991c91a9831c8248ffe60c1647ef6012f9 Mon Sep 17 00:00:00 2001 From: baku-ccron Date: Mon, 7 Sep 2026 16:28:48 +0000 Subject: [PATCH 1/2] Drop the library-logic tests that now run in rain.math.float The crate keeps the tests of its own Rust API: Default, serde, the hex round-trip and error rendering. The proptest seeds went with the tests. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01QyCCzi9WZPhuXcU1hwr2bq --- .../float/proptest-regressions/fuzz_ops.txt | 8 - crates/float/proptest-regressions/lib.txt | 8 - crates/float/src/fuzz_ops.rs | 306 ------------------ 3 files changed, 322 deletions(-) delete mode 100644 crates/float/proptest-regressions/fuzz_ops.txt delete mode 100644 crates/float/proptest-regressions/lib.txt delete mode 100644 crates/float/src/fuzz_ops.rs diff --git a/crates/float/proptest-regressions/fuzz_ops.txt b/crates/float/proptest-regressions/fuzz_ops.txt deleted file mode 100644 index 6ecefab..0000000 --- a/crates/float/proptest-regressions/fuzz_ops.txt +++ /dev/null @@ -1,8 +0,0 @@ -# Seeds for failure cases proptest has generated in the past. It is -# automatically read and these particular cases re-run before any -# novel cases are generated. -# -# It is recommended to check this file in to source control so that -# everyone who runs the test benefits from these saved cases. -cc 285e4d87fcb0e9c1757a0ddf5aa8199fbd17d38208bd0b5ef2288d059eb3934e # shrinks to a = Float(0xffffffff000000000000000000000000000000000000000000009a9939ef8c94) -cc cb8c50af037a017c4a91ac6dede4b4fc2e1372a1de1290ac8fafc546772613d0 # shrinks to a = Float(0xfffffffeffffffffffffffffffffffffffffffffffffffffffffba63064a71d9) diff --git a/crates/float/proptest-regressions/lib.txt b/crates/float/proptest-regressions/lib.txt deleted file mode 100644 index 7070cb9..0000000 --- a/crates/float/proptest-regressions/lib.txt +++ /dev/null @@ -1,8 +0,0 @@ -# Seeds for failure cases proptest has generated in the past. It is -# automatically read and these particular cases re-run before any -# novel cases are generated. -# -# It is recommended to check this file in to source control so that -# everyone who runs the test benefits from these saved cases. -cc 664cbb4a3416dcc12fe1196bdf33bdf3f9e1403c6446f0516dc704da23be5723 # shrinks to a = Float(0x0000000000000000000000000000000000000000000000000000000000000000), b = Float(0xffffffff00000000000000000000000000000000000000000000000000000001) -cc 3fd2457aad2f1f4960353eb70458c7806f645c48898d3d336369b396c6cd13e1 # shrinks to a = Float(0x000000000000000000000000000000000000000000000000534a0c3580000000), b = Float(0x00000000000000000000000000000000000000000000000008e5c21900000000) diff --git a/crates/float/src/fuzz_ops.rs b/crates/float/src/fuzz_ops.rs deleted file mode 100644 index 4c28f78..0000000 --- a/crates/float/src/fuzz_ops.rs +++ /dev/null @@ -1,306 +0,0 @@ -#[cfg(test)] -mod tests { - use crate::Float; - use alloy::primitives::{aliases::I224, I256}; - use proptest::prelude::*; - - /// Convert a Solidity Float to f64 via unpack. - fn sol_to_f64(f: Float) -> Option { - let (coeff, exp) = f.unpack().ok()?; - let c: f64 = i256_to_f64(coeff); - let e: i32 = exp.as_i32(); - Some(c * 10.0_f64.powi(e)) - } - - fn i256_to_f64(v: I256) -> f64 { - if v.is_negative() { - let abs = (!v).wrapping_add(I256::ONE); - -(u256_to_f64(abs.into_raw())) - } else { - u256_to_f64(v.into_raw()) - } - } - - fn u256_to_f64(v: alloy::primitives::U256) -> f64 { - // Convert U256 to f64 via string parsing for accuracy. - v.to_string().parse::().unwrap_or(f64::INFINITY) - } - - // Generate floats in a range where f64 can represent them without - // overflow/underflow. Coefficients up to ~1e15 and exponents -15..15 - // keep values in f64's comfortable range. - prop_compose! { - fn f64_compatible_float()( - coefficient in -10i64.pow(15)..10i64.pow(15), - exponent in -15i32..15i32, - ) -> Float { - Float::pack_lossless( - I224::try_from(coefficient).unwrap(), - exponent, - ).unwrap() - } - } - - /// Check that two f64 values are approximately equal, allowing for - /// f64 rounding errors. Returns true if they're within a relative - /// tolerance of 1e-10 or both are effectively zero. - fn approx_eq(a: f64, b: f64) -> bool { - if a == b { - return true; - } - if a.is_nan() || b.is_nan() { - return false; - } - let max_abs = a.abs().max(b.abs()); - if max_abs < 1e-30 { - return true; - } - ((a - b).abs() / max_abs) < 1e-10 - } - - proptest! { - #[test] - fn fuzz_add( - a in f64_compatible_float(), - b in f64_compatible_float(), - ) { - let sol_result = (a + b).unwrap(); - let a_f64 = sol_to_f64(a).unwrap(); - let b_f64 = sol_to_f64(b).unwrap(); - let expected = a_f64 + b_f64; - let actual = sol_to_f64(sol_result).unwrap(); - prop_assert!( - approx_eq(expected, actual), - "add: {a_f64} + {b_f64} = {expected}, sol = {actual}", - ); - } - - #[test] - fn fuzz_sub( - a in f64_compatible_float(), - b in f64_compatible_float(), - ) { - let sol_result = (a - b).unwrap(); - let a_f64 = sol_to_f64(a).unwrap(); - let b_f64 = sol_to_f64(b).unwrap(); - let expected = a_f64 - b_f64; - let actual = sol_to_f64(sol_result).unwrap(); - prop_assert!( - approx_eq(expected, actual), - "sub: {a_f64} - {b_f64} = {expected}, sol = {actual}", - ); - } - - #[test] - fn fuzz_mul( - a in f64_compatible_float(), - b in f64_compatible_float(), - ) { - let sol_result = (a * b).unwrap(); - let a_f64 = sol_to_f64(a).unwrap(); - let b_f64 = sol_to_f64(b).unwrap(); - let expected = a_f64 * b_f64; - let actual = sol_to_f64(sol_result).unwrap(); - prop_assert!( - approx_eq(expected, actual), - "mul: {a_f64} * {b_f64} = {expected}, sol = {actual}", - ); - } - - #[test] - fn fuzz_div( - a in f64_compatible_float(), - b in f64_compatible_float(), - ) { - let b_f64 = sol_to_f64(b).unwrap(); - // Skip division by zero. - prop_assume!(b_f64.abs() > 1e-30); - - let sol_result = (a / b).unwrap(); - let a_f64 = sol_to_f64(a).unwrap(); - let expected = a_f64 / b_f64; - let actual = sol_to_f64(sol_result).unwrap(); - prop_assert!( - approx_eq(expected, actual), - "div: {a_f64} / {b_f64} = {expected}, sol = {actual}", - ); - } - - #[test] - fn fuzz_neg(a in f64_compatible_float()) { - let sol_result = (-a).unwrap(); - let a_f64 = sol_to_f64(a).unwrap(); - let expected = -a_f64; - let actual = sol_to_f64(sol_result).unwrap(); - prop_assert!( - approx_eq(expected, actual), - "neg: -{a_f64} = {expected}, sol = {actual}", - ); - } - - #[test] - fn fuzz_abs(a in f64_compatible_float()) { - let sol_result = a.abs().unwrap(); - let a_f64 = sol_to_f64(a).unwrap(); - let expected = a_f64.abs(); - let actual = sol_to_f64(sol_result).unwrap(); - prop_assert!( - approx_eq(expected, actual), - "abs: |{a_f64}| = {expected}, sol = {actual}", - ); - } - - #[test] - fn fuzz_inv(a in f64_compatible_float()) { - let a_f64 = sol_to_f64(a).unwrap(); - // Skip values too close to zero. - prop_assume!(a_f64.abs() > 1e-10); - - let sol_result = a.inv().unwrap(); - let expected = 1.0 / a_f64; - let actual = sol_to_f64(sol_result).unwrap(); - prop_assert!( - approx_eq(expected, actual), - "inv: 1/{a_f64} = {expected}, sol = {actual}", - ); - } - - #[test] - fn fuzz_floor(a in f64_compatible_float()) { - let a_f64 = sol_to_f64(a).unwrap(); - let sol_result = a.floor().unwrap(); - let expected = a_f64.floor(); - let actual = sol_to_f64(sol_result).unwrap(); - prop_assert!( - approx_eq(expected, actual), - "floor: floor({a_f64}) = {expected}, sol = {actual}", - ); - } - - #[test] - fn fuzz_integer(a in f64_compatible_float()) { - let a_f64 = sol_to_f64(a).unwrap(); - let sol_result = a.integer().unwrap(); - let expected = a_f64.trunc(); - let actual = sol_to_f64(sol_result).unwrap(); - prop_assert!( - approx_eq(expected, actual), - "integer: trunc({a_f64}) = {expected}, sol = {actual}", - ); - } - - #[test] - fn fuzz_frac( - coefficient in -10i64.pow(10)..10i64.pow(10), - exponent in -5i32..0i32, - ) { - let a = Float::pack_lossless( - I224::try_from(coefficient).unwrap(), - exponent, - ).unwrap(); - let a_f64 = sol_to_f64(a).unwrap(); - let sol_result = a.frac().unwrap(); - let expected = a_f64.fract(); - let actual = sol_to_f64(sol_result).unwrap(); - prop_assert!( - (expected - actual).abs() < 1e-6, - "frac: fract({a_f64}) = {expected}, sol = {actual}", - ); - } - - #[test] - fn fuzz_min( - a in f64_compatible_float(), - b in f64_compatible_float(), - ) { - let sol_result = a.min(b).unwrap(); - let a_f64 = sol_to_f64(a).unwrap(); - let b_f64 = sol_to_f64(b).unwrap(); - let expected = a_f64.min(b_f64); - let actual = sol_to_f64(sol_result).unwrap(); - prop_assert!( - approx_eq(expected, actual), - "min: min({a_f64}, {b_f64}) = {expected}, sol = {actual}", - ); - } - - #[test] - fn fuzz_max( - a in f64_compatible_float(), - b in f64_compatible_float(), - ) { - let sol_result = a.max(b).unwrap(); - let a_f64 = sol_to_f64(a).unwrap(); - let b_f64 = sol_to_f64(b).unwrap(); - let expected = a_f64.max(b_f64); - let actual = sol_to_f64(sol_result).unwrap(); - prop_assert!( - approx_eq(expected, actual), - "max: max({a_f64}, {b_f64}) = {expected}, sol = {actual}", - ); - } - - #[test] - fn fuzz_is_zero(a in f64_compatible_float()) { - let a_f64 = sol_to_f64(a).unwrap(); - let sol_result = a.is_zero().unwrap(); - let expected = a_f64 == 0.0; - prop_assert!( - sol_result == expected, - "is_zero: is_zero({}) = {}, sol = {}", - a_f64, expected, sol_result - ); - } - - #[test] - fn fuzz_fixed_decimal_round_trip( - coefficient in 0i64..10i64.pow(15), - decimals in 0u8..18u8, - ) { - use alloy::primitives::U256; - let value = U256::from(coefficient as u64); - // Convert to float and back. - let float = Float::from_fixed_decimal(value, decimals); - prop_assume!(float.is_ok()); - let float = float.unwrap(); - let (back, lossless) = float.to_fixed_decimal_lossy(decimals).unwrap(); - if lossless { - prop_assert!( - back == value, - "round-trip failed: {} with {} decimals, got {}", - coefficient, decimals, back - ); - } - } - - #[test] - fn fuzz_comparisons( - a in f64_compatible_float(), - b in f64_compatible_float(), - ) { - let a_f64 = sol_to_f64(a).unwrap(); - let b_f64 = sol_to_f64(b).unwrap(); - - // Only test comparisons when values are far enough apart - // that f64 precision issues don't cause false failures. - let diff = (a_f64 - b_f64).abs(); - let max_abs = a_f64.abs().max(b_f64.abs()); - prop_assume!(diff > max_abs * 1e-10 || diff < 1e-30); - - let sol_lt = a.lt(b).unwrap(); - let sol_gt = a.gt(b).unwrap(); - let sol_eq = a.eq(b).unwrap(); - - if diff < 1e-30 { - // Both effectively zero. - prop_assert!(sol_eq, "eq: {a_f64} == {b_f64} should be true"); - } else if a_f64 < b_f64 { - prop_assert!(sol_lt, "lt: {a_f64} < {b_f64} should be true"); - prop_assert!(!sol_gt, "gt: {a_f64} > {b_f64} should be false"); - } else { - prop_assert!(sol_gt, "gt: {a_f64} > {b_f64} should be true"); - prop_assert!(!sol_lt, "lt: {a_f64} < {b_f64} should be false"); - } - } - } -} From d1d86a9df64387faedc41b0f3306411b3258fef3 Mon Sep 17 00:00:00 2001 From: baku-ccron Date: Mon, 7 Sep 2026 16:29:21 +0000 Subject: [PATCH 2/2] Drop the moved test module bodies from lib.rs Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01QyCCzi9WZPhuXcU1hwr2bq --- crates/float/src/lib.rs | 974 ---------------------------------------- 1 file changed, 974 deletions(-) diff --git a/crates/float/src/lib.rs b/crates/float/src/lib.rs index 6b283f5..4670e80 100644 --- a/crates/float/src/lib.rs +++ b/crates/float/src/lib.rs @@ -11,7 +11,6 @@ use alloy::primitives::aliases::I224; pub mod error; mod evm; -mod fuzz_ops; pub mod js_api; #[cfg(any(test, feature = "test-harness"))] pub mod tables; @@ -1285,13 +1284,9 @@ impl From for B256 { value.0 } } - #[cfg(test)] mod tests { - use crate::DecimalFloat::DecimalFloatErrors; - use super::*; - use core::str::FromStr; use proptest::prelude::*; use serde_json::json; @@ -1302,21 +1297,6 @@ mod tests { assert!(zero.eq(Float::default()).unwrap()); } - /// Float::zero() is_zero, formats as "0", equals parsed "0" and default. - #[test] - fn test_zero() { - let zero = Float::zero().unwrap(); - assert!(zero.is_zero().unwrap()); - assert_eq!(zero.format().unwrap(), "0"); - - // Test that zero equals parsed zero - let parsed_zero = Float::parse("0".to_string()).unwrap(); - assert!(zero.eq(parsed_zero).unwrap()); - - // Test that zero equals default - assert!(zero.eq(Float::default()).unwrap()); - } - prop_compose! { fn arb_float()( coefficient in any::(), @@ -1326,21 +1306,6 @@ mod tests { } } - prop_compose! { - fn reasonable_float()( - int_part in -10i128.pow(18)..10i128.pow(18), - decimal_part in 0u128..10u128.pow(18u32) - ) -> Float { - let num_str = if decimal_part == 0 { - format!("{int_part}") - } else { - format!("{int_part}.{decimal_part}") - }; - - Float::parse(num_str).unwrap() - } - } - /// JSON serialize then deserialize preserves equality and hex representation. #[test] fn test_serde() { @@ -1366,95 +1331,6 @@ mod tests { } } - /// Parsing an empty string returns a DecimalFloatSelector error. - #[test] - fn test_parse_empty_string_error() { - let err = Float::parse("".to_string()).unwrap_err(); - // We don't know the exact selector here, just ensure the error path is hit. - assert!(matches!(err, FloatError::DecimalFloatSelector(_))); - } - - #[test] - fn test_parse_exponent_overflow_error() { - // Extremely large exponent expected to overflow (exponent >> i32::MAX). - let err = Float::parse("1e3000000000".to_string()).unwrap_err(); - assert!(matches!( - err, - FloatError::DecimalFloat(e) if matches!(*e, DecimalFloatErrors::ExponentOverflow(_)) - )); - } - - /// Malformed inputs ("1.2.3", "abc") return specific error selectors. - #[test] - fn test_parse_edge_cases() { - let err = Float::parse("1.2.3".to_string()).unwrap_err(); - assert!(matches!( - err, - FloatError::DecimalFloatSelector(Err(selector)) - if selector == fixed_bytes!("ad384e87") - )); - - let err = Float::parse("abc".to_string()).unwrap_err(); - assert!(matches!( - err, - FloatError::DecimalFloatSelector(Err(selector)) - if selector == fixed_bytes!("34bd2069") - )); - } - - /// Boundary constants are distinct, correctly signed, correctly ordered, - /// and bound normal values like 1 and -1. - #[test] - fn test_float_constants() { - // Test that all constant methods return valid floats - let max_pos = Float::max_positive_value().unwrap(); - let min_pos = Float::min_positive_value().unwrap(); - let max_neg = Float::max_negative_value().unwrap(); - let min_neg = Float::min_negative_value().unwrap(); - - let zero = Float::parse("0".to_string()).unwrap(); - - // Test mathematical properties without exposing binary representation - - // All constants should be distinct - assert!(!max_pos.eq(min_pos).unwrap()); - assert!(!max_neg.eq(min_neg).unwrap()); - assert!(!max_pos.eq(max_neg).unwrap()); - assert!(!min_pos.eq(min_neg).unwrap()); - - // Test sign properties - assert!(min_pos.gt(zero).unwrap()); // min positive should be > 0 - assert!(max_pos.gt(zero).unwrap()); // max positive should be > 0 - assert!(max_neg.lt(zero).unwrap()); // max negative should be < 0 - assert!(min_neg.lt(zero).unwrap()); // min negative should be < 0 - - // Test ordering relationships - assert!(min_pos.lt(max_pos).unwrap()); // min positive < max positive - assert!(min_neg.lt(max_neg).unwrap()); // min negative < max negative - - // Test boundary properties - let one = Float::parse("1".to_string()).unwrap(); - let neg_one = Float::parse("-1".to_string()).unwrap(); - - // Positive constants should be greater than normal values - assert!(max_pos.gt(one).unwrap()); - assert!(min_pos.lt(one).unwrap()); - - // Negative constants should be more extreme than normal negative values - assert!(max_neg.gt(neg_one).unwrap()); - assert!(min_neg.lt(neg_one).unwrap()); - } - - proptest! { - #[test] - /// format() then parse() round-trips to an equal value. - fn test_format_parse(float in reasonable_float()) { - let formatted = float.format().unwrap(); - let parsed = Float::parse(formatted.clone()).unwrap(); - prop_assert!(float.eq(parsed).unwrap()); - } - } - proptest! { #[test] /// as_hex() then from_hex() round-trips to identical hex. @@ -1464,854 +1340,4 @@ mod tests { prop_assert_eq!(parsed.as_hex(), hex); } } - - /// Adding two max-exponent floats overflows with ExponentOverflow. - #[test] - fn test_add_exponent_overflow_error() { - let max_coeff_str = "13479973333575319897333507543509815336818572211270286240551805124607"; - let large_coeff_i224 = I224::from_str(max_coeff_str).unwrap(); - let exponent_max = i32::MAX; - - let a = Float::pack_lossless(large_coeff_i224, exponent_max).unwrap(); - - let err = (a + a).unwrap_err(); - - assert!(matches!( - err, - FloatError::DecimalFloat(e) if matches!(*e, DecimalFloatErrors::ExponentOverflow(_)) - )); - } - - /// Subtracting opposite-sign max-exponent floats overflows. - #[test] - fn test_sub_exponent_overflow_error() { - let max_coeff_str = "13479973333575319897333507543509815336818572211270286240551805124607"; - let large_coeff_i224 = I224::from_str(max_coeff_str).unwrap(); - let exponent_max = i32::MAX; - - let a = Float::pack_lossless(large_coeff_i224, exponent_max).unwrap(); - let b = Float::pack_lossless(-large_coeff_i224, exponent_max).unwrap(); - - let err = (b - a).unwrap_err(); - - assert!(matches!( - err, - FloatError::DecimalFloat(e) if matches!(*e, DecimalFloatErrors::ExponentOverflow(_)) - )); - } - - proptest! { - #[test] - /// Addition does not panic for reasonable inputs. - fn test_add(a in reasonable_float(), b in reasonable_float()) { - (a + b).unwrap(); - } - } - - proptest! { - #[test] - /// Subtraction does not panic for reasonable inputs. - fn test_sub(a in reasonable_float(), b in reasonable_float()) { - (a - b).unwrap(); - } - } - - proptest! { - #[test] - /// (a + b) - b == a: subtraction inverts addition. - fn test_add_sub(a in reasonable_float(), b in reasonable_float()) { - let sum = (a + b).unwrap(); - let diff = (sum - b).unwrap(); - prop_assert_eq!( - a.format().unwrap(), - diff.format().unwrap(), - "a: {}, b: {}", - a.format().unwrap(), - b.format().unwrap(), - ); - } - } - - /// Manual check: -1 < 0 < 3, with correct lt/eq/gt for each pair. - #[test] - fn test_lt_eq_gt() { - let negone = Float::parse("-1".to_string()).unwrap(); - let zero = Float::parse("0".to_string()).unwrap(); - let three = Float::parse("3".to_string()).unwrap(); - - assert!(negone.lt(zero).unwrap()); - assert!(!negone.eq(zero).unwrap()); - assert!(!negone.gt(zero).unwrap()); - - assert!(!three.lt(zero).unwrap()); - assert!(!three.eq(zero).unwrap()); - assert!(three.gt(zero).unwrap()); - - assert!(zero.lt(three).unwrap()); - assert!(!zero.eq(three).unwrap()); - assert!(!zero.gt(three).unwrap()); - } - - proptest! { - #[test] - /// a == a, a-1 < a, a+1 > a for all reasonable floats. - fn test_lt_eq_gt_with_add(a in reasonable_float()) { - let b = a; - let eq = a.eq(b).unwrap(); - prop_assert!(eq); - - let one = Float::parse("1".to_string()).unwrap(); - - let a = (a - one).unwrap(); - let lt = a.lt(b).unwrap(); - prop_assert!(lt); - - let a = (a + one).unwrap(); - let eq = a.eq(b).unwrap(); - prop_assert!(eq); - - let a = (a + one).unwrap(); - let gt = a.gt(b).unwrap(); - prop_assert!(gt); - } - - #[test] - /// Trichotomy: exactly one of lt, eq, gt is true for any two floats. - fn test_exactly_one_lt_eq_gt(a in arb_float(), b in arb_float()) { - let eq = a.eq(b).unwrap(); - let lt = a.lt(b).unwrap(); - let gt = a.gt(b).unwrap(); - - let a_str = a.show_unpacked().unwrap(); - let b_str = b.show_unpacked().unwrap(); - - prop_assert!(lt || eq || gt, "a: {a_str}, b: {b_str}"); - prop_assert!(!(lt && eq), "both less than and equal: a: {a_str}, b: {b_str}"); - prop_assert!(!(eq && gt), "both equal and greater than: a: {a_str}, b: {b_str}"); - prop_assert!(!(lt && gt), "both less than and greater than: a: {a_str}, b: {b_str}"); - } - } - - /// abs(-x) == abs(x) == |x| for manual positive, negative, and zero cases. - #[test] - fn test_abs() { - let float = Float::parse("-3613.1324123".to_string()).unwrap(); - let abs = float.abs().unwrap(); - let formatted = abs.format().unwrap(); - assert_eq!(formatted, "3613.1324123"); - - let float = Float::parse("3613.1324123".to_string()).unwrap(); - let abs = float.abs().unwrap(); - let formatted = abs.format().unwrap(); - assert_eq!(formatted, "3613.1324123"); - - let float = Float::parse("0".to_string()).unwrap(); - let abs = float.abs().unwrap(); - let formatted = abs.format().unwrap(); - assert_eq!(formatted, "0"); - } - - proptest! { - #[test] - /// Multiplication does not panic for reasonable inputs. - fn test_mul(a in reasonable_float(), b in reasonable_float()) { - (a * b).unwrap(); - } - } - - /// Negating a negative produces positive format; negating zero stays "0". - #[test] - fn test_minus_format() { - let float = Float::parse("-123.1234234625468391".to_string()).unwrap(); - let negated = float.neg().unwrap(); - - let formatted_decimal = negated.format_with_scientific(false).unwrap(); - assert_eq!(formatted_decimal, "123.1234234625468391"); - - let float = Float::parse("0".to_string()).unwrap(); - let negated = float.neg().unwrap(); - let formatted = negated.format().unwrap(); - assert_eq!(formatted, "0"); - } - - proptest! { - #[test] - /// Double negation is identity: -(-a) == a. - fn test_minus_minus(float in arb_float()) { - let negated = float.neg().unwrap(); - let renegated = negated.neg().unwrap(); - prop_assert!(float.eq(renegated).unwrap()); - } - } - - proptest! { - #[test] - /// a * inv(a) ≈ 1 within ±1e-37 for nonzero a. - fn test_inv_prod(float in reasonable_float()) { - let zero = Float::parse("0".to_string()).unwrap(); - prop_assume!(!float.eq(zero).unwrap()); - - let inv = float.inv().unwrap(); - let product = (float * inv).unwrap(); - let one = Float::parse("1".to_string()).unwrap(); - - // Allow for minor rounding errors introduced by the lossy - // `inv` implementation. We consider the property to - // hold if the product is within `±1e-37` of 1. - - let eps = Float::parse("1e-37".to_string()).unwrap(); - let one_plus_eps = (one + eps).unwrap(); - let one_minus_eps = (one - eps).unwrap(); - - let within_upper = !product.gt(one_plus_eps).unwrap(); - let within_lower = !product.lt(one_minus_eps).unwrap(); - - prop_assert!( - within_upper && within_lower, - "float: {}, inv: {}, product: {} (not within ±ε)", - float.show_unpacked().unwrap(), - inv.show_unpacked().unwrap(), - product.show_unpacked().unwrap(), - ); - } - } - - proptest! { - #[test] - /// abs() never produces a string starting with "-". - fn test_abs_no_minus_sign(float in reasonable_float()) { - let abs = float.abs().unwrap(); - let formatted = abs.format().unwrap(); - prop_assert!(!formatted.starts_with("-")); - } - - #[test] - /// abs is idempotent: abs(abs(a)) == abs(a). - fn test_abs_abs(float in arb_float()) { - let abs = float.abs().unwrap(); - let abs_abs = abs.abs().unwrap(); - prop_assert!(abs.eq(abs_abs).unwrap()); - } - } - - proptest! { - #[test] - /// Division does not panic for nonzero divisor. - fn test_div(a in reasonable_float(), b in reasonable_float()) { - let zero = Float::parse("0".to_string()).unwrap(); - prop_assume!(!b.eq(zero).unwrap()); - - (a / b).unwrap(); - } - } - - prop_compose! { - fn small_int_float()(int_part in -1_000_000_000_000i128..1_000_000_000_000i128) -> Float { - Float::parse(int_part.to_string()).unwrap() - } - } - - proptest! { - #[test] - /// (a * b) / b == a: division inverts multiplication for small integers. - fn test_mul_div_int(a in small_int_float(), b in small_int_float()) { - let zero = Float::parse("0".to_string()).unwrap(); - prop_assume!(!b.eq(zero).unwrap()); - - let product = (a * b).unwrap(); - let quotient = (product / b).unwrap(); - - prop_assert!( - a.eq(quotient).unwrap(), - "a: {}, quotient: {}, b: {}", - a.show_unpacked().unwrap(), - quotient.show_unpacked().unwrap(), - b.show_unpacked().unwrap() - ); - } - } - - /// 6/3 == 2 and 2*3 == 6. - #[test] - fn test_mul_div_manual() { - let two = Float::parse("2".to_string()).unwrap(); - let three = Float::parse("3".to_string()).unwrap(); - let six = Float::parse("6".to_string()).unwrap(); - - assert!(two.eq((six / three).unwrap()).unwrap()); - assert!(six.eq((two * three).unwrap()).unwrap()); - } - - /// 1/0 returns DivisionByZero error. - #[test] - fn test_divide_by_zero_error() { - let one = Float::parse("1".to_string()).unwrap(); - let zero = Float::parse("0".to_string()).unwrap(); - let err = (one / zero).unwrap_err(); - - assert!(matches!( - err, - FloatError::DecimalFloat(e) if matches!(*e, DecimalFloatErrors::DivisionByZero(_)) - )); - } - - /// Multiplying near-max exponents overflows. - #[test] - fn test_mul_exponent_overflow_error() { - let near_max_exp = Float::parse("1e2147483646".to_string()).unwrap(); - let one_e_two = Float::parse("1e2".to_string()).unwrap(); - - let err = (near_max_exp * one_e_two).unwrap_err(); - assert!(matches!( - err, - FloatError::DecimalFloat(e) if matches!(*e, DecimalFloatErrors::ExponentOverflow(_)) - )); - } - - /// Dividing near-max exponent by small exponent overflows. - #[test] - fn test_div_exponent_overflow_error() { - let near_max_exp = Float::parse("1e2147483646".to_string()).unwrap(); - let one_e_neg_hundred = Float::parse("1e-100".to_string()).unwrap(); - - let err = (near_max_exp / one_e_neg_hundred).unwrap_err(); - assert!(matches!( - err, - FloatError::DecimalFloat(e) if matches!(*e, DecimalFloatErrors::ExponentOverflow(_)) - )); - } - - /// Multiplying near-min exponents underflows; the public arithmetic - /// surface reverts with `ExponentUnderflow` rather than silently - /// producing zero. - #[test] - fn test_mul_exponent_underflow_error() { - let near_min_exp = Float::parse("1e-2147483646".to_string()).unwrap(); - let one_e_neg_three = Float::parse("1e-3".to_string()).unwrap(); - - let err = (near_min_exp * one_e_neg_three).unwrap_err(); - assert!(matches!( - err, - FloatError::DecimalFloat(e) if matches!(*e, DecimalFloatErrors::ExponentUnderflow(_)) - )); - } - - /// from_fixed_decimal for known value/decimals pairs matches parsed strings. - #[test] - fn test_from_fixed_decimal() { - let cases = vec![ - (U256::from(0u128), 0u8, "0"), - (U256::from(0u128), 18u8, "0"), - (U256::from(1u128), 18u8, "1e-18"), - (U256::from(123456789u128), 0u8, "123456789"), - (U256::from(123456789u128), 2u8, "123456789e-2"), - (U256::from(1000000000000000000u128), 18u8, "1"), - ]; - - for (amount, decimals, expected) in cases { - let float = Float::from_fixed_decimal(amount, decimals).expect("should convert"); - let expected = Float::parse(expected.to_string()).unwrap(); - assert!(float.eq(expected).unwrap()); - } - } - - /// U256::MAX with 1 decimal overflows (LossyConversionToFloat). - #[test] - fn test_from_fixed_decimal_err() { - let err = Float::from_fixed_decimal(U256::MAX, 1).unwrap_err(); - assert!(matches!( - err, - FloatError::DecimalFloat(e) if matches!(*e, DecimalFloatErrors::LossyConversionToFloat(_)) - )); - } - - /// to_fixed_decimal for known inputs matches expected U256 values. - #[test] - fn test_to_fixed_decimal() { - let cases = vec![ - ("0", 0u8, 0u128), - ("0", 18u8, 0u128), - ("1e-18", 18u8, 1u128), - ("123456789", 0u8, 123456789u128), - ("123456789e-2", 2u8, 123456789u128), - ("1", 18u8, 1000000000000000000u128), - ]; - - for (input, decimals, expected) in cases { - let float = Float::parse(input.to_string()).unwrap(); - let fixed = float.to_fixed_decimal(decimals).unwrap(); - assert_eq!(fixed, U256::from(expected)); - } - } - - /// For integers: floor == self, frac == 0, and floor + frac == self. - #[test] - fn test_frac_and_floor_integers() { - let int_float = Float::parse("12345".to_string()).unwrap(); - let floor = int_float.floor().unwrap(); - let frac = int_float.frac().unwrap(); - let zero = Float::parse("0".to_string()).unwrap(); - - assert!(int_float.eq(floor).unwrap()); - assert!(frac.eq(zero).unwrap()); - - let int_float = Float::parse("-98765".to_string()).unwrap(); - let floor = int_float.floor().unwrap(); - let frac = int_float.frac().unwrap(); - let zero = Float::parse("0".to_string()).unwrap(); - - assert!(int_float.eq(floor).unwrap()); - assert!(frac.eq(zero).unwrap()); - - let recombined = (floor + frac).unwrap(); - assert!(int_float.eq(recombined).unwrap()); - } - - /// floor(12345.6789) == 12345, frac(12345.6789) == 0.6789. - #[test] - fn test_frac_and_floor_floats() { - let float = Float::parse("12345.6789".to_string()).unwrap(); - let floor = float.floor().unwrap(); - let frac = float.frac().unwrap(); - - let expected_floor = Float::parse("12345".to_string()).unwrap(); - let expected_frac = Float::parse("0.6789".to_string()).unwrap(); - - assert!(floor.eq(expected_floor).unwrap()); - assert!(frac.eq(expected_frac).unwrap()); - } - - /// integer(12345.6789) == 12345, and integer + frac == original. - #[test] - fn test_integer_positive() { - let float = Float::parse("12345.6789".to_string()).unwrap(); - let int = float.integer().unwrap(); - let expected = Float::parse("12345".to_string()).unwrap(); - assert!(int.eq(expected).unwrap()); - - let frac = float.frac().unwrap(); - let recombined = (int + frac).unwrap(); - assert!(float.eq(recombined).unwrap()); - } - - /// integer truncates toward zero: integer(-12345.6789) == -12345. - #[test] - fn test_integer_negative() { - let float = Float::parse("-12345.6789".to_string()).unwrap(); - let int = float.integer().unwrap(); - let frac = float.frac().unwrap(); - - // integer truncates toward zero, so -12345.6789 -> -12345 - let expected_int = Float::parse("-12345".to_string()).unwrap(); - let expected_frac = Float::parse("-0.6789".to_string()).unwrap(); - - assert!(int.eq(expected_int).unwrap()); - assert!(frac.eq(expected_frac).unwrap()); - - // integer + frac == original - let recombined = (int + frac).unwrap(); - assert!(float.eq(recombined).unwrap()); - } - - /// integer(42) == 42, frac(42) == 0 for positive and negative whole numbers. - #[test] - fn test_integer_whole_numbers() { - let pos = Float::parse("42".to_string()).unwrap(); - assert!(pos.integer().unwrap().eq(pos).unwrap()); - let zero = Float::parse("0".to_string()).unwrap(); - assert!(pos.frac().unwrap().eq(zero).unwrap()); - - let neg = Float::parse("-42".to_string()).unwrap(); - assert!(neg.integer().unwrap().eq(neg).unwrap()); - assert!(neg.frac().unwrap().eq(zero).unwrap()); - } - - proptest! { - #[test] - /// from_fixed_decimal then to_fixed_decimal round-trips for any non-negative I224. - fn test_from_to_fixed_decimal_valid_range(coeff in any::(), decimals in 0u8..=66u8) { - prop_assume!(coeff >= I224::ZERO); - - let exponent = -(decimals as i32); - let value = U256::from(coeff); - - let float = Float::from_fixed_decimal(value, decimals).unwrap(); - let expected = Float::pack_lossless(coeff, exponent).unwrap(); - prop_assert!(float.eq(expected).unwrap()); - - let fixed = float.to_fixed_decimal(decimals).unwrap(); - assert_eq!(fixed, value); - } - } - - proptest! { - #[test] - /// integer(a) + frac(a) == a, frac has no integer part, integer has - /// no fractional part, and |frac| < 1. - fn test_int_frac_properties(float in arb_float()) { - let int = float.integer().unwrap(); - let frac = float.frac().unwrap(); - - let zero = Float::parse("0".to_string()).unwrap(); - - prop_assert!( - int.frac().unwrap().eq(zero).unwrap(), - "int.frac() is not zero: {}", - int.show_unpacked().unwrap() - ); - - prop_assert!( - frac.integer().unwrap().eq(zero).unwrap(), - "frac.integer() is not zero: {}", - frac.show_unpacked().unwrap() - ); - - let recombined = (int + frac).unwrap(); - prop_assert!( - float.eq(recombined).unwrap(), - "original: {}, int: {}, frac: {}, recombined: {}", - float.show_unpacked().unwrap(), - int.show_unpacked().unwrap(), - frac.show_unpacked().unwrap(), - recombined.show_unpacked().unwrap() - ); - - let one = Float::parse("1".to_string()).unwrap(); - let neg_one = one.neg().unwrap(); - prop_assert!( - frac.lt(one).unwrap(), - "frac not < 1: {}", - frac.show_unpacked().unwrap() - ); - prop_assert!( - frac.gt(neg_one).unwrap(), - "frac not > -1: {}", - frac.show_unpacked().unwrap() - ); - } - } - - /// min/max for known value pairs, including identical arguments. - #[test] - fn test_min_max_manual() { - let negone = Float::parse("-1".to_string()).unwrap(); - let zero = Float::parse("0".to_string()).unwrap(); - let three = Float::parse("3".to_string()).unwrap(); - let seven = Float::parse("7".to_string()).unwrap(); - - // --- min --- - assert!(negone.eq(negone.min(zero).unwrap()).unwrap()); - assert!(negone.eq(negone.min(three).unwrap()).unwrap()); - assert!(zero.eq(zero.min(three).unwrap()).unwrap()); - // min with identical arguments should return that argument - assert!(seven.eq(seven.min(seven).unwrap()).unwrap()); - - // --- max --- - assert!(zero.eq(negone.max(zero).unwrap()).unwrap()); - assert!(three.eq(negone.max(three).unwrap()).unwrap()); - assert!(three.eq(zero.max(three).unwrap()).unwrap()); - // max with identical arguments should return that argument - assert!(seven.eq(seven.max(seven).unwrap()).unwrap()); - } - - /// is_zero for "0", "-0", "0.0" (all true) and "1" (false). - #[test] - fn test_is_zero_manual() { - let zero = Float::parse("0".to_string()).unwrap(); - assert!(zero.is_zero().unwrap()); - - // Alternative zero representations that should also be considered zero. - let neg_zero = Float::parse("-0".to_string()).unwrap(); - assert!(neg_zero.is_zero().unwrap()); - let zero_point = Float::parse("0.0".to_string()).unwrap(); - assert!(zero_point.is_zero().unwrap()); - - let one = Float::parse("1".to_string()).unwrap(); - assert!(!one.is_zero().unwrap()); - } - - proptest! { - #[test] - /// min(a,b) <= both, max(a,b) >= both, each equals one operand, - /// and min <= max. - fn test_min_max_properties(a in reasonable_float(), b in reasonable_float()) { - let min = a.min(b).unwrap(); - let max = a.max(b).unwrap(); - - prop_assert!( - !min.gt(a).unwrap(), - "min > a: min={}, a={}", - min.show_unpacked().unwrap(), - a.show_unpacked().unwrap() - ); - prop_assert!( - !min.gt(b).unwrap(), - "min > b: min={}, b={}", - min.show_unpacked().unwrap(), - b.show_unpacked().unwrap() - ); - - prop_assert!( - !max.lt(a).unwrap(), - "max < a: max={}, a={}", - max.show_unpacked().unwrap(), - a.show_unpacked().unwrap() - ); - prop_assert!( - !max.lt(b).unwrap(), - "max < b: max={}, b={}", - max.show_unpacked().unwrap(), - b.show_unpacked().unwrap() - ); - - let min_is_a = min.eq(a).unwrap(); - let min_is_b = min.eq(b).unwrap(); - prop_assert!( - min_is_a || min_is_b, - "min is not equal to either operand: a={}, b={}, min={}", - a.show_unpacked().unwrap(), - b.show_unpacked().unwrap(), - min.show_unpacked().unwrap() - ); - - let max_is_a = max.eq(a).unwrap(); - let max_is_b = max.eq(b).unwrap(); - prop_assert!( - max_is_a || max_is_b, - "max is not equal to either operand: a={}, b={}, max={}", - a.show_unpacked().unwrap(), - b.show_unpacked().unwrap(), - max.show_unpacked().unwrap() - ); - - prop_assert!( - !min.gt(max).unwrap(), - "min > max: min={}, max={}", - min.show_unpacked().unwrap(), - max.show_unpacked().unwrap() - ); - } - } - - /// Manual lte/gte checks: -1 <= 0 <= 3, 0 >= -1, 3 >= 0. - #[test] - fn test_lte_gte() { - let negone = Float::parse("-1".to_string()).unwrap(); - let zero = Float::parse("0".to_string()).unwrap(); - let three = Float::parse("3".to_string()).unwrap(); - - assert!(negone.lte(zero).unwrap()); - assert!(zero.lte(three).unwrap()); - assert!(negone.lte(three).unwrap()); - - assert!(zero.gte(negone).unwrap()); - assert!(three.gte(zero).unwrap()); - assert!(three.gte(negone).unwrap()); - } - - proptest! { - #[test] - /// a-1 lte a, a lte a and gte a, a+1 gte a. - fn test_lte_gte_fuzz(a in reasonable_float()) { - let b = a; - let one = Float::parse("1".to_string()).unwrap(); - - let a = (a - one).unwrap(); - let lte = a.lte(b).unwrap(); - prop_assert!(lte); // lt - - let a = (a + one).unwrap(); - let gte = a.gte(b).unwrap(); - let lte = a.lte(b).unwrap(); - prop_assert!(gte); // eq - prop_assert!(lte); // eq - - let a = (a + one).unwrap(); - let gte = a.gte(b).unwrap(); - prop_assert!(gte); // gt - } - } - - /// from_fixed_decimal_lossy: lossless for small values, lossy for U256::MAX. - #[test] - fn test_from_fixed_decimal_lossy() { - // Test lossless conversions (values that fit in Float's precision) - let lossless_cases = vec![ - (U256::from(0u128), 0u8, "0"), - (U256::from(0u128), 18u8, "0"), - (U256::from(1u128), 18u8, "1e-18"), - (U256::from(123456789u128), 0u8, "123456789"), - (U256::from(123456789u128), 2u8, "123456789e-2"), - (U256::from(1000000000000000000u128), 18u8, "1"), - ]; - - for (amount, decimals, expected) in lossless_cases { - let (float, lossless) = - Float::from_fixed_decimal_lossy(amount, decimals).expect("should convert"); - let expected = Float::parse(expected.to_string()).unwrap(); - assert!(float.eq(expected).unwrap()); - assert!( - lossless, - "conversion should be lossless for amount={}, decimals={}", - amount, decimals - ); - } - - // Test lossy conversion with U256::MAX (too large to fit in Float's 224-bit coefficient) - let (float, lossless) = Float::from_fixed_decimal_lossy(U256::MAX, 1).unwrap(); - assert!(!lossless, "U256::MAX conversion should be lossy"); - assert!(!float.is_zero().unwrap(), "result should not be zero"); - } - - /// to_fixed_decimal_lossy: correctly reports lossy/lossless for precision loss. - #[test] - fn test_to_fixed_decimal_lossy() { - // Test lossy conversions (loss of precision) - let lossy_cases = vec![ - (U256::from(1), 18u8, 0u128), - (U256::from(123456789), 0u8, 12345678u128), - (U256::from(123456789), 2u8, 12345678u128), - ]; - - for (input, decimals, expected) in lossy_cases { - let float = Float::from_fixed_decimal(input, decimals + 1).unwrap(); - let (fixed, lossless) = float.to_fixed_decimal_lossy(decimals).unwrap(); - assert_eq!( - fixed, - U256::from(expected), - "wrong value for input={}, decimals={}", - input, - decimals - ); - assert!( - !lossless, - "should be lossy for input={}, decimals={}", - input, decimals - ); - } - - // Test lossless conversions (no loss of precision) - let lossless_cases = vec![ - // Zero is always lossless - (U256::from(0), 0u8, 0u128), - (U256::from(0), 18u8, 0u128), - // Converting 12340 with 3 decimals (12.340) to 2 decimals (12.34) is lossless - (U256::from(12340), 3u8, 1234u128), - ]; - - for (input, decimals, expected) in lossless_cases { - let float = Float::from_fixed_decimal(input, decimals + 1).unwrap(); - let (fixed, lossless) = float.to_fixed_decimal_lossy(decimals).unwrap(); - assert_eq!( - fixed, - U256::from(expected), - "wrong value for input={}, decimals={}", - input, - decimals - ); - assert!( - lossless, - "should be lossless for input={}, decimals={}", - input, decimals - ); - } - } - - proptest! { - #[test] - /// Lossy fixed-decimal round-trip: from(decimals+1) then to(decimals) is - /// lossy iff the last digit is nonzero. - fn test_from_to_fixed_decimal_lossy_valid_range(coeff in any::(), decimals in 0u8..=66u8) { - prop_assume!(coeff >= I224::ZERO); - - let exponent = -(decimals as i32 + 1); - let value = U256::from(coeff); - - let (float, from_lossless) = Float::from_fixed_decimal_lossy(value, decimals + 1).unwrap(); - let expected = Float::pack_lossless(coeff, exponent).unwrap(); - prop_assert!(float.eq(expected).unwrap()); - - // from_fixed_decimal_lossy should be lossless for values that fit in Float's precision - prop_assert!(from_lossless, "from_fixed_decimal_lossy should be lossless for coeff={coeff}"); - - let (fixed, to_lossless) = float.to_fixed_decimal_lossy(decimals).unwrap(); - assert_eq!(fixed, value / U256::from(10)); - - // Converting from decimals+1 to decimals should be lossy unless the value is zero or - // the last digit is zero (divisible by 10) - if value == U256::ZERO || value % U256::from(10) == U256::ZERO { - prop_assert!(to_lossless, "to_fixed_decimal_lossy should be lossless when last digit is 0: value={}", value); - } else { - prop_assert!(!to_lossless, "to_fixed_decimal_lossy should be lossy when losing precision: value={}", value); - } - } - } - - proptest! { - #[test] - /// All reasonable positive floats are bounded by min/max_positive_value, - /// all negative by min/max_negative_value. - fn test_constants_relationships(float in reasonable_float()) { - let max_pos = Float::max_positive_value().unwrap(); - let min_pos = Float::min_positive_value().unwrap(); - let max_neg = Float::max_negative_value().unwrap(); - let min_neg = Float::min_negative_value().unwrap(); - let zero = Float::parse("0".to_string()).unwrap(); - - // Test that constants are the extremes - // Any reasonable positive float should be <= max_positive and >= min_positive - if float.gt(zero).unwrap() { - prop_assert!(float.lte(max_pos).unwrap()); - prop_assert!(float.gte(min_pos).unwrap()); - } - - // Any reasonable negative float should be <= max_negative and >= min_negative - // (max_negative is closest to zero, min_negative is furthest from zero) - if float.lt(zero).unwrap() { - prop_assert!(float.lte(max_neg).unwrap()); - prop_assert!(float.gte(min_neg).unwrap()); - } - - // Constants should be consistent regardless of arbitrary float - prop_assert!(max_pos.gt(zero).unwrap()); - prop_assert!(min_pos.gt(zero).unwrap()); - prop_assert!(max_neg.lt(zero).unwrap()); - prop_assert!(min_neg.lt(zero).unwrap()); - - // Verify constants maintain their ordering - prop_assert!(min_pos.lt(max_pos).unwrap()); - prop_assert!(min_neg.lt(max_neg).unwrap()); - prop_assert!(max_neg.lt(zero).unwrap()); - prop_assert!(min_pos.gt(zero).unwrap()); - } - } - - proptest! { - #[test] - /// No arbitrary float exceeds max_positive or is below min_negative. - fn test_constants_edge_cases(float in arb_float()) { - let max_pos = Float::max_positive_value().unwrap(); - let min_pos = Float::min_positive_value().unwrap(); - let max_neg = Float::max_negative_value().unwrap(); - let min_neg = Float::min_negative_value().unwrap(); - - // Constants should always be distinct - prop_assert!(!max_pos.eq(min_pos).unwrap()); - prop_assert!(!max_neg.eq(min_neg).unwrap()); - prop_assert!(!max_pos.eq(max_neg).unwrap()); - prop_assert!(!min_pos.eq(min_neg).unwrap()); - - // Test that constants are at the boundaries - // (Note: We can't test arithmetic operations that would overflow/underflow - // since those would fail, but we can test comparisons) - - // No arbitrary float should be greater than max_pos or less than min_neg - if !float.eq(max_pos).unwrap() { - prop_assert!(!float.gt(max_pos).unwrap()); - } - if !float.eq(min_neg).unwrap() { - prop_assert!(!float.lt(min_neg).unwrap()); - } - } - } }