339 lines
12 KiB
Rust
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());
|
|
}
|
|
}
|
|
}
|