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::Empty); } #[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::NonCalculable); } // ===== 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); } }