From 3562cd039dec0a2ccd94488c737a4c4c54b8c451 Mon Sep 17 00:00:00 2001 From: Nick Cipollo Date: Wed, 16 Oct 2024 09:19:28 -0400 Subject: [PATCH] Fixes #13 Fix threading issue in local inject --- Sources/WhoopDIKit/Container/Container.swift | 50 +++++---- .../Container/ThreadSafeDependencyGraph.swift | 35 ++++++ .../{Module => }/DependencyError.swift | 4 +- .../Options/DefaultOptionProvider.swift | 9 ++ .../WhoopDIKit/Options/WhoopDIOption.swift | 4 + .../Options/WhoopDIOptionProvider.swift | 4 + Sources/WhoopDIKit/WhoopDI.swift | 23 ++-- .../Container/ContainerTests.swift | 72 ++++++++---- .../ThreadSafeDependencyGraphTests.swift | 41 +++++++ .../{Module => }/DependencyErrorTests.swift | 8 +- .../Module/DependencyDefinitionTests.swift | 2 +- .../Options/DefaultOptionProviderTests.swift | 11 ++ .../Options/MockOptionProvider.swift | 14 +++ Tests/WhoopDIKitTests/WhoopDITests.swift | 106 ++++++++++++------ 14 files changed, 294 insertions(+), 89 deletions(-) create mode 100644 Sources/WhoopDIKit/Container/ThreadSafeDependencyGraph.swift rename Sources/WhoopDIKit/{Module => }/DependencyError.swift (87%) create mode 100644 Sources/WhoopDIKit/Options/DefaultOptionProvider.swift create mode 100644 Sources/WhoopDIKit/Options/WhoopDIOption.swift create mode 100644 Sources/WhoopDIKit/Options/WhoopDIOptionProvider.swift create mode 100644 Tests/WhoopDIKitTests/Container/ThreadSafeDependencyGraphTests.swift rename Tests/WhoopDIKitTests/{Module => }/DependencyErrorTests.swift (84%) create mode 100644 Tests/WhoopDIKitTests/Options/DefaultOptionProviderTests.swift create mode 100644 Tests/WhoopDIKitTests/Options/MockOptionProvider.swift diff --git a/Sources/WhoopDIKit/Container/Container.swift b/Sources/WhoopDIKit/Container/Container.swift index 3ad4955..96ebea9 100644 --- a/Sources/WhoopDIKit/Container/Container.swift +++ b/Sources/WhoopDIKit/Container/Container.swift @@ -1,9 +1,15 @@ import Foundation public final class Container { + private let localDependencyGraph: ThreadSafeDependencyGraph + private var isLocalInjectActive: Bool = false + private let options: WhoopDIOptionProvider + private let serviceDict = ServiceDictionary() - private var localServiceDict: ServiceDictionary? = nil - public init() {} + public init(options: WhoopDIOptionProvider = defaultWhoopDIOptions()) { + self.options = options + localDependencyGraph = ThreadSafeDependencyGraph(options: options) + } /// Registers a list of modules with the DI system. /// Typically you will create a `DependencyModule` for your feature, then add it to the module list provided to this method. @@ -54,28 +60,30 @@ public final class Container { public func inject(_ name: String? = nil, params: Any? = nil, _ localDefinition: (DependencyModule) -> Void) -> T { - guard localServiceDict == nil else { + guard !isLocalInjectActive else { fatalError("Nesting WhoopDI.inject with local definitions is not currently supported") } + + isLocalInjectActive = true + defer { + isLocalInjectActive = false + localDependencyGraph.resetDependencyGraph() + } // We need to maintain a reference to the local service dictionary because transient dependencies may also // need to reference dependencies from it. // ---- - // This is a little dangerous since we are mutating a static variable but it should be fine as long as you + // This is a little dangerous since we are mutating a variable but it should be fine as long as you // don't use `inject { }` within the scope of another `inject { }`. - let serviceDict = ServiceDictionary() - localServiceDict = serviceDict - defer { - localServiceDict = nil - } - - let localModule = DependencyModule() - localDefinition(localModule) - localModule.addToServiceDictionary(serviceDict: serviceDict) - - do { - return try get(name, params) - } catch { - fatalError("WhoopDI inject failed with error: \(error)") + return localDependencyGraph.aquireDependencyGraph { localServiceDict in + let localModule = DependencyModule() + localDefinition(localModule) + localModule.addToServiceDictionary(serviceDict: localServiceDict) + + do { + return try get(name, params) + } catch { + fatalError("WhoopDI inject failed with error: \(error)") + } } } @@ -89,7 +97,7 @@ public final class Container { } else if let injectable = T.self as? any Injectable.Type { return try injectable.inject(container: self) as! T } else { - throw DependencyError.missingDependecy(ServiceKey(T.self, name: name)) + throw DependencyError.missingDependency(ServiceKey(T.self, name: name)) } } @@ -106,7 +114,9 @@ public final class Container { } private func getDefinition(_ serviceKey: ServiceKey) -> DependencyDefinition? { - return localServiceDict?[serviceKey] ?? serviceDict[serviceKey] + localDependencyGraph.aquireDependencyGraph { localServiceDict in + return localServiceDict[serviceKey] ?? serviceDict[serviceKey] + } } public func removeAllDependencies() { diff --git a/Sources/WhoopDIKit/Container/ThreadSafeDependencyGraph.swift b/Sources/WhoopDIKit/Container/ThreadSafeDependencyGraph.swift new file mode 100644 index 0000000..5449c44 --- /dev/null +++ b/Sources/WhoopDIKit/Container/ThreadSafeDependencyGraph.swift @@ -0,0 +1,35 @@ +import Foundation + +final class ThreadSafeDependencyGraph: Sendable { + private let lock = NSRecursiveLock() + nonisolated(unsafe) private let serviceDict: ServiceDictionary = .init() + private let options: WhoopDIOptionProvider + + init(options: WhoopDIOptionProvider) { + self.options = options + } + + func aquireDependencyGraph(block: (ServiceDictionary) -> T) -> T { + let threadSafe = options.isOptionEnabled(.threadSafeLocalInject) + if threadSafe { + lock.lock() + } + let result = block(serviceDict) + if threadSafe { + lock.unlock() + } + return result + } + + func resetDependencyGraph() { + let threadSafe = options.isOptionEnabled(.threadSafeLocalInject) + if threadSafe { + lock.lock() + } + serviceDict.removeAll() + if threadSafe { + lock.unlock() + } + } + +} diff --git a/Sources/WhoopDIKit/Module/DependencyError.swift b/Sources/WhoopDIKit/DependencyError.swift similarity index 87% rename from Sources/WhoopDIKit/Module/DependencyError.swift rename to Sources/WhoopDIKit/DependencyError.swift index 4c0aa85..0208729 100644 --- a/Sources/WhoopDIKit/Module/DependencyError.swift +++ b/Sources/WhoopDIKit/DependencyError.swift @@ -1,13 +1,13 @@ enum DependencyError: Error, CustomStringConvertible, Equatable { case badParams(ServiceKey) - case missingDependecy(ServiceKey) + case missingDependency(ServiceKey) case nilDependency(ServiceKey) var description: String { switch self { case .badParams(let serviceKey): return "Bad parameters provided for \(serviceKey.type) with name: \(serviceKey.name ?? "")" - case .missingDependecy(let serviceKey): + case .missingDependency(let serviceKey): return "Missing dependency for \(serviceKey.type) with name: \(serviceKey.name ?? "")" case .nilDependency(let serviceKey): return "Nil dependency for \(serviceKey.type) with name: \(serviceKey.name ?? "")" diff --git a/Sources/WhoopDIKit/Options/DefaultOptionProvider.swift b/Sources/WhoopDIKit/Options/DefaultOptionProvider.swift new file mode 100644 index 0000000..f9830ba --- /dev/null +++ b/Sources/WhoopDIKit/Options/DefaultOptionProvider.swift @@ -0,0 +1,9 @@ +struct DefaultOptionProvider: WhoopDIOptionProvider { + func isOptionEnabled(_ option: WhoopDIOption) -> Bool { + false + } +} + +public func defaultWhoopDIOptions() -> WhoopDIOptionProvider { + DefaultOptionProvider() +} diff --git a/Sources/WhoopDIKit/Options/WhoopDIOption.swift b/Sources/WhoopDIKit/Options/WhoopDIOption.swift new file mode 100644 index 0000000..d453141 --- /dev/null +++ b/Sources/WhoopDIKit/Options/WhoopDIOption.swift @@ -0,0 +1,4 @@ +/// Options for WhoopDI. These are typically experimental features which may be enabled or disabled. +public enum WhoopDIOption: Sendable { + case threadSafeLocalInject +} diff --git a/Sources/WhoopDIKit/Options/WhoopDIOptionProvider.swift b/Sources/WhoopDIKit/Options/WhoopDIOptionProvider.swift new file mode 100644 index 0000000..d1f6785 --- /dev/null +++ b/Sources/WhoopDIKit/Options/WhoopDIOptionProvider.swift @@ -0,0 +1,4 @@ +/// Implement this protocol and pass it into WhoopDI via `WhoopDI.setOptions` to enable and disable various options for WhoopDI. +public protocol WhoopDIOptionProvider: Sendable { + func isOptionEnabled(_ option: WhoopDIOption) -> Bool +} diff --git a/Sources/WhoopDIKit/WhoopDI.swift b/Sources/WhoopDIKit/WhoopDI.swift index c6c6855..e859e9f 100644 --- a/Sources/WhoopDIKit/WhoopDI.swift +++ b/Sources/WhoopDIKit/WhoopDI.swift @@ -1,16 +1,23 @@ import Foundation public final class WhoopDI: DependencyRegister { - nonisolated(unsafe) private static let appContainer = Container() - + nonisolated(unsafe) private static var appContainer = Container() + + /// Setup WhoopDI with the supplied options. + /// This should only be called once when your application launches (and before WhoopDI is used). + /// By default all options are disabled if you do not call this method. + public static func setup(options: WhoopDIOptionProvider) { + appContainer = Container(options: options) + } + /// Registers a list of modules with the DI system. /// Typically you will create a `DependencyModule` for your feature, then add it to the module list provided to this method. public static func registerModules(modules: [DependencyModule]) { appContainer.registerModules(modules: modules) } - /// Injects a dependecy into your code. + /// Injects a dependency into your code. /// - /// The injected dependecy will have all of it's sub-dependencies provided by the object graph defined in WhoopDI. + /// The injected dependency will have all of it's sub-dependencies provided by the object graph defined in WhoopDI. /// Typically this should be called from your top level UI object (ViewController, etc). Intermediate components should rely upon constructor injection (i.e providing dependencies via the constructor) public static func inject(_ name: String? = nil, _ params: Any? = nil) -> T { appContainer.inject(name, params) @@ -18,7 +25,7 @@ public final class WhoopDI: DependencyRegister { /// Injects a dependency into your code, overlaying local dependencies on top of the object graph. /// - /// The injected dependecy will have all of it's sub-dependencies provided by the object graph defined in WhoopDI. + /// The injected dependency will have all of it's sub-dependencies provided by the object graph defined in WhoopDI. /// Typically this should be called from your top level UI object (ViewController, etc). Intermediate components should rely /// upon constructor injection (i.e providing dependencies via the constructor). /// @@ -36,12 +43,12 @@ public final class WhoopDI: DependencyRegister { /// - name: An optional name for the dependency. This can help disambiguate between dependencies of the same type. /// - params: Optional parameters which will be provided to dependencies which require them (i.e dependencies using defintiions such as /// (factoryWithParams, etc). - /// - localDefiniton: A local module definition which can be used to supply local dependencies to the object graph prior to injection. + /// - localDefinition: A local module definition which can be used to supply local dependencies to the object graph prior to injection. /// - Returns: The requested dependency. public static func inject(_ name: String? = nil, params: Any? = nil, - _ localDefiniton: (DependencyModule) -> Void) -> T { - appContainer.inject(name, params: params, localDefiniton) + _ localDefinition: (DependencyModule) -> Void) -> T { + appContainer.inject(name, params: params, localDefinition) } /// Used internally by the DependencyModule get to loop up a sub-dependency in the object graph. diff --git a/Tests/WhoopDIKitTests/Container/ContainerTests.swift b/Tests/WhoopDIKitTests/Container/ContainerTests.swift index 495d317..7f0258f 100644 --- a/Tests/WhoopDIKitTests/Container/ContainerTests.swift +++ b/Tests/WhoopDIKitTests/Container/ContainerTests.swift @@ -1,60 +1,94 @@ -import XCTest +import Testing @testable import WhoopDIKit -class ContainerTests: XCTestCase { - private let container = Container() +// This is unchecked Sendable so we can run our local inject concurrency test +@Suite(.serialized) +class ContainerTests: @unchecked Sendable { + private let container: Container + + init() { + let options = MockOptionProvider(options: [.threadSafeLocalInject: true]) + container = Container(options: options) + } - func test_inject() { + @Test + func inject() { container.registerModules(modules: [GoodTestModule()]) let dependency: Dependency = container.inject("C_Factory", "param") - XCTAssertTrue(dependency is DependencyC) + #expect(dependency is DependencyC) } - func test_inject_generic_integer() { + @Test + func inject_generic_integer() { container.registerModules(modules: [GoodTestModule()]) let dependency: GenericDependency = container.inject() - XCTAssertEqual(42, dependency.value) + #expect(42 == dependency.value) } - func test_inject_generic_string() { + @Test + func inject_generic_string() { container.registerModules(modules: [GoodTestModule()]) let dependency: GenericDependency = container.inject() - XCTAssertEqual("string", dependency.value) + #expect("string" == dependency.value) } - func test_inject_localDefinition() { + @Test + func inject_localDefinition() { container.registerModules(modules: [GoodTestModule()]) let dependency: Dependency = container.inject("C_Factory") { module in // Typically you'd override or provide a transient dependency. I'm using the top level dependency here // for the sake of simplicity. module.factory(name: "C_Factory") { DependencyA() as Dependency } } - XCTAssertTrue(dependency is DependencyA) + #expect(dependency is DependencyA) + } + + @Test(.bug("https://github.com/WhoopInc/WhoopDI/issues/13")) + func inject_localDefinition_concurrency() async { + // You can run this test repeatedly to verify we don't have a concurrency issue when + // performing a local inject. 1000 times should do the trick. + container.registerModules(modules: [GoodTestModule()]) + + async let resultA = Task { + let _: Dependency = container.inject("C_Factory") { module in + module.factory(name: "C_Factory") { DependencyA() as Dependency } + } + }.result + + async let resultB = Task { + let _: DependencyA = container.inject() + }.result + + let _ = await [resultA, resultB] } - func test_inject_localDefinition_noOverride() { + @Test + func inject_localDefinition_noOverride() { container.registerModules(modules: [GoodTestModule()]) let dependency: Dependency = container.inject("C_Factory", params: "params") { _ in } - XCTAssertTrue(dependency is DependencyC) + #expect(dependency is DependencyC) } - func test_inject_localDefinition_withParams() { + @Test + func inject_localDefinition_withParams() { container.registerModules(modules: [GoodTestModule()]) let dependency: Dependency = container.inject("C_Factory", params: "params") { module in module.factoryWithParams(name: "C_Factory") { params in DependencyB(params) as Dependency } } - XCTAssertTrue(dependency is DependencyB) + #expect(dependency is DependencyB) } - func test_injectableWithDependency() throws { + @Test + func injectableWithDependency() throws { container.registerModules(modules: [FakeTestModuleForInjecting()]) let testInjecting: InjectableWithDependency = container.inject() - XCTAssertEqual(testInjecting, InjectableWithDependency(dependency: DependencyA())) + #expect(testInjecting == InjectableWithDependency(dependency: DependencyA())) } - func test_injectableWithNamedDependency() throws { + @Test + func injectableWithNamedDependency() throws { container.registerModules(modules: [FakeTestModuleForInjecting()]) let testInjecting: InjectableWithNamedDependency = container.inject() - XCTAssertEqual(testInjecting, InjectableWithNamedDependency(name: 1)) + #expect(testInjecting == InjectableWithNamedDependency(name: 1)) } } diff --git a/Tests/WhoopDIKitTests/Container/ThreadSafeDependencyGraphTests.swift b/Tests/WhoopDIKitTests/Container/ThreadSafeDependencyGraphTests.swift new file mode 100644 index 0000000..ac772b8 --- /dev/null +++ b/Tests/WhoopDIKitTests/Container/ThreadSafeDependencyGraphTests.swift @@ -0,0 +1,41 @@ +import Testing +@testable import WhoopDIKit + +struct ThreadSafeDependencyGraphTests { + private let key = "key" + + @Test(arguments: [false, true]) + func aquireDependencyGraph_notThreadSafe(threadsafe: Bool) { + let options = MockOptionProvider(options: [.threadSafeLocalInject: threadsafe]) + let graph = ThreadSafeDependencyGraph(options: options) + + graph.aquireDependencyGraph { serviceDict in + serviceDict[DependencyA.self] = FactoryDefinition(name: nil) { _ in DependencyA() } + } + graph.aquireDependencyGraph { serviceDict in + let dependency = serviceDict[DependencyA.self] + #expect(dependency != nil) + } + + graph.resetDependencyGraph() + + graph.aquireDependencyGraph { serviceDict in + let dependency = serviceDict[DependencyA.self] + #expect(dependency == nil) + } + } + + @Test + func aquireDependencyGraph_recursive() { + let options = MockOptionProvider(options: [.threadSafeLocalInject: true]) + let graph = ThreadSafeDependencyGraph(options: options) + + graph.aquireDependencyGraph { outer in + graph.aquireDependencyGraph { serviceDict in + serviceDict[DependencyA.self] = FactoryDefinition(name: nil) { _ in DependencyA() } + } + let dependency = outer[DependencyA.self] + #expect(dependency != nil) + } + } +} diff --git a/Tests/WhoopDIKitTests/Module/DependencyErrorTests.swift b/Tests/WhoopDIKitTests/DependencyErrorTests.swift similarity index 84% rename from Tests/WhoopDIKitTests/Module/DependencyErrorTests.swift rename to Tests/WhoopDIKitTests/DependencyErrorTests.swift index 3fe2a53..bedfd89 100644 --- a/Tests/WhoopDIKitTests/Module/DependencyErrorTests.swift +++ b/Tests/WhoopDIKitTests/DependencyErrorTests.swift @@ -17,15 +17,15 @@ class DependencyErrorTests: XCTestCase { XCTAssertEqual(expected, error.description) } - func test_description_missingDependecy_noServiceKeyName() { + func test_description_missingDependency_noServiceKeyName() { let expected = "Missing dependency for String with name: " - let error = DependencyError.missingDependecy(serviceKey) + let error = DependencyError.missingDependency(serviceKey) XCTAssertEqual(expected, error.description) } - func test_description_missingDependecy_withServiceKeyName() { + func test_description_missingDependency_withServiceKeyName() { let expected = "Missing dependency for String with name: name" - let error = DependencyError.missingDependecy(serviceKeyWithName) + let error = DependencyError.missingDependency(serviceKeyWithName) XCTAssertEqual(expected, error.description) } diff --git a/Tests/WhoopDIKitTests/Module/DependencyDefinitionTests.swift b/Tests/WhoopDIKitTests/Module/DependencyDefinitionTests.swift index 5e5b91c..95f8f18 100644 --- a/Tests/WhoopDIKitTests/Module/DependencyDefinitionTests.swift +++ b/Tests/WhoopDIKitTests/Module/DependencyDefinitionTests.swift @@ -43,7 +43,7 @@ class DependencyDefinitionTests: XCTestCase { } func test_singleton_get_recoversFromThrow() { - let expectedError = DependencyError.missingDependecy(ServiceKey(String.self)) + let expectedError = DependencyError.missingDependency(ServiceKey(String.self)) var callCount = 0 let definition = SingletonDefinition(name: nil) { _ -> Int in callCount += 1 diff --git a/Tests/WhoopDIKitTests/Options/DefaultOptionProviderTests.swift b/Tests/WhoopDIKitTests/Options/DefaultOptionProviderTests.swift new file mode 100644 index 0000000..f3bdd79 --- /dev/null +++ b/Tests/WhoopDIKitTests/Options/DefaultOptionProviderTests.swift @@ -0,0 +1,11 @@ +import Foundation +import Testing +@testable import WhoopDIKit + +struct DefaultOptionProviderTests { + @Test func defaults() async throws { + let options = DefaultOptionProvider() + #expect(options.isOptionEnabled(.threadSafeLocalInject) == false) + } +} + diff --git a/Tests/WhoopDIKitTests/Options/MockOptionProvider.swift b/Tests/WhoopDIKitTests/Options/MockOptionProvider.swift new file mode 100644 index 0000000..0592b75 --- /dev/null +++ b/Tests/WhoopDIKitTests/Options/MockOptionProvider.swift @@ -0,0 +1,14 @@ +import Foundation +@testable import WhoopDIKit + +struct MockOptionProvider: WhoopDIOptionProvider { + private let options: [WhoopDIOption: Bool] + + init(options: [WhoopDIOption : Bool] = [:]) { + self.options = options + } + + func isOptionEnabled(_ option: WhoopDIOption) -> Bool { + options[option] ?? false + } +} diff --git a/Tests/WhoopDIKitTests/WhoopDITests.swift b/Tests/WhoopDIKitTests/WhoopDITests.swift index c1c0622..b9c835d 100644 --- a/Tests/WhoopDIKitTests/WhoopDITests.swift +++ b/Tests/WhoopDIKitTests/WhoopDITests.swift @@ -1,102 +1,144 @@ -import XCTest +import Testing @testable import WhoopDIKit -class WhoopDITests: XCTestCase { +@Suite(.serialized) +class WhoopDITests { - override func tearDown() { + deinit { WhoopDI.removeAllDependencies() } - func test_inject() { + @Test + func inject() { WhoopDI.registerModules(modules: [GoodTestModule()]) let dependency: Dependency = WhoopDI.inject("C_Factory", "param") - XCTAssertTrue(dependency is DependencyC) + #expect(dependency is DependencyC) } - func test_inject_generic_integer() { + @Test + func inject_generic_integer() { WhoopDI.registerModules(modules: [GoodTestModule()]) let dependency: GenericDependency = WhoopDI.inject() - XCTAssertEqual(42, dependency.value) + #expect(42 == dependency.value) } - func test_inject_generic_string() { + @Test + func inject_generic_string() { WhoopDI.registerModules(modules: [GoodTestModule()]) let dependency: GenericDependency = WhoopDI.inject() - XCTAssertEqual("string", dependency.value) + #expect("string" == dependency.value) } - func test_inject_localDefinition() { + @Test + func inject_localDefinition() { WhoopDI.registerModules(modules: [GoodTestModule()]) let dependency: Dependency = WhoopDI.inject("C_Factory") { module in // Typically you'd override or provide a transient dependency. I'm using the top level dependency here // for the sake of simplicity. module.factory(name: "C_Factory") { DependencyA() as Dependency } } - XCTAssertTrue(dependency is DependencyA) + #expect(dependency is DependencyA) } - func test_inject_localDefinition_noOverride() { + @Test + func inject_localDefinition_multipleInjections() { + WhoopDI.registerModules(modules: [GoodTestModule()]) + let dependency1: Dependency = WhoopDI.inject("C_Factory") { module in + module.factory(name: "C_Factory") { DependencyA() as Dependency } + } + let dependency2: Dependency = WhoopDI.inject("C_Factory", "params") + let dependency3: Dependency = WhoopDI.inject("C_Factory") { module in + module.factory(name: "C_Factory") { DependencyB("") as Dependency } + } + + #expect(dependency1 is DependencyA) + #expect(dependency2 is DependencyC) + #expect(dependency3 is DependencyB) + } + + @Test + func inject_localDefinition_noOverride() { WhoopDI.registerModules(modules: [GoodTestModule()]) let dependency: Dependency = WhoopDI.inject("C_Factory", params: "params") { _ in } - XCTAssertTrue(dependency is DependencyC) + #expect(dependency is DependencyC) } - func test_inject_localDefinition_withParams() { + @Test + func inject_localDefinition_withParams() { WhoopDI.registerModules(modules: [GoodTestModule()]) let dependency: Dependency = WhoopDI.inject("C_Factory", params: "params") { module in module.factoryWithParams(name: "C_Factory") { params in DependencyB(params) as Dependency } } - XCTAssertTrue(dependency is DependencyB) + #expect(dependency is DependencyB) + } + + @Test + func injectable() { + WhoopDI.registerModules(modules: [FakeTestModuleForInjecting()]) + let testInjecting: InjectableWithNamedDependency = WhoopDI.inject() + #expect(testInjecting == InjectableWithNamedDependency(name: 1)) } - func test_validation_fails_barParams() { + @Test + func setup() { + // Verify nothing explocdes + WhoopDI.setup(options: DefaultOptionProvider()) + } + + @Test + func validation_fails_barParams() { WhoopDI.registerModules(modules: [GoodTestModule()]) let validator = WhoopDIValidator() var failed = false validator.validate { error in failed = true } - XCTAssertTrue(failed) + #expect(failed) } - func test_validation_fails_missingDependencies() { + @Test + func validation_fails_missingDependencies() { WhoopDI.registerModules(modules: [BadTestModule()]) let validator = WhoopDIValidator() var failed = false validator.validate { error in let expectedKey = ServiceKey(Dependency.self, name: "A_Factory") - let expectedError = DependencyError.missingDependecy(expectedKey) - XCTAssertEqual(expectedError, error as! DependencyError) + let expectedError = DependencyError.missingDependency(expectedKey) + #expect(expectedError == error as! DependencyError) failed = true } - XCTAssertTrue(failed) + #expect(failed) } - func test_validation_fails_nilFactoryDependency() { + + @Test + func validation_fails_nilFactoryDependency() { WhoopDI.registerModules(modules: [NilFactoryModule()]) let validator = WhoopDIValidator() var failed = false validator.validate { error in let expectedKey = ServiceKey(Optional.self) let expectedError = DependencyError.nilDependency(expectedKey) - XCTAssertEqual(expectedError, error as! DependencyError) + #expect(expectedError == error as! DependencyError) failed = true } - XCTAssertTrue(failed) + #expect(failed) } - func test_validation_fails_nilSingletonDependency() { + @Test + func validation_fails_nilSingletonDependency() { WhoopDI.registerModules(modules: [NilSingletonModule()]) let validator = WhoopDIValidator() var failed = false validator.validate { error in let expectedKey = ServiceKey(Optional.self) let expectedError = DependencyError.nilDependency(expectedKey) - XCTAssertEqual(expectedError, error as! DependencyError) + #expect(expectedError == error as! DependencyError) failed = true } - XCTAssertTrue(failed) + #expect(failed) } - func test_validation_succeeds() { + @Test + func validation_succeeds() { WhoopDI.registerModules(modules: [GoodTestModule()]) let validator = WhoopDIValidator() validator.addParams("param", forType: Dependency.self, andName: "B_Factory") @@ -106,13 +148,7 @@ class WhoopDITests: XCTestCase { validator.addParams("param", forType: Dependency.self, andName: "C_Factory") validator.validate { error in - XCTFail("DI failed with error: \(error)") + Issue.record("DI failed with error: \(error)") } } - - func test_injecting() { - WhoopDI.registerModules(modules: [FakeTestModuleForInjecting()]) - let testInjecting: InjectableWithNamedDependency = WhoopDI.inject() - XCTAssertEqual(testInjecting, InjectableWithNamedDependency(name: 1)) - } }