//! Property-based fuzz tests for the evaluation engine. //! //! These use proptest to generate random inputs and verify invariants: //! - The engine never panics on any input //! - Algebraic properties (commutativity, identities) hold //! - Division by zero always produces errors use calcpad_engine::context::EvalContext; use calcpad_engine::pipeline::eval_line; use calcpad_engine::types::ResultType; use proptest::prelude::*; fn arb_small_int() -> impl Strategy { -10000i64..10000 } fn arb_operator() -> impl Strategy { prop::sample::select(vec!["+", "-", "*"]) } // ========================================================================= // No-panic guarantees: valid expressions // ========================================================================= proptest! { #[test] fn valid_number_never_panics(n in arb_small_int()) { let input = format!("{}", n); let mut ctx = EvalContext::new(); let _result = eval_line(&input, &mut ctx); } #[test] fn valid_binary_expr_never_panics( a in arb_small_int(), b in arb_small_int(), op in arb_operator() ) { let input = format!("{} {} {}", a, op, b); let mut ctx = EvalContext::new(); let _result = eval_line(&input, &mut ctx); } #[test] fn valid_complex_expr_never_panics( a in arb_small_int(), b in 1i64..1000, c in arb_small_int(), op1 in arb_operator(), op2 in arb_operator() ) { let input = format!("({} {} {}) {} {}", a, op1, b, op2, c); let mut ctx = EvalContext::new(); let _result = eval_line(&input, &mut ctx); } } // ========================================================================= // No-panic guarantees: malformed / garbage input // ========================================================================= proptest! { #[test] fn malformed_garbage_never_panics( s in "[a-zA-Z0-9!@#$%^&*()_+\\-=\\[\\]{};':\"\\\\|,.<>/?]{1,50}" ) { let mut ctx = EvalContext::new(); let _result = eval_line(&s, &mut ctx); } #[test] fn malformed_unmatched_parens_never_panics( s in "\\({0,5}[0-9]{1,4}[+\\-*/]{0,2}[0-9]{0,4}\\){0,5}" ) { let mut ctx = EvalContext::new(); let _result = eval_line(&s, &mut ctx); } #[test] fn empty_and_whitespace_never_panics(s in "\\s{0,20}") { let mut ctx = EvalContext::new(); let _result = eval_line(&s, &mut ctx); } #[test] fn random_identifiers_never_panics(s in "[a-z]{1,10}") { let mut ctx = EvalContext::new(); let _result = eval_line(&s, &mut ctx); } } // ========================================================================= // Algebraic properties // ========================================================================= proptest! { #[test] fn addition_commutativity(a in arb_small_int(), b in arb_small_int()) { let mut ctx1 = EvalContext::new(); let mut ctx2 = EvalContext::new(); let r1 = eval_line(&format!("{} + {}", a, b), &mut ctx1); let r2 = eval_line(&format!("{} + {}", b, a), &mut ctx2); match (r1.result_type(), r2.result_type()) { (ResultType::Number, ResultType::Number) => { let v1 = r1.metadata.raw_value.unwrap(); let v2 = r2.metadata.raw_value.unwrap(); prop_assert!((v1 - v2).abs() < 1e-10, "{} + {} = {} but {} + {} = {}", a, b, v1, b, a, v2); } _ => { prop_assert_eq!(r1.result_type(), r2.result_type()); } } } #[test] fn multiplication_commutativity(a in arb_small_int(), b in arb_small_int()) { let mut ctx1 = EvalContext::new(); let mut ctx2 = EvalContext::new(); let r1 = eval_line(&format!("{} * {}", a, b), &mut ctx1); let r2 = eval_line(&format!("{} * {}", b, a), &mut ctx2); match (r1.result_type(), r2.result_type()) { (ResultType::Number, ResultType::Number) => { let v1 = r1.metadata.raw_value.unwrap(); let v2 = r2.metadata.raw_value.unwrap(); prop_assert!((v1 - v2).abs() < 1e-6, "{} * {} = {} but {} * {} = {}", a, b, v1, b, a, v2); } _ => { prop_assert_eq!(r1.result_type(), r2.result_type()); } } } } // ========================================================================= // Division by zero // ========================================================================= proptest! { #[test] fn division_by_zero_always_error(a in arb_small_int()) { let mut ctx = EvalContext::new(); let r = eval_line(&format!("{} / 0", a), &mut ctx); prop_assert_eq!(r.result_type(), ResultType::Error); } } // ========================================================================= // Identity operations // ========================================================================= proptest! { #[test] fn add_zero_identity(a in arb_small_int()) { let mut ctx = EvalContext::new(); let r = eval_line(&format!("{} + 0", a), &mut ctx); if r.result_type() == ResultType::Number { let v = r.metadata.raw_value.unwrap(); prop_assert!((v - a as f64).abs() < 1e-10, "{} + 0 should be {} but got {}", a, a, v); } } #[test] fn mul_one_identity(a in arb_small_int()) { let mut ctx = EvalContext::new(); let r = eval_line(&format!("{} * 1", a), &mut ctx); if r.result_type() == ResultType::Number { let v = r.metadata.raw_value.unwrap(); prop_assert!((v - a as f64).abs() < 1e-10, "{} * 1 should be {} but got {}", a, a, v); } } }