Skip to content

Commit b5b4926

Browse files
committed
tests, strings, documentation. check pt before linux test
1 parent 9ad3601 commit b5b4926

8 files changed

Lines changed: 212 additions & 42 deletions

File tree

‎.github/workflows/tests.yml‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,13 @@ jobs:
2626
matrix:
2727
# windows-latest is first because it is the platform this library
2828
# targets; the cross-platform lifecycle stubs must pass there.
29-
os: [windows-latest, ubuntu-latest, macos-latest]
29+
# ubuntu was removed from the matrix 2026-09-18: CI standards track
30+
# HARDWARE-VERIFIED platforms, and Linux has not been verified
31+
# against a real board. When it is, it returns here and is held to
32+
# the same standards (85% gate, hardware-test evidence for core
33+
# changes). Note the Linux pdeathsig dead-man path only executes on
34+
# a Linux runner, so it is regression-untested until then.
35+
os: [windows-latest, macos-latest]
3036
python-version: ["3.11", "3.12", "3.13"]
3137

3238
steps:
@@ -42,12 +48,12 @@ jobs:
4248
run: uv sync
4349

4450
- name: Run test suite (hardware tests deselected)
45-
run: uv run pytest -m "not hardware" --cov=hackrfpy --cov-report=xml --cov-report=term-missing --cov-fail-under=82
51+
run: uv run pytest -m "not hardware" --cov=hackrfpy --cov-report=xml --cov-report=term-missing --cov-fail-under=85
4652

4753
- name: Upload coverage to Codecov
4854
# Optional: only runs once linked at https://codecov.io. Tokenless for
4955
# public repos. Safe to leave in before linking - it just no-ops/soft-fails.
50-
if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12'
56+
if: matrix.os == 'windows-latest' && matrix.python-version == '3.12'
5157
uses: codecov/codecov-action@v5
5258
with:
5359
files: hackrfpy/coverage.xml

‎CHANGELOG.md‎

Lines changed: 56 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ release.
8484
output written by the child's interrupt handler is drained into `stop()`'s
8585
result (the no-truncated-capture contract), and a child that ignores the
8686
interrupt is still reaped and reported unclean. The Windows CI leg proves
87-
the Windows path on the next push. (plan:#4, Phase 3a)
87+
the Windows path on the next push.
8888
- OS dead-man for handle-mode children (closes the long-standing
8989
`TODO(os-deadman)` in `core.py`): the atexit backstop never runs on
9090
SIGKILL / TerminateProcess, so a hard-killed parent could leave a
@@ -99,13 +99,32 @@ release.
9999
`backstop_rx=False`. `tests/test_deadman.py` proves it with a real
100100
SIGKILL of an intermediate parent on POSIX -- the child dies AND its
101101
interrupt handler ran -- plus the opt-out gate; the Windows Job Object
102-
leg proves on the next CI push. (plan:#4, Phase 3b)
103-
- SigMF spec compliance is now tested, not trusted (plan:#8): the official
102+
leg proves on the next CI push.
103+
- No-board detection reported nothing useful on Linux: `hackrf_info` there
104+
prints its version lines and "No HackRF boards found." to STDOUT and
105+
exits 1 with an empty stderr, so `detect()` raised on the exit code and
106+
discarded everything -- `problem` came back blank and `tools_version`
107+
`None`. `detect()` now parses whatever the tool printed regardless of
108+
exit code and reports the tool's own words, and tool errors in general
109+
fall back to the last stdout line when stderr is empty. Found and
110+
verified against the real 2023.01.1 Linux binaries (no board attached);
111+
regression tests pin both behaviors.
112+
- Documentation refresh across both READMEs and CONTRIBUTING: the root
113+
README's claim that an OS-level dead-man was "not yet implemented" (it now
114+
is, and is tested), four dangling links to a `project_summary.md` that does
115+
not exist (scope statements are now inline), the CLI reference extended
116+
with `monitor`, `scan`, `sweep -o/-B/-I`, and `tx --cw`, the
117+
`monitor_frequencies` notes now state the covering-bin semantics, the
118+
package README's features/platform/transmit sections updated for the
119+
lifecycle-safety work, CONTRIBUTING's "mypy (advisory for now)" corrected
120+
to blocking, and the PR checklist extended (changelog entry, docstring
121+
gate, coverage gate).
122+
- SigMF spec compliance is now tested, not trusted: the official
104123
`sigmf` package joined the dev dependency group, and the sidecar writer's
105124
output is validated with it -- including that the `hackrf` extension is
106125
DECLARED, not just used (a strict-validator rejection that regressed once
107126
pre-1.0). GNU Radio / IQEngine interop is a tested property.
108-
- Docstrings across the entire public API (plan:#1): every public method on
127+
- Docstrings across the entire public API: every public method on
109128
`HackRF` and `PersistentReceiver`, both classes, and the module-level
110129
functions -- 60 docstrings where `help()` previously returned nothing.
111130
The documentation used to live only in `#` comments, invisible to
@@ -115,7 +134,7 @@ release.
115134
enforced parent-side, so a copied `--print-cmd` argv carries NO time
116135
bound. `tests/test_docstrings.py` gates the whole surface so it cannot
117136
drift back to undocumented.
118-
- CLI catch-up with the library (plan:#5), each command with tests:
137+
- CLI caught up with the library, each command with tests:
119138
`hrf tx --cw` (a bounded CW test tone; requires `-d/--duration` because an
120139
unbounded carrier is exactly the orphan-transmitter risk the library
121140
exists to prevent; `--cw-amplitude` defaults below full scale),
@@ -128,13 +147,13 @@ release.
128147
threads -- per-instance mutable state (`last_params`, persisted mode,
129148
logging wiring) and per-child drain threads. One instance per thread;
130149
instances are cheap. In the README and on the class.
131-
- Coverage policy recorded in CONTRIBUTING (plan:#11): CI gates at
150+
- Coverage policy recorded in CONTRIBUTING: CI gates at
132151
`--cov-fail-under=82`, deliberately under the measured 85% because that
133152
figure counts Windows-only and hardware-only code as missed on Linux legs;
134153
to be revisited upward after the docstring pass.
135154
- `hackrfpy.__version__`, resolved from installed package metadata
136155
(`importlib.metadata`), with a `0.0.0+unknown` fallback for uninstalled
137-
checkouts. (plan:#7)
156+
checkouts.
138157
- `examples/fm_demod_to_wav.py`: the missing last mile -- demodulate a
139158
captured broadcast-FM IQ file to an audible mono 16-bit WAV using only
140159
numpy and the stdlib. Channelize (staged windowed-sinc decimation to
@@ -148,6 +167,29 @@ release.
148167
modulation, and spectral flatness 0.09 (structured content, not noise).
149168

150169
### Changed
170+
- CI platform policy: the test matrix now contains hardware-verified
171+
platforms only -- currently Windows (primary) and macOS -- and Linux was
172+
removed until it is verified against a real board, at which point it
173+
returns under the same standards. The coverage gate rose from 82% to an
174+
85% minimum on every remaining leg, and the codecov upload moved from the
175+
removed Linux leg to the Windows 3.12 leg. Known consequence, recorded in
176+
the workflow comment: the Linux `pdeathsig` dead-man path only executes on
177+
a Linux runner, so it is regression-untested until Linux re-enters the
178+
matrix.
179+
- CONTRIBUTING policy rewrite to match: 85% minimum coverage, the
180+
hardware-verified-platforms rule, and a new requirement that any change
181+
under `src/hackrfpy/` include evidence of a full-suite run (hardware tests
182+
passing) on Windows with a real board -- CI cannot attach hardware, so
183+
that gate is enforced by review. The `needs_tools` test category (real
184+
binaries on PATH, no board needed) is now documented, which is why
185+
passed/skipped counts differ between machines.
186+
- Second documentation pass: the root README's local-install line pinned a
187+
two-versions-stale wheel name (now a wildcard), its testing notes still
188+
described the Windows CTRL_BREAK path as untested (it now points at the
189+
tests that prove it), and its repo tree was missing three examples; the
190+
package README's platform section now records the real-binaries Linux
191+
verification and the Windows-EXE-on-Linux non-goal, and gained a CLI
192+
quick-start.
151193
- Library diagnostics now go through the standard `logging` module instead of
152194
`print()`. Records are emitted on the `hackrfpy` logger: warnings at
153195
`WARNING`, verbose progress messages at `INFO`. A consumer can now route,
@@ -240,30 +282,32 @@ release.
240282
`433:434` and never covered 434.0-434.1 MHz even while warning about the
241283
snap. Edges now snap OUTWARD (floor the low edge, ceil the high edge) so
242284
the swept range always contains the requested band; the warning text says
243-
so. (plan:#2)
285+
so.
244286
- `monitor_frequencies()` reported the MEAN dB of the whole covering sweep
245287
segment as the power "at" a frequency, diluting a narrowband carrier
246288
toward the noise floor (a -20 dB carrier in one bin of a 10-bin segment
247289
read as -74). It now reads the bin covering the frequency (max of that
248290
bin +/-1 for tuning slop). Behavior change for `examples/channel_monitor.py`
249291
and any monitor consumer: readings of narrowband signals rise to their
250-
true level. (plan:#3)
292+
true level.
251293
- Handle-mode output was not visible to the parent until process exit unless
252294
the child flooded: the drain thread read pipes with `BufferedReader.read(N)`,
253295
which blocks until N bytes (64 KB) accumulate, so small periodic writes --
254296
e.g. hackrf_transfer's ~60-byte-per-second stats lines -- sat invisible for
255297
what would be ~18 minutes of real capture. Now `read1()`: bytes appear in
256298
the drain as soon as the child writes them. Found by the new clean-interrupt
257299
tests; also cut the test suite's wall time roughly in half by removing the
258-
same latency from every stubbed lifecycle test. (Phase 3a finding)
300+
same latency from every stubbed lifecycle test. (Found by the new
301+
clean-interrupt tests.)
259302
- `_Process.stop()`'s escalation fired `terminate()` and returned without
260303
reaping: a child that ignored the interrupt could outlive `stop()`, and
261304
`result()` reported `returncode None`. The escalation is now a bounded,
262305
reaped ladder (interrupt -> terminate -> kill), so `stop()` always returns
263-
with the child dead and a real exit status. (Phase 3a finding)
306+
with the child dead and a real exit status. (Found by the new
307+
clean-interrupt tests.)
264308
- `from_device()` guarded its parsed-info invariant with a bare `assert`,
265309
which `python -O` strips, letting raw text flow onward; it now raises
266-
`HackRFDeviceError`. (plan:#9)
310+
`HackRFDeviceError`.
267311

268312
## [1.0.0] - 2026-06-16
269313

‎CONTRIBUTING.md‎

Lines changed: 54 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ path.
1717
or code that reproduces it.
1818
- **Request a feature** — open an issue with the feature template. Note that
1919
signal processing (demod, FFT, waterfalls) is intentionally **out of scope**
20-
for this repo; see `project_summary.md`.
20+
for the library API — this project is transport + control. The `examples/`
21+
directory may demonstrate downstream processing (`fm_demod_to_wav.py`) to
22+
show where the boundary sits, but the API stays at `complex64` in, files
23+
out.
2124
- **Improve docs** — README fixes, clearer examples, and beginner notes are all
2225
welcome and don't require hardware.
2326
- **Submit code** — see the workflow below.
@@ -44,12 +47,25 @@ Run these from the `hackrfpy/` directory:
4447
uv run pytest -m "not hardware" # full hardware-free suite must pass
4548
uv run ruff check . # lint
4649
uv run ruff format . # apply formatting
47-
uv run mypy src/hackrfpy # type check (advisory for now)
50+
uv run mypy src/hackrfpy # type check (BLOCKING in CI)
4851
```
4952

50-
CI runs the same suite across Windows, Linux, and macOS on Python 3.11–3.13.
51-
Windows is the primary target platform, so process-lifecycle changes must pass
52-
there specifically.
53+
Also expected with a code PR:
54+
55+
- **A CHANGELOG entry** under `[Unreleased]` — this project's changelog is
56+
detailed and rationale-bearing; say what changed and why.
57+
- **Docstrings on new public API** — `tests/test_docstrings.py` gates the
58+
whole public surface and will fail your PR without them.
59+
- **Coverage** — CI gates at 85% (see the policy at the bottom of this file);
60+
new code arrives with its tests.
61+
- **Hardware evidence for core changes** — any change under `src/hackrfpy/`
62+
requires a full-suite run on Windows with a real HackRF attached (hardware
63+
tests passing), reported in the PR. See the policy below.
64+
65+
CI runs the suite on Windows and macOS across Python 3.11–3.13. Windows is
66+
the primary — and only hardware-verified — platform, so process-lifecycle
67+
changes must pass there specifically. Linux is not in the CI matrix: see the
68+
platform standard in the coverage policy below.
5369

5470
## Adding a command
5571

@@ -75,20 +91,48 @@ tests import. Don't hard-code envelope numbers in a method.
7591
## Tests that touch hardware
7692

7793
Tests that need a real board are marked `@pytest.mark.hardware` and self-skip
78-
when no device is detected. Parser fixtures live in `tests/fixtures/` and are
79-
frozen from real hardware output via `tests/collect_real_data.py`. If you add a
80-
parser, add a real-output fixture rather than a hand-written one where possible.
94+
when no device is detected. A second, lighter category is marked with a
95+
`needs_tools` skipif: those need the real `hackrf-tools` binaries on `PATH`
96+
but **no board**, so they run on any machine with the tools installed — which
97+
is why passed/skipped counts differ between machines. Parser fixtures live in
98+
`tests/fixtures/` and are frozen from real hardware output via
99+
`tests/collect_real_data.py`. If you add a parser, add a real-output fixture
100+
rather than a hand-written one where possible.
81101

82102
## Commit and PR conventions
83103

84104
- Keep PRs focused; one logical change per PR is easiest to review.
85105
- Reference the issue the PR closes (`Closes #123`).
86-
- Describe what you tested, and whether it was tested against real hardware or
87-
stubs only.
106+
- Describe what you tested. For changes under `src/hackrfpy/` (the core
107+
library), a full-suite run on Windows with a real HackRF attached — hardware
108+
tests passing — is **required**, and the PR should say so (paste the pytest
109+
tail). Docs, examples, and test-only changes are exempt. CI cannot attach a
110+
board, so this is the human half of the quality gate; maintainers may
111+
re-verify on their own hardware before merge.
88112
- New public methods need a README entry in the Method Reference and, ideally, a
89113
runnable example under `examples/`.
90114

91115
## License
92116

93117
By contributing, you agree that your contributions are licensed under the
94118
project's **GPL-2.0-or-later** license.
119+
120+
## Test coverage and platform policy
121+
122+
CI gates coverage at **85% minimum** (`--cov-fail-under=85`, applied on every
123+
leg of the CI matrix). Coverage below the gate fails the build; new code
124+
arrives with the tests that keep it above.
125+
126+
**The CI matrix contains hardware-verified platforms** — currently Windows.
127+
A platform enters the matrix when it is verified against a real HackRF
128+
board, and is then held to the same standards (the 85% gate and the
129+
hardware-evidence requirement below).
130+
131+
**Core library changes require a passing hardware test.** CI has no board
132+
attached, so the stub suite is necessary but not sufficient: any PR that
133+
changes code under `src/hackrfpy/` must include evidence of a full-suite
134+
run — hardware tests included and passing — on Windows with a real HackRF
135+
attached. Paste the pytest tail in the PR description. Changes limited to
136+
docs, examples, or tests are exempt. The gates that CI *can* enforce
137+
(coverage, lint, types, docstrings) stay automated; this one is enforced by
138+
review.

0 commit comments

Comments
 (0)