Make the simulator reproducible and widen the burst tests
A transmitter seeded itself from the builtin hash() of its label, and that is salted per interpreter, so every run produced different synthetic speech and keying. A test that failed could not be made to fail again -- the one thing needed to fix it. crc32 gives the same content every run. The hang test then compounded it by listening for a 1 s burst in a 6 s cycle over three sweeps: it is the release after the transmission that is under test, but a missed burst failed it just the same, with "nothing recorded". Now 2 s in 5 s over six sweeps, so only the ending can fail it. The neighbouring test had the opposite fault: asserting only that nothing was truncated, it passed whether or not the burst was ever heard. It now requires the recording it is drawing a conclusion from. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
8d94a52942
commit
af51b2657d
2 changed files with 19 additions and 6 deletions
|
|
@ -10,6 +10,7 @@ from __future__ import annotations
|
||||||
|
|
||||||
import math
|
import math
|
||||||
import time
|
import time
|
||||||
|
import zlib
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
@ -115,7 +116,11 @@ class VirtualTransmitter:
|
||||||
_rng: np.random.Generator = field(default=None, init=False, repr=False)
|
_rng: np.random.Generator = field(default=None, init=False, repr=False)
|
||||||
|
|
||||||
def __post_init__(self):
|
def __post_init__(self):
|
||||||
self._seed = abs(hash(self.label or self.frequency)) % 2**31
|
# crc32 rather than hash(): the builtin is salted per interpreter, so
|
||||||
|
# a test's synthetic speech and keying differed on every run and a
|
||||||
|
# failure could not be reproduced by running it again.
|
||||||
|
key = str(self.label or self.frequency).encode()
|
||||||
|
self._seed = zlib.crc32(key)
|
||||||
self._rng = np.random.default_rng(self._seed)
|
self._rng = np.random.default_rng(self._seed)
|
||||||
if not self.label:
|
if not self.label:
|
||||||
self.label = f"{self.frequency/1e6:.4f} MHz {self.mode}"
|
self.label = f"{self.frequency/1e6:.4f} MHz {self.mode}"
|
||||||
|
|
|
||||||
|
|
@ -52,11 +52,16 @@ def test_record_seconds_is_honoured_exactly(tmp_path):
|
||||||
|
|
||||||
|
|
||||||
def test_hang_seconds_ends_a_finished_transmission(tmp_path):
|
def test_hang_seconds_ends_a_finished_transmission(tmp_path):
|
||||||
"""A transmitter that stops must release the scanner after the hang time."""
|
"""A transmitter that stops must release the scanner after the hang time.
|
||||||
|
|
||||||
|
What is under test is the ending, not the catching, so the burst is a
|
||||||
|
generous 40% of the cycle over six sweeps: landing on it is meant to be
|
||||||
|
easy, and only the release afterwards should be able to fail this.
|
||||||
|
"""
|
||||||
tx = [V(146_520_000, "nfm", 0.4, 12_500, "burst",
|
tx = [V(146_520_000, "nfm", 0.4, 12_500, "burst",
|
||||||
period_seconds=6.0, on_seconds=1.0)]
|
period_seconds=5.0, on_seconds=2.0)]
|
||||||
s, hits = run_scan(tmp_path, tx, "146.4M-146.6M",
|
s, hits = run_scan(tmp_path, tx, "146.4M-146.6M",
|
||||||
record_seconds=20.0, hang_seconds=0.8, max_cycles=3)
|
record_seconds=20.0, hang_seconds=0.8, max_cycles=6)
|
||||||
assert hits, "nothing recorded"
|
assert hits, "nothing recorded"
|
||||||
ended_on_silence = [h for h in hits if "quiet" in h.stop_reason]
|
ended_on_silence = [h for h in hits if "quiet" in h.stop_reason]
|
||||||
assert ended_on_silence, [h.stop_reason for h in hits]
|
assert ended_on_silence, [h.stop_reason for h in hits]
|
||||||
|
|
@ -369,9 +374,12 @@ def test_being_cut_off_mid_transmission_is_reported(tmp_path):
|
||||||
|
|
||||||
def test_no_warning_when_the_signal_really_ended(tmp_path):
|
def test_no_warning_when_the_signal_really_ended(tmp_path):
|
||||||
tx = [V(146_520_000, "nfm", 0.4, 12_500, "burst",
|
tx = [V(146_520_000, "nfm", 0.4, 12_500, "burst",
|
||||||
period_seconds=12.0, on_seconds=2.0)]
|
period_seconds=5.0, on_seconds=2.0)]
|
||||||
s, hits = run_scan(tmp_path, tx, "146.4M-146.6M", record_seconds=20.0,
|
s, hits = run_scan(tmp_path, tx, "146.4M-146.6M", record_seconds=20.0,
|
||||||
hang_seconds=1.0, max_cycles=3, revisit_seconds=0.1)
|
hang_seconds=1.0, max_cycles=6, revisit_seconds=0.1)
|
||||||
|
# Without a recording there is nothing to warn about either way, so the
|
||||||
|
# count alone would pass whether or not the burst was ever heard.
|
||||||
|
assert hits, "nothing recorded"
|
||||||
assert s.stats.truncated == 0
|
assert s.stats.truncated == 0
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue