Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
7 changes: 3 additions & 4 deletions .github/workflows/ci-swiftpm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ jobs:
swiftpm_darwin:
name: SwiftPM, Darwin, Xcode ${{ matrix.xcode }}
needs: filter
runs-on: macos-12
runs-on: macos-14
strategy:
matrix:
xcode: ["14.0.1"]
xcode: ["15.3", "16.1"]
fail-fast: false
env:
DEVELOPER_DIR: "/Applications/Xcode_${{ matrix.xcode }}.app"
Expand All @@ -45,10 +45,9 @@ jobs:
strategy:
matrix:
container:
- swift:5.7
- swift:5.8
- swift:5.9
- swift:5.10
- swift:6.0
fail-fast: false
container: ${{ matrix.container }}
steps:
Expand Down
173 changes: 173 additions & 0 deletions Sources/Fakes/DynamicResult.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import Foundation

/// A DynamicResult is specifically for mocking out when multiple results for a call can happen.
///
/// DynamicResult is intended to be an implementation detail of ``Spy``,
/// but is exposed publicly to be composed with other types as desired.
public final class DynamicResult<Arguments, Returning> {
/// A value or closure to be used by the DynamicResult
public enum Stub {
/// A static value
case value(Returning)
/// A closure to be called.
case closure(@Sendable (Arguments) -> Returning)

/// Call the stub.
/// If the stub is a `.value`, then return the value.
/// If the stub is a `.closure`, then call the closure with the arguments.
func call(_ arguments: Arguments) -> Returning {
switch self {
case .value(let returning):
return returning
case .closure(let closure):
return closure(arguments)
}
}
}

private let lock = NSRecursiveLock()
private var stubs: [Stub]

private var _stubHistory: [Returning] = []
var stubHistory: [Returning] {
lock.lock()
defer { lock.unlock () }

Check warning on line 34 in Sources/Fakes/DynamicResult.swift

View workflow job for this annotation

GitHub Actions / lint

No Space in Method Call Violation: Don't add a space between the method name and the parentheses (no_space_in_method_call)
return _stubHistory
}

/// Create a new DynamicResult stubbed to return the values in the given order.
/// That is, given `DynamicResult<Void, Int>(1, 2, 3)`,
/// if you call `.call` 5 times, you will get back `1, 2, 3, 3, 3`.
public init(_ value: Returning, _ values: Returning...) {
self.stubs = Array(value, values).map { Stub.value($0) }
}

internal init(_ values: [Returning]) {
self.stubs = values.map { Stub.value($0) }
}

/// Create a new DynamicResult stubbed to call the given closure.
public init(_ closure: @escaping @Sendable (Arguments) -> Returning) {
self.stubs = [.closure(closure)]
}

/// Create a new DynamicResult stubbed to call the given stubs.
public init(_ stub: Stub, _ stubs: Stub...) {
self.stubs = Array(stub, stubs)
}

internal init(_ stubs: [Stub]) {
self.stubs = stubs
}

/// Call the DynamicResult, returning the next stub in the list of stubs.
public func call(_ arguments: Arguments) -> Returning {
lock.lock()
defer { lock.unlock () }
let value = nextStub().call(arguments)
_stubHistory.append(value)
return value
}

/// Call the DynamicResult, returning the next stub in the list of stubs.
public func call() -> Returning where Arguments == Void {
call(())
}

/// Replace the stubs with the new static values
public func replace(_ value: Returning, _ values: Returning...) {
replace(Array(value, values))
}

/// Replace the stubs with the new static values
internal func replace(_ values: [Returning]) {
lock.lock()
defer { lock.unlock () }
self.resolvePendables()
self.stubs = values.map { .value($0) }
}

/// Replace the stubs with the new closure.
public func replace(_ closure: @escaping @Sendable (Arguments) -> Returning) {
lock.lock()
defer { lock.unlock () }
self.resolvePendables()
self.stubs = [.closure(closure)]
}

/// Replace the stubs with the new list of stubs
public func replace(_ stub: Stub, _ stubs: Stub...) {
lock.lock()
defer { lock.unlock () }
self.resolvePendables()
self.stubs = Array(stub, stubs)
}

/// Replace the stubs with the new list of stubs
internal func replace(_ stubs: [Stub]) {
lock.lock()
defer { lock.unlock () }
self.resolvePendables()
self.stubs = stubs
}

/// Append the values to the list of stubs.
public func append(_ value: Returning, _ values: Returning...) {
append(Array(value, values))
}

internal func append(_ values: [Returning]) {
lock.lock()
defer { lock.unlock () }
stubs.append(contentsOf: values.map { .value($0) })
}

/// Append the closure to the list of stubs.
public func append(_ closure: @escaping @Sendable (Arguments) -> Returning) {
lock.lock()
defer { lock.unlock () }
stubs.append(.closure(closure))
}

/// Append the stubs to the list of stubs.
public func append(_ stub: Stub, _ stubs: Stub...) {
append(Array(stub, stubs))
}

internal func append(_ stubs: [Stub]) {
lock.lock()
defer { lock.unlock () }
self.stubs.append(contentsOf: stubs)
}

private func nextStub() -> Stub {
guard let stub = stubs.first else {
fatalError("Fakes: DynamicResult \(self) has 0 stubs. This should never happen. File a bug at https://github.com/Quick/swift-fakes/issues/new")
}
if stubs.count > 1 {
stubs.removeFirst()
}
return stub
}

private func resolvePendables() {
stubs.forEach {
guard case .value(let value) = $0 else { return }
if let resolvable = value as? ResolvableWithFallback {
resolvable.resolveWithFallback()
}
}
}
}

extension DynamicResult: @unchecked Sendable where Arguments: Sendable, Returning: Sendable {}

internal extension Array {
init(_ value: Element, _ values: [Element]) {
self = [value] + values
}

mutating func append(_ value: Element, _ values: [Element]) {
self.append(contentsOf: Array(value, values))
}
}
8 changes: 7 additions & 1 deletion Sources/Fakes/Spy/Spy+Pendable.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import Foundation

public typealias PendableSpy<Arguments, Value> = Spy<Arguments, Pendable<Value>>
public typealias PendableSpy<
Arguments,
Value
> = Spy<
Arguments,
Pendable<Value>
>

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.
Expand Down
54 changes: 49 additions & 5 deletions Sources/Fakes/Spy/Spy+Result.swift
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
public typealias ThrowingSpy<Arguments, Success, Failure: Error> = Spy<Arguments, Result<Success, Failure>>

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

/// Create a throwing Spy that is pre-stubbed with a Void Success value
Expand All @@ -19,14 +19,36 @@
public convenience init<Success>() where Returning == Result<Success, Error> {
self.init(.failure(EmptyError()))
}

#if swift(>=6.0)
public convenience init<Success, Failure: Error>(_ closure: @escaping @Sendable (Arguments) throws(Failure) -> Success) where Returning == Result<Success, Failure> {

Check warning on line 24 in Sources/Fakes/Spy/Spy+Result.swift

View workflow job for this annotation

GitHub Actions / lint

Line Length Violation: Line should be 160 characters or less; currently it has 169 characters (line_length)
self.init { args in
do {
return .success(try closure(args))
} catch let error {
return .failure(error)
}
}
}
#else
public convenience init<Success>(_ closure: @escaping @Sendable (Arguments) throws -> Success) where Returning == Result<Success, Swift.Error> {
self.init { args in
do {
return .success(try closure(args))
} catch let error {
return .failure(error)
}
}
}
#endif
}

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

/// Update the throwing Spy's stub to be successful, with the given value.
Expand All @@ -42,6 +64,28 @@
public func stub<Success, Failure: Error>(failure: Failure) where Returning == Result<Success, Failure> {
self.stub(.failure(failure))
}

#if swift(>=6.0)
public func stub<Success, Failure: Error>(_ closure: @escaping @Sendable (Arguments) throws(Failure) -> Success) where Returning == Result<Success, Failure> {

Check warning on line 69 in Sources/Fakes/Spy/Spy+Result.swift

View workflow job for this annotation

GitHub Actions / lint

Line Length Violation: Line should be 160 characters or less; currently it has 162 characters (line_length)
self.stub { args in
do {
return .success(try closure(args))
} catch let error {
return .failure(error)
}
}
}
#else
public func stub<Success>(_ closure: @escaping @Sendable (Arguments) throws -> Success) where Returning == Result<Success, Swift.Error> {
self.stub { args in
do {
return .success(try closure(args))
} catch let error {
return .failure(error)
}
}
}
#endif
}

extension Spy {
Expand Down
14 changes: 13 additions & 1 deletion Sources/Fakes/Spy/Spy+ThrowingPendable.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
import Foundation

public typealias ThrowingPendableSpy<Arguments, Success, Failure: Error> = Spy<Arguments, ThrowingPendable<Success, Failure>>
public typealias ThrowingPendableSpy<
Arguments,
Success,
Failure: Error
> = Spy<
Arguments,
Pendable<
Result<
Success,
Failure
>
>
>

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