Files
calctext/calcpad-engine/tests/ffi_tests.rs
C. Cassel 6a8fecd03e feat(engine): establish calcpad-engine workspace with Epic 1 modules
Cherry-picked and integrated the best code from 105 parallel epic
branches into a clean workspace structure:

- calcpad-engine/: Core Rust crate with lexer, parser, AST,
  interpreter, types, FFI (C ABI), pipeline, error handling,
  span tracking, eval context (from epic/1-5 base)
- calcpad-engine/src/number.rs: Arbitrary precision arithmetic
  via dashu crate, WASM-compatible, exact decimals (from epic/1-4)
- calcpad-engine/src/sheet_context.rs: SheetContext with dependency
  graph, dirty tracking, circular detection, cheap clone (from epic/1-8)
- calcpad-wasm/: Thin WASM wrapper crate via wasm-bindgen,
  delegates to calcpad-engine (from epic/1-6)
- Updated .gitignore for target/, node_modules/, build artifacts
- Fixed run-pipeline.sh for macOS compat and CalcPad phases

79 tests passing across workspace.
2026-03-17 07:54:17 -04:00

446 lines
13 KiB
Rust

use calcpad_engine::context::EvalContext;
use calcpad_engine::pipeline::{eval_line, eval_sheet};
use calcpad_engine::types::ResultType;
use calcpad_engine::ffi::{FfiResponse, FfiSheetResponse};
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
use std::ptr;
// Re-declare FFI functions for testing
extern "C" {
fn calcpad_eval_line(input: *const c_char) -> *mut c_char;
fn calcpad_eval_sheet(lines: *const *const c_char, count: i32) -> *mut c_char;
fn calcpad_free_result(ptr: *mut c_char);
}
// ===== Pipeline tests =====
#[test]
fn pipeline_eval_simple_addition() {
let mut ctx = EvalContext::new();
let result = eval_line("2 + 3", &mut ctx);
assert_eq!(result.result_type(), ResultType::Number);
assert_eq!(result.metadata.display, "5");
assert_eq!(result.metadata.raw_value, Some(5.0));
}
#[test]
fn pipeline_eval_multiplication() {
let mut ctx = EvalContext::new();
let result = eval_line("6 * 7", &mut ctx);
assert_eq!(result.metadata.display, "42");
}
#[test]
fn pipeline_eval_division() {
let mut ctx = EvalContext::new();
let result = eval_line("10 / 4", &mut ctx);
assert_eq!(result.metadata.raw_value, Some(2.5));
}
#[test]
fn pipeline_eval_exponentiation() {
let mut ctx = EvalContext::new();
let result = eval_line("2 ^ 10", &mut ctx);
assert_eq!(result.metadata.display, "1024");
}
#[test]
fn pipeline_eval_parentheses() {
let mut ctx = EvalContext::new();
let result = eval_line("(2 + 3) * 4", &mut ctx);
assert_eq!(result.metadata.display, "20");
}
#[test]
fn pipeline_eval_unary_neg() {
let mut ctx = EvalContext::new();
let result = eval_line("-5 + 3", &mut ctx);
assert_eq!(result.metadata.display, "-2");
}
#[test]
fn pipeline_eval_percentage() {
let mut ctx = EvalContext::new();
let result = eval_line("100 - 20%", &mut ctx);
assert_eq!(result.metadata.display, "80");
}
#[test]
fn pipeline_eval_unit_number() {
let mut ctx = EvalContext::new();
let result = eval_line("5kg", &mut ctx);
assert_eq!(result.result_type(), ResultType::UnitValue);
assert_eq!(result.metadata.display, "5 kg");
}
#[test]
fn pipeline_eval_currency() {
let mut ctx = EvalContext::new();
let result = eval_line("$20", &mut ctx);
assert_eq!(result.result_type(), ResultType::CurrencyValue);
assert!(result.metadata.display.contains("20"));
}
#[test]
fn pipeline_eval_comparison() {
let mut ctx = EvalContext::new();
let result = eval_line("5 > 3", &mut ctx);
assert_eq!(result.result_type(), ResultType::Boolean);
assert_eq!(result.metadata.display, "true");
}
#[test]
fn pipeline_eval_division_by_zero() {
let mut ctx = EvalContext::new();
let result = eval_line("10 / 0", &mut ctx);
assert_eq!(result.result_type(), ResultType::Error);
}
#[test]
fn pipeline_eval_empty_input() {
let mut ctx = EvalContext::new();
let result = eval_line("", &mut ctx);
assert_eq!(result.result_type(), ResultType::Error);
}
#[test]
fn pipeline_eval_comment_only() {
let mut ctx = EvalContext::new();
let result = eval_line("// this is a comment", &mut ctx);
assert_eq!(result.result_type(), ResultType::Error);
}
// ===== Variable assignment tests (eval_sheet) =====
#[test]
fn pipeline_eval_sheet_basic() {
let mut ctx = EvalContext::new();
let results = eval_sheet(&["2 + 3", "10 * 2"], &mut ctx);
assert_eq!(results.len(), 2);
assert_eq!(results[0].metadata.display, "5");
assert_eq!(results[1].metadata.display, "20");
}
#[test]
fn pipeline_eval_sheet_variable_assignment() {
let mut ctx = EvalContext::new();
let results = eval_sheet(&["x = 5", "x + 3"], &mut ctx);
assert_eq!(results.len(), 2);
assert_eq!(results[0].metadata.display, "5");
assert_eq!(results[1].metadata.display, "8");
}
#[test]
fn pipeline_eval_sheet_multiple_variables() {
let mut ctx = EvalContext::new();
let results = eval_sheet(&["a = 10", "b = 20", "a + b"], &mut ctx);
assert_eq!(results.len(), 3);
assert_eq!(results[0].metadata.display, "10");
assert_eq!(results[1].metadata.display, "20");
assert_eq!(results[2].metadata.display, "30");
}
#[test]
fn pipeline_eval_sheet_variable_reassignment() {
let mut ctx = EvalContext::new();
let results = eval_sheet(&["x = 5", "x = 10", "x"], &mut ctx);
assert_eq!(results.len(), 3);
assert_eq!(results[2].metadata.display, "10");
}
#[test]
fn pipeline_eval_sheet_undefined_variable() {
let mut ctx = EvalContext::new();
let results = eval_sheet(&["y + 3"], &mut ctx);
assert_eq!(results[0].result_type(), ResultType::Error);
}
// ===== FFI function tests =====
#[test]
fn ffi_eval_line_basic() {
let input = CString::new("2 + 3").unwrap();
unsafe {
let result_ptr = calcpad_eval_line(input.as_ptr());
assert!(!result_ptr.is_null());
let result_str = CStr::from_ptr(result_ptr).to_str().unwrap();
let response: FfiResponse = serde_json::from_str(result_str).unwrap();
assert_eq!(response.schema_version, "1.0");
assert_eq!(response.result.result_type(), ResultType::Number);
assert_eq!(response.result.metadata.display, "5");
assert_eq!(response.result.metadata.raw_value, Some(5.0));
calcpad_free_result(result_ptr);
}
}
#[test]
fn ffi_eval_line_complex_expression() {
let input = CString::new("(10 + 5) * 2").unwrap();
unsafe {
let result_ptr = calcpad_eval_line(input.as_ptr());
assert!(!result_ptr.is_null());
let result_str = CStr::from_ptr(result_ptr).to_str().unwrap();
let response: FfiResponse = serde_json::from_str(result_str).unwrap();
assert_eq!(response.result.metadata.display, "30");
calcpad_free_result(result_ptr);
}
}
#[test]
fn ffi_eval_line_null_input() {
unsafe {
let result_ptr = calcpad_eval_line(ptr::null());
assert!(!result_ptr.is_null());
let result_str = CStr::from_ptr(result_ptr).to_str().unwrap();
let response: FfiResponse = serde_json::from_str(result_str).unwrap();
assert_eq!(response.result.result_type(), ResultType::Error);
calcpad_free_result(result_ptr);
}
}
#[test]
fn ffi_eval_sheet_basic() {
let line1 = CString::new("2 + 3").unwrap();
let line2 = CString::new("10 * 2").unwrap();
let lines: Vec<*const c_char> = vec![line1.as_ptr(), line2.as_ptr()];
unsafe {
let result_ptr = calcpad_eval_sheet(lines.as_ptr(), 2);
assert!(!result_ptr.is_null());
let result_str = CStr::from_ptr(result_ptr).to_str().unwrap();
let response: FfiSheetResponse = serde_json::from_str(result_str).unwrap();
assert_eq!(response.schema_version, "1.0");
assert_eq!(response.results.len(), 2);
assert_eq!(response.results[0].metadata.display, "5");
assert_eq!(response.results[1].metadata.display, "20");
calcpad_free_result(result_ptr);
}
}
#[test]
fn ffi_eval_sheet_with_variables() {
let line1 = CString::new("x = 5").unwrap();
let line2 = CString::new("x + 10").unwrap();
let lines: Vec<*const c_char> = vec![line1.as_ptr(), line2.as_ptr()];
unsafe {
let result_ptr = calcpad_eval_sheet(lines.as_ptr(), 2);
assert!(!result_ptr.is_null());
let result_str = CStr::from_ptr(result_ptr).to_str().unwrap();
let response: FfiSheetResponse = serde_json::from_str(result_str).unwrap();
assert_eq!(response.results.len(), 2);
assert_eq!(response.results[0].metadata.display, "5");
assert_eq!(response.results[1].metadata.display, "15");
calcpad_free_result(result_ptr);
}
}
#[test]
fn ffi_eval_sheet_null_lines() {
unsafe {
let result_ptr = calcpad_eval_sheet(ptr::null(), 0);
assert!(!result_ptr.is_null());
let result_str = CStr::from_ptr(result_ptr).to_str().unwrap();
let response: FfiResponse = serde_json::from_str(result_str).unwrap();
assert_eq!(response.result.result_type(), ResultType::Error);
calcpad_free_result(result_ptr);
}
}
// ===== Panic safety tests =====
#[test]
fn ffi_panic_safety_eval_line() {
// The catch_unwind in eval_line should handle any internal panics.
// Test with various edge cases that might trigger unexpected behavior.
let input = CString::new("2 + 3").unwrap();
unsafe {
let result_ptr = calcpad_eval_line(input.as_ptr());
assert!(!result_ptr.is_null());
calcpad_free_result(result_ptr);
}
}
#[test]
fn ffi_panic_safety_null_input() {
// Null input should not crash — should return error JSON
unsafe {
let result_ptr = calcpad_eval_line(ptr::null());
assert!(!result_ptr.is_null());
let result_str = CStr::from_ptr(result_ptr).to_str().unwrap();
assert!(result_str.contains("error") || result_str.contains("Error"));
calcpad_free_result(result_ptr);
}
}
// ===== Memory management tests =====
#[test]
fn ffi_free_result_null() {
// Freeing null should be a safe no-op
unsafe {
calcpad_free_result(ptr::null_mut());
}
// If we get here without crashing, the test passes
}
#[test]
fn ffi_free_result_valid() {
let input = CString::new("42").unwrap();
unsafe {
let result_ptr = calcpad_eval_line(input.as_ptr());
assert!(!result_ptr.is_null());
// Free the result — should not crash
calcpad_free_result(result_ptr);
}
// If we get here without crashing, the test passes
}
#[test]
fn ffi_multiple_allocations() {
// Allocate and free multiple results to check for memory leaks
for _ in 0..100 {
let input = CString::new("1 + 1").unwrap();
unsafe {
let result_ptr = calcpad_eval_line(input.as_ptr());
assert!(!result_ptr.is_null());
calcpad_free_result(result_ptr);
}
}
}
// ===== JSON schema versioning tests =====
#[test]
fn json_schema_version_present() {
let input = CString::new("42").unwrap();
unsafe {
let result_ptr = calcpad_eval_line(input.as_ptr());
let result_str = CStr::from_ptr(result_ptr).to_str().unwrap();
let json: serde_json::Value = serde_json::from_str(result_str).unwrap();
assert_eq!(json["schema_version"], "1.0");
calcpad_free_result(result_ptr);
}
}
#[test]
fn json_schema_contains_required_fields() {
let input = CString::new("42").unwrap();
unsafe {
let result_ptr = calcpad_eval_line(input.as_ptr());
let result_str = CStr::from_ptr(result_ptr).to_str().unwrap();
let json: serde_json::Value = serde_json::from_str(result_str).unwrap();
// Check schema_version
assert!(json["schema_version"].is_string());
// Check result structure
assert!(json["result"]["value"].is_object());
assert!(json["result"]["metadata"].is_object());
// Check metadata fields
let metadata = &json["result"]["metadata"];
assert!(metadata["result_type"].is_string());
assert!(metadata["display"].is_string());
assert!(metadata["span"].is_object());
assert!(metadata["span"]["start"].is_number());
assert!(metadata["span"]["end"].is_number());
// Check raw_value is present (can be number or null)
assert!(
metadata["raw_value"].is_number() || metadata["raw_value"].is_null(),
"raw_value should be a number or null"
);
calcpad_free_result(result_ptr);
}
}
#[test]
fn json_schema_result_type_field() {
let input = CString::new("42").unwrap();
unsafe {
let result_ptr = calcpad_eval_line(input.as_ptr());
let result_str = CStr::from_ptr(result_ptr).to_str().unwrap();
let json: serde_json::Value = serde_json::from_str(result_str).unwrap();
assert_eq!(json["result"]["metadata"]["result_type"], "Number");
assert_eq!(json["result"]["value"]["kind"], "Number");
calcpad_free_result(result_ptr);
}
}
#[test]
fn json_schema_error_result() {
let input = CString::new("10 / 0").unwrap();
unsafe {
let result_ptr = calcpad_eval_line(input.as_ptr());
let result_str = CStr::from_ptr(result_ptr).to_str().unwrap();
let json: serde_json::Value = serde_json::from_str(result_str).unwrap();
assert_eq!(json["result"]["metadata"]["result_type"], "Error");
assert_eq!(json["result"]["value"]["kind"], "Error");
assert!(json["result"]["value"]["message"].is_string());
calcpad_free_result(result_ptr);
}
}
#[test]
fn json_schema_sheet_version() {
let line1 = CString::new("1 + 1").unwrap();
let lines: Vec<*const c_char> = vec![line1.as_ptr()];
unsafe {
let result_ptr = calcpad_eval_sheet(lines.as_ptr(), 1);
let result_str = CStr::from_ptr(result_ptr).to_str().unwrap();
let json: serde_json::Value = serde_json::from_str(result_str).unwrap();
assert_eq!(json["schema_version"], "1.0");
assert!(json["results"].is_array());
assert_eq!(json["results"].as_array().unwrap().len(), 1);
calcpad_free_result(result_ptr);
}
}
#[test]
fn json_display_value_and_raw_value() {
let input = CString::new("2.5 * 4").unwrap();
unsafe {
let result_ptr = calcpad_eval_line(input.as_ptr());
let result_str = CStr::from_ptr(result_ptr).to_str().unwrap();
let json: serde_json::Value = serde_json::from_str(result_str).unwrap();
assert_eq!(json["result"]["metadata"]["display"], "10");
assert_eq!(json["result"]["metadata"]["raw_value"], 10.0);
calcpad_free_result(result_ptr);
}
}