Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Introduce timeout and after to support delayed verification. #434

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,27 @@ extension Templates {
{{ container.accessibility }} struct __VerificationProxy_{{ container.name }}: Cuckoo.VerificationProxy {
private let cuckoo_manager: Cuckoo.MockManager
private let callMatcher: Cuckoo.CallMatcher
private let continuation: Cuckoo.Continuation
private let sourceLocation: Cuckoo.SourceLocation

{{ container.accessibility }} init(manager: Cuckoo.MockManager, callMatcher: Cuckoo.CallMatcher, sourceLocation: Cuckoo.SourceLocation) {
{{ container.accessibility }} init(manager: Cuckoo.MockManager, callMatcher: Cuckoo.CallMatcher, continuation: Cuckoo.Continuation, sourceLocation: Cuckoo.SourceLocation) {
self.cuckoo_manager = manager
self.callMatcher = callMatcher
self.continuation = continuation
self.sourceLocation = sourceLocation
}

{{ container.accessibility }} init(manager: Cuckoo.MockManager, callMatcher: Cuckoo.CallMatcher, sourceLocation: Cuckoo.SourceLocation) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why opt for duplicate init instead of a default parameter value?

self.init(manager: manager, callMatcher: callMatcher, continuation: Cuckoo.ContinuationOnlyOnce(), sourceLocation: sourceLocation)
}

{% for property in container.properties %}
{{ property.unavailablePlatformsCheck }}
{% for attribute in property.attributes %}
{{ attribute.text }}
{% endfor %}
var {{property.name}}: Cuckoo.{{property.verifyType}}<{% if property.isReadOnly %}{{property.type|genericSafe}}{% else %}{{property.nonOptionalType|genericSafe}}{% endif %}> {
return .init(manager: cuckoo_manager, name: "{{property.name}}", callMatcher: callMatcher, sourceLocation: sourceLocation)
return .init(manager: cuckoo_manager, name: "{{property.name}}", callMatcher: callMatcher, continuation: continuation, sourceLocation: sourceLocation)
}
{% if property.hasUnavailablePlatforms %}
#endif
Expand All @@ -44,7 +50,7 @@ extension Templates {
return cuckoo_manager.verify(
\"\"\"
{{method.fullyQualifiedName}}
\"\"\", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation)
\"\"\", callMatcher: callMatcher, parameterMatchers: matchers, continuation: continuation, sourceLocation: sourceLocation)
}
{% if method.hasUnavailablePlatforms %}
#endif
Expand Down
54 changes: 54 additions & 0 deletions Source/Continuation/Continuation.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
//
// Continuation.swift
// Cuckoo
//
// Created by Shoto Kobayashi on 03/09/2022.
//

import Foundation

public protocol Continuation {
var exitOnSuccess: Bool { get }

func check() -> Bool

func wait()

func times(_ count: Int) -> VerificationSpec

func never() -> VerificationSpec

func atLeastOnce() -> VerificationSpec

func atLeast(_ count: Int) -> VerificationSpec

func atMost(_ count: Int) -> VerificationSpec

func with(_ callMatcher: CallMatcher) -> VerificationSpec
}

public extension Continuation {
func times(_ count: Int) -> VerificationSpec {
VerificationSpec(callMatcher: Cuckoo.times(count), continuation: self)
}

func never() -> VerificationSpec {
VerificationSpec(callMatcher: Cuckoo.times(0), continuation: self)
}

func atLeastOnce() -> VerificationSpec {
VerificationSpec(callMatcher: Cuckoo.atLeast(1), continuation: self)
}

func atLeast(_ count: Int) -> VerificationSpec {
VerificationSpec(callMatcher: Cuckoo.atLeast(count), continuation: self)
}

func atMost(_ count: Int) -> VerificationSpec {
VerificationSpec(callMatcher: Cuckoo.atMost(count), continuation: self)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems to me like this one and every method above it can just call the with(_:) method, is that right?

}

func with(_ callMatcher: CallMatcher) -> VerificationSpec {
VerificationSpec(callMatcher: callMatcher, continuation: self)
}
}
17 changes: 17 additions & 0 deletions Source/Continuation/ContinuationAfterDelay.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//
// ContinuationAfterDelay.swift
// Cuckoo
//
// Created by Shoto Kobayashi on 03/09/2022.
//

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reduntant line.


import Foundation

public class ContinueationAfterDelay: NSObject, ContinuationWrapper {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo "Continueation".

public let wrappedContinuation: ContinuationOverTime

public init(delayDuration: TimeInterval, waitingDuration: TimeInterval) {
wrappedContinuation = ContinuationOverTime(duration: delayDuration, waitingDuration: waitingDuration, exitOnSuccess: false)
}
}
22 changes: 22 additions & 0 deletions Source/Continuation/ContinuationFunctions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//
// ContinuationFunctions.swift
// Cuckoo
//
// Created by Shoto Kobayashi on 03/09/2022.
//

import Foundation

public func timeout(_ timeoutDuration: TimeInterval, waitingDuration: TimeInterval = 0.01) -> ContinuationWithTimeout {
ContinuationWithTimeout(
timeoutDuration: timeoutDuration,
waitingDuration: waitingDuration
)
}

public func after(_ delayDuration: TimeInterval, waitingDuration: TimeInterval = 0.01) -> ContinueationAfterDelay {
ContinueationAfterDelay(
delayDuration: delayDuration,
waitingDuration: waitingDuration
)
}
29 changes: 29 additions & 0 deletions Source/Continuation/ContinuationOnlyOnce.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//
// ContinuationOnlyOnce.swift
// Cuckoo
//
// Created by Shoto Kobayashi on 03/09/2022.
//

import Foundation

public class ContinuationOnlyOnce: NSObject, Continuation {
public let exitOnSuccess = true

private var isAlreadyChecked = false

public override init() {
super.init()
}

public func check() -> Bool {
guard !isAlreadyChecked else {
return false
}
isAlreadyChecked = true
return true
}

public func wait() {
}
}
38 changes: 38 additions & 0 deletions Source/Continuation/ContinuationOverTime.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//
// ContinuationOverTime.swift
// Cuckoo
//
// Created by Shoto Kobayashi on 03/09/2022.
//

import Foundation

public class ContinuationOverTime: NSObject, Continuation {
public let duration: TimeInterval
public let waitingDuration: TimeInterval
public let exitOnSuccess: Bool

private var start: Date?

public init(duration: TimeInterval, waitingDuration: TimeInterval, exitOnSuccess: Bool) {
self.duration = duration
self.waitingDuration = waitingDuration
self.exitOnSuccess = exitOnSuccess
super.init()
}

public func check() -> Bool {
if start == nil {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please bind start as if let start instead of using a force unwrap below.

start = Date()
}
return -start!.timeIntervalSinceNow <= duration
}

public func wait() {
Thread.sleep(forTimeInterval: waitingDuration)
}

public func times(_ count: Int) -> VerificationSpec {
VerificationSpec(callMatcher: Cuckoo.times(count), continuation: self)
}
}
16 changes: 16 additions & 0 deletions Source/Continuation/ContinuationWithTimeout.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//
// ContinuationWithTimeout.swift
// Cuckoo
//
// Created by Shoto Kobayashi on 03/09/2022.
//

import Foundation

public class ContinuationWithTimeout: NSObject, ContinuationWrapper {
public let wrappedContinuation: ContinuationOverTime

public init(timeoutDuration: TimeInterval, waitingDuration: TimeInterval) {
wrappedContinuation = ContinuationOverTime(duration: timeoutDuration, waitingDuration: waitingDuration, exitOnSuccess: true)
}
}
28 changes: 28 additions & 0 deletions Source/Continuation/ContinuationWrapper.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
//
// ContinuationWrapper.swift
// Cuckoo
//
// Created by Shoto Kobayashi on 03/09/2022.
//

import Foundation

public protocol ContinuationWrapper: Continuation {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this type necessary?

associatedtype WrappedContinuation: Continuation

var wrappedContinuation: WrappedContinuation { get }
}

public extension ContinuationWrapper {
var exitOnSuccess: Bool {
wrappedContinuation.exitOnSuccess
}

func check() -> Bool {
wrappedContinuation.check()
}

func wait() {
wrappedContinuation.wait()
}
}
14 changes: 13 additions & 1 deletion Source/CuckooFunctions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,21 @@ public func when<F/*: BaseStubFunctionTrait*/>(_ function: F) -> F {
return function
}

public func verify<M: Mock>(_ mock: M, _ callMatcher: CallMatcher, _ continuation: Continuation, file: StaticString = #file, line: UInt = #line) -> M.Verification {
return mock.getVerificationProxy(callMatcher, continuation, sourceLocation: (file, line))
}

/// Creates object used for verification of calls.
public func verify<M: Mock>(_ mock: M, _ callMatcher: CallMatcher = times(1), file: StaticString = #file, line: UInt = #line) -> M.Verification {
return mock.getVerificationProxy(callMatcher, sourceLocation: (file, line))
return verify(mock, callMatcher, ContinuationOnlyOnce(), file: file, line: line)
}

public func verify<M: Mock>(_ mock: M, _ continuation: Continuation, file: StaticString = #file, line: UInt = #line) -> M.Verification {
return verify(mock, times(1), continuation, file: file, line: line)
}

public func verify<M: Mock>(_ mock: M, _ verificationSpec: VerificationSpec, file: StaticString = #file, line: UInt = #line) -> M.Verification {
return verify(mock, verificationSpec.callMatcher, verificationSpec.continuation, file: file, line: line)
}

/// Clears all invocations and stubs of mocks.
Expand Down
29 changes: 20 additions & 9 deletions Source/Matching/CallMatcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,33 +11,44 @@ public struct CallMatcher {
public let name: String

private let matchesFunction: ([StubCall]) -> Bool
private let canRecoverFromFailureFunction: ([StubCall]) -> Bool
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this necessary?


public init(name: String, matchesFunction: @escaping ([StubCall]) -> Bool) {
public init(name: String, matchesFunction: @escaping ([StubCall]) -> Bool, canRecoverFromFailureFunction: @escaping ([StubCall]) -> Bool) {
self.name = name
self.matchesFunction = matchesFunction
self.canRecoverFromFailureFunction = canRecoverFromFailureFunction
}

public init(name: String, numberOfExpectedCalls: Int, compareCallsFunction: @escaping (_ expected: Int, _ actual: Int) -> Bool) {
self.init(name: name) {
return compareCallsFunction(numberOfExpectedCalls, $0.count)

public init(
name: String,
numberOfExpectedCalls: Int,
compareCallsFunction: @escaping (_ expected: Int, _ actual: Int) -> Bool,
canRecoverFromFailureFunction: @escaping (_ expected: Int, _ actual: Int) -> Bool
) {
self.init(name: name, matchesFunction: { compareCallsFunction(numberOfExpectedCalls, $0.count) }) {
canRecoverFromFailureFunction(numberOfExpectedCalls, $0.count)
}
}

public func matches(_ calls: [StubCall]) -> Bool {
return matchesFunction(calls)
}

public func canRecoverFromFailure(_ calls: [StubCall]) -> Bool {
canRecoverFromFailureFunction(calls)
}

public func or(_ otherMatcher: CallMatcher) -> CallMatcher {
let name = "either \(self.name) or \(otherMatcher.name)"
return CallMatcher(name: name) {
return self.matches($0) || otherMatcher.matches($0)
return CallMatcher(name: name, matchesFunction: { self.matches($0) || otherMatcher.matches($0) }) {
self.canRecoverFromFailure($0) || otherMatcher.canRecoverFromFailureFunction($0)
}
}

public func and(_ otherMatcher: CallMatcher) -> CallMatcher {
let name = "both \(self.name) and \(otherMatcher.name)"
return CallMatcher(name: name) {
return self.matches($0) && otherMatcher.matches($0)
return CallMatcher(name: name, matchesFunction: { self.matches($0) && otherMatcher.matches($0) }) {
self.canRecoverFromFailure($0) && otherMatcher.canRecoverFromFailureFunction($0)
}
}
}
6 changes: 3 additions & 3 deletions Source/Matching/CallMatcherFunctions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
/// Returns a matcher ensuring a call was made **`count`** times.
public func times(_ count: Int) -> CallMatcher {
let name = count == 0 ? "never" : "\(count) times"
return CallMatcher(name: name, numberOfExpectedCalls: count, compareCallsFunction: ==)
return CallMatcher(name: name, numberOfExpectedCalls: count, compareCallsFunction: ==, canRecoverFromFailureFunction: >=)
}

/// Returns a matcher ensuring no call was made.
Expand All @@ -24,10 +24,10 @@ public func atLeastOnce() -> CallMatcher {

/// Returns a matcher ensuring call was made at least `count` times.
public func atLeast(_ count: Int) -> CallMatcher {
return CallMatcher(name: "at least \(count) times", numberOfExpectedCalls: count, compareCallsFunction: <=)
return CallMatcher(name: "at least \(count) times", numberOfExpectedCalls: count, compareCallsFunction: <=, canRecoverFromFailureFunction: <=)
}

/// Returns a matcher ensuring call was made at most `count` times.
public func atMost(_ count: Int) -> CallMatcher {
return CallMatcher(name: "at most \(count) times",numberOfExpectedCalls: count, compareCallsFunction: >=)
return CallMatcher(name: "at most \(count) times",numberOfExpectedCalls: count, compareCallsFunction: >=, canRecoverFromFailureFunction: >=)
}
6 changes: 6 additions & 0 deletions Source/Mock/Mock.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ public protocol Mock: HasMockManager, HasSuperclass {

func getVerificationProxy(_ callMatcher: CallMatcher, sourceLocation: SourceLocation) -> Verification

func getVerificationProxy(_ callMatcher: CallMatcher, _ continuation: Continuation, sourceLocation: SourceLocation) -> Verification

func enableDefaultImplementation(_ stub: MocksType)
}

Expand All @@ -35,6 +37,10 @@ public extension Mock {
return Verification(manager: cuckoo_manager, callMatcher: callMatcher, sourceLocation: sourceLocation)
}

func getVerificationProxy(_ callMatcher: CallMatcher, _ continuation: Continuation, sourceLocation: SourceLocation) -> Verification {
return Verification(manager: cuckoo_manager, callMatcher: callMatcher, continuation: continuation, sourceLocation: sourceLocation)
}

func withEnabledDefaultImplementation(_ stub: MocksType) -> Self {
enableDefaultImplementation(stub)
return self
Expand Down
2 changes: 2 additions & 0 deletions Source/Mock/VerificationProxy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,6 @@

public protocol VerificationProxy {
init(manager: MockManager, callMatcher: CallMatcher, sourceLocation: SourceLocation)

init(manager: MockManager, callMatcher: CallMatcher, continuation: Continuation, sourceLocation: SourceLocation)
}