From b77d831d975694ead52c59f98c51a89614d3e9d8 Mon Sep 17 00:00:00 2001 From: Rachel Brindle Date: Thu, 28 Mar 2024 19:41:35 -0700 Subject: [PATCH 01/10] WIP Adding dynamic pendable --- Sources/Fakes/DynamicPendable.swift | 73 +++++++++++++++++++++ Tests/FakesTests/DynamicPendableTests.swift | 41 ++++++++++++ 2 files changed, 114 insertions(+) create mode 100644 Sources/Fakes/DynamicPendable.swift create mode 100644 Tests/FakesTests/DynamicPendableTests.swift diff --git a/Sources/Fakes/DynamicPendable.swift b/Sources/Fakes/DynamicPendable.swift new file mode 100644 index 0000000..8139108 --- /dev/null +++ b/Sources/Fakes/DynamicPendable.swift @@ -0,0 +1,73 @@ +import Foundation + +public final class DynamicPendable: @unchecked Sendable { + private enum State: Sendable { + case pending + case finished(Value) + } + + private let lock = NSRecursiveLock() + private var state = State.pending + + private var inProgressCalls = [UnsafeContinuation]() + + private let fallbackValue: Value + + deinit { + lock.lock() + if inProgressCalls.isEmpty == false { + self.resolve(with: fallbackValue) + } + lock.unlock() + } + + public static func finished(_ value: Value) -> DynamicPendable { + let pendable = DynamicPendable(fallbackValue: value) + pendable.resolve(with: value) + return pendable + } + + public init(fallbackValue: Value) { + self.fallbackValue = fallbackValue + } + + public convenience init() where Value == Void { + self.init(fallbackValue: ()) + } + + public convenience init() where Value == Optional { + self.init(fallbackValue: nil) + } + + public func call() async -> Value { + return await withUnsafeContinuation { continuation in + lock.lock() + defer { lock.unlock() } + switch state { + case .pending: + recordContinuation(continuation) + case .finished(let value): + continuation.resume(returning: value) + } + } + } + + public func call() async throws -> Success where Value == Result { + try await call().get() + } + + public func resolve(with value: Value) { + lock.lock() + self.state = .finished(value) + self.inProgressCalls.forEach { + $0.resume(returning: value) + } + self.inProgressCalls = [] + + lock.unlock() + } + + private func recordContinuation(_ continuation: UnsafeContinuation) { + self.inProgressCalls.append(continuation) + } +} diff --git a/Tests/FakesTests/DynamicPendableTests.swift b/Tests/FakesTests/DynamicPendableTests.swift new file mode 100644 index 0000000..2792d41 --- /dev/null +++ b/Tests/FakesTests/DynamicPendableTests.swift @@ -0,0 +1,41 @@ +import Fakes +import Nimble +import XCTest + +final class DynamicPendableTests: XCTestCase { + func testSingleCall() async { + let subject = DynamicPendable(fallbackValue: 0) + + async let result = subject.call() + + try! await Task.sleep(nanoseconds: UInt64(0.01 * 1_000_000_000)) + + subject.resolve(with: 2) + + let value = await result + expect(value).to(equal(2)) + } + + func testMultipleCalls() async { + let subject = DynamicPendable(fallbackValue: 0) + + async let result = withTaskGroup(of: Int.self, returning: [Int].self) { taskGroup in + for _ in 0..<100 { + taskGroup.addTask { await subject.call() } + } + + var results = [Int]() + for await value in taskGroup { + results.append(value) + } + return results + } + + try! await Task.sleep(nanoseconds: UInt64(0.1 * 1_000_000_000)) + + subject.resolve(with: 3) + + let value = await result + expect(value).to(equal(Array(repeating: 3, count: 100))) + } +} From f669335888694fdd36a5690a6981c160588b4a26 Mon Sep 17 00:00:00 2001 From: Rachel Brindle Date: Sun, 31 Mar 2024 16:03:09 -0700 Subject: [PATCH 02/10] Make sure that, when re-stubbing a spy, dynamic pendables are always resolved with their fallback. --- Sources/Fakes/DynamicPendable.swift | 16 ++++--- Sources/Fakes/Spy.swift | 30 ++++++++++++ Tests/FakesTests/SpyTests.swift | 74 +++++++++++++++++++++++++++++ 3 files changed, 114 insertions(+), 6 deletions(-) diff --git a/Sources/Fakes/DynamicPendable.swift b/Sources/Fakes/DynamicPendable.swift index 8139108..5bcfe4d 100644 --- a/Sources/Fakes/DynamicPendable.swift +++ b/Sources/Fakes/DynamicPendable.swift @@ -1,6 +1,10 @@ import Foundation -public final class DynamicPendable: @unchecked Sendable { +protocol ResolvableWithFallback { + func resolveWithFallback() +} + +public final class DynamicPendable: @unchecked Sendable, ResolvableWithFallback { private enum State: Sendable { case pending case finished(Value) @@ -14,11 +18,7 @@ public final class DynamicPendable: @unchecked Sendable { private let fallbackValue: Value deinit { - lock.lock() - if inProgressCalls.isEmpty == false { - self.resolve(with: fallbackValue) - } - lock.unlock() + resolveWithFallback() } public static func finished(_ value: Value) -> DynamicPendable { @@ -56,6 +56,10 @@ public final class DynamicPendable: @unchecked Sendable { try await call().get() } + public func resolveWithFallback() { + resolve(with: fallbackValue) + } + public func resolve(with value: Value) { lock.lock() self.state = .finished(value) diff --git a/Sources/Fakes/Spy.swift b/Sources/Fakes/Spy.swift index 5e715df..6e6e1a3 100644 --- a/Sources/Fakes/Spy.swift +++ b/Sources/Fakes/Spy.swift @@ -43,6 +43,10 @@ public final class Spy { /// - parameter value: The value to return when `callAsFunction()` is called. public func stub(_ value: Returning) { lock.lock() + + if let resolvable = _stub as? ResolvableWithFallback { + resolvable.resolveWithFallback() + } _stub = value lock.unlock() } @@ -70,4 +74,30 @@ extension Spy { } } +extension Spy { + // MARK: - Using DynamicPendable + + public convenience init(fallbackValue value: Value) where Returning == DynamicPendable { + self.init(.init(fallbackValue: value)) + } + + public convenience init() where Returning == DynamicPendable { + self.init(.init()) + } + + public func resolveStub(with value: Value) where Returning == DynamicPendable { + lock.lock() + defer { lock.unlock() } + _stub.resolve(with: value) + } + + public func callAsFunction(_ arguments: Arguments) async -> Value where Returning == DynamicPendable { + await call(arguments).call() + } + + public func callAsFunction() async -> Value where Arguments == Void, Returning == DynamicPendable { + await call(()).call() + } +} + extension Spy: @unchecked Sendable where Arguments: Sendable, Returning: Sendable {} diff --git a/Tests/FakesTests/SpyTests.swift b/Tests/FakesTests/SpyTests.swift index 703247f..f3e06d5 100644 --- a/Tests/FakesTests/SpyTests.swift +++ b/Tests/FakesTests/SpyTests.swift @@ -148,6 +148,80 @@ final class SpyTests: XCTestCase { subject.clearCalls() expect(subject.calls).to(beEmpty()) } + + func testDynamicPendable() async { + let subject = Spy>() + + let managedTask = ManagedTask { + await subject() + } + + await expect { await managedTask.isFinished }.toNever(beTrue()) + + subject.resolveStub(with: ()) + + await expect { await managedTask.isFinished }.toEventually(beTrue()) + } + + func testDynamicPendableDeinit() async { + let subject = Spy>() + + let managedTask = ManagedTask { + await subject() + } + + await expect { await managedTask.hasStarted }.toEventually(beTrue()) + + subject.stub(DynamicPendable()) + subject.resolveStub(with: ()) + + await expect { await managedTask.isFinished }.toEventually(beTrue()) + } +} + +actor ManagedTask { + var hasStarted = false + var isFinished = false + + var _task: Task! + + init(closure: @escaping () async throws -> Success) where Failure == Error { + _task = Task { + await self.recordStarted() + let result = try await closure() + await self.recordFinished() + return result + } + } + + init(closure: @escaping () async -> Success) where Failure == Never { + _task = Task { + await self.recordStarted() + let result = await closure() + await self.recordFinished() + return result + } + } + + private func recordStarted() { + self.hasStarted = true + } + + private func recordFinished() { + self.isFinished = true + } + + var result: Result { + get async { + await _task.result + } + } + + var value: Success { + get async throws { + try await _task.value + } + } } enum TestError: Error { From 496381bda63f64c46c8a4f198e5f4f8d79a93317 Mon Sep 17 00:00:00 2001 From: Rachel Brindle Date: Fri, 12 Apr 2024 13:08:56 -0700 Subject: [PATCH 03/10] DynamicPendable should autoresolve after a short delay Add inline documentation for DynamicPendable --- Sources/Fakes/DynamicPendable.swift | 153 ++++++++++++++++---- Sources/Fakes/Spy.swift | 4 +- Tests/FakesTests/DynamicPendableTests.swift | 18 ++- Tests/FakesTests/SpyTests.swift | 36 +++-- 4 files changed, 172 insertions(+), 39 deletions(-) diff --git a/Sources/Fakes/DynamicPendable.swift b/Sources/Fakes/DynamicPendable.swift index 5bcfe4d..98df0ba 100644 --- a/Sources/Fakes/DynamicPendable.swift +++ b/Sources/Fakes/DynamicPendable.swift @@ -4,6 +4,22 @@ protocol ResolvableWithFallback { func resolveWithFallback() } +/// DynamicPendable is a safe way to represent the 2 states that an asynchronous call can be in +/// +/// - `pending`, the state while waiting for the call to finish. +/// - `finished`, the state once the call has finished. +/// +/// DynamicPendable, as the name suggests, is dynamic - it allows you to finish a pending +/// call after it's been made. This makes DynamicPendable behave very similarly to something like +/// Combine's `Future`. +/// +/// - Note: The reason you must provide a fallback value is to prevent deadlock when used in test. +/// Unlike something like Combine's `Future`, it is very often the case that you will write +/// tests which end while the call is in the pending state. If you do this too much, then your +/// entire test suite will deadlock, as Swift Concurrency works under the assumption that +/// blocked tasks of work will always eventually be unblocked. To help prevent this, pending calls +/// are always resolved with the fallback after a given delay. You can also manually force this +/// by calling the ``resolveWithFallback`` method. public final class DynamicPendable: @unchecked Sendable, ResolvableWithFallback { private enum State: Sendable { case pending @@ -17,61 +33,146 @@ public final class DynamicPendable: @unchecked Sendable, Resolv private let fallbackValue: Value - deinit { - resolveWithFallback() + private var currentValue: Value { + switch state { + case .pending: + return fallbackValue + case .finished(let value): + return value + } } - public static func finished(_ value: Value) -> DynamicPendable { - let pendable = DynamicPendable(fallbackValue: value) - pendable.resolve(with: value) - return pendable + deinit { + resolveWithFallback() } + /// Initializes a new `DynamicPendable`, in a pending state, with the given fallback value. public init(fallbackValue: Value) { self.fallbackValue = fallbackValue } - public convenience init() where Value == Void { - self.init(fallbackValue: ()) + /// Gets the value for the `DynamicPendable`, possibly waiting until it's resolved. + /// + /// - parameter resolveDelay: The amount of time (in seconds) to wait until the call returns + /// the fallback value. This is only really used when the `DynamicPendable` is in a pending state. + public func call(resolveDelay: TimeInterval = PendableDefaults.delay) async -> Value { + return await withTaskGroup(of: Value.self) { taskGroup in + taskGroup.addTask { await self.handleCall() } + taskGroup.addTask { await self.resolveAfterDelay(resolveDelay) } + + guard let value = await taskGroup.next() else { + fatalError("There were no tasks in the task group. This should not ever happen.") + } + taskGroup.cancelAll() + return value + + } + } + + /// Resolves the `DynamicPendable` with the fallback value. + /// + /// Note: This no-ops if the pendable is already in a resolved state. + public func resolveWithFallback() { + lock.lock() + defer { lock.unlock() } + + if case .pending = state { + resolve(with: fallbackValue) + } + } + + /// Resolves the `DynamicPendable` with the given value. + /// + /// Even if the pendable is already resolves, this resets the resolved value to the given value. + public func resolve(with value: Value) { + lock.lock() + defer { lock.unlock() } + state = .finished(value) + inProgressCalls.forEach { + $0.resume(returning: value) + } + inProgressCalls = [] + } - public convenience init() where Value == Optional { - self.init(fallbackValue: nil) + /// Resolves any outstanding calls to the `DynamicPendable` with the current value, + /// and resets it back into the pending state. + public func reset() { + lock.lock() + defer { lock.unlock() } + + inProgressCalls.forEach { + $0.resume(returning: currentValue) + } + inProgressCalls = [] + state = .pending } - public func call() async -> Value { + // MARK: - Private + private func handleCall() async -> Value { return await withUnsafeContinuation { continuation in lock.lock() defer { lock.unlock() } switch state { case .pending: - recordContinuation(continuation) + inProgressCalls.append(continuation) case .finished(let value): continuation.resume(returning: value) } } } - public func call() async throws -> Success where Value == Result { - try await call().get() + private func resolveAfterDelay(_ delay: TimeInterval) async -> Value { + do { + try await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000)) + } catch {} + resolveWithFallback() + return fallbackValue } +} - public func resolveWithFallback() { - resolve(with: fallbackValue) +extension DynamicPendable { + /// Gets or throws value for the `DynamicPendable`, possibly waiting until it's resolved. + /// + /// - parameter resolveDelay: The amount of time (in seconds) to wait until the call returns + /// the fallback value. This is only really used when the `DynamicPendable` is in a pending state. + public func call(resolveDelay: TimeInterval = PendableDefaults.delay) async throws -> Success where Value == Result { + try await call(resolveDelay: resolveDelay).get() } +} - public func resolve(with value: Value) { - lock.lock() - self.state = .finished(value) - self.inProgressCalls.forEach { - $0.resume(returning: value) - } - self.inProgressCalls = [] +extension DynamicPendable { + /// Creates a new finished `DynamicPendable` pre-resolved with the given value. + public static func finished(_ value: Value) -> DynamicPendable { + let pendable = DynamicPendable(fallbackValue: value) + pendable.resolve(with: value) + return pendable + } + + /// Creates a new finished `DynamicPendable` pre-resolved with Void. + public static func finished() -> DynamicPendable where Value == Void { + return DynamicPendable.finished(()) + } + + /// Creates a new finished `DynamicPendable` pre-resolved with nil. + public static func finished() -> DynamicPendable where Value == Optional { + return DynamicPendable.finished(nil) + } +} + +extension DynamicPendable { + /// Creates a new pending `DynamicPendable` with the given fallback value. + public static func pending(fallback: Value) -> DynamicPendable { + return DynamicPendable(fallbackValue: fallback) + } - lock.unlock() + /// Creates a new pending `DynamicPendable` with a fallback value of Void. + public static func pending() -> DynamicPendable where Value == Void { + return DynamicPendable(fallbackValue: ()) } - private func recordContinuation(_ continuation: UnsafeContinuation) { - self.inProgressCalls.append(continuation) + /// Creates a new pending `DynamicPendable` with a fallback value of nil. + public static func pending() -> DynamicPendable where Value == Optional { + return DynamicPendable(fallbackValue: nil) } } diff --git a/Sources/Fakes/Spy.swift b/Sources/Fakes/Spy.swift index 6e6e1a3..df30428 100644 --- a/Sources/Fakes/Spy.swift +++ b/Sources/Fakes/Spy.swift @@ -78,11 +78,11 @@ extension Spy { // MARK: - Using DynamicPendable public convenience init(fallbackValue value: Value) where Returning == DynamicPendable { - self.init(.init(fallbackValue: value)) + self.init(.pending(fallback: value)) } public convenience init() where Returning == DynamicPendable { - self.init(.init()) + self.init(.pending()) } public func resolveStub(with value: Value) where Returning == DynamicPendable { diff --git a/Tests/FakesTests/DynamicPendableTests.swift b/Tests/FakesTests/DynamicPendableTests.swift index 2792d41..1c72b49 100644 --- a/Tests/FakesTests/DynamicPendableTests.swift +++ b/Tests/FakesTests/DynamicPendableTests.swift @@ -4,7 +4,7 @@ import XCTest final class DynamicPendableTests: XCTestCase { func testSingleCall() async { - let subject = DynamicPendable(fallbackValue: 0) + let subject = DynamicPendable.pending(fallback: 0) async let result = subject.call() @@ -17,7 +17,7 @@ final class DynamicPendableTests: XCTestCase { } func testMultipleCalls() async { - let subject = DynamicPendable(fallbackValue: 0) + let subject = DynamicPendable.pending(fallback: 0) async let result = withTaskGroup(of: Int.self, returning: [Int].self) { taskGroup in for _ in 0..<100 { @@ -38,4 +38,18 @@ final class DynamicPendableTests: XCTestCase { let value = await result expect(value).to(equal(Array(repeating: 3, count: 100))) } + + func testAutoresolve() async { + let subject = DynamicPendable.pending(fallback: 3) + + let expectation = self.expectation(description: "Autoresolves after the given delay") + + let task = Task { + _ = await subject.call(resolveDelay: 0.1) + expectation.fulfill() + } + + await self.fulfillment(of: [expectation], timeout: 1) + task.cancel() + } } diff --git a/Tests/FakesTests/SpyTests.swift b/Tests/FakesTests/SpyTests.swift index f3e06d5..373176f 100644 --- a/Tests/FakesTests/SpyTests.swift +++ b/Tests/FakesTests/SpyTests.swift @@ -152,7 +152,7 @@ final class SpyTests: XCTestCase { func testDynamicPendable() async { let subject = Spy>() - let managedTask = ManagedTask { + let managedTask = await ManagedTask.running { await subject() } @@ -166,13 +166,13 @@ final class SpyTests: XCTestCase { func testDynamicPendableDeinit() async { let subject = Spy>() - let managedTask = ManagedTask { + let managedTask = await ManagedTask.running { await subject() } await expect { await managedTask.hasStarted }.toEventually(beTrue()) - subject.stub(DynamicPendable()) + subject.stub(DynamicPendable.pending()) subject.resolveStub(with: ()) await expect { await managedTask.isFinished }.toEventually(beTrue()) @@ -185,20 +185,38 @@ actor ManagedTask { var _task: Task! - init(closure: @escaping () async throws -> Success) where Failure == Error { + static func running(closure: @escaping () async throws -> Success) async -> ManagedTask where Failure == Error { + let task = ManagedTask() + + await task.run(closure: closure) + + return task + } + + static func running(closure: @escaping () async -> Success) async -> ManagedTask where Failure == Never { + let task = ManagedTask() + + await task.run(closure: closure) + + return task + } + + private init() {} + + private func run(closure: @escaping () async throws -> Success) where Failure == Error { _task = Task { - await self.recordStarted() + self.recordStarted() let result = try await closure() - await self.recordFinished() + self.recordFinished() return result } } - init(closure: @escaping () async -> Success) where Failure == Never { + private func run(closure: @escaping () async -> Success) where Failure == Never { _task = Task { - await self.recordStarted() + self.recordStarted() let result = await closure() - await self.recordFinished() + self.recordFinished() return result } } From f17af302860feaf58684e6b9c007f391b7fc6ec1 Mon Sep 17 00:00:00 2001 From: Rachel Brindle Date: Fri, 12 Apr 2024 17:27:23 -0700 Subject: [PATCH 04/10] Integrate DynamicPendable with Spy more fully --- .../{ => Pendable}/DynamicPendable.swift | 15 ++-- .../StaticPendable.swift} | 8 +- Sources/Fakes/{ => Spy}/Spy+Nimble.swift | 0 Sources/Fakes/Spy/Spy+Pendable.swift | 73 ++++++++++++++++ Sources/Fakes/{ => Spy}/Spy+Result.swift | 0 .../Fakes/{ => Spy}/Spy+StaticPendable.swift | 20 ++--- .../Spy+StaticThrowingPendable.swift | 24 +++--- Sources/Fakes/Spy/Spy+ThrowingPendable.swift | 84 +++++++++++++++++++ Sources/Fakes/{ => Spy}/Spy.swift | 23 ++--- Tests/FakesTests/DynamicPendableTests.swift | 2 +- Tests/FakesTests/SpyTests.swift | 24 +++--- 11 files changed, 205 insertions(+), 68 deletions(-) rename Sources/Fakes/{ => Pendable}/DynamicPendable.swift (91%) rename Sources/Fakes/{Pendable.swift => Pendable/StaticPendable.swift} (93%) rename Sources/Fakes/{ => Spy}/Spy+Nimble.swift (100%) create mode 100644 Sources/Fakes/Spy/Spy+Pendable.swift rename Sources/Fakes/{ => Spy}/Spy+Result.swift (100%) rename Sources/Fakes/{ => Spy}/Spy+StaticPendable.swift (85%) rename Sources/Fakes/{ => Spy}/Spy+StaticThrowingPendable.swift (82%) create mode 100644 Sources/Fakes/Spy/Spy+ThrowingPendable.swift rename Sources/Fakes/{ => Spy}/Spy.swift (79%) diff --git a/Sources/Fakes/DynamicPendable.swift b/Sources/Fakes/Pendable/DynamicPendable.swift similarity index 91% rename from Sources/Fakes/DynamicPendable.swift rename to Sources/Fakes/Pendable/DynamicPendable.swift index 98df0ba..50db372 100644 --- a/Sources/Fakes/DynamicPendable.swift +++ b/Sources/Fakes/Pendable/DynamicPendable.swift @@ -53,12 +53,12 @@ public final class DynamicPendable: @unchecked Sendable, Resolv /// Gets the value for the `DynamicPendable`, possibly waiting until it's resolved. /// - /// - parameter resolveDelay: The amount of time (in seconds) to wait until the call returns + /// - parameter fallbackDelay: The amount of time (in seconds) to wait until the call returns /// the fallback value. This is only really used when the `DynamicPendable` is in a pending state. - public func call(resolveDelay: TimeInterval = PendableDefaults.delay) async -> Value { + public func call(fallbackDelay: TimeInterval = PendableDefaults.delay) async -> Value { return await withTaskGroup(of: Value.self) { taskGroup in taskGroup.addTask { await self.handleCall() } - taskGroup.addTask { await self.resolveAfterDelay(resolveDelay) } + taskGroup.addTask { await self.resolveAfterDelay(fallbackDelay) } guard let value = await taskGroup.next() else { fatalError("There were no tasks in the task group. This should not ever happen.") @@ -131,13 +131,15 @@ public final class DynamicPendable: @unchecked Sendable, Resolv } } +public typealias ThrowingDynamicPendable = DynamicPendable> + extension DynamicPendable { /// Gets or throws value for the `DynamicPendable`, possibly waiting until it's resolved. /// /// - parameter resolveDelay: The amount of time (in seconds) to wait until the call returns /// the fallback value. This is only really used when the `DynamicPendable` is in a pending state. public func call(resolveDelay: TimeInterval = PendableDefaults.delay) async throws -> Success where Value == Result { - try await call(resolveDelay: resolveDelay).get() + try await call(fallbackDelay: resolveDelay).get() } } @@ -153,11 +155,6 @@ extension DynamicPendable { public static func finished() -> DynamicPendable where Value == Void { return DynamicPendable.finished(()) } - - /// Creates a new finished `DynamicPendable` pre-resolved with nil. - public static func finished() -> DynamicPendable where Value == Optional { - return DynamicPendable.finished(nil) - } } extension DynamicPendable { diff --git a/Sources/Fakes/Pendable.swift b/Sources/Fakes/Pendable/StaticPendable.swift similarity index 93% rename from Sources/Fakes/Pendable.swift rename to Sources/Fakes/Pendable/StaticPendable.swift index 8f8b09b..2776423 100644 --- a/Sources/Fakes/Pendable.swift +++ b/Sources/Fakes/Pendable/StaticPendable.swift @@ -45,7 +45,7 @@ public final class PendableDefaults: @unchecked Sendable { /// the equivalent of forcing Swift Concurrency to wait until some other function returns. /// Which is possible, but incredibly tricky and very prone to deadlocks. /// Using a Static value for Pendable enables us to essentially cheat that. -public enum Pendable { +public enum StaticPendable { /// an in-progress call state /// /// The associated value is a fallback value. @@ -83,9 +83,9 @@ public enum Pendable { } } -public typealias ThrowingPendable< +public typealias ThrowingStaticPendable< Success, Failure: Error -> = Pendable> +> = StaticPendable> -extension Pendable: Sendable where Value: Sendable {} +extension StaticPendable: Sendable where Value: Sendable {} diff --git a/Sources/Fakes/Spy+Nimble.swift b/Sources/Fakes/Spy/Spy+Nimble.swift similarity index 100% rename from Sources/Fakes/Spy+Nimble.swift rename to Sources/Fakes/Spy/Spy+Nimble.swift diff --git a/Sources/Fakes/Spy/Spy+Pendable.swift b/Sources/Fakes/Spy/Spy+Pendable.swift new file mode 100644 index 0000000..c992fa7 --- /dev/null +++ b/Sources/Fakes/Spy/Spy+Pendable.swift @@ -0,0 +1,73 @@ +import Foundation + +public typealias PendableSpy = Spy> + +extension Spy { + /// Create a pendable Spy that is pre-stubbed to return return a a pending that will block for a bit before returning the fallback value. + public convenience init(pendingFallback: Value) where Returning == DynamicPendable { + self.init(.pending(fallback: pendingFallback)) + } + + /// Create a pendable Spy that is pre-stubbed to return return a a pending that will block for a bit before returning Void. + public convenience init() where Returning == DynamicPendable { + self.init(.pending(fallback: ())) + } + + /// Create a pendable Spy that is pre-stubbed to return a finished value. + public convenience init(finished: Value) where Returning == DynamicPendable { + self.init(.finished(finished)) + } +} + +extension Spy { + /// Update the pendable Spy's stub to be in a pending state. + public func stub(pendingFallback: Value) where Returning == DynamicPendable { + self.stub(.pending(fallback: pendingFallback)) + } + + /// Update the pendable Spy's stub to be in a pending state. + public func stubPending() where Returning == DynamicPendable { + self.stub(.pending(fallback: ())) + } + + /// Update the pendable Spy's stub to be in a pending state. + public func stubPending() where Returning == DynamicPendable> { + self.stub(.pending(fallback: nil)) + } + + /// Update the pendable Spy's stub to return the given value. + /// + /// - parameter finished: The value to return when `callAsFunction` is called. + public func stub(finished: Value) where Returning == DynamicPendable { + self.stub(.finished(finished)) + } + + /// Update the pendable Spy's stub to be in a pending state. + public func stubFinished() where Returning == DynamicPendable { + self.stub(.finished(())) + } +} + +extension Spy { + /// Records the arguments and handles the result according to ``Pendable/resolve(delay:)-hvhg``. + /// + /// - parameter arguments: The arguments to record. + /// - parameter fallbackDelay: The amount of seconds to delay if the `Pendable` is pending before + /// returning its fallback value. If the `Pendable` is finished, then this value is ignored. + public func callAsFunction( + _ arguments: Arguments, + fallbackDelay: TimeInterval = PendableDefaults.delay + ) async -> Value where Returning == DynamicPendable { + return await call(arguments).call(fallbackDelay: fallbackDelay) + } + + /// Records that a call was made and handles the result according to ``Pendable/resolve(delay:)-hvhg``. + /// + /// - parameter fallbackDelay: The amount of seconds to delay if the `Pendable` is pending before + /// returning its fallback value. If the `Pendable` is finished, then this value is ignored. + public func callAsFunction( + fallbackDelay: TimeInterval = PendableDefaults.delay + ) async -> Value where Arguments == Void, Returning == DynamicPendable { + return await call(()).call(fallbackDelay: fallbackDelay) + } +} diff --git a/Sources/Fakes/Spy+Result.swift b/Sources/Fakes/Spy/Spy+Result.swift similarity index 100% rename from Sources/Fakes/Spy+Result.swift rename to Sources/Fakes/Spy/Spy+Result.swift diff --git a/Sources/Fakes/Spy+StaticPendable.swift b/Sources/Fakes/Spy/Spy+StaticPendable.swift similarity index 85% rename from Sources/Fakes/Spy+StaticPendable.swift rename to Sources/Fakes/Spy/Spy+StaticPendable.swift index f7cca8a..374c0dd 100644 --- a/Sources/Fakes/Spy+StaticPendable.swift +++ b/Sources/Fakes/Spy/Spy+StaticPendable.swift @@ -1,44 +1,42 @@ import Foundation -public typealias PendableSpy = Spy> - extension Spy { /// Create a pendable Spy that is pre-stubbed to return return a a pending that will block for a bit before returning the fallback value. - public convenience init(pendingFallback: Value) where Returning == Pendable { + public convenience init(pendingFallback: Value) where Returning == StaticPendable { self.init(.pending(fallback: pendingFallback)) } /// Create a pendable Spy that is pre-stubbed to return return a a pending that will block for a bit before returning Void. - public convenience init() where Returning == Pendable { + public convenience init() where Returning == StaticPendable { self.init(.pending(fallback: ())) } /// Create a pendable Spy that is pre-stubbed to return a finished value. - public convenience init(finished: Value) where Returning == Pendable { + public convenience init(finished: Value) where Returning == StaticPendable { self.init(.finished(finished)) } } extension Spy { /// Update the pendable Spy's stub to be in a pending state. - public func stub(pendingFallback: Value) where Returning == Pendable { + public func stub(pendingFallback: Value) where Returning == StaticPendable { self.stub(.pending(fallback: pendingFallback)) } /// Update the pendable Spy's stub to be in a pending state. - public func stubPending() where Returning == Pendable { + public func stubPending() where Returning == StaticPendable { self.stub(.pending(fallback: ())) } /// Update the pendable Spy's stub to return the given value. /// /// - parameter finished: The value to return when `callAsFunction` is called. - public func stub(finished: Value) where Returning == Pendable { + public func stub(finished: Value) where Returning == StaticPendable { self.stub(.finished(finished)) } /// Update the pendable Spy's stub to be in a pending state. - public func stubFinished() where Returning == Pendable { + public func stubFinished() where Returning == StaticPendable { self.stub(.finished(())) } } @@ -55,7 +53,7 @@ extension Spy { public func callAsFunction( _ arguments: Arguments, pendingDelay: TimeInterval = PendableDefaults.delay - ) async -> Value where Returning == Pendable { + ) async -> Value where Returning == StaticPendable { return await call(arguments).resolve(delay: pendingDelay) } @@ -68,7 +66,7 @@ extension Spy { /// Alternatively, you can use the throwing version of `callAsFunction`, which will thorw an error instead of returning the fallback. public func callAsFunction( pendingDelay: TimeInterval = PendableDefaults.delay - ) async -> Value where Arguments == Void, Returning == Pendable { + ) async -> Value where Arguments == Void, Returning == StaticPendable { return await call(()).resolve(delay: pendingDelay) } } diff --git a/Sources/Fakes/Spy+StaticThrowingPendable.swift b/Sources/Fakes/Spy/Spy+StaticThrowingPendable.swift similarity index 82% rename from Sources/Fakes/Spy+StaticThrowingPendable.swift rename to Sources/Fakes/Spy/Spy+StaticThrowingPendable.swift index 36bf885..3a492fe 100644 --- a/Sources/Fakes/Spy+StaticThrowingPendable.swift +++ b/Sources/Fakes/Spy/Spy+StaticThrowingPendable.swift @@ -1,56 +1,54 @@ import Foundation -public typealias ThrowingPendableSpy = Spy> - extension Spy { /// Create a throwing pendable Spy that is pre-stubbed to return a pending that will block for a bit before returning success. - public convenience init(pendingSuccess: Success) where Returning == ThrowingPendable { + public convenience init(pendingSuccess: Success) where Returning == ThrowingStaticPendable { self.init(.pending(fallback: .success(pendingSuccess))) } /// Create a throwing pendable Spy that is pre-stubbed to return a pending that will block for a bit before returning Void. - public convenience init() where Returning == ThrowingPendable<(), Failure> { + public convenience init() where Returning == ThrowingStaticPendable<(), Failure> { self.init(.pending(fallback: .success(()))) } /// Create a throwing pendable Spy that is pre-stubbed to return a pending that will block for a bit before throwing an error. - public convenience init(pendingFailure: Failure) where Returning == ThrowingPendable { + public convenience init(pendingFailure: Failure) where Returning == ThrowingStaticPendable { self.init(.pending(fallback: .failure(pendingFailure))) } /// Create a throwing pendable Spy that is pre-stubbed to return a finished & successful value. - public convenience init(success: Success) where Returning == ThrowingPendable { + public convenience init(success: Success) where Returning == ThrowingStaticPendable { self.init(.finished(.success(success))) } /// Create a throwing pendable Spy that is pre-stubbed to throw the given error. - public convenience init(failure: Failure) where Returning == ThrowingPendable { + public convenience init(failure: Failure) where Returning == ThrowingStaticPendable { self.init(.finished(.failure(failure))) } } extension Spy { /// Update the pendable Spy's stub to be in a pending state. - public func stub(pendingSuccess: Success) where Returning == ThrowingPendable { + public func stub(pendingSuccess: Success) where Returning == ThrowingStaticPendable { self.stub(.pending(fallback: .success(pendingSuccess))) } /// Update the pendable Spy's stub to be in a pending state. - public func stub(pendingFailure: Failure) where Returning == ThrowingPendable { + public func stub(pendingFailure: Failure) where Returning == ThrowingStaticPendable { self.stub(.pending(fallback: .failure(pendingFailure))) } /// Update the throwing pendable Spy's stub to be successful, with the given value. /// /// - parameter success: The value to return when `callAsFunction` is called. - public func stub(success: Success) where Returning == ThrowingPendable { + public func stub(success: Success) where Returning == ThrowingStaticPendable { self.stub(.finished(.success(success))) } /// Update the throwing pendable Spy's stub to throw the given error. /// /// - parameter failure: The error to throw when `callAsFunction` is called. - public func stub(failure: Failure) where Returning == ThrowingPendable { + public func stub(failure: Failure) where Returning == ThrowingStaticPendable { self.stub(.finished(.failure(failure))) } } @@ -66,7 +64,7 @@ extension Spy { public func callAsFunction( _ arguments: Arguments, pendingDelay: TimeInterval = PendableDefaults.delay - ) async throws -> Success where Returning == ThrowingPendable { + ) async throws -> Success where Returning == ThrowingStaticPendable { return try await call(arguments).resolve(delay: pendingDelay) } @@ -77,7 +75,7 @@ extension Spy { /// throwing a `PendableInProgressError`. If the `Pendable` is .finished, then this value is ignored. public func callAsFunction( pendingDelay: TimeInterval = PendableDefaults.delay - ) async throws -> Success where Arguments == Void, Returning == ThrowingPendable { + ) async throws -> Success where Arguments == Void, Returning == ThrowingStaticPendable { return try await call(()).resolve(delay: pendingDelay) } } diff --git a/Sources/Fakes/Spy/Spy+ThrowingPendable.swift b/Sources/Fakes/Spy/Spy+ThrowingPendable.swift new file mode 100644 index 0000000..fe41614 --- /dev/null +++ b/Sources/Fakes/Spy/Spy+ThrowingPendable.swift @@ -0,0 +1,84 @@ +import Foundation + +public typealias ThrowingPendableSpy = Spy> + +extension Spy { + /// Create a throwing pendable Spy that is pre-stubbed to return a pending that will block for a bit before returning success. + public convenience init(pendingSuccess: Success) where Returning == ThrowingDynamicPendable { + self.init(.pending(fallback: .success(pendingSuccess))) + } + + /// Create a throwing pendable Spy that is pre-stubbed to return a pending that will block for a bit before returning Void. + public convenience init() where Returning == ThrowingDynamicPendable<(), Failure> { + self.init(.pending(fallback: .success(()))) + } + + /// Create a throwing pendable Spy that is pre-stubbed to return a pending that will block for a bit before throwing an error. + public convenience init(pendingFailure: Failure) where Returning == ThrowingDynamicPendable { + self.init(.pending(fallback: .failure(pendingFailure))) + } + + /// Create a throwing pendable Spy that is pre-stubbed to return a finished & successful value. + public convenience init(success: Success) where Returning == ThrowingDynamicPendable { + self.init(.finished(.success(success))) + } + + /// Create a throwing pendable Spy that is pre-stubbed to throw the given error. + public convenience init(failure: Failure) where Returning == ThrowingDynamicPendable { + self.init(.finished(.failure(failure))) + } +} + +extension Spy { + /// Update the pendable Spy's stub to be in a pending state. + public func stub(pendingSuccess: Success) where Returning == ThrowingDynamicPendable { + self.stub(.pending(fallback: .success(pendingSuccess))) + } + + /// Update the pendable Spy's stub to be in a pending state. + public func stub(pendingFailure: Failure) where Returning == ThrowingDynamicPendable { + self.stub(.pending(fallback: .failure(pendingFailure))) + } + + /// Update the throwing pendable Spy's stub to be successful, with the given value. + /// + /// - parameter success: The value to return when `callAsFunction` is called. + public func stub(success: Success) where Returning == ThrowingDynamicPendable { + self.stub(.finished(.success(success))) + } + + /// Update the throwing pendable Spy's stub to throw the given error. + /// + /// - parameter failure: The error to throw when `callAsFunction` is called. + public func stub(failure: Failure) where Returning == ThrowingDynamicPendable { + self.stub(.finished(.failure(failure))) + } +} + +extension Spy { + // Returning == ThrowingPendable + /// Records the arguments and handles the result according to ``Pendable/resolve(delay:)-1bb25``. + /// This call then throws or returns the success, according to `Result.get`. + /// + /// - parameter arguments: The arguments to record. + /// - parameter pendingDelay: The amount of seconds to delay if the `Pendable` is .pending before + /// throwing a `PendableInProgressError`. If the `Pendable` is .finished, then this value is ignored. + public func callAsFunction( + _ arguments: Arguments, + fallbackDelay: TimeInterval = PendableDefaults.delay + ) async throws -> Success where Returning == ThrowingDynamicPendable { + return try await call(arguments).call(fallbackDelay: fallbackDelay).get() + } + + /// Records that a call was made and handles the result according to ``Pendable/resolve(delay:)-1bb25``. + /// This call then throws or returns the success, according to `Result.get`. + /// + /// - parameter pendingDelay: The amount of seconds to delay if the `Pendable` is .pending before + /// throwing a `PendableInProgressError`. If the `Pendable` is .finished, then this value is ignored. + public func callAsFunction( + fallbackDelay: TimeInterval = PendableDefaults.delay + ) async throws -> Success where Arguments == Void, Returning == ThrowingDynamicPendable { + return try await call(()).call(fallbackDelay: fallbackDelay).get() + } +} + diff --git a/Sources/Fakes/Spy.swift b/Sources/Fakes/Spy/Spy.swift similarity index 79% rename from Sources/Fakes/Spy.swift rename to Sources/Fakes/Spy/Spy.swift index df30428..0cc5c83 100644 --- a/Sources/Fakes/Spy.swift +++ b/Sources/Fakes/Spy/Spy.swift @@ -27,6 +27,11 @@ public final class Spy { self.init(()) } + /// Create a Spy that returns nil + public convenience init() where Returning == Optional { + self.init(nil) + } + /// Clear out existing call records. /// /// This removes all previously recorded calls from the spy. It does not otherwise @@ -75,29 +80,11 @@ extension Spy { } extension Spy { - // MARK: - Using DynamicPendable - - public convenience init(fallbackValue value: Value) where Returning == DynamicPendable { - self.init(.pending(fallback: value)) - } - - public convenience init() where Returning == DynamicPendable { - self.init(.pending()) - } - public func resolveStub(with value: Value) where Returning == DynamicPendable { lock.lock() defer { lock.unlock() } _stub.resolve(with: value) } - - public func callAsFunction(_ arguments: Arguments) async -> Value where Returning == DynamicPendable { - await call(arguments).call() - } - - public func callAsFunction() async -> Value where Arguments == Void, Returning == DynamicPendable { - await call(()).call() - } } extension Spy: @unchecked Sendable where Arguments: Sendable, Returning: Sendable {} diff --git a/Tests/FakesTests/DynamicPendableTests.swift b/Tests/FakesTests/DynamicPendableTests.swift index 1c72b49..59c013b 100644 --- a/Tests/FakesTests/DynamicPendableTests.swift +++ b/Tests/FakesTests/DynamicPendableTests.swift @@ -45,7 +45,7 @@ final class DynamicPendableTests: XCTestCase { let expectation = self.expectation(description: "Autoresolves after the given delay") let task = Task { - _ = await subject.call(resolveDelay: 0.1) + _ = await subject.call(fallbackDelay: 0.1) expectation.fulfill() } diff --git a/Tests/FakesTests/SpyTests.swift b/Tests/FakesTests/SpyTests.swift index 373176f..c632736 100644 --- a/Tests/FakesTests/SpyTests.swift +++ b/Tests/FakesTests/SpyTests.swift @@ -94,20 +94,20 @@ final class SpyTests: XCTestCase { let subject = PendableSpy(pendingFallback: 1) await expect { - await subject(pendingDelay: 0) + await subject(fallbackDelay: 0) }.toEventually(equal(1)) subject.stub(finished: 4) await expect { - await subject(pendingDelay: 0) + await subject(fallbackDelay: 0) }.toEventually(equal(4)) } func testPendableTakesNonVoidArguments() async throws { let subject = PendableSpy(finished: ()) - await subject(3, pendingDelay: 0) + await subject(3, fallbackDelay: 0) expect(subject.calls).to(equal([3])) } @@ -116,25 +116,25 @@ final class SpyTests: XCTestCase { let subject = ThrowingPendableSpy(pendingSuccess: 0) await expect { - try await subject(pendingDelay: 0) + try await subject(fallbackDelay: 0) }.toEventually(equal(0)) subject.stub(success: 5) await expect { - try await subject(pendingDelay: 0) + try await subject(fallbackDelay: 0) }.toEventually(equal(5)) subject.stub(failure: TestError.uhOh) await expect { - try await subject(pendingDelay: 0) + try await subject(fallbackDelay: 0) }.toEventually(throwError(TestError.uhOh)) } func testThrowingPendableTakesNonVoidArguments() async throws { let subject = ThrowingPendableSpy(success: ()) - try await subject(8, pendingDelay: 0) + try await subject(8, fallbackDelay: 0) expect(subject.calls).to(equal([8])) } @@ -179,13 +179,13 @@ final class SpyTests: XCTestCase { } } -actor ManagedTask { +actor ManagedTask { var hasStarted = false var isFinished = false var _task: Task! - static func running(closure: @escaping () async throws -> Success) async -> ManagedTask where Failure == Error { + static func running(closure: @escaping @Sendable () async throws -> Success) async -> ManagedTask where Failure == Error { let task = ManagedTask() await task.run(closure: closure) @@ -193,7 +193,7 @@ actor ManagedTask { return task } - static func running(closure: @escaping () async -> Success) async -> ManagedTask where Failure == Never { + static func running(closure: @escaping @Sendable () async -> Success) async -> ManagedTask where Failure == Never { let task = ManagedTask() await task.run(closure: closure) @@ -203,7 +203,7 @@ actor ManagedTask { private init() {} - private func run(closure: @escaping () async throws -> Success) where Failure == Error { + private func run(closure: @escaping @Sendable () async throws -> Success) where Failure == Error { _task = Task { self.recordStarted() let result = try await closure() @@ -212,7 +212,7 @@ actor ManagedTask { } } - private func run(closure: @escaping () async -> Success) where Failure == Never { + private func run(closure: @escaping @Sendable () async -> Success) where Failure == Never { _task = Task { self.recordStarted() let result = await closure() From 3dc02095567ae6642d17829e711c98caef2adf65 Mon Sep 17 00:00:00 2001 From: Rachel Brindle Date: Fri, 12 Apr 2024 17:28:42 -0700 Subject: [PATCH 05/10] Remove Static Pendable --- Sources/Fakes/Pendable/PendableDefaults.swift | 32 +++++++ Sources/Fakes/Pendable/StaticPendable.swift | 91 ------------------- Sources/Fakes/Spy/Spy+StaticPendable.swift | 72 --------------- .../Spy/Spy+StaticThrowingPendable.swift | 81 ----------------- 4 files changed, 32 insertions(+), 244 deletions(-) create mode 100644 Sources/Fakes/Pendable/PendableDefaults.swift delete mode 100644 Sources/Fakes/Pendable/StaticPendable.swift delete mode 100644 Sources/Fakes/Spy/Spy+StaticPendable.swift delete mode 100644 Sources/Fakes/Spy/Spy+StaticThrowingPendable.swift diff --git a/Sources/Fakes/Pendable/PendableDefaults.swift b/Sources/Fakes/Pendable/PendableDefaults.swift new file mode 100644 index 0000000..13b82fb --- /dev/null +++ b/Sources/Fakes/Pendable/PendableDefaults.swift @@ -0,0 +1,32 @@ +import Foundation + +/// Default values for use with Pendable. +public final class PendableDefaults: @unchecked Sendable { + public static let shared = PendableDefaults() + private let lock = NSLock() + + public init() {} + + public static var delay: TimeInterval { + get { + PendableDefaults.shared.delay + } + set { + PendableDefaults.shared.delay = newValue + } + } + + private var _delay: TimeInterval = 1 + public var delay: TimeInterval { + get { + lock.lock() + defer { lock.unlock() } + return _delay + } + set { + lock.lock() + _delay = newValue + lock.unlock() + } + } +} diff --git a/Sources/Fakes/Pendable/StaticPendable.swift b/Sources/Fakes/Pendable/StaticPendable.swift deleted file mode 100644 index 2776423..0000000 --- a/Sources/Fakes/Pendable/StaticPendable.swift +++ /dev/null @@ -1,91 +0,0 @@ -import Foundation - -/// Default values for use with Pendable. -public final class PendableDefaults: @unchecked Sendable { - public static let shared = PendableDefaults() - private let lock = NSLock() - - public init() {} - - public static var delay: TimeInterval { - get { - PendableDefaults.shared.delay - } - set { - PendableDefaults.shared.delay = newValue - } - } - - private var _delay: TimeInterval = 1 - public var delay: TimeInterval { - get { - lock.lock() - defer { lock.unlock() } - return _delay - } - set { - lock.lock() - _delay = newValue - lock.unlock() - } - } -} - -/// Pendable represents the 2 states that an asynchronous call can be in -/// -/// - `pending`, the state while waiting for some asynchronous call to finish -/// - `finished`, the state once an asynchronous call has finished. -/// -/// Oftentimes, async calls also throw. For that, use the `ThrowingPendable` type. -/// `ThrowingPendable` is a typealias for when `Value` is -/// a `Result`. -/// -/// Pendable is a static value, there is no way to dynamically resolve a Pendable. -/// This is because to allow you to resolve the call whenever you want is -/// the equivalent of forcing Swift Concurrency to wait until some other function returns. -/// Which is possible, but incredibly tricky and very prone to deadlocks. -/// Using a Static value for Pendable enables us to essentially cheat that. -public enum StaticPendable { - /// an in-progress call state - /// - /// The associated value is a fallback value. - case pending(fallback: Value) - - /// a finished call state - case finished(Value) - - public func resolve( - delay: TimeInterval = PendableDefaults.delay - ) async -> Value { - switch self { - case .pending(let fallback): - _ = try? await Task.sleep( - nanoseconds: UInt64(1_000_000_000 * delay) - ) - return fallback - case .finished(let value): - return value - } - } - - public func resolve( - delay: TimeInterval = PendableDefaults.delay - ) async throws -> Success where Value == Result { - switch self { - case .pending(let fallback): - _ = try? await Task.sleep( - nanoseconds: UInt64(1_000_000_000 * delay) - ) - return try fallback.get() - case .finished(let value): - return try value.get() - } - } -} - -public typealias ThrowingStaticPendable< - Success, - Failure: Error -> = StaticPendable> - -extension StaticPendable: Sendable where Value: Sendable {} diff --git a/Sources/Fakes/Spy/Spy+StaticPendable.swift b/Sources/Fakes/Spy/Spy+StaticPendable.swift deleted file mode 100644 index 374c0dd..0000000 --- a/Sources/Fakes/Spy/Spy+StaticPendable.swift +++ /dev/null @@ -1,72 +0,0 @@ -import Foundation - -extension Spy { - /// Create a pendable Spy that is pre-stubbed to return return a a pending that will block for a bit before returning the fallback value. - public convenience init(pendingFallback: Value) where Returning == StaticPendable { - self.init(.pending(fallback: pendingFallback)) - } - - /// Create a pendable Spy that is pre-stubbed to return return a a pending that will block for a bit before returning Void. - public convenience init() where Returning == StaticPendable { - self.init(.pending(fallback: ())) - } - - /// Create a pendable Spy that is pre-stubbed to return a finished value. - public convenience init(finished: Value) where Returning == StaticPendable { - self.init(.finished(finished)) - } -} - -extension Spy { - /// Update the pendable Spy's stub to be in a pending state. - public func stub(pendingFallback: Value) where Returning == StaticPendable { - self.stub(.pending(fallback: pendingFallback)) - } - - /// Update the pendable Spy's stub to be in a pending state. - public func stubPending() where Returning == StaticPendable { - self.stub(.pending(fallback: ())) - } - - /// Update the pendable Spy's stub to return the given value. - /// - /// - parameter finished: The value to return when `callAsFunction` is called. - public func stub(finished: Value) where Returning == StaticPendable { - self.stub(.finished(finished)) - } - - /// Update the pendable Spy's stub to be in a pending state. - public func stubFinished() where Returning == StaticPendable { - self.stub(.finished(())) - } -} - -extension Spy { - /// Records the arguments and handles the result according to ``Pendable/resolve(delay:)-hvhg``. - /// - /// - parameter arguments: The arguments to record. - /// - parameter pendingDelay: The amount of seconds to delay if the `Pendable` is .pending before - /// returning the `pendingFallback`. If the `Pendable` is .finished, then this value is ignored. - /// - /// Because of how ``Pendable`` currently works, you must provide a fallback option for when the Pendable is pending. - /// Alternatively, you can use the throwing version of `callAsFunction`, which will thorw an error instead of returning the fallback. - public func callAsFunction( - _ arguments: Arguments, - pendingDelay: TimeInterval = PendableDefaults.delay - ) async -> Value where Returning == StaticPendable { - return await call(arguments).resolve(delay: pendingDelay) - } - - /// Records that a call was made and handles the result according to ``Pendable/resolve(delay:)-hvhg``. - /// - /// - parameter pendingDelay: The amount of seconds to delay if the `Pendable` is .pending before - /// returning the `pendingFallback`. If the `Pendable` is .finished, then this value is ignored. - /// - /// Because of how ``Pendable`` currently works, you must provide a fallback option for when the Pendable is pending. - /// Alternatively, you can use the throwing version of `callAsFunction`, which will thorw an error instead of returning the fallback. - public func callAsFunction( - pendingDelay: TimeInterval = PendableDefaults.delay - ) async -> Value where Arguments == Void, Returning == StaticPendable { - return await call(()).resolve(delay: pendingDelay) - } -} diff --git a/Sources/Fakes/Spy/Spy+StaticThrowingPendable.swift b/Sources/Fakes/Spy/Spy+StaticThrowingPendable.swift deleted file mode 100644 index 3a492fe..0000000 --- a/Sources/Fakes/Spy/Spy+StaticThrowingPendable.swift +++ /dev/null @@ -1,81 +0,0 @@ -import Foundation - -extension Spy { - /// Create a throwing pendable Spy that is pre-stubbed to return a pending that will block for a bit before returning success. - public convenience init(pendingSuccess: Success) where Returning == ThrowingStaticPendable { - self.init(.pending(fallback: .success(pendingSuccess))) - } - - /// Create a throwing pendable Spy that is pre-stubbed to return a pending that will block for a bit before returning Void. - public convenience init() where Returning == ThrowingStaticPendable<(), Failure> { - self.init(.pending(fallback: .success(()))) - } - - /// Create a throwing pendable Spy that is pre-stubbed to return a pending that will block for a bit before throwing an error. - public convenience init(pendingFailure: Failure) where Returning == ThrowingStaticPendable { - self.init(.pending(fallback: .failure(pendingFailure))) - } - - /// Create a throwing pendable Spy that is pre-stubbed to return a finished & successful value. - public convenience init(success: Success) where Returning == ThrowingStaticPendable { - self.init(.finished(.success(success))) - } - - /// Create a throwing pendable Spy that is pre-stubbed to throw the given error. - public convenience init(failure: Failure) where Returning == ThrowingStaticPendable { - self.init(.finished(.failure(failure))) - } -} - -extension Spy { - /// Update the pendable Spy's stub to be in a pending state. - public func stub(pendingSuccess: Success) where Returning == ThrowingStaticPendable { - self.stub(.pending(fallback: .success(pendingSuccess))) - } - - /// Update the pendable Spy's stub to be in a pending state. - public func stub(pendingFailure: Failure) where Returning == ThrowingStaticPendable { - self.stub(.pending(fallback: .failure(pendingFailure))) - } - - /// Update the throwing pendable Spy's stub to be successful, with the given value. - /// - /// - parameter success: The value to return when `callAsFunction` is called. - public func stub(success: Success) where Returning == ThrowingStaticPendable { - self.stub(.finished(.success(success))) - } - - /// Update the throwing pendable Spy's stub to throw the given error. - /// - /// - parameter failure: The error to throw when `callAsFunction` is called. - public func stub(failure: Failure) where Returning == ThrowingStaticPendable { - self.stub(.finished(.failure(failure))) - } -} - -extension Spy { - // Returning == ThrowingPendable - /// Records the arguments and handles the result according to ``Pendable/resolve(delay:)-1bb25``. - /// This call then throws or returns the success, according to `Result.get`. - /// - /// - parameter arguments: The arguments to record. - /// - parameter pendingDelay: The amount of seconds to delay if the `Pendable` is .pending before - /// throwing a `PendableInProgressError`. If the `Pendable` is .finished, then this value is ignored. - public func callAsFunction( - _ arguments: Arguments, - pendingDelay: TimeInterval = PendableDefaults.delay - ) async throws -> Success where Returning == ThrowingStaticPendable { - return try await call(arguments).resolve(delay: pendingDelay) - } - - /// Records that a call was made and handles the result according to ``Pendable/resolve(delay:)-1bb25``. - /// This call then throws or returns the success, according to `Result.get`. - /// - /// - parameter pendingDelay: The amount of seconds to delay if the `Pendable` is .pending before - /// throwing a `PendableInProgressError`. If the `Pendable` is .finished, then this value is ignored. - public func callAsFunction( - pendingDelay: TimeInterval = PendableDefaults.delay - ) async throws -> Success where Arguments == Void, Returning == ThrowingStaticPendable { - return try await call(()).resolve(delay: pendingDelay) - } -} From 75d108316d49834779fb8ff9a1ae9871e1877d60 Mon Sep 17 00:00:00 2001 From: Rachel Brindle Date: Fri, 12 Apr 2024 17:34:34 -0700 Subject: [PATCH 06/10] Rename DynamicPendable to Pendable --- .../{DynamicPendable.swift => Pendable.swift} | 68 +++++++++---------- Sources/Fakes/Spy/Spy+Pendable.swift | 26 +++---- Sources/Fakes/Spy/Spy+ThrowingPendable.swift | 4 +- Sources/Fakes/Spy/Spy.swift | 2 +- Tests/FakesTests/DynamicPendableTests.swift | 6 +- Tests/FakesTests/SpyTests.swift | 6 +- 6 files changed, 56 insertions(+), 56 deletions(-) rename Sources/Fakes/Pendable/{DynamicPendable.swift => Pendable.swift} (62%) diff --git a/Sources/Fakes/Pendable/DynamicPendable.swift b/Sources/Fakes/Pendable/Pendable.swift similarity index 62% rename from Sources/Fakes/Pendable/DynamicPendable.swift rename to Sources/Fakes/Pendable/Pendable.swift index 50db372..27058a3 100644 --- a/Sources/Fakes/Pendable/DynamicPendable.swift +++ b/Sources/Fakes/Pendable/Pendable.swift @@ -4,14 +4,13 @@ protocol ResolvableWithFallback { func resolveWithFallback() } -/// DynamicPendable is a safe way to represent the 2 states that an asynchronous call can be in +/// Pendable is a safe way to represent the 2 states that an asynchronous call can be in /// /// - `pending`, the state while waiting for the call to finish. /// - `finished`, the state once the call has finished. /// -/// DynamicPendable, as the name suggests, is dynamic - it allows you to finish a pending -/// call after it's been made. This makes DynamicPendable behave very similarly to something like -/// Combine's `Future`. +/// Pendable allows you to finish a pending call after it's been made. This makes Pendable behave very +/// similarly to something like Combine's `Future`. /// /// - Note: The reason you must provide a fallback value is to prevent deadlock when used in test. /// Unlike something like Combine's `Future`, it is very often the case that you will write @@ -19,8 +18,8 @@ protocol ResolvableWithFallback { /// entire test suite will deadlock, as Swift Concurrency works under the assumption that /// blocked tasks of work will always eventually be unblocked. To help prevent this, pending calls /// are always resolved with the fallback after a given delay. You can also manually force this -/// by calling the ``resolveWithFallback`` method. -public final class DynamicPendable: @unchecked Sendable, ResolvableWithFallback { +/// by calling the ``Pendable\resolveWithFallback()`` method. +public final class Pendable: @unchecked Sendable, ResolvableWithFallback { private enum State: Sendable { case pending case finished(Value) @@ -46,15 +45,15 @@ public final class DynamicPendable: @unchecked Sendable, Resolv resolveWithFallback() } - /// Initializes a new `DynamicPendable`, in a pending state, with the given fallback value. + /// Initializes a new `Pendable`, in a pending state, with the given fallback value. public init(fallbackValue: Value) { self.fallbackValue = fallbackValue } - /// Gets the value for the `DynamicPendable`, possibly waiting until it's resolved. + /// Gets the value for the `Pendable`, possibly waiting until it's resolved. /// /// - parameter fallbackDelay: The amount of time (in seconds) to wait until the call returns - /// the fallback value. This is only really used when the `DynamicPendable` is in a pending state. + /// the fallback value. This is only used when the `Pendable` is in a pending state. public func call(fallbackDelay: TimeInterval = PendableDefaults.delay) async -> Value { return await withTaskGroup(of: Value.self) { taskGroup in taskGroup.addTask { await self.handleCall() } @@ -69,9 +68,10 @@ public final class DynamicPendable: @unchecked Sendable, Resolv } } - /// Resolves the `DynamicPendable` with the fallback value. + /// Resolves the `Pendable` with the fallback value. /// - /// Note: This no-ops if the pendable is already in a resolved state. + /// - Note: This no-ops if the pendable is already in a resolved state. + /// - Note: This is called for when you re-stub a `Spy` in ``Spy/stub(_:)`` public func resolveWithFallback() { lock.lock() defer { lock.unlock() } @@ -81,7 +81,7 @@ public final class DynamicPendable: @unchecked Sendable, Resolv } } - /// Resolves the `DynamicPendable` with the given value. + /// Resolves the `Pendable` with the given value. /// /// Even if the pendable is already resolves, this resets the resolved value to the given value. public func resolve(with value: Value) { @@ -95,7 +95,7 @@ public final class DynamicPendable: @unchecked Sendable, Resolv } - /// Resolves any outstanding calls to the `DynamicPendable` with the current value, + /// Resolves any outstanding calls to the `Pendable` with the current value, /// and resets it back into the pending state. public func reset() { lock.lock() @@ -131,45 +131,45 @@ public final class DynamicPendable: @unchecked Sendable, Resolv } } -public typealias ThrowingDynamicPendable = DynamicPendable> +public typealias ThrowingDynamicPendable = Pendable> -extension DynamicPendable { - /// Gets or throws value for the `DynamicPendable`, possibly waiting until it's resolved. +extension Pendable { + /// Gets or throws value for the `Pendable`, possibly waiting until it's resolved. /// /// - parameter resolveDelay: The amount of time (in seconds) to wait until the call returns - /// the fallback value. This is only really used when the `DynamicPendable` is in a pending state. + /// the fallback value. This is only used when the `Pendable` is in a pending state. public func call(resolveDelay: TimeInterval = PendableDefaults.delay) async throws -> Success where Value == Result { try await call(fallbackDelay: resolveDelay).get() } } -extension DynamicPendable { - /// Creates a new finished `DynamicPendable` pre-resolved with the given value. - public static func finished(_ value: Value) -> DynamicPendable { - let pendable = DynamicPendable(fallbackValue: value) +extension Pendable { + /// Creates a new finished `Pendable` pre-resolved with the given value. + public static func finished(_ value: Value) -> Pendable { + let pendable = Pendable(fallbackValue: value) pendable.resolve(with: value) return pendable } - /// Creates a new finished `DynamicPendable` pre-resolved with Void. - public static func finished() -> DynamicPendable where Value == Void { - return DynamicPendable.finished(()) + /// Creates a new finished `Pendable` pre-resolved with Void. + public static func finished() -> Pendable where Value == Void { + return Pendable.finished(()) } } -extension DynamicPendable { - /// Creates a new pending `DynamicPendable` with the given fallback value. - public static func pending(fallback: Value) -> DynamicPendable { - return DynamicPendable(fallbackValue: fallback) +extension Pendable { + /// Creates a new pending `Pendable` with the given fallback value. + public static func pending(fallback: Value) -> Pendable { + return Pendable(fallbackValue: fallback) } - /// Creates a new pending `DynamicPendable` with a fallback value of Void. - public static func pending() -> DynamicPendable where Value == Void { - return DynamicPendable(fallbackValue: ()) + /// Creates a new pending `Pendable` with a fallback value of Void. + public static func pending() -> Pendable where Value == Void { + return Pendable(fallbackValue: ()) } - /// Creates a new pending `DynamicPendable` with a fallback value of nil. - public static func pending() -> DynamicPendable where Value == Optional { - return DynamicPendable(fallbackValue: nil) + /// Creates a new pending `Pendable` with a fallback value of nil. + public static func pending() -> Pendable where Value == Optional { + return Pendable(fallbackValue: nil) } } diff --git a/Sources/Fakes/Spy/Spy+Pendable.swift b/Sources/Fakes/Spy/Spy+Pendable.swift index c992fa7..51de0ea 100644 --- a/Sources/Fakes/Spy/Spy+Pendable.swift +++ b/Sources/Fakes/Spy/Spy+Pendable.swift @@ -1,55 +1,55 @@ import Foundation -public typealias PendableSpy = Spy> +public typealias PendableSpy = Spy> extension Spy { /// Create a pendable Spy that is pre-stubbed to return return a a pending that will block for a bit before returning the fallback value. - public convenience init(pendingFallback: Value) where Returning == DynamicPendable { + public convenience init(pendingFallback: Value) where Returning == Pendable { self.init(.pending(fallback: pendingFallback)) } /// Create a pendable Spy that is pre-stubbed to return return a a pending that will block for a bit before returning Void. - public convenience init() where Returning == DynamicPendable { + public convenience init() where Returning == Pendable { self.init(.pending(fallback: ())) } /// Create a pendable Spy that is pre-stubbed to return a finished value. - public convenience init(finished: Value) where Returning == DynamicPendable { + public convenience init(finished: Value) where Returning == Pendable { self.init(.finished(finished)) } } extension Spy { /// Update the pendable Spy's stub to be in a pending state. - public func stub(pendingFallback: Value) where Returning == DynamicPendable { + public func stub(pendingFallback: Value) where Returning == Pendable { self.stub(.pending(fallback: pendingFallback)) } /// Update the pendable Spy's stub to be in a pending state. - public func stubPending() where Returning == DynamicPendable { + public func stubPending() where Returning == Pendable { self.stub(.pending(fallback: ())) } /// Update the pendable Spy's stub to be in a pending state. - public func stubPending() where Returning == DynamicPendable> { + public func stubPending() where Returning == Pendable> { self.stub(.pending(fallback: nil)) } /// Update the pendable Spy's stub to return the given value. /// /// - parameter finished: The value to return when `callAsFunction` is called. - public func stub(finished: Value) where Returning == DynamicPendable { + public func stub(finished: Value) where Returning == Pendable { self.stub(.finished(finished)) } /// Update the pendable Spy's stub to be in a pending state. - public func stubFinished() where Returning == DynamicPendable { + public func stubFinished() where Returning == Pendable { self.stub(.finished(())) } } extension Spy { - /// Records the arguments and handles the result according to ``Pendable/resolve(delay:)-hvhg``. + /// Records the arguments and handles the result according to ``Pendable/call(fallbackDelay:)``. /// /// - parameter arguments: The arguments to record. /// - parameter fallbackDelay: The amount of seconds to delay if the `Pendable` is pending before @@ -57,17 +57,17 @@ extension Spy { public func callAsFunction( _ arguments: Arguments, fallbackDelay: TimeInterval = PendableDefaults.delay - ) async -> Value where Returning == DynamicPendable { + ) async -> Value where Returning == Pendable { return await call(arguments).call(fallbackDelay: fallbackDelay) } - /// Records that a call was made and handles the result according to ``Pendable/resolve(delay:)-hvhg``. + /// Records that a call was made and handles the result according to ``Pendable/call(fallbackDelay:)``. /// /// - parameter fallbackDelay: The amount of seconds to delay if the `Pendable` is pending before /// returning its fallback value. If the `Pendable` is finished, then this value is ignored. public func callAsFunction( fallbackDelay: TimeInterval = PendableDefaults.delay - ) async -> Value where Arguments == Void, Returning == DynamicPendable { + ) async -> Value where Arguments == Void, Returning == Pendable { return await call(()).call(fallbackDelay: fallbackDelay) } } diff --git a/Sources/Fakes/Spy/Spy+ThrowingPendable.swift b/Sources/Fakes/Spy/Spy+ThrowingPendable.swift index fe41614..27df926 100644 --- a/Sources/Fakes/Spy/Spy+ThrowingPendable.swift +++ b/Sources/Fakes/Spy/Spy+ThrowingPendable.swift @@ -57,7 +57,7 @@ extension Spy { extension Spy { // Returning == ThrowingPendable - /// Records the arguments and handles the result according to ``Pendable/resolve(delay:)-1bb25``. + /// Records the arguments and handles the result according to ``Pendable/call(fallbackDelay:)``. /// This call then throws or returns the success, according to `Result.get`. /// /// - parameter arguments: The arguments to record. @@ -70,7 +70,7 @@ extension Spy { return try await call(arguments).call(fallbackDelay: fallbackDelay).get() } - /// Records that a call was made and handles the result according to ``Pendable/resolve(delay:)-1bb25``. + /// Records that a call was made and handles the result according to ``Pendable/call(fallbackDelay:)``. /// This call then throws or returns the success, according to `Result.get`. /// /// - parameter pendingDelay: The amount of seconds to delay if the `Pendable` is .pending before diff --git a/Sources/Fakes/Spy/Spy.swift b/Sources/Fakes/Spy/Spy.swift index 0cc5c83..aeefac4 100644 --- a/Sources/Fakes/Spy/Spy.swift +++ b/Sources/Fakes/Spy/Spy.swift @@ -80,7 +80,7 @@ extension Spy { } extension Spy { - public func resolveStub(with value: Value) where Returning == DynamicPendable { + public func resolveStub(with value: Value) where Returning == Pendable { lock.lock() defer { lock.unlock() } _stub.resolve(with: value) diff --git a/Tests/FakesTests/DynamicPendableTests.swift b/Tests/FakesTests/DynamicPendableTests.swift index 59c013b..32ec2ef 100644 --- a/Tests/FakesTests/DynamicPendableTests.swift +++ b/Tests/FakesTests/DynamicPendableTests.swift @@ -4,7 +4,7 @@ import XCTest final class DynamicPendableTests: XCTestCase { func testSingleCall() async { - let subject = DynamicPendable.pending(fallback: 0) + let subject = Pendable.pending(fallback: 0) async let result = subject.call() @@ -17,7 +17,7 @@ final class DynamicPendableTests: XCTestCase { } func testMultipleCalls() async { - let subject = DynamicPendable.pending(fallback: 0) + let subject = Pendable.pending(fallback: 0) async let result = withTaskGroup(of: Int.self, returning: [Int].self) { taskGroup in for _ in 0..<100 { @@ -40,7 +40,7 @@ final class DynamicPendableTests: XCTestCase { } func testAutoresolve() async { - let subject = DynamicPendable.pending(fallback: 3) + let subject = Pendable.pending(fallback: 3) let expectation = self.expectation(description: "Autoresolves after the given delay") diff --git a/Tests/FakesTests/SpyTests.swift b/Tests/FakesTests/SpyTests.swift index c632736..6519093 100644 --- a/Tests/FakesTests/SpyTests.swift +++ b/Tests/FakesTests/SpyTests.swift @@ -150,7 +150,7 @@ final class SpyTests: XCTestCase { } func testDynamicPendable() async { - let subject = Spy>() + let subject = Spy>() let managedTask = await ManagedTask.running { await subject() @@ -164,7 +164,7 @@ final class SpyTests: XCTestCase { } func testDynamicPendableDeinit() async { - let subject = Spy>() + let subject = Spy>() let managedTask = await ManagedTask.running { await subject() @@ -172,7 +172,7 @@ final class SpyTests: XCTestCase { await expect { await managedTask.hasStarted }.toEventually(beTrue()) - subject.stub(DynamicPendable.pending()) + subject.stub(Pendable.pending()) subject.resolveStub(with: ()) await expect { await managedTask.isFinished }.toEventually(beTrue()) From 26f9a59a800e50c1c6226adab22a790b7dfcee2e Mon Sep 17 00:00:00 2001 From: Rachel Brindle Date: Fri, 12 Apr 2024 17:49:41 -0700 Subject: [PATCH 07/10] Set PendableDefaults.delay to 2 seconds, and document it. Address swiftlint concerns --- Sources/Fakes/Pendable/Pendable.swift | 5 ++++- Sources/Fakes/Pendable/PendableDefaults.swift | 6 +++++- Sources/Fakes/Spy/Spy+Pendable.swift | 1 + Sources/Fakes/Spy/Spy+ThrowingPendable.swift | 1 - Sources/Fakes/Spy/Spy.swift | 1 + Tests/FakesTests/DynamicPendableTests.swift | 8 ++++---- Tests/FakesTests/SpyTests.swift | 10 +++++----- 7 files changed, 20 insertions(+), 12 deletions(-) diff --git a/Sources/Fakes/Pendable/Pendable.swift b/Sources/Fakes/Pendable/Pendable.swift index 27058a3..8981119 100644 --- a/Sources/Fakes/Pendable/Pendable.swift +++ b/Sources/Fakes/Pendable/Pendable.swift @@ -138,7 +138,9 @@ extension Pendable { /// /// - parameter resolveDelay: The amount of time (in seconds) to wait until the call returns /// the fallback value. This is only used when the `Pendable` is in a pending state. - public func call(resolveDelay: TimeInterval = PendableDefaults.delay) async throws -> Success where Value == Result { + public func call( + resolveDelay: TimeInterval = PendableDefaults.delay + ) async throws -> Success where Value == Result { try await call(fallbackDelay: resolveDelay).get() } } @@ -170,6 +172,7 @@ extension Pendable { /// Creates a new pending `Pendable` with a fallback value of nil. public static func pending() -> Pendable where Value == Optional { + // swiftlint:disable:previous syntactic_sugar return Pendable(fallbackValue: nil) } } diff --git a/Sources/Fakes/Pendable/PendableDefaults.swift b/Sources/Fakes/Pendable/PendableDefaults.swift index 13b82fb..33ec0e9 100644 --- a/Sources/Fakes/Pendable/PendableDefaults.swift +++ b/Sources/Fakes/Pendable/PendableDefaults.swift @@ -7,6 +7,10 @@ public final class PendableDefaults: @unchecked Sendable { public init() {} + /// The amount of time to delay before resolving a pending Pendable with the fallback value. + /// By default this is 2 seconds. Conveniently, just long enough to be twice Nimble's default polling timeout. + /// In general, you should keep this set to some number greater than Nimble's default polling timeout, + /// in order to allow polling matchers to work correctly. public static var delay: TimeInterval { get { PendableDefaults.shared.delay @@ -16,7 +20,7 @@ public final class PendableDefaults: @unchecked Sendable { } } - private var _delay: TimeInterval = 1 + private var _delay: TimeInterval = 2 public var delay: TimeInterval { get { lock.lock() diff --git a/Sources/Fakes/Spy/Spy+Pendable.swift b/Sources/Fakes/Spy/Spy+Pendable.swift index 51de0ea..cc3514e 100644 --- a/Sources/Fakes/Spy/Spy+Pendable.swift +++ b/Sources/Fakes/Spy/Spy+Pendable.swift @@ -32,6 +32,7 @@ extension Spy { /// Update the pendable Spy's stub to be in a pending state. public func stubPending() where Returning == Pendable> { + // swiftlint:disable:previous syntactic_sugar self.stub(.pending(fallback: nil)) } diff --git a/Sources/Fakes/Spy/Spy+ThrowingPendable.swift b/Sources/Fakes/Spy/Spy+ThrowingPendable.swift index 27df926..569cf01 100644 --- a/Sources/Fakes/Spy/Spy+ThrowingPendable.swift +++ b/Sources/Fakes/Spy/Spy+ThrowingPendable.swift @@ -81,4 +81,3 @@ extension Spy { return try await call(()).call(fallbackDelay: fallbackDelay).get() } } - diff --git a/Sources/Fakes/Spy/Spy.swift b/Sources/Fakes/Spy/Spy.swift index aeefac4..cde5c3b 100644 --- a/Sources/Fakes/Spy/Spy.swift +++ b/Sources/Fakes/Spy/Spy.swift @@ -29,6 +29,7 @@ public final class Spy { /// Create a Spy that returns nil public convenience init() where Returning == Optional { + // swiftlint:disable:previous syntactic_sugar self.init(nil) } diff --git a/Tests/FakesTests/DynamicPendableTests.swift b/Tests/FakesTests/DynamicPendableTests.swift index 32ec2ef..c02a307 100644 --- a/Tests/FakesTests/DynamicPendableTests.swift +++ b/Tests/FakesTests/DynamicPendableTests.swift @@ -3,12 +3,12 @@ import Nimble import XCTest final class DynamicPendableTests: XCTestCase { - func testSingleCall() async { + func testSingleCall() async throws { let subject = Pendable.pending(fallback: 0) async let result = subject.call() - try! await Task.sleep(nanoseconds: UInt64(0.01 * 1_000_000_000)) + try await Task.sleep(nanoseconds: UInt64(0.01 * 1_000_000_000)) subject.resolve(with: 2) @@ -16,7 +16,7 @@ final class DynamicPendableTests: XCTestCase { expect(value).to(equal(2)) } - func testMultipleCalls() async { + func testMultipleCalls() async throws { let subject = Pendable.pending(fallback: 0) async let result = withTaskGroup(of: Int.self, returning: [Int].self) { taskGroup in @@ -31,7 +31,7 @@ final class DynamicPendableTests: XCTestCase { return results } - try! await Task.sleep(nanoseconds: UInt64(0.1 * 1_000_000_000)) + try await Task.sleep(nanoseconds: UInt64(0.1 * 1_000_000_000)) subject.resolve(with: 3) diff --git a/Tests/FakesTests/SpyTests.swift b/Tests/FakesTests/SpyTests.swift index 6519093..096f04f 100644 --- a/Tests/FakesTests/SpyTests.swift +++ b/Tests/FakesTests/SpyTests.swift @@ -183,7 +183,7 @@ actor ManagedTask { var hasStarted = false var isFinished = false - var _task: Task! + var task: Task! static func running(closure: @escaping @Sendable () async throws -> Success) async -> ManagedTask where Failure == Error { let task = ManagedTask() @@ -204,7 +204,7 @@ actor ManagedTask { private init() {} private func run(closure: @escaping @Sendable () async throws -> Success) where Failure == Error { - _task = Task { + task = Task { self.recordStarted() let result = try await closure() self.recordFinished() @@ -213,7 +213,7 @@ actor ManagedTask { } private func run(closure: @escaping @Sendable () async -> Success) where Failure == Never { - _task = Task { + task = Task { self.recordStarted() let result = await closure() self.recordFinished() @@ -231,13 +231,13 @@ actor ManagedTask { var result: Result { get async { - await _task.result + await task.result } } var value: Success { get async throws { - try await _task.value + try await task.value } } } From 341be9515ba58f88598e8d9c06052d067877ef7d Mon Sep 17 00:00:00 2001 From: Rachel Brindle Date: Fri, 12 Apr 2024 18:00:13 -0700 Subject: [PATCH 08/10] Rename DynamicPendableTests to PendableTests --- .../{DynamicPendableTests.swift => PendableTests.swift} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename Tests/FakesTests/{DynamicPendableTests.swift => PendableTests.swift} (96%) diff --git a/Tests/FakesTests/DynamicPendableTests.swift b/Tests/FakesTests/PendableTests.swift similarity index 96% rename from Tests/FakesTests/DynamicPendableTests.swift rename to Tests/FakesTests/PendableTests.swift index c02a307..1d2fe81 100644 --- a/Tests/FakesTests/DynamicPendableTests.swift +++ b/Tests/FakesTests/PendableTests.swift @@ -2,7 +2,7 @@ import Fakes import Nimble import XCTest -final class DynamicPendableTests: XCTestCase { +final class PendableTests: XCTestCase { func testSingleCall() async throws { let subject = Pendable.pending(fallback: 0) From 1366527cc951015e923e3f630e2ec9b6f255f5ed Mon Sep 17 00:00:00 2001 From: Rachel Brindle Date: Fri, 12 Apr 2024 18:06:09 -0700 Subject: [PATCH 09/10] Use Nimble's awaitUntil instead of XCTest's expectation API --- Tests/FakesTests/PendableTests.swift | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/Tests/FakesTests/PendableTests.swift b/Tests/FakesTests/PendableTests.swift index 1d2fe81..3d02755 100644 --- a/Tests/FakesTests/PendableTests.swift +++ b/Tests/FakesTests/PendableTests.swift @@ -42,14 +42,11 @@ final class PendableTests: XCTestCase { func testAutoresolve() async { let subject = Pendable.pending(fallback: 3) - let expectation = self.expectation(description: "Autoresolves after the given delay") - - let task = Task { - _ = await subject.call(fallbackDelay: 0.1) - expectation.fulfill() + await waitUntil(timeout: .milliseconds(500)) { done in + Task { + _ = await subject.call(fallbackDelay: 0.1) + done() + } } - - await self.fulfillment(of: [expectation], timeout: 1) - task.cancel() } } From d6ba208366c7abc87c18b2c78429cedd61f592de Mon Sep 17 00:00:00 2001 From: Rachel Brindle Date: Fri, 12 Apr 2024 22:35:11 -0700 Subject: [PATCH 10/10] add additional helpers for stubbing pendables --- Sources/Fakes/Spy/Spy+Pendable.swift | 7 +++++++ Sources/Fakes/Spy/Spy+ThrowingPendable.swift | 12 ++++++++++++ 2 files changed, 19 insertions(+) diff --git a/Sources/Fakes/Spy/Spy+Pendable.swift b/Sources/Fakes/Spy/Spy+Pendable.swift index cc3514e..337d61a 100644 --- a/Sources/Fakes/Spy/Spy+Pendable.swift +++ b/Sources/Fakes/Spy/Spy+Pendable.swift @@ -19,6 +19,13 @@ extension Spy { } } +extension Spy { + /// Resolve the pendable Spy's stub with Void + public func resolveStub() where Returning == Pendable { + self.resolveStub(with: ()) + } +} + extension Spy { /// Update the pendable Spy's stub to be in a pending state. public func stub(pendingFallback: Value) where Returning == Pendable { diff --git a/Sources/Fakes/Spy/Spy+ThrowingPendable.swift b/Sources/Fakes/Spy/Spy+ThrowingPendable.swift index 569cf01..f7c15b1 100644 --- a/Sources/Fakes/Spy/Spy+ThrowingPendable.swift +++ b/Sources/Fakes/Spy/Spy+ThrowingPendable.swift @@ -29,6 +29,18 @@ extension Spy { } } +extension Spy { + /// Resolve the pendable Spy's stub with the success value. + public func resolveStub(success: Success) where Returning == ThrowingDynamicPendable { + self.resolveStub(with: .success(success)) + } + + /// Resolve the pendable spy's stub with the given error + public func resolveStub(failure: Failure) where Returning == ThrowingDynamicPendable { + self.resolveStub(with: .failure(failure)) + } +} + extension Spy { /// Update the pendable Spy's stub to be in a pending state. public func stub(pendingSuccess: Success) where Returning == ThrowingDynamicPendable {