Files
calctext/calcpad-engine/tests/proptest_fuzz.rs
C. Cassel 806e2f1ec6 feat: add platform shells, CLI, formatting, plugins, tests, and benchmarks
Phase 4 — Platform shells:
- calcpad-macos/: SwiftUI two-column editor with Rust FFI bridge (16 files)
- calcpad-windows/: iced GUI with Windows 11 Fluent theme (7 files, 13 tests)
- calcpad-web/: React 18 + CodeMirror 6 + WASM Worker + PWA (20 files)
- calcpad-cli/: clap-based CLI with expression eval, pipe/stdin, JSON/CSV
  output, and interactive REPL with rustyline history

Phase 5 — Engine modules:
- formatting/: answer formatting (decimal/scientific/SI notation, thousands
  separators, currency), line type classification, clipboard values (93 tests)
- plugins/: CalcPadPlugin trait, PluginRegistry, Rhai scripting stub (43 tests)
- benches/: criterion benchmarks (single-line, 100/500-line sheets, DAG, incremental)
- tests/sheet_scenarios.rs: 20 real-world integration tests
- tests/proptest_fuzz.rs: 12 property-based fuzz tests

771 tests passing across workspace, 0 failures.
2026-03-17 09:46:40 -04:00

178 lines
5.8 KiB
Rust

//! 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<Value = i64> {
-10000i64..10000
}
fn arb_operator() -> impl Strategy<Value = &'static str> {
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);
}
}
}