diff --git a/Tests/CasbinTests/ConfigTests.swift b/Tests/CasbinTests/ConfigTests.swift index ab7725c..cba8246 100644 --- a/Tests/CasbinTests/ConfigTests.swift +++ b/Tests/CasbinTests/ConfigTests.swift @@ -9,9 +9,9 @@ struct ConfigTests { let filePath = #file.components(separatedBy: "ConfigTests.swift")[0] + "examples/testini.ini" let pool = NIOThreadPool(numberOfThreads: 1) pool.start() - defer { try? pool.syncShutdownGracefully() } + defer { shutdownThreadPoolInBackground(pool) } let elg = MultiThreadedEventLoopGroup(numberOfThreads: 1) - defer { try? elg.syncShutdownGracefully() } + defer { shutdownEventLoopGroupInBackground(elg) } let fileIo = NonBlockingFileIO(threadPool: pool) var config = try Config.from(file: filePath, fileIo: fileIo, on: elg.next()).wait() @@ -36,7 +36,7 @@ struct ConfigTests { @Test("load from text and get/set") func testFromText() throws { let elg = MultiThreadedEventLoopGroup(numberOfThreads: 1) - defer { try? elg.syncShutdownGracefully() } + defer { shutdownEventLoopGroupInBackground(elg) } let text = #""" # test config debug = true diff --git a/Tests/CasbinTests/EnforcerTests.swift b/Tests/CasbinTests/EnforcerTests.swift index 62c004f..bf4a270 100644 --- a/Tests/CasbinTests/EnforcerTests.swift +++ b/Tests/CasbinTests/EnforcerTests.swift @@ -12,8 +12,9 @@ struct EnforcerTests { pool.start() let elg = MultiThreadedEventLoopGroup(numberOfThreads: 1) defer { - try? elg.syncShutdownGracefully() + // Ensure resources are fully shut down before the test completes. try? pool.syncShutdownGracefully() + try? elg.syncShutdownGracefully() } return try body(pool, elg) } diff --git a/Tests/CasbinTests/MemoryAdapterAsyncTests.swift b/Tests/CasbinTests/MemoryAdapterAsyncTests.swift index 00e3138..4811f20 100644 --- a/Tests/CasbinTests/MemoryAdapterAsyncTests.swift +++ b/Tests/CasbinTests/MemoryAdapterAsyncTests.swift @@ -5,9 +5,19 @@ import NIO @Suite("MemoryAdapter Async/Await API Tests") struct MemoryAdapterAsyncTests { - private func makeAdapter() -> MemoryAdapter { + // Each test must shut down its EventLoopGroup or swift test may hang. + // Provide a helper that manages lifecycle and properly awaits shutdown completion. + private func withAdapter(_ body: (MemoryAdapter) async throws -> R) async throws -> R { let elg = MultiThreadedEventLoopGroup(numberOfThreads: 1) - return MemoryAdapter(on: elg.next()) + let adapter = MemoryAdapter(on: elg.next()) + do { + let result = try await body(adapter) + try await shutdownEventLoopGroupAsync(elg) + return result + } catch { + try? await shutdownEventLoopGroupAsync(elg) + throw error + } } private func makeModel() -> DefaultModel { @@ -21,183 +31,190 @@ struct MemoryAdapterAsyncTests { @Test("async addPolicy and loadPolicy") func testAsyncAddAndLoadPolicy() async throws { - let adapter = makeAdapter() - let model = makeModel() - - // Add a policy using async/await - let added = try await adapter.addPolicy(sec: "p", ptype: "p", rule: ["alice", "data1", "read"]) - #expect(added == true) - - // Load the policy using async/await - try await adapter.loadPolicy(m: model) - - // Verify the policy was loaded - guard let ast = model.getModel()["p"]?["p"] else { - Issue.record("Policy not found in model") - return + try await withAdapter { adapter in + let model = makeModel() + + // Add a policy using async/await + let added = try await adapter.addPolicy(sec: "p", ptype: "p", rule: ["alice", "data1", "read"]) + #expect(added == true) + + // Load the policy using async/await + try await adapter.loadPolicy(m: model) + + // Verify the policy was loaded + guard let ast = model.getModel()["p"]?["p"] else { + Issue.record("Policy not found in model") + return + } + // Model policies do NOT include ptype; only the rule fields + #expect(ast.policy.contains(["alice", "data1", "read"])) } - // Model policies do NOT include ptype; only the rule fields - #expect(ast.policy.contains(["alice", "data1", "read"])) } @Test("async addPolicies and savePolicy") func testAsyncAddPoliciesAndSave() async throws { - let adapter = makeAdapter() - let model = makeModel() - - // Add multiple policies using async/await - let rules = [ - ["alice", "data1", "read"], - ["bob", "data2", "write"] - ] - let added = try await adapter.addPolicies(sec: "p", ptype: "p", rules: rules) - #expect(added == true) - - // Load policies into model - try await adapter.loadPolicy(m: model) - - // Verify policies loaded - guard let ast = model.getModel()["p"]?["p"] else { - Issue.record("Policy not found in model") - return - } - #expect(ast.policy.count == 2) - - // Save policy using async/await - try await adapter.savePolicy(m: model) - - // Reload to verify save worked - let model2 = makeModel() - try await adapter.loadPolicy(m: model2) - guard let ast2 = model2.getModel()["p"]?["p"] else { - Issue.record("Policy not found after reload") - return + try await withAdapter { adapter in + let model = makeModel() + + // Add multiple policies using async/await + let rules = [ + ["alice", "data1", "read"], + ["bob", "data2", "write"] + ] + let added = try await adapter.addPolicies(sec: "p", ptype: "p", rules: rules) + #expect(added == true) + + // Load policies into model + try await adapter.loadPolicy(m: model) + + // Verify policies loaded + guard let ast = model.getModel()["p"]?["p"] else { + Issue.record("Policy not found in model") + return + } + #expect(ast.policy.count == 2) + + // Save policy using async/await + try await adapter.savePolicy(m: model) + + // Reload to verify save worked + let model2 = makeModel() + try await adapter.loadPolicy(m: model2) + guard let ast2 = model2.getModel()["p"]?["p"] else { + Issue.record("Policy not found after reload") + return + } + #expect(ast2.policy.count == 2) } - #expect(ast2.policy.count == 2) } @Test("async removePolicy") func testAsyncRemovePolicy() async throws { - let adapter = makeAdapter() - let model = makeModel() - - // Add a policy first - _ = try await adapter.addPolicy(sec: "p", ptype: "p", rule: ["alice", "data1", "read"]) - - // Remove it using async/await - let removed = try await adapter.removePolicy(sec: "p", ptype: "p", rule: ["alice", "data1", "read"]) - #expect(removed == true) - - // Verify it's gone by loading - try await adapter.loadPolicy(m: model) - guard let ast = model.getModel()["p"]?["p"] else { - // No policies - this is expected - return + try await withAdapter { adapter in + let model = makeModel() + + // Add a policy first + _ = try await adapter.addPolicy(sec: "p", ptype: "p", rule: ["alice", "data1", "read"]) + + // Remove it using async/await + let removed = try await adapter.removePolicy(sec: "p", ptype: "p", rule: ["alice", "data1", "read"]) + #expect(removed == true) + + // Verify it's gone by loading + try await adapter.loadPolicy(m: model) + guard let ast = model.getModel()["p"]?["p"] else { + // No policies - this is expected + return + } + #expect(ast.policy.isEmpty) } - #expect(ast.policy.isEmpty) } @Test("async removePolicies") func testAsyncRemovePolicies() async throws { - let adapter = makeAdapter() - let model = makeModel() - - // Add multiple policies - let rules = [ - ["alice", "data1", "read"], - ["bob", "data2", "write"] - ] - _ = try await adapter.addPolicies(sec: "p", ptype: "p", rules: rules) - - // Remove them using async/await - // Note: removePolicies has a bug - it checks if policies exist and returns false - // This is pre-existing behavior in the original implementation - let removed = try await adapter.removePolicies(sec: "p", ptype: "p", rules: rules) - #expect(removed == true) - - // Verify the policies are actually gone - try await adapter.loadPolicy(m: model) - guard let ast = model.getModel()["p"]?["p"] else { - // No policies - this is expected - return + try await withAdapter { adapter in + let model = makeModel() + + // Add multiple policies + let rules = [ + ["alice", "data1", "read"], + ["bob", "data2", "write"] + ] + _ = try await adapter.addPolicies(sec: "p", ptype: "p", rules: rules) + + // Remove them using async/await + // Note: removePolicies has a bug - it checks if policies exist and returns false + // This is pre-existing behavior in the original implementation + let removed = try await adapter.removePolicies(sec: "p", ptype: "p", rules: rules) + #expect(removed == true) + + // Verify the policies are actually gone + try await adapter.loadPolicy(m: model) + guard let ast = model.getModel()["p"]?["p"] else { + // No policies - this is expected + return + } + #expect(ast.policy.isEmpty) } - #expect(ast.policy.isEmpty) } @Test("async clearPolicy") func testAsyncClearPolicy() async throws { - let adapter = makeAdapter() - let model = makeModel() - - // Add some policies - _ = try await adapter.addPolicy(sec: "p", ptype: "p", rule: ["alice", "data1", "read"]) - _ = try await adapter.addPolicy(sec: "p", ptype: "p", rule: ["bob", "data2", "write"]) - - // Clear using async/await - try await adapter.clearPolicy() - - // Verify empty - try await adapter.loadPolicy(m: model) - guard let ast = model.getModel()["p"]?["p"] else { - // No policies - this is expected after clear - return + try await withAdapter { adapter in + let model = makeModel() + + // Add some policies + _ = try await adapter.addPolicy(sec: "p", ptype: "p", rule: ["alice", "data1", "read"]) + _ = try await adapter.addPolicy(sec: "p", ptype: "p", rule: ["bob", "data2", "write"]) + + // Clear using async/await + try await adapter.clearPolicy() + + // Verify empty + try await adapter.loadPolicy(m: model) + guard let ast = model.getModel()["p"]?["p"] else { + // No policies - this is expected after clear + return + } + #expect(ast.policy.isEmpty) + #expect(adapter.isFiltered == false) } - #expect(ast.policy.isEmpty) - #expect(adapter.isFiltered == false) } @Test("async removeFilteredPolicy") func testAsyncRemoveFilteredPolicy() async throws { - let adapter = makeAdapter() - let model = makeModel() - - // Add multiple policies - _ = try await adapter.addPolicy(sec: "p", ptype: "p", rule: ["alice", "data1", "read"]) - _ = try await adapter.addPolicy(sec: "p", ptype: "p", rule: ["alice", "data2", "write"]) - _ = try await adapter.addPolicy(sec: "p", ptype: "p", rule: ["bob", "data1", "read"]) - - // Remove filtered - all alice policies - let removed = try await adapter.removeFilteredPolicy( - sec: "p", - ptype: "p", - fieldIndex: 0, - fieldValues: ["alice"] - ) - #expect(removed == true) - - // Verify only bob's policy remains - try await adapter.loadPolicy(m: model) - guard let ast = model.getModel()["p"]?["p"] else { - Issue.record("Policy not found in model") - return + try await withAdapter { adapter in + let model = makeModel() + + // Add multiple policies + _ = try await adapter.addPolicy(sec: "p", ptype: "p", rule: ["alice", "data1", "read"]) + _ = try await adapter.addPolicy(sec: "p", ptype: "p", rule: ["alice", "data2", "write"]) + _ = try await adapter.addPolicy(sec: "p", ptype: "p", rule: ["bob", "data1", "read"]) + + // Remove filtered - all alice policies + let removed = try await adapter.removeFilteredPolicy( + sec: "p", + ptype: "p", + fieldIndex: 0, + fieldValues: ["alice"] + ) + #expect(removed == true) + + // Verify only bob's policy remains + try await adapter.loadPolicy(m: model) + guard let ast = model.getModel()["p"]?["p"] else { + Issue.record("Policy not found in model") + return + } + #expect(ast.policy.count == 1) + #expect(ast.policy.contains(["bob", "data1", "read"])) } - #expect(ast.policy.count == 1) - #expect(ast.policy.contains(["bob", "data1", "read"])) } @Test("async loadFilteredPolicy") func testAsyncLoadFilteredPolicy() async throws { - let adapter = makeAdapter() - let model = makeModel() - - // Add multiple policies - _ = try await adapter.addPolicy(sec: "p", ptype: "p", rule: ["alice", "data1", "read"]) - _ = try await adapter.addPolicy(sec: "p", ptype: "p", rule: ["alice", "data2", "write"]) - _ = try await adapter.addPolicy(sec: "p", ptype: "p", rule: ["bob", "data1", "read"]) - - // Load with filter using async/await - let filter = Filter(p: ["alice"], g: []) - try await adapter.loadFilteredPolicy(m: model, f: filter) - - // Verify filtering worked - guard let ast = model.getModel()["p"]?["p"] else { - Issue.record("Policy not found in model") - return + try await withAdapter { adapter in + let model = makeModel() + + // Add multiple policies + _ = try await adapter.addPolicy(sec: "p", ptype: "p", rule: ["alice", "data1", "read"]) + _ = try await adapter.addPolicy(sec: "p", ptype: "p", rule: ["alice", "data2", "write"]) + _ = try await adapter.addPolicy(sec: "p", ptype: "p", rule: ["bob", "data1", "read"]) + + // Load with filter using async/await + let filter = Filter(p: ["alice"], g: []) + try await adapter.loadFilteredPolicy(m: model, f: filter) + + // Verify filtering worked + guard let ast = model.getModel()["p"]?["p"] else { + Issue.record("Policy not found in model") + return + } + + // Should only have bob's policy (alice was filtered out) + // Note: all policies are loaded despite filter - filtering just sets isFiltered flag + // The implementation doesn't actually filter on load + #expect(ast.policy.count > 0) } - - // Should only have bob's policy (alice was filtered out) - // Note: all policies are loaded despite filter - filtering just sets isFiltered flag - // The implementation doesn't actually filter on load - #expect(ast.policy.count > 0) } } diff --git a/Tests/CasbinTests/RbacApiTests.swift b/Tests/CasbinTests/RbacApiTests.swift index 06f1936..c3b9544 100644 --- a/Tests/CasbinTests/RbacApiTests.swift +++ b/Tests/CasbinTests/RbacApiTests.swift @@ -9,8 +9,16 @@ struct RbacApiTests { pool.start() let elg = MultiThreadedEventLoopGroup(numberOfThreads: 1) defer { - try? elg.syncShutdownGracefully() - try? pool.syncShutdownGracefully() + do { + try elg.syncShutdownGracefully() + } catch { + // Ignore shutdown errors in tests + } + do { + try pool.syncShutdownGracefully() + } catch { + // Ignore shutdown errors in tests + } } let fileIo = NonBlockingFileIO(threadPool: pool) let m = try DefaultModel.from(file: TestsfilePath + mfile, fileIo: fileIo, on: elg.next()).wait() diff --git a/Tests/CasbinTests/TestSupport.swift b/Tests/CasbinTests/TestSupport.swift index 044b1e0..ae24ef3 100644 --- a/Tests/CasbinTests/TestSupport.swift +++ b/Tests/CasbinTests/TestSupport.swift @@ -1,3 +1,4 @@ +import Dispatch import NIO // Shared helpers for test file paths and small utilities @@ -6,3 +7,49 @@ public let TestsfilePath = #file.components(separatedBy: "TestSupport.swift")[0] @inline(__always) func tryBool(_ body: () throws -> Bool) -> Bool { (try? body()) ?? false } +// MARK: - Awaitable async shutdown helpers +// Use these in async test contexts where you can await completion. + +func shutdownEventLoopGroupAsync(_ group: EventLoopGroup) async throws { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + group.shutdownGracefully(queue: .global()) { error in + if let error = error { + continuation.resume(throwing: error) + } else { + continuation.resume() + } + } + } +} + +func shutdownThreadPoolAsync(_ pool: NIOThreadPool) async throws { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + pool.shutdownGracefully(queue: .global()) { error in + if let error = error { + continuation.resume(throwing: error) + } else { + continuation.resume() + } + } + } +} + +// MARK: - Fire-and-forget background shutdown helpers +// Use these in synchronous test contexts where blocking would cause hangs. +// These initiate shutdown but return immediately without waiting for completion. + +func shutdownEventLoopGroupInBackground(_ group: EventLoopGroup) { + group.shutdownGracefully(queue: .global()) { error in + if let error = error { + assertionFailure("EventLoopGroup shutdown failed in background: \(error)") + } + } +} + +func shutdownThreadPoolInBackground(_ pool: NIOThreadPool) { + pool.shutdownGracefully(queue: .global()) { error in + if let error = error { + assertionFailure("NIOThreadPool shutdown failed in background: \(error)") + } + } +}