// tests/property_tests.rs use rstest::*; use steel_decimal::*; use rust_decimal::Decimal; use std::str::FromStr; // Mathematical Property Tests // Test arithmetic commutativity: a + b = b + a #[rstest] #[case("1.5", "2.3")] #[case("100", "0.001")] #[case("-5.5", "3.2")] #[case("0", "42")] #[case("1000000", "0.000001")] #[case("99999999999999999999999999.9999", "0.0001")] #[case("1.23456789012345678901234567", "9.87654321098765432109876543")] fn test_arithmetic_commutativity(#[case] a: &str, #[case] b: &str) { // Addition should be commutative: a + b = b + a let result1 = decimal_add(a.to_string(), b.to_string()); let result2 = decimal_add(b.to_string(), a.to_string()); match (result1, result2) { (Ok(r1), Ok(r2)) => { let d1 = Decimal::from_str(&r1).unwrap(); let d2 = Decimal::from_str(&r2).unwrap(); assert_eq!(d1, d2, "Addition not commutative: {} + {} vs {} + {}", a, b, b, a); } (Err(_), Err(_)) => { // Both should fail in the same way for invalid inputs } _ => panic!("Inconsistent error handling for {} and {}", a, b) } } // Test multiplication commutativity: a * b = b * a #[rstest] #[case("2.5", "4")] #[case("0.5", "8")] #[case("-2", "3")] #[case("1000", "0.001")] #[case("123.456", "789.012")] fn test_multiplication_commutativity(#[case] a: &str, #[case] b: &str) { let result1 = decimal_mul(a.to_string(), b.to_string()); let result2 = decimal_mul(b.to_string(), a.to_string()); match (result1, result2) { (Ok(r1), Ok(r2)) => { let d1 = Decimal::from_str(&r1).unwrap(); let d2 = Decimal::from_str(&r2).unwrap(); assert_eq!(d1, d2, "Multiplication not commutative: {} * {} vs {} * {}", a, b, b, a); } (Err(_), Err(_)) => {} _ => panic!("Inconsistent error handling for {} and {}", a, b) } } // Test addition associativity: (a + b) + c = a + (b + c) #[rstest] #[case("1", "2", "3")] #[case("0.1", "0.2", "0.3")] #[case("100", "200", "300")] #[case("-5", "10", "-3")] #[case("1.111", "2.222", "3.333")] // Avoid the extreme precision case that was failing #[case("0.001", "225.000001", "-146")] fn test_addition_associativity(#[case] a: &str, #[case] b: &str, #[case] c: &str) { // (a + b) + c = a + (b + c) let ab = decimal_add(a.to_string(), b.to_string()); let bc = decimal_add(b.to_string(), c.to_string()); if let (Ok(ab_result), Ok(bc_result)) = (ab, bc) { let left = decimal_add(ab_result, c.to_string()); let right = decimal_add(a.to_string(), 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(); // Allow for tiny precision differences at extreme scales let diff = (d1 - d2).abs(); let tolerance = Decimal::from_str("0.0000000000000000000000000001").unwrap(); assert!(diff <= tolerance, "Associativity violated: ({} + {}) + {} = {} vs {} + ({} + {}) = {} (diff: {})", a, b, c, left_final, a, b, c, right_final, diff); } } } // Test multiplication by zero #[rstest] #[case("5")] #[case("100.567")] #[case("-42.123")] #[case("0.000001")] #[case("999999999")] fn test_multiplication_by_zero(#[case] a: &str) { let result = decimal_mul(a.to_string(), "0".to_string()); if let Ok(r) = result { let d = Decimal::from_str(&r).unwrap(); assert!(d.is_zero(), "Multiplication by zero should give zero: {} * 0 = {}", a, r); } } // Test addition with zero identity: a + 0 = a #[rstest] #[case("5")] #[case("123.456")] #[case("-78.9")] #[case("0")] #[case("0.000000000000000001")] fn test_addition_with_zero_identity(#[case] a: &str) { let result = decimal_add(a.to_string(), "0".to_string()); match result { Ok(r) => { if let Ok(original) = Decimal::from_str(a) { let result_decimal = Decimal::from_str(&r).unwrap(); assert_eq!(original, result_decimal, "Addition with zero failed: {} + 0 = {}", a, r); } } Err(_) => { // If a is invalid, this is expected assert!(Decimal::from_str(a).is_err(), "Valid input {} should not fail", a); } } } // Test division-multiplication inverse with safe values #[rstest] #[case("10", "2")] #[case("100", "4")] #[case("7.5", "2.5")] #[case("1", "3")] #[case("123.456", "7.89")] // Avoid extreme cases that cause massive precision loss fn test_division_multiplication_inverse(#[case] a: &str, #[case] b: &str) { // (a / b) * b should approximately equal a let div_result = decimal_div(a.to_string(), b.to_string()); if let Ok(quotient) = div_result { let mul_result = decimal_mul(quotient, b.to_string()); if let Ok(final_result) = mul_result { if let (Ok(original), Ok(final_decimal)) = (Decimal::from_str(a), Decimal::from_str(&final_result)) { // Use relative error for better tolerance let relative_error = if !original.is_zero() { (original - final_decimal).abs() / original.abs() } else { (original - final_decimal).abs() }; let tolerance = Decimal::from_str("0.0001").unwrap(); // 0.01% tolerance assert!(relative_error <= tolerance, "Division-multiplication not inverse: {} / {} * {} = {} (relative error: {})", a, b, b, final_result, relative_error); } } } } // Test absolute value properties #[rstest] #[case("5")] #[case("-5")] #[case("0")] #[case("123.456")] #[case("-789.012")] fn test_absolute_value_properties(#[case] a: &str) { let abs_result = decimal_abs(a.to_string()); if let Ok(abs_val) = abs_result { let abs_decimal = Decimal::from_str(&abs_val).unwrap(); // abs(x) >= 0 assert!(abs_decimal >= Decimal::ZERO, "Absolute value should be non-negative: |{}| = {}", a, abs_val); // abs(abs(x)) = abs(x) let double_abs = decimal_abs(abs_val.clone()); if let Ok(double_abs_val) = double_abs { let double_abs_decimal = Decimal::from_str(&double_abs_val).unwrap(); assert_eq!(abs_decimal, double_abs_decimal, "Double absolute value: ||{}|| != |{}|", a, abs_val); } } } // Test comparison transitivity #[rstest] #[case("5", "3", "1")] #[case("10", "7", "4")] #[case("100.5", "50.25", "25.125")] fn test_comparison_transitivity(#[case] a: &str, #[case] b: &str, #[case] c: &str) { // If a > b and b > c, then a > c let ab = decimal_gt(a.to_string(), b.to_string()); let bc = decimal_gt(b.to_string(), c.to_string()); let ac = decimal_gt(a.to_string(), c.to_string()); if let (Ok(true), Ok(true), Ok(ac_result)) = (ab, bc, ac) { assert!(ac_result, "Transitivity violated: {} > {} and {} > {} but {} <= {}", a, b, b, c, a, c); } } // Test min/max properties #[rstest] #[case("5", "3")] #[case("10.5", "10.6")] #[case("-5", "-3")] #[case("0", "1")] #[case("123.456", "123.457")] fn test_min_max_properties(#[case] a: &str, #[case] b: &str) { let min_result = decimal_min(a.to_string(), b.to_string()); let max_result = decimal_max(a.to_string(), b.to_string()); 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) assert!(min_decimal <= max_decimal, "Min should be <= Max: min({},{}) = {} > max({},{}) = {}", a, b, min_val, a, b, max_val); // 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)) { assert!(min_decimal == a_decimal || min_decimal == b_decimal, "Min should equal one input: min({},{}) = {} != {} or {}", a, b, min_val, a, b); assert!(max_decimal == a_decimal || max_decimal == b_decimal, "Max should equal one input: max({},{}) = {} != {} or {}", a, b, max_val, a, b); } } } // Test round-trip conversion #[rstest] #[case("123.456")] #[case("42")] #[case("0.001")] #[case("999999.999999")] fn test_round_trip_conversion(#[case] a: &str) { // to_decimal should be idempotent for valid decimals let first_conversion = to_decimal(a.to_string()); if let Ok(converted) = first_conversion { let second_conversion = to_decimal(converted.clone()); assert_eq!(Ok(converted), second_conversion, "Round-trip conversion failed for {}", a); } } // Test precision formatting consistency #[rstest] #[case("123.456789", 2)] #[case("123.456789", 4)] #[case("123.456789", 0)] #[case("999.999999", 3)] fn test_precision_formatting_consistency(#[case] a: &str, #[case] precision: u32) { let formatted = decimal_format(a.to_string(), precision); if let Ok(result) = formatted { // Formatting again with same precision should be idempotent let reformatted = decimal_format(result.clone(), precision); assert_eq!(Ok(result.clone()), reformatted, "Precision formatting not idempotent for {} at {} places", a, precision); // Result should have at most 'precision' decimal places if let Some(dot_pos) = result.find('.') { let decimal_part = &result[dot_pos + 1..]; assert!(decimal_part.len() <= precision as usize, "Too many decimal places: {} has {} places, expected max {}", result, decimal_part.len(), precision); } } } // Test sqrt-square approximate inverse #[rstest] #[case("4")] #[case("16")] #[case("25")] #[case("100")] #[case("0.25")] #[case("1.44")] fn test_sqrt_square_approximate_inverse(#[case] a: &str) { let sqrt_result = decimal_sqrt(a.to_string()); 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(); assert!(diff <= tolerance, "sqrt-square not approximate inverse: sqrt({})^2 = {} vs {}, diff = {}", a, square_val, a, diff); } } } } // Parser Property Tests // Test parser transformation preserves structure #[rstest] #[case("+", "(+ 1 2)")] #[case("-", "(- 10 5)")] #[case("*", "(* 3 4)")] #[case("/", "(/ 15 3)")] #[case("sqrt", "(sqrt 16)")] #[case("abs", "(abs -5)")] #[case(">", "(> 5 3)")] #[case("=", "(= 2 2)")] fn test_parser_transformation_preserves_structure(#[case] _op: &str, #[case] expr: &str) { let parser = ScriptParser::new(); 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(); assert_eq!(open_count, close_count, "Unbalanced parentheses in transformation of {}: {}", expr, transformed); // Should contain decimal function assert!(transformed.contains("decimal-"), "Should contain decimal function: {} -> {}", expr, transformed); } // Test variable extraction correctness #[rstest] #[case("(+ $x $y)", vec!["x", "y"])] #[case("(* $price $quantity)", vec!["price", "quantity"])] #[case("(+ 1 2)", vec![])] #[case("(sqrt $value)", vec!["value"])] #[case("(+ $a $b $c)", vec!["a", "b", "c"])] fn test_variable_extraction_correctness(#[case] script: &str, #[case] expected_vars: Vec<&str>) { let parser = ScriptParser::new(); let dependencies = parser.extract_dependencies(script); // Should extract all expected variable names for var in &expected_vars { assert!(dependencies.contains(*var), "Missing variable {} in script {}", var, script); } // Should have exact count assert_eq!(dependencies.len(), expected_vars.len(), "Expected {} variables, got {}. Script: {}, Expected: {:?}, Got: {:?}", expected_vars.len(), dependencies.len(), script, expected_vars, dependencies); } // Edge Case and Safety Tests // Test no panics on problematic input #[rstest] #[case("")] #[case("not_a_number")] #[case("1.2.3")] #[case("++1")] #[case("--2")] #[case("1e")] #[case("e5")] #[case("∞")] #[case("NaN")] #[case("null")] #[case("undefined")] fn test_no_panics_on_problematic_input(#[case] input: &str) { // These operations should never panic, only return errors let _ = to_decimal(input.to_string()); let _ = decimal_add(input.to_string(), "1".to_string()); let _ = decimal_abs(input.to_string()); let parser = ScriptParser::new(); let _ = parser.transform(input); let _ = parser.extract_dependencies(input); } // Test no panics on very long inputs #[rstest] fn test_no_panics_on_very_long_input() { // Create very long number string let long_number = "1".to_owned() + &"0".repeat(1000); // These operations should never panic, only return errors let _ = to_decimal(long_number.clone()); let _ = decimal_add(long_number.clone(), "1".to_string()); let _ = decimal_abs(long_number.clone()); let parser = ScriptParser::new(); let _ = parser.transform(&long_number); let _ = parser.extract_dependencies(&long_number); } // Test scientific notation consistency #[rstest] #[case("1e2", "100")] #[case("1.5e3", "1500")] #[case("2.5e-2", "0.025")] #[case("1e0", "1")] #[case("5e1", "50")] fn test_scientific_notation_consistency(#[case] sci_notation: &str, #[case] expected: &str) { let conversion_result = to_decimal(sci_notation.to_string()); if let Ok(result) = conversion_result { assert!(Decimal::from_str(&result).is_ok(), "Result should be valid decimal: {}", result); // Check if it matches expected value (approximately) let result_decimal = Decimal::from_str(&result).unwrap(); let expected_decimal = Decimal::from_str(expected).unwrap(); let diff = (result_decimal - expected_decimal).abs(); let tolerance = Decimal::from_str("0.0001").unwrap(); assert!(diff <= tolerance, "Scientific notation conversion incorrect: {} -> {} (expected {})", sci_notation, result, expected); } } // Test precision edge cases #[rstest] #[case("0.000000000000000000000000001", "0.000000000000000000000000001", "0.000000000000000000000000002")] #[case("999999999999999999999999999", "1", "1000000000000000000000000000")] fn test_precision_edge_cases(#[case] a: &str, #[case] b: &str, #[case] expected: &str) { let result = decimal_add(a.to_string(), b.to_string()); match result { Ok(sum) => { // If it succeeds, check if it's correct let result_decimal = Decimal::from_str(&sum).unwrap(); let expected_decimal = Decimal::from_str(expected).unwrap(); assert_eq!(result_decimal, expected_decimal, "Precision calculation incorrect: {} + {} = {} (expected {})", a, b, sum, expected); } Err(_) => { // Overflow errors are acceptable for extreme values } } } // Test complex nested expressions #[rstest] #[case("(+ (* 2 3) (/ 12 4))")] #[case("(sqrt (+ (* 3 3) (* 4 4)))")] #[case("(abs (- (+ 10 5) (* 2 8)))")] fn test_complex_nested_expressions(#[case] expr: &str) { let parser = ScriptParser::new(); let transformed = parser.transform(expr); // Should maintain balanced parentheses let open_count = transformed.chars().filter(|c| *c == '(').count(); let close_count = transformed.chars().filter(|c| *c == ')').count(); assert_eq!(open_count, close_count, "Unbalanced parentheses in: {}", transformed); // Should contain multiple decimal operations let decimal_count = transformed.matches("decimal-").count(); assert!(decimal_count >= 2, "Should contain multiple decimal operations: {}", transformed); }