diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2a4fa735f..c0356c628 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -52,6 +52,26 @@ jobs: sudo apt install libxkbcommon-x11-0 xvfb libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-xfixes0 x11-utils /sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -screen 0 1920x1200x24 -ac +extension GLX - name: Run the tests - # test_HighContentScreeningGui.py: PySide6 QObject double-init bug (separate issue) + # SQUID_PYTEST_HARD_EXIT: the suite intermittently segfaults during + # interpreter shutdown AFTER all tests pass (Qt destructor order vs + # Python GC — the same crash main_hcs.py avoids with os._exit(); seen + # on master in 3 of the 15 runs before 2026-07-13). The conftest hook + # hard-exits with pytest's real status once the session is complete, + # so real test failures still fail the step. run: python3 -m pytest --ignore=tests/control/test_HighContentScreeningGui.py working-directory: ./software + env: + SQUID_PYTEST_HARD_EXIT: "1" + - name: Run the GUI test in its own process + # Own invocation: the full-application GUI test leaves Qt/thread state + # behind that deterministically segfaults later Qt tests (e.g. + # test_channel_sequence) when run in the same pytest process. + # Step-scoped pyqt5 pin: without it, pytest-qt picks PySide6 for qtbot + # while the app code (qtpy) uses PyQt5, and the mixed bindings fail + # with a QObject double-init error. + run: python3 -m pytest tests/control/test_HighContentScreeningGui.py + working-directory: ./software + env: + QT_API: pyqt5 + PYTEST_QT_API: pyqt5 + SQUID_PYTEST_HARD_EXIT: "1" diff --git a/software/tests/control/conftest.py b/software/tests/control/conftest.py index 485098188..297f26d27 100644 --- a/software/tests/control/conftest.py +++ b/software/tests/control/conftest.py @@ -6,6 +6,8 @@ """ import logging +import os +import sys from unittest.mock import patch import pytest @@ -16,6 +18,27 @@ logger = logging.getLogger(__name__) +def pytest_sessionfinish(session, exitstatus): + session.config._squid_exitstatus = int(exitstatus) + + +def pytest_unconfigure(config): + """Optionally skip interpreter teardown after the test session. + + Under PyQt5 on Linux, a pytest process that constructed the full HCS GUI + segfaults during interpreter shutdown (Qt C++ destructor order conflicts + with Python GC) even though every test passed. main_hcs.py sidesteps the + same crash with os._exit(); SQUID_PYTEST_HARD_EXIT=1 lets CI's isolated + GUI-test invocation do likewise, preserving pytest's exit status. + """ + if os.environ.get("SQUID_PYTEST_HARD_EXIT") == "1": + sys.stdout.flush() + sys.stderr.flush() + # Default 1, not 0: if pytest_sessionfinish never ran (e.g. a + # sessionstart failure), an unrecorded status must fail the step. + os._exit(getattr(config, "_squid_exitstatus", 1)) + + def _make_tracking_init(original_init, instances_list): """Create a wrapper that tracks Microcontroller instances."""