From f1a44b0fb7a15e5505608d7fabf8a64e083e0e48 Mon Sep 17 00:00:00 2001 From: John Flanagan Date: Wed, 16 Oct 2024 09:57:39 -0500 Subject: [PATCH 1/2] Override Test ID to ensure trait dependencies are cached --- Sources/Dependencies/DependencyValues.swift | 37 +++++++++++++--- .../DependenciesTestSupport/TestTrait.swift | 8 ++-- .../DependenciesTests/SwiftTestingTests.swift | 42 +++++++++++++++++++ 3 files changed, 78 insertions(+), 9 deletions(-) diff --git a/Sources/Dependencies/DependencyValues.swift b/Sources/Dependencies/DependencyValues.swift index 5774ba65..241a9536 100644 --- a/Sources/Dependencies/DependencyValues.swift +++ b/Sources/Dependencies/DependencyValues.swift @@ -361,6 +361,31 @@ private let defaultContext: DependencyContext = { } }() +@_spi(Internals) +public struct TestID: @unchecked Sendable { + @TaskLocal private static var override: Self? + + public let value: AnyHashable + + public init(_ value: AnyHashable) { + self.value = value + } + + public static func withCurrent(_ testID: AnyHashable, perform body: () async throws -> R) async rethrows -> R { + try await Self.$override.withValue(TestID(testID), operation: body) + } + + public static var current: Self? { + if let value = override { return value } + + if case let .swiftTesting(.some(testing)) = TestContext.current { + return .init(testing.test.id.rawValue) + } + + return nil + } +} + @_spi(Internals) public final class CachedValues: @unchecked Sendable { @TaskLocal static var isAccessingCachedDependencies = false @@ -373,10 +398,10 @@ public final class CachedValues: @unchecked Sendable { 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: + if let testID = TestID.current { + // unsafeBitCast used because current version of xctest-dynamic-overlay does not expose init + self.testIdentifier = unsafeBitCast(testID, to: TestContext.Testing.Test.ID.self) + } else { self.testIdentifier = nil } } @@ -415,8 +440,8 @@ public final class CachedValues: @unchecked Sendable { } case .test: if !CachedValues.isAccessingCachedDependencies, - case let .swiftTesting(.some(testing)) = TestContext.current, - let testValues = testValuesByTestID.withValue({ $0[testing.test.id.rawValue] }) + let testID = TestID.current, + let testValues = testValuesByTestID.withValue({ $0[testID.value] }) { value = CachedValues.$isAccessingCachedDependencies.withValue(true) { testValues[key] diff --git a/Sources/DependenciesTestSupport/TestTrait.swift b/Sources/DependenciesTestSupport/TestTrait.swift index b5a3f4a7..e28e723e 100644 --- a/Sources/DependenciesTestSupport/TestTrait.swift +++ b/Sources/DependenciesTestSupport/TestTrait.swift @@ -1,5 +1,5 @@ #if canImport(Testing) - import Dependencies + @_spi(Internals) import Dependencies import Testing extension Trait where Self == _DependenciesTrait { @@ -59,8 +59,10 @@ public var isRecursive: Bool { true } public func prepare(for test: Test) async throws { - testValuesByTestID.withValue { - self.updateValues(&$0[test.id, default: DependencyValues(context: .test)]) + await TestID.withCurrent(test.id) { + testValuesByTestID.withValue { values in + self.updateValues(&values[test.id, default: DependencyValues(context: .test)]) + } } } } diff --git a/Tests/DependenciesTests/SwiftTestingTests.swift b/Tests/DependenciesTests/SwiftTestingTests.swift index f4f2b8d8..dba34744 100644 --- a/Tests/DependenciesTests/SwiftTestingTests.swift +++ b/Tests/DependenciesTests/SwiftTestingTests.swift @@ -48,6 +48,24 @@ #endif } + static let updateValues: @Sendable (inout DependencyValues) -> Void = { + $0[ValueProvidingKey.self].setValue(5) + } + + @Test(.dependencies(updateValues)) + func cachedTraitUpdate() { + @Dependency(ValueProvidingKey.self) var provider + + #expect(provider.value == 5, "Updates in made in .dependencies closure update cached dependency") + } + + @Test + func cacheIsolatedBetweenTests() { + @Dependency(ValueProvidingKey.self) var provider + + #expect(provider.value == 0) + } + @Test(.dependency(\.date.now, Date(timeIntervalSinceReferenceDate: 0))) func trait() { @Dependency(\.date.now) var now @@ -92,4 +110,28 @@ } } } + + private protocol ValueProviding: Sendable { + var value: Int { get } + + func setValue(_ value: Int) + } + + private final class ValueProvidingMock: ValueProviding { + let _value: LockIsolated + + var value: Int { _value.value } + + init(value: Int) { + _value = .init(value) + } + + func setValue(_ value: Int) { + _value.setValue(value) + } + } + + private enum ValueProvidingKey: TestDependencyKey { + static var testValue: ValueProviding { ValueProvidingMock(value: 0) } + } #endif From d5a9004f64b2df7a5753420f250864f91a3c683e Mon Sep 17 00:00:00 2001 From: John Flanagan Date: Wed, 30 Oct 2024 10:51:25 -0500 Subject: [PATCH 2/2] Use swift-issue-reporting PR --- Package.resolved | 8 ++-- Package@swift-6.0.swift | 2 +- Sources/Dependencies/DependencyValues.swift | 37 +++---------------- .../DependenciesTestSupport/TestTrait.swift | 4 +- 4 files changed, 13 insertions(+), 38 deletions(-) diff --git a/Package.resolved b/Package.resolved index 8f5d4c31..0fb0d955 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "ac879199bc109c96e02f389573ce5b101fa5c8a274b809fc57dba0d4736f5b6f", + "originHash" : "dc8209f1497546c820fac828af8d595b6882cb88310f412367280d8cde593506", "pins" : [ { "identity" : "combine-schedulers", @@ -76,10 +76,10 @@ { "identity" : "xctest-dynamic-overlay", "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", + "location" : "https://github.com/jflan-dd/xctest-dynamic-overlay", "state" : { - "revision" : "27d767d643fa2cf083d0a73d74fa84cacb53e85c", - "version" : "1.4.1" + "branch" : "jflan/override-test-id", + "revision" : "09a97f20368a8c47b1fd86c4be9beb49585414ea" } } ], diff --git a/Package@swift-6.0.swift b/Package@swift-6.0.swift index 25dfe592..07cada9a 100644 --- a/Package@swift-6.0.swift +++ b/Package@swift-6.0.swift @@ -29,7 +29,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.4.0"), + .package(url: "https://github.com/jflan-dd/xctest-dynamic-overlay", branch: "jflan/override-test-id"), .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 241a9536..5774ba65 100644 --- a/Sources/Dependencies/DependencyValues.swift +++ b/Sources/Dependencies/DependencyValues.swift @@ -361,31 +361,6 @@ private let defaultContext: DependencyContext = { } }() -@_spi(Internals) -public struct TestID: @unchecked Sendable { - @TaskLocal private static var override: Self? - - public let value: AnyHashable - - public init(_ value: AnyHashable) { - self.value = value - } - - public static func withCurrent(_ testID: AnyHashable, perform body: () async throws -> R) async rethrows -> R { - try await Self.$override.withValue(TestID(testID), operation: body) - } - - public static var current: Self? { - if let value = override { return value } - - if case let .swiftTesting(.some(testing)) = TestContext.current { - return .init(testing.test.id.rawValue) - } - - return nil - } -} - @_spi(Internals) public final class CachedValues: @unchecked Sendable { @TaskLocal static var isAccessingCachedDependencies = false @@ -398,10 +373,10 @@ public final class CachedValues: @unchecked Sendable { init(id: TypeIdentifier, context: DependencyContext) { self.id = id self.context = context - if let testID = TestID.current { - // unsafeBitCast used because current version of xctest-dynamic-overlay does not expose init - self.testIdentifier = unsafeBitCast(testID, to: TestContext.Testing.Test.ID.self) - } else { + switch TestContext.current { + case let .swiftTesting(.some(testing)): + self.testIdentifier = testing.test.id + default: self.testIdentifier = nil } } @@ -440,8 +415,8 @@ public final class CachedValues: @unchecked Sendable { } case .test: if !CachedValues.isAccessingCachedDependencies, - let testID = TestID.current, - let testValues = testValuesByTestID.withValue({ $0[testID.value] }) + case let .swiftTesting(.some(testing)) = TestContext.current, + let testValues = testValuesByTestID.withValue({ $0[testing.test.id.rawValue] }) { value = CachedValues.$isAccessingCachedDependencies.withValue(true) { testValues[key] diff --git a/Sources/DependenciesTestSupport/TestTrait.swift b/Sources/DependenciesTestSupport/TestTrait.swift index e28e723e..74c66eb8 100644 --- a/Sources/DependenciesTestSupport/TestTrait.swift +++ b/Sources/DependenciesTestSupport/TestTrait.swift @@ -1,5 +1,5 @@ #if canImport(Testing) - @_spi(Internals) import Dependencies + import Dependencies import Testing extension Trait where Self == _DependenciesTrait { @@ -59,7 +59,7 @@ public var isRecursive: Bool { true } public func prepare(for test: Test) async throws { - await TestID.withCurrent(test.id) { + TestContext.withTestID(test.id) { testValuesByTestID.withValue { values in self.updateValues(&values[test.id, default: DependencyValues(context: .test)]) }