diff --git a/Dependencies.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Dependencies.xcworkspace/xcshareddata/swiftpm/Package.resolved index d8f73cae..08275e55 100644 --- a/Dependencies.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Dependencies.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,4 @@ { - "originHash" : "b78e1fa69770050e204024326b4d77e8451007e7f7ecc1b640d5889ffbe0b3a7", "pins" : [ { "identity" : "combine-schedulers", @@ -15,8 +14,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-clocks", "state" : { - "revision" : "3581e280bf0d90c3fb9236fb23e75a5d8c46b533", - "version" : "1.0.4" + "revision" : "b9b24b69e2adda099a1fa381cda1eeec272d5b53", + "version" : "1.0.5" } }, { @@ -33,14 +32,14 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-docc-plugin", "state" : { - "revision" : "26ac5758409154cc448d7ab82389c520fa8a8247", - "version" : "1.3.0" + "revision" : "2eb22993b3dfd0c0d32729b357c8dabb6cd44680", + "version" : "1.4.2" } }, { "identity" : "swift-docc-symbolkit", "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-docc-symbolkit", + "location" : "https://github.com/swiftlang/swift-docc-symbolkit", "state" : { "revision" : "b45d1f2ed151d057b54504d653e0da5552844e34", "version" : "1.0.0" @@ -51,8 +50,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-macro-testing", "state" : { - "revision" : "a35257b7e9ce44e92636447003a8eeefb77b145c", - "version" : "0.5.1" + "revision" : "20c1a8f3b624fb5d1503eadcaa84743050c350f4", + "version" : "0.5.2" } }, { @@ -60,8 +59,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-snapshot-testing", "state" : { - "revision" : "c097f955b4e724690f0fc8ffb7a6d4b881c9c4e3", - "version" : "1.17.2" + "revision" : "6d932a79e7173b275b96c600c86c603cf84f153c", + "version" : "1.17.4" } }, { @@ -69,8 +68,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/swiftlang/swift-syntax", "state" : { - "revision" : "4c6cc0a3b9e8f14b3ae2307c5ccae4de6167ac2c", - "version" : "600.0.0-prerelease-2024-06-12" + "revision" : "515f79b522918f83483068d99c68daeb5116342d", + "version" : "600.0.0-prerelease-2024-09-04" } }, { @@ -78,10 +77,10 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", "state" : { - "revision" : "357ca1e5dd31f613a1d43320870ebc219386a495", - "version" : "1.2.2" + "revision" : "3fcc3f21695ad5bb889a024b1b046d61bebb1ef3", + "version" : "1.3.0" } } ], - "version" : 3 + "version" : 2 } diff --git a/Package.resolved b/Package.resolved index 1e6cdaa7..c9e7e594 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,4 +1,5 @@ { + "originHash" : "583c00d70f39319a7eca67f614e30ccaab233ad9a104a40007e982cf4584d4d6", "pins" : [ { "identity" : "combine-schedulers", @@ -14,8 +15,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-clocks", "state" : { - "revision" : "3581e280bf0d90c3fb9236fb23e75a5d8c46b533", - "version" : "1.0.4" + "revision" : "b9b24b69e2adda099a1fa381cda1eeec272d5b53", + "version" : "1.0.5" } }, { @@ -32,14 +33,14 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-docc-plugin", "state" : { - "revision" : "26ac5758409154cc448d7ab82389c520fa8a8247", - "version" : "1.3.0" + "revision" : "2eb22993b3dfd0c0d32729b357c8dabb6cd44680", + "version" : "1.4.2" } }, { "identity" : "swift-docc-symbolkit", "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-docc-symbolkit", + "location" : "https://github.com/swiftlang/swift-docc-symbolkit", "state" : { "revision" : "b45d1f2ed151d057b54504d653e0da5552844e34", "version" : "1.0.0" @@ -50,8 +51,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-macro-testing", "state" : { - "revision" : "a35257b7e9ce44e92636447003a8eeefb77b145c", - "version" : "0.5.1" + "revision" : "20c1a8f3b624fb5d1503eadcaa84743050c350f4", + "version" : "0.5.2" } }, { @@ -59,8 +60,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-snapshot-testing", "state" : { - "revision" : "c097f955b4e724690f0fc8ffb7a6d4b881c9c4e3", - "version" : "1.17.2" + "revision" : "6d932a79e7173b275b96c600c86c603cf84f153c", + "version" : "1.17.4" } }, { @@ -68,8 +69,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/swiftlang/swift-syntax", "state" : { - "revision" : "4c6cc0a3b9e8f14b3ae2307c5ccae4de6167ac2c", - "version" : "600.0.0-prerelease-2024-06-12" + "revision" : "515f79b522918f83483068d99c68daeb5116342d", + "version" : "600.0.0-prerelease-2024-09-04" } }, { @@ -77,10 +78,10 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", "state" : { - "revision" : "357ca1e5dd31f613a1d43320870ebc219386a495", - "version" : "1.2.2" + "branch" : "test-case-parameterization", + "revision" : "be48dda989581f65f82e09041b11e12da837c49d" } } ], - "version" : 2 + "version" : 3 } diff --git a/Package.swift b/Package.swift index 9e185ebb..0b892447 100644 --- a/Package.swift +++ b/Package.swift @@ -25,7 +25,7 @@ let package = Package( .package(url: "https://github.com/pointfreeco/combine-schedulers", from: "1.0.2"), .package(url: "https://github.com/pointfreeco/swift-clocks", from: "1.0.4"), .package(url: "https://github.com/pointfreeco/swift-concurrency-extras", from: "1.0.0"), - .package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "1.2.2"), + .package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "1.3.0"), .package(url: "https://github.com/swiftlang/swift-syntax", "509.0.0"..<"601.0.0-prerelease"), ], targets: [ diff --git a/Package@swift-6.0.swift b/Package@swift-6.0.swift index 96dd6664..a63315c3 100644 --- a/Package@swift-6.0.swift +++ b/Package@swift-6.0.swift @@ -25,7 +25,7 @@ let package = Package( .package(url: "https://github.com/pointfreeco/combine-schedulers", from: "1.0.2"), .package(url: "https://github.com/pointfreeco/swift-clocks", from: "1.0.4"), .package(url: "https://github.com/pointfreeco/swift-concurrency-extras", from: "1.0.0"), - .package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "1.2.2"), + .package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "1.3.0"), .package(url: "https://github.com/swiftlang/swift-syntax", "509.0.0"..<"601.0.0-prerelease"), ], targets: [ diff --git a/Sources/Dependencies/DependencyValues.swift b/Sources/Dependencies/DependencyValues.swift index 9c9614da..5f46beec 100644 --- a/Sources/Dependencies/DependencyValues.swift +++ b/Sources/Dependencies/DependencyValues.swift @@ -302,6 +302,10 @@ public struct DependencyValues: Sendable { } @_spi(Beta) + @available( + *, deprecated, + message: "'resetCache' is no longer necessary for most (unparameterized) '@Test' cases" + ) public func resetCache() { cachedValues.cached = [:] } @@ -353,8 +357,20 @@ private let defaultContext: DependencyContext = { @_spi(Internals) public final class CachedValues: @unchecked Sendable { public struct CacheKey: Hashable, Sendable { - let id: ObjectIdentifier + let id: TypeIdentifier let context: DependencyContext + let testIdentifier: TestContext.Testing.Test.ID? + + init(id: TypeIdentifier, context: DependencyContext) { + self.id = id + self.context = context + switch TestContext.current { + case let .swiftTesting(.some(testing)): + self.testIdentifier = testing.test.id + default: + self.testIdentifier = nil + } + } } private let lock = NSRecursiveLock() @@ -373,7 +389,7 @@ public final class CachedValues: @unchecked Sendable { defer { lock.unlock() } return withIssueContext(fileID: fileID, filePath: filePath, line: line, column: column) { - let cacheKey = CacheKey(id: ObjectIdentifier(key), context: context) + let cacheKey = CacheKey(id: TypeIdentifier(key), context: context) guard let base = cached[cacheKey], let value = base as? Key.Value else { let value: Key.Value? @@ -461,3 +477,25 @@ public final class CachedValues: @unchecked Sendable { } } } + +struct TypeIdentifier: Hashable { + let id: ObjectIdentifier + #if DEBUG + let base: Any.Type + #endif + + init(_ type: T.Type) { + self.id = ObjectIdentifier(type) + #if DEBUG + self.base = type + #endif + } + + static func == (lhs: Self, rhs: Self) -> Bool { + lhs.id == rhs.id + } + + func hash(into hasher: inout Hasher) { + hasher.combine(id) + } +} diff --git a/Sources/Dependencies/Documentation.docc/Articles/Testing.md b/Sources/Dependencies/Documentation.docc/Articles/Testing.md index 9dcf257d..6006a457 100644 --- a/Sources/Dependencies/Documentation.docc/Articles/Testing.md +++ b/Sources/Dependencies/Documentation.docc/Articles/Testing.md @@ -23,7 +23,7 @@ how your feature deals with data returned from an API, and your feature doesn't with the file system just to test how data gets loaded or persisted. The tool for doing this is ``withDependencies(_:operation:)-3vrqy``, which allows you to specify -which dependencies should be overriden for the test, and then construct your feature's model +which dependencies should be overridden for the test, and then construct your feature's model in that context: ```swift @@ -237,47 +237,23 @@ to ever have a static dependency, and so you should avoid this pattern. ## Swift's native Testing framework -The library comes with beta support for Swift's new native Testing framework. However, as there -are still features missing from the Testing framework that XCTest has, there are some additional +The library comes with support for Swift's new native Testing framework. However, as there are still +still features missing from the Testing framework that XCTest has, there may be some additional steps you must take. -> Warning: Currently our support of the Swift Testing framework is considered "beta" because Swift's -> own testing framework has not even officially been released yet. Once it is officially released, -> probably sometime in September, we will have an official release of our libraries with support. - -If you are are writing a test using the `@Test` macro, you will need to surround the entire body -of your test in [`withDependencies`]() that resets -the entire set of values: +If you are are writing a _parameterized_ test using the `@Test` macro, you will need to surround the +entire body of your test in [`withDependencies`]() that +resets the entire set of values to guarantee that a fresh set of dependencies is used per parameter: ```swift -@Test -func feature() { +@Test(arguments: [1, 2, 3]) +func feature(_ number: Int) { withDependencies { $0 = DependencyValues() } operation: { - // All test code in here… + // All test code in here... } } ``` -This will guarantee that tests do not bleed over to other tests when run in parallel. - -Alternatively, you can create a class-based `@Suite` that runs in serial _and_ resets the -dependency case after each test is run. To do so you will need to `@_spi` import the -Dependencies library to get access to a `resetCache` method: - -```swift -@_spi(Beta) import Dependencies - -@Suite(.serialized) -class FeatureTests { - deinit { - DependencyValues._current.resetCache() - } - - @Test - func feature() { - // All test code in here… - } -} -``` +This will guarantee that dependency state does not bleed over to each parameter of the test. diff --git a/Tests/DependenciesTests/SwiftTestingTests.swift b/Tests/DependenciesTests/SwiftTestingTests.swift index f9562855..fedb394b 100644 --- a/Tests/DependenciesTests/SwiftTestingTests.swift +++ b/Tests/DependenciesTests/SwiftTestingTests.swift @@ -3,31 +3,57 @@ import Testing struct SwiftTestingTests { - @Test - func cachePollution1() async { - await withDependencies { - $0 = DependencyValues() - } operation: { - @Dependency(\.cachedDependency) var cachedDependency: CachedDependency - let value = await cachedDependency.increment() + @Test(.serialized, arguments: 1...5) + func parameterizedCachePollution(_ argument: Int) { + @Dependency(Client.self) var client + let value = client.increment() + if argument == 1 { #expect(value == 1) + } else { + withKnownIssue { + #expect(value == 1) + } } } - @Test - func cachePollution2() async { - await withDependencies { + @Test(arguments: 1...5) + func parameterizedCachePollution_ResetDependencies(_ argument: Int) { + withDependencies { $0 = DependencyValues() } operation: { - @Dependency(\.cachedDependency) var cachedDependency: CachedDependency - let value = await cachedDependency.increment() - // NB: Wasm has different behavior here. - #if os(WASI) - #expect(value == 2) - #else - #expect(value == 1) - #endif + @Dependency(Client.self) var client + let value = client.increment() + #expect(value == 1) } } + + @Test + func cachePollution1() { + @Dependency(Client.self) var client + let value = client.increment() + #expect(value == 1) + } + + @Test + func cachePollution2() { + @Dependency(Client.self) var client + let value = client.increment() + // NB: Wasm has different behavior here. + #if os(WASI) + #expect(value == 2) + #else + #expect(value == 1) + #endif + } + } + +private struct Client: TestDependencyKey { + var increment: @Sendable () -> Int + static var testValue: Client { + let count = LockIsolated(0) + return Self { + count.withValue { $0 += 1; return $0 } + } } +} #endif