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`))wkt/replay_provider_e2e
parent
46ee5a8c4c
commit
233fa590f9
|
|
@ -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 <one-file-or-node>
|
||||
timeout -k 5 300 .venv/bin/python -m pytest -q <one-file-or-node>
|
||||
```
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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 ------
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
#[pytest]
|
||||
#trio_mode=True
|
||||
#log_cli=1
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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] = []
|
||||
|
|
|
|||
22
uv.lock
22
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"
|
||||
|
|
|
|||
Loading…
Reference in New Issue