Quick / Nimble

A Matcher Framework for Swift and Objective-C

Home Page:https://quick.github.io/Nimble/documentation/nimble/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

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

obrhoff opened this issue · comments

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:

  • Nimble: 13.0.0
  • Xcode Version: 15.0.1 (15A507)
  • Swift Version: 5.9

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

  • Swift Package Manager *5.8.

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

@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.

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

@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.

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.