diff --git a/Sources/WhoopDIKit/Container/Container.swift b/Sources/WhoopDIKit/Container/Container.swift index 3ad4955..10dfbcc 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,29 @@ public final class Container { public func inject(_ name: String? = nil, params: Any? = nil, _ localDefinition: (DependencyModule) -> Void) -> T { - guard localServiceDict == nil else { - fatalError("Nesting WhoopDI.inject with local definitions is not currently supported") - } - // 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 - // 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.acquireDependencyGraph { localServiceDict in + // Nested local injects are not currently supported. Fail fast here. + guard !isLocalInjectActive else { + fatalError("Nesting WhoopDI.inject with local definitions is not currently supported") + } + + isLocalInjectActive = true + defer { + isLocalInjectActive = false + localDependencyGraph.resetDependencyGraph() + } + + let localModule = DependencyModule() + localDefinition(localModule) + localModule.addToServiceDictionary(serviceDict: localServiceDict) + + do { + return try get(name, params) + } catch { + print("Inject failed with stack trace:") + Thread.callStackSymbols.forEach { print($0) } + fatalError("WhoopDI inject failed with error: \(error)") + } } } @@ -89,7 +96,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 +113,9 @@ public final class Container { } private func getDefinition(_ serviceKey: ServiceKey) -> DependencyDefinition? { - return localServiceDict?[serviceKey] ?? serviceDict[serviceKey] + localDependencyGraph.acquireDependencyGraph { 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..829cbcb --- /dev/null +++ b/Sources/WhoopDIKit/Container/ThreadSafeDependencyGraph.swift @@ -0,0 +1,35 @@ +import Foundation + +final class ThreadSafeDependencyGraph: @unchecked Sendable { + private let lock = NSRecursiveLock() + private let serviceDict: ServiceDictionary = .init() + private let options: WhoopDIOptionProvider + + init(options: WhoopDIOptionProvider) { + self.options = options + } + + func acquireDependencyGraph(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..0a8e2d9 100644 --- a/Tests/WhoopDIKitTests/Container/ContainerTests.swift +++ b/Tests/WhoopDIKitTests/Container/ContainerTests.swift @@ -1,60 +1,95 @@ -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 +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 { + container.registerModules(modules: [GoodTestModule()]) + // Run many times to try and capture race condition + for _ in 0..<500 { + let taskA = Task.detached { + let _: Dependency = self.container.inject("C_Factory") { module in + module.factory(name: "C_Factory") { DependencyA() as Dependency } + } + } + + let taskB = Task.detached { + let _: DependencyA = self.container.inject() + } + + for task in [taskA, taskB] { + let _ = await task.result + } + } } - 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..aab8356 --- /dev/null +++ b/Tests/WhoopDIKitTests/Container/ThreadSafeDependencyGraphTests.swift @@ -0,0 +1,39 @@ +import Testing +@testable import WhoopDIKit + +struct ThreadSafeDependencyGraphTests { + @Test(arguments: [false, true]) + func acquireDependencyGraph_notThreadSafe(threadsafe: Bool) { + let options = MockOptionProvider(options: [.threadSafeLocalInject: threadsafe]) + let graph = ThreadSafeDependencyGraph(options: options) + + graph.acquireDependencyGraph { serviceDict in + serviceDict[DependencyA.self] = FactoryDefinition(name: nil) { _ in DependencyA() } + } + graph.acquireDependencyGraph { serviceDict in + let dependency = serviceDict[DependencyA.self] + #expect(dependency != nil) + } + + graph.resetDependencyGraph() + + graph.acquireDependencyGraph { serviceDict in + let dependency = serviceDict[DependencyA.self] + #expect(dependency == nil) + } + } + + @Test + func acquireDependencyGraph_recursive() { + let options = MockOptionProvider(options: [.threadSafeLocalInject: true]) + let graph = ThreadSafeDependencyGraph(options: options) + + graph.acquireDependencyGraph { outer in + graph.acquireDependencyGraph { 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/Module/DependencyModuleTests.swift b/Tests/WhoopDIKitTests/Module/DependencyModuleTests.swift index d0f0f1c..4b64679 100644 --- a/Tests/WhoopDIKitTests/Module/DependencyModuleTests.swift +++ b/Tests/WhoopDIKitTests/Module/DependencyModuleTests.swift @@ -1,77 +1,96 @@ import Foundation -import XCTest +import Testing @testable import WhoopDIKit -class DependencyModuleTests: XCTestCase { +@MainActor +class DependencyModuleTests { private let serviceKey = ServiceKey(String.self, name: "name") private let serviceDict = ServiceDictionary() private let module = DependencyModule() - func test_factory() { + @Test + func defineDependencies_defaultDoesNothing() { + module.defineDependencies() + module.addToServiceDictionary(serviceDict: serviceDict) + #expect(serviceDict.allKeys().isEmpty) + } + + @Test + func factory() { module.factory(name: "name") { "dependency" } module.addToServiceDictionary(serviceDict: serviceDict) let defintion = serviceDict[serviceKey] - XCTAssertTrue(defintion is FactoryDefinition) + #expect(defintion is FactoryDefinition) } - func test_get_missingContainer_fallsBackOnAppContainer() throws { + @Test + func get_missingContainer_fallsBackOnAppContainer() throws { WhoopDI.registerModules(modules: [GoodTestModule()]) + let dependencyC: DependencyC = try module.get(params: "params") - XCTAssertNotNil(dependencyC) + #expect(dependencyC != nil) + WhoopDI.removeAllDependencies() } - func test_factoryWithParams() { + @Test + func factoryWithParams() { module.factoryWithParams(name: "name") { (_: Any) in "dependency" } module.addToServiceDictionary(serviceDict: serviceDict) let defintion = serviceDict[serviceKey] - XCTAssertTrue(defintion is FactoryDefinition) + #expect(defintion is FactoryDefinition) } - func test_singleton() { + @Test + func singleton() { module.singleton(name: "name") { "dependency" } module.addToServiceDictionary(serviceDict: serviceDict) let defintion = serviceDict[serviceKey] - XCTAssertTrue(defintion is SingletonDefinition) + #expect(defintion is SingletonDefinition) } - func test_singletonWithParams() { + @Test + func singletonWithParams() { module.singletonWithParams(name: "name") { (_: Any) in "dependency" } module.addToServiceDictionary(serviceDict: serviceDict) let defintion = serviceDict[serviceKey] - XCTAssertTrue(defintion is SingletonDefinition) + #expect(defintion is SingletonDefinition) } - func test_serviceKey_Returns_Subclass_Type() { + @Test + func serviceKey_Returns_Subclass_Type() { let testModule = TestDependencyModule(testModuleDependencies: []) - XCTAssertEqual(testModule.serviceKey, ServiceKey(type(of: TestDependencyModule()))) + #expect(testModule.serviceKey == ServiceKey(type(of: TestDependencyModule()))) } - func test_setMultipleModuleDependencies() { + @Test + func setMultipleModuleDependencies() { let moduleA = DependencyModule() let moduleB = DependencyModule() let moduleC = DependencyModule() let moduleD = DependencyModule() let module = TestDependencyModule(testModuleDependencies: [moduleD, moduleC, moduleB, moduleA]) - XCTAssertEqual(module.moduleDependencies, [moduleD, moduleC, moduleB, moduleA]) + #expect(module.moduleDependencies == [moduleD, moduleC, moduleB, moduleA]) } - func test_setSingleModuleDependency() { + @Test + func setSingleModuleDependency() { let moduleA = DependencyModule() let module = TestDependencyModule(testModuleDependencies: [moduleA]) - XCTAssertEqual(module.moduleDependencies, [moduleA]) + #expect(module.moduleDependencies == [moduleA]) } - func test_setNoModuleDependencies() { + @Test + func setNoModuleDependencies() { let module = TestDependencyModule() - XCTAssertEqual(module.moduleDependencies, []) + #expect(module.moduleDependencies.isEmpty) } } 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..0180211 100644 --- a/Tests/WhoopDIKitTests/WhoopDITests.swift +++ b/Tests/WhoopDIKitTests/WhoopDITests.swift @@ -1,102 +1,152 @@ -import XCTest +import Testing @testable import WhoopDIKit -class WhoopDITests: XCTestCase { - - override func tearDown() { - WhoopDI.removeAllDependencies() - } - - func test_inject() { +@MainActor +class WhoopDITests { + @Test + func inject() { WhoopDI.registerModules(modules: [GoodTestModule()]) let dependency: Dependency = WhoopDI.inject("C_Factory", "param") - XCTAssertTrue(dependency is DependencyC) + #expect(dependency is DependencyC) + WhoopDI.removeAllDependencies() } - 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) + WhoopDI.removeAllDependencies() } - 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) + WhoopDI.removeAllDependencies() } - 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) + WhoopDI.removeAllDependencies() + } + + @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) + WhoopDI.removeAllDependencies() } - func test_inject_localDefinition_noOverride() { + @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) + WhoopDI.removeAllDependencies() } - 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) + WhoopDI.removeAllDependencies() } - func test_validation_fails_barParams() { + @Test + func injectable() { + WhoopDI.registerModules(modules: [FakeTestModuleForInjecting()]) + let testInjecting: InjectableWithNamedDependency = WhoopDI.inject() + #expect(testInjecting == InjectableWithNamedDependency(name: 1)) + WhoopDI.removeAllDependencies() + } + + @Test + func setup() { + // Verify nothing explocdes + WhoopDI.setup(options: DefaultOptionProvider()) + WhoopDI.removeAllDependencies() + } + + @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) + WhoopDI.removeAllDependencies() } - 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) + WhoopDI.removeAllDependencies() } - 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) + WhoopDI.removeAllDependencies() } - 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) + WhoopDI.removeAllDependencies() } - 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 +156,8 @@ 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)) + WhoopDI.removeAllDependencies() } }