diff --git a/Sources/Nimble/DSL+Wait.swift b/Sources/Nimble/DSL+Wait.swift index a91e919a..4f912dd7 100644 --- a/Sources/Nimble/DSL+Wait.swift +++ b/Sources/Nimble/DSL+Wait.swift @@ -69,7 +69,7 @@ public class NMBWait: NSObject { } } } - }.timeout(timeout, forcefullyAbortTimeout: leeway).wait( + }.timeout(timeout, forcefullyAbortTimeout: leeway, isContinuous: false).wait( "waitUntil(...)", sourceLocation: SourceLocation(fileID: fileID, filePath: file, line: line, column: column) ) diff --git a/Sources/Nimble/Polling+AsyncAwait.swift b/Sources/Nimble/Polling+AsyncAwait.swift index 2238fb42..d4486b8a 100644 --- a/Sources/Nimble/Polling+AsyncAwait.swift +++ b/Sources/Nimble/Polling+AsyncAwait.swift @@ -42,12 +42,12 @@ internal actor Poller { fnName: fnName) { if self.updateMatcherResult(result: try await matcherRunner()) .toBoolean(expectation: style) { - if matchStyle.isContinous { + if matchStyle.isContinuous { return .incomplete } return .finished(true) } else { - if matchStyle.isContinous { + if matchStyle.isContinuous { return .finished(false) } else { return .incomplete diff --git a/Sources/Nimble/Polling.swift b/Sources/Nimble/Polling.swift index c74facb6..4ff995f1 100644 --- a/Sources/Nimble/Polling.swift +++ b/Sources/Nimble/Polling.swift @@ -69,7 +69,7 @@ public struct PollingDefaults: @unchecked Sendable { internal enum AsyncMatchStyle { case eventually, never, always - var isContinous: Bool { + var isContinuous: Bool { switch self { case .eventually: return false @@ -96,15 +96,16 @@ internal func poll( pollInterval: poll, timeoutInterval: timeout, sourceLocation: actualExpression.location, - fnName: fnName) { + fnName: fnName, + isContinuous: matchStyle.isContinuous) { lastMatcherResult = try matcher.satisfies(uncachedExpression) if lastMatcherResult!.toBoolean(expectation: style) { - if matchStyle.isContinous { + if matchStyle.isContinuous { return .incomplete } return .finished(true) } else { - if matchStyle.isContinous { + if matchStyle.isContinuous { return .finished(false) } else { return .incomplete diff --git a/Sources/Nimble/Utils/PollAwait.swift b/Sources/Nimble/Utils/PollAwait.swift index 35758c70..edac3ae0 100644 --- a/Sources/Nimble/Utils/PollAwait.swift +++ b/Sources/Nimble/Utils/PollAwait.swift @@ -157,7 +157,7 @@ internal class AwaitPromiseBuilder { self.trigger = trigger } - func timeout(_ timeoutInterval: NimbleTimeInterval, forcefullyAbortTimeout: NimbleTimeInterval) -> Self { + func timeout(_ timeoutInterval: NimbleTimeInterval, forcefullyAbortTimeout: NimbleTimeInterval, isContinuous: Bool) -> Self { /// = Discussion = /// /// There's a lot of technical decisions here that is useful to elaborate on. This is @@ -217,7 +217,7 @@ internal class AwaitPromiseBuilder { let didNotTimeOut = timedOutSem.wait(timeout: now) != .success let timeoutWasNotTriggered = semTimedOutOrBlocked.wait(timeout: .now()) == .success if didNotTimeOut && timeoutWasNotTriggered { - if self.promise.resolveResult(.blockedRunLoop) { + if self.promise.resolveResult(isContinuous ? .timedOut : .blockedRunLoop) { #if canImport(CoreFoundation) CFRunLoopStop(CFRunLoopGetMain()) #else @@ -385,6 +385,7 @@ internal func pollBlock( timeoutInterval: NimbleTimeInterval, sourceLocation: SourceLocation, fnName: String = #function, + isContinuous: Bool, expression: @escaping () throws -> PollStatus) -> PollResult { let awaiter = NimbleEnvironment.activeInstance.awaiter let result = awaiter.poll(pollInterval) { () throws -> Bool? in @@ -393,7 +394,7 @@ internal func pollBlock( } return nil } - .timeout(timeoutInterval, forcefullyAbortTimeout: timeoutInterval.divided) + .timeout(timeoutInterval, forcefullyAbortTimeout: timeoutInterval.divided, isContinuous: isContinuous) .wait(fnName, sourceLocation: sourceLocation) return result diff --git a/Tests/NimbleTests/PollingTest.swift b/Tests/NimbleTests/PollingTest.swift index 9960eec1..b2a3a66d 100644 --- a/Tests/NimbleTests/PollingTest.swift +++ b/Tests/NimbleTests/PollingTest.swift @@ -137,6 +137,33 @@ final class PollingTest: XCTestCase { } } } + + func testToEventuallyDetectsStalledMainThreadActivity() { + func spinAndReturnTrue() -> Bool { + Thread.sleep(forTimeInterval: 0.5) + return true + } + let msg = "expected to eventually be true, got (timed out, but main run loop was unresponsive)." + failsWithErrorMessage(msg) { + expect(spinAndReturnTrue()).toEventually(beTrue()) + } + } + + func testToNeverDoesNotFailStalledMainThreadActivity() { + func spinAndReturnTrue() -> Bool { + Thread.sleep(forTimeInterval: 0.5) + return true + } + expect(spinAndReturnTrue()).toNever(beFalse()) + } + + func testToAlwaysDetectsStalledMainThreadActivity() { + func spinAndReturnTrue() -> Bool { + Thread.sleep(forTimeInterval: 0.5) + return true + } + expect(spinAndReturnTrue()).toAlways(beTrue()) + } func testCombiningAsyncWaitUntilAndToEventuallyIsNotAllowed() { // Currently we are unable to catch Objective-C exceptions when built by the Swift Package Manager