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.
314 lines
10 KiB
Rust
314 lines
10 KiB
Rust
//! Integration tests covering real-world sheet scenarios.
|
|
//!
|
|
//! These exercise the full pipeline (lexer -> parser -> interpreter) through
|
|
//! the eval_line / eval_sheet / SheetContext public APIs, verifying end-to-end
|
|
//! behavior rather than individual module internals.
|
|
|
|
use calcpad_engine::context::EvalContext;
|
|
use calcpad_engine::pipeline::{eval_line, eval_sheet};
|
|
use calcpad_engine::types::ResultType;
|
|
use calcpad_engine::SheetContext;
|
|
|
|
// =========================================================================
|
|
// Variable assignment and reference across lines
|
|
// =========================================================================
|
|
|
|
#[test]
|
|
fn sheet_variable_assignment_and_reference() {
|
|
let mut ctx = EvalContext::new();
|
|
let results = eval_sheet(
|
|
&["price = 100", "quantity = 5", "total = price * quantity"],
|
|
&mut ctx,
|
|
);
|
|
assert_eq!(results.len(), 3);
|
|
assert_eq!(results[0].metadata.display, "100");
|
|
assert_eq!(results[1].metadata.display, "5");
|
|
assert_eq!(results[2].metadata.display, "500");
|
|
}
|
|
|
|
#[test]
|
|
fn sheet_variable_used_in_multiple_lines() {
|
|
let mut ctx = EvalContext::new();
|
|
let results = eval_sheet(
|
|
&[
|
|
"rate = 25",
|
|
"hours = 8",
|
|
"daily = rate * hours",
|
|
"weekly = daily * 5",
|
|
],
|
|
&mut ctx,
|
|
);
|
|
assert_eq!(results[2].metadata.display, "200");
|
|
assert_eq!(results[3].metadata.display, "1000");
|
|
}
|
|
|
|
// =========================================================================
|
|
// Chained dependencies
|
|
// =========================================================================
|
|
|
|
#[test]
|
|
fn sheet_chained_dependencies() {
|
|
let mut ctx = EvalContext::new();
|
|
let results = eval_sheet(
|
|
&["a = 10", "b = a + 5", "c = b * 2", "d = c - a"],
|
|
&mut ctx,
|
|
);
|
|
assert_eq!(results[0].metadata.display, "10");
|
|
assert_eq!(results[1].metadata.display, "15");
|
|
assert_eq!(results[2].metadata.display, "30");
|
|
assert_eq!(results[3].metadata.display, "20");
|
|
}
|
|
|
|
#[test]
|
|
fn sheet_deep_chain() {
|
|
let mut ctx = EvalContext::new();
|
|
let results = eval_sheet(
|
|
&[
|
|
"v1 = 1",
|
|
"v2 = v1 + 1",
|
|
"v3 = v2 + 1",
|
|
"v4 = v3 + 1",
|
|
"v5 = v4 + 1",
|
|
],
|
|
&mut ctx,
|
|
);
|
|
assert_eq!(results[4].metadata.display, "5");
|
|
}
|
|
|
|
// =========================================================================
|
|
// Forward references and undefined variables
|
|
// =========================================================================
|
|
|
|
#[test]
|
|
fn sheet_forward_reference_error() {
|
|
let mut ctx = EvalContext::new();
|
|
let results = eval_sheet(&["a = b + 1", "b = 10"], &mut ctx);
|
|
// eval_sheet processes sequentially; b isn't defined yet when line 0 runs
|
|
assert_eq!(results[0].result_type(), ResultType::Error);
|
|
assert_eq!(results[1].metadata.display, "10");
|
|
}
|
|
|
|
#[test]
|
|
fn sheet_undefined_variable_in_chain() {
|
|
let mut ctx = EvalContext::new();
|
|
let results = eval_sheet(&["x = undefined_var + 1"], &mut ctx);
|
|
assert_eq!(results[0].result_type(), ResultType::Error);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Unit conversion across lines
|
|
// =========================================================================
|
|
|
|
#[test]
|
|
fn sheet_unit_arithmetic_same_unit() {
|
|
let mut ctx = EvalContext::new();
|
|
let results = eval_sheet(&["a = 5kg", "b = 3kg", "total = a + b"], &mut ctx);
|
|
assert_eq!(results[2].result_type(), ResultType::UnitValue);
|
|
assert_eq!(results[2].metadata.raw_value, Some(8.0));
|
|
}
|
|
|
|
// =========================================================================
|
|
// Mixed expressions: comments, empty lines, text
|
|
// =========================================================================
|
|
|
|
#[test]
|
|
fn sheet_with_comments() {
|
|
let mut ctx = EvalContext::new();
|
|
let results = eval_sheet(
|
|
&[
|
|
"// Budget calculation",
|
|
"income = 5000",
|
|
"expenses = 3000",
|
|
"savings = income - expenses",
|
|
],
|
|
&mut ctx,
|
|
);
|
|
assert_eq!(results[0].result_type(), ResultType::Error); // comment
|
|
assert_eq!(results[3].metadata.display, "2000");
|
|
}
|
|
|
|
#[test]
|
|
fn sheet_with_empty_lines() {
|
|
let mut ctx = EvalContext::new();
|
|
let results = eval_sheet(&["x = 10", "", "y = x + 5"], &mut ctx);
|
|
assert_eq!(results[0].metadata.display, "10");
|
|
assert_eq!(results[1].result_type(), ResultType::Error); // empty line
|
|
assert_eq!(results[2].metadata.display, "15");
|
|
}
|
|
|
|
#[test]
|
|
fn sheet_mixed_comments_and_calcs() {
|
|
let mut ctx = EvalContext::new();
|
|
let results = eval_sheet(
|
|
&[
|
|
"// Shopping list",
|
|
"apples = 3 * 2",
|
|
"// bananas are on sale",
|
|
"bananas = 5 * 1",
|
|
"total = apples + bananas",
|
|
],
|
|
&mut ctx,
|
|
);
|
|
assert_eq!(results[1].metadata.display, "6");
|
|
assert_eq!(results[3].metadata.display, "5");
|
|
assert_eq!(results[4].metadata.display, "11");
|
|
}
|
|
|
|
// =========================================================================
|
|
// Variable reassignment / shadowing
|
|
// =========================================================================
|
|
|
|
#[test]
|
|
fn sheet_variable_reassignment() {
|
|
let mut ctx = EvalContext::new();
|
|
let results = eval_sheet(&["x = 5", "y = x * 2", "x = 20", "z = x * 2"], &mut ctx);
|
|
assert_eq!(results[0].metadata.display, "5");
|
|
assert_eq!(results[1].metadata.display, "10");
|
|
assert_eq!(results[2].metadata.display, "20");
|
|
assert_eq!(results[3].metadata.display, "40"); // uses reassigned x=20
|
|
}
|
|
|
|
// =========================================================================
|
|
// Real-world calculation scenarios
|
|
// =========================================================================
|
|
|
|
#[test]
|
|
fn sheet_invoice_calculation() {
|
|
let mut ctx = EvalContext::new();
|
|
let results = eval_sheet(
|
|
&[
|
|
"// Invoice",
|
|
"subtotal = 1500",
|
|
"tax_rate = 8.5",
|
|
"tax = subtotal * tax_rate / 100",
|
|
"total = subtotal + tax",
|
|
],
|
|
&mut ctx,
|
|
);
|
|
assert_eq!(results[1].metadata.display, "1500");
|
|
assert_eq!(results[3].metadata.display, "127.5");
|
|
assert_eq!(results[4].metadata.display, "1627.5");
|
|
}
|
|
|
|
#[test]
|
|
fn sheet_tip_calculation() {
|
|
let mut ctx = EvalContext::new();
|
|
let results = eval_sheet(
|
|
&["bill = 85", "tip = bill + 20%", "per_person = tip / 4"],
|
|
&mut ctx,
|
|
);
|
|
assert_eq!(results[0].metadata.display, "85");
|
|
assert_eq!(results[1].metadata.display, "102");
|
|
assert!((results[2].metadata.raw_value.unwrap() - 25.5).abs() < 0.01);
|
|
}
|
|
|
|
#[test]
|
|
fn sheet_accumulator_pattern() {
|
|
let mut ctx = EvalContext::new();
|
|
let lines: Vec<String> = (0..20)
|
|
.map(|i| {
|
|
if i == 0 {
|
|
"acc = 0".to_string()
|
|
} else {
|
|
format!("acc = acc + {}", i)
|
|
}
|
|
})
|
|
.collect();
|
|
let refs: Vec<&str> = lines.iter().map(|s| s.as_str()).collect();
|
|
let results = eval_sheet(&refs, &mut ctx);
|
|
// Sum of 1..19 = 190
|
|
let last = results.last().unwrap();
|
|
assert_eq!(last.metadata.raw_value, Some(190.0));
|
|
}
|
|
|
|
// =========================================================================
|
|
// Context isolation
|
|
// =========================================================================
|
|
|
|
#[test]
|
|
fn context_variables_independent() {
|
|
let mut ctx1 = EvalContext::new();
|
|
let mut ctx2 = EvalContext::new();
|
|
eval_line("x = 100", &mut ctx1);
|
|
let r = eval_line("x", &mut ctx2);
|
|
assert_eq!(r.result_type(), ResultType::Error); // x not defined in ctx2
|
|
}
|
|
|
|
// =========================================================================
|
|
// SheetContext: incremental evaluation
|
|
// =========================================================================
|
|
|
|
#[test]
|
|
fn sheet_context_incremental_independent_lines() {
|
|
let mut sheet = SheetContext::new();
|
|
sheet.set_line(0, "a = 10");
|
|
sheet.set_line(1, "b = 20");
|
|
sheet.set_line(2, "c = a + 5");
|
|
sheet.set_line(3, "d = b + 5");
|
|
let results = sheet.eval();
|
|
assert_eq!(results.len(), 4);
|
|
|
|
// Change only b: a and c should be unaffected
|
|
sheet.set_line(1, "b = 99");
|
|
let results = sheet.eval();
|
|
assert_eq!(results[0].metadata.raw_value, Some(10.0));
|
|
assert_eq!(results[1].metadata.raw_value, Some(99.0));
|
|
assert_eq!(results[2].metadata.raw_value, Some(15.0)); // a + 5, unchanged
|
|
assert_eq!(results[3].metadata.raw_value, Some(104.0)); // b + 5
|
|
}
|
|
|
|
#[test]
|
|
fn sheet_context_aggregator_invoice() {
|
|
let mut sheet = SheetContext::new();
|
|
sheet.set_line(0, "## Monthly Expenses");
|
|
sheet.set_line(1, "1200"); // rent
|
|
sheet.set_line(2, "150"); // utilities
|
|
sheet.set_line(3, "400"); // groceries
|
|
sheet.set_line(4, "subtotal");
|
|
sheet.set_line(5, "## One-Time Costs");
|
|
sheet.set_line(6, "500"); // furniture
|
|
sheet.set_line(7, "200"); // electronics
|
|
sheet.set_line(8, "subtotal");
|
|
sheet.set_line(9, "grand total");
|
|
|
|
let results = sheet.eval();
|
|
assert_eq!(results[4].metadata.raw_value, Some(1750.0));
|
|
assert_eq!(results[8].metadata.raw_value, Some(700.0));
|
|
assert_eq!(results[9].metadata.raw_value, Some(2450.0));
|
|
}
|
|
|
|
#[test]
|
|
fn sheet_context_prev_through_sections() {
|
|
let mut sheet = SheetContext::new();
|
|
sheet.set_line(0, "100");
|
|
sheet.set_line(1, "prev + 50"); // 150
|
|
sheet.set_line(2, "prev * 2"); // 300
|
|
let results = sheet.eval();
|
|
assert_eq!(results[0].metadata.raw_value, Some(100.0));
|
|
assert_eq!(results[1].metadata.raw_value, Some(150.0));
|
|
assert_eq!(results[2].metadata.raw_value, Some(300.0));
|
|
}
|
|
|
|
#[test]
|
|
fn sheet_context_comparison_result() {
|
|
let mut ctx = EvalContext::new();
|
|
let results = eval_sheet(
|
|
&["budget = 1000", "spent = 750", "budget - spent > 0"],
|
|
&mut ctx,
|
|
);
|
|
assert_eq!(results[2].result_type(), ResultType::Boolean);
|
|
assert_eq!(results[2].metadata.display, "true");
|
|
}
|
|
|
|
#[test]
|
|
fn sheet_context_percentage_discount() {
|
|
let mut ctx = EvalContext::new();
|
|
let results = eval_sheet(
|
|
&["original = $200", "discounted = $200 - 30%"],
|
|
&mut ctx,
|
|
);
|
|
assert_eq!(results[0].result_type(), ResultType::CurrencyValue);
|
|
assert_eq!(results[1].result_type(), ResultType::CurrencyValue);
|
|
assert!((results[1].metadata.raw_value.unwrap() - 140.0).abs() < 0.01);
|
|
}
|