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.
112 lines
4.1 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|