135 lines
4.6 KiB
Go
135 lines
4.6 KiB
Go
package pskrtgt
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// feed builds a watcher with a window already populated, so the analysis can be
|
|
// pinned without a broker.
|
|
func feed(target string, spots ...spot) *Watcher {
|
|
w := New(Config{MyCall: "F4BPO", MyGrid: "JN36BQ"})
|
|
w.target, w.mode, w.dialHz = target, "FT8", 14_074_000
|
|
w.spots = spots
|
|
return w
|
|
}
|
|
|
|
func rep(tx, txGrid, rx, rxGrid string, snr int, offset int, ago time.Duration) spot {
|
|
return spot{
|
|
TxCall: tx, TxGrid: txGrid, RxCall: rx, RxGrid: rxGrid,
|
|
SNR: snr, Freq: 14_074_000 + int64(offset), at: time.Now().Add(-ago),
|
|
}
|
|
}
|
|
|
|
func TestHeardYouIsTheOperatorsOwnCallOnly(t *testing.T) {
|
|
w := feed("YI5RLS",
|
|
rep("F4BPO", "JN36", "YI5RLS", "LM43", -14, 1200, 20*time.Second),
|
|
rep("F4XYZ", "JN36", "YI5RLS", "LM43", -8, 900, 30*time.Second),
|
|
)
|
|
a := w.Snapshot()
|
|
if !a.HeMe {
|
|
t.Fatal("the DX decoded the operator and the panel says he did not")
|
|
}
|
|
if a.HeMeSNR != -14 || a.HeMeOffset != 1200 {
|
|
t.Errorf("he_me = %d dB @ %d Hz, want -14 dB @ 1200 Hz", a.HeMeSNR, a.HeMeOffset)
|
|
}
|
|
// The operator is not part of the pileup he is calling into: counting
|
|
// yourself as competition is how a "1 caller" band looks contested.
|
|
if a.PileupCount != 1 {
|
|
t.Errorf("pileup = %d, want 1 (the other station only)", a.PileupCount)
|
|
}
|
|
// F4XYZ shares the operator's Maidenhead field, so the path from this
|
|
// region is demonstrably open.
|
|
if !a.PathOpen || a.FromMyAreaCount != 1 {
|
|
t.Errorf("from my area = %d (open=%v), want 1 open", a.FromMyAreaCount, a.PathOpen)
|
|
}
|
|
}
|
|
|
|
func TestNearHimNeedsHisSquareAndTheOperatorsCall(t *testing.T) {
|
|
// He transmits (so his square is known), and a station in that square hears
|
|
// the operator. He himself has decoded nobody.
|
|
w := feed("YI5RLS",
|
|
rep("YI5RLS", "LM43", "OH5CX", "KP30", -3, 0, 40*time.Second),
|
|
rep("F4BPO", "JN36", "YI9XY", "LM43CC", -19, 1500, 25*time.Second),
|
|
)
|
|
a := w.Snapshot()
|
|
if a.TargetGrid != "LM43" {
|
|
t.Fatalf("his square = %q, want LM43", a.TargetGrid)
|
|
}
|
|
if a.NearHimCount != 1 || len(a.NearHimTop) != 1 || a.NearHimTop[0].Call != "YI9XY" {
|
|
t.Errorf("near him = %d %v, want the one receiver in his square", a.NearHimCount, a.NearHimTop)
|
|
}
|
|
// He uploads nothing: the panel must be able to say so, or an operator
|
|
// reads an empty panel as a closed band.
|
|
if a.TargetUploads {
|
|
t.Error("he received nothing in the window, yet the panel claims he uploads")
|
|
}
|
|
if a.HeMe {
|
|
t.Error("nobody reported HIM decoding the operator")
|
|
}
|
|
}
|
|
|
|
func TestWindowDropsWhatIsTooOld(t *testing.T) {
|
|
// Inside the window: a report from six minutes ago is still evidence. Five
|
|
// minutes was too short — measured against DXHunter on the same station at
|
|
// the same moment, it hid a third of the decodes and a co-area station.
|
|
w := feed("YI5RLS",
|
|
rep("F4BPO", "JN36", "YI5RLS", "LM43", -14, 1200, 6*time.Minute),
|
|
)
|
|
if a := w.Snapshot(); !a.HeMe {
|
|
t.Errorf("a six-minute-old report was dropped from a ten-minute window: %+v", a)
|
|
}
|
|
// Past it, it goes.
|
|
w = feed("YI5RLS",
|
|
rep("F4BPO", "JN36", "YI5RLS", "LM43", -14, 1200, 11*time.Minute),
|
|
)
|
|
if a := w.Snapshot(); a.HeMe || a.Spots != 0 {
|
|
t.Errorf("an eleven-minute-old report survived: %+v", a)
|
|
}
|
|
}
|
|
|
|
func TestSuggestOffsetAvoidsTheOccupiedBinsAndTheCeiling(t *testing.T) {
|
|
// Busy from 1000 to 1500 Hz, empty from 1560 to 2400, ceiling 2400.
|
|
bins := []Bin{}
|
|
for hz := 1020; hz <= 1500; hz += binHz {
|
|
bins = append(bins, Bin{OffsetHz: hz, Count: 3})
|
|
}
|
|
bins = append(bins, Bin{OffsetHz: 2400, Count: 1})
|
|
got := suggestOffset(bins, 2400)
|
|
if got < 1620 || got > 2340 {
|
|
t.Errorf("suggested %d Hz, want somewhere in the empty 1560-2400 run", got)
|
|
}
|
|
// A passband that stops low must not produce advice above it: transmitting
|
|
// past the DX's filter is the one outcome worse than picking a busy slot.
|
|
if got := suggestOffset(bins, 1500); got != 0 {
|
|
t.Errorf("suggested %d Hz with a 1500 Hz ceiling and no room, want none", got)
|
|
}
|
|
if got := suggestOffset(nil, 0); got != 0 {
|
|
t.Errorf("suggested %d Hz with no data at all, want none", got)
|
|
}
|
|
}
|
|
|
|
func TestTopicsFollowTheScope(t *testing.T) {
|
|
w := New(Config{MyCall: "F4BPO", Scope: ScopeTarget})
|
|
w.target, w.mode, w.band = "YI5RLS", "FT8", "20m"
|
|
got := w.topicsLocked()
|
|
want := []string{
|
|
"pskr/filter/v2/+/FT8/YI5RLS/#",
|
|
"pskr/filter/v2/+/FT8/+/YI5RLS/#",
|
|
"pskr/filter/v2/+/FT8/F4BPO/#",
|
|
}
|
|
if len(got) != len(want) {
|
|
t.Fatalf("narrow scope = %v, want %v", got, want)
|
|
}
|
|
for i := range want {
|
|
if got[i] != want[i] {
|
|
t.Errorf("filter %d = %q, want %q", i, got[i], want[i])
|
|
}
|
|
}
|
|
|
|
w.cfg.Scope = ScopeBand
|
|
if got := w.topicsLocked(); len(got) != 1 || got[0] != "pskr/filter/v2/20m/FT8/#" {
|
|
t.Errorf("band scope = %v, want the one band-wide filter", got)
|
|
}
|
|
}
|