From 69c53c47701b4fb1b33c47950667f7717e0990d7 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Wed, 18 Feb 2026 19:38:17 -0800 Subject: [PATCH] wip --- .github/workflows/ci.yml | 2 +- Package.swift | 2 +- Package@swift-5.9.swift | 113 ----- Sources/Dependencies/Dependency.swift | 10 +- Sources/Dependencies/DependencyValues.swift | 15 +- .../Documentation.docc/Articles/Testing.md | 26 -- .../Dependencies/Internal/Deprecations.swift | 2 +- .../Internal/SendableKeyPath.swift | 5 - Sources/Dependencies/Traits/TestTrait.swift | 3 - Sources/Dependencies/WithDependencies.swift | 411 +++++++----------- .../DependenciesTestSupport/TestTrait.swift | 333 +++++--------- .../DependencyValuesTests.swift | 3 +- .../PrepareDependenciesTests.swift | 36 +- .../RootResettingTests.swift | 2 +- .../DependenciesTests/SwiftTestingTests.swift | 37 +- 15 files changed, 301 insertions(+), 699 deletions(-) delete mode 100644 Package@swift-5.9.swift delete mode 100644 Sources/Dependencies/Internal/SendableKeyPath.swift delete mode 100644 Sources/Dependencies/Traits/TestTrait.swift diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 559a7502..a9006962 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,7 +38,7 @@ jobs: strategy: matrix: swift: - - '6.0' + - '6.1' name: Ubuntu (Swift ${{ matrix.swift }}) runs-on: ubuntu-latest container: swift:${{ matrix.swift }} diff --git a/Package.swift b/Package.swift index a21428d0..7fc650c4 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 6.0 +// swift-tools-version: 6.1 import CompilerPluginSupport import PackageDescription diff --git a/Package@swift-5.9.swift b/Package@swift-5.9.swift deleted file mode 100644 index 9234098d..00000000 --- a/Package@swift-5.9.swift +++ /dev/null @@ -1,113 +0,0 @@ -// swift-tools-version: 5.9 - -import CompilerPluginSupport -import PackageDescription - -let package = Package( - name: "swift-dependencies", - platforms: [ - .iOS(.v13), - .macOS(.v10_15), - .tvOS(.v13), - .watchOS(.v6), - ], - products: [ - .library( - name: "Dependencies", - targets: ["Dependencies"] - ), - .library( - name: "DependenciesMacros", - targets: ["DependenciesMacros"] - ), - ], - dependencies: [ - .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/swiftlang/swift-syntax", "509.0.0"..<"603.0.0"), - ], - targets: [ - .target( - name: "DependenciesTestObserver", - dependencies: [ - .product(name: "IssueReporting", package: "xctest-dynamic-overlay") - ] - ), - .target( - name: "Dependencies", - dependencies: [ - .product(name: "Clocks", package: "swift-clocks"), - .product(name: "CombineSchedulers", package: "combine-schedulers"), - .product(name: "ConcurrencyExtras", package: "swift-concurrency-extras"), - .product(name: "IssueReporting", package: "xctest-dynamic-overlay"), - .product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay"), - ] - ), - .testTarget( - name: "DependenciesTests", - dependencies: [ - "Dependencies", - "DependenciesMacros", - .product(name: "ConcurrencyExtras", package: "swift-concurrency-extras"), - .product(name: "IssueReportingTestSupport", package: "xctest-dynamic-overlay"), - ] - ), - .target( - name: "DependenciesMacros", - dependencies: [ - "DependenciesMacrosPlugin", - .product(name: "IssueReporting", package: "xctest-dynamic-overlay"), - .product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay"), - ] - ), - .macro( - name: "DependenciesMacrosPlugin", - dependencies: [ - .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), - .product(name: "SwiftCompilerPlugin", package: "swift-syntax"), - ] - ), - ] -) - -#if !os(macOS) && !os(WASI) - package.products.append( - .library( - name: "DependenciesTestObserver", - type: .dynamic, - targets: ["DependenciesTestObserver"] - ) - ) -#endif - -#if !os(WASI) - package.dependencies.append(contentsOf: [ - .package(url: "https://github.com/pointfreeco/swift-macro-testing", from: "0.2.0") - ]) - package.targets.append(contentsOf: [ - .testTarget( - name: "DependenciesMacrosPluginTests", - dependencies: [ - "DependenciesMacros", - "DependenciesMacrosPlugin", - .product(name: "MacroTesting", package: "swift-macro-testing"), - ] - ) - ]) -#endif - -#if !os(Windows) - // Add the documentation compiler plugin if possible - package.dependencies.append( - .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0") - ) -#endif - -for target in package.targets { - target.swiftSettings = target.swiftSettings ?? [] - target.swiftSettings?.append(contentsOf: [ - .enableExperimentalFeature("StrictConcurrency") - ]) -} diff --git a/Sources/Dependencies/Dependency.swift b/Sources/Dependencies/Dependency.swift index 9dab9aa2..408d283f 100644 --- a/Sources/Dependencies/Dependency.swift +++ b/Sources/Dependencies/Dependency.swift @@ -69,14 +69,14 @@ /// /// [tca]: https://github.com/pointfreeco/swift-composable-architecture @propertyWrapper -public struct Dependency: _HasInitialValues { +public struct Dependency: _HasInitialValues, Sendable { let initialValues: DependencyValues = DependencyValues._current private var installValues: DependencyValues? #if canImport(SwiftUI) @Environment(\.dependencies) private var environmentValues #endif - private let keyPath: SendableKeyPath + private let keyPath: KeyPath & Sendable private let filePath: StaticString private let fileID: StaticString private let line: UInt @@ -201,12 +201,6 @@ public struct Dependency: _HasInitialValues { } } -#if compiler(>=6) - extension Dependency: Sendable {} -#else - extension Dependency: @unchecked Sendable {} -#endif - #if canImport(SwiftUI) extension Dependency: DynamicProperty { public mutating func update() { diff --git a/Sources/Dependencies/DependencyValues.swift b/Sources/Dependencies/DependencyValues.swift index 42e75b10..fcbf88ac 100644 --- a/Sources/Dependencies/DependencyValues.swift +++ b/Sources/Dependencies/DependencyValues.swift @@ -598,20 +598,7 @@ public final class CachedValues: @unchecked Sendable { value = Key.previewValue } case .test: - #if compiler(<6.1) - if !CachedValues.isAccessingCachedDependencies, - case .swiftTesting(.some(let testing)) = TestContext.current, - let testValues = testValuesByTestID.withValue({ $0[testing.test.id.rawValue] }) - { - value = CachedValues.$isAccessingCachedDependencies.withValue(true) { - testValues[key] - } - } else { - value = Key.testValue - } - #else - value = Key.testValue - #endif + value = Key.testValue } let cacheableValue = value ?? Key.testValue diff --git a/Sources/Dependencies/Documentation.docc/Articles/Testing.md b/Sources/Dependencies/Documentation.docc/Articles/Testing.md index c39080a1..9e6c9f1d 100644 --- a/Sources/Dependencies/Documentation.docc/Articles/Testing.md +++ b/Sources/Dependencies/Documentation.docc/Articles/Testing.md @@ -315,30 +315,4 @@ class Model { You will not be able to override this dependency in the normal fashion. In general there is no need to ever have a static dependency, and so you should avoid this pattern. -### Parameterized and repeated @Test runs - -> Important: If targeting Swift 6.1+ (Xcode 16.3+), then this gotcha does not apply and can be -> ignored. - -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. - -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(arguments: [1, 2, 3]) -func feature(_ number: Int) { - withDependencies { - $0 = DependencyValues() - } operation: { - // All test code in here... - } -} -``` - -This will guarantee that dependency state does not bleed over to each parameter of the test. - [issue-reporting-gh]: http://github.com/pointfreeco/swift-issue-reporting diff --git a/Sources/Dependencies/Internal/Deprecations.swift b/Sources/Dependencies/Internal/Deprecations.swift index f49748d5..9216d909 100644 --- a/Sources/Dependencies/Internal/Deprecations.swift +++ b/Sources/Dependencies/Internal/Deprecations.swift @@ -4,7 +4,7 @@ // MARK: - Deprecated after 1.9.2 -#if canImport(SwiftUI) && compiler(>=6) +#if canImport(SwiftUI) @available(iOS 18, macOS 15, tvOS 18, watchOS 11, visionOS 2, *) extension PreviewTrait where T == Preview.ViewTraits { @available( diff --git a/Sources/Dependencies/Internal/SendableKeyPath.swift b/Sources/Dependencies/Internal/SendableKeyPath.swift deleted file mode 100644 index 2662969e..00000000 --- a/Sources/Dependencies/Internal/SendableKeyPath.swift +++ /dev/null @@ -1,5 +0,0 @@ -#if compiler(>=6) - typealias SendableKeyPath = KeyPath & Sendable -#else - typealias SendableKeyPath = KeyPath -#endif diff --git a/Sources/Dependencies/Traits/TestTrait.swift b/Sources/Dependencies/Traits/TestTrait.swift deleted file mode 100644 index fd83d77c..00000000 --- a/Sources/Dependencies/Traits/TestTrait.swift +++ /dev/null @@ -1,3 +0,0 @@ -#if compiler(<6.1) - package let testValuesByTestID = LockIsolated<[AnyHashable: DependencyValues]>([:]) -#endif diff --git a/Sources/Dependencies/WithDependencies.swift b/Sources/Dependencies/WithDependencies.swift index 5cc14741..680b9a2c 100644 --- a/Sources/Dependencies/WithDependencies.swift +++ b/Sources/Dependencies/WithDependencies.swift @@ -128,9 +128,7 @@ public func withDependencies( return try DependencyValues.$_current.withValue(dependencies) { try DependencyValues.$isSetting.withValue(false) { let result = try operation() - if R.self is AnyClass { - dependencyObjects.store(result as AnyObject) - } + storeDependenciesIfNeeded(result) return result } } @@ -140,101 +138,60 @@ public func withDependencies( try updateValuesForOperation(&dependencies) return try DependencyValues.$_current.withValue(dependencies) { let result = try operation() - if R.self is AnyClass { - dependencyObjects.store(result as AnyObject) - } + storeDependenciesIfNeeded(result) return result } #endif } -#if compiler(>=6) - /// Updates the current dependencies for the duration of an asynchronous operation. - /// - /// Any mutations made to ``DependencyValues`` inside `updateValuesForOperation` will be visible - /// to everything executed in the operation. For example, if you wanted to force the - /// ``DependencyValues/date`` dependency to be a particular date, you can do: - /// - /// ```swift - /// await withDependencies { - /// $0.date.now = Date(timeIntervalSince1970: 1234567890) - /// } operation: { - /// // References to date in here are pinned to 1234567890. - /// } - /// ``` - /// - /// - Parameters: - /// - isolation: The isolation associated with the operation. - /// - updateValuesForOperation: A closure for updating the current dependency values for the - /// duration of the operation. - /// - operation: An operation to perform wherein dependencies have been overridden. - /// - Returns: The result returned from `operation`. - @discardableResult - public func withDependencies( - isolation: isolated (any Actor)? = #isolation, - _ updateValuesForOperation: (inout DependencyValues) async throws -> Void, - operation: () async throws -> R - ) async rethrows -> R { - #if DEBUG - try await DependencyValues.$isSetting.withValue(true) { - var dependencies = DependencyValues._current - try await updateValuesForOperation(&dependencies) - return try await DependencyValues.$_current.withValue(dependencies) { - try await DependencyValues.$isSetting.withValue(false) { - let result = try await operation() - if R.self is AnyClass { - dependencyObjects.store(result as AnyObject) - } - return result - } - } - } - #else - var dependencies = DependencyValues._current - try await updateValuesForOperation(&dependencies) - return try await DependencyValues.$_current.withValue(dependencies) { - let result = try await operation() - if R.self is AnyClass { - dependencyObjects.store(result as AnyObject) - } - return result - } - #endif - } -#else - @_unsafeInheritExecutor - @discardableResult - public func withDependencies( - _ updateValuesForOperation: (inout DependencyValues) async throws -> Void, - operation: () async throws -> R - ) async rethrows -> R { - #if DEBUG - try await DependencyValues.$isSetting.withValue(true) { - var dependencies = DependencyValues._current - try await updateValuesForOperation(&dependencies) - return try await DependencyValues.$_current.withValue(dependencies) { - try await DependencyValues.$isSetting.withValue(false) { - let result = try await operation() - if R.self is AnyClass { - dependencyObjects.store(result as AnyObject) - } - return result - } - } - } - #else +/// Updates the current dependencies for the duration of an asynchronous operation. +/// +/// Any mutations made to ``DependencyValues`` inside `updateValuesForOperation` will be visible +/// to everything executed in the operation. For example, if you wanted to force the +/// ``DependencyValues/date`` dependency to be a particular date, you can do: +/// +/// ```swift +/// await withDependencies { +/// $0.date.now = Date(timeIntervalSince1970: 1234567890) +/// } operation: { +/// // References to date in here are pinned to 1234567890. +/// } +/// ``` +/// +/// - Parameters: +/// - isolation: The isolation associated with the operation. +/// - updateValuesForOperation: A closure for updating the current dependency values for the +/// duration of the operation. +/// - operation: An operation to perform wherein dependencies have been overridden. +/// - Returns: The result returned from `operation`. +@discardableResult +public func withDependencies( + isolation: isolated (any Actor)? = #isolation, + _ updateValuesForOperation: (inout DependencyValues) async throws -> Void, + operation: () async throws -> R +) async rethrows -> R { + #if DEBUG + try await DependencyValues.$isSetting.withValue(true) { var dependencies = DependencyValues._current try await updateValuesForOperation(&dependencies) return try await DependencyValues.$_current.withValue(dependencies) { - let result = try await operation() - if R.self is AnyClass { - dependencyObjects.store(result as AnyObject) + try await DependencyValues.$isSetting.withValue(false) { + let result = try await operation() + storeDependenciesIfNeeded(result) + return result } - return result } - #endif - } -#endif + } + #else + var dependencies = DependencyValues._current + try await updateValuesForOperation(&dependencies) + return try await DependencyValues.$_current.withValue(dependencies) { + let result = try await operation() + storeDependenciesIfNeeded(result) + return result + } + #endif +} /// Updates the current dependencies for the duration of a synchronous operation by taking the /// dependencies tied to a given object. @@ -260,19 +217,14 @@ public func withDependencies( line: UInt = #line, column: UInt = #column ) rethrows -> R { - guard let values = dependencyObjects.values(from: model) + guard let values = dependencyValues( + from: model, + fileID: fileID, + filePath: filePath, + line: line, + column: column + ) else { - reportIssue( - """ - You are trying to propagate dependencies to a child model from a model with no dependencies. \ - To fix this, the given '\(Model.self)' must be returned from another 'withDependencies' \ - closure, or the class must hold at least one '@Dependency' property. - """, - fileID: fileID, - filePath: filePath, - line: line, - column: column - ) return try operation() } return try withDependencies { @@ -280,9 +232,7 @@ public func withDependencies( try updateValuesForOperation(&$0) } operation: { let result = try operation() - if R.self is AnyClass { - dependencyObjects.store(result as AnyObject) - } + storeDependenciesIfNeeded(result) return result } } @@ -319,155 +269,87 @@ public func withDependencies( ) } -#if compiler(>=6) - /// Updates the current dependencies for the duration of an asynchronous operation by taking the - /// dependencies tied to a given object. - /// - /// - Parameters: - /// - model: An object with dependencies. The given model should have at least one `@Dependency` - /// property, or should have been initialized and returned from a `withDependencies` - /// operation. - /// - isolation: The isolation associated with the operation. - /// - updateValuesForOperation: A closure for updating the current dependency values for the - /// duration of the operation. - /// - operation: The operation to run with the updated dependencies. - /// - fileID: The source `#fileID` associated with the operation. - /// - filePath: The source `#filePath` associated with the operation. - /// - line: The source `#line` associated with the operation. - /// - column: The source `#column` associated with the operation. - /// - Returns: The result returned from `operation`. - @discardableResult - public func withDependencies( - from model: Model, - isolation: (any Actor)? = #isolation, - _ updateValuesForOperation: (inout DependencyValues) async throws -> Void, - operation: () async throws -> R, - fileID: StaticString = #fileID, - filePath: StaticString = #filePath, - line: UInt = #line, - column: UInt = #column - ) async rethrows -> R { - guard let values = dependencyObjects.values(from: model) - else { - reportIssue( - """ - You are trying to propagate dependencies to a child model from a model with no \ - dependencies. To fix this, the given '\(Model.self)' must be returned from another \ - 'withDependencies' closure, or the class must hold at least one '@Dependency' property. - """, - fileID: fileID, - filePath: filePath, - line: line, - column: column - ) - return try await operation() - } - return try await withDependencies { - $0 = values.merging(DependencyValues._current) - try await updateValuesForOperation(&$0) - } operation: { - let result = try await operation() - if R.self is AnyClass { - dependencyObjects.store(result as AnyObject) - } - return result - } - } - - /// Updates the current dependencies for the duration of an asynchronous operation by taking the - /// dependencies tied to a given object. - /// - /// - Parameters: - /// - model: An object with dependencies. The given model should have at least one `@Dependency` - /// property, or should have been initialized and returned from a `withDependencies` - /// operation. - /// - isolation: The isolation associated with the operation. - /// - operation: The operation to run with the updated dependencies. - /// - fileID: The source `#fileID` associated with the operation. - /// - filePath: The source `#filePath` associated with the operation. - /// - line: The source `#line` associated with the operation. - /// - column: The source `#column` associated with the operation. - /// - Returns: The result returned from `operation`. - @discardableResult - public func withDependencies( - from model: Model, - isolation: (any Actor)? = #isolation, - operation: () async throws -> R, - fileID: StaticString = #fileID, - filePath: StaticString = #filePath, - line: UInt = #line, - column: UInt = #column - ) async rethrows -> R { - try await withDependencies( - from: model, - { _ in }, - operation: operation, - fileID: fileID, - filePath: filePath, - line: line, - column: column - ) +/// Updates the current dependencies for the duration of an asynchronous operation by taking the +/// dependencies tied to a given object. +/// +/// - Parameters: +/// - model: An object with dependencies. The given model should have at least one `@Dependency` +/// property, or should have been initialized and returned from a `withDependencies` +/// operation. +/// - isolation: The isolation associated with the operation. +/// - updateValuesForOperation: A closure for updating the current dependency values for the +/// duration of the operation. +/// - operation: The operation to run with the updated dependencies. +/// - fileID: The source `#fileID` associated with the operation. +/// - filePath: The source `#filePath` associated with the operation. +/// - line: The source `#line` associated with the operation. +/// - column: The source `#column` associated with the operation. +/// - Returns: The result returned from `operation`. +@discardableResult +public func withDependencies( + from model: Model, + isolation: (any Actor)? = #isolation, + _ updateValuesForOperation: (inout DependencyValues) async throws -> Void, + operation: () async throws -> R, + fileID: StaticString = #fileID, + filePath: StaticString = #filePath, + line: UInt = #line, + column: UInt = #column +) async rethrows -> R { + guard let values = dependencyValues( + from: model, + fileID: fileID, + filePath: filePath, + line: line, + column: column + ) + else { + return try await operation() } -#else - @_unsafeInheritExecutor - @discardableResult - public func withDependencies( - from model: Model, - _ updateValuesForOperation: (inout DependencyValues) async throws -> Void, - operation: () async throws -> R, - fileID: StaticString = #fileID, - filePath: StaticString = #filePath, - line: UInt = #line, - column: UInt = #column - ) async rethrows -> R { - guard let values = dependencyObjects.values(from: model) - else { - reportIssue( - """ - You are trying to propagate dependencies to a child model from a model with no \ - dependencies. To fix this, the given '\(Model.self)' must be returned from another \ - 'withDependencies' closure, or the class must hold at least one '@Dependency' property. - """, - fileID: fileID, - filePath: filePath, - line: line, - column: column - ) - return try await operation() - } - return try await withDependencies { - $0 = values.merging(DependencyValues._current) - try await updateValuesForOperation(&$0) - } operation: { - let result = try await operation() - if R.self is AnyClass { - dependencyObjects.store(result as AnyObject) - } - return result - } + return try await withDependencies { + $0 = values.merging(DependencyValues._current) + try await updateValuesForOperation(&$0) + } operation: { + let result = try await operation() + storeDependenciesIfNeeded(result) + return result } +} - @_unsafeInheritExecutor - @discardableResult - public func withDependencies( - from model: Model, - operation: () async throws -> R, - fileID: StaticString = #fileID, - filePath: StaticString = #filePath, - line: UInt = #line, - column: UInt = #column - ) async rethrows -> R { - try await withDependencies( - from: model, - { _ in }, - operation: operation, - fileID: fileID, - filePath: filePath, - line: line, - column: column - ) - } -#endif +/// Updates the current dependencies for the duration of an asynchronous operation by taking the +/// dependencies tied to a given object. +/// +/// - Parameters: +/// - model: An object with dependencies. The given model should have at least one `@Dependency` +/// property, or should have been initialized and returned from a `withDependencies` +/// operation. +/// - isolation: The isolation associated with the operation. +/// - operation: The operation to run with the updated dependencies. +/// - fileID: The source `#fileID` associated with the operation. +/// - filePath: The source `#filePath` associated with the operation. +/// - line: The source `#line` associated with the operation. +/// - column: The source `#column` associated with the operation. +/// - Returns: The result returned from `operation`. +@discardableResult +public func withDependencies( + from model: Model, + isolation: (any Actor)? = #isolation, + operation: () async throws -> R, + fileID: StaticString = #fileID, + filePath: StaticString = #filePath, + line: UInt = #line, + column: UInt = #column +) async rethrows -> R { + try await withDependencies( + from: model, + { _ in }, + operation: operation, + fileID: fileID, + filePath: filePath, + line: line, + column: column + ) +} /// Propagates the current dependencies to an escaping context. /// @@ -572,6 +454,37 @@ extension DependencyValues { private let dependencyObjects = DependencyObjects() +private func storeDependenciesIfNeeded(_ result: R) { + if R.self is AnyClass { + dependencyObjects.store(result as AnyObject) + } +} + +private func dependencyValues( + from model: Model, + fileID: StaticString, + filePath: StaticString, + line: UInt, + column: UInt +) -> DependencyValues? { + guard let values = dependencyObjects.values(from: model) + else { + reportIssue( + """ + You are trying to propagate dependencies to a child model from a model with no dependencies. \ + To fix this, the given '\(Model.self)' must be returned from another 'withDependencies' \ + closure, or the class must hold at least one '@Dependency' property. + """, + fileID: fileID, + filePath: filePath, + line: line, + column: column + ) + return nil + } + return values +} + private final class DependencyObjects: Sendable { private let storage = LockIsolated<[ObjectIdentifier: DependencyObject]>([:]) diff --git a/Sources/DependenciesTestSupport/TestTrait.swift b/Sources/DependenciesTestSupport/TestTrait.swift index dc40e15c..f8712294 100644 --- a/Sources/DependenciesTestSupport/TestTrait.swift +++ b/Sources/DependenciesTestSupport/TestTrait.swift @@ -1,240 +1,129 @@ -#if canImport(Testing) && compiler(>=6) - import ConcurrencyExtras +#if canImport(Testing) import Dependencies import Testing - #if compiler(>=6.1) - @_documentation(visibility: private) - public struct _DependenciesTrait: TestScoping, TestTrait, SuiteTrait { - let updateValues: @Sendable (inout DependencyValues) async throws -> Void + @_documentation(visibility: private) + public struct _DependenciesTrait: TestScoping, TestTrait, SuiteTrait { + let updateValues: @Sendable (inout DependencyValues) async throws -> Void - @TaskLocal static var isRoot = true + @TaskLocal static var isRoot = true - public var isRecursive: Bool { true } - public func provideScope( - for test: Test, - testCase: Test.Case?, - performing function: @Sendable () async throws -> Void - ) async throws { - try await withDependencies { - if Self.isRoot { - $0 = DependencyValues() - } - try await updateValues(&$0) - } operation: { - try await Self.$isRoot.withValue(false) { - try await function() - } + public var isRecursive: Bool { true } + public func provideScope( + for test: Test, + testCase: Test.Case?, + performing function: @Sendable () async throws -> Void + ) async throws { + try await withDependencies { + if Self.isRoot { + $0 = DependencyValues() } - } - } - - extension Trait where Self == _DependenciesTrait { - /// A trait that quarantines a test's dependencies from other tests. - /// - /// When applied to a `@Suite` (or `@Test`), the dependencies used for that suite (or test) - /// will be kept separate from any other suites (and tests) running in parallel. - /// - /// It is recommended to use a base `@Suite` to apply this to all tests. You can do this by - /// defining a `@Suite` with the trait: - /// - /// ```swift - /// @Suite(.dependencies) struct BaseSuite {} - /// ``` - /// - /// Then any suite or test you write can be nested inside the base suite: - /// - /// ```swift - /// extension BaseSuite { - /// @Suite struct MyTests { - /// @Test func login() { - /// // Dependencies accessed in here are independent from 'logout' tests. - /// } - /// - /// @Test func logout() { - /// // Dependencies accessed in here are independent from 'login' tests. - /// } - /// } - /// } - /// ``` - public static var dependencies: Self { - Self { _ in } - } - - /// A trait that overrides a test's or suite's dependency. - /// - /// Useful for overriding a dependency in a test without incurring the nesting and - /// indentation of ``withDependencies(_:operation:)-4uz6m``. - /// - /// ```swift - /// @Test(.dependency(\.continuousClock, .immediate)) - /// func feature() { - /// // ... - /// } - /// ``` - /// - /// - Parameters: - /// - keyPath: A key path to a dependency value. - /// - value: A dependency value to override for the test. - public static func dependency( - _ keyPath: WritableKeyPath & Sendable, - _ value: @autoclosure @escaping @Sendable () throws -> Value - ) -> Self { - Self { - $0[keyPath: keyPath] = try value() + try await updateValues(&$0) + } operation: { + try await Self.$isRoot.withValue(false) { + try await function() } } - - /// A trait that overrides a test's or suite's dependency. - /// - /// Useful for overriding a dependency in a test without incurring the nesting and - /// indentation of ``withDependencies(_:operation:)-4uz6m``. - /// - /// ```swift - /// struct Client: DependencyKey { … } - /// @Test(.dependency(Client.mock)) - /// func feature() { - /// // ... - /// } - /// ``` - /// - /// - Parameters: - /// - keyPath: A key path to a dependency value. - /// - value: A dependency value to override for the test. - public static func dependency( - _ value: @autoclosure @escaping @Sendable () throws -> Value - ) -> Self where Value == Value.Value { - Self { $0[Value.self] = try value() } - } - - /// A trait that overrides a test's or suite's dependencies. - /// - /// Useful for overriding a dependency in a test without incurring the nesting and - /// indentation of ``withDependencies(_:operation:)-4uz6m``. - /// - /// ```swift - /// @Test(.dependencies { - /// $0.date.now = Date(timeIntervalSince1970: 1234567890) - /// $0.uuid = .incrementing - /// }) - /// func feature() { - /// // ... - /// } - /// ``` - /// - public static func dependencies( - _ updateValues: @escaping @Sendable (inout DependencyValues) async throws -> Void - ) -> Self { - Self(updateValues: updateValues) - } } - #else - @_documentation(visibility: private) - public struct _DependenciesTrait: Sendable { - package let updateValues: @Sendable (inout DependencyValues) throws -> Void + } - package init(_ updateValues: @escaping @Sendable (inout DependencyValues) throws -> Void) { - self.updateValues = updateValues - } + extension Trait where Self == _DependenciesTrait { + /// A trait that quarantines a test's dependencies from other tests. + /// + /// When applied to a `@Suite` (or `@Test`), the dependencies used for that suite (or test) + /// will be kept separate from any other suites (and tests) running in parallel. + /// + /// It is recommended to use a base `@Suite` to apply this to all tests. You can do this by + /// defining a `@Suite` with the trait: + /// + /// ```swift + /// @Suite(.dependencies) struct BaseSuite {} + /// ``` + /// + /// Then any suite or test you write can be nested inside the base suite: + /// + /// ```swift + /// extension BaseSuite { + /// @Suite struct MyTests { + /// @Test func login() { + /// // Dependencies accessed in here are independent from 'logout' tests. + /// } + /// + /// @Test func logout() { + /// // Dependencies accessed in here are independent from 'login' tests. + /// } + /// } + /// } + /// ``` + public static var dependencies: Self { + Self { _ in } } - extension Trait where Self == _DependenciesTrait { - /// A trait that overrides a test's or suite's dependency. - /// - /// Useful for overriding a dependency in a test without incurring the nesting and - /// indentation of ``withDependencies(_:operation:)-4uz6m``. - /// - /// ```swift - /// @Test( - /// .dependency(\.continuousClock, .immediate) - /// ) - /// func feature() { - /// // ... - /// } - /// ``` - /// - /// > Important: Due to [a Swift bug](https://github.com/swiftlang/swift/issues/76409), it is - /// > not possible to specify a closure directly inside a `@Suite` or `@Test` macro: - /// > - /// > ```swift - /// > @Suite( - /// > .dependency(\.apiClient.fetchUser, { _ in .mock }) // 🛑 - /// > ) - /// > struct FeatureTests { /* ... */ } - /// > ``` - /// > - /// > To work around: extract the closure so that it is created outside the macro: - /// > - /// ```swift - /// > private let fetchUser: @Sendable (Int) async throws -> User = { _ in .mock } - /// > @Suite( - /// > .dependency(\.apiClient.fetchUser, fetchUser) - /// > ) - /// > struct FeatureTests { /* ... */ } - /// ``` - /// - /// - Parameters: - /// - keyPath: A key path to a dependency value. - /// - value: A dependency value to override for the test. - public static func dependency( - _ keyPath: WritableKeyPath & Sendable, - _ value: @autoclosure @escaping @Sendable () throws -> Value - ) -> Self { - Self { - $0[keyPath: keyPath] = try value() - } - } - - /// A trait that overrides a test's or suite's dependency. - /// - /// Useful for overriding a dependency in a test without incurring the nesting and - /// indentation of ``withDependencies(_:operation:)-4uz6m``. - /// - /// ```swift - /// struct Client: DependencyKey { … } - /// @Test( - /// .dependency(Client.mock) - /// ) - /// func feature() { - /// // ... - /// } - /// ``` - /// - /// > Important: Due to [a Swift bug](https://github.com/swiftlang/swift/issues/76409), it is - /// > not possible to specify a closure directly inside a `@Suite` or `@Test` macro: - /// > - /// > ```swift - /// > @Suite( - /// > .dependency(Client { _ in .mock }) // 🛑 - /// > ) - /// > struct FeatureTests { /* ... */ } - /// > ``` - /// - /// - Parameters: - /// - keyPath: A key path to a dependency value. - /// - value: A dependency value to override for the test. - public static func dependency( - _ value: @autoclosure @escaping @Sendable () throws -> Value - ) -> Self where Value == Value.Value { - Self { $0[Value.self] = try value() } - } - - /// A trait that overrides a test's or suite's dependencies. - public static func dependencies( - _ updateValues: @escaping @Sendable (inout DependencyValues) -> Void - ) -> Self { - Self(updateValues) + /// A trait that overrides a test's or suite's dependency. + /// + /// Useful for overriding a dependency in a test without incurring the nesting and + /// indentation of ``withDependencies(_:operation:)-4uz6m``. + /// + /// ```swift + /// @Test(.dependency(\.continuousClock, .immediate)) + /// func feature() { + /// // ... + /// } + /// ``` + /// + /// - Parameters: + /// - keyPath: A key path to a dependency value. + /// - value: A dependency value to override for the test. + public static func dependency( + _ keyPath: WritableKeyPath & Sendable, + _ value: @autoclosure @escaping @Sendable () throws -> Value + ) -> Self { + Self { + $0[keyPath: keyPath] = try value() } } - extension _DependenciesTrait: SuiteTrait, TestTrait { - public var isRecursive: Bool { true } + /// A trait that overrides a test's or suite's dependency. + /// + /// Useful for overriding a dependency in a test without incurring the nesting and + /// indentation of ``withDependencies(_:operation:)-4uz6m``. + /// + /// ```swift + /// struct Client: DependencyKey { … } + /// @Test(.dependency(Client.mock)) + /// func feature() { + /// // ... + /// } + /// ``` + /// + /// - Parameters: + /// - keyPath: A key path to a dependency value. + /// - value: A dependency value to override for the test. + public static func dependency( + _ value: @autoclosure @escaping @Sendable () throws -> Value + ) -> Self where Value == Value.Value { + Self { $0[Value.self] = try value() } + } - public func prepare(for test: Test) async throws { - try testValuesByTestID.withValue { - try self.updateValues(&$0[test.id, default: DependencyValues(context: .test)]) - } - } + /// A trait that overrides a test's or suite's dependencies. + /// + /// Useful for overriding a dependency in a test without incurring the nesting and + /// indentation of ``withDependencies(_:operation:)-4uz6m``. + /// + /// ```swift + /// @Test(.dependencies { + /// $0.date.now = Date(timeIntervalSince1970: 1234567890) + /// $0.uuid = .incrementing + /// }) + /// func feature() { + /// // ... + /// } + /// ``` + /// + public static func dependencies( + _ updateValues: @escaping @Sendable (inout DependencyValues) async throws -> Void + ) -> Self { + Self(updateValues: updateValues) } - #endif + } #endif diff --git a/Tests/DependenciesTests/DependencyValuesTests.swift b/Tests/DependenciesTests/DependencyValuesTests.swift index 046841df..cbdbdfc5 100644 --- a/Tests/DependenciesTests/DependencyValuesTests.swift +++ b/Tests/DependenciesTests/DependencyValuesTests.swift @@ -437,8 +437,7 @@ final class DependencyValuesTests: XCTestCase { self.wait(for: [expectation], timeout: 1) } - // TODO: Remove this condition when Linux CI is updated to a more recent Swift 6. - #if !os(Linux) || compiler(<6) + #if !os(Linux) @MainActor func testEscapingInFeatureModel_InstanceVariablePropagated() async { let expectation = self.expectation(description: "escape") diff --git a/Tests/DependenciesTests/PrepareDependenciesTests.swift b/Tests/DependenciesTests/PrepareDependenciesTests.swift index d2fae1bf..16ec7cb2 100644 --- a/Tests/DependenciesTests/PrepareDependenciesTests.swift +++ b/Tests/DependenciesTests/PrepareDependenciesTests.swift @@ -4,33 +4,15 @@ import Testing @Suite struct PrepareDependenciesTests { - #if compiler(>=6.1) - @Test( - .serialized, - .dependency(\.uuid, .incrementing), - arguments: [1, 2, 3] - ) - func uuid(value: Int) { - @Dependency(\.uuid) var uuid - #expect(uuid() == UUID(0)) - } - #else - @Test( - .serialized, - .dependency(\.uuid, .incrementing), - arguments: [1, 2, 3] - ) - func uuid(value: Int) { - @Dependency(\.uuid) var uuid - if value == 1 { - #expect(uuid() == UUID(0)) - } else { - withKnownIssue { - #expect(uuid() == UUID(0)) - } - } - } - #endif + @Test( + .serialized, + .dependency(\.uuid, .incrementing), + arguments: [1, 2, 3] + ) + func uuid(value: Int) { + @Dependency(\.uuid) var uuid + #expect(uuid() == UUID(0)) + } @Test func isolation1() { prepareDependencies { diff --git a/Tests/DependenciesTests/RootResettingTests.swift b/Tests/DependenciesTests/RootResettingTests.swift index 3a17d7ad..95ef345a 100644 --- a/Tests/DependenciesTests/RootResettingTests.swift +++ b/Tests/DependenciesTests/RootResettingTests.swift @@ -1,4 +1,4 @@ -#if compiler(>=6.1) && canImport(Testing) +#if canImport(Testing) import Dependencies import Foundation import Testing diff --git a/Tests/DependenciesTests/SwiftTestingTests.swift b/Tests/DependenciesTests/SwiftTestingTests.swift index 61298bcf..9bbf3475 100644 --- a/Tests/DependenciesTests/SwiftTestingTests.swift +++ b/Tests/DependenciesTests/SwiftTestingTests.swift @@ -6,33 +6,18 @@ import Testing @Suite struct SwiftTestingTests { - #if compiler(>=6.1) - @Test(.dependencies, .serialized, arguments: 1...5) - func parameterizedCachePollution(_ argument: Int) { - @Dependency(Client.self) var client - let value = client.increment() - #expect(value == 1) - } + @Test(.dependencies, .serialized, arguments: 1...5) + func parameterizedCachePollution(_ argument: Int) { + @Dependency(Client.self) var client + let value = client.increment() + #expect(value == 1) + } - @Test(.dependencies) func repeatedTest() { - @Dependency(Client.self) var client - let value = client.increment() - #expect(value == 1) - } - #else - @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) - } - } - } - #endif + @Test(.dependencies) func repeatedTest() { + @Dependency(Client.self) var client + let value = client.increment() + #expect(value == 1) + } @Test(arguments: 1...5) func parameterizedCachePollution_ResetDependencies(_ argument: Int) {