Files
OpsLog/gridscope_test.go
T
2026-08-20 17:53:55 +02:00

127 lines
4.9 KiB
Go

package main
import (
"testing"
"hamlog/internal/qso"
)
// idx builds a worked-grid index the way GridWorkedIndex does, so the test
// exercises the real key shape rather than a convenient one.
func idx(entries ...struct {
grid, band, mode string
confirmed bool
}) map[string]bool {
m := map[string]bool{}
for _, e := range entries {
for _, cls := range gridClassesOf(e.mode) {
for _, k := range []string{qso.GridKey(e.grid, e.band, cls), qso.GridKey(e.grid, "", cls)} {
m[k] = m[k] || e.confirmed
}
}
}
return m
}
type entry = struct {
grid, band, mode string
confirmed bool
}
// The six scopes are six different questions, and the whole point of offering
// them is that the same square answers differently.
func TestGridScopes(t *testing.T) {
// JN36 worked once: 20 m, FT8, not confirmed.
i := idx(entry{"JN36", "20m", "FT8", false})
for _, tc := range []struct {
scope string
band string
mode string
want bool
why string
}{
{"band_ftx", "20m", "FT8", false, "same band, same family — already mine"},
{"band_ftx", "15m", "FT8", true, "another band, and the scope counts bands"},
{"mix_ftx", "15m", "FT8", false, "another band, but this scope does not care"},
{"band_mode", "20m", "FT4", true, "same band, but FT4 is not FT8 under an exact-mode scope"},
{"band_ftx", "20m", "FT4", false, "same band, and FT4 is in the same FT family"},
{"band_digi", "20m", "RTTY", false, "digital counts every digital mode"},
{"band_ftx", "20m", "RTTY", false, "RTTY is outside an FTx scope — it is not evidence either way, so nothing is flagged"},
{"mix_digi", "15m", "FT4", false, "the widest scope: worked anywhere, any digital"},
} {
got := gridIsWanted(i, lookupGridScope(tc.scope), "new", "JN36", tc.band, tc.mode)
if got != tc.want {
t.Errorf("scope %s, %s %s → wanted=%v, want %v — %s", tc.scope, tc.band, tc.mode, got, tc.want, tc.why)
}
}
}
// "New + unconfirmed" chases a square that is worked but not yet confirmed: it
// is still missing from the award, which is the only sense in which it is done.
func TestGridHuntUnconfirmed(t *testing.T) {
unconf := idx(entry{"JN36", "20m", "FT8", false})
conf := idx(entry{"IO91", "20m", "FT8", true})
if gridIsWanted(unconf, lookupGridScope("band_ftx"), "new", "JN36", "20m", "FT8") {
t.Error("worked-but-unconfirmed must NOT be wanted when hunting only what is new")
}
if !gridIsWanted(unconf, lookupGridScope("band_ftx"), "new_unconfirmed", "JN36", "20m", "FT8") {
t.Error("worked-but-unconfirmed must BE wanted when hunting unconfirmed too")
}
if gridIsWanted(conf, lookupGridScope("band_ftx"), "new_unconfirmed", "IO91", "20m", "FT8") {
t.Error("a confirmed square is finished under either hunt")
}
}
// A mode the scope cannot judge flags nothing rather than guessing: a phone
// contact under an FTx scope is not evidence either way.
func TestGridScopeIgnoresModesItCannotJudge(t *testing.T) {
i := idx(entry{"JN36", "20m", "FT8", false})
for _, mode := range []string{"SSB", "CW", ""} {
if gridIsWanted(i, lookupGridScope("band_ftx"), "new", "KO01", "20m", mode) {
t.Errorf("mode %q is outside an FTx scope — nothing should be flagged", mode)
}
}
}
// An unknown or unset scope falls back rather than answering "wanted" for
// everything, which is what an empty class would have done.
func TestGridScopeFallsBack(t *testing.T) {
if got := lookupGridScope("").Key; got != defaultGridScope {
t.Errorf("unset scope = %q, want the default %q", got, defaultGridScope)
}
if got := lookupGridScope("nonsense").Key; got != defaultGridScope {
t.Errorf("unknown scope = %q, want the default %q", got, defaultGridScope)
}
if got := gridHunt("nonsense"); got != "new" {
t.Errorf("unknown hunt = %q, want new", got)
}
}
// The two wanted states must be told apart, not merged. A square worked an hour
// ago and awaiting a QSL is a different job from one never worked, and reporting
// both as plain NEW GRID is what made a station the log plainly shows as worked
// still wear the badge.
func TestGridStateSeparatesNewFromUnconfirmed(t *testing.T) {
i := idx(
entry{"JN74", "20m", "FT8", false}, // worked, no confirmation yet
entry{"IO91", "20m", "FT8", true}, // worked and confirmed
)
s := lookupGridScope("band_ftx")
if got := gridStateFor(i, s, "new_unconfirmed", "KO99", "20m", "FT8"); got != gridStateNew {
t.Errorf("never worked → %q, want %q", got, gridStateNew)
}
if got := gridStateFor(i, s, "new_unconfirmed", "JN74", "20m", "FT8"); got != gridStateUnconf {
t.Errorf("worked but unconfirmed → %q, want %q", got, gridStateUnconf)
}
if got := gridStateFor(i, s, "new_unconfirmed", "IO91", "20m", "FT8"); got != "" {
t.Errorf("confirmed → %q, want nothing to chase", got)
}
// Under the plain "new" hunt an unconfirmed square is finished business.
if got := gridStateFor(i, s, "new", "JN74", "20m", "FT8"); got != "" {
t.Errorf("unconfirmed under the new-only hunt → %q, want nothing", got)
}
}