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
118 changes: 84 additions & 34 deletions Nimble.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

79 changes: 79 additions & 0 deletions Sources/Nimble/Adapters/AssertionRecorder+Async.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/// Allows you to temporarily replace the current Nimble assertion handler with
/// the one provided for the scope of the closure.
///
/// Once the closure finishes, then the original Nimble assertion handler is restored.
///
/// @warning
/// Unlike the synchronous version of this call, this does not support catching Objective-C exceptions.
///
/// @see AssertionHandler
public func withAssertionHandler(_ tempAssertionHandler: AssertionHandler,
file: FileString = #file,
line: UInt = #line,
closure: () async throws -> Void) async {
let environment = NimbleEnvironment.activeInstance
let oldRecorder = environment.assertionHandler
let capturer = NMBExceptionCapture(handler: nil, finally: ({
environment.assertionHandler = oldRecorder
}))
environment.assertionHandler = tempAssertionHandler

do {
try await closure()
} catch {
let failureMessage = FailureMessage()
failureMessage.stringValue = "unexpected error thrown: <\(error)>"
let location = SourceLocation(file: file, line: line)
tempAssertionHandler.assert(false, message: failureMessage, location: location)
}
}

/// Captures expectations that occur in the given closure. Note that all
/// expectations will still go through to the default Nimble handler.
///
/// This can be useful if you want to gather information about expectations
/// that occur within a closure.
///
/// @warning
/// Unlike the synchronous version of this call, this does not support catching Objective-C exceptions.
///
/// @param silently expectations are no longer send to the default Nimble
/// assertion handler when this is true. Defaults to false.
///
/// @see gatherFailingExpectations
public func gatherExpectations(silently: Bool = false, closure: () async -> Void) async -> [AssertionRecord] {
let previousRecorder = NimbleEnvironment.activeInstance.assertionHandler
let recorder = AssertionRecorder()
let handlers: [AssertionHandler]

if silently {
handlers = [recorder]
} else {
handlers = [recorder, previousRecorder]
}

let dispatcher = AssertionDispatcher(handlers: handlers)
await withAssertionHandler(dispatcher, closure: closure)
return recorder.assertions
}

/// Captures failed expectations that occur in the given closure. Note that all
/// expectations will still go through to the default Nimble handler.
///
/// This can be useful if you want to gather information about failed
/// expectations that occur within a closure.
///
/// @warning
/// Unlike the synchronous version of this call, this does not support catching Objective-C exceptions.
///
/// @param silently expectations are no longer send to the default Nimble
/// assertion handler when this is true. Defaults to false.
///
/// @see gatherExpectations
/// @see raiseException source for an example use case.
public func gatherFailingExpectations(silently: Bool = false, closure: () async -> Void) async -> [AssertionRecord] {
let assertions = await gatherExpectations(silently: silently, closure: closure)
return assertions.filter { assertion in
!assertion.success
}
}
15 changes: 15 additions & 0 deletions Sources/Nimble/Adapters/AssertionRecorder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ extension NMBExceptionCapture {
/// Allows you to temporarily replace the current Nimble assertion handler with
/// the one provided for the scope of the closure.
///
/// @warning
/// This form of `withAssertionHandler` does not work in any kind of
/// async context. Use the async form of `withAssertionHandler`
/// if you are running tests in an async context.
///
/// Once the closure finishes, then the original Nimble assertion handler is restored.
///
/// @see AssertionHandler
Expand Down Expand Up @@ -86,6 +91,11 @@ public func withAssertionHandler(_ tempAssertionHandler: AssertionHandler,
/// This can be useful if you want to gather information about expectations
/// that occur within a closure.
///
/// @warning
/// This form of `gatherExpectations` does not work in any kind of
/// async context. Use the async form of `gatherExpectations`
/// if you are running tests in an async context.
///
/// @param silently expectations are no longer send to the default Nimble
/// assertion handler when this is true. Defaults to false.
///
Expand All @@ -112,6 +122,11 @@ public func gatherExpectations(silently: Bool = false, closure: () -> Void) -> [
/// This can be useful if you want to gather information about failed
/// expectations that occur within a closure.
///
/// @warning
/// This form of `gatherFailingExpectations` does not work in any kind of
/// async context. Use the async form of `gatherFailingExpectations`
/// if you are running tests in an async context.
///
/// @param silently expectations are no longer send to the default Nimble
/// assertion handler when this is true. Defaults to false.
///
Expand Down
70 changes: 70 additions & 0 deletions Sources/Nimble/DSL+AsyncAwait.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import Dispatch

private func convertAsyncExpression<T>(_ asyncExpression: () async throws -> T) async -> (() throws -> T) {
let result: Result<T, Error>
do {
Expand Down Expand Up @@ -43,3 +45,71 @@ public func expect(file: FileString = #file, line: UInt = #line, _ expression: @
location: SourceLocation(file: file, line: line),
isClosure: true))
}

/// Wait asynchronously until the done closure is called or the timeout has been reached.
///
/// @discussion
/// Call the done() closure to indicate the waiting has completed.
///
/// @warning
/// Unlike the synchronous version of this call, this does not support catching Objective-C exceptions.
public func waitUntil(timeout: DispatchTimeInterval = AsyncDefaults.timeout, file: FileString = #file, line: UInt = #line, action: @escaping (@escaping () -> Void) async -> Void) async {
await throwableUntil(timeout: timeout) { done in
await action(done)
}
}

/// Wait asynchronously until the done closure is called or the timeout has been reached.
///
/// @discussion
/// Call the done() closure to indicate the waiting has completed.
///
/// @warning
/// Unlike the synchronous version of this call, this does not support catching Objective-C exceptions.
public func waitUntil(timeout: DispatchTimeInterval = AsyncDefaults.timeout, file: FileString = #file, line: UInt = #line, action: @escaping (@escaping () -> Void) -> Void) async {
await throwableUntil(timeout: timeout, file: file, line: line) { done in
action(done)
}
}

private enum ErrorResult {
case error(Error)
case none
}

private func throwableUntil(
timeout: DispatchTimeInterval,
file: FileString = #file,
line: UInt = #line,
action: @escaping (@escaping () -> Void) async throws -> Void) async {
let awaiter = NimbleEnvironment.activeInstance.awaiter
let leeway = timeout.divided
let result = await awaiter.performBlock(file: file, line: line) { @MainActor (done: @escaping (ErrorResult) -> Void) async throws -> Void in
do {
try await action {
done(.none)
}
} catch let e {
done(.error(e))
}
}
.timeout(timeout, forcefullyAbortTimeout: leeway)
.wait("waitUntil(...)", file: file, line: line)

switch result {
case .incomplete: internalError("Reached .incomplete state for waitUntil(...).")
case .blockedRunLoop:
fail(blockedRunLoopErrorMessageFor("-waitUntil()", leeway: leeway),
file: file, line: line)
case .timedOut:
fail("Waited more than \(timeout.description)", file: file, line: line)
case let .raisedException(exception):
fail("Unexpected exception raised: \(exception)")
case let .errorThrown(error):
fail("Unexpected error thrown: \(error)")
case .completed(.error(let error)):
fail("Unexpected error thrown: \(error)")
case .completed(.none): // success
break
}
}
6 changes: 2 additions & 4 deletions Sources/Nimble/Matchers/BeCloseTo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -110,10 +110,8 @@ public func beCloseTo<Value: FloatingPoint, Values: Collection>(
return .doesNotMatch
}

for index in actualValues.indices {
if abs(actualValues[index] - expectedValues[index]) > delta {
return .doesNotMatch
}
for index in actualValues.indices where abs(actualValues[index] - expectedValues[index]) > delta {
return .doesNotMatch
}
return .matches
}
Expand Down
6 changes: 2 additions & 4 deletions Sources/Nimble/Matchers/ContainElementSatisfying.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,8 @@ public func containElementSatisfying<S: Sequence>(
}

if let sequence = try actualExpression.evaluate() {
for object in sequence {
if predicate(object) {
return PredicateResult(bool: true, message: message)
}
for object in sequence where predicate(object) {
return PredicateResult(bool: true, message: message)
}

return PredicateResult(bool: false, message: message)
Expand Down
4 changes: 2 additions & 2 deletions Sources/Nimble/Matchers/ThrowAssertion.swift
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
// swiftlint:disable all
#if canImport(CwlPreconditionTesting) && (os(macOS) || os(iOS))
import CwlPreconditionTesting
#elseif canImport(CwlPosixPreconditionTesting)
import CwlPosixPreconditionTesting
#elseif canImport(Glibc)
// swiftlint:disable all
import Glibc

// This function is called from the signal handler to shut down the thread and return 1 (indicating a SIGILL was received).
Expand Down Expand Up @@ -79,7 +79,6 @@ public func catchBadInstruction(block: @escaping () -> Void) -> BadInstructionEx

return caught ? BadInstructionException() : nil
}
// swiftlint:enable all
#endif

public func throwAssertion<Out>() -> Predicate<Out> {
Expand Down Expand Up @@ -144,3 +143,4 @@ public func throwAssertion<Out>() -> Predicate<Out> {
#endif
}
}
// swiftlint:enable all
Loading