Skip to content
Open
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

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Sources/MockingbirdFramework/Mocking/MockingContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ import Foundation

func clearInvocations() {
invocations.update { $0.removeAll() }
allInvocations.update { $0.removeAll() }
}

func removeInvocations(before invocation: Invocation, inclusive: Bool = false) {
Expand Down
77 changes: 77 additions & 0 deletions Sources/MockingbirdFramework/Verification/AsyncVerification.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,20 @@ public extension NSObject {
createAsyncContext(expectation: expectation, block: block)
return TestExpectation.create(from: expectation)
}

@available(iOS 13.0, *)
@discardableResult
func eventually(_ description: String = "Async verification group",
_ block: @escaping () async -> Void) -> TestExpectation {
let expectation: XCTestExpectation = {
guard let testCase = self as? XCTestCase else {
return XCTestExpectation(description: description)
}
return testCase.expectation(description: description)
}()
createAsyncContext(expectation: expectation, block: block)
return TestExpectation.create(from: expectation)
}
}

/// Internal helper for `eventually` async verification scopes.
Expand Down Expand Up @@ -99,3 +113,66 @@ func createAsyncContext(expectation: XCTestExpectation, block scope: () -> Void)

try? group.verify()
}

@available(iOS 13.0, *)
func createAsyncContext(expectation: XCTestExpectation, block scope: @escaping () async -> Void) {
let group = ExpectationGroup { group in
expectation.expectedFulfillmentCount = group.countExpectations()

group.expectations.forEach({ capturedExpectation in
let observer = InvocationObserver({ (invocation, mockingContext) -> Bool in
do {
try expect(mockingContext,
handled: capturedExpectation.invocation,
using: capturedExpectation.expectation)
expectation.fulfill()
return true
} catch {
return false
}
})
capturedExpectation.mockingContext
.addObserver(observer, for: capturedExpectation.invocation.selectorName)
})

group.subgroups.forEach({ subgroup in
let observer = InvocationObserver({ (invocation, mockingContext) -> Bool in
do {
try subgroup.verify()
expectation.fulfill()
return true
} catch {
return false
}
})
subgroup.expectations.forEach({ $0.mockingContext.addObserver(observer) })
})
}

let queue = DispatchQueue(label: "co.bird.mockingbird.verify.eventually")
queue.setSpecific(key: ExpectationGroup.contextKey, value: group)

let semaphore = DispatchSemaphore(value: 0)

queue.sync {

Task {

await Transaction.$currentExpectationGroup.withValue(group) {
await scope()
}

semaphore.signal()
}

semaphore.wait()
semaphore.signal()
}

try? group.verify()
}

@available(iOS 13.0, *)
enum Transaction {
@TaskLocal static var currentExpectationGroup: ExpectationGroup? = nil
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ class ExpectationGroup {

extension DispatchQueue {
class var currentExpectationGroup: ExpectationGroup? {
return DispatchQueue.getSpecific(key: ExpectationGroup.contextKey)
if #available(iOS 13.0, *) {
return Transaction.currentExpectationGroup ?? DispatchQueue.getSpecific(key: ExpectationGroup.contextKey)
} else {
return DispatchQueue.getSpecific(key: ExpectationGroup.contextKey)
}
}
}