From 646bb192e5925dbe21390e7273e60fec9aa6aaf7 Mon Sep 17 00:00:00 2001 From: Brian Nickel Date: Fri, 29 Aug 2025 10:38:00 -0700 Subject: [PATCH 1/3] Prevent failures on heavy load when doing continuous tests --- Sources/Nimble/DSL+Wait.swift | 2 +- Sources/Nimble/Polling.swift | 3 ++- Sources/Nimble/Utils/PollAwait.swift | 7 ++++--- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/Sources/Nimble/DSL+Wait.swift b/Sources/Nimble/DSL+Wait.swift index a91e919a2..4f912dd76 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.swift b/Sources/Nimble/Polling.swift index c74facb61..5f66a2c1e 100644 --- a/Sources/Nimble/Polling.swift +++ b/Sources/Nimble/Polling.swift @@ -96,7 +96,8 @@ internal func poll( pollInterval: poll, timeoutInterval: timeout, sourceLocation: actualExpression.location, - fnName: fnName) { + fnName: fnName, + isContinuous: matchStyle.isContinous) { lastMatcherResult = try matcher.satisfies(uncachedExpression) if lastMatcherResult!.toBoolean(expectation: style) { if matchStyle.isContinous { diff --git a/Sources/Nimble/Utils/PollAwait.swift b/Sources/Nimble/Utils/PollAwait.swift index 1bc1311ba..67ff0a59a 100644 --- a/Sources/Nimble/Utils/PollAwait.swift +++ b/Sources/Nimble/Utils/PollAwait.swift @@ -159,7 +159,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 @@ -234,7 +234,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 @@ -402,6 +402,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 @@ -410,7 +411,7 @@ internal func pollBlock( } return nil } - .timeout(timeoutInterval, forcefullyAbortTimeout: timeoutInterval.divided) + .timeout(timeoutInterval, forcefullyAbortTimeout: timeoutInterval.divided, isContinuous: isContinuous) .wait(fnName, sourceLocation: sourceLocation) return result From 573b001807405a0afac5d8bec1a0fa2ec55ced49 Mon Sep 17 00:00:00 2001 From: Brian Nickel Date: Fri, 29 Aug 2025 11:06:06 -0700 Subject: [PATCH 2/3] Add tests --- Tests/NimbleTests/PollingTest.swift | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/Tests/NimbleTests/PollingTest.swift b/Tests/NimbleTests/PollingTest.swift index a9d4d72ec..e30ef87c4 100644 --- a/Tests/NimbleTests/PollingTest.swift +++ b/Tests/NimbleTests/PollingTest.swift @@ -139,6 +139,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 From 2cb9d0bf5b40c91c5bbef5139c00c13a1799f1c6 Mon Sep 17 00:00:00 2001 From: Brian Nickel Date: Fri, 29 Aug 2025 11:19:22 -0700 Subject: [PATCH 3/3] Fix typo --- Sources/Nimble/Polling+AsyncAwait.swift | 4 ++-- Sources/Nimble/Polling.swift | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/Nimble/Polling+AsyncAwait.swift b/Sources/Nimble/Polling+AsyncAwait.swift index 2238fb425..d4486b8af 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 5f66a2c1e..4ff995f1b 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 @@ -97,15 +97,15 @@ internal func poll( timeoutInterval: timeout, sourceLocation: actualExpression.location, fnName: fnName, - isContinuous: matchStyle.isContinous) { + 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