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/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", 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) } } 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 + ) + } +}