From 233fa590f96ba223c7416f1f180caca4affcb208 Mon Sep 17 00:00:00 2001 From: goodboy Date: Mon, 31 Aug 2026 16:18:35 -0400 Subject: [PATCH] Use `pytest-qt` ownership for gap overlay tests Let `pytest-qt` own each real `PlotWidget` and input source so fixture teardown closes them exactly once. Also, - select `PyQt6` and offscreen `Qt` before widget imports - isolate `QSettings` and activate `pyproject.toml` config - lock `pytest-qt` 4.5.0 and record verified harness commands - retain real `tractor` dialog coverage beside `Qt` regressions Prompt-IO: ai/prompt-io/opencode/20260831T005712Z_09ddcf50_prompt_io.md (this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`)) --- .../run-tests/test-harness-reference.md | 137 ++++++++++++------ .../20260831T005712Z_09ddcf50_prompt_io.md | 44 ++++++ ...20260831T005712Z_09ddcf50_prompt_io.raw.md | 45 ++++++ pyproject.toml | 2 + pytest.ini | 3 - tests/conftest.py | 18 +++ tests/test_gap_overlays.py | 84 ++++------- uv.lock | 22 ++- 8 files changed, 249 insertions(+), 106 deletions(-) create mode 100644 ai/prompt-io/opencode/20260831T005712Z_09ddcf50_prompt_io.md create mode 100644 ai/prompt-io/opencode/20260831T005712Z_09ddcf50_prompt_io.raw.md delete mode 100644 pytest.ini diff --git a/.claude/skills/run-tests/test-harness-reference.md b/.claude/skills/run-tests/test-harness-reference.md index dc419d64..d4c9b225 100644 --- a/.claude/skills/run-tests/test-harness-reference.md +++ b/.claude/skills/run-tests/test-harness-reference.md @@ -10,30 +10,34 @@ here. - Project/import: `piker` - Test root: `tests/` - Supported Python: `>=3.12,<3.14` -- Preferred complete environment: worktree-local `py313` inside the current - `nix develop` shell -- The flake shell pins CPython 3.13 and sets - `UV_PROJECT_ENVIRONMENT=py313`. -- Verify the interpreter, package resolution, and dependency import before - running tests. A bare `py313` may lack the Qt binding supplied by Nix. +- Preferred verified environment: worktree-local `.venv`. +- The testing group includes pytest and the locked `pytest-qt==4.5.0`. +- Verify the interpreter, local package resolution, and dependency versions + before running tests. -Use an already-provisioned `py313` only when `import piker` succeeds: +This worktree's environment was provisioned with: ```text -py313/bin/python -m pytest -p no:xonsh +env UV_PROJECT_ENVIRONMENT=.venv uv sync --group testing --frozen ``` -If direct environment paths are unavailable, an existing uv environment can -be used without changing it, subject to the same import check: +Do not rerun provisioning without approval. Use the already-provisioned +environment directly: ```text -UV_PROJECT_ENVIRONMENT=py313 uv run --frozen --no-sync python -m pytest -p no:xonsh +.venv/bin/python -m pytest ``` -Ask before running provisioning commands such as: +If direct environment paths are unavailable, use uv without changing the +environment, subject to the same import check: + +```text +env UV_PROJECT_ENVIRONMENT=.venv uv run --frozen --no-sync python -m pytest +``` + +Ask before entering or provisioning alternate environments such as: ```text -UV_PROJECT_ENVIRONMENT=py313 uv sync --dev --all-extras --no-group lint nix develop nix-shell default.nix ``` @@ -42,43 +46,41 @@ nix-shell default.nix X11 shell. Do not use `develop.nix` for current testing; it retains the old Python 3.11, Poetry, and Qt 5 stack. -The current root-checkout `py313` resolves `piker` locally but fails -`import piker` outside Nix because PyQtGraph cannot import PyQt or PySide. Do -not treat that environment as test-ready and do not enter `nix develop` -without approval: its shell hook may recreate and sync `py313`. - -Plain `uv sync` does not include the testing group. The `dbs` dependency group -is also absent from normal dev-shell provisioning. +Do not enter `nix develop` without approval: its shell hook may provision its +own configured environment. Plain `uv sync` does not include the testing +group; use `--group testing` when provisioning is explicitly authorized. The +`dbs` dependency group is also absent from normal dev-shell provisioning. ## Commands Base command in the preferred environment: ```text -py313/bin/python -m pytest -p no:xonsh +.venv/bin/python -m pytest ``` -The explicit `-p no:xonsh` is required. The tracked comments-only -`pytest.ini` takes precedence over `pyproject.toml`, so the intended -`addopts = "-p no:xonsh"` and `testpaths = ["tests"]` are inactive. -Always pass a test path or node ID explicitly. +The comments-only `pytest.ini` has been deleted. The authoritative +`[tool.pytest.ini_options]` in `pyproject.toml` sets `testpaths = ["tests"]`, +`addopts = "-p no:xonsh"`, and `qt_api = "pyqt6"`; do not repeat +`-p no:xonsh` in ordinary commands. Still pass a test path or node ID when a +deterministic scope is required. Package-resolution check that does not import Piker's dependencies: ```text -py313/bin/python -c 'import importlib.util, pathlib, sys; root = pathlib.Path.cwd().resolve(); spec = importlib.util.find_spec("piker"); mod = pathlib.Path(spec.origin).resolve(); print(sys.executable); print(mod); assert mod.is_relative_to(root)' +.venv/bin/python -c 'import importlib.util, pathlib, sys; root = pathlib.Path.cwd().resolve(); spec = importlib.util.find_spec("piker"); mod = pathlib.Path(spec.origin).resolve(); print(sys.executable); print(mod); assert mod.is_relative_to(root)' ``` Dependency import check, required before collection or execution: ```text -py313/bin/python -c 'import pathlib, piker, sys; root = pathlib.Path.cwd().resolve(); mod = pathlib.Path(piker.__file__).resolve(); print(sys.executable); print(mod); assert mod.is_relative_to(root)' +.venv/bin/python -c 'from importlib.metadata import version; import pathlib, piker, pytestqt, sys; root = pathlib.Path.cwd().resolve(); mod = pathlib.Path(piker.__file__).resolve(); qt_ver = version("pytest-qt"); print(sys.executable); print(mod); print(qt_ver); assert mod.is_relative_to(root); assert qt_ver == "4.5.0"' ``` Safe core collection check: ```text -py313/bin/python -m pytest -p no:xonsh -q --collect-only tests/test_watchlists.py tests/test_accounting.py tests/test_services.py tests/test_ems.py tests/test_feeds.py tests/test_cli.py +.venv/bin/python -m pytest -q --collect-only tests/test_watchlists.py tests/test_accounting.py tests/test_services.py tests/test_ems.py tests/test_feeds.py tests/test_cli.py ``` Default first-pass flags are `-q -x --tb=short --no-header` unless the user @@ -86,7 +88,7 @@ requests otherwise. For actor-heavy tests, use one file or node per process with an outer timeout: ```text -timeout -k 5 300 py313/bin/python -m pytest -p no:xonsh -q +timeout -k 5 300 .venv/bin/python -m pytest -q ``` If an actor-heavy command exits `124` or `143`, retry that exact command once @@ -111,6 +113,8 @@ Deterministic or local first-pass targets: - `tests/test_ib_method_proxy.py` - `tests/test_history_backfill.py` - `tests/test_ldshm.py` +- `tests/test_dpi_font.py` +- `tests/test_gap_overlays.py` - `tests/test_accounting.py::test_account_file_default_empty` - `tests/test_services.py::test_runtime_boot` - `tests/test_services.py::test_datad_spawn` @@ -128,7 +132,6 @@ Require explicit authorization before running: fixtures plus possible live symcache generation; - `tests/test_accounting.py::test_ib_account_with_duplicated_mktids` - active broker/account configuration and state writes; -- `tests/test_dpi_font.py` - Qt/UI import and user-config side effects; - `tests/test_docker_services.py` - optional dependencies and containers; - `tests/test_questrade.py` - obsolete credentialed imports. @@ -169,9 +172,9 @@ whole suite supports UDS merely because the Tractor plugin exposes it. `tests/_inputs/account.binance.paper.toml` are used in place. Accounting contexts can write them on exit. Inspect `git diff -- tests/_inputs` after any selected accounting case. -- Importing `tests/test_dpi_font.py` constructs module-global font objects - before fixtures can isolate config. If explicitly requested, isolate - `XDG_CONFIG_HOME` before Python starts and use the proper Qt/Nix shell. +- `tests/conftest.py` selects offscreen PyQt6 defaults before test modules + import Qt, and its `qapp_args` fixture isolates `XDG_CONFIG_HOME` before + pytest-qt creates the application. - Piker and the installed Tractor pytest plugin do not provide a repository-local process or socket reaper. Never apply historical broad `pkill -f tractor._child` guidance automatically. @@ -181,6 +184,44 @@ whole suite supports UDS merely because the Tractor plugin exposes it. unlinks exact surviving names before failing the leaking test. It never scans `/dev/shm` or unlinks attachments created by another process. +## Qt/UI Tier + +The default pytest process environment is `QT_QPA_PLATFORM=offscreen` and +`PYTEST_QT_API=pyqt6`. `tests/conftest.py` installs both with `setdefault` +before Qt imports, so an explicit caller environment still wins. The +authoritative pytest config also selects `qt_api = "pyqt6"`, and +`pytest-qt==4.5.0` is locked. + +The session `qapp_args` fixture assigns a temporary `XDG_CONFIG_HOME` before +pytest-qt creates its `QApplication` and uses `piker-tests` as the application +argument. Deterministic offscreen Qt tests are normal local targets; they are +not opt-in merely because they import Qt or might otherwise read user config. + +Exact verified gap-overlay commands: + +```text +.venv/bin/python -m pytest -q --collect-only tests/test_gap_overlays.py +.venv/bin/python -m pytest -q -x --tb=short --no-header tests/test_gap_overlays.py +``` + +The first command collected 9 tests. The second passed all 9 in 2.00s with +one Tractor `trio.Event` boolean-use deprecation warning. + +- Let `qtbot` own every widget registered with `qtbot.addWidget()` through + teardown. Do not manually close or delete the same widget a second time. +- Keep production `MainWindow` out of this tier: its `closeEvent()` sends + `SIGINT` to the pytest process. +- A real compositor is an explicit process override, for example: + +```text +QT_QPA_PLATFORM=wayland .venv/bin/python -m pytest -q -x --tb=short --no-header tests/test_gap_overlays.py +QT_QPA_PLATFORM=xcb .venv/bin/python -m pytest -q -x --tb=short --no-header tests/test_gap_overlays.py +``` + +Real-compositor, visual, manual, and live UI tests remain opt-in. Do not +replace the offscreen default or infer compositor availability without +explicit authorization. + ## Test Layout ```text @@ -193,6 +234,7 @@ tests/ test_dpi_font.py Qt DPI/font behavior test_ems.py actor, EMS, and paper-position behavior test_feeds.py live Binance/Kraken feeds and shared memory + test_gap_overlays.py typed gap logic, offscreen Qt, and Tractor IPC test_ib_history.py deterministic IB history request formatting test_ib_method_proxy.py deterministic IB asyncio proxy routing test_history_backfill.py deterministic history/SHM orchestration @@ -220,7 +262,8 @@ tests/ | `piker/storage/cli.py` SHM null-slot guard | `tests/test_ldshm.py` | synthetic timestamps, no SHM mutation | | `piker/config.py` | `test_account_file_default_empty` | root-network test has a known mismatch | | `piker/accounting/` | targeted accounting node | some cases use live/configured state | -| `piker/ui/_style.py`, `piker/ui/qt.py` | `tests/test_dpi_font.py` | GUI/config-isolated opt-in | +| `piker/ui/_gaps.py`, `_annotate.py`, `_display.py`, `_interaction.py`, `_remote_ctl.py` | `tests/test_gap_overlays.py` | offscreen PyQt6 plus one local Tractor actor | +| `piker/ui/_style.py`, `piker/ui/qt.py` | `tests/test_dpi_font.py` | deterministic offscreen Qt | | `piker/service/_actor_runtime.py`, `_registry.py`, `_mngr.py` | `test_runtime_boot` | then `test_datad_spawn` | | `piker/service/`, `piker/data/_daemon.py` | `test_datad_spawn` | feed lifecycle cases are live | | `piker/data/feed.py`, `flows.py`, `_sharedmem.py`, `_sampling.py` | collect first | feed execution needs live permission | @@ -230,26 +273,28 @@ tests/ | Docker/service adapters | `tests/test_docker_services.py` | optional deps and containers | | project, lock, or Nix files | import check and safe collection | full collection is not safe by default | -Prefer deterministic filesystem/config tests, then local actor-runtime nodes, -then explicitly approved live broker, GUI, or container coverage. +Prefer deterministic filesystem/config and offscreen Qt tests, then local +actor-runtime nodes, then explicitly approved live broker, visual UI, or +container coverage. ## Quick Checks ```text -py313/bin/python -c 'import importlib.util, pathlib, sys; root = pathlib.Path.cwd().resolve(); spec = importlib.util.find_spec("piker"); mod = pathlib.Path(spec.origin).resolve(); print(sys.executable); print(mod); assert mod.is_relative_to(root)' -py313/bin/python -c 'import pathlib, piker, sys; root = pathlib.Path.cwd().resolve(); mod = pathlib.Path(piker.__file__).resolve(); print(sys.executable); print(mod); assert mod.is_relative_to(root)' -py313/bin/python -m pytest -p no:xonsh -q tests/test_watchlists.py -py313/bin/python -m pytest -p no:xonsh -q tests/test_accounting.py::test_account_file_default_empty -timeout -k 5 300 py313/bin/python -m pytest -p no:xonsh -q tests/test_services.py::test_runtime_boot -timeout -k 5 300 py313/bin/python -m pytest -p no:xonsh -q tests/test_services.py::test_datad_spawn -timeout -k 5 300 py313/bin/python -m pytest -p no:xonsh -q tests/test_ems.py::test_ems_err_on_bad_broker +.venv/bin/python -c 'import importlib.util, pathlib, sys; root = pathlib.Path.cwd().resolve(); spec = importlib.util.find_spec("piker"); mod = pathlib.Path(spec.origin).resolve(); print(sys.executable); print(mod); assert mod.is_relative_to(root)' +.venv/bin/python -c 'from importlib.metadata import version; import pathlib, piker, pytestqt, sys; root = pathlib.Path.cwd().resolve(); mod = pathlib.Path(piker.__file__).resolve(); qt_ver = version("pytest-qt"); print(sys.executable); print(mod); print(qt_ver); assert mod.is_relative_to(root); assert qt_ver == "4.5.0"' +.venv/bin/python -m pytest -q --collect-only tests/test_gap_overlays.py +.venv/bin/python -m pytest -q -x --tb=short --no-header tests/test_gap_overlays.py +.venv/bin/python -m pytest -q tests/test_watchlists.py +.venv/bin/python -m pytest -q tests/test_accounting.py::test_account_file_default_empty +timeout -k 5 300 .venv/bin/python -m pytest -q tests/test_services.py::test_runtime_boot +timeout -k 5 300 .venv/bin/python -m pytest -q tests/test_services.py::test_datad_spawn +timeout -k 5 300 .venv/bin/python -m pytest -q tests/test_ems.py::test_ems_err_on_bad_broker ``` ## Known Outcomes -- The current root-checkout `py313` fails `import piker` outside the Nix shell - with `ImportError: PyQtGraph requires one of PyQt5, PyQt6, PySide2 or - PySide6`. This is an incomplete environment, not an application regression. +- The verified `.venv` import check resolves `piker` beneath this worktree and + imports the locked `pytest-qt` 4.5.0. - `tests/test_accounting.py::test_root_conf_networking_section` currently expects `network.tsdb`, which is absent from the tracked config template. Match the current `KeyError: 'tsdb'` before classifying it as the known diff --git a/ai/prompt-io/opencode/20260831T005712Z_09ddcf50_prompt_io.md b/ai/prompt-io/opencode/20260831T005712Z_09ddcf50_prompt_io.md new file mode 100644 index 00000000..eebb1e99 --- /dev/null +++ b/ai/prompt-io/opencode/20260831T005712Z_09ddcf50_prompt_io.md @@ -0,0 +1,44 @@ +--- +model: openai/gpt-5.6-sol +service: opencode +session: unavailable +timestamp: 2026-08-31T00:57:12Z +git_ref: 09ddcf50 +scope: tests +substantive: true +raw_file: 20260831T005712Z_09ddcf50_prompt_io.raw.md +--- + +## Prompt + +Deeply evaluate Piker's lacking suites and build a comprehensive plan for +non-mocked, human-driven E2E coverage, beginning with the pytest-qt plan on +`chart_local_gapper`. Work in `/open-wkt`, build the first feasible GUI test +slice, add minimal adjacent CLI and Qt guide docs, keep the `/run-tests` +reference current, and stop before the stated Eastern deadline. + +## Response summary + +Audits the existing suite and human-facing surfaces, creates a risk-ranked +cross-interface E2E plan, activates pytest-qt, migrates the real-Qt gap suite +to pytest-qt widget ownership, and records verified commands and minimal user +guides. Full `MainWindow` E2E remains deferred behind safe shutdown, guest +outcome, symbol-session ownership, and deterministic provider seams. + +## Files changed + +- `pyproject.toml`, `uv.lock`, `pytest.ini` - activate pytest-qt config. +- `tests/conftest.py` - establish the Qt process and settings contract. +- `tests/test_gap_overlays.py` - migrate real widgets to `qtbot` ownership. +- `piker/cli/README.rst` - add the minimal command-line guide. +- `piker/ui/README.rst` - add the minimal chart UI guide. +- `plans/opencode/human-facing-e2e-coverage.md` - persist the suite plan. +- `.claude/skills/run-tests/test-harness-reference.md` - record verified + test commands and boundaries. + +## Human edits + +The human requires isolated worktree execution, adjacent guide-style docs, +continuous `/run-tests` reference maintenance, non-mocked Qt interactions, +and a hard time cutoff. These constraints materially determine the patch +scope and defer unsafe full-window work. diff --git a/ai/prompt-io/opencode/20260831T005712Z_09ddcf50_prompt_io.raw.md b/ai/prompt-io/opencode/20260831T005712Z_09ddcf50_prompt_io.raw.md new file mode 100644 index 00000000..918b49ad --- /dev/null +++ b/ai/prompt-io/opencode/20260831T005712Z_09ddcf50_prompt_io.raw.md @@ -0,0 +1,45 @@ +--- +model: openai/gpt-5.6-sol +service: opencode +timestamp: 2026-08-31T00:57:12Z +git_ref: 09ddcf50 +diff_cmd: git diff HEAD +--- + +## Generated changes + +> `git diff HEAD -- pyproject.toml pytest.ini uv.lock` + +Activates pytest-qt with PyQt6 through the repository's authoritative +pytest configuration and locks pytest-qt 4.5.0. + +> `git diff HEAD -- tests/conftest.py tests/test_gap_overlays.py` + +Selects the offscreen PyQt6 test process before Qt imports, isolates Qt +configuration, migrates real chart-gap widgets to pytest-qt ownership, +and removes the conflicting manual widget teardown path. + +> `git diff HEAD -- piker/cli/README.rst piker/ui/README.rst` + +Adds minimal adjacent guides for the installed command and Qt chart +interfaces without freezing volatile subsystem details. + +> `git diff HEAD -- plans/opencode/human-facing-e2e-coverage.md` + +Records the evidence-based test-suite audit and phased human-facing E2E +strategy, distinguishing component, application, system, and live +qualification boundaries. + +> `git diff HEAD -- .claude/skills/run-tests/test-harness-reference.md` + +Records the verified environment, commands, Qt ownership rules, current +tier boundaries, and test outcomes. + +## Verification + +- Local package and pytest-qt 4.5.0 import check passes. +- Nine gap-overlay tests collect. +- The targeted file passes: 9 tests, one upstream Tractor warning. +- A duplicated same-process run passes: 18 tests, two equivalent + upstream Tractor warnings. +- `git diff --check` passes. diff --git a/pyproject.toml b/pyproject.toml index abbc90b7..68aac6a3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -149,6 +149,7 @@ repl = [ ] testing = [ "pytest", + "pytest-qt", ] de = [ # (linux) specific DEs "i3ipc>=2.2.1", @@ -177,6 +178,7 @@ console_output_style = 'progress' # https://docs.pytest.org/en/stable/how-to/plugins.html#disabling-plugins-from-autoloading # https://docs.pytest.org/en/stable/how-to/plugins.html#deactivating-unregistering-a-plugin-by-name addopts = '-p no:xonsh' +qt_api = 'pyqt6' # ------ tool.pytest ------ diff --git a/pytest.ini b/pytest.ini deleted file mode 100644 index 19d9d41b..00000000 --- a/pytest.ini +++ /dev/null @@ -1,3 +0,0 @@ -#[pytest] -#trio_mode=True -#log_cli=1 diff --git a/tests/conftest.py b/tests/conftest.py index ad7ecce4..0b098a76 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -9,6 +9,10 @@ from weakref import ( ref, ) +# These must be selected before test modules import Qt. +os.environ.setdefault('QT_QPA_PLATFORM', 'offscreen') +os.environ.setdefault('PYTEST_QT_API', 'pyqt6') + import pytest import tractor from piker import ( @@ -26,6 +30,20 @@ pytest_plugins: tuple[str] = ( ) +@pytest.fixture(scope='session') +def qapp_args( + tmp_path_factory: pytest.TempPathFactory, + +) -> list[str]: + ''' + Isolate Qt settings before pytest-qt creates `QApplication`. + + ''' + config_home: Path = tmp_path_factory.mktemp('qt-config') + os.environ['XDG_CONFIG_HOME'] = str(config_home) + return ['piker-tests'] + + def pytest_addoption(parser): parser.addoption("--ll", action="store", dest='loglevel', default=None, help="logging level to set when testing") diff --git a/tests/test_gap_overlays.py b/tests/test_gap_overlays.py index 5dbdee5f..629fec89 100644 --- a/tests/test_gap_overlays.py +++ b/tests/test_gap_overlays.py @@ -4,20 +4,17 @@ Typed chart-local gap-overlay regressions. ''' from collections.abc import ( Callable, - Iterator, ) from contextlib import AsyncExitStack -import os from types import SimpleNamespace -os.environ['QT_QPA_PLATFORM'] = 'offscreen' - import msgspec import numpy as np from PyQt6.QtGui import QKeyEvent from PyQt6.QtWidgets import QGraphicsScene import pyqtgraph as pg import pytest +from pytestqt.qtbot import QtBot import tractor from tractor._testing import tractor_test import trio @@ -59,10 +56,12 @@ class _ChartStub: self, fqme: str, array: np.ndarray, + qtbot: QtBot, ) -> None: self.fqme: str = fqme self.widget: pg.PlotWidget = pg.PlotWidget() + qtbot.addWidget(self.widget) self.viz: SimpleNamespace = SimpleNamespace( plot=self.widget.plotItem, shm=SimpleNamespace(array=array), @@ -115,6 +114,7 @@ def _ohlcv_array( def _display_state( fqme: str, + qtbot: QtBot, ) -> tuple[ SimpleNamespace, @@ -127,10 +127,12 @@ def _display_state( rt_chart: _ChartStub = _ChartStub( fqme, _ohlcv_array((1, 2, 4, 5)), + qtbot, ) hist_chart: _ChartStub = _ChartStub( fqme, _ohlcv_array((60, 120, 300, 360)), + qtbot, ) ds: SimpleNamespace = SimpleNamespace( fqme=fqme, @@ -142,22 +144,6 @@ def _display_state( return ds, (rt_chart, hist_chart) -@pytest.fixture(scope='session') -def qapp() -> Iterator[QApplication]: - ''' - Keep one offscreen Qt application alive for graphics tests. - - ''' - app: QApplication|None = QApplication.instance() - if app is None: - app = QApplication(['piker-gap-tests']) - app.setQuitOnLastWindowClosed(False) - - yield app - - app.processEvents() - - def test_gap_specs_and_wire_roundtrip() -> None: ''' Local detection and remote IPC share one typed request model. @@ -291,7 +277,7 @@ def test_gap_overlay_unknown_fqme_returns_typed_error() -> None: def test_gap_manager_real_qt_lifecycle( - qapp: QApplication, + qtbot: QtBot, ) -> None: ''' @@ -307,7 +293,7 @@ def test_gap_manager_real_qt_lifecycle( ''' ds: SimpleNamespace charts: tuple[_ChartStub, _ChartStub] - ds, charts = _display_state(FQME) + ds, charts = _display_state(FQME, qtbot) annots: dict[int, GapAnnotations] = {} gapman: GapOverlayMngr = GapOverlayMngr( dss={FQME: ds}, @@ -403,14 +389,11 @@ def test_gap_manager_real_qt_lifecycle( gapman.remove_owner('chart-local') assert annots == {} finally: - chart: _ChartStub - for chart in charts: - chart.close() - qapp.processEvents() + gapman.remove_owner('chart-local') def test_gap_annotations_reposition_after_prepend( - qapp: QApplication, + qtbot: QtBot, ) -> None: ''' @@ -426,7 +409,7 @@ def test_gap_annotations_reposition_after_prepend( ''' ds: SimpleNamespace charts: tuple[_ChartStub, _ChartStub] - ds, charts = _display_state(FQME) + ds, charts = _display_state(FQME, qtbot) annots: dict[int, GapAnnotations] = {} gapman: GapOverlayMngr = GapOverlayMngr( dss={FQME: ds}, @@ -445,7 +428,7 @@ def test_gap_annotations_reposition_after_prepend( old_scene_point: QPointF = item.mapToScene( old_br.center() ) - qapp.processEvents() + QApplication.processEvents() assert item in scene.items(old_scene_point) old_rects: np.ndarray = ( item._rectarray.ndarray().copy() @@ -458,7 +441,7 @@ def test_gap_annotations_reposition_after_prepend( fqme=FQME, timeframe=60, ) - qapp.processEvents() + QApplication.processEvents() new_rects: np.ndarray = item._rectarray.ndarray() np.testing.assert_allclose( @@ -476,14 +459,10 @@ def test_gap_annotations_reposition_after_prepend( assert item.scene() is charts[1].widget.scene() finally: gapman.remove_owner('chart-local') - chart: _ChartStub - for chart in charts: - chart.close() - qapp.processEvents() def test_duplicate_fqme_layers_use_local_chart_identity( - qapp: QApplication, + qtbot: QtBot, ) -> None: ''' @@ -499,10 +478,10 @@ def test_duplicate_fqme_layers_use_local_chart_identity( ''' first_ds: SimpleNamespace first_charts: tuple[_ChartStub, _ChartStub] - first_ds, first_charts = _display_state(FQME) + first_ds, first_charts = _display_state(FQME, qtbot) second_ds: SimpleNamespace second_charts: tuple[_ChartStub, _ChartStub] - second_ds, second_charts = _display_state(FQME) + second_ds, second_charts = _display_state(FQME, qtbot) annots: dict[int, GapAnnotations] = {} gapman: GapOverlayMngr = GapOverlayMngr( dss={FQME: second_ds}, @@ -534,14 +513,10 @@ def test_duplicate_fqme_layers_use_local_chart_identity( assert annots[second.aid].scene() is not None finally: gapman.remove_owner('chart-local') - chart: _ChartStub - for chart in (*first_charts, *second_charts): - chart.close() - qapp.processEvents() def test_startup_and_focused_toggle_use_real_qt( - qapp: QApplication, + qtbot: QtBot, monkeypatch: pytest.MonkeyPatch, ) -> None: @@ -560,10 +535,16 @@ def test_startup_and_focused_toggle_use_real_qt( first_ds: SimpleNamespace first_charts: tuple[_ChartStub, _ChartStub] - first_ds, first_charts = _display_state('first.test') + first_ds, first_charts = _display_state( + 'first.test', + qtbot, + ) second_ds: SimpleNamespace second_charts: tuple[_ChartStub, _ChartStub] - second_ds, second_charts = _display_state('second.test') + second_ds, second_charts = _display_state( + 'second.test', + qtbot, + ) dss: dict[str, SimpleNamespace] = { first_ds.fqme: first_ds, second_ds.fqme: second_ds, @@ -642,14 +623,10 @@ def test_startup_and_focused_toggle_use_real_qt( assert unrelated == [] finally: gapman.remove_owner('chart-local') - chart: _ChartStub - for chart in (*first_charts, *second_charts): - chart.close() - qapp.processEvents() def test_ctrl_g_event_renders_real_history_overlay( - qapp: QApplication, + qtbot: QtBot, ) -> None: ''' @@ -665,7 +642,7 @@ def test_ctrl_g_event_renders_real_history_overlay( ''' ds: SimpleNamespace charts: tuple[_ChartStub, _ChartStub] - ds, charts = _display_state(FQME) + ds, charts = _display_state(FQME, qtbot) annots: dict[int, GapAnnotations] = {} gapman: GapOverlayMngr = GapOverlayMngr( dss={FQME: ds}, @@ -686,6 +663,7 @@ def test_ctrl_g_event_renders_real_history_overlay( active=False, ) source: QWidget = QWidget() + qtbot.addWidget(source) source.order_mode = order_mode source._chart = ds.hist_chart source.linked = SimpleNamespace( @@ -727,12 +705,6 @@ def test_ctrl_g_event_renders_real_history_overlay( assert annots[aid].scene() is charts[1].widget.scene() finally: gapman.remove_owner('chart-local') - source.close() - source.deleteLater() - chart: _ChartStub - for chart in charts: - chart.close() - qapp.processEvents() _actor_requests: list[dict] = [] diff --git a/uv.lock b/uv.lock index c0233eb2..99ae3c54 100644 --- a/uv.lock +++ b/uv.lock @@ -1132,6 +1132,7 @@ dev = [ { name = "pyqt6" }, { name = "pyqtgraph" }, { name = "pytest" }, + { name = "pytest-qt" }, { name = "qdarkstyle" }, { name = "rapidfuzz" }, { name = "xonsh" }, @@ -1149,6 +1150,7 @@ repl = [ ] testing = [ { name = "pytest" }, + { name = "pytest-qt" }, ] uis = [ { name = "pyqt6" }, @@ -1206,6 +1208,7 @@ dev = [ { name = "pyqt6", specifier = ">=6.7.0,<7.0.0" }, { name = "pyqtgraph", specifier = ">=0.14.0" }, { name = "pytest" }, + { name = "pytest-qt" }, { name = "qdarkstyle", specifier = ">=3.0.2,<4.0.0" }, { name = "rapidfuzz", specifier = ">=3.2.0,<4.0.0" }, { name = "xonsh", specifier = ">=0.23.0" }, @@ -1219,7 +1222,10 @@ repl = [ { name = "pyperclip", specifier = ">=1.9.0" }, { name = "xonsh", specifier = ">=0.23.0" }, ] -testing = [{ name = "pytest" }] +testing = [ + { name = "pytest" }, + { name = "pytest-qt" }, +] uis = [ { name = "pyqt6", specifier = ">=6.7.0,<7.0.0" }, { name = "pyqtgraph", specifier = ">=0.14.0" }, @@ -1629,6 +1635,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, ] +[[package]] +name = "pytest-qt" +version = "4.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pluggy" }, + { name = "pytest" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d3/61/8bdec02663c18bf5016709b909411dce04a868710477dc9b9844ffcf8dd2/pytest_qt-4.5.0.tar.gz", hash = "sha256:51620e01c488f065d2036425cbc1cbcf8a6972295105fd285321eb47e66a319f", size = 128702, upload-time = "2025-07-01T17:24:39.889Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/d0/8339b888ad64a3d4e508fed8245a402b503846e1972c10ad60955883dcbb/pytest_qt-4.5.0-py3-none-any.whl", hash = "sha256:ed21ea9b861247f7d18090a26bfbda8fb51d7a8a7b6f776157426ff2ccf26eff", size = 37214, upload-time = "2025-07-01T17:24:38.226Z" }, +] + [[package]] name = "python-baseconv" version = "1.2.2"