Flaky test: arm_movr12_hang intermittently fails on the macos-14 - x86_64 wheel job
tests/regress/arm_movr12_hang.py fails intermittently in the cibuildwheel workflow, only in the Building on macos-14 - x86_64 - cp* job:
FAIL: runTest (arm_movr12_hang.MovHang)
File ".../tests/regress/arm_movr12_hang.py", line 27, in runTest
self.assertEqual(0x0, uc.reg_read(UC_ARM_REG_R12))
AssertionError: 0 != 291291 is 0x123, the sentinel written to r12 at line 18 — the movw r12, #0 never executed.
Occurrences
| Date | PR | Job |
|---|---|---|
| 2026-08-07 | #2382 | 92875026139 |
| 2026-07-30 | #2377 | 90842789202 |
| 2026-07-04 | #2318 | 85140337567 |
Each on changes touching neither ARM nor the Python bindings — and it is flaky, not platform-deterministic: in run 28706149643 the same job passed under cp37 and failed under cp38.
Cause
Line 25 gives a cold emu_start 500 microseconds, and the timer is started before vm_start (uc.c:1230), so that budget covers TCG code generation as well as execution. Codegen is most of it: the mapped page is zero-filled and 0x00000000 is a valid andeq r0, r0, r0, so translation runs to the 256-instruction block limit before a single instruction retires.
Natively that work measures ~82 µs (75–102 µs, x86_64 Linux, fresh process, timer disabled) — about 16% of the budget. So steady slowness does not explain the failure by itself, Rosetta 2 included, though the job does run under it (Image: macos-14-arm64, via /usr/bin/arch -x86_64 ...; no QEMU, that step is skipped here).
A scheduling stall is a better fit, and reproduces locally: under 2× CPU oversubscription the current test fails 2.5% of 1000 runs. Such stalls are not limited to loaded machines. The worst off-CPU stall measured 627 µs on an idle 16-core box, 14 ms at 2× load and 81 ms at 6×. All exceed the 500 µs budget, which would also account for the cp37-passes/cp38-fails signature.
The test was skipped for the same-looking failure until dea3c376 (#2100); that dropped QEMU from the Linux jobs, but macOS x86_64 still runs under translation.
Fix
#2385: pre-translate the block with uc_ctl_request_cache so the timeout bounds execution only (approach suggested by @PhilippTakacs below), and raise it to 10 ms for scheduling headroom. Measured 2.50% → 0% under the same load.
Edited: the native timing figure first given here (150–220 µs, "30–45% of the budget") was overstated. It included timer-thread creation, which is not charged against the timeout — the clock starts inside that thread.
Source: unicorn-engine/unicorn