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:
The Dust Council 2026-08-21 22:29:29 -07:00
parent 8d94a52942
commit af51b2657d
2 changed files with 19 additions and 6 deletions

View file

@ -10,6 +10,7 @@ from __future__ import annotations
import math
import time
import zlib
from dataclasses import dataclass, field
import numpy as np
@ -115,7 +116,11 @@ class VirtualTransmitter:
_rng: np.random.Generator = field(default=None, init=False, repr=False)
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)
if not self.label:
self.label = f"{self.frequency/1e6:.4f} MHz {self.mode}"

View file

@ -52,11 +52,16 @@ def test_record_seconds_is_honoured_exactly(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",
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",
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"
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]
@ -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):
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,
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