Files
OpsLog/pskchase_test.go
T
rouggy 94cba2592e feat(chase-new): drop the frequency column, the beacons and the bands you don't have
Three cuts, all of them removing things nobody could act on.

The frequency column is gone. Clicking the row tunes the rig to it, so the
number was there to be read and never used. It is still carried on the spot —
the click is what needs it — and it is in the row's tooltip. The country takes
the space.

Modes are limited to FT8, FT4, FT2, PSK31 and RTTY. WSPR is the one worth
spelling out: it is a beacon, nobody answers a WSPR transmission, so a "new
entity on WSPR" is a path report and not a station to work — and on a quiet
band it would have been most of the list.

Bands are limited to the operator's own list. PSK Reporter carries every band
its receivers listen on, and a 13 cm decode is not an opportunity for a station
with no 13 cm.

Both tests run before the status lookup and both read a cached map: they throw
away most of the feed, so nothing downstream pays for it. The band list is
re-read when the lists are saved, so ticking a band in Settings applies to the
next decode rather than the next restart — which would have looked like the
filter not working.
2026-08-14 15:41:37 +02:00

112 lines
4.1 KiB
Go

package main
import (
"testing"
"time"
)
// The panel must not become one station repeated. PSK Reporter re-reports a
// calling operator every cycle, and dozens of receivers report the same
// transmission, so a station arrives many times a minute.
func TestChaseNewStoreDeduplicates(t *testing.T) {
s := newChaseNewStore()
t0 := time.Date(2026, 8, 14, 12, 0, 0, 0, time.UTC)
row := ChaseNewSpot{Call: "VK9XX", Band: "20m", Mode: "FT8", At: t0.Format(time.RFC3339)}
if !s.put(row, t0) {
t.Fatal("the first sighting was rejected")
}
if s.put(row, t0.Add(2*time.Minute)) {
t.Error("the same station on the same band and mode was listed twice")
}
// A different band is a different opportunity — a new-band slot is exactly
// what an operator is watching for.
other := row
other.Band = "15m"
if !s.put(other, t0.Add(2*time.Minute)) {
t.Error("the same station on another band was suppressed")
}
// Once the window has passed it is worth showing again: the station is still
// there, and the row it had has aged out of the list.
if !s.put(row, t0.Add(chaseSeenTTL+time.Minute)) {
t.Error("the station never came back after the de-duplication window")
}
}
// The list is aged with the operator's own spot lifetime, so a station heard an
// hour ago is not offered as something to chase now.
func TestChaseNewStoreAgesOut(t *testing.T) {
s := newChaseNewStore()
t0 := time.Date(2026, 8, 14, 12, 0, 0, 0, time.UTC)
s.put(ChaseNewSpot{Call: "OLD", Band: "20m", Mode: "FT8", At: t0.Format(time.RFC3339)}, t0)
s.put(ChaseNewSpot{Call: "NEW", Band: "20m", Mode: "FT8",
At: t0.Add(20 * time.Minute).Format(time.RFC3339)}, t0.Add(20*time.Minute))
got := s.list(15*time.Minute, t0.Add(21*time.Minute))
if len(got) != 1 || got[0].Call != "NEW" {
t.Fatalf("got %+v, want only NEW", got)
}
// Newest first: an operator reads the top of this list and nothing else.
s.put(ChaseNewSpot{Call: "NEWEST", Band: "20m", Mode: "FT8",
At: t0.Add(25 * time.Minute).Format(time.RFC3339)}, t0.Add(25*time.Minute))
got = s.list(15*time.Minute, t0.Add(26*time.Minute))
if len(got) != 2 || got[0].Call != "NEWEST" {
t.Fatalf("got %+v, want NEWEST first", got)
}
}
// The list is bounded. A widget that grows without limit under a 6 m opening
// costs memory for rows nobody will ever scroll to.
func TestChaseNewStoreIsBounded(t *testing.T) {
s := newChaseNewStore()
t0 := time.Date(2026, 8, 14, 12, 0, 0, 0, time.UTC)
for i := 0; i < chaseNewMax+50; i++ {
at := t0.Add(time.Duration(i) * time.Second)
s.put(ChaseNewSpot{
Call: "S" + time.Duration(i).String(), Band: "20m", Mode: "FT8",
At: at.Format(time.RFC3339),
}, at)
}
if got := len(s.list(0, t0.Add(time.Hour))); got != chaseNewMax {
t.Errorf("kept %d rows, want the %d cap", got, chaseNewMax)
}
}
// The panel is for stations an operator can call. WSPR is a beacon — nobody
// answers one — and on a quiet band it would be most of the list.
func TestChaseModesExcludeBeaconsAndOddities(t *testing.T) {
for _, m := range []string{"FT8", "FT4", "FT2", "PSK31", "RTTY"} {
if !chaseModes[m] {
t.Errorf("%s should be listed", m)
}
}
for _, m := range []string{"WSPR", "JT65", "JT9", "FST4W", "Q65", "JS8", "MSK144", "OLIVIA", ""} {
if chaseModes[m] {
t.Errorf("%s should not be listed", m)
}
}
}
// A decode on a band the operator does not have is not an opportunity, it is a
// row in the way — PSK Reporter carries microwave segments nobody in the region
// is equipped for.
func TestChaseBandAllowed(t *testing.T) {
a := &App{}
// Nothing loaded yet: show rather than swallow, or the panel would look
// broken for the first seconds after it is switched on.
if !a.chaseBandAllowed("13cm") {
t.Error("with no band list loaded, nothing should be filtered")
}
a.chaseBands = map[string]bool{"20m": true, "40m": true, "6m": true}
for _, b := range []string{"20m", "40m", "6m", " 20M "} {
if !a.chaseBandAllowed(b) {
t.Errorf("%q is in the operator's list and was dropped", b)
}
}
for _, b := range []string{"13cm", "23cm", "2m", "630m", ""} {
if a.chaseBandAllowed(b) {
t.Errorf("%q is not in the operator's list and was kept", b)
}
}
}