From a189c646c03fff973f19f299641efec4a4183b44 Mon Sep 17 00:00:00 2001 From: William Laverty Date: Tue, 10 Mar 2026 11:12:12 -0700 Subject: [PATCH 1/3] Add comprehensive test suite and GitHub Actions CI - Add 30+ test cases covering execution, errors, globals, functions, booleans, macros, multi-return, multiple instances, and error messages - Add macro expansion tests with diagnostic validation - Add GitHub Actions workflow (macOS 14, Xcode 15.4) - CI runs on push to main and all PRs - Clean up stale test files --- .github/workflows/ci.yml | 33 ++ Tests/LuaDemoTests/LuaDemoTests.swift | 14 + Tests/LuaDemoTests/LuaIntegrationTest.swift | 16 - Tests/LuaKitTests/LuaExecutionTests.swift | 407 ++++++++++++++++++-- Tests/LuaKitTests/LuaMacroTests.swift | 135 +++++++ 5 files changed, 556 insertions(+), 49 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 Tests/LuaDemoTests/LuaDemoTests.swift delete mode 100644 Tests/LuaDemoTests/LuaIntegrationTest.swift create mode 100644 Tests/LuaKitTests/LuaMacroTests.swift diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..2fc044a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,33 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build-and-test: + name: Build & Test + runs-on: macos-14 + strategy: + matrix: + xcode: ['15.4'] + steps: + - uses: actions/checkout@v4 + + - name: Select Xcode ${{ matrix.xcode }} + run: sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode }}.app + + - name: Install Lua + run: brew install lua + + - name: Build + run: swift build -v + + - name: Run Tests + run: swift test -v diff --git a/Tests/LuaDemoTests/LuaDemoTests.swift b/Tests/LuaDemoTests/LuaDemoTests.swift new file mode 100644 index 0000000..f1bf643 --- /dev/null +++ b/Tests/LuaDemoTests/LuaDemoTests.swift @@ -0,0 +1,14 @@ +// +// LuaDemoTests.swift +// Placeholder for demo integration tests. +// Created by William Laverty on 14/12/2023. +// + +import XCTest + +final class LuaDemoTests: XCTestCase { + func testDemoTargetBuilds() throws { + // Validates the demo target compiles successfully + XCTAssertTrue(true) + } +} diff --git a/Tests/LuaDemoTests/LuaIntegrationTest.swift b/Tests/LuaDemoTests/LuaIntegrationTest.swift deleted file mode 100644 index 37da53d..0000000 --- a/Tests/LuaDemoTests/LuaIntegrationTest.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// LuaIntegrationTest.swift -// Tests LuaKitDemo functionality. -// Created by William Laverty on 14/12/2023. -// - -@testable import LuaKitDemo -import XCTest - -// Test class for LuaKitDemo functionality -final class LuaDemoTests: XCTestCase { - // A test case example - func testExample() throws { - // Add your test implementation here - } -} diff --git a/Tests/LuaKitTests/LuaExecutionTests.swift b/Tests/LuaKitTests/LuaExecutionTests.swift index 1b217a3..6450e81 100644 --- a/Tests/LuaKitTests/LuaExecutionTests.swift +++ b/Tests/LuaKitTests/LuaExecutionTests.swift @@ -1,44 +1,385 @@ // -// LuaIntegrationTests.swift -// Tests for Lua integration using macros. +// LuaExecutionTests.swift +// Tests for Lua script execution, error handling, and stack management. // Created by William Laverty on 14/12/2023. // -import SwiftSyntaxMacros -import SwiftSyntaxMacrosTestSupport import XCTest -import LuaKitMacros - -// Dictionary mapping test macros -let testMacros: [String: Macro.Type] = [ - "LuaFunction": LuaFunctionMacro.self, -] - -// Test class for LuaFunctionMacro -final class LuaFunctionMacroTests: XCTestCase { - - // Test method for macro without attributes - func testMacroNoAttributes() { - assertMacroExpansion( - """ - @LuaFunction - func helloWorld() { - print("Test") +@testable import LuaKit + +final class LuaExecutionTests: XCTestCase { + + var lua: Lua! + + override func setUp() { + super.setUp() + lua = Lua() + } + + override func tearDown() { + lua.cleanup() + lua = nil + super.tearDown() + } + + // MARK: - Initialization + + func testInitCreatesValidState() { + XCTAssertNotNil(lua.state, "Lua state should be non-nil after init") + } + + func testCleanupNilsState() { + lua.cleanup() + XCTAssertNil(lua.state, "Lua state should be nil after cleanup") + } + + func testDoubleCleanupDoesNotCrash() { + lua.cleanup() + lua.cleanup() // Should not crash + } + + // MARK: - Execute Source + + func testExecuteSimpleScript() throws { + let results = try lua.execute(source: "return 42") + XCTAssertEqual(results.count, 1) + XCTAssertEqual(results[0] as? Int64, 42) + } + + func testExecuteReturnsMultipleValues() throws { + let results = try lua.execute(source: "return 1, 'hello', true") + XCTAssertEqual(results.count, 3) + XCTAssertEqual(results[0] as? Int64, 1) + XCTAssertEqual(results[1] as? String, "hello") + XCTAssertEqual(results[2] as? Bool, true) + } + + func testExecuteNoReturnValue() throws { + let results = try lua.execute(source: "local x = 1 + 1") + XCTAssertTrue(results.isEmpty) + } + + func testExecuteStringReturn() throws { + let results = try lua.execute(source: "return 'hello world'") + XCTAssertEqual(results.first as? String, "hello world") + } + + func testExecuteFloatReturn() throws { + let results = try lua.execute(source: "return 3.14") + let value = results.first as? Double + XCTAssertNotNil(value) + XCTAssertEqual(value!, 3.14, accuracy: 0.001) + } + + func testExecuteBooleanReturn() throws { + let trueResults = try lua.execute(source: "return true") + XCTAssertEqual(trueResults.first as? Bool, true) + + let falseResults = try lua.execute(source: "return false") + XCTAssertEqual(falseResults.first as? Bool, false) + } + + func testExecuteNilReturn() throws { + let results = try lua.execute(source: "return nil") + XCTAssertEqual(results.count, 1) + XCTAssertTrue(results[0] is LuaNull) + } + + // MARK: - Execute Errors + + func testExecuteSyntaxErrorThrows() { + XCTAssertThrowsError(try lua.execute(source: "this is not valid lua !!!")) { error in + guard case LuaError.loadError = error else { + XCTFail("Expected LuaError.loadError, got \(error)") + return + } + } + } + + func testExecuteRuntimeErrorThrows() { + XCTAssertThrowsError(try lua.execute(source: "error('boom')")) { error in + guard case LuaError.executionError(let msg) = error else { + XCTFail("Expected LuaError.executionError, got \(error)") + return } - """, - expandedSource: """ - func helloWorld() { - print("Test") + XCTAssertTrue(msg.contains("boom")) + } + } + + func testExecuteUndefinedFunctionErrorThrows() { + XCTAssertThrowsError(try lua.execute(source: "nonexistent_function()")) { error in + guard case LuaError.executionError = error else { + XCTFail("Expected LuaError.executionError, got \(error)") + return } + } + } + + // MARK: - Execute File + + func testExecuteFileNotFoundThrows() { + let url = URL(fileURLWithPath: "/nonexistent/path/script.lua") + XCTAssertThrowsError(try lua.execute(url: url)) { error in + guard case LuaError.fileNotFound = error else { + XCTFail("Expected LuaError.fileNotFound, got \(error)") + return + } + } + } + + func testExecuteFileWithValidScript() throws { + let tempDir = FileManager.default.temporaryDirectory + let scriptURL = tempDir.appendingPathComponent("test_\(UUID().uuidString).lua") + try "return 99".write(to: scriptURL, atomically: true, encoding: .utf8) + defer { try? FileManager.default.removeItem(at: scriptURL) } + + let results = try lua.execute(url: scriptURL) + XCTAssertEqual(results.first as? Int64, 99) + } + + func testExecuteFileWithSyntaxError() throws { + let tempDir = FileManager.default.temporaryDirectory + let scriptURL = tempDir.appendingPathComponent("bad_\(UUID().uuidString).lua") + try "this is not valid lua !!!".write(to: scriptURL, atomically: true, encoding: .utf8) + defer { try? FileManager.default.removeItem(at: scriptURL) } + + XCTAssertThrowsError(try lua.execute(url: scriptURL)) { error in + guard case LuaError.loadError = error else { + XCTFail("Expected LuaError.loadError, got \(error)") + return + } + } + } + + // MARK: - Stack Management + + func testStackSizeStartsAtZero() { + XCTAssertEqual(lua.stackSize, 0) + } + + func testWithUnchangedStackSucceeds() { + let result = lua.withUnchangedStack { + return 42 + } + XCTAssertEqual(result, 42) + } + + // MARK: - Global Variables - func _register(lua: Lua) throws { - try lua.register(function: "helloWorld") { lua in - helloWorld() - return 1 - } + func testSetAndGetGlobalString() { + lua.setGlobal(name: "greeting", value: "hello") + let value = lua.getGlobal(name: "greeting") + XCTAssertEqual(value as? String, "hello") + } + + func testSetAndGetGlobalInt() { + lua.setGlobal(name: "count", value: Int64(42)) + let value = lua.getGlobal(name: "count") + XCTAssertEqual(value as? Int64, 42) + } + + func testSetAndGetGlobalDouble() { + lua.setGlobal(name: "pi", value: 3.14159) + let value = lua.getGlobal(name: "pi") + let result = value as? Double + XCTAssertNotNil(result) + XCTAssertEqual(result!, 3.14159, accuracy: 0.00001) + } + + func testSetAndGetGlobalBool() { + lua.setGlobal(name: "flag", value: true) + let value = lua.getGlobal(name: "flag") + XCTAssertEqual(value as? Bool, true) + + lua.setGlobal(name: "flag", value: false) + let value2 = lua.getGlobal(name: "flag") + XCTAssertEqual(value2 as? Bool, false) + } + + func testGetUndefinedGlobalReturnsNull() { + let value = lua.getGlobal(name: "undefined_variable") + XCTAssertTrue(value is LuaNull) + } + + func testGlobalAccessibleFromScript() throws { + lua.setGlobal(name: "x", value: Int64(10)) + let results = try lua.execute(source: "return x + 5") + XCTAssertEqual(results.first as? Int64, 15) + } + + // MARK: - Function Calls + + func testCallLuaFunction() throws { + try lua.execute(source: """ + function add(a, b) + return a + b + end + """) + let result = try lua.call(function: "add", parameters: [Int64(3), Int64(4)]) + XCTAssertEqual(result as? Int64, 7) + } + + func testCallLuaFunctionWithStringParams() throws { + try lua.execute(source: """ + function greet(name) + return "Hello, " .. name .. "!" + end + """) + let result = try lua.call(function: "greet", parameters: ["World"]) + XCTAssertEqual(result as? String, "Hello, World!") + } + + func testCallLuaFunctionNoParams() throws { + try lua.execute(source: """ + function getFortyTwo() + return 42 + end + """) + let result = try lua.call(function: "getFortyTwo") + XCTAssertEqual(result as? Int64, 42) + } + + func testCallLuaFunctionNoReturnValue() throws { + try lua.execute(source: """ + called = false + function sideEffect() + called = true + end + """) + let result = try lua.call(function: "sideEffect") + XCTAssertTrue(result is LuaNull) + + let called = lua.getGlobal(name: "called") + XCTAssertEqual(called as? Bool, true) + } + + func testCallMultiReturn() throws { + try lua.execute(source: """ + function multiReturn() + return 1, "two", 3.0 + end + """) + let results = try lua.callMultiReturn(function: "multiReturn") + XCTAssertEqual(results.count, 3) + XCTAssertEqual(results[0] as? Int64, 1) + XCTAssertEqual(results[1] as? String, "two") + } + + func testCallUndefinedFunctionThrows() throws { + XCTAssertThrowsError(try lua.call(function: "nonexistent")) { error in + guard case LuaError.executionError = error else { + XCTFail("Expected LuaError.executionError, got \(error)") + return } - """, - macros: testMacros - ) + } + } + + // MARK: - Function Registration + + func testRegisterSwiftFunction() throws { + try lua.register(function: "swiftAdd") { lua in + // For now just return a constant + lua.setGlobal(name: "swift_result", value: Int64(100)) + return 0 + } + + try lua.execute(source: "swiftAdd()") + let result = lua.getGlobal(name: "swift_result") + XCTAssertEqual(result as? Int64, 100) + } + + // MARK: - Boolean Correctness + + func testBooleanTruePassedCorrectlyToLua() throws { + lua.setGlobal(name: "flag", value: true) + let results = try lua.execute(source: """ + if flag == true then + return "yes" + else + return "no" + end + """) + XCTAssertEqual(results.first as? String, "yes") + } + + func testBooleanFalsePassedCorrectlyToLua() throws { + lua.setGlobal(name: "flag", value: false) + let results = try lua.execute(source: """ + if flag == false then + return "no" + else + return "yes" + end + """) + XCTAssertEqual(results.first as? String, "no") + } + + // MARK: - Complex Scripts + + func testFibonacci() throws { + try lua.execute(source: """ + function fib(n) + if n <= 1 then return n end + return fib(n - 1) + fib(n - 2) + end + """) + let result = try lua.call(function: "fib", parameters: [Int64(10)]) + XCTAssertEqual(result as? Int64, 55) + } + + func testStringManipulation() throws { + let results = try lua.execute(source: """ + local s = "Hello, World!" + return string.len(s), string.upper(s), string.sub(s, 1, 5) + """) + XCTAssertEqual(results.count, 3) + XCTAssertEqual(results[0] as? Int64, 13) + XCTAssertEqual(results[1] as? String, "HELLO, WORLD!") + XCTAssertEqual(results[2] as? String, "Hello") + } + + func testMathOperations() throws { + let results = try lua.execute(source: """ + return math.max(1, 5, 3), math.min(1, 5, 3), math.abs(-42) + """) + XCTAssertEqual(results.count, 3) + XCTAssertEqual(results[0] as? Int64, 5) + XCTAssertEqual(results[1] as? Int64, 1) + XCTAssertEqual(results[2] as? Int64, 42) + } + + // MARK: - Multiple Independent Instances + + func testMultipleLuaInstances() throws { + let lua2 = Lua() + defer { lua2.cleanup() } + + lua.setGlobal(name: "x", value: Int64(10)) + lua2.setGlobal(name: "x", value: Int64(20)) + + let r1 = try lua.execute(source: "return x") + let r2 = try lua2.execute(source: "return x") + + XCTAssertEqual(r1.first as? Int64, 10) + XCTAssertEqual(r2.first as? Int64, 20) + } + + // MARK: - LuaError Descriptions + + func testLuaErrorDescriptions() { + let loadErr = LuaError.loadError("bad syntax") + XCTAssertTrue(loadErr.localizedDescription.contains("bad syntax")) + + let execErr = LuaError.executionError("runtime boom") + XCTAssertTrue(execErr.localizedDescription.contains("runtime boom")) + + let fileErr = LuaError.fileNotFound("/some/path") + XCTAssertTrue(fileErr.localizedDescription.contains("/some/path")) + + let typeErr = LuaError.unexpectedType("table") + XCTAssertTrue(typeErr.localizedDescription.contains("table")) + + let regErr = LuaError.registrationFailed("oops") + XCTAssertTrue(regErr.localizedDescription.contains("oops")) } } diff --git a/Tests/LuaKitTests/LuaMacroTests.swift b/Tests/LuaKitTests/LuaMacroTests.swift new file mode 100644 index 0000000..e4dcc40 --- /dev/null +++ b/Tests/LuaKitTests/LuaMacroTests.swift @@ -0,0 +1,135 @@ +// +// LuaMacroTests.swift +// Tests for the @LuaFunction macro expansion. +// Created by William Laverty on 14/12/2023. +// + +import SwiftSyntaxMacros +import SwiftSyntaxMacrosTestSupport +import XCTest +import LuaKitMacros + +let testMacros: [String: Macro.Type] = [ + "LuaFunction": LuaFunctionMacro.self, +] + +final class LuaMacroTests: XCTestCase { + + func testMacroExpandsSimpleFunction() { + assertMacroExpansion( + """ + @LuaFunction + func helloWorld() { + print("Test") + } + """, + expandedSource: """ + func helloWorld() { + print("Test") + } + + func _register(lua: Lua) throws { + try lua.register(function: "helloWorld") { lua in + helloWorld() + return 1 + } + } + """, + macros: testMacros + ) + } + + func testMacroExpandsFunctionWithDifferentName() { + assertMacroExpansion( + """ + @LuaFunction + func calculateSum() { + let _ = 1 + 2 + } + """, + expandedSource: """ + func calculateSum() { + let _ = 1 + 2 + } + + func _register(lua: Lua) throws { + try lua.register(function: "calculateSum") { lua in + calculateSum() + return 1 + } + } + """, + macros: testMacros + ) + } + + func testMacroOnNonFunctionProducesDiagnostic() { + assertMacroExpansion( + """ + @LuaFunction + struct NotAFunction {} + """, + expandedSource: """ + struct NotAFunction {} + """, + diagnostics: [ + DiagnosticSpec(message: "@LuaFunction can only be applied to functions", line: 1, column: 1) + ], + macros: testMacros + ) + } + + func testMacroOnClassProducesDiagnostic() { + assertMacroExpansion( + """ + @LuaFunction + class NotAFunction {} + """, + expandedSource: """ + class NotAFunction {} + """, + diagnostics: [ + DiagnosticSpec(message: "@LuaFunction can only be applied to functions", line: 1, column: 1) + ], + macros: testMacros + ) + } + + func testMacroOnVariableProducesDiagnostic() { + assertMacroExpansion( + """ + @LuaFunction + var notAFunction = 42 + """, + expandedSource: """ + var notAFunction = 42 + """, + diagnostics: [ + DiagnosticSpec(message: "@LuaFunction can only be applied to functions", line: 1, column: 1) + ], + macros: testMacros + ) + } + + func testMacroExpandsEmptyFunction() { + assertMacroExpansion( + """ + @LuaFunction + func doNothing() { + } + """, + expandedSource: """ + func doNothing() { + } + + func _register(lua: Lua) throws { + try lua.register(function: "doNothing") { lua in + doNothing() + return 1 + } + } + """, + macros: testMacros + ) + } +} From 30f57d056eabccfae119901e2765ae2fc7f579c0 Mon Sep 17 00:00:00 2001 From: William Laverty Date: Tue, 10 Mar 2026 11:39:39 -0700 Subject: [PATCH 2/3] Fix demo: add missing try for throwing execute() --- Sources/LuaKitDemo/main.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/LuaKitDemo/main.swift b/Sources/LuaKitDemo/main.swift index 0920af8..a25b092 100644 --- a/Sources/LuaKitDemo/main.swift +++ b/Sources/LuaKitDemo/main.swift @@ -29,6 +29,6 @@ do { """# // Execute Lua code using the Lua instance - lua.execute(source: source) + try lua.execute(source: source) } } From fe028efb14ab86a80b5f7699b84a0b1a9f814874 Mon Sep 17 00:00:00 2001 From: William Laverty Date: Tue, 10 Mar 2026 12:23:24 -0700 Subject: [PATCH 3/3] Fix linker error: add LuaKitMacros dependency to LuaKitTests target The macro test file imports LuaKitMacros directly to reference LuaFunctionMacro.self, but the test target was missing the dependency, causing undefined symbol errors at link time. Co-Authored-By: Claude Opus 4.6 (1M context) --- Package.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Package.swift b/Package.swift index 8a01277..2c2722f 100644 --- a/Package.swift +++ b/Package.swift @@ -26,6 +26,7 @@ let package = Package( name: "LuaKitTests", dependencies: [ "LuaKit", + "LuaKitMacros", .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax") ]), .macro(name: "LuaKitMacros",