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.
178 lines
5.8 KiB
Rust
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);
|
|
}
|
|
}
|
|
}
|