Compare commits
No commits in common. "4ab49082c1bfe2b4274915d8e09c068b5e833e5b" and "aadfd1cfcce14eae2ce24b5887c773a7f7822f4d" have entirely different histories.
4ab49082c1
...
aadfd1cfcc
|
|
@ -1,59 +0,0 @@
|
|||
---
|
||||
model: openai/gpt-5.6-sol
|
||||
service: opencode
|
||||
session: ses_faae5c497ffeS0ofaTEfbRo2LP
|
||||
timestamp: 2026-09-03T20:54:04Z
|
||||
git_ref: aadfd1cf
|
||||
scope: code
|
||||
substantive: true
|
||||
raw_file: 20260903T205404Z_aadfd1cf_prompt_io.raw.md
|
||||
---
|
||||
|
||||
## Prompt
|
||||
|
||||
Continue from the completed replay handoff when next steps are clear.
|
||||
Honor the prior decision to park replay and begin the live-backend-first
|
||||
chart E2E roadmap with the Phase A Qt/Trio lifecycle seam. First perform
|
||||
the approved exhaustive local scan, then create
|
||||
`wkt/live_chart_qtractor` from exact Phase 1 commit `aadfd1cf` and
|
||||
implement nonblocking startup, structured shutdown, observable readiness
|
||||
and completion, and provider-independent lifecycle coverage without new
|
||||
interlayer mocks.
|
||||
|
||||
## Response summary
|
||||
|
||||
Creates the isolated worktree after classifying two stale historical
|
||||
lifecycle experiments as related but not reusable. Extracts
|
||||
`start_qtractor()` and `QtractorSession`, replaces process-wide SIGINT on
|
||||
window close with owned Trio cancellation, publishes application
|
||||
readiness at the full `_async_main()` startup boundary, and preserves
|
||||
`run_qtractor()` as the blocking Qt-loop owner which unwraps the guest
|
||||
outcome. A headless regression proves two sequential real Tractor
|
||||
runtimes start and stop cleanly under one pytest-owned Qt application.
|
||||
|
||||
## Files changed
|
||||
|
||||
- `piker/ui/_exec.py` - add nonblocking session startup, lifecycle
|
||||
signals, owned cancellation, outcome capture, Qt cleanup, and the
|
||||
blocking CLI adapter.
|
||||
- `piker/ui/_window.py` - inject structured shutdown and remove normal
|
||||
window-close SIGINT while cleaning the global zoom filter.
|
||||
- `piker/ui/_app.py` - publish readiness after all application startup
|
||||
scopes are live.
|
||||
- `tests/ui/test_qtractor.py` - prove repeated real-Tractor lifecycle
|
||||
startup and close under pytest-qt.
|
||||
|
||||
## Human edits
|
||||
|
||||
The human chose exhaustive local discovery specifically to catch prior
|
||||
Qt/Trio experiments, confirmed the replay branch had been pushed to
|
||||
Gitea for backup, and directed continuation without another planning
|
||||
pause. The inherited human-approved roadmap parks replay, prioritizes a
|
||||
real Kraken chart, forbids additional interlayer mocks without a concrete
|
||||
regression, and fixes `aadfd1cf` as this worktree's base. The human also
|
||||
explicitly authorized the focused headless test through `nix develop`;
|
||||
no real-compositor run was authorized or performed. During final review,
|
||||
the human corrected nonconforming boolean layout, requested local type
|
||||
annotations wherever helper return types were not obvious at the binding,
|
||||
and replaced silent Qt disconnect suppression with Piker-style exception
|
||||
logging.
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
---
|
||||
model: openai/gpt-5.6-sol
|
||||
service: opencode
|
||||
timestamp: 2026-09-03T20:54:04Z
|
||||
git_ref: aadfd1cf
|
||||
diff_cmd: git diff HEAD
|
||||
---
|
||||
|
||||
## Generated changes
|
||||
|
||||
> `git diff HEAD -- piker/ui/_exec.py piker/ui/_window.py`
|
||||
|
||||
Extracts nonblocking `start_qtractor()` from the blocking CLI adapter.
|
||||
The returned `QtractorSession` exposes the production window and central
|
||||
widget, the Trio token and cancellation scope, readiness and completion
|
||||
Qt signals, the final `outcome.Outcome`, Qt-loop ownership, and an
|
||||
idempotent structured shutdown request. Caller-owned Qt application
|
||||
state and installed callbacks are restored when the guest completes.
|
||||
`MainWindow.closeEvent()` retains geometry persistence but delegates
|
||||
runtime teardown through the injected callback instead of sending
|
||||
process-wide SIGINT.
|
||||
|
||||
> `git diff HEAD -- piker/ui/_app.py`
|
||||
|
||||
Publishes application readiness only after initial symbol loading,
|
||||
order-mode startup, search registration, provider-search task startup,
|
||||
input handler entry, and startup-status removal.
|
||||
|
||||
> `git diff HEAD -- tests/ui/test_qtractor.py`
|
||||
|
||||
Adds a provider-independent lifecycle regression which runs two real
|
||||
Tractor root runtimes in sequence on pytest's unique registry. Real
|
||||
window-close events drive structured cancellation while signal, outcome,
|
||||
task-finalization, global-window, runtime, and shared-QApplication
|
||||
assertions prove restartable teardown without an interlayer fake.
|
||||
|
||||
Human review then brought the generated Python into project style:
|
||||
boolean connectives use dedicated vertical lines, opaque helper returns
|
||||
and control-flow targets carry local annotations, and Qt disconnect
|
||||
failures are logged instead of silently suppressed.
|
||||
|
||||
## Verification
|
||||
|
||||
- Ruff passes for all four changed Python files.
|
||||
- Python static compilation and `git diff --check` pass.
|
||||
- The first focused run exposed the test widget's missing production
|
||||
`on_win_resize()` contract; the narrow test widget was corrected.
|
||||
- The focused lifecycle regression passes with two sequential real
|
||||
Tractor sessions.
|
||||
- The combined UI harness and lifecycle run passes: 3 tests and two
|
||||
upstream Tractor `Registrar.uid` deprecation warnings.
|
||||
- A post-pass attempt to disconnect a pre-existing Qt signal proxy
|
||||
segfaulted during PyQt teardown and was fully reverted; it is not part
|
||||
of this patch. The restored lifecycle regression passes again.
|
||||
- All automated Qt execution used `--headless` and the offscreen
|
||||
platform.
|
||||
- The approved `nix develop` shell hook unexpectedly provisioned the
|
||||
ignored worktree-local `py313/` environment; no tracked dependency or
|
||||
lock files changed.
|
||||
|
|
@ -1,179 +0,0 @@
|
|||
---
|
||||
model: openai/gpt-6-astra
|
||||
service: opencode
|
||||
session: null
|
||||
timestamp: 2026-09-08T21:50:45Z
|
||||
git_ref: 1a2d4893
|
||||
scope: code
|
||||
substantive: true
|
||||
raw_file: 20260908T215045Z_1a2d4893_prompt_io.raw.md
|
||||
---
|
||||
|
||||
## Prompt
|
||||
|
||||
Continue implementation in SAME session/worktree
|
||||
/home/goodboy/repos/piker/wkts/live_chart_qtractor. User explicitly
|
||||
says proceed. No ownership takeover needed, no branch creation, no
|
||||
staging/commits, no ai.skillz changes. Implement minimal prerequisite
|
||||
fixes before live Kraken tests: inspect current code and fix
|
||||
start_qtractor merge precedence so explicit caller registry/runtime
|
||||
kwargs beat global defaults; fix PyQt6 blocking run_qtractor exec_ ->
|
||||
exec; add focused offline headless lifecycle regressions proving
|
||||
explicit registry selection despite stale globals and CLI adapter as
|
||||
feasible. Load py-codestyle, piker-conc-expert, run-tests via tools;
|
||||
read deployed test harness. Preserve existing comments and user-owned
|
||||
state. Use apply_patch. Existing staged baseline is committed at
|
||||
1a2d4893, .gitignore/.ai workflow state unrelated leave untouched. User
|
||||
prefers typed opaque function returns, no redundant obvious
|
||||
constructor types, vertical boolean operators, logging exceptions.
|
||||
Run only targeted headless tests with existing py313 interpreter and
|
||||
Nix wrapper (already user-authorized in this session); do not run full
|
||||
suite or public network charts. Do not expand to paper mode or
|
||||
readiness yet. Return changes/tests, exact remaining blockers, and any
|
||||
provenance modifications. Persist prompt-io for code per skill with
|
||||
correct active model identity if known, not invented old model. Keep
|
||||
minimal.
|
||||
|
||||
## Response Summary
|
||||
|
||||
Fixed explicit caller kwarg precedence and the PyQt6 blocking API.
|
||||
Extended real runtime lifecycle coverage and added subprocess-isolated
|
||||
CLI adapter success/error regressions. Focused tests passed 3/3;
|
||||
combined UI isolation and lifecycle tests passed 5/5, with only two
|
||||
existing Tractor deprecation warnings per run.
|
||||
|
||||
## Files Changed
|
||||
|
||||
- `piker/ui/_exec.py`: merge order and `QApplication.exec()`.
|
||||
- `tests/ui/test_qtractor.py`: stale-global and blocking regressions.
|
||||
- This log and its raw companion: prompt and generated-output record.
|
||||
|
||||
Existing provenance entries and the service README are unchanged.
|
||||
Unrelated `.gitignore` and `.ai` state are unchanged. No staging,
|
||||
commits, branch creation, or ai.skillz changes were performed.
|
||||
The active harness did not expose a session UUID; it is not invented.
|
||||
|
||||
## Human Edits
|
||||
|
||||
The human identified both prerequisite defects, selected this existing
|
||||
worktree, restricted implementation to minimal offline coverage, and
|
||||
specified code style, test environment, provenance identity, and
|
||||
untouched state. These are human-directed agent edits. No direct
|
||||
manual edits during this turn were observed; acceptance remains with
|
||||
the human.
|
||||
|
||||
## Continuation Prompt: 2026-09-08T21:56:56Z
|
||||
|
||||
Implement next minimal safety prerequisite in
|
||||
/home/goodboy/repos/piker/wkts/live_chart_qtractor. Same session user
|
||||
authorized continuation. Previous agent modified piker/ui/_exec.py,
|
||||
tests/ui/test_qtractor.py and added
|
||||
ai/prompt-io/opencode/20260908T215045Z_1a2d4893_prompt_io.md/.raw.md for
|
||||
registry precedence and Qt exec fixes. Preserve all those and unrelated
|
||||
.gitignore/.ai files. No staging, commits, branch/worktree operations
|
||||
or ai.skillz edits. Task: inspect production chart startup
|
||||
_app._async_main -> GodWidget/load_symbols/display -> open_order_mode
|
||||
-> open_ems. Thread a minimal explicit execution mode option so a chart
|
||||
integration caller can require paper EMS BEFORE any brokerd spawn.
|
||||
Preserve shipped default live CLI behavior (concrete existing callers).
|
||||
Prefer one clear existing convention/type and defaults, no
|
||||
compatibility scaffolding. Also inspect readiness provider search
|
||||
start_soon: implement minimal nursery.start/task_status handshake if
|
||||
appropriate to ensure application readiness follows provider search
|
||||
registration, without arbitrary waits. Load piker-clearing-expert,
|
||||
piker-conc-expert, py-codestyle, run-tests skills and harness. Tests
|
||||
must be real structured runtime tests or narrow direct unit tests as
|
||||
existing conventions permit; do not add interlayer mocking without
|
||||
asking. Do not run live providers, GUI compositor or full suite. Run
|
||||
relevant headless tests using existing py313 and already-authorized
|
||||
nix develop. If prerequisite changes grow too broad or unsafe stop
|
||||
and report exact tradeoff. Update provenance for your changes
|
||||
appropriately (same logical prerequisite patch may extend newest entry
|
||||
without erasing original generation). Return code paths, exact test
|
||||
evidence, risks and next live chart test requirements.
|
||||
|
||||
## Continuation Summary
|
||||
|
||||
Added explicit session-wide EMS mode plumbing across `_async_main`,
|
||||
`GodWidget.load_symbols`, `display_symbol_data`, `open_order_mode`,
|
||||
and `open_ems`. The existing string convention and live defaults are
|
||||
preserved. Invalid startup modes fail before widget access. Added the
|
||||
provider-search Trio startup handshake before application readiness.
|
||||
|
||||
New direct unit coverage is in `tests/ui/test_chart_startup.py`.
|
||||
Combined with existing Qtractor and UI-isolation tests, 9 tests passed
|
||||
in 2.67s; the separate display/gap-overlay run passed 9 in 2.00s.
|
||||
Warnings were limited to existing Tractor deprecations. Full forwarding
|
||||
and provider registration ordering remain live-integration coverage
|
||||
requirements, not claims of these narrow tests.
|
||||
|
||||
This continuation changed four UI production files, added the startup
|
||||
test file, and appended to this provenance pair. Original generation,
|
||||
`_exec.py`, `test_qtractor.py`, `.gitignore`, `.ai`, and ai.skillz were
|
||||
left untouched. No staging or Git history/worktree operations occurred.
|
||||
|
||||
## Continuation Human Contributions
|
||||
|
||||
The human selected paper execution and search readiness as the next
|
||||
safety prerequisite, required shipped live defaults, prohibited
|
||||
interlayer mocking without approval, limited verification to headless
|
||||
offline tests, and authorized appending this existing provenance pair.
|
||||
These requirements directed the generated edits. No direct manual
|
||||
changes during this continuation were observed; acceptance remains
|
||||
with the human.
|
||||
|
||||
## Live Qualification Prompt: 2026-09-08
|
||||
|
||||
The human explicitly authorizes public Kraken market data for
|
||||
xbtusd.spot.kraken, headless integration, isolated registry/config,
|
||||
and explicit paper execution with no credentials or real orders.
|
||||
Implement the minimal production live chart test in this same owned
|
||||
worktree, preserving the uncommitted prerequisites and unrelated
|
||||
.gitignore/.ai state. No takeover, staging/commit, or ai.skillz edits.
|
||||
Use the authoritative live-backend roadmap, required concurrency,
|
||||
clearing, Python style and test-harness skills, and existing py313/Nix
|
||||
wrapper. Add explicit --live-provider=kraken opt-in; preserve original
|
||||
selected-run errors. Use production GodWidget and _async_main via
|
||||
start_qtractor, owned empty broker config, unique reg_addr, inherited
|
||||
piker_test_dir and isolated settings. Record paper/no-brokerd and
|
||||
exact owned resources where possible. Always cancel in finally and
|
||||
pump Qt with a bound before widget destruction. No new interlayer
|
||||
mocks, symbol switching, blanket retries, broad cleanup, or expanded
|
||||
test excludes. Stop if production blockers require broader repairs.
|
||||
Append accurate gpt-6-astra provenance and human network authorization
|
||||
to the newest pair, preserving earlier generation.
|
||||
|
||||
## Live Qualification Summary
|
||||
|
||||
Active model remains gpt-6-astra (`openai/gpt-6-astra`). Added the
|
||||
explicit live-provider option and production Kraken smoke test.
|
||||
Corrected one demonstrated Kraken public-client constructor keyword
|
||||
from `conf` to `config`. No other production code was changed during
|
||||
this continuation. Exact new patch scope is:
|
||||
|
||||
- `tests/conftest.py`: six-line opt-in option.
|
||||
- `tests/ui/test_live_chart.py`: new production integration test.
|
||||
- `piker/brokers/kraken/api.py`: one keyword correction.
|
||||
- This existing Prompt-IO pair: append-only continuation records.
|
||||
|
||||
Final offline targeted checks: 9 passed, 1 skipped in 2.65s. The live
|
||||
run failed before the keyword fix (1 failed, 1 teardown error in
|
||||
2.66s), then progressed to public history and paper EMS but failed
|
||||
again (1 failed, 1 teardown error in 12.44s). Readiness preceded
|
||||
rt_linked.mode assignment; Qt reported abstract paint errors and
|
||||
widget teardown survivors. No successful live qualification is claimed.
|
||||
Commands, original signatures, proposed minimal readiness repair,
|
||||
and evidence limitations are retained in the raw companion.
|
||||
|
||||
Guest completion preceded widget destruction, but Tractor escalated
|
||||
its own child teardown to SIGKILL and a paperboi.kraken orphan remained:
|
||||
PID 752893, UID e8317c39-ebbb-4526-85f0-3440a2e6e39c, PPID 1.
|
||||
No agent-issued signal or SHM cleanup was performed. Further live
|
||||
runs and production repairs were stopped. The final test improvements
|
||||
to failure-path SHM/endpoint accounting were checked offline only.
|
||||
|
||||
The human directed all safety boundaries, the public network opt-in,
|
||||
the minimal scope and stop condition, and continuation of this pair.
|
||||
These are human-directed agent edits; acceptance remains with the
|
||||
human. Earlier provenance, prerequisite work, task-state markers,
|
||||
.gitignore, .ai, and ai.skillz are unchanged. No staging or commits.
|
||||
|
|
@ -1,226 +0,0 @@
|
|||
---
|
||||
model: openai/gpt-6-astra
|
||||
service: opencode
|
||||
timestamp: 2026-09-08T21:50:45Z
|
||||
git_ref: 1a2d4893
|
||||
diff_cmd: git diff HEAD
|
||||
---
|
||||
|
||||
## Generated Changes
|
||||
|
||||
> `git diff HEAD -- piker/ui/_exec.py`
|
||||
|
||||
`start_qtractor()` copies global runtime defaults first and explicit
|
||||
caller kwargs last. `run_qtractor()` uses PyQt6's `exec()` API.
|
||||
Existing comments and exception logging are preserved.
|
||||
|
||||
> `git diff HEAD -- tests/ui/test_qtractor.py`
|
||||
|
||||
The existing repeated-session regression now seeds stale global
|
||||
registry and runtime overrides before each startup. Its real guest
|
||||
asserts the requested address is bound and the caller's runtime
|
||||
overrides are active before publishing readiness.
|
||||
|
||||
A parametrized child-process regression exercises the real blocking
|
||||
adapter with normal guest completion and a sentinel exception. It
|
||||
checks exception identity, window cleanup, and actor teardown. Each
|
||||
child uses offscreen Qt and test-owned config and registry addresses.
|
||||
No public provider, paper mode, or chart readiness work is included.
|
||||
|
||||
## Verification
|
||||
|
||||
The existing worktree-local interpreter resolves Piker from this
|
||||
worktree: Python 3.13.14, pytest-qt 4.5.0. The authorized Nix wrapper
|
||||
ran its normal uv check; no environment provisioning was requested.
|
||||
|
||||
```xsh
|
||||
nix develop --command timeout -k 5 300 py313/bin/python -m pytest --headless -q -x --tb=short --no-header tests/ui/test_qtractor.py
|
||||
nix develop --command timeout -k 5 300 py313/bin/python -m pytest --headless -q -x --tb=short --no-header tests/ui/test_harness.py tests/ui/test_qtractor.py
|
||||
```
|
||||
|
||||
The focused run passed 3 tests in 2.44s. The combined targeted run
|
||||
passed 5 tests in 2.36s. Each reported two existing Tractor
|
||||
`Registrar.uid` deprecation warnings. `git diff --check` passed.
|
||||
|
||||
Live Kraken charts, the full suite, paper mode, and readiness changes
|
||||
were not run or implemented. The deployed harness still contains a
|
||||
stale SIGINT warning for `MainWindow`; current source uses structured
|
||||
shutdown. The reference was left untouched.
|
||||
|
||||
## Continuation: 2026-09-08T21:56:56Z
|
||||
|
||||
Model: openai/gpt-6-astra. The original generation above is preserved.
|
||||
|
||||
> `git diff HEAD -- piker/ui/_app.py`
|
||||
|
||||
`_async_main()` accepts `exec_mode: str = 'live'`, rejects values other
|
||||
than 'live' and 'paper' before touching the widget, and stores the
|
||||
selection on `GodWidget`. `load_provider_search()` publishes Trio task
|
||||
startup after entering provider search registration and symcache.
|
||||
The application awaits `nursery.start()` before publishing readiness.
|
||||
|
||||
> `git diff HEAD -- piker/ui/_widget.py`
|
||||
> `git diff HEAD -- piker/ui/_display.py`
|
||||
> `git diff HEAD -- piker/ui/order_mode.py`
|
||||
|
||||
`GodWidget.load_symbols()` forwards its session execution mode to
|
||||
`display_symbol_data()`, which passes it to `open_order_mode()` and
|
||||
then `open_ems(mode=exec_mode)`. Defaults remain live, matching the
|
||||
existing CLI call. Keeping selection on the widget also covers later
|
||||
search-driven symbol loads. No EMS, broker, or CLI implementation was
|
||||
changed. Existing EMS code checks paper before acquiring brokerd;
|
||||
paper relay cache keys are already distinct from live keys.
|
||||
|
||||
> `git diff HEAD -- tests/ui/test_chart_startup.py`
|
||||
|
||||
Added narrow direct tests for invalid-mode rejection before widget
|
||||
access, existing CLI argument binding, explicit paper binding, and
|
||||
entrypoint defaults. There are no interlayer mocks. These assertions
|
||||
do not prove full chart forwarding or provider readiness at runtime.
|
||||
|
||||
### Verification
|
||||
|
||||
The authorized Nix wrapper and existing py313 interpreter resolve
|
||||
Piker inside this worktree, with Python 3.13.14 and pytest-qt 4.5.0.
|
||||
VIRTUAL_ENV is the worktree's py313 directory. Nix ran its normal uv
|
||||
check; no separate environment provisioning was performed.
|
||||
|
||||
```xsh
|
||||
nix develop --command timeout -k 5 300 py313/bin/python -m pytest --headless -q -x --tb=short --no-header tests/ui/test_chart_startup.py tests/ui/test_harness.py tests/ui/test_qtractor.py
|
||||
nix develop --command timeout -k 5 300 py313/bin/python -m pytest --headless -q -x --tb=short --no-header tests/test_gap_overlays.py
|
||||
git diff --check
|
||||
```
|
||||
|
||||
First run: 9 passed in 2.67s, two existing `Registrar.uid` deprecation
|
||||
warnings. Second run: 9 passed in 2.00s, one upstream `trio.Event`
|
||||
boolean-use deprecation warning. Diff whitespace check passed.
|
||||
|
||||
No live provider, compositor, or full-suite execution was performed.
|
||||
Full chart readiness and zero-brokerd behavior need a separately
|
||||
authorized live-feed test using explicit paper mode, isolated registry
|
||||
and config, real search registration assertions, symbol switching,
|
||||
and structured close/restart checks. Paper mode still accesses market
|
||||
data and may write paper accounting state. It is not a process-wide
|
||||
ban on other clients' live execution.
|
||||
|
||||
## Live Qualification Continuation: 2026-09-08
|
||||
|
||||
Active model: gpt-6-astra (`openai/gpt-6-astra`). Earlier content
|
||||
above is historical and is preserved. The human now explicitly
|
||||
authorizes public Kraken market data, headless Qt, and paper EMS in
|
||||
this same worktree. No credentials, real orders, ownership takeover,
|
||||
staging, commits, ai.skillz edits, or unrelated state changes are
|
||||
authorized. The human requires an opt-in production chart test,
|
||||
isolated registry/config, bounded guest teardown before widget
|
||||
destruction, original errors, and stopping rather than broad repairs.
|
||||
|
||||
> `git diff HEAD -- tests/conftest.py`
|
||||
> `git diff --no-index -- /dev/null tests/ui/test_live_chart.py`
|
||||
> `git diff HEAD -- piker/brokers/kraken/api.py`
|
||||
|
||||
Added `--live-provider=kraken` and a production GodWidget/_async_main
|
||||
journey through start_qtractor. The test supplies explicit paper mode,
|
||||
the Tractor fixture's unique registry, owned root config paths, an
|
||||
empty brokers.toml, and inherited piker_test_dir. Existing process XDG
|
||||
isolation protects Qt settings and is inherited by children. There
|
||||
are no interlayer mocks, submitted orders, or symbol interactions.
|
||||
|
||||
Qt polling races application readiness against guest completion.
|
||||
Finally requests shutdown and pumps Qt with a 60-second bound before
|
||||
normal pytest-qt widget destruction. Guest errors are unwrapped;
|
||||
multiple body/teardown failures are grouped. Exact registry identities
|
||||
and chart SHM names are recorded as pytest properties. Detached known
|
||||
startup widgets are registered with QtBot after guest completion.
|
||||
The test checks paper actors/no observed brokerd, title, markets,
|
||||
nonempty history/realtime arrays, and a subsequent close-price change.
|
||||
|
||||
The first selected run reproduced a public-client constructor typo:
|
||||
`get_client()` passed `conf={}` to `Client(config, httpx_client, ...)`.
|
||||
Changed only that keyword to `config={}`. The next selected run got
|
||||
past this defect and consumed public Kraken history with paper EMS.
|
||||
|
||||
### Exact Commands And Results
|
||||
|
||||
The existing authorized Nix wrapper resolves Python 3.13.14, Piker in
|
||||
this worktree, pytest-qt 4.5.0, and VIRTUAL_ENV pointing at py313.
|
||||
Its normal uv check ran; no separate provisioning was requested.
|
||||
|
||||
```xsh
|
||||
nix develop --command timeout -k 5 300 py313/bin/python -m pytest --headless -q -x --tb=short --no-header tests/ui/test_live_chart.py tests/ui/test_chart_startup.py tests/ui/test_qtractor.py tests/ui/test_harness.py
|
||||
nix develop --command timeout -k 5 300 py313/bin/python -m pytest --headless --live-provider=kraken -q -x --tb=short --no-header tests/ui/test_live_chart.py
|
||||
git diff --check
|
||||
```
|
||||
|
||||
Offline command: initially 9 passed, 1 skipped in 2.66s; final rerun
|
||||
9 passed, 1 skipped in 2.65s, with two Tractor deprecation warnings.
|
||||
Diff whitespace check passed.
|
||||
|
||||
Live command was run twice with a targeted fix between runs, not as
|
||||
a blanket provider retry. First: 1 failed, 1 teardown error, 4 warnings
|
||||
in 2.66s. Original guest error was retained:
|
||||
|
||||
```text
|
||||
piker/brokers/kraken/api.py:764 in get_client
|
||||
Client(conf={}, httpx_client=trio_client)
|
||||
TypeError: Client.__init__() got an unexpected keyword argument
|
||||
'conf'. Did you mean 'config'?
|
||||
```
|
||||
|
||||
Second: 1 failed, 1 teardown error, 13 warnings in 12.44s.
|
||||
Readiness and observed paper actor/config checks passed, then:
|
||||
|
||||
```text
|
||||
assert set(godw.rt_linked.mode.trackers) == {'paper'}
|
||||
AttributeError: 'LinkedSplits' object has no attribute 'mode'.
|
||||
Did you mean: 'move'?
|
||||
```
|
||||
|
||||
`open_order_mode()` sets its started event at order_mode.py:1024,
|
||||
before `_display.py:1693` assigns rt_linked.mode after entering chart
|
||||
input handlers. `_async_main()` waits for the earlier event before
|
||||
publishing session readiness. Minimal proposed follow-up: publish a
|
||||
display-complete readiness event after these scopes and assignment,
|
||||
rather than weakening this assertion or adding a timing sleep.
|
||||
|
||||
Qt also reported twice, without a Python item-identifying traceback:
|
||||
|
||||
```text
|
||||
NotImplementedError: QGraphicsObject.paint() is abstract and must
|
||||
be overridden
|
||||
```
|
||||
|
||||
The UI isolation fixture reported remaining QFrame, QLabel, QMenu,
|
||||
and ViewBoxMenu objects. Rendering and widget-ownership fixes were
|
||||
not attempted. Identify the offending graphics item and give detached
|
||||
chart resources explicit ownership in a separate bounded follow-up.
|
||||
|
||||
Guest completion was observed before widgets were destroyed, but it
|
||||
did NOT prove clean actor teardown. Tractor logs show its own forced
|
||||
SIGKILL escalation for datad, samplerd, emsd and both FSP actors.
|
||||
A non-mutating `ps` check found the exact paper child still alive:
|
||||
|
||||
```text
|
||||
PID 752893, PPID 1, paperboi.kraken
|
||||
UID e8317c39-ebbb-4526-85f0-3440a2e6e39c
|
||||
parent_addr ('127.0.0.1', 34915)
|
||||
```
|
||||
|
||||
Checked known PIDs 738066, 738721, 749265, 749778, 750704, 751382,
|
||||
751383, 752409 and 752893; only 752893 remained. No agent-issued
|
||||
signals, manual SHM unlinking, broad cleanup, or subsequent live runs
|
||||
were performed. The runtime's descendant teardown cause is not yet
|
||||
established; it must be diagnosed before claiming clean shutdown.
|
||||
|
||||
After that failure, the test was tightened to collect chart SHM during
|
||||
startup polling and check exact SHM/endpoints even after a body error.
|
||||
The final version was checked offline only, since the paper survivor
|
||||
remains. The reached run did not execute market/title/array/live-price
|
||||
assertions or the original success-only SHM/endpoint checks. Those
|
||||
are implemented expectations, not passing qualification evidence.
|
||||
|
||||
Further gaps: polling can miss transient actors; chart tokens omit
|
||||
unpublished and non-chart child SHM; closed endpoints do not prove
|
||||
PID exit; hard guest-timeout safety before widget destruction is not
|
||||
qualified. No full suite, compositor, symbol switching, order journey,
|
||||
or test-exclude changes were made. Existing prerequisite changes and
|
||||
unrelated .gitignore/.ai state were preserved.
|
||||
|
|
@ -762,7 +762,7 @@ async def get_client() -> Client:
|
|||
)
|
||||
else:
|
||||
client = Client(
|
||||
config={},
|
||||
conf={},
|
||||
httpx_client=trio_client,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -38,26 +38,17 @@ from ..log import (
|
|||
get_console_log,
|
||||
)
|
||||
from ..service import maybe_spawn_datad
|
||||
from ._exec import (
|
||||
QtractorSession,
|
||||
run_qtractor,
|
||||
)
|
||||
from ._exec import run_qtractor
|
||||
|
||||
log = get_logger(__name__)
|
||||
|
||||
|
||||
async def load_provider_search(
|
||||
brokermod: ModuleType,
|
||||
brokermod: str,
|
||||
loglevel: str,
|
||||
|
||||
*,
|
||||
task_status: trio.TaskStatus[None] = trio.TASK_STATUS_IGNORED,
|
||||
|
||||
) -> None:
|
||||
'''
|
||||
Publish startup after provider search and symcache are installed.
|
||||
|
||||
'''
|
||||
name = brokermod.name
|
||||
log.info(f'loading datad for {name}..')
|
||||
|
||||
|
|
@ -79,8 +70,6 @@ async def load_provider_search(
|
|||
f'BACKEND DOES NOT (yet) support symcaching: `{brokermod.name}`'
|
||||
)
|
||||
|
||||
task_status.started()
|
||||
|
||||
# keep search engine stream up until cancelled
|
||||
await trio.sleep_forever()
|
||||
|
||||
|
|
@ -93,23 +82,14 @@ async def _async_main(
|
|||
syms: list[str],
|
||||
brokers: dict[str, ModuleType],
|
||||
loglevel: str,
|
||||
exec_mode: str = 'live',
|
||||
|
||||
) -> None:
|
||||
'''
|
||||
"""
|
||||
Main Qt-trio routine invoked by the Qt loop with the widgets ``dict``.
|
||||
|
||||
Provision the "main" widget with initial symbol data and root nursery.
|
||||
|
||||
`exec_mode` selects 'live' or 'paper' EMS for this chart session,
|
||||
including subsequent symbol loads through `GodWidget`.
|
||||
|
||||
'''
|
||||
# Reject typos before chart tasks can request a live EMS dialog.
|
||||
if exec_mode not in ('live', 'paper'):
|
||||
raise ValueError(f'Invalid EMS execution mode: {exec_mode!r}')
|
||||
main_widget.exec_mode = exec_mode
|
||||
|
||||
"""
|
||||
# enable chart's console logging
|
||||
if loglevel:
|
||||
get_console_log(
|
||||
|
|
@ -185,7 +165,7 @@ async def _async_main(
|
|||
# load other providers into search **after**
|
||||
# the chart's select cache
|
||||
for brokername, mod in needed_brokermods.items():
|
||||
await tn.start(
|
||||
tn.start_soon(
|
||||
load_provider_search,
|
||||
mod,
|
||||
loglevel,
|
||||
|
|
@ -214,14 +194,6 @@ async def _async_main(
|
|||
):
|
||||
# remove startup status text
|
||||
starting_done()
|
||||
qtractor_session: QtractorSession|None = (
|
||||
godwidget.window.qtractor_session
|
||||
)
|
||||
if qtractor_session is None:
|
||||
raise RuntimeError(
|
||||
'No Qtractor session owns the main window'
|
||||
)
|
||||
qtractor_session.set_ready()
|
||||
await trio.sleep_forever()
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1298,7 +1298,6 @@ async def display_symbol_data(
|
|||
fqmes: list[str],
|
||||
loglevel: str,
|
||||
order_mode_started: trio.Event,
|
||||
exec_mode: str = 'live',
|
||||
|
||||
) -> None:
|
||||
'''
|
||||
|
|
@ -1670,8 +1669,7 @@ async def display_symbol_data(
|
|||
godwidget,
|
||||
order_ctl_fqme,
|
||||
order_mode_started,
|
||||
loglevel=loglevel,
|
||||
exec_mode=exec_mode,
|
||||
loglevel=loglevel
|
||||
) as mode,
|
||||
|
||||
# TODO: maybe have these startup sooner before
|
||||
|
|
|
|||
|
|
@ -22,22 +22,24 @@ All global Qt runtime settings are mostly defined here.
|
|||
"""
|
||||
from __future__ import annotations
|
||||
from typing import (
|
||||
Any,
|
||||
Awaitable,
|
||||
Callable,
|
||||
Any,
|
||||
Type,
|
||||
TYPE_CHECKING,
|
||||
)
|
||||
import platform
|
||||
import traceback
|
||||
|
||||
import qdarkstyle
|
||||
from qdarkstyle import DarkPalette
|
||||
# import qdarkgraystyle # TODO: play with it
|
||||
import trio
|
||||
from outcome import Outcome
|
||||
from outcome import Error
|
||||
|
||||
# Qt version-agnostic
|
||||
from .qt import (
|
||||
QWidget,
|
||||
QMainWindow,
|
||||
QApplication,
|
||||
QtCore,
|
||||
pyqtRemoveInputHook,
|
||||
|
|
@ -54,7 +56,6 @@ from . import _style
|
|||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ._window import MainWindow
|
||||
from ._widget import GodWidget
|
||||
|
||||
|
||||
|
|
@ -69,7 +70,7 @@ _do_overrides()
|
|||
# XXX: pretty sure none of this shit works on linux as per:
|
||||
# https://bugreports.qt.io/browse/QTBUG-53022
|
||||
# it seems to work on windows.. no idea wtf is up.
|
||||
is_windows: bool = False
|
||||
is_windows = False
|
||||
if platform.system() == "Windows":
|
||||
is_windows = True
|
||||
|
||||
|
|
@ -82,131 +83,24 @@ if platform.system() == "Windows":
|
|||
QCoreApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True)
|
||||
|
||||
|
||||
class QtractorSession(QtCore.QObject):
|
||||
def run_qtractor(
|
||||
func: Callable,
|
||||
args: tuple,
|
||||
main_widget_type: Type[QWidget],
|
||||
tractor_kwargs: dict[str, Any] = {},
|
||||
window_type: QMainWindow = None,
|
||||
|
||||
) -> None:
|
||||
'''
|
||||
Own one Trio guest run embedded in a Qt application.
|
||||
|
||||
'''
|
||||
application_ready = QtCore.pyqtSignal()
|
||||
completed = QtCore.pyqtSignal(object)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
app: QApplication,
|
||||
window: MainWindow,
|
||||
main_widget: QWidget,
|
||||
owns_app_exec: bool,
|
||||
|
||||
) -> None:
|
||||
'''
|
||||
Initialize state owned by one Qt-hosted Trio guest run.
|
||||
|
||||
'''
|
||||
super().__init__()
|
||||
self.app = app
|
||||
self.window = window
|
||||
self.main_widget = main_widget
|
||||
self.owns_app_exec = owns_app_exec
|
||||
self.trio_token: trio.lowlevel.TrioToken|None = None
|
||||
self.cancel_scope: trio.CancelScope|None = None
|
||||
self.outcome: Outcome[Any]|None = None
|
||||
self.is_ready: bool = False
|
||||
self._shutdown_requested: bool = False
|
||||
self._shutdown_scheduled: bool = False
|
||||
|
||||
def set_ready(self) -> None:
|
||||
'''
|
||||
Publish that the application entered all startup scopes.
|
||||
|
||||
'''
|
||||
if self.is_ready:
|
||||
return
|
||||
|
||||
self.is_ready = True
|
||||
self.application_ready.emit()
|
||||
|
||||
def request_shutdown(self) -> None:
|
||||
'''
|
||||
Cancel the owned Trio guest run through its host token.
|
||||
|
||||
'''
|
||||
if self.outcome is not None:
|
||||
return
|
||||
|
||||
self._shutdown_requested = True
|
||||
token: trio.lowlevel.TrioToken|None = self.trio_token
|
||||
cancel_scope: trio.CancelScope|None = self.cancel_scope
|
||||
if (
|
||||
token is None
|
||||
or
|
||||
cancel_scope is None
|
||||
or
|
||||
self._shutdown_scheduled
|
||||
):
|
||||
return
|
||||
|
||||
self._shutdown_scheduled = True
|
||||
try:
|
||||
token.run_sync_soon(cancel_scope.cancel)
|
||||
except trio.RunFinishedError:
|
||||
log.debug(
|
||||
'Trio guest run already finished before shutdown'
|
||||
)
|
||||
|
||||
def _guest_started(
|
||||
self,
|
||||
token: trio.lowlevel.TrioToken,
|
||||
cancel_scope: trio.CancelScope,
|
||||
|
||||
) -> None:
|
||||
self.trio_token = token
|
||||
self.cancel_scope = cancel_scope
|
||||
if self._shutdown_requested:
|
||||
cancel_scope.cancel()
|
||||
|
||||
def _guest_completed(
|
||||
self,
|
||||
final_outcome: Outcome[Any],
|
||||
|
||||
) -> None:
|
||||
self.outcome = final_outcome
|
||||
self.completed.emit(final_outcome)
|
||||
|
||||
|
||||
def start_qtractor(
|
||||
func: Callable[..., Awaitable[Any]],
|
||||
args: tuple[Any, ...],
|
||||
main_widget_type: type[QWidget],
|
||||
tractor_kwargs: dict[str, Any]|None = None,
|
||||
window_type: type[MainWindow]|None = None,
|
||||
app: QApplication|None = None,
|
||||
owns_app_exec: bool = False,
|
||||
|
||||
) -> QtractorSession:
|
||||
'''
|
||||
Start a Trio guest run without entering Qt's event loop.
|
||||
Run the Qt event loop and embed `trio` via guest mode on it.
|
||||
|
||||
'''
|
||||
# avoids annoying message when entering debugger from qt loop
|
||||
pyqtRemoveInputHook()
|
||||
|
||||
running_app: QApplication|None = QApplication.instance()
|
||||
app = QApplication.instance()
|
||||
if app is None:
|
||||
app = running_app or QApplication([])
|
||||
elif (
|
||||
running_app is not None
|
||||
and
|
||||
running_app is not app
|
||||
):
|
||||
raise RuntimeError(
|
||||
'The requested QApplication is not the active instance'
|
||||
)
|
||||
|
||||
prior_quit_on_last_window: bool = (
|
||||
app.quitOnLastWindowClosed()
|
||||
)
|
||||
prior_cursor_flash_time: int = app.cursorFlashTime()
|
||||
prior_stylesheet: str = app.styleSheet()
|
||||
app = QApplication([])
|
||||
|
||||
# TODO: we might not need this if it's desired
|
||||
# to cancel the tractor machinery on Qt loop
|
||||
|
|
@ -224,58 +118,39 @@ def start_qtractor(
|
|||
|
||||
# source gist and credit to njs:
|
||||
# https://gist.github.com/njsmith/d996e80b700a339e0623f97f48bcf0cb
|
||||
REENTER_EVENT: QtCore.QEvent.Type = QtCore.QEvent.Type(
|
||||
QtCore.QEvent.registerEventType()
|
||||
)
|
||||
REENTER_EVENT = QtCore.QEvent.Type(QtCore.QEvent.registerEventType())
|
||||
|
||||
class ReenterEvent(QtCore.QEvent):
|
||||
'''
|
||||
Carry one Trio guest callback through Qt's event queue.
|
||||
|
||||
'''
|
||||
def __init__(
|
||||
self,
|
||||
fn: Callable[[], None],
|
||||
|
||||
) -> None:
|
||||
'''
|
||||
Initialize the event with its scheduled callback.
|
||||
|
||||
'''
|
||||
super().__init__(REENTER_EVENT)
|
||||
self.fn: Callable[[], None] = fn
|
||||
pass
|
||||
|
||||
class Reenter(QtCore.QObject):
|
||||
'''
|
||||
Dispatch Trio re-entry events received from Qt.
|
||||
|
||||
'''
|
||||
def event(self, event: QtCore.QEvent) -> bool:
|
||||
'''
|
||||
Run a scheduled callback or defer another event to Qt.
|
||||
|
||||
'''
|
||||
if not isinstance(event, ReenterEvent):
|
||||
return super().event(event)
|
||||
|
||||
def event(self, event):
|
||||
event.fn()
|
||||
return False
|
||||
|
||||
reenter: Reenter = Reenter()
|
||||
reenter = Reenter()
|
||||
|
||||
def run_sync_soon_threadsafe(
|
||||
fn: Callable[[], None],
|
||||
|
||||
) -> None:
|
||||
'''
|
||||
Post one Trio guest callback onto Qt's event queue.
|
||||
|
||||
'''
|
||||
event: ReenterEvent = ReenterEvent(fn)
|
||||
def run_sync_soon_threadsafe(fn):
|
||||
event = ReenterEvent(REENTER_EVENT)
|
||||
event.fn = fn
|
||||
app.postEvent(reenter, event)
|
||||
|
||||
def done_callback(outcome):
|
||||
|
||||
if isinstance(outcome, Error):
|
||||
exc = outcome.error
|
||||
|
||||
if isinstance(outcome.error, KeyboardInterrupt):
|
||||
# make it kinda look like ``trio``
|
||||
print("Terminated!")
|
||||
|
||||
else:
|
||||
traceback.print_exception(type(exc), exc, exc.__traceback__)
|
||||
|
||||
app.quit()
|
||||
|
||||
# load dark theme
|
||||
stylesheet: str = qdarkstyle.load_stylesheet(
|
||||
stylesheet = qdarkstyle.load_stylesheet(
|
||||
qt_api='pyqt6',
|
||||
palette=DarkPalette,
|
||||
)
|
||||
|
|
@ -287,7 +162,7 @@ def start_qtractor(
|
|||
if window_type is None:
|
||||
window_type = _window.MainWindow
|
||||
|
||||
window: MainWindow = window_type()
|
||||
window = window_type()
|
||||
|
||||
# set global app's main window singleton
|
||||
_window._qt_win = window
|
||||
|
|
@ -302,66 +177,19 @@ def start_qtractor(
|
|||
instance: GodWidget = main_widget_type()
|
||||
instance.window = window
|
||||
|
||||
session = QtractorSession(
|
||||
app=app,
|
||||
window=window,
|
||||
main_widget=instance,
|
||||
owns_app_exec=owns_app_exec,
|
||||
)
|
||||
window.qtractor_session = session
|
||||
window.set_shutdown_callback(session.request_shutdown)
|
||||
|
||||
# override tractor's defaults
|
||||
tractor_kwargs = {
|
||||
**get_runtime_vars(),
|
||||
**(tractor_kwargs or {}),
|
||||
}
|
||||
tractor_kwargs.update(
|
||||
get_runtime_vars()
|
||||
)
|
||||
|
||||
# define tractor entrypoint
|
||||
async def main() -> None:
|
||||
'''
|
||||
Run the application under its owned Trio cancel scope.
|
||||
async def main():
|
||||
|
||||
'''
|
||||
cancel_scope: trio.CancelScope
|
||||
with trio.CancelScope() as cancel_scope:
|
||||
session._guest_started(
|
||||
trio.lowlevel.current_trio_token(),
|
||||
cancel_scope,
|
||||
)
|
||||
async with maybe_open_pikerd(
|
||||
**tractor_kwargs,
|
||||
):
|
||||
await func(*((instance,) + args))
|
||||
|
||||
def done_callback(final_outcome: Outcome[Any]) -> None:
|
||||
'''
|
||||
Restore Qt host state and publish guest completion.
|
||||
|
||||
'''
|
||||
try:
|
||||
app.focusChanged.disconnect(window.on_focus_change)
|
||||
except (
|
||||
TypeError,
|
||||
RuntimeError,
|
||||
):
|
||||
log.exception(
|
||||
'Failed to disconnect '
|
||||
'`MainWindow.on_focus_change()`'
|
||||
)
|
||||
window.uninstall_global_zoom_filter()
|
||||
window.set_shutdown_callback(None)
|
||||
window.qtractor_session = None
|
||||
if _window._qt_win is window:
|
||||
_window._qt_win = None
|
||||
if not owns_app_exec:
|
||||
app.setQuitOnLastWindowClosed(
|
||||
prior_quit_on_last_window
|
||||
)
|
||||
app.setCursorFlashTime(prior_cursor_flash_time)
|
||||
app.setStyleSheet(prior_stylesheet)
|
||||
session._guest_completed(final_outcome)
|
||||
|
||||
# guest mode entry
|
||||
trio.lowlevel.start_guest_run(
|
||||
main,
|
||||
|
|
@ -380,46 +208,4 @@ def start_qtractor(
|
|||
|
||||
# actually render to screen
|
||||
window.show()
|
||||
return session
|
||||
|
||||
|
||||
def run_qtractor(
|
||||
func: Callable[..., Awaitable[Any]],
|
||||
args: tuple[Any, ...],
|
||||
main_widget_type: type[QWidget],
|
||||
tractor_kwargs: dict[str, Any]|None = None,
|
||||
window_type: type[MainWindow]|None = None,
|
||||
|
||||
) -> None:
|
||||
'''
|
||||
Run Qt and unwrap the embedded Trio guest run's result.
|
||||
|
||||
'''
|
||||
session: QtractorSession = start_qtractor(
|
||||
func=func,
|
||||
args=args,
|
||||
main_widget_type=main_widget_type,
|
||||
tractor_kwargs=tractor_kwargs,
|
||||
window_type=window_type,
|
||||
owns_app_exec=True,
|
||||
)
|
||||
session.completed.connect(session.app.quit)
|
||||
try:
|
||||
session.app.exec()
|
||||
finally:
|
||||
try:
|
||||
session.completed.disconnect(session.app.quit)
|
||||
except (
|
||||
TypeError,
|
||||
RuntimeError,
|
||||
):
|
||||
log.exception(
|
||||
'Failed to disconnect `QApplication.quit()`'
|
||||
)
|
||||
|
||||
final_outcome: Outcome[Any]|None = session.outcome
|
||||
if final_outcome is None:
|
||||
raise RuntimeError(
|
||||
'Qt exited before the Trio guest run completed'
|
||||
)
|
||||
final_outcome.unwrap()
|
||||
app.exec_()
|
||||
|
|
|
|||
|
|
@ -80,8 +80,6 @@ class GodWidget(QWidget):
|
|||
'''
|
||||
search: SearchWidget
|
||||
mode_name: str = 'god'
|
||||
# `_async_main()` selects EMS execution before any symbol loads.
|
||||
exec_mode: str = 'live'
|
||||
|
||||
def __init__(
|
||||
|
||||
|
|
@ -209,7 +207,6 @@ class GodWidget(QWidget):
|
|||
fqmes,
|
||||
loglevel,
|
||||
order_mode_started,
|
||||
self.exec_mode,
|
||||
)
|
||||
|
||||
# self.vbox.addWidget(hist_charts)
|
||||
|
|
|
|||
|
|
@ -19,10 +19,11 @@ Qt main window singletons and stuff.
|
|||
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import os
|
||||
import signal
|
||||
import time
|
||||
from typing import (
|
||||
Callable,
|
||||
TYPE_CHECKING,
|
||||
Union,
|
||||
)
|
||||
import uuid
|
||||
|
|
@ -50,10 +51,6 @@ from ._style import (
|
|||
from ._widget import GodWidget
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ._exec import QtractorSession
|
||||
|
||||
|
||||
log = get_logger(__name__)
|
||||
|
||||
|
||||
|
|
@ -294,45 +291,15 @@ class MainWindow(QMainWindow):
|
|||
|
||||
# event filter for global zoom shortcuts
|
||||
self._zoom_filter: GlobalZoomEventFilter | None = None
|
||||
self._shutdown_callback: Callable[[], None]|None = None
|
||||
self.qtractor_session: QtractorSession|None = None
|
||||
|
||||
def install_global_zoom_filter(self) -> None:
|
||||
'''Install application-level event filter for global UI zoom shortcuts.'''
|
||||
if self._zoom_filter is None:
|
||||
self._zoom_filter = GlobalZoomEventFilter(self)
|
||||
app: QApplication|None = QApplication.instance()
|
||||
if app is None:
|
||||
raise RuntimeError(
|
||||
'Cannot install zoom filter without a QApplication'
|
||||
)
|
||||
app = QApplication.instance()
|
||||
app.installEventFilter(self._zoom_filter)
|
||||
log.info('Installed global zoom shortcuts: Ctrl+Shift+Plus/Minus/0')
|
||||
|
||||
def uninstall_global_zoom_filter(self) -> None:
|
||||
'''
|
||||
Remove this window's application-level zoom filter.
|
||||
|
||||
'''
|
||||
if self._zoom_filter is None:
|
||||
return
|
||||
|
||||
app: QApplication|None = QApplication.instance()
|
||||
if app is not None:
|
||||
app.removeEventFilter(self._zoom_filter)
|
||||
self._zoom_filter = None
|
||||
|
||||
def set_shutdown_callback(
|
||||
self,
|
||||
callback: Callable[[], None]|None,
|
||||
|
||||
) -> None:
|
||||
'''
|
||||
Set the structured runtime-shutdown request callback.
|
||||
|
||||
'''
|
||||
self._shutdown_callback = callback
|
||||
|
||||
@property
|
||||
def mode_label(self) -> QLabel:
|
||||
|
||||
|
|
@ -374,10 +341,11 @@ class MainWindow(QMainWindow):
|
|||
settings.setValue('windowGeometry', self.saveGeometry())
|
||||
log.debug('Saved window geometry for next session')
|
||||
|
||||
if self._shutdown_callback is not None:
|
||||
self._shutdown_callback()
|
||||
|
||||
super().closeEvent(event)
|
||||
# raising KBI seems to get intercepted by by Qt so just use the system.
|
||||
os.kill(
|
||||
os.getpid(),
|
||||
signal.SIGINT,
|
||||
)
|
||||
|
||||
@property
|
||||
def status_bar(self) -> QStatusBar:
|
||||
|
|
|
|||
|
|
@ -785,8 +785,7 @@ async def open_order_mode(
|
|||
godw: GodWidget,
|
||||
fqme: str,
|
||||
started: trio.Event,
|
||||
loglevel: str = 'info',
|
||||
exec_mode: str = 'live',
|
||||
loglevel: str = 'info'
|
||||
|
||||
) -> None:
|
||||
'''
|
||||
|
|
@ -817,7 +816,6 @@ async def open_order_mode(
|
|||
async with (
|
||||
open_ems(
|
||||
fqme,
|
||||
mode=exec_mode,
|
||||
loglevel=loglevel,
|
||||
) as (
|
||||
client,
|
||||
|
|
|
|||
|
|
@ -123,12 +123,6 @@ def qapp_args(
|
|||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
parser.addoption(
|
||||
'--live-provider',
|
||||
choices=('kraken',),
|
||||
default=None,
|
||||
help='Opt into public-provider chart qualification',
|
||||
)
|
||||
parser.addoption(
|
||||
'--headless',
|
||||
action='store_true',
|
||||
|
|
|
|||
|
|
@ -1,76 +0,0 @@
|
|||
from inspect import (
|
||||
BoundArguments,
|
||||
signature,
|
||||
)
|
||||
|
||||
import pytest
|
||||
import trio
|
||||
|
||||
from piker.ui._app import _async_main
|
||||
from piker.ui._display import display_symbol_data
|
||||
from piker.ui._widget import GodWidget
|
||||
from piker.ui.order_mode import open_order_mode
|
||||
|
||||
|
||||
@pytest.mark.parametrize('exec_mode', ['', 'Paper', 'dark'])
|
||||
def test_chart_rejects_invalid_ems_mode(exec_mode: str) -> None:
|
||||
'''
|
||||
Reject misspelled execution modes before touching chart startup.
|
||||
|
||||
EMS treats non-paper mode as live when a backend supports trades.
|
||||
A chart caller must not accidentally request that path with a
|
||||
typo or the order-trigger mode 'dark'. Invoke the actual startup
|
||||
coroutine with no widget or backend: ValueError, rather than a
|
||||
widget access or runtime error, proves rejection precedes chart
|
||||
initialization and any tasks that could acquire brokerd.
|
||||
|
||||
This is a direct input-boundary test, not an EMS integration test.
|
||||
|
||||
'''
|
||||
with pytest.raises(ValueError, match='Invalid EMS execution mode'):
|
||||
trio.run(
|
||||
_async_main,
|
||||
None,
|
||||
[],
|
||||
{},
|
||||
'warning',
|
||||
exec_mode,
|
||||
)
|
||||
|
||||
|
||||
def test_chart_ems_mode_call_contract() -> None:
|
||||
'''
|
||||
Preserve the shipped CLI argument shape while enabling paper EMS.
|
||||
|
||||
`_main()` passes only widget, symbols, brokers and loglevel to
|
||||
`_async_main()`. Bind that concrete call shape and apply defaults
|
||||
to verify it remains live; bind an explicit paper argument to
|
||||
verify integration callers need no CLI or runtime adapter changes.
|
||||
Check the widget and lower entrypoint defaults as well, since
|
||||
omitted arguments must not silently change shipped execution.
|
||||
|
||||
These are direct API-contract assertions. They do not claim to
|
||||
exercise forwarding through a running chart or broker selection.
|
||||
|
||||
'''
|
||||
cli_call: BoundArguments = signature(_async_main).bind(
|
||||
None,
|
||||
['xbtusd.kraken'],
|
||||
{},
|
||||
'warning',
|
||||
)
|
||||
cli_call.apply_defaults()
|
||||
assert cli_call.arguments['exec_mode'] == 'live'
|
||||
|
||||
paper_call: BoundArguments = signature(_async_main).bind(
|
||||
*cli_call.args[:-1],
|
||||
exec_mode='paper',
|
||||
)
|
||||
assert paper_call.arguments['exec_mode'] == 'paper'
|
||||
assert GodWidget.exec_mode == 'live'
|
||||
assert signature(display_symbol_data).parameters[
|
||||
'exec_mode'
|
||||
].default == 'live'
|
||||
assert signature(open_order_mode).parameters[
|
||||
'exec_mode'
|
||||
].default == 'live'
|
||||
|
|
@ -1,213 +0,0 @@
|
|||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
import socket
|
||||
from time import monotonic
|
||||
from types import SimpleNamespace
|
||||
|
||||
import numpy as np
|
||||
from PyQt6.QtWidgets import QApplication
|
||||
import pytest
|
||||
from pytestqt.qtbot import QtBot
|
||||
import tractor
|
||||
from tractor.ipc import _shm
|
||||
|
||||
from piker import config
|
||||
from piker.brokers import get_brokermod
|
||||
from piker.ui._app import _async_main
|
||||
from piker.ui._exec import start_qtractor
|
||||
from piker.ui._widget import GodWidget
|
||||
|
||||
|
||||
def test_kraken_chart_boots_and_closes(
|
||||
request: pytest.FixtureRequest,
|
||||
isolated_ui_state: SimpleNamespace,
|
||||
qapp: QApplication,
|
||||
qtbot: QtBot,
|
||||
reg_addr: tuple[str, int],
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
record_property: Callable[[str, object], None],
|
||||
|
||||
) -> None:
|
||||
'''
|
||||
Qualify production chart boot against public Kraken, not mocks.
|
||||
|
||||
A blocking Qt host and hidden Trio errors previously prevented
|
||||
testing the composed chart. Explicit selection starts GodWidget
|
||||
and `_async_main` with paper EMS, an empty owned broker config,
|
||||
and the test's unique registry. No orders or input are submitted.
|
||||
Readiness races guest completion so a provider/startup exception
|
||||
is unwrapped rather than replaced by a readiness timeout.
|
||||
|
||||
Nonempty historical/realtime visualizations and a later price
|
||||
change prove actual feed consumption. Registry snapshots check
|
||||
paperboi and absence of brokerd; exact chart SHM names and actor
|
||||
endpoints are retained for non-mutating teardown checks. These
|
||||
snapshots cannot rule out short-lived actors between polls or
|
||||
account for SHM never published to a chart before early failure.
|
||||
|
||||
Every exit requests cancellation and pumps Qt to guest completion
|
||||
before pytest-qt may destroy widgets. Cleanup and guest failures
|
||||
are retained together rather than hiding either original error.
|
||||
|
||||
'''
|
||||
if request.config.getoption('--live-provider') != 'kraken':
|
||||
pytest.skip('requires --live-provider=kraken')
|
||||
|
||||
fqme: str = 'xbtusd.spot.kraken'
|
||||
confdir: Path = isolated_ui_state.test_config_dir
|
||||
# A missing brokers.toml loads templates with example accounts.
|
||||
(confdir / 'brokers.toml').write_text('', encoding='utf-8')
|
||||
for name in ('_config_dir', '_click_config_dir'):
|
||||
monkeypatch.setattr(config, name, confdir)
|
||||
monkeypatch.setattr(
|
||||
config,
|
||||
'_watchlists_data_path',
|
||||
confdir / 'watchlists.json',
|
||||
)
|
||||
assert config.load('brokers')[0] == {}
|
||||
|
||||
owned_actors: dict[tuple[str, str], list] = {}
|
||||
shm_names: set[str] = set()
|
||||
errors: list[BaseException] = []
|
||||
started: float = monotonic()
|
||||
session = start_qtractor(
|
||||
func=_async_main,
|
||||
args=(
|
||||
[fqme],
|
||||
{'kraken': get_brokermod('kraken')},
|
||||
'warning',
|
||||
'paper',
|
||||
),
|
||||
main_widget_type=GodWidget,
|
||||
tractor_kwargs={
|
||||
'registry_addrs': [reg_addr],
|
||||
'tractor_runtime_overrides': {
|
||||
'piker_test_dir': str(confdir),
|
||||
},
|
||||
'loglevel': 'warning',
|
||||
},
|
||||
app=qapp,
|
||||
)
|
||||
|
||||
def observe_runtime() -> None:
|
||||
'''
|
||||
Retain identities from this guest's local Registrar only.
|
||||
|
||||
'''
|
||||
actor = tractor.current_actor(err_on_no_runtime=False)
|
||||
if actor is not None:
|
||||
assert actor.is_registrar
|
||||
assert reg_addr in actor.accept_addrs
|
||||
owned_actors.update(actor._registry)
|
||||
assert not any(
|
||||
uid[0].startswith('brokerd.')
|
||||
for uid in owned_actors
|
||||
)
|
||||
for attr in ('hist_linked', 'rt_linked'):
|
||||
linked = getattr(session.main_widget, attr, None)
|
||||
chart = getattr(linked, 'chart', None)
|
||||
viz = getattr(chart, 'main_viz', None)
|
||||
if viz is not None:
|
||||
token: dict = viz.shm.token
|
||||
shm_names.update(
|
||||
token[key] for key in (
|
||||
'shm_name',
|
||||
'shm_first_index_name',
|
||||
'shm_last_index_name',
|
||||
)
|
||||
)
|
||||
|
||||
def boot_finished() -> bool:
|
||||
observe_runtime()
|
||||
return session.is_ready or session.outcome is not None
|
||||
|
||||
try:
|
||||
qtbot.addWidget(session.window)
|
||||
qtbot.waitUntil(boot_finished, timeout=120_000)
|
||||
record_property('boot_seconds', monotonic() - started)
|
||||
if session.outcome is None:
|
||||
godw = session.main_widget
|
||||
assert session.is_ready
|
||||
assert godw.exec_mode == 'paper'
|
||||
assert config.get_conf_dir() == confdir
|
||||
assert tractor.runtime._state._runtime_vars[
|
||||
'piker_vars'
|
||||
]['piker_test_dir'] == str(confdir)
|
||||
names: set[str] = {uid[0] for uid in owned_actors}
|
||||
assert {
|
||||
'pikerd', 'datad.kraken', 'samplerd',
|
||||
'emsd', 'paperboi.kraken',
|
||||
} <= names
|
||||
assert set(godw.rt_linked.mode.trackers) == {'paper'}
|
||||
assert not godw.rt_linked.mode.client._sent_orders
|
||||
assert session.window.windowTitle().startswith(fqme)
|
||||
for linked in (godw.hist_linked, godw.rt_linked):
|
||||
assert linked.mkt.fqme == fqme
|
||||
viz = linked.chart.main_viz
|
||||
data = viz.shm.array
|
||||
assert len(data) > 1
|
||||
assert np.isfinite(data['close']).all()
|
||||
assert (data['close'] > 0).any()
|
||||
|
||||
rt_shm = godw.rt_linked.chart.main_viz.shm
|
||||
before = rt_shm.array[-1].copy()
|
||||
|
||||
def live_update_or_exit() -> bool:
|
||||
observe_runtime()
|
||||
if session.outcome is not None:
|
||||
return True
|
||||
# A sampler clock tick alone must not satisfy this.
|
||||
return bool(
|
||||
rt_shm.array[-1]['close'] != before['close']
|
||||
)
|
||||
|
||||
qtbot.waitUntil(live_update_or_exit, timeout=60_000)
|
||||
except BaseException as exc:
|
||||
errors.append(exc)
|
||||
finally:
|
||||
session.request_shutdown()
|
||||
try:
|
||||
qtbot.waitUntil(
|
||||
lambda: session.outcome is not None,
|
||||
timeout=60_000,
|
||||
)
|
||||
session.outcome.unwrap()
|
||||
except BaseException as exc:
|
||||
errors.append(exc)
|
||||
if session.outcome is not None:
|
||||
# Early failure can leave these outside Qt parent trees.
|
||||
for attr in ('search', 'hist_linked', 'rt_linked'):
|
||||
widget = getattr(session.main_widget, attr, None)
|
||||
if widget is not None and widget.parent() is None:
|
||||
qtbot.addWidget(widget)
|
||||
record_property('owned_actors', repr(owned_actors))
|
||||
record_property('chart_shm_names', sorted(shm_names))
|
||||
|
||||
# Check exact survivors even when boot assertions already failed.
|
||||
if tractor.current_actor(err_on_no_runtime=False) is not None:
|
||||
errors.append(AssertionError('Guest runtime survived'))
|
||||
for name in shm_names:
|
||||
try:
|
||||
segment = _shm.SharedMemory(name=name, create=False)
|
||||
except FileNotFoundError:
|
||||
continue
|
||||
segment.close()
|
||||
errors.append(AssertionError(
|
||||
f'Chart SHM survived guest completion: {name}'
|
||||
))
|
||||
endpoints: set[tuple] = {reg_addr}
|
||||
for addrs in owned_actors.values():
|
||||
endpoints.update(map(tuple, addrs))
|
||||
for addr in endpoints:
|
||||
with socket.socket() as sock:
|
||||
sock.settimeout(1)
|
||||
if sock.connect_ex(addr) == 0:
|
||||
errors.append(AssertionError(
|
||||
f'Owned actor endpoint survived: {addr}'
|
||||
))
|
||||
|
||||
if errors:
|
||||
if len(errors) == 1:
|
||||
raise errors[0]
|
||||
raise BaseExceptionGroup('Chart and shutdown failed', errors)
|
||||
assert session.is_ready, 'guest returned before readiness'
|
||||
|
|
@ -1,268 +0,0 @@
|
|||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
from textwrap import dedent
|
||||
from types import SimpleNamespace
|
||||
from typing import Any
|
||||
|
||||
from PyQt6.QtGui import QResizeEvent
|
||||
from PyQt6.QtWidgets import (
|
||||
QApplication,
|
||||
QWidget,
|
||||
)
|
||||
from pytestqt.qtbot import QtBot
|
||||
from pytestqt.wait_signal import SignalBlocker
|
||||
import pytest
|
||||
import tractor
|
||||
import trio
|
||||
|
||||
from piker.service import get_runtime_vars
|
||||
from piker.ui import _window
|
||||
from piker.ui._exec import (
|
||||
QtractorSession,
|
||||
start_qtractor,
|
||||
)
|
||||
from piker.ui._window import MainWindow
|
||||
|
||||
|
||||
class QtractorTestWidget(QWidget):
|
||||
'''
|
||||
Supply the central-widget resize hook expected by `MainWindow`.
|
||||
|
||||
'''
|
||||
window: MainWindow
|
||||
|
||||
def on_win_resize(self, event: QResizeEvent) -> None:
|
||||
'''
|
||||
Accept production `MainWindow.resizeEvent()` notifications.
|
||||
|
||||
'''
|
||||
|
||||
|
||||
async def run_until_window_close(
|
||||
main_widget: QtractorTestWidget,
|
||||
exited: list[QtractorTestWidget],
|
||||
reg_addr: tuple[str, int],
|
||||
runtime_overrides: dict[str, Any],
|
||||
|
||||
) -> None:
|
||||
'''
|
||||
Publish readiness and retain the guest task until cancellation.
|
||||
|
||||
'''
|
||||
session: QtractorSession|None = (
|
||||
main_widget.window.qtractor_session
|
||||
)
|
||||
if session is None:
|
||||
raise RuntimeError(
|
||||
'No Qtractor session owns the test window'
|
||||
)
|
||||
actor: tractor.Actor = tractor.current_actor()
|
||||
assert reg_addr in actor.accept_addrs
|
||||
assert tractor.runtime._state._runtime_vars[
|
||||
'piker_vars'
|
||||
] == runtime_overrides
|
||||
session.set_ready()
|
||||
try:
|
||||
await trio.sleep_forever()
|
||||
finally:
|
||||
exited.append(main_widget)
|
||||
|
||||
|
||||
def test_qtractor_restarts_after_structured_window_close(
|
||||
isolated_ui_state: SimpleNamespace,
|
||||
qapp: QApplication,
|
||||
qtbot: QtBot,
|
||||
reg_addr: tuple[str, int],
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
|
||||
) -> None:
|
||||
'''
|
||||
Repeated Qt sessions must not signal or strand the Trio runtime.
|
||||
|
||||
`MainWindow.closeEvent()` previously sent process-wide SIGINT
|
||||
while `run_qtractor()` hid the guest outcome behind blocking
|
||||
`app.exec_()`. A caller-owned pytest-qt application therefore
|
||||
could not observe a clean startup or close one session without
|
||||
interrupting pytest.
|
||||
|
||||
Start two real Tractor root runtimes in sequence on the
|
||||
test-owned registry. Each guest publishes readiness only after
|
||||
entering its task, then a real window-close event requests
|
||||
cancellation through `QtractorSession`. Completion, the guest
|
||||
`finally` block, the empty runtime slot, and successful second
|
||||
startup prove structured teardown completed without quitting the
|
||||
shared Qt application.
|
||||
|
||||
Before each start, seed `get_runtime_vars()` with a stale
|
||||
loopback registry and conflicting runtime overrides. Previously
|
||||
these globals overwrote explicit caller kwargs. Assertions inside
|
||||
the guest verify the actual bound address and runtime state before
|
||||
readiness, rather than merely checking forwarded arguments.
|
||||
|
||||
'''
|
||||
assert isolated_ui_state.qapp is qapp
|
||||
exited: list[QtractorTestWidget] = []
|
||||
qt_state: tuple[bool, int, str] = (
|
||||
qapp.quitOnLastWindowClosed(),
|
||||
qapp.cursorFlashTime(),
|
||||
qapp.styleSheet(),
|
||||
)
|
||||
tractor_kwargs: dict[str, Any] = {
|
||||
'registry_addrs': [reg_addr],
|
||||
'tractor_runtime_overrides': {
|
||||
'piker_test_dir': str(
|
||||
isolated_ui_state.test_config_dir
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
_: int
|
||||
for _ in range(2):
|
||||
monkeypatch.setitem(
|
||||
get_runtime_vars(),
|
||||
'registry_addrs',
|
||||
[('127.0.0.1', 0)],
|
||||
)
|
||||
monkeypatch.setitem(
|
||||
get_runtime_vars(),
|
||||
'tractor_runtime_overrides',
|
||||
{'stale': True},
|
||||
)
|
||||
session: QtractorSession = start_qtractor(
|
||||
func=run_until_window_close,
|
||||
args=(
|
||||
exited,
|
||||
reg_addr,
|
||||
tractor_kwargs['tractor_runtime_overrides'],
|
||||
),
|
||||
main_widget_type=QtractorTestWidget,
|
||||
tractor_kwargs=tractor_kwargs,
|
||||
app=qapp,
|
||||
)
|
||||
qtbot.addWidget(session.window)
|
||||
|
||||
assert session.app is qapp
|
||||
assert not session.owns_app_exec
|
||||
assert session.window.godwidget is session.main_widget
|
||||
assert session.window.isVisible()
|
||||
assert session.outcome is None
|
||||
|
||||
ready: SignalBlocker = qtbot.waitSignal(
|
||||
session.application_ready,
|
||||
timeout=30_000,
|
||||
)
|
||||
ready.wait()
|
||||
assert session.is_ready
|
||||
assert session.trio_token is not None
|
||||
assert session.cancel_scope is not None
|
||||
|
||||
completed: SignalBlocker
|
||||
with qtbot.waitSignal(
|
||||
session.completed,
|
||||
timeout=30_000,
|
||||
) as completed:
|
||||
session.window.close()
|
||||
|
||||
assert completed.args == [session.outcome]
|
||||
assert session.outcome is not None
|
||||
assert session.outcome.unwrap() is None
|
||||
assert session.cancel_scope.cancel_called
|
||||
assert exited[-1] is session.main_widget
|
||||
assert session.window.qtractor_session is None
|
||||
assert _window._qt_win is None
|
||||
assert tractor.current_actor(
|
||||
err_on_no_runtime=False,
|
||||
) is None
|
||||
assert not qapp.closingDown()
|
||||
assert (
|
||||
qapp.quitOnLastWindowClosed(),
|
||||
qapp.cursorFlashTime(),
|
||||
qapp.styleSheet(),
|
||||
) == qt_state
|
||||
|
||||
|
||||
@pytest.mark.parametrize('fail', [False, True])
|
||||
def test_run_qtractor_blocking_adapter(
|
||||
reg_addr: tuple[str, int],
|
||||
tmp_path: Path,
|
||||
fail: bool,
|
||||
|
||||
) -> None:
|
||||
'''
|
||||
The blocking CLI adapter must drive PyQt6 and unwrap its guest.
|
||||
|
||||
`run_qtractor()` called the removed `QApplication.exec_()` API,
|
||||
failing before Trio could start. Run the real adapter in a fresh
|
||||
process with an offscreen application and test-owned registry and
|
||||
config. The guest either returns normally or raises a sentinel
|
||||
error after registering its window for inspection.
|
||||
|
||||
Guest completion must quit `QApplication.exec()` without a timer
|
||||
or external shutdown. A bounded subprocess wait detects hangs;
|
||||
exit status, propagated error identity, and the cleared actor and
|
||||
window slots prove both outcome paths finish structured teardown.
|
||||
Process isolation keeps the CLI's Qt ownership out of pytest-qt.
|
||||
|
||||
'''
|
||||
script: str = dedent(f'''
|
||||
from PyQt6.QtGui import QResizeEvent
|
||||
from PyQt6.QtWidgets import QWidget
|
||||
import tractor
|
||||
from piker.ui import _window
|
||||
from piker.ui._exec import run_qtractor
|
||||
|
||||
class Widget(QWidget):
|
||||
def on_win_resize(self, event: QResizeEvent) -> None:
|
||||
pass
|
||||
|
||||
windows: list[QWidget] = []
|
||||
error = RuntimeError('qtractor guest sentinel')
|
||||
|
||||
async def main(widget: Widget) -> None:
|
||||
windows.append(widget.window)
|
||||
assert {reg_addr!r} in (
|
||||
tractor.current_actor().accept_addrs
|
||||
)
|
||||
if {fail!r}:
|
||||
raise error
|
||||
|
||||
try:
|
||||
run_qtractor(
|
||||
func=main,
|
||||
args=(),
|
||||
main_widget_type=Widget,
|
||||
tractor_kwargs={{
|
||||
'registry_addrs': [{reg_addr!r}],
|
||||
'tractor_runtime_overrides': {{
|
||||
'piker_test_dir': {str(tmp_path)!r},
|
||||
}},
|
||||
}},
|
||||
)
|
||||
except RuntimeError as exc:
|
||||
assert {fail!r}
|
||||
assert exc is error
|
||||
else:
|
||||
assert not {fail!r}
|
||||
|
||||
assert len(windows) == 1
|
||||
assert windows[0].qtractor_session is None
|
||||
assert _window._qt_win is None
|
||||
assert tractor.current_actor(
|
||||
err_on_no_runtime=False,
|
||||
) is None
|
||||
''')
|
||||
result: subprocess.CompletedProcess[str] = subprocess.run(
|
||||
[sys.executable, '-c', script],
|
||||
env={
|
||||
**os.environ,
|
||||
'QT_QPA_PLATFORM': 'offscreen',
|
||||
'XDG_CONFIG_HOME': str(tmp_path / 'config'),
|
||||
'XDG_CONFIG_DIRS': str(tmp_path / 'system-config'),
|
||||
},
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=60,
|
||||
)
|
||||
assert result.returncode == 0, result.stdout + result.stderr
|
||||
Loading…
Reference in New Issue