Skip to content

Commit 4377102

Browse files
committed
Support dynamic responses in Spy
Adds a new type: DynamicResult, which encapsulates handling multiple responses, as well as side effects/closures Updates Spy to use DynamicResult internally, though document DynamicResult for those who wish to use it directly.
1 parent 380cce3 commit 4377102

File tree

8 files changed

+511
-26
lines changed

8 files changed

+511
-26
lines changed

.github/workflows/ci-swiftpm.yml

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,10 @@ jobs:
2323
swiftpm_darwin:
2424
name: SwiftPM, Darwin, Xcode ${{ matrix.xcode }}
2525
needs: filter
26-
runs-on: macos-12
26+
runs-on: macos-14
2727
strategy:
2828
matrix:
29-
xcode: ["14.0.1"]
29+
xcode: ["15.3", "16.1"]
3030
fail-fast: false
3131
env:
3232
DEVELOPER_DIR: "/Applications/Xcode_${{ matrix.xcode }}.app"
@@ -45,10 +45,9 @@ jobs:
4545
strategy:
4646
matrix:
4747
container:
48-
- swift:5.7
49-
- swift:5.8
5048
- swift:5.9
5149
- swift:5.10
50+
- swift:6.0
5251
fail-fast: false
5352
container: ${{ matrix.container }}
5453
steps:

Sources/Fakes/DynamicResult.swift

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
import Foundation
2+
3+
/// A DynamicResult is specifically for mocking out when multiple results for a call can happen.
4+
///
5+
/// DynamicResult is intended to be an implementation detail of ``Spy``,
6+
/// but is exposed publicly to be composed with other types as desired.
7+
public final class DynamicResult<Arguments, Returning> {
8+
/// A value or closure to be used by the DynamicResult
9+
public enum Stub {
10+
/// A static value
11+
case value(Returning)
12+
/// A closure to be called.
13+
case closure(@Sendable (Arguments) -> Returning)
14+
15+
/// Call the stub.
16+
/// If the stub is a `.value`, then return the value.
17+
/// If the stub is a `.closure`, then call the closure with the arguments.
18+
func call(_ arguments: Arguments) -> Returning {
19+
switch self {
20+
case .value(let returning):
21+
return returning
22+
case .closure(let closure):
23+
return closure(arguments)
24+
}
25+
}
26+
}
27+
28+
private let lock = NSRecursiveLock()
29+
private var stubs: [Stub]
30+
31+
private var _stubHistory: [Returning] = []
32+
var stubHistory: [Returning] {
33+
lock.lock()
34+
defer { lock.unlock () }
35+
return _stubHistory
36+
}
37+
38+
/// Create a new DynamicResult stubbed to return the values in the given order.
39+
/// That is, given `DynamicResult<Void, Int>(1, 2, 3)`,
40+
/// if you call `.call` 5 times, you will get back `1, 2, 3, 3, 3`.
41+
public init(_ value: Returning, _ values: Returning...) {
42+
self.stubs = Array(value, values).map { Stub.value($0) }
43+
}
44+
45+
internal init(_ values: [Returning]) {
46+
self.stubs = values.map { Stub.value($0) }
47+
}
48+
49+
/// Create a new DynamicResult stubbed to call the given closure.
50+
public init(_ closure: @escaping @Sendable (Arguments) -> Returning) {
51+
self.stubs = [.closure(closure)]
52+
}
53+
54+
/// Create a new DynamicResult stubbed to call the given stubs.
55+
public init(_ stub: Stub, _ stubs: Stub...) {
56+
self.stubs = Array(stub, stubs)
57+
}
58+
59+
internal init(_ stubs: [Stub]) {
60+
self.stubs = stubs
61+
}
62+
63+
/// Call the DynamicResult, returning the next stub in the list of stubs.
64+
public func call(_ arguments: Arguments) -> Returning {
65+
lock.lock()
66+
defer { lock.unlock () }
67+
let value = nextStub().call(arguments)
68+
_stubHistory.append(value)
69+
return value
70+
}
71+
72+
/// Call the DynamicResult, returning the next stub in the list of stubs.
73+
public func call() -> Returning where Arguments == Void {
74+
call(())
75+
}
76+
77+
/// Replace the stubs with the new static values
78+
public func replace(_ value: Returning, _ values: Returning...) {
79+
replace(Array(value, values))
80+
}
81+
82+
/// Replace the stubs with the new static values
83+
internal func replace(_ values: [Returning]) {
84+
lock.lock()
85+
defer { lock.unlock () }
86+
self.resolvePendables()
87+
self.stubs = values.map { .value($0) }
88+
}
89+
90+
/// Replace the stubs with the new closure.
91+
public func replace(_ closure: @escaping @Sendable (Arguments) -> Returning) {
92+
lock.lock()
93+
defer { lock.unlock () }
94+
self.resolvePendables()
95+
self.stubs = [.closure(closure)]
96+
}
97+
98+
/// Replace the stubs with the new list of stubs
99+
public func replace(_ stub: Stub, _ stubs: Stub...) {
100+
lock.lock()
101+
defer { lock.unlock () }
102+
self.resolvePendables()
103+
self.stubs = Array(stub, stubs)
104+
}
105+
106+
/// Replace the stubs with the new list of stubs
107+
internal func replace(_ stubs: [Stub]) {
108+
lock.lock()
109+
defer { lock.unlock () }
110+
self.resolvePendables()
111+
self.stubs = stubs
112+
}
113+
114+
/// Append the values to the list of stubs.
115+
public func append(_ value: Returning, _ values: Returning...) {
116+
append(Array(value, values))
117+
}
118+
119+
internal func append(_ values: [Returning]) {
120+
lock.lock()
121+
defer { lock.unlock () }
122+
stubs.append(contentsOf: values.map { .value($0) })
123+
}
124+
125+
/// Append the closure to the list of stubs.
126+
public func append(_ closure: @escaping @Sendable (Arguments) -> Returning) {
127+
lock.lock()
128+
defer { lock.unlock () }
129+
stubs.append(.closure(closure))
130+
}
131+
132+
/// Append the stubs to the list of stubs.
133+
public func append(_ stub: Stub, _ stubs: Stub...) {
134+
append(Array(stub, stubs))
135+
}
136+
137+
internal func append(_ stubs: [Stub]) {
138+
lock.lock()
139+
defer { lock.unlock () }
140+
self.stubs.append(contentsOf: stubs)
141+
}
142+
143+
private func nextStub() -> Stub {
144+
guard let stub = stubs.first else {
145+
fatalError("Fakes: DynamicResult \(self) has 0 stubs. This should never happen. File a bug at https://github.com/Quick/swift-fakes/issues/new")
146+
}
147+
if stubs.count > 1 {
148+
stubs.removeFirst()
149+
}
150+
return stub
151+
}
152+
153+
private func resolvePendables() {
154+
stubs.forEach {
155+
guard case .value(let value) = $0 else { return }
156+
if let resolvable = value as? ResolvableWithFallback {
157+
resolvable.resolveWithFallback()
158+
}
159+
}
160+
}
161+
}
162+
163+
extension DynamicResult: @unchecked Sendable where Arguments: Sendable, Returning: Sendable {}
164+
165+
internal extension Array {
166+
init(_ value: Element, _ values: [Element]) {
167+
self = [value] + values
168+
}
169+
170+
mutating func append(_ value: Element, _ values: [Element]) {
171+
self.append(contentsOf: Array(value, values))
172+
}
173+
}

Sources/Fakes/Spy/Spy+Pendable.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import Foundation
22

3-
public typealias PendableSpy<Arguments, Value> = Spy<Arguments, Pendable<Value>>
3+
public typealias PendableSpy<
4+
Arguments,
5+
Value
6+
> = Spy<
7+
Arguments,
8+
Pendable<Value>
9+
>
410

511
extension Spy {
612
/// 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.

Sources/Fakes/Spy/Spy+Result.swift

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
public typealias ThrowingSpy<Arguments, Success, Failure: Error> = Spy<Arguments, Result<Success, Failure>>
22

33
extension Spy {
4-
/// Create a throwing Spy that is pre-stubbed with some Success value.
5-
public convenience init<Success, Failure: Error>(success: Success) where Returning == Result<Success, Failure> {
6-
self.init(.success(success))
4+
/// Create a throwing Spy that is pre-stubbed with some Success values.
5+
public convenience init<Success, Failure: Error>(success: Success, _ successes: Success...) where Returning == Result<Success, Failure> {
6+
self.init(Array(success, successes).map { .success($0) })
77
}
88

99
/// Create a throwing Spy that is pre-stubbed with a Void Success value
@@ -19,14 +19,36 @@ extension Spy {
1919
public convenience init<Success>() where Returning == Result<Success, Error> {
2020
self.init(.failure(EmptyError()))
2121
}
22+
23+
#if swift(>=6.0)
24+
public convenience init<Success, Failure: Error>(_ closure: @escaping @Sendable (Arguments) throws(Failure) -> Success) where Returning == Result<Success, Failure> {
25+
self.init { args in
26+
do {
27+
return .success(try closure(args))
28+
} catch let error {
29+
return .failure(error)
30+
}
31+
}
32+
}
33+
#else
34+
public convenience init<Success>(_ closure: @escaping @Sendable (Arguments) throws -> Success) where Returning == Result<Success, Swift.Error> {
35+
self.init { args in
36+
do {
37+
return .success(try closure(args))
38+
} catch let error {
39+
return .failure(error)
40+
}
41+
}
42+
}
43+
#endif
2244
}
2345

2446
extension Spy {
2547
/// Update the throwing Spy's stub to be successful, with the given value.
2648
///
2749
/// - parameter success: The success state to set the stub to, returned when `callAsFunction` is called.
28-
public func stub<Success, Failure: Error>(success: Success) where Returning == Result<Success, Failure> {
29-
self.stub(.success(success))
50+
public func stub<Success, Failure: Error>(success: Success, _ successes: Success...) where Returning == Result<Success, Failure> {
51+
self.stub(Array(success, successes).map { .success($0) })
3052
}
3153

3254
/// Update the throwing Spy's stub to be successful, with the given value.
@@ -42,6 +64,28 @@ extension Spy {
4264
public func stub<Success, Failure: Error>(failure: Failure) where Returning == Result<Success, Failure> {
4365
self.stub(.failure(failure))
4466
}
67+
68+
#if swift(>=6.0)
69+
public func stub<Success, Failure: Error>(_ closure: @escaping @Sendable (Arguments) throws(Failure) -> Success) where Returning == Result<Success, Failure> {
70+
self.stub { args in
71+
do {
72+
return .success(try closure(args))
73+
} catch let error {
74+
return .failure(error)
75+
}
76+
}
77+
}
78+
#else
79+
public func stub<Success>(_ closure: @escaping @Sendable (Arguments) throws -> Success) where Returning == Result<Success, Swift.Error> {
80+
self.stub { args in
81+
do {
82+
return .success(try closure(args))
83+
} catch let error {
84+
return .failure(error)
85+
}
86+
}
87+
}
88+
#endif
4589
}
4690

4791
extension Spy {

Sources/Fakes/Spy/Spy+ThrowingPendable.swift

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,18 @@
11
import Foundation
22

3-
public typealias ThrowingPendableSpy<Arguments, Success, Failure: Error> = Spy<Arguments, ThrowingPendable<Success, Failure>>
3+
public typealias ThrowingPendableSpy<
4+
Arguments,
5+
Success,
6+
Failure: Error
7+
> = Spy<
8+
Arguments,
9+
Pendable<
10+
Result<
11+
Success,
12+
Failure
13+
>
14+
>
15+
>
416

517
extension Spy {
618
/// Create a throwing pendable Spy that is pre-stubbed to return a pending that will block for a bit before returning success.

0 commit comments

Comments
 (0)