From 710147502d056340d8b10befd314d42d2f112fc7 Mon Sep 17 00:00:00 2001 From: Gemma Barlow Date: Wed, 25 Sep 2024 14:34:16 -0400 Subject: [PATCH 1/8] Restore Library Evolution support for Xcode 16 (#279) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update xctest-dynamic-overlay to 1.4.1 * Restore ‘Build for Library Evolution’ target This partially reverts commit 4b65e0323c94a6f67f462b52c10d1fdc146e6ea4. *waves* Hi - we’re depending on this. * Update version of swift-syntax * Update to build DependenciesMacros for Library Evolution * Address a syntax error in Xcode 16 “switch covers known cases, but 'AccessorBlockSyntax.Accessors' may have additional unknown values” * Revert back to `macos-14` This partially reverts commit 7c5590d2a6c918b964e8ed406e6d2e6d6f017851. --- .github/workflows/ci.yml | 2 ++ Makefile | 7 +++++++ Package.resolved | 10 +++++----- .../DependencyClientMacro.swift | 1 + 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0ddc26c0..dd1e470d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,6 +31,8 @@ jobs: run: make test-swift - name: Build platforms ${{ matrix.config }} run: CONFIG=${{ matrix.config }} make build-all-platforms + - name: Build for library evolution + run: make build-for-library-evolution ubuntu: strategy: diff --git a/Makefile b/Makefile index 3920739d..8a76ba11 100644 --- a/Makefile +++ b/Makefile @@ -50,6 +50,13 @@ build-for-library-evolution: -Xswiftc -emit-module-interface \ -Xswiftc -enable-library-evolution + swift build \ + -c release \ + --target DependenciesMacros \ + -Xswiftc -emit-module-interface \ + -Xswiftc -enable-library-evolution \ + -Xswiftc -DRESILIENT_LIBRARIES # Required to build swift-syntax; see https://github.com/swiftlang/swift-syntax/pull/2540 + build-for-static-stdlib-docker: @docker run \ -v "$(PWD):$(PWD)" \ diff --git a/Package.resolved b/Package.resolved index 354bb108..8f5d4c31 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "b70d24a284d4a26856cfef0d2ef69b0e2326a2fa8a91e22c08eba0cad0f59bcd", + "originHash" : "ac879199bc109c96e02f389573ce5b101fa5c8a274b809fc57dba0d4736f5b6f", "pins" : [ { "identity" : "combine-schedulers", @@ -69,8 +69,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/swiftlang/swift-syntax", "state" : { - "revision" : "515f79b522918f83483068d99c68daeb5116342d", - "version" : "600.0.0-prerelease-2024-09-04" + "revision" : "cb53fa1bd3219b0b23ded7dfdd3b2baff266fd25", + "version" : "600.0.0" } }, { @@ -78,8 +78,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", "state" : { - "revision" : "bc2a151366f2cd0e347274544933bc2acb00c9fe", - "version" : "1.4.0" + "revision" : "27d767d643fa2cf083d0a73d74fa84cacb53e85c", + "version" : "1.4.1" } } ], diff --git a/Sources/DependenciesMacrosPlugin/DependencyClientMacro.swift b/Sources/DependenciesMacrosPlugin/DependencyClientMacro.swift index 942aa6b3..62646fec 100644 --- a/Sources/DependenciesMacrosPlugin/DependencyClientMacro.swift +++ b/Sources/DependenciesMacrosPlugin/DependencyClientMacro.swift @@ -120,6 +120,7 @@ public enum DependencyClientMacro: MemberAttributeMacro, MemberMacro { if accessors.contains(where: { $0.accessorSpecifier.tokenKind == .keyword(.get) }) { continue } + @unknown default: return [] } } From 06cbf43e0d51eda4a764dee60f4cd92fabb89894 Mon Sep 17 00:00:00 2001 From: Alex Lorenz Date: Fri, 4 Oct 2024 07:42:21 -0700 Subject: [PATCH 2/8] fix the android build (#282) The Android module is available in Swift 6 for Android --- Sources/Dependencies/DependencyValues.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/Dependencies/DependencyValues.swift b/Sources/Dependencies/DependencyValues.swift index 4e5009a4..5774ba65 100644 --- a/Sources/Dependencies/DependencyValues.swift +++ b/Sources/Dependencies/DependencyValues.swift @@ -3,6 +3,8 @@ import IssueReporting #if os(Windows) import WinSDK +#elseif canImport(Android) + import Android #elseif os(Linux) import Glibc #endif From e25189b83514356c5ab28733b8a0cdeac3dd0b36 Mon Sep 17 00:00:00 2001 From: "Sven A. Schmidt" Date: Mon, 14 Oct 2024 17:48:53 +0200 Subject: [PATCH 3/8] Update Package@swift-6.0.swift (#291) --- Package@swift-6.0.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package@swift-6.0.swift b/Package@swift-6.0.swift index e28f5c08..25dfe592 100644 --- a/Package@swift-6.0.swift +++ b/Package@swift-6.0.swift @@ -81,7 +81,7 @@ let package = Package( ] ), ], - swiftLanguageVersions: [.v6] + swiftLanguageModes: [.v6] ) #if !os(macOS) && !os(WASI) From 653531e4d04ef3fc42de9778274e95cfa653ceda Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 7 Nov 2024 14:40:52 -0800 Subject: [PATCH 4/8] Add `prepareDependencies` (#288) * Add `prepareDependencies` Right now, dependencies can only be overridden using scoped operations like `withDependencies` or attached to object livecycles (`withDependencies(from:)`). This introduces a new top-level way of preparing global dependencies so that they are accessible in the top-level scope of your application. * wip * wip * wip * Add failing test for preparing dependency twice in the same prepareDependencies{} * another failing test * wip * Allow preparing dependency multiple times in same scope (#298) * Attempted fix of preparing dependency in multiple lines. * fixes * fix * tests * wip * fixes * clean up * clean up * clean up * wip * wip * fix * wip * wip * wip --------- Co-authored-by: Brandon Williams Co-authored-by: Brandon Williams <135203+mbrandonw@users.noreply.github.com> --- Package.resolved | 6 +- Package@swift-6.0.swift | 5 +- Sources/Dependencies/DependencyValues.swift | 89 ++++++++- .../Articles/WhatAreDependencies.md | 3 + Sources/Dependencies/WithDependencies.swift | 17 ++ .../DependencyEndpointTests.swift | 0 .../DependenciesTests/Dependencies.xctestplan | 24 +++ .../DependencyValuesTests.swift | 173 ++++++++++++++++++ .../DependenciesTests/Internal/XCTTODO.swift | 13 ++ 9 files changed, 319 insertions(+), 11 deletions(-) rename Tests/{DependenciesTests => DependenciesMacrosPluginTests}/DependencyEndpointTests.swift (100%) create mode 100644 Tests/DependenciesTests/Dependencies.xctestplan create mode 100644 Tests/DependenciesTests/Internal/XCTTODO.swift diff --git a/Package.resolved b/Package.resolved index 8f5d4c31..0c1bf166 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "ac879199bc109c96e02f389573ce5b101fa5c8a274b809fc57dba0d4736f5b6f", + "originHash" : "46c7c52f0c1617cc1d5bc47663541c21a6e6734ddf2ad859bf5bf5bc207fa8f4", "pins" : [ { "identity" : "combine-schedulers", @@ -78,8 +78,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", "state" : { - "revision" : "27d767d643fa2cf083d0a73d74fa84cacb53e85c", - "version" : "1.4.1" + "revision" : "770f990d3e4eececb57ac04a6076e22f8c97daeb", + "version" : "1.4.2" } } ], diff --git a/Package@swift-6.0.swift b/Package@swift-6.0.swift index 25dfe592..87e861f8 100644 --- a/Package@swift-6.0.swift +++ b/Package@swift-6.0.swift @@ -60,10 +60,10 @@ let package = Package( name: "DependenciesTests", dependencies: [ "Dependencies", - "DependenciesMacros", "DependenciesTestSupport", .product(name: "IssueReportingTestSupport", package: "xctest-dynamic-overlay"), - ] + ], + exclude: ["Dependencies.xctestplan"] ), .target( name: "DependenciesMacros", @@ -102,6 +102,7 @@ let package = Package( .testTarget( name: "DependenciesMacrosPluginTests", dependencies: [ + "Dependencies", "DependenciesMacros", "DependenciesMacrosPlugin", .product(name: "MacroTesting", package: "swift-macro-testing"), diff --git a/Sources/Dependencies/DependencyValues.swift b/Sources/Dependencies/DependencyValues.swift index 5774ba65..2d76ee4c 100644 --- a/Sources/Dependencies/DependencyValues.swift +++ b/Sources/Dependencies/DependencyValues.swift @@ -118,8 +118,12 @@ import IssueReporting /// Read the article for more information. public struct DependencyValues: Sendable { @TaskLocal public static var _current = Self() - @TaskLocal static var isSetting = false @TaskLocal static var currentDependency = CurrentDependency() + @TaskLocal static var isSetting = false + @TaskLocal static var preparationID: UUID? + static var isPreparing: Bool { + preparationID != nil + } @_spi(Internals) public var cachedValues = CachedValues() @@ -263,7 +267,75 @@ public struct DependencyValues: Sendable { return dependency } set { - self.storage[ObjectIdentifier(key)] = newValue + if DependencyValues.isPreparing { + let cacheKey = CachedValues.CacheKey(id: TypeIdentifier(key), context: context) + guard !cachedValues.cached.keys.contains(cacheKey) else { + if cachedValues.cached[cacheKey]?.preparationID != DependencyValues.preparationID { + reportIssue( + { + var dependencyDescription = "" + if let fileID = DependencyValues.currentDependency.fileID, + let line = DependencyValues.currentDependency.line + { + dependencyDescription.append( + """ + Location: + \(fileID):\(line) + + """ + ) + } + dependencyDescription.append( + Key.self == Key.Value.self + ? """ + Dependency: + \(typeName(Key.Value.self)) + """ + : """ + Key: + \(typeName(Key.self)) + Value: + \(typeName(Key.Value.self)) + """ + ) + var argument: String { + "\(function)" == "subscript(key:)" + ? "\(typeName(Key.self)).self" + : "\\.\(function)" + } + return """ + @Dependency(\(argument)) has already been accessed or prepared. + + \(dependencyDescription) + + A global dependency can only be prepared a single time and cannot be accessed \ + beforehand. Prepare dependencies as early as possible in the lifecycle of your \ + application. + + To temporarily override a dependency in your application, use 'withDependencies' \ + to do so in a well-defined scope. + """ + }(), + fileID: DependencyValues.currentDependency.fileID ?? fileID, + filePath: DependencyValues.currentDependency.filePath ?? filePath, + line: DependencyValues.currentDependency.line ?? line, + column: DependencyValues.currentDependency.column ?? column + ) + } else { + cachedValues.cached[cacheKey] = CachedValues.CachedValue( + base: newValue, + preparationID: DependencyValues.preparationID + ) + } + return + } + cachedValues.cached[cacheKey] = CachedValues.CachedValue( + base: newValue, + preparationID: DependencyValues.preparationID + ) + } else { + self.storage[ObjectIdentifier(key)] = newValue + } } } @@ -382,8 +454,13 @@ public final class CachedValues: @unchecked Sendable { } } + public struct CachedValue { + let base: any Sendable + let preparationID: UUID? + } + private let lock = NSRecursiveLock() - public var cached = [CacheKey: any Sendable]() + public var cached = [CacheKey: CachedValue]() func value( for key: Key.Type, @@ -399,7 +476,7 @@ public final class CachedValues: @unchecked Sendable { return withIssueContext(fileID: fileID, filePath: filePath, line: line, column: column) { let cacheKey = CacheKey(id: TypeIdentifier(key), context: context) - guard let base = cached[cacheKey], let value = base as? Key.Value + guard let base = cached[cacheKey]?.base, let value = base as? Key.Value else { let value: Key.Value? switch context { @@ -488,12 +565,12 @@ public final class CachedValues: @unchecked Sendable { #endif let value = Key.testValue if !DependencyValues.isSetting { - cached[cacheKey] = value + cached[cacheKey] = CachedValue(base: value, preparationID: DependencyValues.preparationID) } return value } - cached[cacheKey] = value + cached[cacheKey] = CachedValue(base: value, preparationID: DependencyValues.preparationID) return value } diff --git a/Sources/Dependencies/Documentation.docc/Articles/WhatAreDependencies.md b/Sources/Dependencies/Documentation.docc/Articles/WhatAreDependencies.md index 6dbc835f..fc4beebe 100644 --- a/Sources/Dependencies/Documentation.docc/Articles/WhatAreDependencies.md +++ b/Sources/Dependencies/Documentation.docc/Articles/WhatAreDependencies.md @@ -94,6 +94,9 @@ final class FeatureModel { } ``` +> Note: Using the `@ObservationIgnored` macro is necessary when using `@Observable` because +> `@Dependency` is a property wrapper. + That small change makes this feature much friendlier to Xcode previews and testing. For previews, you can use the `.dependencies` preview trait to override the diff --git a/Sources/Dependencies/WithDependencies.swift b/Sources/Dependencies/WithDependencies.swift index 4f72a6fa..f24899ec 100644 --- a/Sources/Dependencies/WithDependencies.swift +++ b/Sources/Dependencies/WithDependencies.swift @@ -1,5 +1,22 @@ import Foundation +/// Prepares global dependencies for the lifetime of your application. +/// +/// > Important: A dependency key can be prepared at most a single time, and _must_ be prepared +/// > before it has been accessed. Call `prepareDependencies` as early as possible in your +/// > application. +/// +/// - Parameter updateValues: A closure for updating the current dependency values for the +/// lifetime of your application. +public func prepareDependencies( + _ updateValues: (inout DependencyValues) throws -> Void +) rethrows { + var dependencies = DependencyValues._current + try DependencyValues.$preparationID.withValue(UUID()) { + try updateValues(&dependencies) + } +} + /// Updates the current dependencies for the duration of a synchronous operation. /// /// Any mutations made to ``DependencyValues`` inside `updateValuesForOperation` will be visible to diff --git a/Tests/DependenciesTests/DependencyEndpointTests.swift b/Tests/DependenciesMacrosPluginTests/DependencyEndpointTests.swift similarity index 100% rename from Tests/DependenciesTests/DependencyEndpointTests.swift rename to Tests/DependenciesMacrosPluginTests/DependencyEndpointTests.swift diff --git a/Tests/DependenciesTests/Dependencies.xctestplan b/Tests/DependenciesTests/Dependencies.xctestplan new file mode 100644 index 00000000..680b7fd5 --- /dev/null +++ b/Tests/DependenciesTests/Dependencies.xctestplan @@ -0,0 +1,24 @@ +{ + "configurations" : [ + { + "id" : "AAA44865-978E-4BB3-B5D5-4875FE9FCB65", + "name" : "Test Scheme Action", + "options" : { + + } + } + ], + "defaultOptions" : { + "codeCoverage" : false + }, + "testTargets" : [ + { + "target" : { + "containerPath" : "container:", + "identifier" : "DependenciesTests", + "name" : "DependenciesTests" + } + } + ], + "version" : 1 +} diff --git a/Tests/DependenciesTests/DependencyValuesTests.swift b/Tests/DependenciesTests/DependencyValuesTests.swift index 49b2fafe..62216579 100644 --- a/Tests/DependenciesTests/DependencyValuesTests.swift +++ b/Tests/DependenciesTests/DependencyValuesTests.swift @@ -121,6 +121,16 @@ final class DependencyValuesTests: XCTestCase { } } + func testSetDependencyAcrossMultipleLines() { + withDependencies { + $0.date = .constant(someDate) + $0.date = .constant(someDate.addingTimeInterval(10)) + } operation: { + @Dependency(\.date) var date + XCTAssertEqual(date.now, someDate.addingTimeInterval(10)) + } + } + func testOptionalDependency() { for value in [nil, ""] { withDependencies { @@ -688,6 +698,162 @@ final class DependencyValuesTests: XCTestCase { @Dependency(\.date) var date _ = date } + + func testPrepareDependencies_setsDependency() { + prepareDependencies { + $0.date = DateGenerator { Date(timeIntervalSinceReferenceDate: 0) } + } + @Dependency(\.date.now) var now + XCTAssertEqual(now, Date(timeIntervalSinceReferenceDate: 0)) + } + + #if DEBUG && !os(Linux) && !os(WASI) && !os(Windows) + func testPrepareDependencies_MultiplePreparesWithNoAccessBetween() { + prepareDependencies { + $0.date = DateGenerator { Date(timeIntervalSinceReferenceDate: 0) } + } + XCTExpectFailure( + """ + Currently this fails, but in the future we may allow a dependency to be changed in + multiple 'prepareDependencies' as long as the dependency has not yet been accessed. + """ + ) { + $0.compactDescription == """ + failed - @Dependency(\\.date) has already been accessed or prepared. + + Key: + DependencyValues.DateGeneratorKey + Value: + DateGenerator + + A global dependency can only be prepared a single time and cannot be accessed \ + beforehand. Prepare dependencies as early as possible in the lifecycle of your \ + application. + + To temporarily override a dependency in your application, use 'withDependencies' to do \ + so in a well-defined scope. + """ + } + prepareDependencies { + $0.date = DateGenerator { Date(timeIntervalSince1970: 0) } + } + @Dependency(\.date.now) var now + XCTAssertEqual(now, Date(timeIntervalSinceReferenceDate: 0)) + } + #endif + + #if DEBUG && !os(Linux) && !os(WASI) && !os(Windows) + func testPrepareDependencies_MultiplePreparesWithAccessBetween() { + prepareDependencies { + $0.date = DateGenerator { Date(timeIntervalSinceReferenceDate: 0) } + } + XCTExpectFailure { + $0.compactDescription == """ + failed - @Dependency(\\.date) has already been accessed or prepared. + + Key: + DependencyValues.DateGeneratorKey + Value: + DateGenerator + + A global dependency can only be prepared a single time and cannot be accessed \ + beforehand. Prepare dependencies as early as possible in the lifecycle of your \ + application. + + To temporarily override a dependency in your application, use 'withDependencies' to do \ + so in a well-defined scope. + """ + } + @Dependency(\.date) var date + _ = date + prepareDependencies { + $0.date = DateGenerator { Date(timeIntervalSince1970: 0) } + } + } + #endif + + func testPrepareDependencies_setDependencyMultipleTimesInSamePrepare() { + prepareDependencies { + $0.date = DateGenerator { Date(timeIntervalSinceReferenceDate: 42) } + $0.date = DateGenerator { Date(timeIntervalSinceReferenceDate: 1729) } + } + @Dependency(\.date.now) var now + XCTAssertEqual(now, Date(timeIntervalSinceReferenceDate: 1729)) + } + + #if DEBUG && !os(Linux) && !os(WASI) && !os(Windows) + func testPrepareDependencies_DependencyAccessBeforePrepare() { + withDependencies { + $0.context = .live + } operation: { + @Dependency(\.date) var date + _ = date() + XCTExpectFailure { + prepareDependencies { + $0.date = DateGenerator { Date(timeIntervalSinceReferenceDate: 42) } + } + } issueMatcher: { + $0.compactDescription.hasPrefix( + #""" + failed - @Dependency(\.date) has already been accessed or prepared. + """#) + } + XCTAssertNotEqual(date(), Date(timeIntervalSinceReferenceDate: 42)) + } + } + #endif + + #if DEBUG && !os(Linux) && !os(WASI) && !os(Windows) + func testPrepareDependencies_PrepareContext() { + prepareDependencies { $0.context = .live } + + XCTTODO( + """ + Currently 'context' cannot be overridden with 'prepareDependencies'. + """) + @Dependency(\.date) var date + _ = date() + } + #endif + + func testPrepareDependencies_setDependencyEndpoint() { + prepareDependencies { + $0[ClientWithEndpoint.self].get = { @Sendable in 42 } + } + @Dependency(ClientWithEndpoint.self) var client + XCTAssertEqual(client.get(), 42) + } + + #if DEBUG && !os(Linux) && !os(WASI) && !os(Windows) + func testPrepareDependencies_alreadyCached() { + withDependencies { + $0.context = .live + } operation: { + @Dependency(\.date.now) var now + _ = now + XCTExpectFailure { + $0.compactDescription == """ + failed - @Dependency(\\.date) has already been accessed or prepared. + + Key: + DependencyValues.DateGeneratorKey + Value: + DateGenerator + + A global dependency can only be prepared a single time and cannot be accessed \ + beforehand. Prepare dependencies as early as possible in the lifecycle of your \ + application. + + To temporarily override a dependency in your application, use 'withDependencies' to do \ + so in a well-defined scope. + """ + } + prepareDependencies { + $0.date = DateGenerator { Date(timeIntervalSince1970: 0) } + } + } + } + #endif } struct CountInitDependency: TestDependencyKey { @@ -825,3 +991,10 @@ extension DependencyValues { set { self[FullDependency.self] = newValue } } } + +private struct ClientWithEndpoint: TestDependencyKey { + var get: @Sendable () -> Int + static var testValue: ClientWithEndpoint { + Self { 42 } + } +} diff --git a/Tests/DependenciesTests/Internal/XCTTODO.swift b/Tests/DependenciesTests/Internal/XCTTODO.swift new file mode 100644 index 00000000..b11e2f28 --- /dev/null +++ b/Tests/DependenciesTests/Internal/XCTTODO.swift @@ -0,0 +1,13 @@ +#if !os(Linux) && !os(WASI) && !os(Windows) +import XCTest + +@_transparent +@available( + *, + deprecated, + message: "This is a test that currently fails but should not in the future." +) +func XCTTODO(_ message: String) { + XCTExpectFailure(message) +} +#endif From 6f17eb98440d7865e34b085c3bf93e601ec76e6d Mon Sep 17 00:00:00 2001 From: stephencelis Date: Thu, 7 Nov 2024 22:41:23 +0000 Subject: [PATCH 5/8] Run swift-format --- Sources/Dependencies/DependencyValues.swift | 3 ++- .../DependenciesTests/Internal/XCTTODO.swift | 20 +++++++++---------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/Sources/Dependencies/DependencyValues.swift b/Sources/Dependencies/DependencyValues.swift index 2d76ee4c..52ae81cd 100644 --- a/Sources/Dependencies/DependencyValues.swift +++ b/Sources/Dependencies/DependencyValues.swift @@ -565,7 +565,8 @@ public final class CachedValues: @unchecked Sendable { #endif let value = Key.testValue if !DependencyValues.isSetting { - cached[cacheKey] = CachedValue(base: value, preparationID: DependencyValues.preparationID) + cached[cacheKey] = CachedValue( + base: value, preparationID: DependencyValues.preparationID) } return value } diff --git a/Tests/DependenciesTests/Internal/XCTTODO.swift b/Tests/DependenciesTests/Internal/XCTTODO.swift index b11e2f28..217025e9 100644 --- a/Tests/DependenciesTests/Internal/XCTTODO.swift +++ b/Tests/DependenciesTests/Internal/XCTTODO.swift @@ -1,13 +1,13 @@ #if !os(Linux) && !os(WASI) && !os(Windows) -import XCTest + import XCTest -@_transparent -@available( - *, - deprecated, - message: "This is a test that currently fails but should not in the future." -) -func XCTTODO(_ message: String) { - XCTExpectFailure(message) -} + @_transparent + @available( + *, + deprecated, + message: "This is a test that currently fails but should not in the future." + ) + func XCTTODO(_ message: String) { + XCTExpectFailure(message) + } #endif From 26b95e1e02700e05d7c5e45d1759c31a70caa570 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 8 Nov 2024 10:06:17 -0800 Subject: [PATCH 6/8] Cache dependencies in `withDependencies` update closure (#287) * wip * add #if debug --------- Co-authored-by: Brandon Williams --- Sources/Dependencies/DependencyValues.swift | 91 +++++++++---------- .../DependencyValuesTests.swift | 45 +++++++++ 2 files changed, 88 insertions(+), 48 deletions(-) diff --git a/Sources/Dependencies/DependencyValues.swift b/Sources/Dependencies/DependencyValues.swift index 52ae81cd..388d1b29 100644 --- a/Sources/Dependencies/DependencyValues.swift +++ b/Sources/Dependencies/DependencyValues.swift @@ -476,37 +476,10 @@ public final class CachedValues: @unchecked Sendable { return withIssueContext(fileID: fileID, filePath: filePath, line: line, column: column) { let cacheKey = CacheKey(id: TypeIdentifier(key), context: context) - guard let base = cached[cacheKey]?.base, let value = base as? Key.Value - else { - let value: Key.Value? - switch context { - case .live: - value = (key as? any DependencyKey.Type)?.liveValue as? Key.Value - case .preview: - if !CachedValues.isAccessingCachedDependencies { - value = CachedValues.$isAccessingCachedDependencies.withValue(true) { - previewValues.withValue { $0[key] } - } - } else { - value = Key.previewValue - } - case .test: - if !CachedValues.isAccessingCachedDependencies, - case let .swiftTesting(.some(testing)) = TestContext.current, - let testValues = testValuesByTestID.withValue({ $0[testing.test.id.rawValue] }) - { - value = CachedValues.$isAccessingCachedDependencies.withValue(true) { - testValues[key] - } - } else { - value = Key.testValue - } - } - - guard let value - else { - #if DEBUG - if !DependencyValues.isSetting { + #if DEBUG + if context == .live, !DependencyValues.isSetting, !(key is any DependencyKey.Type) { + reportIssue( + { var dependencyDescription = "" if let fileID = DependencyValues.currentDependency.fileID, let line = DependencyValues.currentDependency.line @@ -538,9 +511,7 @@ public final class CachedValues: @unchecked Sendable { ? "\(typeName(Key.self)).self" : "\\.\(function)" } - - reportIssue( - """ + return """ @Dependency(\(argument)) has no live implementation, but was accessed from a live \ context. @@ -555,24 +526,48 @@ public final class CachedValues: @unchecked Sendable { • Override the implementation of '\(typeName(Key.self))' using \ 'withDependencies'. This is typically done at the entry point of your \ application, but can be done later too. - """, - fileID: DependencyValues.currentDependency.fileID ?? fileID, - filePath: DependencyValues.currentDependency.filePath ?? filePath, - line: DependencyValues.currentDependency.line ?? line, - column: DependencyValues.currentDependency.column ?? column - ) + """ + }(), + fileID: DependencyValues.currentDependency.fileID ?? fileID, + filePath: DependencyValues.currentDependency.filePath ?? filePath, + line: DependencyValues.currentDependency.line ?? line, + column: DependencyValues.currentDependency.column ?? column + ) + } + #endif + + guard let base = cached[cacheKey]?.base, let value = base as? Key.Value + else { + let value: Key.Value? + switch context { + case .live: + value = (key as? any DependencyKey.Type)?.liveValue as? Key.Value + case .preview: + if !CachedValues.isAccessingCachedDependencies { + value = CachedValues.$isAccessingCachedDependencies.withValue(true) { + previewValues.withValue { $0[key] } } - #endif - let value = Key.testValue - if !DependencyValues.isSetting { - cached[cacheKey] = CachedValue( - base: value, preparationID: DependencyValues.preparationID) + } else { + value = Key.previewValue + } + case .test: + if !CachedValues.isAccessingCachedDependencies, + case let .swiftTesting(.some(testing)) = TestContext.current, + let testValues = testValuesByTestID.withValue({ $0[testing.test.id.rawValue] }) + { + value = CachedValues.$isAccessingCachedDependencies.withValue(true) { + testValues[key] + } + } else { + value = Key.testValue } - return value } - cached[cacheKey] = CachedValue(base: value, preparationID: DependencyValues.preparationID) - return value + let cacheableValue = value ?? Key.testValue + cached[cacheKey] = CachedValue( + base: cacheableValue, preparationID: DependencyValues.preparationID + ) + return cacheableValue } return value diff --git a/Tests/DependenciesTests/DependencyValuesTests.swift b/Tests/DependenciesTests/DependencyValuesTests.swift index 62216579..0329fc65 100644 --- a/Tests/DependenciesTests/DependencyValuesTests.swift +++ b/Tests/DependenciesTests/DependencyValuesTests.swift @@ -265,6 +265,35 @@ final class DependencyValuesTests: XCTestCase { #endif } + func testUpdatingTestDependencyFromLiveContext_WhenUpdatingDependencies() { + @Dependency(\.reuseClient) var reuseClient: ReuseClient + + #if !os(Linux) && !os(WASI) && !os(Windows) + withDependencies { + $0.context = .live + } operation: { + withDependencies { + $0.reuseClient.setCount(42) + XCTAssertEqual($0.reuseClient.count(), 42) + XCTAssertEqual(reuseClient.count(), 42) + } operation: { + #if DEBUG + XCTExpectFailure { + $0.compactDescription.contains( + """ + @Dependency(\\.reuseClient) has no live implementation, but was accessed from a \ + live context. + """ + ) + } + #endif + XCTAssertEqual(reuseClient.count(), 42) + } + } + XCTAssertEqual(reuseClient.count(), 0) + #endif + } + func testBinding() { withDependencies { $0.context = .test @@ -854,6 +883,22 @@ final class DependencyValuesTests: XCTestCase { } } #endif + + func testPrepareDependencies_WithDependencies() { + prepareDependencies { + $0.date.now = Date(timeIntervalSince1970: 42) + } + + withDependencies { + $0.date.now = Date(timeIntervalSince1970: 1729) + } operation: { + @Dependency(\.date.now) var now + XCTAssertEqual(now, Date(timeIntervalSince1970: 1729)) + } + + @Dependency(\.date.now) var now + XCTAssertEqual(now, Date(timeIntervalSince1970: 42)) + } } struct CountInitDependency: TestDependencyKey { From e84352a6cecde2bfdbc3860cd8bceef395347f7c Mon Sep 17 00:00:00 2001 From: Brandon Williams <135203+mbrandonw@users.noreply.github.com> Date: Fri, 8 Nov 2024 19:14:56 -0500 Subject: [PATCH 7/8] Relax preview traits sendability. (#299) --- Sources/Dependencies/DependencyValues.swift | 6 +++++- Sources/Dependencies/Traits/PreviewTrait.swift | 12 +++++------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/Sources/Dependencies/DependencyValues.swift b/Sources/Dependencies/DependencyValues.swift index 388d1b29..37cdb01a 100644 --- a/Sources/Dependencies/DependencyValues.swift +++ b/Sources/Dependencies/DependencyValues.swift @@ -545,7 +545,11 @@ public final class CachedValues: @unchecked Sendable { case .preview: if !CachedValues.isAccessingCachedDependencies { value = CachedValues.$isAccessingCachedDependencies.withValue(true) { - previewValues.withValue { $0[key] } + #if compiler(>=6) + return previewValues[key] + #else + return Key.previewValue + #endif } } else { value = Key.previewValue diff --git a/Sources/Dependencies/Traits/PreviewTrait.swift b/Sources/Dependencies/Traits/PreviewTrait.swift index f8e24aec..ff137ce0 100644 --- a/Sources/Dependencies/Traits/PreviewTrait.swift +++ b/Sources/Dependencies/Traits/PreviewTrait.swift @@ -19,7 +19,7 @@ /// - Parameters: /// - keyPath: A key path to a dependency value. /// - value: A dependency value to override for the lifetime of the preview. - public static func dependency( + public static func dependency( _ keyPath: WritableKeyPath & Sendable, _ value: Value ) -> PreviewTrait { @@ -45,14 +45,12 @@ /// - Parameter updateValuesForPreview: A closure for updating the current dependency values for /// the lifetime of the preview. public static func dependencies( - _ updateValuesForPreview: @Sendable (inout DependencyValues) -> Void + _ updateValuesForPreview: (inout DependencyValues) -> Void ) -> PreviewTrait { - previewValues.withValue { - updateValuesForPreview(&$0) - } + updateValuesForPreview(&previewValues) return PreviewTrait() } } -#endif -let previewValues = LockIsolated(DependencyValues(context: .preview)) + nonisolated(unsafe) var previewValues = DependencyValues(context: .preview) +#endif From a24a0f1037b5c65885876be89f598b6f1e70f21c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20B=C4=85k?= Date: Sat, 9 Nov 2024 05:17:15 +0100 Subject: [PATCH 8/8] Add Macro Compatibility Check to CI workflow (#285) Co-authored-by: Stephen Celis --- .github/workflows/ci.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dd1e470d..7cea4816 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -77,3 +77,15 @@ jobs: # run: swift test # - name: Run tests (release) # run: swift test -c release + + check-macro-compatibility: + name: Check Macro Compatibility + runs-on: macos-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Run Swift Macro Compatibility Check + uses: Matejkob/swift-macro-compatibility-check@v1 + with: + run-tests: false + major-versions-only: true