// tests/property_tests.rs use proptest::prelude::*; use steel_decimal::*; use rust_decimal::Decimal; use std::str::FromStr; // Strategy for generating valid decimal strings fn decimal_string() -> impl Strategy { prop_oneof![ // Small integers (-1000i32..1000i32).prop_map(|i| i.to_string()), // Small decimals with 1-6 decimal places ( -1000i32..1000i32, 1..1000000u32 ).prop_map(|(whole, frac)| { let frac_str = format!("{:06}", frac); format!("{}.{}", whole, frac_str.trim_end_matches('0')) }), // Scientific notation ( -100i32..100i32, -10i32..10i32 ).prop_map(|(mantissa, exp)| format!("{}e{}", mantissa, exp)), // Very small numbers Just("0.000000000000000001".to_string()), Just("0.000000000000000000000000001".to_string()), // Numbers at decimal precision limits Just("99999999999999999999999999.9999".to_string()), ] } // Strategy for generating valid precision values fn precision_value() -> impl Strategy { 0..=28u32 } // Property: Basic arithmetic operations preserve decimal precision semantics proptest! { #[test] fn test_arithmetic_commutativity( a in decimal_string(), b in decimal_string() ) { // Addition should be commutative: a + b = b + a let result1 = decimal_add(a.clone(), b.clone()); let result2 = decimal_add(b, a); match (result1, result2) { (Ok(r1), Ok(r2)) => { // Parse both results and compare as decimals let d1 = Decimal::from_str(&r1).unwrap(); let d2 = Decimal::from_str(&r2).unwrap(); prop_assert_eq!(d1, d2); } (Err(_), Err(_)) => { // Both should fail in the same way for invalid inputs } _ => prop_assert!(false, "Inconsistent error handling") } } #[test] fn test_multiplication_commutativity( a in decimal_string(), b in decimal_string() ) { let result1 = decimal_mul(a.clone(), b.clone()); let result2 = decimal_mul(b, a); match (result1, result2) { (Ok(r1), Ok(r2)) => { let d1 = Decimal::from_str(&r1).unwrap(); let d2 = Decimal::from_str(&r2).unwrap(); prop_assert_eq!(d1, d2); } (Err(_), Err(_)) => {} _ => prop_assert!(false, "Inconsistent error handling") } } #[test] fn test_addition_associativity( a in decimal_string(), b in decimal_string(), c in decimal_string() ) { // (a + b) + c = a + (b + c) let ab = decimal_add(a.clone(), b.clone()); let bc = decimal_add(b, c.clone()); if let (Ok(ab_result), Ok(bc_result)) = (ab, bc) { let left = decimal_add(ab_result, c); let right = decimal_add(a, bc_result); if let (Ok(left_final), Ok(right_final)) = (left, right) { let d1 = Decimal::from_str(&left_final).unwrap(); let d2 = Decimal::from_str(&right_final).unwrap(); prop_assert_eq!(d1, d2); } } } #[test] fn test_multiplication_by_zero(a in decimal_string()) { let result = decimal_mul(a, "0".to_string()); if let Ok(r) = result { let d = Decimal::from_str(&r).unwrap(); prop_assert!(d.is_zero()); } } #[test] fn test_addition_with_zero_identity(a in decimal_string()) { let result = decimal_add(a.clone(), "0".to_string()); match result { Ok(r) => { // Converting through decimal and back should give equivalent result if let Ok(original) = Decimal::from_str(&a) { let result_decimal = Decimal::from_str(&r).unwrap(); prop_assert_eq!(original, result_decimal); } } Err(_) => { // If a is invalid, this is expected prop_assert!(Decimal::from_str(&a).is_err()); } } } #[test] fn test_division_then_multiplication_inverse( a in decimal_string(), b in decimal_string().prop_filter("b != 0", |b| b != "0") ) { // (a / b) * b should approximately equal a let div_result = decimal_div(a.clone(), b.clone()); if let Ok(quotient) = div_result { let mul_result = decimal_mul(quotient, b); if let Ok(final_result) = mul_result { if let (Ok(original), Ok(final_decimal)) = (Decimal::from_str(&a), Decimal::from_str(&final_result)) { // Allow for small rounding differences let diff = (original - final_decimal).abs(); let tolerance = Decimal::from_str("0.000000000001").unwrap(); prop_assert!(diff <= tolerance, "Division-multiplication not inverse: {} vs {}", original, final_decimal); } } } } #[test] fn test_absolute_value_properties(a in decimal_string()) { let abs_result = decimal_abs(a.clone()); if let Ok(abs_val) = abs_result { let abs_decimal = Decimal::from_str(&abs_val).unwrap(); // abs(x) >= 0 prop_assert!(abs_decimal >= Decimal::ZERO); // abs(abs(x)) = abs(x) let double_abs = decimal_abs(abs_val); if let Ok(double_abs_val) = double_abs { let double_abs_decimal = Decimal::from_str(&double_abs_val).unwrap(); prop_assert_eq!(abs_decimal, double_abs_decimal); } } } #[test] fn test_comparison_transitivity( a in decimal_string(), b in decimal_string(), c in decimal_string() ) { // If a > b and b > c, then a > c let ab = decimal_gt(a.clone(), b.clone()); let bc = decimal_gt(b, c.clone()); let ac = decimal_gt(a, c); if let (Ok(true), Ok(true), Ok(ac_result)) = (ab, bc, ac) { prop_assert!(ac_result, "Transitivity violated for > comparison"); } } #[test] fn test_min_max_properties( a in decimal_string(), b in decimal_string() ) { let min_result = decimal_min(a.clone(), b.clone()); let max_result = decimal_max(a.clone(), b.clone()); if let (Ok(min_val), Ok(max_val)) = (min_result, max_result) { let min_decimal = Decimal::from_str(&min_val).unwrap(); let max_decimal = Decimal::from_str(&max_val).unwrap(); // min(a,b) <= max(a,b) prop_assert!(min_decimal <= max_decimal); // min(a,b) should equal either a or b if let (Ok(a_decimal), Ok(b_decimal)) = (Decimal::from_str(&a), Decimal::from_str(&b)) { prop_assert!(min_decimal == a_decimal || min_decimal == b_decimal); prop_assert!(max_decimal == a_decimal || max_decimal == b_decimal); } } } #[test] fn test_round_trip_conversion(a in decimal_string()) { // to_decimal should be idempotent for valid decimals let first_conversion = to_decimal(a.clone()); if let Ok(converted) = first_conversion { let second_conversion = to_decimal(converted.clone()); prop_assert_eq!(Ok(converted), second_conversion); } } #[test] fn test_precision_formatting_consistency( a in decimal_string(), precision in precision_value() ) { let formatted = decimal_format(a.clone(), precision); if let Ok(result) = formatted { // Formatting again with same precision should be idempotent let reformatted = decimal_format(result.clone(), precision); prop_assert_eq!(Ok(result.clone()), reformatted); // Result should have at most 'precision' decimal places if let Some(dot_pos) = result.find('.') { let decimal_part = &result[dot_pos + 1..]; prop_assert!(decimal_part.len() <= precision as usize); } } } #[test] fn test_sqrt_then_square_approximate_inverse( a in decimal_string().prop_filter("positive", |s| { Decimal::from_str(s).map(|d| d >= Decimal::ZERO).unwrap_or(false) }) ) { let sqrt_result = decimal_sqrt(a.clone()); if let Ok(sqrt_val) = sqrt_result { let square_result = decimal_mul(sqrt_val.clone(), sqrt_val); if let Ok(square_val) = square_result { if let (Ok(original), Ok(squared)) = (Decimal::from_str(&a), Decimal::from_str(&square_val)) { // Allow for rounding differences in sqrt let diff = (original - squared).abs(); let tolerance = Decimal::from_str("0.0001").unwrap(); prop_assert!(diff <= tolerance, "sqrt-square not approximate inverse: {} vs {}", original, squared); } } } } } // Property tests for parser transformation proptest! { #[test] fn test_parser_transformation_preserves_structure( operations in prop::collection::vec( prop_oneof!["+" , "-", "*", "/", "sqrt", "abs"], 1..5usize ) ) { let parser = ScriptParser::new(); // Generate a simple expression let expr = format!("({} 1 2)", operations[0]); let transformed = parser.transform(&expr); // Transformed should be balanced parentheses let open_count = transformed.chars().filter(|c| *c == '(').count(); let close_count = transformed.chars().filter(|c| *c == ')').count(); prop_assert_eq!(open_count, close_count); // Should contain decimal function prop_assert!(transformed.contains("decimal-")); } #[test] fn test_variable_extraction_correctness( var_names in prop::collection::vec("[a-zA-Z][a-zA-Z0-9_]*", 1..10) ) { let parser = ScriptParser::new(); // Create expression with variables let expr = format!("(+ ${})", var_names.join(" $")); let dependencies = parser.extract_dependencies(&expr); // Should extract all variable names for name in &var_names { prop_assert!(dependencies.contains(name)); } // Should not extract extra variables prop_assert_eq!(dependencies.len(), var_names.len()); } } // Fuzzing-style tests for edge cases proptest! { #[test] fn test_no_panics_on_random_input( input in ".*" ) { // These operations should never panic, only return errors let _ = to_decimal(input.clone()); let _ = decimal_add(input.clone(), "1".to_string()); let _ = decimal_abs(input.clone()); let parser = ScriptParser::new(); let _ = parser.transform(&input); let _ = parser.extract_dependencies(&input); } #[test] fn test_scientific_notation_consistency( mantissa in -1000f64..1000f64, exponent in -10i32..10i32 ) { let sci_notation = format!("{}e{}", mantissa, exponent); let conversion_result = to_decimal(sci_notation); // If conversion succeeds, result should be a valid decimal if let Ok(result) = conversion_result { prop_assert!(Decimal::from_str(&result).is_ok()); } } }