Skip to content

tests: run the qt widget tests on the process main thread - #13374

Open
tilladam wants to merge 1 commit into
slint-ui:masterfrom
tilladam:fix/qt-widgets-test-hang
Open

tilladam wants to merge 1 commit into
slint-ui:masterfrom
tilladam:fix/qt-widgets-test-hang

Conversation

@tilladam

Copy link
Copy Markdown
Collaborator

On macOS, cargo test -p test-driver-rust --test widgets-qt hangs forever with 0% CPU. The qt style calls QStyle from the thread running the test, and QMacStyle instantiates AppKit controls for hit testing. On current macOS, AppKit's control layout goes through SwiftUI and blocks until it can run on the process main thread. libtest runs every test on a worker thread while the main thread waits for results, so the first NativeSlider hit test deadlocks:

NativeSlider::input_event
→ QMacStyle::hitTestComplexControl
→ QMacStylePrivate::cocoaControl
→ -[NSSlider initWithFrame:]
→ SwiftUICore ViewGraphRootValueUpdater.updateGraph()
→ _MovableLockSyncMain → pthread_cond_wait   (waits for the main thread, forever)

--test-threads=1 doesn't help: libtest still spawns a worker thread per test. CI never sees the hang because it filters these tests with --skip=qt::.

This complements #13370, which runs tests/run_tests.sh offscreen for machines without a display. With a display, the offscreen platform is not in effect for plain cargo test, and this change also lets the tests exercise the real macOS Qt style.

Design

The widgets-qt target gets harness = false and a build.rs-generated harness, following the pattern of the test-backends crate: libtest-mimic forks one subprocess per test (marked with --exact <name>, the same convention cargo nextest uses), and each subprocess runs its single test on its own main thread with its own thread-local testing platform. Parallelism is preserved because every subprocess has its own main thread.

The libtest CLI surface stays compatible: --skip=qt:: filters all tests out as before, --list prints the same names, and an --exact filter that names a test from another target reports zero matching tests instead of failing.

Testing

Everything measured on macOS arm64 (macOS 26.6, Qt with QMacStyle):

  • widgets-qt: 25/25 pass in ~7s, on both the macro and the build-time generation paths. Both previously hung on the first slider hit test.
  • Full test-driver-rust suite: 35/35 test binaries, 855 passed, 0 failed. The suite could not complete on this machine before.
  • -- --skip=qt:: (the CI invocation), -- --list, and cross-target -- --exact selection verified against the built binary.

Follow-up

tests/backends' own fork harness has the same latent panic on an unmatched --exact filter (run_test's unwrap_or_else(panic!) in tests/backends/tests/cases/harness.rs); the same fallthrough fix would apply there. Left out to keep this change to one target.

On macOS the qt style creates AppKit controls, and AppKit deadlocks
on any thread but the process main thread. libtest runs every test
on a worker thread, so the widgets-qt driver tests hung forever in
NativeSlider's QMacStyle hit test.

Give the widgets-qt target its own harness, following the pattern of
the test-backends crate: libtest-mimic forks one subprocess per test,
and each subprocess runs its single test on its own main thread with
its own thread-local testing platform. CI's '--skip=qt::' filter and
'--list' behave as before.
@tilladam
tilladam force-pushed the fix/qt-widgets-test-hang branch from 7949e89 to 908769c Compare September 13, 2026 09:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant