Run each GUI test in its own forked subprocess#7
Merged
Conversation
Offscreen Qt is a single process-wide QApplication with a global QThreadPool
and a PySide wrapper cache keyed by C++ pointer address. Objects leaked by one
test get their address — and a stale Python wrapper — reused by a later test,
surfacing as an intermittent SIGSEGV or a bogus AttributeError on a recycled
wrapper (e.g. '.connect' on a QGraphicsItemGroup inside QMenu.addAction). The
between-tests drain narrowed the window but can't fully close it from inside a
shared process.
Mark every GUI test (the existing GUI_TEST_MODULES / mixed-Qt detection) with
pytest.mark.forked when the Qt preflight passes, so each runs in a fresh address
space. Cross-test object/heap reuse becomes impossible, and any residual crash
is contained to a single reported test ("CRASHED with signal N") instead of
aborting the whole run. Requires pytest-forked (added to the dev/all extras);
without it the marker is a harmless no-op and tests run in-process as before.
Non-GUI tests are unaffected — they are not marked and keep running in-process.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
jacobson30-bot
added a commit
that referenced
this pull request
Jun 5, 2026
Several test modules construct a real QApplication (via a local `qapp` fixture or inline) but were absent from conftest's GUI_TEST_MODULES / MIXED_QT sets. Consequently they bypassed the `_qt_application_preflight` subprocess gate -- hard-crashing (SIGABRT) instead of skipping on boxes without a working Qt offscreen plugin -- and were not marked `pytest.mark.forked`, so in CI they ran in-process, re-exposing the cross-test Qt wrapper/heap reuse that PR #7 eliminated for the other GUI modules. Add the fully-GUI modules (every test needs Qt) to GUI_TEST_MODULES: fft_phase_gui, fft_selection_gui, mains_pickup_gui, roi_resize_handles_canvas, worker_signals_lifetime. Add the mixed modules (only some tests use the qapp fixture; the rest are pure math/text and must keep running) to MIXED_QT_FIXTURE_MODULES: fft_viewer_utils, definitions_dialog, lattice_grid. The lattice_grid inline-QApplication export test stays gated via MIXED_QT_TESTS, which fixturename-based gating cannot see. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #6 (the between-tests Qt drain). The drain killed the run-aborting SIGSEGV, but a rarer soft manifestation of the same disease remained: PySide's wrapper cache (keyed by C++ pointer address) hands back a stale wrapper for a recycled address, e.g.
'QGraphicsItemGroup' object has no attribute 'connect'raised by a plainQMenu.addAction(...). You can't fully close cross-test Qt heap reuse from inside one shared process.Fix
Run each GUI test in its own forked subprocess (
pytest.mark.forked, applied to the existing GUI-test detection inconftest.pywhen the Qt preflight passes). A fresh address space per test makes cross-test object/heap reuse impossible, and any residual crash is contained to a single reported test instead of aborting the whole run.Verified the containment locally: a
@pytest.mark.forkedtest that segfaults reportsCRASHED with signal 11and the next test still runs (1 failed, 1 passed).pytest-forkedto thedev/allextras. Without it, the marker is a harmless no-op and tests run in-process (prior behaviour).Running CI several times to confirm the GUI suite is stably green.
🤖 Generated with Claude Code