Skip to content

Commit

Permalink
Bucket dependency cache by Swift Testing test (#269)
Browse files Browse the repository at this point in the history
* Bucket dependency cache by Swift Testing test

Currently, the global cache can bleed between test cases. This updates
the underlying cache key to take the current test into account.

* wip

* clean up tests

* wip

* Preserve more information in cache key for debug builds.

* TypeIdentifier

* Update Sources/Dependencies/DependencyValues.swift

* wip

* improve tests

---------

Co-authored-by: Brandon Williams <[email protected]>
  • Loading branch information
stephencelis and mbrandonw authored Sep 10, 2024
1 parent 3ef38bb commit dac7c17
Show file tree
Hide file tree
Showing 7 changed files with 125 additions and 85 deletions.
29 changes: 14 additions & 15 deletions Dependencies.xcworkspace/xcshareddata/swiftpm/Package.resolved
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
{
"originHash" : "b78e1fa69770050e204024326b4d77e8451007e7f7ecc1b640d5889ffbe0b3a7",
"pins" : [
{
"identity" : "combine-schedulers",
Expand All @@ -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"
}
},
{
Expand All @@ -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"
Expand All @@ -51,37 +50,37 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-macro-testing",
"state" : {
"revision" : "a35257b7e9ce44e92636447003a8eeefb77b145c",
"version" : "0.5.1"
"revision" : "20c1a8f3b624fb5d1503eadcaa84743050c350f4",
"version" : "0.5.2"
}
},
{
"identity" : "swift-snapshot-testing",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-snapshot-testing",
"state" : {
"revision" : "c097f955b4e724690f0fc8ffb7a6d4b881c9c4e3",
"version" : "1.17.2"
"revision" : "6d932a79e7173b275b96c600c86c603cf84f153c",
"version" : "1.17.4"
}
},
{
"identity" : "swift-syntax",
"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"
}
},
{
"identity" : "xctest-dynamic-overlay",
"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
}
29 changes: 15 additions & 14 deletions Package.resolved
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"originHash" : "583c00d70f39319a7eca67f614e30ccaab233ad9a104a40007e982cf4584d4d6",
"pins" : [
{
"identity" : "combine-schedulers",
Expand All @@ -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"
}
},
{
Expand All @@ -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"
Expand All @@ -50,37 +51,37 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-macro-testing",
"state" : {
"revision" : "a35257b7e9ce44e92636447003a8eeefb77b145c",
"version" : "0.5.1"
"revision" : "20c1a8f3b624fb5d1503eadcaa84743050c350f4",
"version" : "0.5.2"
}
},
{
"identity" : "swift-snapshot-testing",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-snapshot-testing",
"state" : {
"revision" : "c097f955b4e724690f0fc8ffb7a6d4b881c9c4e3",
"version" : "1.17.2"
"revision" : "6d932a79e7173b275b96c600c86c603cf84f153c",
"version" : "1.17.4"
}
},
{
"identity" : "swift-syntax",
"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"
}
},
{
"identity" : "xctest-dynamic-overlay",
"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
}
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand Down
2 changes: 1 addition & 1 deletion [email protected]
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand Down
42 changes: 40 additions & 2 deletions Sources/Dependencies/DependencyValues.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [:]
}
Expand Down Expand Up @@ -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()
Expand All @@ -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?
Expand Down Expand Up @@ -461,3 +477,25 @@ public final class CachedValues: @unchecked Sendable {
}
}
}

struct TypeIdentifier: Hashable {
let id: ObjectIdentifier
#if DEBUG
let base: Any.Type
#endif

init<T>(_ 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)
}
}
44 changes: 10 additions & 34 deletions Sources/Dependencies/Documentation.docc/Articles/Testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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`](<doc:withDependencies(_:operation:)-3vrqy>) 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`](<doc:withDependencies(_:operation:)-3vrqy>) 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.
62 changes: 44 additions & 18 deletions Tests/DependenciesTests/SwiftTestingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit dac7c17

Please sign in to comment.