Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bucket dependency cache by Swift Testing test #269

Merged
merged 9 commits into from
Sep 10, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 14 additions & 14 deletions Dependencies.xcworkspace/xcshareddata/swiftpm/Package.resolved
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"originHash" : "b78e1fa69770050e204024326b4d77e8451007e7f7ecc1b640d5889ffbe0b3a7",
"originHash" : "583c00d70f39319a7eca67f614e30ccaab233ad9a104a40007e982cf4584d4d6",
"pins" : [
{
"identity" : "combine-schedulers",
Expand All @@ -15,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 @@ -33,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 @@ -51,35 +51,35 @@
"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"
}
}
],
Expand Down
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
}
5 changes: 4 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ 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",
branch: "test-case-parameterization"
),
.package(url: "https://github.com/swiftlang/swift-syntax", "509.0.0"..<"601.0.0-prerelease"),
],
targets: [
Expand Down
5 changes: 4 additions & 1 deletion [email protected]
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ 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",
branch: "test-case-parameterization"
),
.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(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 typeName: String
#endif

init<T>(_ type: T.Type) {
self.id = ObjectIdentifier(type)
#if DEBUG
self.typeName = Dependencies.typeName(type)
#endif
}
stephencelis marked this conversation as resolved.
Show resolved Hide resolved

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.
51 changes: 31 additions & 20 deletions Tests/DependenciesTests/SwiftTestingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,42 @@
import Testing

struct SwiftTestingTests {
@Test
func cachePollution1() async {
await withDependencies {
$0 = DependencyValues()
} operation: {
@Dependency(\.cachedDependency) var cachedDependency: CachedDependency
let value = await cachedDependency.increment()
@Test(arguments: 1...5)
func parameterizedCachePollution(_: Int) {
@Dependency(Client.self) var client
let value = client.increment()
withKnownIssue(isIntermittent: true) {
#expect(value == 1)
}
}

@Test
func cachePollution2() async {
await 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
}
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