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.
228 lines
7.4 KiB
Swift
228 lines
7.4 KiB
Swift
import Testing
|
|
import Foundation
|
|
@testable import CalcPad
|
|
|
|
@Suite("FFI JSON Model Decoding Tests")
|
|
struct FFIModelTests {
|
|
|
|
// MARK: - Single Line Response
|
|
|
|
@Test("Decode number result")
|
|
func decodeNumberResult() throws {
|
|
let json = """
|
|
{
|
|
"schema_version": "1.0",
|
|
"result": {
|
|
"value": { "kind": "Number", "value": 42.0 },
|
|
"metadata": {
|
|
"span": { "start": 0, "end": 5 },
|
|
"result_type": "Number",
|
|
"display": "42",
|
|
"raw_value": 42.0
|
|
}
|
|
}
|
|
}
|
|
"""
|
|
let response = try JSONDecoder().decode(FFIResponse.self, from: Data(json.utf8))
|
|
#expect(response.schemaVersion == "1.0")
|
|
#expect(response.result.metadata.display == "42")
|
|
#expect(response.result.metadata.resultType == "Number")
|
|
#expect(response.result.metadata.rawValue == 42.0)
|
|
if case .number(let value) = response.result.value {
|
|
#expect(value == 42.0)
|
|
} else {
|
|
Issue.record("Expected .number variant")
|
|
}
|
|
}
|
|
|
|
@Test("Decode unit value result")
|
|
func decodeUnitValueResult() throws {
|
|
let json = """
|
|
{
|
|
"schema_version": "1.0",
|
|
"result": {
|
|
"value": { "kind": "UnitValue", "value": 5.0, "unit": "kg" },
|
|
"metadata": {
|
|
"span": { "start": 0, "end": 4 },
|
|
"result_type": "UnitValue",
|
|
"display": "5 kg",
|
|
"raw_value": 5.0
|
|
}
|
|
}
|
|
}
|
|
"""
|
|
let response = try JSONDecoder().decode(FFIResponse.self, from: Data(json.utf8))
|
|
if case .unitValue(let value, let unit) = response.result.value {
|
|
#expect(value == 5.0)
|
|
#expect(unit == "kg")
|
|
} else {
|
|
Issue.record("Expected .unitValue variant")
|
|
}
|
|
}
|
|
|
|
@Test("Decode currency value result")
|
|
func decodeCurrencyValueResult() throws {
|
|
let json = """
|
|
{
|
|
"schema_version": "1.0",
|
|
"result": {
|
|
"value": { "kind": "CurrencyValue", "amount": 19.99, "currency": "USD" },
|
|
"metadata": {
|
|
"span": { "start": 0, "end": 6 },
|
|
"result_type": "CurrencyValue",
|
|
"display": "$19.99",
|
|
"raw_value": 19.99
|
|
}
|
|
}
|
|
}
|
|
"""
|
|
let response = try JSONDecoder().decode(FFIResponse.self, from: Data(json.utf8))
|
|
if case .currencyValue(let amount, let currency) = response.result.value {
|
|
#expect(amount == 19.99)
|
|
#expect(currency == "USD")
|
|
} else {
|
|
Issue.record("Expected .currencyValue variant")
|
|
}
|
|
}
|
|
|
|
@Test("Decode datetime result")
|
|
func decodeDateTimeResult() throws {
|
|
let json = """
|
|
{
|
|
"schema_version": "1.0",
|
|
"result": {
|
|
"value": { "kind": "DateTime", "date": "2024-03-17" },
|
|
"metadata": {
|
|
"span": { "start": 0, "end": 10 },
|
|
"result_type": "DateTime",
|
|
"display": "2024-03-17",
|
|
"raw_value": null
|
|
}
|
|
}
|
|
}
|
|
"""
|
|
let response = try JSONDecoder().decode(FFIResponse.self, from: Data(json.utf8))
|
|
if case .dateTime(let date) = response.result.value {
|
|
#expect(date == "2024-03-17")
|
|
} else {
|
|
Issue.record("Expected .dateTime variant")
|
|
}
|
|
#expect(response.result.metadata.rawValue == nil)
|
|
}
|
|
|
|
@Test("Decode time delta result")
|
|
func decodeTimeDeltaResult() throws {
|
|
let json = """
|
|
{
|
|
"schema_version": "1.0",
|
|
"result": {
|
|
"value": { "kind": "TimeDelta", "days": 30, "description": "30 days" },
|
|
"metadata": {
|
|
"span": { "start": 0, "end": 8 },
|
|
"result_type": "TimeDelta",
|
|
"display": "30 days",
|
|
"raw_value": 30.0
|
|
}
|
|
}
|
|
}
|
|
"""
|
|
let response = try JSONDecoder().decode(FFIResponse.self, from: Data(json.utf8))
|
|
if case .timeDelta(let days, let desc) = response.result.value {
|
|
#expect(days == 30)
|
|
#expect(desc == "30 days")
|
|
} else {
|
|
Issue.record("Expected .timeDelta variant")
|
|
}
|
|
}
|
|
|
|
@Test("Decode boolean result")
|
|
func decodeBooleanResult() throws {
|
|
let json = """
|
|
{
|
|
"schema_version": "1.0",
|
|
"result": {
|
|
"value": { "kind": "Boolean", "value": true },
|
|
"metadata": {
|
|
"span": { "start": 0, "end": 5 },
|
|
"result_type": "Boolean",
|
|
"display": "true",
|
|
"raw_value": null
|
|
}
|
|
}
|
|
}
|
|
"""
|
|
let response = try JSONDecoder().decode(FFIResponse.self, from: Data(json.utf8))
|
|
if case .boolean(let value) = response.result.value {
|
|
#expect(value == true)
|
|
} else {
|
|
Issue.record("Expected .boolean variant")
|
|
}
|
|
}
|
|
|
|
@Test("Decode error result")
|
|
func decodeErrorResult() throws {
|
|
let json = """
|
|
{
|
|
"schema_version": "1.0",
|
|
"result": {
|
|
"value": { "kind": "Error", "message": "unexpected token", "span": { "start": 0, "end": 3 } },
|
|
"metadata": {
|
|
"span": { "start": 0, "end": 3 },
|
|
"result_type": "Error",
|
|
"display": "Error: unexpected token",
|
|
"raw_value": null
|
|
}
|
|
}
|
|
}
|
|
"""
|
|
let response = try JSONDecoder().decode(FFIResponse.self, from: Data(json.utf8))
|
|
#expect(response.result.value.isError)
|
|
if case .error(let message) = response.result.value {
|
|
#expect(message == "unexpected token")
|
|
} else {
|
|
Issue.record("Expected .error variant")
|
|
}
|
|
}
|
|
|
|
// MARK: - Sheet Response
|
|
|
|
@Test("Decode sheet response with multiple results")
|
|
func decodeSheetResponse() throws {
|
|
let json = """
|
|
{
|
|
"schema_version": "1.0",
|
|
"results": [
|
|
{
|
|
"value": { "kind": "Number", "value": 4.0 },
|
|
"metadata": { "span": { "start": 0, "end": 5 }, "result_type": "Number", "display": "4", "raw_value": 4.0 }
|
|
},
|
|
{
|
|
"value": { "kind": "Error", "message": "empty expression", "span": { "start": 0, "end": 0 } },
|
|
"metadata": { "span": { "start": 0, "end": 0 }, "result_type": "Error", "display": "Error: empty expression", "raw_value": null }
|
|
},
|
|
{
|
|
"value": { "kind": "Number", "value": 30.0 },
|
|
"metadata": { "span": { "start": 0, "end": 6 }, "result_type": "Number", "display": "30", "raw_value": 30.0 }
|
|
}
|
|
]
|
|
}
|
|
"""
|
|
let response = try JSONDecoder().decode(FFISheetResponse.self, from: Data(json.utf8))
|
|
#expect(response.results.count == 3)
|
|
#expect(response.results[0].metadata.display == "4")
|
|
#expect(response.results[1].value.isError)
|
|
#expect(response.results[2].metadata.display == "30")
|
|
}
|
|
|
|
// MARK: - FFICalcValue.isError
|
|
|
|
@Test("isError returns false for non-error values")
|
|
func isErrorFalseForNonError() throws {
|
|
let json = """
|
|
{ "kind": "Number", "value": 42.0 }
|
|
"""
|
|
let value = try JSONDecoder().decode(FFICalcValue.self, from: Data(json.utf8))
|
|
#expect(value.isError == false)
|
|
}
|
|
}
|