Files
komp_ac/steel_decimal/tests/property_tests.rs

339 lines
12 KiB
Rust

// 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<Value = String> {
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<Value = u32> {
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());
}
}
}