Quick / Nimble

A Matcher Framework for Swift and Objective-C
https://quick.github.io/Nimble/documentation/nimble/
Apache License 2.0
4.8k stars 596 forks source link

toEventually in Xcode 15 somtimes results in "main run loop was unresponsive" #1095

Open obrhoff opened 9 months ago

obrhoff commented 9 months ago

What did you do?

We upgraded our Azure DevOps Pipeline from Xcode 14 to 15.

What did you expect to happen?

Tests that use toEventually() should succeed like before.

What actually happened instead?

Some Tests are now randomly failing with main run loop was unresponsive when they use toEventually()

The tests are really simple.

func testConsentOnAppear() {
    // when
    sut.handle(.appear)

    // then
    expect(self.userConsentProvider.calls)
        .toEventually(
            equal([.presentInitialConsent]
            )
        )
}

Environment

List the software versions you're using:

Please also mention which package manager you used and its version. Delete the other package managers in this list:

obrhoff commented 9 months ago

After digging more into this topic, I suspect that this is related to the issue: https://discuss.circleci.com/t/severe-performance-problems-with-xcode-15/49205

tahirmt commented 1 month ago

@obrhoff were you able to find any solution for this? I have many of these issues with toEventually and waitUntil tests. They work if the test is run individually but as a full suite I get this error a lot.

obrhoff commented 1 month ago

Unfortunately not. It still happened randomly, and I left the project to keep track of what happened in the meantime.

tahirmt commented 1 month ago

@younata do you have any ideas about this? Not sure how common this issue is but in our projects this happens 100% of the time when running the full suite.

younata commented 1 month ago

Hey @tahirmt

The "main run loop was unresponsive" error happens when the the main thread gets blocked while canceling the polling.

The way that polling expectations work is that they run 2 tasks at the same time. The first task runs on the main thread and does the work of continuously polling the matcher until the conditions of the assertion pass. The second task runs on a background thread, where it waits until the timeout period has passed, and if the second task hasn't been canceled yet, then it cancels the first task and reports a failure.

This second task failing to cancel the first task in time is what causes the "main run loop was unresponsive" error to happen. It's a minor race condition where if the main thread is blocked long enough for the DispatchSemaphore.wait(timeout:) to not return .success, then Nimble reports that error. With large codebases that make liberal use of polling expectations, the chances of this error happening increases dramatically, as you've discovered.

Fixing this race condition is on my radar, but this is rather complex code and I'm always a bit hesitant to mess with it.

aleksanderlorenc-lw commented 1 week ago

Has anyone found a resilient workaround for this? Every once in a while we seem to be affected by this issue