Summary
When the test suite runs with visible Qt windows (native mode, i.e. without QT_QPA_PLATFORM=offscreen), the process sporadically crashes with a Windows access violation (0xC0000005, exit code -1073741819) during the teardown of datalab/tests/features/common/textimport_unit_test.py::test_import_wizard. The crash is a native C++ fault (no Python faulthandler traceback is produced) that occurs while the TextImportWizard's embedded PlotPy preview is being destroyed by the garbage collector.
Impact
- CI is not affected: continuous integration runs in
offscreen mode, where the raster backend allocates no native windows and the suite is 100% green (24/24 matrix cells in our benchmark).
- Real-world usage is unlikely to hit it: a user opens the import wizard once and closes it; the crash requires many wizard create/destroy cycles (or accumulated native state) in a single process.
- Test reliability: in on-screen / native test runs it produces flaky crashes (~12–17% over a full native suite run) that abort the whole pytest session, polluting the native test matrix.
Environment
- OS: Windows
- Qt: PyQt5 5.15.11 / Qt 5.15.2
- Plotting stack: PlotPy 2.10, PythonQwt (version-independent — reproduced identically on 0.15.0 and 0.16.2), guidata 3.14
- Python: 3.9 → 3.14 (version-independent)
- Mode: native (on-screen) only; never reproduced in
offscreen
Symptoms
- Exit code
-1073741819 (0xC0000005, ACCESS_VIOLATION).
- The crash always occurs at
test_import_wizard: the test line is printed but no PASSED/FAILED ever follows.
- No
faulthandler traceback → the fault is in native code (Qt/PlotPy), not in Python.
- Sporadic: the same cell sometimes passes and sometimes crashes; in isolation the test passes repeatedly, which initially masked the issue.
Root cause
test_import_wizard builds four TextImportWizard instances in a loop. Each wizard embeds a heavy PlotPy preview (GraphicalRepresentationPage → PlotWidget with toolbar, item list and contrast panels, plus a connected SIG_ITEMS_CHANGED signal). The wizards are never closed or deleted explicitly, so their native resources are torn down lazily by the garbage collector. In native mode this lazy, unordered teardown of several plot widgets races inside Qt/PlotPy and occasionally dereferences freed memory, producing the access violation. In offscreen mode there are no native windows, so the path is never exercised.
The crash is independent of PythonQwt version (controlled native matrix: v0.15.0 ≡ 0.16.2, both crash at the same rate), of the DataLab branch (develop and main), and of the Python version.
Reproduction (controlled)
A throwaway native harness reproduces the fault deterministically by mimicking the original lazy-GC teardown and amplifying it (accumulate a batch of fully-built wizards, then drop all references at once and force gc.collect()), run directly (not through run_with_env.py, which masks the child exit code). At a bounded load (80 wizards per run, well below the USER-handle cap so the access violation is the only failure mode):
| Behaviour |
Native crashes (0xC0000005) |
| Original (lazy-GC teardown) |
4 / 10 (~40%) |
| Deterministic cleanup (fix) |
0 / 6 |
Notes for anyone re-running this:
- Launch
python directly with PYTHONPATH set; run_with_env.py swallows the real exit code (always reports 1 on failure), which makes a native crash indistinguishable from a Python error.
- Do not
show() hundreds of wizards in a tight loop without letting the event loop recycle them: this exhausts the per-process USER handle quota (CreateWindowEx failed), a creation-side artifact unrelated to the teardown bug.
Fix
Release the wizard's native resources deterministically instead of relying on the garbage collector:
GraphicalRepresentationPage.cleanup() — disconnect SIG_ITEMS_CHANGED, call plot.del_all_items(), and clear the object/item list before destruction.
Wizard.cleanup() (base class) — invoke cleanup() on any page that exposes it, wired to the dialog's closeEvent.
test_import_wizard — close() + deleteLater() + processEvents() after each wizard so each teardown happens in a controlled, single-threaded order.
Validation
- Controlled native repro: 4/10 crashes before the fix, 0/6 after, at identical load.
- 100 sequential native create/destroy cycles with the fix: exit 0, no crash.
test_import_wizard unit test: green.
Summary
When the test suite runs with visible Qt windows (native mode, i.e. without
QT_QPA_PLATFORM=offscreen), the process sporadically crashes with a Windows access violation (0xC0000005, exit code-1073741819) during the teardown ofdatalab/tests/features/common/textimport_unit_test.py::test_import_wizard. The crash is a native C++ fault (no Pythonfaulthandlertraceback is produced) that occurs while theTextImportWizard's embedded PlotPy preview is being destroyed by the garbage collector.Impact
offscreenmode, where the raster backend allocates no native windows and the suite is 100% green (24/24 matrix cells in our benchmark).Environment
offscreenSymptoms
-1073741819(0xC0000005,ACCESS_VIOLATION).test_import_wizard: the test line is printed but noPASSED/FAILEDever follows.faulthandlertraceback → the fault is in native code (Qt/PlotPy), not in Python.Root cause
test_import_wizardbuilds fourTextImportWizardinstances in a loop. Each wizard embeds a heavy PlotPy preview (GraphicalRepresentationPage→PlotWidgetwith toolbar, item list and contrast panels, plus a connectedSIG_ITEMS_CHANGEDsignal). The wizards are never closed or deleted explicitly, so their native resources are torn down lazily by the garbage collector. In native mode this lazy, unordered teardown of several plot widgets races inside Qt/PlotPy and occasionally dereferences freed memory, producing the access violation. Inoffscreenmode there are no native windows, so the path is never exercised.The crash is independent of PythonQwt version (controlled native matrix: v0.15.0 ≡ 0.16.2, both crash at the same rate), of the DataLab branch (develop and main), and of the Python version.
Reproduction (controlled)
A throwaway native harness reproduces the fault deterministically by mimicking the original lazy-GC teardown and amplifying it (accumulate a batch of fully-built wizards, then drop all references at once and force
gc.collect()), run directly (not throughrun_with_env.py, which masks the child exit code). At a bounded load (80 wizards per run, well below the USER-handle cap so the access violation is the only failure mode):0xC0000005)Notes for anyone re-running this:
pythondirectly withPYTHONPATHset;run_with_env.pyswallows the real exit code (always reports1on failure), which makes a native crash indistinguishable from a Python error.show()hundreds of wizards in a tight loop without letting the event loop recycle them: this exhausts the per-process USER handle quota (CreateWindowEx failed), a creation-side artifact unrelated to the teardown bug.Fix
Release the wizard's native resources deterministically instead of relying on the garbage collector:
GraphicalRepresentationPage.cleanup()— disconnectSIG_ITEMS_CHANGED, callplot.del_all_items(), and clear the object/item list before destruction.Wizard.cleanup()(base class) — invokecleanup()on any page that exposes it, wired to the dialog'scloseEvent.test_import_wizard—close()+deleteLater()+processEvents()after each wizard so each teardown happens in a controlled, single-threaded order.Validation
test_import_wizardunit test: green.