pgntui 0.3.0__tar.gz → 0.3.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pgntui-0.3.0 → pgntui-0.3.2}/CHANGELOG.md +16 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/PKG-INFO +1 -1
- pgntui-0.3.2/docs/audits/2026-06-04-round2-audit.md +71 -0
- pgntui-0.3.2/docs/audits/2026-06-04-round3-audit.md +79 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/packaging/homebrew/pgntui.rb +1 -1
- {pgntui-0.3.0 → pgntui-0.3.2}/packaging/winget/phobic.pgntui.yaml +2 -2
- {pgntui-0.3.0 → pgntui-0.3.2}/pyproject.toml +1 -1
- {pgntui-0.3.0 → pgntui-0.3.2}/src/pgntui/__init__.py +1 -1
- {pgntui-0.3.0 → pgntui-0.3.2}/src/pgntui/__main__.py +7 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/src/pgntui/app.py +2 -2
- {pgntui-0.3.0 → pgntui-0.3.2}/src/pgntui/recording/reader.py +5 -2
- {pgntui-0.3.0 → pgntui-0.3.2}/tests/test_app_record_toggle.py +55 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/tests/test_example_workspace.py +10 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/tests/test_recording_roundtrip.py +4 -8
- {pgntui-0.3.0 → pgntui-0.3.2}/tests/test_replay_mode.py +8 -5
- {pgntui-0.3.0 → pgntui-0.3.2}/tests/test_smoke.py +1 -1
- {pgntui-0.3.0 → pgntui-0.3.2}/.github/workflows/ci.yml +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/.github/workflows/release.yml +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/.gitignore +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/LICENSE +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/README.md +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/docs/audits/2026-06-04-audit-A-concurrency.md +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/docs/audits/2026-06-04-audit-B-decoding.md +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/docs/audits/2026-06-04-audit-C-platform.md +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/docs/audits/2026-06-04-audit-D-packaging.md +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/docs/audits/2026-06-04-release-notes-v0.3.0.md +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/docs/audits/2026-06-04-summary.md +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/docs/superpowers/plans/2026-06-04-pgntui-implementation.md +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/docs/superpowers/specs/2026-06-04-pgntui-design.md +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/packaging/README.md +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/src/pgntui/config.py +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/src/pgntui/containers/__init__.py +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/src/pgntui/containers/loader.py +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/src/pgntui/containers/screen.py +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/src/pgntui/debug/__init__.py +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/src/pgntui/debug/tab.py +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/src/pgntui/decode/__init__.py +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/src/pgntui/decode/canboat.py +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/src/pgntui/decode/fastpacket.py +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/src/pgntui/decode/pgns.json +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/src/pgntui/decode/router.py +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/src/pgntui/drivers/__init__.py +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/src/pgntui/drivers/actisense.py +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/src/pgntui/drivers/base.py +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/src/pgntui/drivers/replay.py +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/src/pgntui/examples/__init__.py +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/src/pgntui/examples/config.toml +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/src/pgntui/examples/containers/main.json +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/src/pgntui/examples/signals/anchor_light.json +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/src/pgntui/examples/signals/bilge_alarm.json +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/src/pgntui/examples/signals/depth.json +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/src/pgntui/examples/signals/engine_rpm.json +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/src/pgntui/examples/signals/speed.json +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/src/pgntui/examples/signals/target_heading.json +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/src/pgntui/examples/signals/water_temp.json +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/src/pgntui/logging/__init__.py +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/src/pgntui/logging/csv.py +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/src/pgntui/recording/__init__.py +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/src/pgntui/recording/writer.py +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/src/pgntui/replay_mode.py +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/src/pgntui/signals/__init__.py +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/src/pgntui/signals/base.py +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/src/pgntui/signals/widgets.py +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/src/pgntui/themes/__init__.py +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/src/pgntui/themes/builtin/__init__.py +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/src/pgntui/themes/builtin/amber-crt.json +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/src/pgntui/themes/builtin/dark.json +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/src/pgntui/themes/builtin/green-phosphor.json +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/src/pgntui/themes/builtin/light.json +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/src/pgntui/themes/builtin/mono-ascii.json +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/src/pgntui/themes/builtin/rainbow-disco.json +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/src/pgntui/themes/loader.py +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/tests/__init__.py +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/tests/fixtures/__init__.py +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/tests/fixtures/e2e_containers/engine.json +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/tests/fixtures/e2e_containers/nav.json +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/tests/fixtures/e2e_session.pgnlog +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/tests/fixtures/e2e_signals/engine_rpm.json +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/tests/fixtures/e2e_signals/wind_speed.json +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/tests/fixtures/frames.py +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/tests/fixtures/sample.pgnlog +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/tests/test_actisense_driver.py +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/tests/test_app_empty_welcome.py +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/tests/test_app_frame_loop.py +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/tests/test_app_on_write.py +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/tests/test_app_quit_binding.py +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/tests/test_app_shell.py +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/tests/test_app_worker_cancel_on_exit.py +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/tests/test_builtin_themes.py +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/tests/test_canboat.py +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/tests/test_cli.py +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/tests/test_config.py +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/tests/test_container_screen.py +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/tests/test_containers_loader.py +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/tests/test_csv_logger.py +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/tests/test_debug_tab.py +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/tests/test_drivers_base.py +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/tests/test_e2e_replay.py +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/tests/test_fastpacket.py +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/tests/test_main_replay.py +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/tests/test_packaging.py +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/tests/test_recording_writer.py +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/tests/test_replay_driver.py +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/tests/test_router.py +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/tests/test_signals_base.py +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/tests/test_signals_loader.py +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/tests/test_themes_loader.py +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/tests/test_widgets_analog_in.py +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/tests/test_widgets_analog_out.py +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/tests/test_widgets_digital_in.py +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/tests/test_widgets_digital_out.py +0 -0
- {pgntui-0.3.0 → pgntui-0.3.2}/tests/test_writer_race.py +0 -0
|
@@ -5,6 +5,20 @@ All notable changes to this project are documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.3.2] — 2026-06-04
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- Recording writer errors during `_handle_frame` now surface in the status bar instead of being silently swallowed.
|
|
12
|
+
- `pgntui --example` targeting an existing file path now prints a clean error instead of a `NotADirectoryError` traceback.
|
|
13
|
+
|
|
14
|
+
## [0.3.1] — 2026-06-04
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- Recording reader now parses timestamps as UTC. Replay frame deltas were unaffected, but CSV export and debug log absolute timestamps were off by the local UTC offset.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
- `test_iter_frames_honors_pause_with_sliding_resume` now polls instead of hard-sleeping; faster suite, less flake attractor.
|
|
21
|
+
|
|
8
22
|
## [0.3.0] — 2026-06-04
|
|
9
23
|
|
|
10
24
|
Bedtime audit fix wave: concurrency, decoding, packaging, recording, platform paths,
|
|
@@ -87,6 +101,8 @@ validation, and CI gating. Captures audits A/B/C/D from `docs/audits/2026-06-04-
|
|
|
87
101
|
|
|
88
102
|
Initial published release.
|
|
89
103
|
|
|
104
|
+
[0.3.2]: https://github.com/phobicdotno/pgntui/releases/tag/v0.3.2
|
|
105
|
+
[0.3.1]: https://github.com/phobicdotno/pgntui/releases/tag/v0.3.1
|
|
90
106
|
[0.3.0]: https://github.com/phobicdotno/pgntui/releases/tag/v0.3.0
|
|
91
107
|
[0.2.2]: https://github.com/phobicdotno/pgntui/releases/tag/v0.2.2
|
|
92
108
|
[0.2.0]: https://github.com/phobicdotno/pgntui/releases/tag/v0.2.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pgntui
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: Cross-platform TUI for NMEA 2000 with canboat decoding and pluggable drivers
|
|
5
5
|
Project-URL: Homepage, https://github.com/phobicdotno/pgntui
|
|
6
6
|
Project-URL: Source, https://github.com/phobicdotno/pgntui
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Round 2 Audit (2026-06-04, post fix-wave, pre 0.3.0)
|
|
2
|
+
Branch: main @ 010e7de
|
|
3
|
+
|
|
4
|
+
## Verification Results
|
|
5
|
+
|
|
6
|
+
All 23 Round 1 findings verified PASS at HEAD.
|
|
7
|
+
|
|
8
|
+
| Finding | Status | Evidence |
|
|
9
|
+
|---|---|---|
|
|
10
|
+
| A-1 | PASS | `app.py:346-351` iterates workers, calls `cancel()` on frame_loop group before `exit()` |
|
|
11
|
+
| A-2 | PASS | `_writer_lock` guards `_handle_frame` snapshot (185-187) and `_stop_recording` swap (312-314) |
|
|
12
|
+
| A-3 | PASS | `actisense.py:122` checks `self._stop.is_set()` at top of `while True` |
|
|
13
|
+
| A-4 | PASS | `replay.py:73,79,97,109` — stop event respected in sleep and yield points |
|
|
14
|
+
| A-5 | PASS | `__main__.py:255-260` — writer backstop in finally |
|
|
15
|
+
| B-1 | PASS | `canboat.py:126-142` applies offset + resolution |
|
|
16
|
+
| B-2 | PASS | `canboat.py:66,92` constructs and routes via `FastPacketReassembler` |
|
|
17
|
+
| B-4 | PASS | `router.py:44` — None != 2 correctly skips |
|
|
18
|
+
| B-6 | PASS | Writer/reader column order match; Frame priority+destination roundtrip |
|
|
19
|
+
| B-7 | PASS | `containers/loader.py:45-63` rejects overlap |
|
|
20
|
+
| B-8 | PASS | `themes/loader.py:54-61` validates gradient stops |
|
|
21
|
+
| C-1 | PASS | `config.py:10` uses platformdirs |
|
|
22
|
+
| C-2 | PASS | `signals/base.py:114-117,129-132` coerces thresholds to float |
|
|
23
|
+
| C-3 | PASS | `pgntui.spec:9-12` includes examples/ + copy_metadata |
|
|
24
|
+
| C-4 | PASS | `writer.py:23` uses `newline=""` |
|
|
25
|
+
| C-5 | PASS | `config.py:46-47` catches TOMLDecodeError |
|
|
26
|
+
| C-6 | PASS | `pyproject.toml:28` — `textual>=8.0` |
|
|
27
|
+
| C-7 | PASS | `app.py:113` — typed `_view_pairs`; no property |
|
|
28
|
+
| D-I1 | PASS | release.yml — test:10 / binaries:30 timeouts |
|
|
29
|
+
| D-I2 | PASS | pypi + binaries declare `needs: test` |
|
|
30
|
+
| D-I3 | PASS | pyproject.toml has [project.urls] |
|
|
31
|
+
| D-I4 | PASS | README expanded to 174 lines |
|
|
32
|
+
| D-I5 | PASS | packaging/README.md exists with runbook |
|
|
33
|
+
|
|
34
|
+
## New Findings
|
|
35
|
+
|
|
36
|
+
### NF-1: Reader parses UTC timestamps as local time
|
|
37
|
+
- **File:** `src/pgntui/recording/reader.py:17`
|
|
38
|
+
- **Severity:** P2 (replay speed correct via frame deltas; absolute timestamps off by UTC offset)
|
|
39
|
+
- **What's wrong:** Writer formats with `tz=UTC`. Reader calls `datetime.strptime(...).timestamp()` on naive datetime, which Python interprets as local time. In UTC+2 every replayed timestamp is 7200s low. CSV export and debug log timestamps are wrong by the local UTC offset.
|
|
40
|
+
- **Fix:** `datetime.strptime(parts[0], "%Y-%m-%d-%H:%M:%S.%f").replace(tzinfo=UTC).timestamp()` — one line.
|
|
41
|
+
|
|
42
|
+
### NF-2: Hard 1.5s sleep in pause test
|
|
43
|
+
- **File:** `tests/test_replay_mode.py:95`
|
|
44
|
+
- **Severity:** Low (CI flake attractor; no prod impact)
|
|
45
|
+
- **What's wrong:** `time.sleep(1.5)` to assert no frames during pause. Accumulates suite time and is fragile under scheduler jitter.
|
|
46
|
+
- **Fix:** Replace with polling loop — exit early on `len(emitted) > 1` (fast-fail) capped at a budget.
|
|
47
|
+
|
|
48
|
+
## Cross-fix Interaction Notes
|
|
49
|
+
|
|
50
|
+
- **F3 + F4 (Offset + fast-packet):** Clean. `_decode_fields` applies offset on fully assembled payload. No fast-packet PGN in bundled DB has non-zero Offset, but code path is correct regardless.
|
|
51
|
+
- **F5 + F2 in actisense.py:** Clean. `_stop` event in `__init__` + read_frames; `parse_frame` returns Frame with priority/destination.
|
|
52
|
+
- **F6 + F2 in app.py:** Both present. Welcome text + worker cancel + _writer_lock + atomic swap all in HEAD.
|
|
53
|
+
- **F6 + F2 in __main__.py:** Both present. platformdirs config path + TOMLDecodeError catch + SIGINT writer backstop all in HEAD.
|
|
54
|
+
- **Writer/reader columns:** `[ts, priority, pgn, src, dst, len, *hex]` — exact alignment.
|
|
55
|
+
- **Fast-packet duplicate frame 0 (same seq_id):** Second overwrites buffer with fresh state — N2K retransmit handled.
|
|
56
|
+
- **Fast-packet malformed length=0xFF:** Buffer expires via `_expire_stale(5.0s)`. No garbage decode path.
|
|
57
|
+
- **FastPacketReassembler thread safety:** Owned by CanboatDecoder, called only from single frame_loop worker. No concurrent access.
|
|
58
|
+
|
|
59
|
+
## Test Suite
|
|
60
|
+
|
|
61
|
+
- 156 tests passing
|
|
62
|
+
- Real threads in: test_writer_race, test_app_worker_cancel_on_exit, test_replay_mode — all use event-sync or generous-timeout polling except NF-2's flat sleep
|
|
63
|
+
- No network, no FS writes outside tmp_path
|
|
64
|
+
- No global state mutation; test_fastpacket patches time.monotonic in local context manager
|
|
65
|
+
- mypy --strict: clean
|
|
66
|
+
- ruff: clean
|
|
67
|
+
- No TODO/FIXME/HACK/XXX in src/
|
|
68
|
+
|
|
69
|
+
## Verdict
|
|
70
|
+
|
|
71
|
+
**Clean to ship 0.3.0.** All Round 1 findings fixed. NF-1 and NF-2 are minor: reader UTC offset doesn't break replay (frame deltas are correct) and the test sleep is a flake attractor not a prod bug. Fix in 0.3.1.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Round 3 Audit (post 0.3.1)
|
|
2
|
+
Branch: main @ 71d5f78
|
|
3
|
+
|
|
4
|
+
## NF-1 + NF-2 Verification
|
|
5
|
+
|
|
6
|
+
- **NF-1 (reader UTC):** PASS — `recording/reader.py:20` does `.replace(tzinfo=UTC).timestamp()` correctly.
|
|
7
|
+
- **NF-1 test:** PASS — `test_recording_roundtrip.py:52-55` asserts `abs(restored.timestamp - written.timestamp) < 1e-3`.
|
|
8
|
+
- **NF-2 (polling pause):** PASS — `test_replay_mode.py` budget loop polls with 50ms quantum, fast-fails on leak. Sliding margin 20× test threshold.
|
|
9
|
+
|
|
10
|
+
## Sweep A — Errors
|
|
11
|
+
|
|
12
|
+
| Severity | File:line | Issue |
|
|
13
|
+
|---|---|---|
|
|
14
|
+
| OK | reader.py:27 | `except (ValueError, IndexError)` — line parser, returns None |
|
|
15
|
+
| OK | fastpacket.py:158,167 | Skips bad PGN/length entries in pgns.json |
|
|
16
|
+
| OK | containers/themes/signals/config loaders | All raise typed exceptions |
|
|
17
|
+
| OK | __main__.py:139 | importlib.metadata fallback |
|
|
18
|
+
| OK | __main__.py:157 | driver.open exception → stderr + None (correct for optional hardware) |
|
|
19
|
+
| OK | app.py:175 | Frame loop safety net logs to UI |
|
|
20
|
+
| **SILENT** | **app.py:190** | **`except Exception: pass` on writer.write() — silent drop on disk full or closed file** |
|
|
21
|
+
| OK | app.py:273 | Pre-mount _set_status guard |
|
|
22
|
+
| OK | app.py:339,349 / __main__.py:250,259 | Defensive teardown finals |
|
|
23
|
+
|
|
24
|
+
### NF-3 (P3): app.py:190 silent write error
|
|
25
|
+
- **What's wrong:** Recording writer.write() error silently swallowed. On full disk or closed file the recording silently stops while UI shows "REC -> filename".
|
|
26
|
+
- **Fix:** One-liner — `self.call_from_thread(self._set_status, f"rec error: {e}")` in the except block.
|
|
27
|
+
|
|
28
|
+
## Sweep B — Resources
|
|
29
|
+
|
|
30
|
+
- All file handles via `with` blocks or explicit close in finally/close()
|
|
31
|
+
- `_writer_lock`, `_stop` Events properly initialized, no deadlock risk
|
|
32
|
+
- No subprocess, signal handlers, or fork calls
|
|
33
|
+
- `CSVSignalLogger` not currently wired into PgntuiApp (standalone) — no live handle leak
|
|
34
|
+
|
|
35
|
+
## Sweep C — Public API
|
|
36
|
+
|
|
37
|
+
- `__init__.py` exports only `__version__`. Minimal.
|
|
38
|
+
- entry-points correct: `actisense-ngt1 → NGT1Driver`, `file-replay → FileReplayDriver`. Both verified to exist + implement Driver Protocol.
|
|
39
|
+
- Minor: `__main__.py` imports `_default_workspace` (leading-underscore) from config. Convention violation, not functional bug.
|
|
40
|
+
|
|
41
|
+
## Sweep D — CLI UX
|
|
42
|
+
|
|
43
|
+
- `--help` documents `--workspace`, `--enable-write`, `--check`, `--example`, `replay` ✓
|
|
44
|
+
- `pgntui replay` (no file) → argparse usage error ✓
|
|
45
|
+
- `pgntui --example /path/to/file` → `iterdir()` raises `NotADirectoryError` traceback
|
|
46
|
+
- `pgntui --workspace /no/such/dir` → silent default config + welcome (reasonable)
|
|
47
|
+
- `pgntui replay --help` → argparse subparser help ✓
|
|
48
|
+
|
|
49
|
+
### NF-4 (P3): __main__.py:59 --example handles file path poorly
|
|
50
|
+
- **What's wrong:** `--example` targeting an existing file (not dir) raises `NotADirectoryError` instead of clean message.
|
|
51
|
+
- **Fix:** One-liner — `if workspace.is_file(): print("error: workspace path is a file"); return 1` before iterdir.
|
|
52
|
+
|
|
53
|
+
## Sweep E — Wheel hygiene
|
|
54
|
+
|
|
55
|
+
Verified via pyproject.toml static analysis (build not run):
|
|
56
|
+
- All force-include entries map to real files
|
|
57
|
+
- `decode/fastpacket.py` covered by packages=[src/pgntui] auto-discovery ✓
|
|
58
|
+
- `examples/__pycache__/` exists on disk (cosmetic; hatchling excludes by default)
|
|
59
|
+
- entry_points.txt derived correctly
|
|
60
|
+
- [project.urls] complete ✓
|
|
61
|
+
|
|
62
|
+
## Sweep F — Docs accuracy
|
|
63
|
+
|
|
64
|
+
- README.md: install, quickstart, workspace, themes, hotkeys, status — all match code ✓
|
|
65
|
+
- packaging/README.md: runbook executable; example version `0.2.2` is a template (cosmetic)
|
|
66
|
+
- CHANGELOG.md: 0.3.1 + 0.3.0 entries match commits ✓
|
|
67
|
+
- release-notes-v0.3.0.md: test count 95→156 accurate ✓
|
|
68
|
+
|
|
69
|
+
## Verdict
|
|
70
|
+
|
|
71
|
+
| Round | New P0/P1 | New P2 | New P3 |
|
|
72
|
+
|---|---|---|---|
|
|
73
|
+
| 1 | 5 | 13 | 12 |
|
|
74
|
+
| 2 | 0 | 1 (NF-1) | 1 (NF-2) |
|
|
75
|
+
| 3 | 0 | 0 | 2 (NF-3, NF-4) |
|
|
76
|
+
|
|
77
|
+
**Severity distribution this round: P0=0 P1=0 P2=0 P3=2.**
|
|
78
|
+
|
|
79
|
+
**Recommendation: STOP.** NF-3 and NF-4 are both edge-case P3s with trivial one-liner fixes. The codebase is clean to leave at 0.3.1, or alternatively ship 0.3.2 with both P3 fixes bundled (~10 min of work). After that, expected diminishing returns from further audit rounds.
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
class Pgntui < Formula
|
|
4
4
|
desc "Cross-platform TUI for NMEA 2000 with canboat decoding"
|
|
5
5
|
homepage "https://github.com/phobicdotno/pgntui"
|
|
6
|
-
url "https://files.pythonhosted.org/packages/source/p/pgntui/pgntui-0.3.
|
|
6
|
+
url "https://files.pythonhosted.org/packages/source/p/pgntui/pgntui-0.3.2.tar.gz"
|
|
7
7
|
sha256 "REPLACE_ON_RELEASE"
|
|
8
8
|
license "MIT"
|
|
9
9
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Stub manifest for microsoft/winget-pkgs PR
|
|
2
2
|
PackageIdentifier: phobic.pgntui
|
|
3
|
-
PackageVersion: 0.3.
|
|
3
|
+
PackageVersion: 0.3.2
|
|
4
4
|
PackageLocale: en-US
|
|
5
5
|
Publisher: phobic
|
|
6
6
|
PackageName: pgntui
|
|
@@ -15,7 +15,7 @@ Tags: [nmea, marine, tui, terminal]
|
|
|
15
15
|
Installers:
|
|
16
16
|
- Architecture: x64
|
|
17
17
|
InstallerType: portable
|
|
18
|
-
InstallerUrl: https://github.com/phobicdotno/pgntui/releases/download/v0.3.
|
|
18
|
+
InstallerUrl: https://github.com/phobicdotno/pgntui/releases/download/v0.3.2/pgntui-windows-x86_64.exe
|
|
19
19
|
InstallerSha256: REPLACE_ON_RELEASE
|
|
20
20
|
ManifestType: singleton
|
|
21
21
|
ManifestVersion: 1.6.0
|
|
@@ -56,6 +56,13 @@ def scaffold_example(workspace: Path) -> int:
|
|
|
56
56
|
Refuses to clobber an existing directory — the user must point at a fresh
|
|
57
57
|
path or delete the old one.
|
|
58
58
|
"""
|
|
59
|
+
if workspace.is_file():
|
|
60
|
+
print(
|
|
61
|
+
f"workspace path {workspace} is a file, not a directory. "
|
|
62
|
+
"Pass --workspace <other> or delete the file.",
|
|
63
|
+
file=sys.stderr,
|
|
64
|
+
)
|
|
65
|
+
return 2
|
|
59
66
|
if workspace.exists() and any(workspace.iterdir()):
|
|
60
67
|
print(
|
|
61
68
|
f"workspace exists at {workspace}, refusing to overwrite. "
|
|
@@ -187,8 +187,8 @@ class PgntuiApp(App[None]):
|
|
|
187
187
|
if writer is not None:
|
|
188
188
|
try:
|
|
189
189
|
writer.write(frame)
|
|
190
|
-
except Exception: # pragma: no cover — defensive
|
|
191
|
-
|
|
190
|
+
except Exception as e: # pragma: no cover — defensive
|
|
191
|
+
self.call_from_thread(self._set_status, f"rec error: {e}")
|
|
192
192
|
decoded = self._decoder.decode(frame)
|
|
193
193
|
if decoded is None:
|
|
194
194
|
return
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from collections.abc import Iterator
|
|
6
|
-
from datetime import datetime
|
|
6
|
+
from datetime import UTC, datetime
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
|
|
9
9
|
from pgntui.drivers.base import Frame
|
|
@@ -14,7 +14,10 @@ def parse_line(line: str) -> Frame | None:
|
|
|
14
14
|
if len(parts) < 7:
|
|
15
15
|
return None
|
|
16
16
|
try:
|
|
17
|
-
|
|
17
|
+
# Writer emits UTC timestamps via datetime.utcfromtimestamp; parse
|
|
18
|
+
# them back as UTC so .timestamp() returns the original epoch value
|
|
19
|
+
# rather than shifting by the local UTC offset.
|
|
20
|
+
ts = datetime.strptime(parts[0], "%Y-%m-%d-%H:%M:%S.%f").replace(tzinfo=UTC).timestamp()
|
|
18
21
|
priority = int(parts[1])
|
|
19
22
|
pgn = int(parts[2])
|
|
20
23
|
src = int(parts[3])
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
6
7
|
|
|
7
8
|
import pytest
|
|
8
9
|
|
|
@@ -94,3 +95,57 @@ async def test_record_writer_receives_frames(tmp_path: Path) -> None:
|
|
|
94
95
|
assert writer.frame_count >= 0
|
|
95
96
|
app.action_toggle_record()
|
|
96
97
|
await pilot.pause()
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class _RaisingWriter:
|
|
101
|
+
"""Stand-in writer whose write() always blows up."""
|
|
102
|
+
|
|
103
|
+
def __init__(self, path: Path) -> None:
|
|
104
|
+
self.path = path
|
|
105
|
+
self.opened = False
|
|
106
|
+
self.closed = False
|
|
107
|
+
|
|
108
|
+
def open(self) -> None:
|
|
109
|
+
self.opened = True
|
|
110
|
+
|
|
111
|
+
def close(self) -> None:
|
|
112
|
+
self.closed = True
|
|
113
|
+
|
|
114
|
+
def write(self, _frame: Frame) -> None:
|
|
115
|
+
raise OSError("disk full")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@pytest.mark.asyncio
|
|
119
|
+
async def test_record_write_error_surfaces_to_status(tmp_path: Path) -> None:
|
|
120
|
+
"""When the writer's write() raises, the status bar shows 'rec error'."""
|
|
121
|
+
import asyncio
|
|
122
|
+
|
|
123
|
+
app = PgntuiApp(
|
|
124
|
+
theme=load_builtin("dark"),
|
|
125
|
+
containers=[],
|
|
126
|
+
decoder=CanboatDecoder.load_bundled(),
|
|
127
|
+
router=SignalRouter(),
|
|
128
|
+
record_dir=tmp_path / "rec",
|
|
129
|
+
)
|
|
130
|
+
async with app.run_test() as pilot:
|
|
131
|
+
await pilot.pause()
|
|
132
|
+
# Inject a raising writer directly so we don't depend on the real
|
|
133
|
+
# ActisenseLogWriter for failure simulation.
|
|
134
|
+
bad_writer: Any = _RaisingWriter(tmp_path / "rec" / "fake.pgnlog")
|
|
135
|
+
with app._writer_lock:
|
|
136
|
+
app._writer = bad_writer
|
|
137
|
+
app._writer_path = bad_writer.path
|
|
138
|
+
frame = Frame(timestamp=1.0, source_addr=23, pgn=127488, data=b"\x00" * 8)
|
|
139
|
+
# _handle_frame runs on the worker thread in production. Use
|
|
140
|
+
# asyncio.to_thread so `call_from_thread` runs from a non-event-loop
|
|
141
|
+
# thread and can marshal the status update back without deadlocking.
|
|
142
|
+
await asyncio.to_thread(app._handle_frame, frame)
|
|
143
|
+
await pilot.pause()
|
|
144
|
+
# Status bar should reflect the recording error rather than silently
|
|
145
|
+
# carrying on as if writes were succeeding.
|
|
146
|
+
from textual.widgets import Static
|
|
147
|
+
|
|
148
|
+
status = app.query_one("#status-bar", Static)
|
|
149
|
+
rendered = str(status.render())
|
|
150
|
+
assert "rec error" in rendered
|
|
151
|
+
assert "disk full" in rendered
|
|
@@ -65,3 +65,13 @@ def test_example_works_with_brand_new_workspace_dir(tmp_path: Path) -> None:
|
|
|
65
65
|
ws.mkdir() # empty dir is OK
|
|
66
66
|
rc = main(["--workspace", str(ws), "--example"])
|
|
67
67
|
assert rc == 0
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_example_refuses_file_path(tmp_path: Path, capsys) -> None: # type: ignore[no-untyped-def]
|
|
71
|
+
"""Pointing --example at an existing file (not dir) errors cleanly."""
|
|
72
|
+
tmp_file = tmp_path / "notadir.txt"
|
|
73
|
+
tmp_file.write_text("oops")
|
|
74
|
+
rc = main(["--workspace", str(tmp_file), "--example"])
|
|
75
|
+
assert rc == 2
|
|
76
|
+
captured = capsys.readouterr()
|
|
77
|
+
assert "is a file" in captured.err
|
|
@@ -49,14 +49,10 @@ def test_write_read_roundtrip_preserves_all_fields(tmp_path: Path) -> None:
|
|
|
49
49
|
assert restored.data == written.data
|
|
50
50
|
assert restored.priority == written.priority
|
|
51
51
|
assert restored.destination == written.destination
|
|
52
|
-
#
|
|
53
|
-
#
|
|
54
|
-
#
|
|
55
|
-
|
|
56
|
-
# "%Y-%m-%d-%H:%M:%S.%f"[:-3] truncation), modulo whole-second TZ offsets.
|
|
57
|
-
sub_second_written = written.timestamp - int(written.timestamp)
|
|
58
|
-
sub_second_restored = restored.timestamp - int(restored.timestamp)
|
|
59
|
-
assert abs(sub_second_restored - sub_second_written) < 1e-3
|
|
52
|
+
# Reader now parses as UTC (NF-1), so the absolute timestamp survives
|
|
53
|
+
# the roundtrip modulo the writer's millisecond truncation
|
|
54
|
+
# ("%Y-%m-%d-%H:%M:%S.%f"[:-3] drops microsecond precision).
|
|
55
|
+
assert abs(restored.timestamp - written.timestamp) < 1e-3
|
|
60
56
|
|
|
61
57
|
|
|
62
58
|
def test_writer_uses_lf_only(tmp_path: Path) -> None:
|
|
@@ -88,12 +88,15 @@ def test_iter_frames_honors_pause_with_sliding_resume(tmp_path: Path) -> None:
|
|
|
88
88
|
|
|
89
89
|
# Pause immediately; the inter-frame sleep before frame 2 is in progress.
|
|
90
90
|
# Frames 2 (+500ms) and 3 (+1000ms) would normally land within the next
|
|
91
|
-
# ~1000ms.
|
|
92
|
-
#
|
|
93
|
-
# arrives. The wider window absorbs CI runner jitter and scheduler stalls.
|
|
91
|
+
# ~1000ms. Poll across a 1.5s budget and fail-fast the moment a frame
|
|
92
|
+
# leaks through — keeps the suite snappy while still absorbing CI jitter.
|
|
94
93
|
s.toggle_pause()
|
|
95
|
-
time.
|
|
96
|
-
|
|
94
|
+
budget_end = time.monotonic() + 1.5
|
|
95
|
+
while time.monotonic() < budget_end:
|
|
96
|
+
if len(emitted) > 1:
|
|
97
|
+
break # leak detected, will assert below
|
|
98
|
+
time.sleep(0.05)
|
|
99
|
+
assert len(emitted) == 1, f"pause leaked: emitted={len(emitted)}"
|
|
97
100
|
|
|
98
101
|
# Resume — frame 2 should arrive after the leftover inter-frame delay
|
|
99
102
|
# (sliding semantics), not instantly (which would mean catching up the
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|