From 9e6fca272a035b54823297ab9f85f1828bd386d1 Mon Sep 17 00:00:00 2001 From: Akos Grabecz Date: Mon, 12 May 2025 12:04:08 +0200 Subject: [PATCH 1/7] add XCContainerTestCase and update accordingly --- Factory.xctestplan | 1 + Sources/FactoryTesting/ContainerTrait.swift | 15 +- .../FactoryTesting/FactoryTestingHelper.swift | 36 +++ .../FactoryTesting/XCContainerTestCase.swift | 18 ++ .../FactoryTests/FactoryComponentTests.swift | 3 +- .../FactoryTests/FactoryContainerTests.swift | 3 +- Tests/FactoryTests/FactoryContextTests.swift | 3 +- Tests/FactoryTests/FactoryCoreTests.swift | 3 +- Tests/FactoryTests/FactoryDefectTests.swift | 3 +- .../FactoryTests/FactoryFunctionalTests.swift | 3 +- .../FactoryTests/FactoryInjectionTests.swift | 3 +- .../FactoryTests/FactoryIsolationTests.swift | 4 +- .../FactoryMultithreadingTests.swift | 3 +- .../FactoryTests/FactoryParameterTests.swift | 3 +- Tests/FactoryTests/FactoryResolverTests.swift | 3 +- Tests/FactoryTests/FactoryScopeTests.swift | 3 +- Tests/FactoryTests/ParallelXCTest.swift | 263 +++++++++++++++++- 17 files changed, 348 insertions(+), 22 deletions(-) create mode 100644 Sources/FactoryTesting/FactoryTestingHelper.swift create mode 100644 Sources/FactoryTesting/XCContainerTestCase.swift diff --git a/Factory.xctestplan b/Factory.xctestplan index b7c4783c..cb1d3269 100644 --- a/Factory.xctestplan +++ b/Factory.xctestplan @@ -13,6 +13,7 @@ }, "testTargets" : [ { + "parallelizable" : true, "target" : { "containerPath" : "container:", "identifier" : "FactoryTests", diff --git a/Sources/FactoryTesting/ContainerTrait.swift b/Sources/FactoryTesting/ContainerTrait.swift index 12c7a465..7f073046 100644 --- a/Sources/FactoryTesting/ContainerTrait.swift +++ b/Sources/FactoryTesting/ContainerTrait.swift @@ -36,7 +36,7 @@ import Testing /// /// If you use a custom container, you have to create your own trait and container variable extensions. /// -/// That said, it's also possible to leverage this behavior in `XCTestCase`, by using the `@TaskLocal` provided `withValue` method. +/// That said, it's also possible to leverage this behavior in XCTesting by inheriting `XCContainerTestCase` instead of `XCTestCase`. /// See examples in the ``ParallelXCTests`` file. public struct ContainerTrait: TestTrait, SuiteTrait, TestScoping { @@ -56,12 +56,12 @@ public struct ContainerTrait: TestTrait, SuiteTrait, TestSco } public func provideScope(for test: Test, testCase: Test.Case?, performing function: () async throws -> Void) async throws { - try await Scope.$singleton.withValue(Scope.singleton.clone()) { - try await shared.withValue(container()) { - await transform?(C.shared) - try await function() - } - } + try await FactoryTestingHelper.withContainer( + shared: self.shared, + container: self.container(), + function, + transform: transform + ) } public func callAsFunction(transform: @escaping Transform) -> Self { @@ -69,7 +69,6 @@ public struct ContainerTrait: TestTrait, SuiteTrait, TestSco copy.transform = transform return copy } - } /// Provides test trait for default container diff --git a/Sources/FactoryTesting/FactoryTestingHelper.swift b/Sources/FactoryTesting/FactoryTestingHelper.swift new file mode 100644 index 00000000..bd2bf507 --- /dev/null +++ b/Sources/FactoryTesting/FactoryTestingHelper.swift @@ -0,0 +1,36 @@ +import Factory + +// This should be a global function but Swift doesn't like global generic functions with default values. +public enum FactoryTestingHelper { + /// Asynchronously provides a new container for each operation. + public static func withContainer( + shared: TaskLocal = Container.$shared, + container: @autoclosure @Sendable () -> C = Container(), + _ operation: () async throws -> Void, + transform: ContainerTrait.Transform? + ) async rethrows { + try await Scope.$singleton.withValue(Scope.singleton.clone()) { + try await shared.withValue(container()) { + await transform?(C.shared) + try await operation() + } + } + } + + public typealias SynchronousTransform = @Sendable (C) -> Void + + /// Synchronous version of `withContainer` for use in non-async contexts. + public static func withContainer( + shared: TaskLocal = Container.$shared, + container: @autoclosure @Sendable () -> C = Container(), + _ operation: () throws -> Void, + transform: SynchronousTransform? + ) rethrows { + try Scope.$singleton.withValue(Scope.singleton.clone()) { + try shared.withValue(container()) { + transform?(C.shared) + try operation() + } + } + } +} diff --git a/Sources/FactoryTesting/XCContainerTestCase.swift b/Sources/FactoryTesting/XCContainerTestCase.swift new file mode 100644 index 00000000..d7b9844e --- /dev/null +++ b/Sources/FactoryTesting/XCContainerTestCase.swift @@ -0,0 +1,18 @@ +import Factory +import XCTest + +/// Provides a unique Container to every unit test function in the class. Mimicking the behavior of `ContainerTrait` for `swift-testing`. +open class XCContainerTestCase: XCTestCase { + + /// The optional transformation to apply to the Container before invoking the test. + /// Due to the nature of XCTest, this is not async and should be used for synchronous transformations only. + open var transform: (@Sendable (Container) -> Void)? + + /// Scopes the unit test function to a unique Container instance transformed via the `transform` variable (if overridden to non-nil). + public override func invokeTest() { + FactoryTestingHelper.withContainer( + super.invokeTest, + transform: self.transform + ) + } +} diff --git a/Tests/FactoryTests/FactoryComponentTests.swift b/Tests/FactoryTests/FactoryComponentTests.swift index 63e9474b..dfdadcfb 100644 --- a/Tests/FactoryTests/FactoryComponentTests.swift +++ b/Tests/FactoryTests/FactoryComponentTests.swift @@ -1,4 +1,5 @@ import XCTest +import FactoryTesting @testable import Factory let key1String = StaticString(stringLiteral: "s1") @@ -7,7 +8,7 @@ let key2String = StaticString(stringLiteral: "s2") let key3Unicode = MyStaticScalar("\u{1F600}").value let key4Unicode = MyStaticScalar("\u{1F601}").value -final class FactoryComponentTests: XCTestCase { +final class FactoryComponentTests: XCContainerTestCase { let key1 = FactoryKey(type: UUID.self, key: key1String) let key1D = FactoryKey(type: UUID.self, key: key1StringDup) diff --git a/Tests/FactoryTests/FactoryContainerTests.swift b/Tests/FactoryTests/FactoryContainerTests.swift index 5ed9b883..d1d45c80 100644 --- a/Tests/FactoryTests/FactoryContainerTests.swift +++ b/Tests/FactoryTests/FactoryContainerTests.swift @@ -1,11 +1,12 @@ import XCTest +import FactoryTesting @testable import Factory #if canImport(SwiftUI) import SwiftUI #endif -final class FactoryContainerTests: XCTestCase { +final class FactoryContainerTests: XCContainerTestCase { override func setUp() { super.setUp() diff --git a/Tests/FactoryTests/FactoryContextTests.swift b/Tests/FactoryTests/FactoryContextTests.swift index f2447ef6..8ed8f8e6 100644 --- a/Tests/FactoryTests/FactoryContextTests.swift +++ b/Tests/FactoryTests/FactoryContextTests.swift @@ -1,7 +1,8 @@ import XCTest +import FactoryTesting @testable import Factory -final class FactoryContextTests: XCTestCase { +final class FactoryContextTests: XCContainerTestCase { override func setUp() { super.setUp() diff --git a/Tests/FactoryTests/FactoryCoreTests.swift b/Tests/FactoryTests/FactoryCoreTests.swift index 3adb7c35..5fbeca88 100644 --- a/Tests/FactoryTests/FactoryCoreTests.swift +++ b/Tests/FactoryTests/FactoryCoreTests.swift @@ -1,7 +1,8 @@ import XCTest +import FactoryTesting @testable import Factory -final class FactoryCoreTests: XCTestCase { +final class FactoryCoreTests: XCContainerTestCase { override func setUp() { Container.shared.reset() diff --git a/Tests/FactoryTests/FactoryDefectTests.swift b/Tests/FactoryTests/FactoryDefectTests.swift index 50bf54ea..a12efc70 100644 --- a/Tests/FactoryTests/FactoryDefectTests.swift +++ b/Tests/FactoryTests/FactoryDefectTests.swift @@ -1,7 +1,8 @@ import XCTest +import FactoryTesting @testable import Factory -final class FactoryDefectTests: XCTestCase { +final class FactoryDefectTests: XCContainerTestCase { override func setUp() { super.setUp() diff --git a/Tests/FactoryTests/FactoryFunctionalTests.swift b/Tests/FactoryTests/FactoryFunctionalTests.swift index 59952a4c..63fd9993 100644 --- a/Tests/FactoryTests/FactoryFunctionalTests.swift +++ b/Tests/FactoryTests/FactoryFunctionalTests.swift @@ -1,6 +1,7 @@ #if canImport(os) import XCTest +import FactoryTesting import os @testable import Factory @@ -33,7 +34,7 @@ final class OpenURLFunctionMock: Sendable { } @available(iOS 16.0, *) -final class FactoryFunctionalTests: XCTestCase { +final class FactoryFunctionalTests: XCContainerTestCase { override func setUp() { super.setUp() diff --git a/Tests/FactoryTests/FactoryInjectionTests.swift b/Tests/FactoryTests/FactoryInjectionTests.swift index 6dcaa445..18cf15b9 100644 --- a/Tests/FactoryTests/FactoryInjectionTests.swift +++ b/Tests/FactoryTests/FactoryInjectionTests.swift @@ -6,6 +6,7 @@ import Observation import SwiftUI #endif +import FactoryTesting @testable import Factory class Services1 { @@ -112,7 +113,7 @@ extension Container { } } -final class FactoryInjectionTests: XCTestCase { +final class FactoryInjectionTests: XCContainerTestCase { override func setUp() { super.setUp() diff --git a/Tests/FactoryTests/FactoryIsolationTests.swift b/Tests/FactoryTests/FactoryIsolationTests.swift index e56d3354..4bda24c5 100644 --- a/Tests/FactoryTests/FactoryIsolationTests.swift +++ b/Tests/FactoryTests/FactoryIsolationTests.swift @@ -1,5 +1,5 @@ import XCTest - +import FactoryTesting @testable import Factory private struct SomeSendableType: Sendable {} @@ -53,7 +53,7 @@ extension Container { } } -final class FactoryIsolationTests: XCTestCase { +final class FactoryIsolationTests: XCContainerTestCase { override func setUp() { super.setUp() diff --git a/Tests/FactoryTests/FactoryMultithreadingTests.swift b/Tests/FactoryTests/FactoryMultithreadingTests.swift index 59dadd5c..1dca9daf 100644 --- a/Tests/FactoryTests/FactoryMultithreadingTests.swift +++ b/Tests/FactoryTests/FactoryMultithreadingTests.swift @@ -1,7 +1,8 @@ import XCTest +import FactoryTesting @testable import Factory -final class FactoryMultithreadingTests: XCTestCase, @unchecked Sendable { +final class FactoryMultithreadingTests: XCContainerTestCase, @unchecked Sendable { let qa = DispatchQueue(label: "A", qos: .userInteractive, attributes: .concurrent) let qb = DispatchQueue(label: "B", qos: .userInitiated, attributes: .concurrent) diff --git a/Tests/FactoryTests/FactoryParameterTests.swift b/Tests/FactoryTests/FactoryParameterTests.swift index 8adfe6b9..47d6a37e 100644 --- a/Tests/FactoryTests/FactoryParameterTests.swift +++ b/Tests/FactoryTests/FactoryParameterTests.swift @@ -1,7 +1,8 @@ import XCTest +import FactoryTesting @testable import Factory -final class FactoryParameterTests: XCTestCase { +final class FactoryParameterTests: XCContainerTestCase { override func setUp() { super.setUp() diff --git a/Tests/FactoryTests/FactoryResolverTests.swift b/Tests/FactoryTests/FactoryResolverTests.swift index 68da323d..cbc91402 100644 --- a/Tests/FactoryTests/FactoryResolverTests.swift +++ b/Tests/FactoryTests/FactoryResolverTests.swift @@ -1,7 +1,8 @@ import XCTest +import FactoryTesting @testable import Factory -final class FactoryResolverTests: XCTestCase { +final class FactoryResolverTests: XCContainerTestCase { fileprivate var container: ResolvingContainer! diff --git a/Tests/FactoryTests/FactoryScopeTests.swift b/Tests/FactoryTests/FactoryScopeTests.swift index 9e2af636..b88dca76 100644 --- a/Tests/FactoryTests/FactoryScopeTests.swift +++ b/Tests/FactoryTests/FactoryScopeTests.swift @@ -1,7 +1,8 @@ import XCTest +import FactoryTesting @testable import Factory -final class FactoryScopeTests: XCTestCase { +final class FactoryScopeTests: XCContainerTestCase { override func setUp() { super.setUp() diff --git a/Tests/FactoryTests/ParallelXCTest.swift b/Tests/FactoryTests/ParallelXCTest.swift index 2657d0ca..9cd60010 100644 --- a/Tests/FactoryTests/ParallelXCTest.swift +++ b/Tests/FactoryTests/ParallelXCTest.swift @@ -1,9 +1,11 @@ -#if swift(>=6.1) +#if swift(>=5.5) import XCTest @testable import Factory +import FactoryTesting final class ParallelXCTest: XCTestCase { + //TODO: maybe delete because these examples do not contain the singleton resetting logic... func testFooBarBaz() { let container = Container() let fooExpectation = expectation(description: "foo") @@ -38,5 +40,264 @@ final class ParallelXCTest: XCTestCase { wait(for: [fooExpectation, barExpectation, bazExpectation], timeout: 60) } + + // Illustrates using the withContainer() helper with a synchronous transform closure + func testFooBarBazWithContainer() { + let fooExpectation = expectation(description: "foo") + + FactoryTestingHelper.withContainer() { + let sut = TaskLocalUseCase() + XCTAssertEqual(sut.fooBarBaz.value, "foo") + fooExpectation.fulfill() + } transform: { + $0.fooBarBaz.register { Foo() } + } + + let barExpectation = expectation(description: "bar") + + FactoryTestingHelper.withContainer { + let sut = TaskLocalUseCase() + XCTAssertEqual(sut.fooBarBaz.value, "bar") + barExpectation.fulfill() + } transform: { + $0.fooBarBaz.register { Bar() } + } + + let bazExpectation = expectation(description: "baz") + + FactoryTestingHelper.withContainer { + let sut = TaskLocalUseCase() + XCTAssertEqual(sut.fooBarBaz.value, "baz") + bazExpectation.fulfill() + } transform: { + $0.fooBarBaz.register { Baz() } + } + + wait(for: [fooExpectation, barExpectation, bazExpectation], timeout: 60) + } + + // Illustrates using the withContainer() helper with an asynchronous transform closure + func testFooBarBazWithContainerAsync() async { + let fooExpectation = expectation(description: "foo") + + await FactoryTestingHelper.withContainer() { + let sut = await IsolatedTaskLocalUseCase() + let value = await sut.isolatedToMainActor.value + XCTAssertEqual(value, "foo") + fooExpectation.fulfill() + } transform: { + await $0.isolatedToMainActor.register { @MainActor in MainActorFooBarBaz(value: "foo") } + } + + let barExpectation = expectation(description: "bar") + + await FactoryTestingHelper.withContainer { + let sut = await IsolatedTaskLocalUseCase() + let value = await sut.isolatedToMainActor.value + XCTAssertEqual(value, "bar") + barExpectation.fulfill() + } transform: { + await $0.isolatedToMainActor.register { @MainActor in MainActorFooBarBaz(value: "bar") } + } + + let bazExpectation = expectation(description: "baz") + + await FactoryTestingHelper.withContainer { + let sut = await IsolatedTaskLocalUseCase() + let value = await sut.isolatedToMainActor.value + XCTAssertEqual(value, "baz") + bazExpectation.fulfill() + } transform: { + await $0.isolatedToMainActor.register { @MainActor in MainActorFooBarBaz(value: "baz") } + } + + await fulfillment(of: [fooExpectation, barExpectation, bazExpectation], timeout: 60) + } +} + +/// Illustrates using the `XCContainerTestCase` +final class ParallelXCContainerTestFoo: XCContainerTestCase { + func testFoo() { + let c = Container.shared + c.fooBarBaz.register { Foo() } + c.fooBarBazCached.register { Foo() } + c.fooBarBazSingleton.register { Foo() } + + commonTests("foo") + } +} + +/// Illustrates using the `XCContainerTestCase` +final class ParallelXCContainerTestBar: XCContainerTestCase { + func testBar() { + let c = Container.shared + c.fooBarBaz.register { Bar() } + c.fooBarBazCached.register { Bar() } + c.fooBarBazSingleton.register { Bar() } + + commonTests("bar") + } +} + +/// Illustrates using the `XCContainerTestCase` +final class ParallelXCContainerTestBaz: XCContainerTestCase { + func testBaz() { + let c = Container.shared + c.fooBarBaz.register { Baz() } + c.fooBarBazCached.register { Baz() } + c.fooBarBazSingleton.register { Baz() } + + commonTests("baz") + } +} + +/// Illustrates using the `XCContainerTestCase` with different isolations. +final class ParallelIsolatedXCTestsFoo: XCContainerTestCase { + func testIsolatedFoo() async { + let c = Container.shared + + c.fooBarBaz.register { Foo() } + c.fooBarBazCached.register { Foo() } + c.fooBarBazSingleton.register { Foo() } + + await c.isolatedToMainActor.register { @MainActor in MainActorFooBarBaz(value: "foo") } + await c.isolatedToMainActorCached.register { @MainActor in MainActorFooBarBaz(value: "foo") } + await c.isolatedToMainActorSingleton.register { @MainActor in MainActorFooBarBaz(value: "foo") } + + await c.isolatedToCustomGlobalActor.register { IsolatedFoo() } + await c.isolatedToCustomGlobalActorCached.register { IsolatedFoo() } + await c.isolatedToCustomGlobalActorSingleton.register { IsolatedFoo() } + + await isolatedAsyncTests("foo") + } +} + +/// Illustrates using the `XCContainerTestCase` with different isolations. +final class ParallelIsolatedXCTestsBar: XCContainerTestCase { + func testIsolatedBar() async { + let c = Container.shared + + c.fooBarBaz.register { Bar() } + c.fooBarBazCached.register { Bar() } + c.fooBarBazSingleton.register { Bar() } + + await c.isolatedToMainActor.register { @MainActor in MainActorFooBarBaz(value: "bar") } + await c.isolatedToMainActorCached.register { @MainActor in MainActorFooBarBaz(value: "bar") } + await c.isolatedToMainActorSingleton.register { @MainActor in MainActorFooBarBaz(value: "bar") } + + await c.isolatedToCustomGlobalActor.register { IsolatedBar() } + await c.isolatedToCustomGlobalActorCached.register { IsolatedBar() } + await c.isolatedToCustomGlobalActorSingleton.register { IsolatedBar() } + + await isolatedAsyncTests("bar") + } +} + +/// Illustrates using the `XCContainerTestCase` with the transform sugar via the initalizer and with different isolations. +final class ParallelIsolatedXCTestsBaz: XCContainerTestCase { + + /// Overriding the transform property to register dependencies in the Container for every unit test inside this class. + /// This is a synchronous transform, so it should not contain any async code, unlike the `transform` in the `ContainerTrait` for `swift-testing`. + override var transform: (@Sendable (Container) -> Void)? { + get { + { + $0.fooBarBaz.register { Baz() } + $0.fooBarBazCached.register { Baz() } + $0.fooBarBazSingleton.register { Baz() } + } + } + set { } + } + + func testIsolatedBaz() async { + let c = Container.shared + + await c.isolatedToMainActor.register { @MainActor in MainActorFooBarBaz(value: "baz") } + await c.isolatedToMainActorCached.register { @MainActor in MainActorFooBarBaz(value: "baz") } + await c.isolatedToMainActorSingleton.register { @MainActor in MainActorFooBarBaz(value: "baz") } + + await c.isolatedToCustomGlobalActor.register { IsolatedBaz() } + await c.isolatedToCustomGlobalActorCached.register { IsolatedBaz() } + await c.isolatedToCustomGlobalActorSingleton.register { IsolatedBaz() } + + await isolatedAsyncTests("baz") + } +} + +private func commonTests(_ value: String) { + let sut1 = TaskLocalUseCase() + XCTAssertEqual(sut1.fooBarBaz.value, value) + XCTAssertEqual(sut1.fooBarBazCached.value, value) + XCTAssertEqual(sut1.fooBarBazSingleton.value, value) + + let sut2 = TaskLocalUseCase() + XCTAssertEqual(sut2.fooBarBaz.value, value) + XCTAssertEqual(sut2.fooBarBazCached.value, value) + XCTAssertEqual(sut2.fooBarBazSingleton.value, value) + + XCTAssertNotEqual(sut1.fooBarBaz.id, sut2.fooBarBaz.id) + XCTAssertEqual(sut1.fooBarBazCached.id, sut2.fooBarBazCached.id) + XCTAssertEqual(sut1.fooBarBazSingleton.id, sut2.fooBarBazSingleton.id) + + Container.shared.fooBarBazSingleton.register { Foo() } + + let sut3 = TaskLocalUseCase() + XCTAssertEqual(sut3.fooBarBazSingleton.value, "foo") + XCTAssertNotEqual(sut1.fooBarBazSingleton.id, sut3.fooBarBazSingleton.id) +} + +@MainActor +private func isolatedAsyncTests(_ value: String) async { + let sut1 = await IsolatedTaskLocalUseCase() + + XCTAssertEqual(sut1.fooBarBaz.value, value) + XCTAssertEqual(sut1.fooBarBazCached.value, value) + XCTAssertEqual(sut1.fooBarBazSingleton.value, value) + + XCTAssertEqual(sut1.isolatedToMainActor.value, value) + XCTAssertEqual(sut1.isolatedToMainActorCached.value, value) + XCTAssertEqual(sut1.isolatedToMainActorSingleton.value, value) + + XCTAssertEqual(sut1.isolatedToCustomGlobalActor.value, value) + XCTAssertEqual(sut1.isolatedToCustomGlobalActorCached.value, value) + XCTAssertEqual(sut1.isolatedToCustomGlobalActorSingleton.value, value) + + let sut2 = await IsolatedTaskLocalUseCase() + XCTAssertEqual(sut2.fooBarBaz.value, value) + XCTAssertEqual(sut2.fooBarBazCached.value, value) + XCTAssertEqual(sut2.fooBarBazSingleton.value, value) + + XCTAssertEqual(sut2.isolatedToMainActor.value, value) + XCTAssertEqual(sut2.isolatedToMainActorCached.value, value) + XCTAssertEqual(sut2.isolatedToMainActorSingleton.value, value) + + XCTAssertEqual(sut2.isolatedToCustomGlobalActor.value, value) + XCTAssertEqual(sut2.isolatedToCustomGlobalActorCached.value, value) + XCTAssertEqual(sut2.isolatedToCustomGlobalActorSingleton.value, value) + + XCTAssertNotEqual(sut1.fooBarBaz.id, sut2.fooBarBaz.id) + XCTAssertEqual(sut1.fooBarBazCached.id, sut2.fooBarBazCached.id) + XCTAssertEqual(sut1.fooBarBazSingleton.id, sut2.fooBarBazSingleton.id) + + XCTAssertNotEqual(sut1.isolatedToMainActor.id, sut2.isolatedToMainActor.id) + XCTAssertEqual(sut1.isolatedToMainActorCached.id, sut2.isolatedToMainActorCached.id) + XCTAssertEqual(sut1.isolatedToMainActorSingleton.id, sut2.isolatedToMainActorSingleton.id) + + XCTAssertNotEqual(sut1.isolatedToCustomGlobalActor.id, sut2.isolatedToCustomGlobalActor.id) + XCTAssertEqual(sut1.isolatedToCustomGlobalActorCached.id, sut2.isolatedToCustomGlobalActorCached.id) + XCTAssertEqual(sut1.isolatedToCustomGlobalActorSingleton.id, sut2.isolatedToCustomGlobalActorSingleton.id) + + Container.shared.fooBarBazSingleton.register { Foo() } + Container.shared.isolatedToMainActorSingleton.register { @MainActor in MainActorFooBarBaz(value: "foo") } + await Container.shared.isolatedToCustomGlobalActorSingleton.register { IsolatedFoo() } + + let sut3 = await IsolatedTaskLocalUseCase() + XCTAssertEqual(sut3.fooBarBazSingleton.value, "foo") + XCTAssertEqual(sut3.isolatedToMainActorSingleton.value, "foo") + XCTAssertEqual(sut3.isolatedToCustomGlobalActorSingleton.value, "foo") + + XCTAssertNotEqual(sut1.fooBarBazSingleton.id, sut3.fooBarBazSingleton.id) + XCTAssertNotEqual(sut1.isolatedToMainActorSingleton.id, sut3.isolatedToMainActorSingleton.id) + XCTAssertNotEqual(sut1.isolatedToCustomGlobalActorSingleton.id, sut3.isolatedToCustomGlobalActorSingleton.id) } #endif From 92e5c473e937a5ccd13c5cbdb1a885b5430ec7ba Mon Sep 17 00:00:00 2001 From: Akos Grabecz Date: Mon, 26 May 2025 10:39:51 +0200 Subject: [PATCH 2/7] FactoryContext to be tasklocalized --- Sources/Factory/Factory/Contexts.swift | 61 ++++++++++++++++++- .../FactoryTesting/FactoryTestingHelper.swift | 1 + .../FactoryTests/FactoryComponentTests.swift | 5 -- .../FactoryTests/FactoryContainerTests.swift | 1 - Tests/FactoryTests/FactoryContextTests.swift | 9 --- Tests/FactoryTests/FactoryCoreTests.swift | 1 - Tests/FactoryTests/FactoryDefectTests.swift | 7 --- .../FactoryTests/FactoryFunctionalTests.swift | 7 --- .../FactoryTests/FactoryInjectionTests.swift | 5 -- .../FactoryTests/FactoryIsolationTests.swift | 5 -- .../FactoryTests/FactoryParameterTests.swift | 5 -- Tests/FactoryTests/FactoryScopeTests.swift | 5 -- 12 files changed, 61 insertions(+), 51 deletions(-) diff --git a/Sources/Factory/Factory/Contexts.swift b/Sources/Factory/Factory/Contexts.swift index 6229137b..1967822c 100644 --- a/Sources/Factory/Factory/Contexts.swift +++ b/Sources/Factory/Factory/Contexts.swift @@ -44,7 +44,7 @@ public enum FactoryContextType: Equatable { case device } -public struct FactoryContext { +public struct FactoryContext: Sendable { /// Proxy for application arguments. public var arguments: [String] = ProcessInfo.processInfo.arguments /// Runtime arguments @@ -75,17 +75,76 @@ public struct FactoryContext { extension FactoryContext { /// Global current context. + #if swift(>=5.5) + @TaskLocal nonisolated(unsafe) public static var current = FactoryContext() + #else nonisolated(unsafe) public static var current = FactoryContext() + #endif } extension FactoryContext { /// Add argument to global context. + #if swift(>=5.5) + public static func setArg( + _ arg: String, + forKey key: String, + for operation: () throws -> Void + ) rethrows { + var factoryContextCopy = FactoryContext.current + factoryContextCopy.runtimeArguments[key] = arg + + try FactoryContext.$current.withValue(factoryContextCopy) { + try operation() + } + } + + /// Add argument to global context asynchronously. + public static func setArg( + _ arg: String, + forKey key: String, + for operation: () async throws -> Void + ) async rethrows { + var factoryContextCopy = FactoryContext.current + factoryContextCopy.runtimeArguments[key] = arg + + try await FactoryContext.$current.withValue(factoryContextCopy) { + try await operation() + } + } + #else public static func setArg(_ arg: String, forKey key: String) { FactoryContext.current.runtimeArguments[key] = arg } + #endif + + /// Add argument to global context. + #if swift(>=5.5) + public static func removeArg(forKey key: String, for operation: () throws -> Void) rethrows { + var factoryContextCopy = FactoryContext.current + factoryContextCopy.runtimeArguments.removeValue(forKey: key) + + try FactoryContext.$current.withValue(factoryContextCopy) { + try operation() + } + } + + /// Add argument to global context asynchronously. + public static func removeArg( + forKey key: String, + for operation: () async throws -> Void + ) async rethrows { + var factoryContextCopy = FactoryContext.current + factoryContextCopy.runtimeArguments.removeValue(forKey: key) + + try await FactoryContext.$current.withValue(factoryContextCopy) { + try await operation() + } + } + #else public static func removeArg(forKey key: String) { FactoryContext.current.runtimeArguments.removeValue(forKey: key) } + #endif } diff --git a/Sources/FactoryTesting/FactoryTestingHelper.swift b/Sources/FactoryTesting/FactoryTestingHelper.swift index bd2bf507..8642c991 100644 --- a/Sources/FactoryTesting/FactoryTestingHelper.swift +++ b/Sources/FactoryTesting/FactoryTestingHelper.swift @@ -1,6 +1,7 @@ import Factory // This should be a global function but Swift doesn't like global generic functions with default values. +//TODO: FactoryContext.current to be added public enum FactoryTestingHelper { /// Asynchronously provides a new container for each operation. public static func withContainer( diff --git a/Tests/FactoryTests/FactoryComponentTests.swift b/Tests/FactoryTests/FactoryComponentTests.swift index dfdadcfb..c1102cd2 100644 --- a/Tests/FactoryTests/FactoryComponentTests.swift +++ b/Tests/FactoryTests/FactoryComponentTests.swift @@ -17,11 +17,6 @@ final class FactoryComponentTests: XCContainerTestCase { let key3U = FactoryKey(type: UUID.self, key: key3Unicode) let key4U = FactoryKey(type: UUID.self, key: key4Unicode) - override func setUp() { - super.setUp() - Container.shared.reset() - } - func testScopeCache() { let cache = Scope.Cache() let scopeID = UUID() diff --git a/Tests/FactoryTests/FactoryContainerTests.swift b/Tests/FactoryTests/FactoryContainerTests.swift index d1d45c80..a7a8cc1d 100644 --- a/Tests/FactoryTests/FactoryContainerTests.swift +++ b/Tests/FactoryTests/FactoryContainerTests.swift @@ -10,7 +10,6 @@ final class FactoryContainerTests: XCContainerTestCase { override func setUp() { super.setUp() - Container.shared.reset() CustomContainer.shared.reset() } diff --git a/Tests/FactoryTests/FactoryContextTests.swift b/Tests/FactoryTests/FactoryContextTests.swift index 8ed8f8e6..e5f2be66 100644 --- a/Tests/FactoryTests/FactoryContextTests.swift +++ b/Tests/FactoryTests/FactoryContextTests.swift @@ -7,9 +7,6 @@ final class FactoryContextTests: XCContainerTestCase { override func setUp() { super.setUp() - // start over - Container.shared.reset() - // externally defined contexts Container.shared.externalContextService .register { ContextService(name: "REGISTERED") } @@ -34,12 +31,6 @@ final class FactoryContextTests: XCContainerTestCase { .onArgs(["ARG1","ARG2"]) { ContextService(name: "ARG") } } - override func tearDown() { - super.tearDown() - // restore current arg state - FactoryContext.current = FactoryContext() - } - #if os(macOS) // on linux the detection of test or siumator fails, so skipping seams logical until there is an alternative. func testDefaultRunningUnitTest() { diff --git a/Tests/FactoryTests/FactoryCoreTests.swift b/Tests/FactoryTests/FactoryCoreTests.swift index 5fbeca88..af69b47e 100644 --- a/Tests/FactoryTests/FactoryCoreTests.swift +++ b/Tests/FactoryTests/FactoryCoreTests.swift @@ -5,7 +5,6 @@ import FactoryTesting final class FactoryCoreTests: XCContainerTestCase { override func setUp() { - Container.shared.reset() CustomContainer.shared.reset() CustomContainer.shared.count = 0 } diff --git a/Tests/FactoryTests/FactoryDefectTests.swift b/Tests/FactoryTests/FactoryDefectTests.swift index a12efc70..079ef910 100644 --- a/Tests/FactoryTests/FactoryDefectTests.swift +++ b/Tests/FactoryTests/FactoryDefectTests.swift @@ -3,13 +3,6 @@ import FactoryTesting @testable import Factory final class FactoryDefectTests: XCContainerTestCase { - - override func setUp() { - super.setUp() - Container.shared.reset() - Scope.singleton.reset() - } - // scope would not correctly resolve a factory with an optional type. e.g. Factory(scope: .cached) { nil } func testNilScopedService() throws { Container.shared.nilCachedService.reset() diff --git a/Tests/FactoryTests/FactoryFunctionalTests.swift b/Tests/FactoryTests/FactoryFunctionalTests.swift index 63fd9993..6fcfd98c 100644 --- a/Tests/FactoryTests/FactoryFunctionalTests.swift +++ b/Tests/FactoryTests/FactoryFunctionalTests.swift @@ -35,13 +35,6 @@ final class OpenURLFunctionMock: Sendable { @available(iOS 16.0, *) final class FactoryFunctionalTests: XCContainerTestCase { - - override func setUp() { - super.setUp() - Container.shared.reset() - } - - func testOpenFunctionality() throws { let openedURL: OSAllocatedUnfairLock = .init(initialState: nil) Container.shared.openURL.register { diff --git a/Tests/FactoryTests/FactoryInjectionTests.swift b/Tests/FactoryTests/FactoryInjectionTests.swift index 18cf15b9..d5c07dca 100644 --- a/Tests/FactoryTests/FactoryInjectionTests.swift +++ b/Tests/FactoryTests/FactoryInjectionTests.swift @@ -115,11 +115,6 @@ extension Container { final class FactoryInjectionTests: XCContainerTestCase { - override func setUp() { - super.setUp() - Container.shared.reset() - } - func testBasicInjection() throws { let services = Services1() XCTAssertEqual(services.service.text(), "MyService") diff --git a/Tests/FactoryTests/FactoryIsolationTests.swift b/Tests/FactoryTests/FactoryIsolationTests.swift index 4bda24c5..c57902fc 100644 --- a/Tests/FactoryTests/FactoryIsolationTests.swift +++ b/Tests/FactoryTests/FactoryIsolationTests.swift @@ -55,11 +55,6 @@ extension Container { final class FactoryIsolationTests: XCContainerTestCase { - override func setUp() { - super.setUp() - Container.shared.reset() - } - // test resolution of sendable type @MainActor func testInjectSendableDependency() { diff --git a/Tests/FactoryTests/FactoryParameterTests.swift b/Tests/FactoryTests/FactoryParameterTests.swift index 47d6a37e..39d33c35 100644 --- a/Tests/FactoryTests/FactoryParameterTests.swift +++ b/Tests/FactoryTests/FactoryParameterTests.swift @@ -4,11 +4,6 @@ import FactoryTesting final class FactoryParameterTests: XCContainerTestCase { - override func setUp() { - super.setUp() - Container.shared.reset() - } - func testParameterServiceResolutions() throws { let service1 = Container.shared.parameterService(5) XCTAssertEqual(service1.value, 5) diff --git a/Tests/FactoryTests/FactoryScopeTests.swift b/Tests/FactoryTests/FactoryScopeTests.swift index b88dca76..0f3a3e9f 100644 --- a/Tests/FactoryTests/FactoryScopeTests.swift +++ b/Tests/FactoryTests/FactoryScopeTests.swift @@ -4,11 +4,6 @@ import FactoryTesting final class FactoryScopeTests: XCContainerTestCase { - override func setUp() { - super.setUp() - Container.shared.reset() - } - func testUniqueScope() throws { let service1 = Container.shared.myServiceType() let service2 = Container.shared.myServiceType() From 84ac763266d52f6c98875e826d7e9e10540507d3 Mon Sep 17 00:00:00 2001 From: Akos Grabecz Date: Mon, 26 May 2025 10:39:56 +0200 Subject: [PATCH 3/7] Revert "FactoryContext to be tasklocalized" This reverts commit cc7ff241efda18140a5efb6784011e6a78b3c8e8. --- Sources/Factory/Factory/Contexts.swift | 61 +------------------ .../FactoryTesting/FactoryTestingHelper.swift | 1 - .../FactoryTests/FactoryComponentTests.swift | 5 ++ .../FactoryTests/FactoryContainerTests.swift | 1 + Tests/FactoryTests/FactoryContextTests.swift | 9 +++ Tests/FactoryTests/FactoryCoreTests.swift | 1 + Tests/FactoryTests/FactoryDefectTests.swift | 7 +++ .../FactoryTests/FactoryFunctionalTests.swift | 7 +++ .../FactoryTests/FactoryInjectionTests.swift | 5 ++ .../FactoryTests/FactoryIsolationTests.swift | 5 ++ .../FactoryTests/FactoryParameterTests.swift | 5 ++ Tests/FactoryTests/FactoryScopeTests.swift | 5 ++ 12 files changed, 51 insertions(+), 61 deletions(-) diff --git a/Sources/Factory/Factory/Contexts.swift b/Sources/Factory/Factory/Contexts.swift index 1967822c..6229137b 100644 --- a/Sources/Factory/Factory/Contexts.swift +++ b/Sources/Factory/Factory/Contexts.swift @@ -44,7 +44,7 @@ public enum FactoryContextType: Equatable { case device } -public struct FactoryContext: Sendable { +public struct FactoryContext { /// Proxy for application arguments. public var arguments: [String] = ProcessInfo.processInfo.arguments /// Runtime arguments @@ -75,76 +75,17 @@ public struct FactoryContext: Sendable { extension FactoryContext { /// Global current context. - #if swift(>=5.5) - @TaskLocal nonisolated(unsafe) public static var current = FactoryContext() - #else nonisolated(unsafe) public static var current = FactoryContext() - #endif } extension FactoryContext { /// Add argument to global context. - #if swift(>=5.5) - public static func setArg( - _ arg: String, - forKey key: String, - for operation: () throws -> Void - ) rethrows { - var factoryContextCopy = FactoryContext.current - factoryContextCopy.runtimeArguments[key] = arg - - try FactoryContext.$current.withValue(factoryContextCopy) { - try operation() - } - } - - /// Add argument to global context asynchronously. - public static func setArg( - _ arg: String, - forKey key: String, - for operation: () async throws -> Void - ) async rethrows { - var factoryContextCopy = FactoryContext.current - factoryContextCopy.runtimeArguments[key] = arg - - try await FactoryContext.$current.withValue(factoryContextCopy) { - try await operation() - } - } - #else public static func setArg(_ arg: String, forKey key: String) { FactoryContext.current.runtimeArguments[key] = arg } - #endif - - /// Add argument to global context. - #if swift(>=5.5) - public static func removeArg(forKey key: String, for operation: () throws -> Void) rethrows { - var factoryContextCopy = FactoryContext.current - factoryContextCopy.runtimeArguments.removeValue(forKey: key) - - try FactoryContext.$current.withValue(factoryContextCopy) { - try operation() - } - } - - /// Add argument to global context asynchronously. - public static func removeArg( - forKey key: String, - for operation: () async throws -> Void - ) async rethrows { - var factoryContextCopy = FactoryContext.current - factoryContextCopy.runtimeArguments.removeValue(forKey: key) - - try await FactoryContext.$current.withValue(factoryContextCopy) { - try await operation() - } - } - #else public static func removeArg(forKey key: String) { FactoryContext.current.runtimeArguments.removeValue(forKey: key) } - #endif } diff --git a/Sources/FactoryTesting/FactoryTestingHelper.swift b/Sources/FactoryTesting/FactoryTestingHelper.swift index 8642c991..bd2bf507 100644 --- a/Sources/FactoryTesting/FactoryTestingHelper.swift +++ b/Sources/FactoryTesting/FactoryTestingHelper.swift @@ -1,7 +1,6 @@ import Factory // This should be a global function but Swift doesn't like global generic functions with default values. -//TODO: FactoryContext.current to be added public enum FactoryTestingHelper { /// Asynchronously provides a new container for each operation. public static func withContainer( diff --git a/Tests/FactoryTests/FactoryComponentTests.swift b/Tests/FactoryTests/FactoryComponentTests.swift index c1102cd2..dfdadcfb 100644 --- a/Tests/FactoryTests/FactoryComponentTests.swift +++ b/Tests/FactoryTests/FactoryComponentTests.swift @@ -17,6 +17,11 @@ final class FactoryComponentTests: XCContainerTestCase { let key3U = FactoryKey(type: UUID.self, key: key3Unicode) let key4U = FactoryKey(type: UUID.self, key: key4Unicode) + override func setUp() { + super.setUp() + Container.shared.reset() + } + func testScopeCache() { let cache = Scope.Cache() let scopeID = UUID() diff --git a/Tests/FactoryTests/FactoryContainerTests.swift b/Tests/FactoryTests/FactoryContainerTests.swift index a7a8cc1d..d1d45c80 100644 --- a/Tests/FactoryTests/FactoryContainerTests.swift +++ b/Tests/FactoryTests/FactoryContainerTests.swift @@ -10,6 +10,7 @@ final class FactoryContainerTests: XCContainerTestCase { override func setUp() { super.setUp() + Container.shared.reset() CustomContainer.shared.reset() } diff --git a/Tests/FactoryTests/FactoryContextTests.swift b/Tests/FactoryTests/FactoryContextTests.swift index e5f2be66..8ed8f8e6 100644 --- a/Tests/FactoryTests/FactoryContextTests.swift +++ b/Tests/FactoryTests/FactoryContextTests.swift @@ -7,6 +7,9 @@ final class FactoryContextTests: XCContainerTestCase { override func setUp() { super.setUp() + // start over + Container.shared.reset() + // externally defined contexts Container.shared.externalContextService .register { ContextService(name: "REGISTERED") } @@ -31,6 +34,12 @@ final class FactoryContextTests: XCContainerTestCase { .onArgs(["ARG1","ARG2"]) { ContextService(name: "ARG") } } + override func tearDown() { + super.tearDown() + // restore current arg state + FactoryContext.current = FactoryContext() + } + #if os(macOS) // on linux the detection of test or siumator fails, so skipping seams logical until there is an alternative. func testDefaultRunningUnitTest() { diff --git a/Tests/FactoryTests/FactoryCoreTests.swift b/Tests/FactoryTests/FactoryCoreTests.swift index af69b47e..5fbeca88 100644 --- a/Tests/FactoryTests/FactoryCoreTests.swift +++ b/Tests/FactoryTests/FactoryCoreTests.swift @@ -5,6 +5,7 @@ import FactoryTesting final class FactoryCoreTests: XCContainerTestCase { override func setUp() { + Container.shared.reset() CustomContainer.shared.reset() CustomContainer.shared.count = 0 } diff --git a/Tests/FactoryTests/FactoryDefectTests.swift b/Tests/FactoryTests/FactoryDefectTests.swift index 079ef910..a12efc70 100644 --- a/Tests/FactoryTests/FactoryDefectTests.swift +++ b/Tests/FactoryTests/FactoryDefectTests.swift @@ -3,6 +3,13 @@ import FactoryTesting @testable import Factory final class FactoryDefectTests: XCContainerTestCase { + + override func setUp() { + super.setUp() + Container.shared.reset() + Scope.singleton.reset() + } + // scope would not correctly resolve a factory with an optional type. e.g. Factory(scope: .cached) { nil } func testNilScopedService() throws { Container.shared.nilCachedService.reset() diff --git a/Tests/FactoryTests/FactoryFunctionalTests.swift b/Tests/FactoryTests/FactoryFunctionalTests.swift index 6fcfd98c..63fd9993 100644 --- a/Tests/FactoryTests/FactoryFunctionalTests.swift +++ b/Tests/FactoryTests/FactoryFunctionalTests.swift @@ -35,6 +35,13 @@ final class OpenURLFunctionMock: Sendable { @available(iOS 16.0, *) final class FactoryFunctionalTests: XCContainerTestCase { + + override func setUp() { + super.setUp() + Container.shared.reset() + } + + func testOpenFunctionality() throws { let openedURL: OSAllocatedUnfairLock = .init(initialState: nil) Container.shared.openURL.register { diff --git a/Tests/FactoryTests/FactoryInjectionTests.swift b/Tests/FactoryTests/FactoryInjectionTests.swift index d5c07dca..18cf15b9 100644 --- a/Tests/FactoryTests/FactoryInjectionTests.swift +++ b/Tests/FactoryTests/FactoryInjectionTests.swift @@ -115,6 +115,11 @@ extension Container { final class FactoryInjectionTests: XCContainerTestCase { + override func setUp() { + super.setUp() + Container.shared.reset() + } + func testBasicInjection() throws { let services = Services1() XCTAssertEqual(services.service.text(), "MyService") diff --git a/Tests/FactoryTests/FactoryIsolationTests.swift b/Tests/FactoryTests/FactoryIsolationTests.swift index c57902fc..4bda24c5 100644 --- a/Tests/FactoryTests/FactoryIsolationTests.swift +++ b/Tests/FactoryTests/FactoryIsolationTests.swift @@ -55,6 +55,11 @@ extension Container { final class FactoryIsolationTests: XCContainerTestCase { + override func setUp() { + super.setUp() + Container.shared.reset() + } + // test resolution of sendable type @MainActor func testInjectSendableDependency() { diff --git a/Tests/FactoryTests/FactoryParameterTests.swift b/Tests/FactoryTests/FactoryParameterTests.swift index 39d33c35..47d6a37e 100644 --- a/Tests/FactoryTests/FactoryParameterTests.swift +++ b/Tests/FactoryTests/FactoryParameterTests.swift @@ -4,6 +4,11 @@ import FactoryTesting final class FactoryParameterTests: XCContainerTestCase { + override func setUp() { + super.setUp() + Container.shared.reset() + } + func testParameterServiceResolutions() throws { let service1 = Container.shared.parameterService(5) XCTAssertEqual(service1.value, 5) diff --git a/Tests/FactoryTests/FactoryScopeTests.swift b/Tests/FactoryTests/FactoryScopeTests.swift index 0f3a3e9f..b88dca76 100644 --- a/Tests/FactoryTests/FactoryScopeTests.swift +++ b/Tests/FactoryTests/FactoryScopeTests.swift @@ -4,6 +4,11 @@ import FactoryTesting final class FactoryScopeTests: XCContainerTestCase { + override func setUp() { + super.setUp() + Container.shared.reset() + } + func testUniqueScope() throws { let service1 = Container.shared.myServiceType() let service2 = Container.shared.myServiceType() From 9204d4caadf0463673b80a518e33896d76c73241 Mon Sep 17 00:00:00 2001 From: Akos Grabecz Date: Mon, 26 May 2025 10:45:47 +0200 Subject: [PATCH 4/7] FactoryCoreTests to inherit from XCTestCase FactoryContext is not yet tasklocalized --- Tests/FactoryTests/FactoryCoreTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/FactoryTests/FactoryCoreTests.swift b/Tests/FactoryTests/FactoryCoreTests.swift index 5fbeca88..3a32517f 100644 --- a/Tests/FactoryTests/FactoryCoreTests.swift +++ b/Tests/FactoryTests/FactoryCoreTests.swift @@ -2,7 +2,7 @@ import XCTest import FactoryTesting @testable import Factory -final class FactoryCoreTests: XCContainerTestCase { +final class FactoryCoreTests: XCTestCase { override func setUp() { Container.shared.reset() From d4f62508ab891aa1994c5037b16fee2bc4d042a0 Mon Sep 17 00:00:00 2001 From: Akos Grabecz Date: Mon, 26 May 2025 10:46:06 +0200 Subject: [PATCH 5/7] remove unneccessary Container.shared.reset() and Scope.singleton.reset() --- Tests/FactoryTests/FactoryComponentTests.swift | 5 ----- Tests/FactoryTests/FactoryContainerTests.swift | 1 - Tests/FactoryTests/FactoryContextTests.swift | 3 --- Tests/FactoryTests/FactoryDefectTests.swift | 6 ------ Tests/FactoryTests/FactoryFunctionalTests.swift | 6 ------ Tests/FactoryTests/FactoryInjectionTests.swift | 5 ----- Tests/FactoryTests/FactoryIsolationTests.swift | 5 ----- Tests/FactoryTests/FactoryParameterTests.swift | 5 ----- Tests/FactoryTests/FactoryScopeTests.swift | 5 ----- 9 files changed, 41 deletions(-) diff --git a/Tests/FactoryTests/FactoryComponentTests.swift b/Tests/FactoryTests/FactoryComponentTests.swift index dfdadcfb..c1102cd2 100644 --- a/Tests/FactoryTests/FactoryComponentTests.swift +++ b/Tests/FactoryTests/FactoryComponentTests.swift @@ -17,11 +17,6 @@ final class FactoryComponentTests: XCContainerTestCase { let key3U = FactoryKey(type: UUID.self, key: key3Unicode) let key4U = FactoryKey(type: UUID.self, key: key4Unicode) - override func setUp() { - super.setUp() - Container.shared.reset() - } - func testScopeCache() { let cache = Scope.Cache() let scopeID = UUID() diff --git a/Tests/FactoryTests/FactoryContainerTests.swift b/Tests/FactoryTests/FactoryContainerTests.swift index d1d45c80..a7a8cc1d 100644 --- a/Tests/FactoryTests/FactoryContainerTests.swift +++ b/Tests/FactoryTests/FactoryContainerTests.swift @@ -10,7 +10,6 @@ final class FactoryContainerTests: XCContainerTestCase { override func setUp() { super.setUp() - Container.shared.reset() CustomContainer.shared.reset() } diff --git a/Tests/FactoryTests/FactoryContextTests.swift b/Tests/FactoryTests/FactoryContextTests.swift index 8ed8f8e6..b132bc88 100644 --- a/Tests/FactoryTests/FactoryContextTests.swift +++ b/Tests/FactoryTests/FactoryContextTests.swift @@ -7,9 +7,6 @@ final class FactoryContextTests: XCContainerTestCase { override func setUp() { super.setUp() - // start over - Container.shared.reset() - // externally defined contexts Container.shared.externalContextService .register { ContextService(name: "REGISTERED") } diff --git a/Tests/FactoryTests/FactoryDefectTests.swift b/Tests/FactoryTests/FactoryDefectTests.swift index a12efc70..dc7cf38b 100644 --- a/Tests/FactoryTests/FactoryDefectTests.swift +++ b/Tests/FactoryTests/FactoryDefectTests.swift @@ -4,12 +4,6 @@ import FactoryTesting final class FactoryDefectTests: XCContainerTestCase { - override func setUp() { - super.setUp() - Container.shared.reset() - Scope.singleton.reset() - } - // scope would not correctly resolve a factory with an optional type. e.g. Factory(scope: .cached) { nil } func testNilScopedService() throws { Container.shared.nilCachedService.reset() diff --git a/Tests/FactoryTests/FactoryFunctionalTests.swift b/Tests/FactoryTests/FactoryFunctionalTests.swift index 63fd9993..d6df3f98 100644 --- a/Tests/FactoryTests/FactoryFunctionalTests.swift +++ b/Tests/FactoryTests/FactoryFunctionalTests.swift @@ -36,12 +36,6 @@ final class OpenURLFunctionMock: Sendable { @available(iOS 16.0, *) final class FactoryFunctionalTests: XCContainerTestCase { - override func setUp() { - super.setUp() - Container.shared.reset() - } - - func testOpenFunctionality() throws { let openedURL: OSAllocatedUnfairLock = .init(initialState: nil) Container.shared.openURL.register { diff --git a/Tests/FactoryTests/FactoryInjectionTests.swift b/Tests/FactoryTests/FactoryInjectionTests.swift index 18cf15b9..d5c07dca 100644 --- a/Tests/FactoryTests/FactoryInjectionTests.swift +++ b/Tests/FactoryTests/FactoryInjectionTests.swift @@ -115,11 +115,6 @@ extension Container { final class FactoryInjectionTests: XCContainerTestCase { - override func setUp() { - super.setUp() - Container.shared.reset() - } - func testBasicInjection() throws { let services = Services1() XCTAssertEqual(services.service.text(), "MyService") diff --git a/Tests/FactoryTests/FactoryIsolationTests.swift b/Tests/FactoryTests/FactoryIsolationTests.swift index 4bda24c5..c57902fc 100644 --- a/Tests/FactoryTests/FactoryIsolationTests.swift +++ b/Tests/FactoryTests/FactoryIsolationTests.swift @@ -55,11 +55,6 @@ extension Container { final class FactoryIsolationTests: XCContainerTestCase { - override func setUp() { - super.setUp() - Container.shared.reset() - } - // test resolution of sendable type @MainActor func testInjectSendableDependency() { diff --git a/Tests/FactoryTests/FactoryParameterTests.swift b/Tests/FactoryTests/FactoryParameterTests.swift index 47d6a37e..39d33c35 100644 --- a/Tests/FactoryTests/FactoryParameterTests.swift +++ b/Tests/FactoryTests/FactoryParameterTests.swift @@ -4,11 +4,6 @@ import FactoryTesting final class FactoryParameterTests: XCContainerTestCase { - override func setUp() { - super.setUp() - Container.shared.reset() - } - func testParameterServiceResolutions() throws { let service1 = Container.shared.parameterService(5) XCTAssertEqual(service1.value, 5) diff --git a/Tests/FactoryTests/FactoryScopeTests.swift b/Tests/FactoryTests/FactoryScopeTests.swift index b88dca76..0f3a3e9f 100644 --- a/Tests/FactoryTests/FactoryScopeTests.swift +++ b/Tests/FactoryTests/FactoryScopeTests.swift @@ -4,11 +4,6 @@ import FactoryTesting final class FactoryScopeTests: XCContainerTestCase { - override func setUp() { - super.setUp() - Container.shared.reset() - } - func testUniqueScope() throws { let service1 = Container.shared.myServiceType() let service2 = Container.shared.myServiceType() From bc3b93b916c1437ba1b96db5ff25aa96ebe782aa Mon Sep 17 00:00:00 2001 From: Akos Grabecz Date: Mon, 26 May 2025 16:05:44 +0200 Subject: [PATCH 6/7] polish XCContainerTestCase --- Sources/FactoryTesting/ContainerTrait.swift | 4 +- .../FactoryTesting/FactoryTestingHelper.swift | 36 --- Sources/FactoryTesting/WithContainer.swift | 33 +++ .../FactoryTesting/XCContainerTestCase.swift | 14 +- .../FactoryTests/FactoryContainerTests.swift | 19 +- Tests/FactoryTests/FactoryCoreTests.swift | 65 ++--- Tests/FactoryTests/FactoryDefectTests.swift | 89 ++++--- .../FactoryMultithreadingTests.swift | 35 ++- Tests/FactoryTests/FactoryResolverTests.swift | 37 ++- Tests/FactoryTests/FactoryScopeTests.swift | 88 +++++-- Tests/FactoryTests/MockServices.swift | 12 +- Tests/FactoryTests/ParallelServices.swift | 55 ++++ Tests/FactoryTests/ParallelXCTest.swift | 242 ++++++++++++++---- 13 files changed, 515 insertions(+), 214 deletions(-) delete mode 100644 Sources/FactoryTesting/FactoryTestingHelper.swift create mode 100644 Sources/FactoryTesting/WithContainer.swift diff --git a/Sources/FactoryTesting/ContainerTrait.swift b/Sources/FactoryTesting/ContainerTrait.swift index 929e989e..5c8fdcc4 100644 --- a/Sources/FactoryTesting/ContainerTrait.swift +++ b/Sources/FactoryTesting/ContainerTrait.swift @@ -62,10 +62,10 @@ public struct ContainerTrait: TestTrait, SuiteTrait, TestSco } public func provideScope(for test: Test, testCase: Test.Case?, performing function: () async throws -> Void) async throws { - try await FactoryTestingHelper.withContainer( + try await withContainer( shared: self.shared, container: self.container(), - function, + operation: function, transform: transform ) } diff --git a/Sources/FactoryTesting/FactoryTestingHelper.swift b/Sources/FactoryTesting/FactoryTestingHelper.swift deleted file mode 100644 index bd2bf507..00000000 --- a/Sources/FactoryTesting/FactoryTestingHelper.swift +++ /dev/null @@ -1,36 +0,0 @@ -import Factory - -// This should be a global function but Swift doesn't like global generic functions with default values. -public enum FactoryTestingHelper { - /// Asynchronously provides a new container for each operation. - public static func withContainer( - shared: TaskLocal = Container.$shared, - container: @autoclosure @Sendable () -> C = Container(), - _ operation: () async throws -> Void, - transform: ContainerTrait.Transform? - ) async rethrows { - try await Scope.$singleton.withValue(Scope.singleton.clone()) { - try await shared.withValue(container()) { - await transform?(C.shared) - try await operation() - } - } - } - - public typealias SynchronousTransform = @Sendable (C) -> Void - - /// Synchronous version of `withContainer` for use in non-async contexts. - public static func withContainer( - shared: TaskLocal = Container.$shared, - container: @autoclosure @Sendable () -> C = Container(), - _ operation: () throws -> Void, - transform: SynchronousTransform? - ) rethrows { - try Scope.$singleton.withValue(Scope.singleton.clone()) { - try shared.withValue(container()) { - transform?(C.shared) - try operation() - } - } - } -} diff --git a/Sources/FactoryTesting/WithContainer.swift b/Sources/FactoryTesting/WithContainer.swift new file mode 100644 index 00000000..01244ffd --- /dev/null +++ b/Sources/FactoryTesting/WithContainer.swift @@ -0,0 +1,33 @@ +import FactoryKit + +/// Asynchronously provides a new container and singleton scope for each operation. +public func withContainer( + shared: TaskLocal, + container: @autoclosure @Sendable () -> C, + operation: () async throws -> Void, + transform: ContainerTrait.Transform? = nil +) async rethrows { + try await Scope.$singleton.withValue(Scope.singleton.clone()) { + try await shared.withValue(container()) { + await transform?(C.shared) + try await operation() + } + } +} + +public typealias SynchronousTransform = @Sendable (C) -> Void + +/// Synchronous version of `withContainer` for use in non-async contexts. +public func withContainer( + shared: TaskLocal, + container: @autoclosure @Sendable () -> C, + operation: () throws -> Void, + transform: SynchronousTransform? = nil +) rethrows { + try Scope.$singleton.withValue(Scope.singleton.clone()) { + try shared.withValue(container()) { + transform?(C.shared) + try operation() + } + } +} diff --git a/Sources/FactoryTesting/XCContainerTestCase.swift b/Sources/FactoryTesting/XCContainerTestCase.swift index d7b9844e..680d0e81 100644 --- a/Sources/FactoryTesting/XCContainerTestCase.swift +++ b/Sources/FactoryTesting/XCContainerTestCase.swift @@ -1,17 +1,19 @@ -import Factory +import FactoryKit import XCTest -/// Provides a unique Container to every unit test function in the class. Mimicking the behavior of `ContainerTrait` for `swift-testing`. +/// Provides a unique `Container` and a singleton scope to every unit test function in the class. Mimicking the behavior of `ContainerTrait` for `swift-testing`. open class XCContainerTestCase: XCTestCase { - /// The optional transformation to apply to the Container before invoking the test. + /// The optional transformation to apply to the `Container` before invoking the test. /// Due to the nature of XCTest, this is not async and should be used for synchronous transformations only. open var transform: (@Sendable (Container) -> Void)? - /// Scopes the unit test function to a unique Container instance transformed via the `transform` variable (if overridden to non-nil). + /// Scopes the unit test function to a unique singleton scope and a `Container` instance transformed via the `transform` variable (if overridden to non-nil). public override func invokeTest() { - FactoryTestingHelper.withContainer( - super.invokeTest, + withContainer( + shared: Container.$shared, + container: Container(), + operation: super.invokeTest, transform: self.transform ) } diff --git a/Tests/FactoryTests/FactoryContainerTests.swift b/Tests/FactoryTests/FactoryContainerTests.swift index 145da280..5b8dc9ac 100644 --- a/Tests/FactoryTests/FactoryContainerTests.swift +++ b/Tests/FactoryTests/FactoryContainerTests.swift @@ -8,17 +8,6 @@ import SwiftUI final class FactoryContainerTests: XCContainerTestCase { - override func setUp() { - super.setUp() - CustomContainer.shared.reset() - } - - func testDecorators() { - CustomContainer.count = 0 - let _ = CustomContainer.shared.decorated() - XCTAssertEqual(CustomContainer.shared.count, 2) - } - func testPushPop() throws { let service1 = Container.shared.myServiceType() XCTAssertTrue(service1.text() == "MyService") @@ -112,6 +101,14 @@ final class FactoryContainerTests: XCContainerTestCase { } +final class FactoryCustomContainerTests: XCCustomContainerTestCase { + func testDecorators() { + CustomContainer.count = 0 + let _ = CustomContainer.shared.decorated() + XCTAssertEqual(CustomContainer.shared.count, 2) + } +} + private extension Container { var cachedCoverage: Factory { self { MyService() }.cached } var graphCoverage: Factory { self { MyService() }.graph } diff --git a/Tests/FactoryTests/FactoryCoreTests.swift b/Tests/FactoryTests/FactoryCoreTests.swift index 4d635663..d26a9b52 100644 --- a/Tests/FactoryTests/FactoryCoreTests.swift +++ b/Tests/FactoryTests/FactoryCoreTests.swift @@ -6,11 +6,9 @@ import FactoryTesting import SwiftUI #endif -final class FactoryCoreTests: XCTestCase { +final class FactoryCoreTests: XCContainerAndCustomContainerTestCase { override func setUp() { - Container.shared.reset() - CustomContainer.shared.reset() CustomContainer.shared.count = 0 } @@ -194,34 +192,6 @@ final class FactoryCoreTests: XCTestCase { } } - @MainActor - func testStrictPromise() { - // Expect non fatal error when strict and NOT in debug mode - Container.shared.manager.promiseTriggersError = false - expectNonFatalError { - let _ = Container.shared.strictPromisedService() - } - // Expect fatal error when strict and in debug mode - Container.shared.manager.promiseTriggersError = true - expectFatalError(expectedMessage: "MyServiceType was not registered") { - let _ = Container.shared.strictPromisedService() - } - } - - @MainActor - func testStrictParameterPromise() { - // Expect non fatal error when strict and NOT in debug mode - Container.shared.manager.promiseTriggersError = false - expectNonFatalError { - let _ = Container.shared.strictPromisedParameterService(23) - } - // Expect fatal error when strict and in debug mode - Container.shared.manager.promiseTriggersError = true - expectFatalError(expectedMessage: "ParameterService was not registered") { - let _ = Container.shared.strictPromisedParameterService(23) - } - } - func testTrace() { var logged: [String] = [] Container.shared.manager.trace.toggle() @@ -258,3 +228,36 @@ final class FactoryCoreTests: XCTestCase { #endif } + +// FactoryContext.current is not yet using @TaskLocal therefore we cannot use safely the `XCContainerTestCase` here. +final class FactoryCoreStrictPromiseTests: XCTestCase { + + @MainActor + func testStrictPromise() { + // Expect non fatal error when strict and NOT in debug mode + Container.shared.manager.promiseTriggersError = false + expectNonFatalError { + let _ = Container.shared.strictPromisedService() + } + // Expect fatal error when strict and in debug mode + Container.shared.manager.promiseTriggersError = true + expectFatalError(expectedMessage: "MyServiceType was not registered") { + let _ = Container.shared.strictPromisedService() + } + } + + @MainActor + func testStrictParameterPromise() { + // Expect non fatal error when strict and NOT in debug mode + Container.shared.manager.promiseTriggersError = false + expectNonFatalError { + let _ = Container.shared.strictPromisedParameterService(23) + } + // Expect fatal error when strict and in debug mode + Container.shared.manager.promiseTriggersError = true + expectFatalError(expectedMessage: "ParameterService was not registered") { + let _ = Container.shared.strictPromisedParameterService(23) + } + } + +} diff --git a/Tests/FactoryTests/FactoryDefectTests.swift b/Tests/FactoryTests/FactoryDefectTests.swift index 38024fa5..b9480fe4 100644 --- a/Tests/FactoryTests/FactoryDefectTests.swift +++ b/Tests/FactoryTests/FactoryDefectTests.swift @@ -128,37 +128,6 @@ final class FactoryDefectTests: XCContainerTestCase { XCTAssertFalse(Container.shared.manager.isEmpty(.scope)) } - // Registration on a new container could be overridden by auto registration - func testRegistrationOverriddenByAutoRegistration() throws { - let container1 = AutoRegisteringContainer() - let service1 = container1.test() - XCTAssertEqual(service1.value, 32) - let container2 = AutoRegisteringContainer() - container2.test.register { - MockServiceN(64) - } - let service2 = container2.test() - XCTAssertEqual(service2.value, 64) - } - - // AutoRegistration should have no effect on singletons - func testAutoRegistrationAndSingletonCache() throws { - let container1 = AutoRegisteringContainer() - let service1 = container1.singletonTest() - // should be auto registration value - XCTAssertEqual(service1.value, 32) - container1.singletonTest.register { - MockServiceN(64) - } - let service2 = container1.singletonTest() - // register should have cleared singleton scope cache so should be new value - XCTAssertEqual(service2.value, 64) - let container2 = AutoRegisteringContainer() - let service3 = container2.singletonTest() - // auto registration should have no effect on scope cache - XCTAssertEqual(service3.value, 64) - } - // #114 #146 setting a context would not clear scope cache as does register func testContextClearingScope() throws { let service1 = Container.shared.cachedService() @@ -191,7 +160,39 @@ final class FactoryDefectTests: XCContainerTestCase { XCTAssertFalse(service4.id == service5.id) XCTAssertFalse(service3.id == service5.id) } +} +final class FactoryAutoRegisteringTests: XCAutoRegisteringContainerTestCase { + // Registration on a new container could be overridden by auto registration + func testRegistrationOverriddenByAutoRegistration() throws { + let container1 = AutoRegisteringContainer() + let service1 = container1.test() + XCTAssertEqual(service1.value, 32) + let container2 = AutoRegisteringContainer() + container2.test.register { + MockServiceN(64) + } + let service2 = container2.test() + XCTAssertEqual(service2.value, 64) + } + + // AutoRegistration should have no effect on singletons + func testAutoRegistrationAndSingletonCache() throws { + let container1 = AutoRegisteringContainer() + let service1 = container1.singletonTest() + // should be auto registration value + XCTAssertEqual(service1.value, 32) + container1.singletonTest.register { + MockServiceN(64) + } + let service2 = container1.singletonTest() + // register should have cleared singleton scope cache so should be new value + XCTAssertEqual(service2.value, 64) + let container2 = AutoRegisteringContainer() + let service3 = container2.singletonTest() + // auto registration should have no effect on scope cache + XCTAssertEqual(service3.value, 64) + } } extension Container { @@ -220,17 +221,35 @@ fileprivate class LockingTestB { init() {} } -fileprivate final class AutoRegisteringContainer: SharedContainer, AutoRegistering { - static let shared = AutoRegisteringContainer() +package final class AutoRegisteringContainer: SharedContainer, AutoRegistering { + #if swift(>=5.5) + @TaskLocal package static var shared = AutoRegisteringContainer() + #else + package static let shared = AutoRegisteringContainer() + #endif var test: Factory { self { MockServiceN(16) } } var singletonTest: Factory { self { MockServiceN(16) }.singleton } - func autoRegister() { + package func autoRegister() { test.register { MockServiceN(32) } singletonTest.register { MockServiceN(32) } } - let manager = ContainerManager() + package let manager = ContainerManager() +} + +package class XCAutoRegisteringContainerTestCase: XCTestCase { + package var transform: (@Sendable (AutoRegisteringContainer) -> Void)? + + package override func invokeTest() { + withContainer( + shared: AutoRegisteringContainer.$shared, + container: AutoRegisteringContainer(), + operation: super.invokeTest, + transform: self.transform + ) + } } + diff --git a/Tests/FactoryTests/FactoryMultithreadingTests.swift b/Tests/FactoryTests/FactoryMultithreadingTests.swift index f296e082..89c6c98b 100644 --- a/Tests/FactoryTests/FactoryMultithreadingTests.swift +++ b/Tests/FactoryTests/FactoryMultithreadingTests.swift @@ -2,7 +2,7 @@ import XCTest import FactoryTesting @testable import FactoryKit -final class FactoryMultithreadingTests: XCContainerTestCase, @unchecked Sendable { +final class FactoryMultithreadingTests: XCMultiThreadedContainerTestCase, @unchecked Sendable { let qa = DispatchQueue(label: "A", qos: .userInteractive, attributes: .concurrent) let qb = DispatchQueue(label: "B", qos: .userInitiated, attributes: .concurrent) @@ -111,7 +111,7 @@ func increment() { lock.unlock() } -fileprivate class A { +package class A { var b: B init(b: B) { self.b = b @@ -121,7 +121,7 @@ fileprivate class A { } } -fileprivate class B { +package class B { var c: C init(c: C) { self.c = c @@ -131,7 +131,7 @@ fileprivate class B { } } -fileprivate class C { +package class C { var d: D init(d: D) { self.d = d @@ -141,14 +141,14 @@ fileprivate class C { } } -fileprivate class D { +package class D { init() {} func test() { increment() } } -fileprivate class E { +package class E { @LazyInjected(\MultiThreadedContainer.d) var d: D init() {} func test() { @@ -157,12 +157,29 @@ fileprivate class E { } } -fileprivate final class MultiThreadedContainer: SharedContainer { - fileprivate static let shared = MultiThreadedContainer() +package final class MultiThreadedContainer: SharedContainer { + #if swift(>=5.5) + @TaskLocal package static var shared = MultiThreadedContainer() + #else + package static let shared = MultiThreadedContainer() + #endif fileprivate var a: Factory { self { A(b: self.b()) } } fileprivate var b: Factory { self { B(c: self.c()) } } fileprivate var c: Factory { self { C(d: self.d()) } } fileprivate var d: Factory { self { D() }.cached } fileprivate var e: Factory { self { E() } } - let manager = ContainerManager() + package let manager = ContainerManager() +} + +package class XCMultiThreadedContainerTestCase: XCTestCase { + package var transform: (@Sendable (MultiThreadedContainer) -> Void)? + + package override func invokeTest() { + withContainer( + shared: MultiThreadedContainer.$shared, + container: MultiThreadedContainer(), + operation: super.invokeTest, + transform: self.transform + ) + } } diff --git a/Tests/FactoryTests/FactoryResolverTests.swift b/Tests/FactoryTests/FactoryResolverTests.swift index ce01afa8..2cbf5a05 100644 --- a/Tests/FactoryTests/FactoryResolverTests.swift +++ b/Tests/FactoryTests/FactoryResolverTests.swift @@ -2,16 +2,10 @@ import XCTest import FactoryTesting @testable import FactoryKit -final class FactoryResolverTests: XCContainerTestCase { - - fileprivate var container: ResolvingContainer! - - override func setUp() { - super.setUp() - container = ResolvingContainer() - } +final class FactoryResolverTests: XCResolvingContainerTestCase { func testBasicResolve() throws { + let container = ResolvingContainer() let service1: MyService? = container.resolve() let service2: MyService? = container.resolve() XCTAssertNotNil(service1) @@ -21,6 +15,7 @@ final class FactoryResolverTests: XCContainerTestCase { } func testResolvingScope() throws { + let container = ResolvingContainer() let service0: MyServiceType? = container.resolve() XCTAssertNil(service0) container.register { MyService() as MyServiceType } @@ -34,6 +29,7 @@ final class FactoryResolverTests: XCContainerTestCase { } func testFactoryScope() throws { + let container = ResolvingContainer() container.factory(MyService.self)? .scope(.singleton) let service1: MyService? = container.resolve() @@ -46,12 +42,16 @@ final class FactoryResolverTests: XCContainerTestCase { } -fileprivate final class ResolvingContainer: SharedContainer, AutoRegistering, Resolving { - static let shared = ResolvingContainer() - func autoRegister() { +package final class ResolvingContainer: SharedContainer, AutoRegistering, Resolving { + #if swift(>=5.5) + @TaskLocal package static var shared = ResolvingContainer() + #else + package static let shared = ResolvingContainer() + #endif + package func autoRegister() { register { MyService() } } - let manager = ContainerManager() + package let manager = ContainerManager() func someService() -> MyServiceType { self { MyService() }() @@ -73,3 +73,16 @@ fileprivate final class ResolvingContainer: SharedContainer, AutoRegistering, Re } } + +package class XCResolvingContainerTestCase: XCTestCase { + package var transform: (@Sendable (ResolvingContainer) -> Void)? + + package override func invokeTest() { + withContainer( + shared: ResolvingContainer.$shared, + container: ResolvingContainer(), + operation: super.invokeTest, + transform: self.transform + ) + } +} diff --git a/Tests/FactoryTests/FactoryScopeTests.swift b/Tests/FactoryTests/FactoryScopeTests.swift index 3c15c5cc..e2cc1057 100644 --- a/Tests/FactoryTests/FactoryScopeTests.swift +++ b/Tests/FactoryTests/FactoryScopeTests.swift @@ -359,6 +359,21 @@ final class FactoryScopeTests: XCContainerTestCase { XCTAssertTrue(service3?.id == service4?.id) // should be cached } + @available(iOS 13, *) + func testSingletonScopeTimeToLive() async throws { + Container.shared.singletonService.timeToLive(0.01) + let service1 = Container.shared.singletonService() + let service2 = Container.shared.singletonService() + XCTAssertTrue(service1.id == service2.id) + // delay + try await Task.sleep(nanoseconds: 10_100_000) + // resolution should fail ttl test and return new instance + let service3 = Container.shared.singletonService() + XCTAssertTrue(service2.id != service3.id) + } +} + +final class FactoryScopeTestsFirstSingleton: XCFirstSingletonContainerTestCase { func testSingletonSameContainerType() throws { let container1 = FirstSingletonContainer() //container1.manager.trace.toggle() @@ -371,7 +386,9 @@ final class FactoryScopeTests: XCContainerTestCase { XCTAssertTrue(service3.id == service4.id) XCTAssertTrue(service1.id == service3.id) } +} +final class FactoryScopeTestsFirstAndSecondSingleton: XCFirstAndSecondSingletonContainerTestCase { func testSingletonAcrossContainerTypes() throws { let container1 = FirstSingletonContainer() container1.manager.trace.toggle() @@ -381,20 +398,6 @@ final class FactoryScopeTests: XCContainerTestCase { XCTAssertTrue(service1.id == service2.id) container1.manager.trace.toggle() } - - @available(iOS 13, *) - func testSingletonScopeTimeToLive() async throws { - Container.shared.singletonService.timeToLive(0.01) - let service1 = Container.shared.singletonService() - let service2 = Container.shared.singletonService() - XCTAssertTrue(service1.id == service2.id) - // delay - try await Task.sleep(nanoseconds: 10_100_000) - // resolution should fail ttl test and return new instance - let service3 = Container.shared.singletonService() - XCTAssertTrue(service2.id != service3.id) - } - } extension SharedContainer { @@ -403,25 +406,66 @@ extension SharedContainer { } } -fileprivate final class FirstSingletonContainer: SharedContainer, AutoRegistering { - static let shared = FirstSingletonContainer() - func autoRegister() { +package final class FirstSingletonContainer: SharedContainer, AutoRegistering { + #if swift(>=5.5) + @TaskLocal package static var shared = FirstSingletonContainer() + #else + package static let shared = FirstSingletonContainer() + #endif + package func autoRegister() { manager.defaultScope = .singleton } var myServiceType: Factory { self { MyService() } } - let manager = ContainerManager() + package let manager = ContainerManager() +} + +package class XCFirstSingletonContainerTestCase: XCTestCase { + package var transform: (@Sendable (FirstSingletonContainer) -> Void)? + + package override func invokeTest() { + withContainer( + shared: FirstSingletonContainer.$shared, + container: FirstSingletonContainer(), + operation: super.invokeTest, + transform: self.transform + ) + } } -fileprivate final class SecondSingletonContainer: SharedContainer, AutoRegistering { - static let shared = SecondSingletonContainer() - func autoRegister() { +package final class SecondSingletonContainer: SharedContainer, AutoRegistering { + #if swift(>=5.5) + @TaskLocal package static var shared = SecondSingletonContainer() + #else + package static let shared = SecondSingletonContainer() + #endif + package func autoRegister() { manager.defaultScope = .singleton } var myServiceType: Factory { self { MyService() } } - let manager = ContainerManager() + package let manager = ContainerManager() } +package class XCFirstAndSecondSingletonContainerTestCase: XCTestCase { + package var firstTransform: (@Sendable (FirstSingletonContainer) -> Void)? + package var secondTransform: (@Sendable (SecondSingletonContainer) -> Void)? + + package override func invokeTest() { + withContainer( + shared: SecondSingletonContainer.$shared, + container: SecondSingletonContainer(), + operation: { + withContainer( + shared: FirstSingletonContainer.$shared, + container: FirstSingletonContainer(), + operation: super.invokeTest, + transform: self.firstTransform + ) + }, + transform: self.secondTransform + ) + } +} diff --git a/Tests/FactoryTests/MockServices.swift b/Tests/FactoryTests/MockServices.swift index c72ec850..8c3c4b56 100644 --- a/Tests/FactoryTests/MockServices.swift +++ b/Tests/FactoryTests/MockServices.swift @@ -194,8 +194,12 @@ extension Container { // Custom Container -final class CustomContainer: SharedContainer, AutoRegistering { - @TaskLocal static var shared = CustomContainer() +package final class CustomContainer: SharedContainer, AutoRegistering { + #if swift(>=5.5) + @TaskLocal package static var shared = CustomContainer() + #else + package static let shared = CustomContainer() + #endif nonisolated(unsafe) static var count = 0 nonisolated(unsafe) var count = 0 var test: Factory { @@ -231,7 +235,7 @@ final class CustomContainer: SharedContainer, AutoRegistering { } .once() } - func autoRegister() { + package func autoRegister() { print("CustomContainer AUTOREGISTERING") Self.count = 1 self.count = 1 @@ -244,5 +248,5 @@ final class CustomContainer: SharedContainer, AutoRegistering { } #endif } - let manager = ContainerManager() + package let manager = ContainerManager() } diff --git a/Tests/FactoryTests/ParallelServices.swift b/Tests/FactoryTests/ParallelServices.swift index 22f6584f..59a60d84 100644 --- a/Tests/FactoryTests/ParallelServices.swift +++ b/Tests/FactoryTests/ParallelServices.swift @@ -7,6 +7,7 @@ import Foundation import Testing +import XCTest @testable import FactoryKit import FactoryTesting @@ -18,6 +19,60 @@ extension Trait where Self == ContainerTrait { .init(shared: CustomContainer.$shared, container: .init()) } } + +package class XCCustomContainerTestCase: XCTestCase { + package var transform: (@Sendable (CustomContainer) -> Void)? + + package override func invokeTest() { + withContainer( + shared: CustomContainer.$shared, + container: CustomContainer(), + operation: super.invokeTest, + transform: self.transform + ) + } +} + +func withContainerAndCustomContainer( + operation: @Sendable () -> Void, + containerTransform: (@Sendable (Container) -> Void)? = nil, + customContainerTransform: (@Sendable (CustomContainer) -> Void)? = nil +) { + withContainer( + shared: CustomContainer.$shared, + container: CustomContainer(), + operation: { + withContainer( + shared: Container.$shared, + container: Container(), + operation: operation, + transform: containerTransform + ) + }, + transform: customContainerTransform + ) +} + +package class XCContainerAndCustomContainerTestCase: XCTestCase { + package var containerTransform: (@Sendable (Container) -> Void)? + package var customContainerTransform: (@Sendable (CustomContainer) -> Void)? + + package override func invokeTest() { + withContainer( + shared: CustomContainer.$shared, + container: CustomContainer(), + operation: { + withContainer( + shared: Container.$shared, + container: Container(), + operation: super.invokeTest, + transform: self.containerTransform + ) + }, + transform: self.customContainerTransform + ) + } +} #endif // Classes for @TaskLocal and TestTrait tests diff --git a/Tests/FactoryTests/ParallelXCTest.swift b/Tests/FactoryTests/ParallelXCTest.swift index a980ee62..abc288fe 100644 --- a/Tests/FactoryTests/ParallelXCTest.swift +++ b/Tests/FactoryTests/ParallelXCTest.swift @@ -4,114 +4,217 @@ import XCTest import FactoryTesting @testable import FactoryKit -final class ParallelXCTest: XCTestCase { - //TODO: maybe delete because these examples do not contain the singleton resetting logic... - func testFooBarBaz() { - let container = Container() +/// Illustrates using the regular `XCTestCase` with `FactoryTesting.withContainer` to enable parallel XCTests. +final class ParallelXCTestFoo: XCTestCase { + func testFoo() { let fooExpectation = expectation(description: "foo") - Container.$shared.withValue(container) { - Container.shared.fooBarBaz.register { Foo() } + withContainer( + shared: Container.$shared, + container: Container() + ) { + let c = Container.shared + c.fooBarBaz.register { Foo() } + c.fooBarBazCached.register { Foo() } + c.fooBarBazSingleton.register { Foo() } - let sut = TaskLocalUseCase() - XCTAssertEqual(sut.fooBarBaz.value, "foo") + commonTests("foo") fooExpectation.fulfill() } + wait(for: [fooExpectation], timeout: 60) + } +} + +final class ParallelXCTestBar: XCTestCase { + func testBar() { let barExpectation = expectation(description: "bar") - Container.$shared.withValue(container) { - Container.shared.fooBarBaz.register { Bar() } + withContainer( + shared: Container.$shared, + container: Container() + ) { + let c = Container.shared + c.fooBarBaz.register { Bar() } + c.fooBarBazCached.register { Bar() } + c.fooBarBazSingleton.register { Bar() } - let sut = TaskLocalUseCase() - XCTAssertEqual(sut.fooBarBaz.value, "bar") + commonTests("bar") barExpectation.fulfill() } + wait(for: [barExpectation], timeout: 60) + } +} + +final class ParallelXCTestBaz: XCTestCase { + func testBaz() { let bazExpectation = expectation(description: "baz") - Container.$shared.withValue(container) { - Container.shared.fooBarBaz.register { Baz() } + withContainer( + shared: Container.$shared, + container: Container() + ) { + let c = Container.shared + c.fooBarBaz.register { Baz() } + c.fooBarBazCached.register { Baz() } + c.fooBarBazSingleton.register { Baz() } - let sut = TaskLocalUseCase() - XCTAssertEqual(sut.fooBarBaz.value, "baz") + commonTests("baz") bazExpectation.fulfill() } - wait(for: [fooExpectation, barExpectation, bazExpectation], timeout: 60) + wait(for: [bazExpectation], timeout: 60) } +} - // Illustrates using the withContainer() helper with a synchronous transform closure - func testFooBarBazWithContainer() { +// Illustrates using the withContainer() helper with a synchronous transform closure +final class ParallelXCTestFooWithContainerAndTransform: XCTestCase { + func testFooWithContainerSyncTransform() { let fooExpectation = expectation(description: "foo") - FactoryTestingHelper.withContainer() { - let sut = TaskLocalUseCase() - XCTAssertEqual(sut.fooBarBaz.value, "foo") + withContainer( + shared: Container.$shared, + container: Container() + ) { + commonTests("foo") fooExpectation.fulfill() } transform: { $0.fooBarBaz.register { Foo() } + $0.fooBarBazCached.register { Foo() } + $0.fooBarBazSingleton.register { Foo() } } + wait(for: [fooExpectation], timeout: 60) + } +} + +// Illustrates using the withContainer() helper with a synchronous transform closure +final class ParallelXCTestBarWithContainerAndTransform: XCTestCase { + func testBarWithContainerSyncTransform() { let barExpectation = expectation(description: "bar") - FactoryTestingHelper.withContainer { - let sut = TaskLocalUseCase() - XCTAssertEqual(sut.fooBarBaz.value, "bar") + withContainer( + shared: Container.$shared, + container: Container() + ) { + commonTests("bar") barExpectation.fulfill() } transform: { $0.fooBarBaz.register { Bar() } + $0.fooBarBazCached.register { Bar() } + $0.fooBarBazSingleton.register { Bar() } } + wait(for: [barExpectation], timeout: 60) + } +} + +// Illustrates using the withContainer() helper with a synchronous transform closure +final class ParallelXCTestBazWithContainerAndTransform: XCTestCase { + func testBazWithContainerSyncTransform() { let bazExpectation = expectation(description: "baz") - FactoryTestingHelper.withContainer { - let sut = TaskLocalUseCase() - XCTAssertEqual(sut.fooBarBaz.value, "baz") + withContainer( + shared: Container.$shared, + container: Container() + ) { + commonTests("baz") bazExpectation.fulfill() } transform: { $0.fooBarBaz.register { Baz() } + $0.fooBarBazCached.register { Baz() } + $0.fooBarBazSingleton.register { Baz() } } - wait(for: [fooExpectation, barExpectation, bazExpectation], timeout: 60) + wait(for: [bazExpectation], timeout: 60) } +} - // Illustrates using the withContainer() helper with an asynchronous transform closure - func testFooBarBazWithContainerAsync() async { +// Illustrates using the withContainer() helper asynchronously +final class ParallelXCTestFooWithContainerAndAsyncTransform: XCTestCase { + func testFooWithContainerAsync() async { let fooExpectation = expectation(description: "foo") - await FactoryTestingHelper.withContainer() { - let sut = await IsolatedTaskLocalUseCase() - let value = await sut.isolatedToMainActor.value - XCTAssertEqual(value, "foo") + await withContainer( + shared: Container.$shared, + container: Container() + ) { + Container.shared.fooBarBaz.register { Foo() } + Container.shared.fooBarBazCached.register { Foo() } + Container.shared.fooBarBazSingleton.register { Foo() } + + await Container.shared.isolatedToMainActor.register { @MainActor in MainActorFooBarBaz(value: "foo") } + await Container.shared.isolatedToMainActorCached.register { @MainActor in MainActorFooBarBaz(value: "foo") } + await Container.shared.isolatedToMainActorSingleton.register { @MainActor in MainActorFooBarBaz(value: "foo") } + + await Container.shared.isolatedToCustomGlobalActor.register { IsolatedFoo() } + await Container.shared.isolatedToCustomGlobalActorCached.register { IsolatedFoo() } + await Container.shared.isolatedToCustomGlobalActorSingleton.register { IsolatedFoo() } + + await isolatedAsyncTests("foo") fooExpectation.fulfill() - } transform: { - await $0.isolatedToMainActor.register { @MainActor in MainActorFooBarBaz(value: "foo") } } + await fulfillment(of: [fooExpectation], timeout: 60) + } +} + +// Illustrates using the withContainer() helper with an asynchronous transform closure +final class ParallelXCTestBarWithContainerAndAsyncTransform: XCTestCase { + func testBarWithContainerAsyncTransform() async { let barExpectation = expectation(description: "bar") - await FactoryTestingHelper.withContainer { - let sut = await IsolatedTaskLocalUseCase() - let value = await sut.isolatedToMainActor.value - XCTAssertEqual(value, "bar") + await withContainer( + shared: Container.$shared, + container: Container() + ) { + await isolatedAsyncTests("bar") barExpectation.fulfill() } transform: { + $0.fooBarBaz.register { Bar() } + $0.fooBarBazCached.register { Bar() } + $0.fooBarBazSingleton.register { Bar() } + await $0.isolatedToMainActor.register { @MainActor in MainActorFooBarBaz(value: "bar") } + await $0.isolatedToMainActorCached.register { @MainActor in MainActorFooBarBaz(value: "bar") } + await $0.isolatedToMainActorSingleton.register { @MainActor in MainActorFooBarBaz(value: "bar") } + + await $0.isolatedToCustomGlobalActor.register { IsolatedBar() } + await $0.isolatedToCustomGlobalActorCached.register { IsolatedBar() } + await $0.isolatedToCustomGlobalActorSingleton.register { IsolatedBar() } } + await fulfillment(of: [barExpectation], timeout: 60) + } +} + +// Illustrates using the withContainer() helper with an asynchronous transform closure +final class ParallelXCTestBazWithContainerAndAsyncTransform: XCTestCase { + func testBazWithContainerAsyncTransform() async { let bazExpectation = expectation(description: "baz") - await FactoryTestingHelper.withContainer { - let sut = await IsolatedTaskLocalUseCase() - let value = await sut.isolatedToMainActor.value - XCTAssertEqual(value, "baz") + await withContainer( + shared: Container.$shared, + container: Container() + ) { + await isolatedAsyncTests("baz") bazExpectation.fulfill() } transform: { + $0.fooBarBaz.register { Baz() } + $0.fooBarBazCached.register { Baz() } + $0.fooBarBazSingleton.register { Baz() } + await $0.isolatedToMainActor.register { @MainActor in MainActorFooBarBaz(value: "baz") } + await $0.isolatedToMainActorCached.register { @MainActor in MainActorFooBarBaz(value: "baz") } + await $0.isolatedToMainActorSingleton.register { @MainActor in MainActorFooBarBaz(value: "baz") } + + await $0.isolatedToCustomGlobalActor.register { IsolatedBaz() } + await $0.isolatedToCustomGlobalActorCached.register { IsolatedBaz() } + await $0.isolatedToCustomGlobalActorSingleton.register { IsolatedBaz() } } - await fulfillment(of: [fooExpectation, barExpectation, bazExpectation], timeout: 60) + await fulfillment(of: [bazExpectation], timeout: 60) } } @@ -224,6 +327,53 @@ final class ParallelIsolatedXCTestsBaz: XCContainerTestCase { } } +final class ParallelCustomContainerTest: XCCustomContainerTestCase { + func testCustomContainer() { + let sut1 = CustomContainer.shared.myServiceType() + XCTAssertEqual(sut1.text(), "MyService") + CustomContainer.shared.myServiceType.register { MockService() } + let sut2 = CustomContainer.shared.myServiceType() + XCTAssertEqual(sut2.text(), "MockService") + } +} + +/// Illustrates using multiple containers with a custom withContainer variant +final class ParallelWithContainerAndCustomContainerTest: XCTestCase { + func testContainerAndCustomContainer() { + withContainerAndCustomContainer { + commonTests("baz") + + let sut1 = CustomContainer.shared.myServiceType() + XCTAssertEqual(sut1.text(), "MyService") + CustomContainer.shared.myServiceType.register { MockService() } + let sut2 = CustomContainer.shared.myServiceType() + XCTAssertEqual(sut2.text(), "MockService") + } containerTransform: { + $0.fooBarBaz.register { Baz() } + $0.fooBarBazCached.register { Baz() } + $0.fooBarBazSingleton.register { Baz() } + } + } +} + +/// Illustrates using multiple containers with a custom XCContainerAndCustomContainerTestCase +final class ParallelContainerAndCustomContainerTest: XCContainerAndCustomContainerTestCase { + func testContainerAndCustomContainer() { + let c = Container.shared + c.fooBarBaz.register { Baz() } + c.fooBarBazCached.register { Baz() } + c.fooBarBazSingleton.register { Baz() } + + commonTests("baz") + + let sut1 = CustomContainer.shared.myServiceType() + XCTAssertEqual(sut1.text(), "MyService") + CustomContainer.shared.myServiceType.register { MockService() } + let sut2 = CustomContainer.shared.myServiceType() + XCTAssertEqual(sut2.text(), "MockService") + } +} + private func commonTests(_ value: String) { let sut1 = TaskLocalUseCase() XCTAssertEqual(sut1.fooBarBaz.value, value) From e7fdf3587fa5354ef5ae0e5458c38f9a5c756d31 Mon Sep 17 00:00:00 2001 From: Akos Grabecz Date: Mon, 2 Jun 2025 16:57:40 +0200 Subject: [PATCH 7/7] add XCCachedContainerTestCase --- Tests/FactoryTests/FactoryScopeTests.swift | 46 ++++++++++++++-------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/Tests/FactoryTests/FactoryScopeTests.swift b/Tests/FactoryTests/FactoryScopeTests.swift index 129d698c..fac32b7c 100644 --- a/Tests/FactoryTests/FactoryScopeTests.swift +++ b/Tests/FactoryTests/FactoryScopeTests.swift @@ -371,6 +371,7 @@ final class FactoryScopeTests: XCContainerTestCase { let service3 = Container.shared.singletonService() XCTAssertTrue(service2.id != service3.id) } + } final class FactoryScopeTestsFirstSingleton: XCFirstSingletonContainerTestCase { @@ -389,6 +390,7 @@ final class FactoryScopeTestsFirstSingleton: XCFirstSingletonContainerTestCase { } final class FactoryScopeTestsFirstAndSecondSingleton: XCFirstAndSecondSingletonContainerTestCase { + func testSingletonAcrossContainerTypes() throws { let container1 = FirstSingletonContainer() container1.manager.trace.toggle() @@ -399,19 +401,10 @@ final class FactoryScopeTestsFirstAndSecondSingleton: XCFirstAndSecondSingletonC container1.manager.trace.toggle() } - @available(iOS 13, *) - func testSingletonScopeTimeToLive() async throws { - Container.shared.singletonService.timeToLive(0.01) - let service1 = Container.shared.singletonService() - let service2 = Container.shared.singletonService() - XCTAssertTrue(service1.id == service2.id) - // delay - try await Task.sleep(nanoseconds: 10_100_000) - // resolution should fail ttl test and return new instance - let service3 = Container.shared.singletonService() - XCTAssertTrue(service2.id != service3.id) - } - +} + +final class FactoryScopeTestsCachedContainer: XCCachedContainerTestCase { + func testUniqueResolutionOnCachedContainer() throws { let service1 = CachedContainer.shared.uniqueService() let service2 = CachedContainer.shared.uniqueService() @@ -491,13 +484,32 @@ package class XCFirstAndSecondSingletonContainerTestCase: XCTestCase { } } -fileprivate final class CachedContainer: SharedContainer, AutoRegistering { - static let shared: CachedContainer = CachedContainer() - let manager: ContainerManager = ContainerManager() - func autoRegister() { +package final class CachedContainer: SharedContainer, AutoRegistering { + #if swift(>=5.5) + @TaskLocal package static var shared: CachedContainer = CachedContainer() + #else + package static let shared = CachedContainer() + #endif + + package let manager: ContainerManager = ContainerManager() + package func autoRegister() { manager.defaultScope = .cached } var uniqueService: Factory { self { MyService() }.unique } } + +package class XCCachedContainerTestCase: XCTestCase { + package var transform: (@Sendable (CachedContainer) -> Void)? + + package override func invokeTest() { + withContainer( + shared: CachedContainer.$shared, + container: CachedContainer(), + operation: super.invokeTest, + transform: self.transform + ) + } + +}