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:
|
else:
|
||||||
client = Client(
|
client = Client(
|
||||||
config={},
|
conf={},
|
||||||
httpx_client=trio_client,
|
httpx_client=trio_client,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,26 +38,17 @@ from ..log import (
|
||||||
get_console_log,
|
get_console_log,
|
||||||
)
|
)
|
||||||
from ..service import maybe_spawn_datad
|
from ..service import maybe_spawn_datad
|
||||||
from ._exec import (
|
from ._exec import run_qtractor
|
||||||
QtractorSession,
|
|
||||||
run_qtractor,
|
|
||||||
)
|
|
||||||
|
|
||||||
log = get_logger(__name__)
|
log = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
async def load_provider_search(
|
async def load_provider_search(
|
||||||
brokermod: ModuleType,
|
brokermod: str,
|
||||||
loglevel: str,
|
loglevel: str,
|
||||||
|
|
||||||
*,
|
|
||||||
task_status: trio.TaskStatus[None] = trio.TASK_STATUS_IGNORED,
|
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
'''
|
|
||||||
Publish startup after provider search and symcache are installed.
|
|
||||||
|
|
||||||
'''
|
|
||||||
name = brokermod.name
|
name = brokermod.name
|
||||||
log.info(f'loading datad for {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}`'
|
f'BACKEND DOES NOT (yet) support symcaching: `{brokermod.name}`'
|
||||||
)
|
)
|
||||||
|
|
||||||
task_status.started()
|
|
||||||
|
|
||||||
# keep search engine stream up until cancelled
|
# keep search engine stream up until cancelled
|
||||||
await trio.sleep_forever()
|
await trio.sleep_forever()
|
||||||
|
|
||||||
|
|
@ -93,23 +82,14 @@ async def _async_main(
|
||||||
syms: list[str],
|
syms: list[str],
|
||||||
brokers: dict[str, ModuleType],
|
brokers: dict[str, ModuleType],
|
||||||
loglevel: str,
|
loglevel: str,
|
||||||
exec_mode: str = 'live',
|
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
'''
|
"""
|
||||||
Main Qt-trio routine invoked by the Qt loop with the widgets ``dict``.
|
Main Qt-trio routine invoked by the Qt loop with the widgets ``dict``.
|
||||||
|
|
||||||
Provision the "main" widget with initial symbol data and root nursery.
|
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
|
# enable chart's console logging
|
||||||
if loglevel:
|
if loglevel:
|
||||||
get_console_log(
|
get_console_log(
|
||||||
|
|
@ -185,7 +165,7 @@ async def _async_main(
|
||||||
# load other providers into search **after**
|
# load other providers into search **after**
|
||||||
# the chart's select cache
|
# the chart's select cache
|
||||||
for brokername, mod in needed_brokermods.items():
|
for brokername, mod in needed_brokermods.items():
|
||||||
await tn.start(
|
tn.start_soon(
|
||||||
load_provider_search,
|
load_provider_search,
|
||||||
mod,
|
mod,
|
||||||
loglevel,
|
loglevel,
|
||||||
|
|
@ -214,14 +194,6 @@ async def _async_main(
|
||||||
):
|
):
|
||||||
# remove startup status text
|
# remove startup status text
|
||||||
starting_done()
|
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()
|
await trio.sleep_forever()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1298,7 +1298,6 @@ async def display_symbol_data(
|
||||||
fqmes: list[str],
|
fqmes: list[str],
|
||||||
loglevel: str,
|
loglevel: str,
|
||||||
order_mode_started: trio.Event,
|
order_mode_started: trio.Event,
|
||||||
exec_mode: str = 'live',
|
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
'''
|
'''
|
||||||
|
|
@ -1670,8 +1669,7 @@ async def display_symbol_data(
|
||||||
godwidget,
|
godwidget,
|
||||||
order_ctl_fqme,
|
order_ctl_fqme,
|
||||||
order_mode_started,
|
order_mode_started,
|
||||||
loglevel=loglevel,
|
loglevel=loglevel
|
||||||
exec_mode=exec_mode,
|
|
||||||
) as mode,
|
) as mode,
|
||||||
|
|
||||||
# TODO: maybe have these startup sooner before
|
# 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 __future__ import annotations
|
||||||
from typing import (
|
from typing import (
|
||||||
Any,
|
|
||||||
Awaitable,
|
|
||||||
Callable,
|
Callable,
|
||||||
|
Any,
|
||||||
|
Type,
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
)
|
)
|
||||||
import platform
|
import platform
|
||||||
|
import traceback
|
||||||
|
|
||||||
import qdarkstyle
|
import qdarkstyle
|
||||||
from qdarkstyle import DarkPalette
|
from qdarkstyle import DarkPalette
|
||||||
# import qdarkgraystyle # TODO: play with it
|
# import qdarkgraystyle # TODO: play with it
|
||||||
import trio
|
import trio
|
||||||
from outcome import Outcome
|
from outcome import Error
|
||||||
|
|
||||||
# Qt version-agnostic
|
# Qt version-agnostic
|
||||||
from .qt import (
|
from .qt import (
|
||||||
QWidget,
|
QWidget,
|
||||||
|
QMainWindow,
|
||||||
QApplication,
|
QApplication,
|
||||||
QtCore,
|
QtCore,
|
||||||
pyqtRemoveInputHook,
|
pyqtRemoveInputHook,
|
||||||
|
|
@ -54,7 +56,6 @@ from . import _style
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ._window import MainWindow
|
|
||||||
from ._widget import GodWidget
|
from ._widget import GodWidget
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -69,7 +70,7 @@ _do_overrides()
|
||||||
# XXX: pretty sure none of this shit works on linux as per:
|
# XXX: pretty sure none of this shit works on linux as per:
|
||||||
# https://bugreports.qt.io/browse/QTBUG-53022
|
# https://bugreports.qt.io/browse/QTBUG-53022
|
||||||
# it seems to work on windows.. no idea wtf is up.
|
# it seems to work on windows.. no idea wtf is up.
|
||||||
is_windows: bool = False
|
is_windows = False
|
||||||
if platform.system() == "Windows":
|
if platform.system() == "Windows":
|
||||||
is_windows = True
|
is_windows = True
|
||||||
|
|
||||||
|
|
@ -82,131 +83,24 @@ if platform.system() == "Windows":
|
||||||
QCoreApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True)
|
QCoreApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True)
|
||||||
|
|
||||||
|
|
||||||
class QtractorSession(QtCore.QObject):
|
def run_qtractor(
|
||||||
'''
|
func: Callable,
|
||||||
Own one Trio guest run embedded in a Qt application.
|
args: tuple,
|
||||||
|
main_widget_type: Type[QWidget],
|
||||||
'''
|
tractor_kwargs: dict[str, Any] = {},
|
||||||
application_ready = QtCore.pyqtSignal()
|
window_type: QMainWindow = None,
|
||||||
completed = QtCore.pyqtSignal(object)
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
app: QApplication,
|
|
||||||
window: MainWindow,
|
|
||||||
main_widget: QWidget,
|
|
||||||
owns_app_exec: bool,
|
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
'''
|
'''
|
||||||
Initialize state owned by one Qt-hosted Trio guest run.
|
Run the Qt event loop and embed `trio` via guest mode on it.
|
||||||
|
|
||||||
'''
|
|
||||||
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.
|
|
||||||
|
|
||||||
'''
|
'''
|
||||||
# avoids annoying message when entering debugger from qt loop
|
# avoids annoying message when entering debugger from qt loop
|
||||||
pyqtRemoveInputHook()
|
pyqtRemoveInputHook()
|
||||||
|
|
||||||
running_app: QApplication|None = QApplication.instance()
|
app = QApplication.instance()
|
||||||
if app is None:
|
if app is None:
|
||||||
app = running_app or QApplication([])
|
app = 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()
|
|
||||||
|
|
||||||
# TODO: we might not need this if it's desired
|
# TODO: we might not need this if it's desired
|
||||||
# to cancel the tractor machinery on Qt loop
|
# to cancel the tractor machinery on Qt loop
|
||||||
|
|
@ -224,58 +118,39 @@ def start_qtractor(
|
||||||
|
|
||||||
# source gist and credit to njs:
|
# source gist and credit to njs:
|
||||||
# https://gist.github.com/njsmith/d996e80b700a339e0623f97f48bcf0cb
|
# https://gist.github.com/njsmith/d996e80b700a339e0623f97f48bcf0cb
|
||||||
REENTER_EVENT: QtCore.QEvent.Type = QtCore.QEvent.Type(
|
REENTER_EVENT = QtCore.QEvent.Type(QtCore.QEvent.registerEventType())
|
||||||
QtCore.QEvent.registerEventType()
|
|
||||||
)
|
|
||||||
|
|
||||||
class ReenterEvent(QtCore.QEvent):
|
class ReenterEvent(QtCore.QEvent):
|
||||||
'''
|
pass
|
||||||
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
|
|
||||||
|
|
||||||
class Reenter(QtCore.QObject):
|
class Reenter(QtCore.QObject):
|
||||||
'''
|
def event(self, event):
|
||||||
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)
|
|
||||||
|
|
||||||
event.fn()
|
event.fn()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
reenter: Reenter = Reenter()
|
reenter = Reenter()
|
||||||
|
|
||||||
def run_sync_soon_threadsafe(
|
def run_sync_soon_threadsafe(fn):
|
||||||
fn: Callable[[], None],
|
event = ReenterEvent(REENTER_EVENT)
|
||||||
|
event.fn = fn
|
||||||
) -> None:
|
|
||||||
'''
|
|
||||||
Post one Trio guest callback onto Qt's event queue.
|
|
||||||
|
|
||||||
'''
|
|
||||||
event: ReenterEvent = ReenterEvent(fn)
|
|
||||||
app.postEvent(reenter, event)
|
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
|
# load dark theme
|
||||||
stylesheet: str = qdarkstyle.load_stylesheet(
|
stylesheet = qdarkstyle.load_stylesheet(
|
||||||
qt_api='pyqt6',
|
qt_api='pyqt6',
|
||||||
palette=DarkPalette,
|
palette=DarkPalette,
|
||||||
)
|
)
|
||||||
|
|
@ -287,7 +162,7 @@ def start_qtractor(
|
||||||
if window_type is None:
|
if window_type is None:
|
||||||
window_type = _window.MainWindow
|
window_type = _window.MainWindow
|
||||||
|
|
||||||
window: MainWindow = window_type()
|
window = window_type()
|
||||||
|
|
||||||
# set global app's main window singleton
|
# set global app's main window singleton
|
||||||
_window._qt_win = window
|
_window._qt_win = window
|
||||||
|
|
@ -302,66 +177,19 @@ def start_qtractor(
|
||||||
instance: GodWidget = main_widget_type()
|
instance: GodWidget = main_widget_type()
|
||||||
instance.window = window
|
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
|
# override tractor's defaults
|
||||||
tractor_kwargs = {
|
tractor_kwargs.update(
|
||||||
**get_runtime_vars(),
|
get_runtime_vars()
|
||||||
**(tractor_kwargs or {}),
|
)
|
||||||
}
|
|
||||||
|
|
||||||
# define tractor entrypoint
|
# define tractor entrypoint
|
||||||
async def main() -> None:
|
async def main():
|
||||||
'''
|
|
||||||
Run the application under its owned Trio cancel scope.
|
|
||||||
|
|
||||||
'''
|
|
||||||
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(
|
async with maybe_open_pikerd(
|
||||||
**tractor_kwargs,
|
**tractor_kwargs,
|
||||||
):
|
):
|
||||||
await func(*((instance,) + args))
|
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
|
# guest mode entry
|
||||||
trio.lowlevel.start_guest_run(
|
trio.lowlevel.start_guest_run(
|
||||||
main,
|
main,
|
||||||
|
|
@ -380,46 +208,4 @@ def start_qtractor(
|
||||||
|
|
||||||
# actually render to screen
|
# actually render to screen
|
||||||
window.show()
|
window.show()
|
||||||
return session
|
app.exec_()
|
||||||
|
|
||||||
|
|
||||||
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()
|
|
||||||
|
|
|
||||||
|
|
@ -80,8 +80,6 @@ class GodWidget(QWidget):
|
||||||
'''
|
'''
|
||||||
search: SearchWidget
|
search: SearchWidget
|
||||||
mode_name: str = 'god'
|
mode_name: str = 'god'
|
||||||
# `_async_main()` selects EMS execution before any symbol loads.
|
|
||||||
exec_mode: str = 'live'
|
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
|
||||||
|
|
@ -209,7 +207,6 @@ class GodWidget(QWidget):
|
||||||
fqmes,
|
fqmes,
|
||||||
loglevel,
|
loglevel,
|
||||||
order_mode_started,
|
order_mode_started,
|
||||||
self.exec_mode,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# self.vbox.addWidget(hist_charts)
|
# self.vbox.addWidget(hist_charts)
|
||||||
|
|
|
||||||
|
|
@ -19,10 +19,11 @@ Qt main window singletons and stuff.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
import os
|
||||||
|
import signal
|
||||||
import time
|
import time
|
||||||
from typing import (
|
from typing import (
|
||||||
Callable,
|
Callable,
|
||||||
TYPE_CHECKING,
|
|
||||||
Union,
|
Union,
|
||||||
)
|
)
|
||||||
import uuid
|
import uuid
|
||||||
|
|
@ -50,10 +51,6 @@ from ._style import (
|
||||||
from ._widget import GodWidget
|
from ._widget import GodWidget
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from ._exec import QtractorSession
|
|
||||||
|
|
||||||
|
|
||||||
log = get_logger(__name__)
|
log = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -294,45 +291,15 @@ class MainWindow(QMainWindow):
|
||||||
|
|
||||||
# event filter for global zoom shortcuts
|
# event filter for global zoom shortcuts
|
||||||
self._zoom_filter: GlobalZoomEventFilter | None = None
|
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:
|
def install_global_zoom_filter(self) -> None:
|
||||||
'''Install application-level event filter for global UI zoom shortcuts.'''
|
'''Install application-level event filter for global UI zoom shortcuts.'''
|
||||||
if self._zoom_filter is None:
|
if self._zoom_filter is None:
|
||||||
self._zoom_filter = GlobalZoomEventFilter(self)
|
self._zoom_filter = GlobalZoomEventFilter(self)
|
||||||
app: QApplication|None = QApplication.instance()
|
app = QApplication.instance()
|
||||||
if app is None:
|
|
||||||
raise RuntimeError(
|
|
||||||
'Cannot install zoom filter without a QApplication'
|
|
||||||
)
|
|
||||||
app.installEventFilter(self._zoom_filter)
|
app.installEventFilter(self._zoom_filter)
|
||||||
log.info('Installed global zoom shortcuts: Ctrl+Shift+Plus/Minus/0')
|
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
|
@property
|
||||||
def mode_label(self) -> QLabel:
|
def mode_label(self) -> QLabel:
|
||||||
|
|
||||||
|
|
@ -374,10 +341,11 @@ class MainWindow(QMainWindow):
|
||||||
settings.setValue('windowGeometry', self.saveGeometry())
|
settings.setValue('windowGeometry', self.saveGeometry())
|
||||||
log.debug('Saved window geometry for next session')
|
log.debug('Saved window geometry for next session')
|
||||||
|
|
||||||
if self._shutdown_callback is not None:
|
# raising KBI seems to get intercepted by by Qt so just use the system.
|
||||||
self._shutdown_callback()
|
os.kill(
|
||||||
|
os.getpid(),
|
||||||
super().closeEvent(event)
|
signal.SIGINT,
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def status_bar(self) -> QStatusBar:
|
def status_bar(self) -> QStatusBar:
|
||||||
|
|
|
||||||
|
|
@ -785,8 +785,7 @@ async def open_order_mode(
|
||||||
godw: GodWidget,
|
godw: GodWidget,
|
||||||
fqme: str,
|
fqme: str,
|
||||||
started: trio.Event,
|
started: trio.Event,
|
||||||
loglevel: str = 'info',
|
loglevel: str = 'info'
|
||||||
exec_mode: str = 'live',
|
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
'''
|
'''
|
||||||
|
|
@ -817,7 +816,6 @@ async def open_order_mode(
|
||||||
async with (
|
async with (
|
||||||
open_ems(
|
open_ems(
|
||||||
fqme,
|
fqme,
|
||||||
mode=exec_mode,
|
|
||||||
loglevel=loglevel,
|
loglevel=loglevel,
|
||||||
) as (
|
) as (
|
||||||
client,
|
client,
|
||||||
|
|
|
||||||
|
|
@ -123,12 +123,6 @@ def qapp_args(
|
||||||
|
|
||||||
|
|
||||||
def pytest_addoption(parser):
|
def pytest_addoption(parser):
|
||||||
parser.addoption(
|
|
||||||
'--live-provider',
|
|
||||||
choices=('kraken',),
|
|
||||||
default=None,
|
|
||||||
help='Opt into public-provider chart qualification',
|
|
||||||
)
|
|
||||||
parser.addoption(
|
parser.addoption(
|
||||||
'--headless',
|
'--headless',
|
||||||
action='store_true',
|
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