223 lines
8.4 KiB
Go
223 lines
8.4 KiB
Go
package award
|
|
|
|
import (
|
|
"sort"
|
|
"testing"
|
|
|
|
"hamlog/internal/qso"
|
|
)
|
|
|
|
func TestWPXPrefix(t *testing.T) {
|
|
cases := map[string]string{
|
|
"F4BPO": "F4",
|
|
"EA8ABC": "EA8",
|
|
"9A1AA": "9A1",
|
|
"OH2BH": "OH2",
|
|
"K1ABC": "K1",
|
|
"RAEM": "RA0",
|
|
"F4BPO/P": "F4",
|
|
"F4BPO/9": "F9",
|
|
"VP8/F4BPO": "VP8",
|
|
"PA0XYZ": "PA0",
|
|
}
|
|
for in, want := range cases {
|
|
if got := wpxPrefix(in); got != want {
|
|
t.Errorf("wpxPrefix(%q) = %q, want %q", in, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func ip(n int) *int { return &n }
|
|
|
|
func TestComputeDXCCAndConfirm(t *testing.T) {
|
|
qsos := []qso.QSO{
|
|
{Callsign: "K1ABC", Band: "20m", DXCC: ip(291), State: "MA", LOTWRcvd: "Y"},
|
|
{Callsign: "K2DEF", Band: "40m", DXCC: ip(291), State: "NY"}, // worked, not confirmed
|
|
{Callsign: "DL1XYZ", Band: "20m", DXCC: ip(230), QSLRcvd: "Y"}, // DXCC Germany confirmed
|
|
{Callsign: "F4BPO", Band: "20m", DXCC: ip(227), Notes: "nice qso D74", EQSLRcvd: "Y"}, // France dept 74 in note
|
|
{Callsign: "F5ABC", Band: "40m", DXCC: ip(227), Notes: "D2A Corsica", QSLRcvd: "Y"}, // France dept 2A, confirmed
|
|
}
|
|
res := Compute(Defaults(), qsos, nil, nil)
|
|
by := map[string]Result{}
|
|
for _, r := range res {
|
|
by[r.Code] = r
|
|
}
|
|
|
|
dxcc := by["DXCC"]
|
|
if dxcc.Worked != 3 { // USA, Germany, France
|
|
t.Errorf("DXCC worked = %d, want 3", dxcc.Worked)
|
|
}
|
|
// DXCC confirms on lotw|qsl → USA(lotw) + Germany(qsl) + France(qsl via F5ABC).
|
|
if dxcc.Confirmed != 3 {
|
|
t.Errorf("DXCC confirmed = %d, want 3", dxcc.Confirmed)
|
|
}
|
|
// Validated is the stricter LoTW-only tier: a paper QSL confirms but does
|
|
// NOT validate, so only USA (LoTW) counts.
|
|
if dxcc.Validated != 1 {
|
|
t.Errorf("DXCC validated = %d, want 1 (LoTW only, QSL doesn't validate)", dxcc.Validated)
|
|
}
|
|
|
|
was := by["WAS"]
|
|
if was.Worked != 2 { // MA, NY only (France excluded by DXCC filter)
|
|
t.Errorf("WAS worked = %d, want 2", was.Worked)
|
|
}
|
|
|
|
// DDFM scans the Note field with pattern D(\d{1,2}[AB]?): 74 and 2A.
|
|
ddfm := by["DDFM"]
|
|
if ddfm.Worked != 2 {
|
|
t.Errorf("DDFM worked = %d, want 2 (refs %v)", ddfm.Worked, refCodes(ddfm))
|
|
}
|
|
if ddfm.Confirmed != 1 { // 2A confirmed via QSL; 74 only eQSL (not accepted)
|
|
t.Errorf("DDFM confirmed = %d, want 1", ddfm.Confirmed)
|
|
}
|
|
}
|
|
|
|
func TestNatLess(t *testing.T) {
|
|
in := []string{"10", "2", "1", "20", "3", "D10", "D2A", "D2", "AL", "AK"}
|
|
want := []string{"1", "2", "3", "10", "20", "AK", "AL", "D2", "D2A", "D10"}
|
|
sort.Slice(in, func(i, j int) bool { return natLess(in[i], in[j]) })
|
|
for i := range want {
|
|
if in[i] != want[i] {
|
|
t.Fatalf("natLess order = %v, want %v", in, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// A multi-reference field (n-fer POTA) counts each park separately.
|
|
func TestComputeMultiRef(t *testing.T) {
|
|
def := Def{Code: "POTA", Type: TypeReference, Field: "pota_ref", Dynamic: true, Confirm: []string{"lotw", "qsl"}, Valid: true}
|
|
qsos := []qso.QSO{
|
|
{Callsign: "W2QMI", Band: "20m", POTARef: "US-6544,US-0680", LOTWRcvd: "Y"},
|
|
{Callsign: "K1ABC", Band: "40m", POTARef: "US-0680"}, // shared park
|
|
}
|
|
r := Compute([]Def{def}, qsos, nil, nil)[0]
|
|
if r.Worked != 2 { // distinct parks: US-6544, US-0680
|
|
t.Errorf("POTA worked = %d, want 2 (%v)", r.Worked, refCodes(r))
|
|
}
|
|
}
|
|
|
|
// WAJA-style award: MatchBy="description", non-exact, scanning the QTH for a
|
|
// reference's NAME (the prefecture). Also guards against the nil-slice crash:
|
|
// an award with nothing worked must return empty (non-nil) Refs/Bands.
|
|
func TestComputeMatchByDescription(t *testing.T) {
|
|
def := Def{Code: "WAJA", Type: TypeQSOFields, Field: "qth", MatchBy: "description",
|
|
DXCCFilter: []int{339}, Confirm: []string{"lotw", "qsl"}, Valid: true}
|
|
qsos := []qso.QSO{
|
|
{Callsign: "JA1ABC", Band: "20m", DXCC: ip(339), QTH: "Tokyo city", LOTWRcvd: "Y"},
|
|
{Callsign: "JA3DEF", Band: "40m", DXCC: ip(339), QTH: "Osaka"},
|
|
{Callsign: "JA9XYZ", Band: "20m", DXCC: ip(339), QTH: "nowhere special"}, // no prefecture name
|
|
}
|
|
refMetas := map[string][]RefMeta{"WAJA": {
|
|
{Code: "100", Name: "Tokyo", Valid: true},
|
|
{Code: "270", Name: "Osaka", Valid: true},
|
|
{Code: "010", Name: "Hokkaido", Valid: true},
|
|
}}
|
|
r := Compute([]Def{def}, qsos, refMetas, nil)[0]
|
|
if r.Worked != 2 { // Tokyo + Osaka found by name inside QTH
|
|
t.Errorf("WAJA worked = %d, want 2 (%v)", r.Worked, refCodes(r))
|
|
}
|
|
if r.Total != 3 { // predefined denominator = list size
|
|
t.Errorf("WAJA total = %d, want 3", r.Total)
|
|
}
|
|
|
|
// Nil-slice guard: an award with zero worked refs must still return
|
|
// non-nil (empty) Refs/Bands so the JSON isn't null (UI white-screen).
|
|
empty := Compute([]Def{{Code: "WWFF", Type: TypeReference, Field: "wwff", Dynamic: true, Valid: true}}, nil, nil, nil)[0]
|
|
if empty.Refs == nil || empty.Bands == nil {
|
|
t.Errorf("empty award must have non-nil Refs/Bands, got Refs=%v Bands=%v", empty.Refs, empty.Bands)
|
|
}
|
|
}
|
|
|
|
func refCodes(r Result) []string {
|
|
out := make([]string, 0, len(r.Refs))
|
|
for _, rf := range r.Refs {
|
|
out = append(out, rf.Ref)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// Legacy defs (Type=="", Valid==false, old DDFM pattern) get upgraded.
|
|
func TestMigrate(t *testing.T) {
|
|
legacy := []Def{
|
|
{Code: "DXCC", Field: "dxcc", Confirm: []string{"lotw", "qsl"}, Total: 340}, // Valid=false, Type=""
|
|
{Code: "DDFM", Field: "note", Pattern: `(?i)\bD(\d{1,2}[AB]?)\b`, Total: 96},
|
|
{Code: "MYAWARD", Field: "note"}, // custom legacy
|
|
}
|
|
out, changed := Migrate(legacy)
|
|
if !changed {
|
|
t.Fatal("expected migration to change legacy defs")
|
|
}
|
|
for _, d := range out {
|
|
if !d.Valid {
|
|
t.Errorf("%s should be enabled after migration", d.Code)
|
|
}
|
|
if d.Type == "" {
|
|
t.Errorf("%s should have a type after migration", d.Code)
|
|
}
|
|
}
|
|
if out[1].Pattern != `(?i)\b(D\d{1,2}[AB]?)\b` {
|
|
t.Errorf("DDFM pattern not fixed: %q", out[1].Pattern)
|
|
}
|
|
if out[2].Type != TypeQSOFields {
|
|
t.Errorf("custom legacy award type = %q, want QSOFIELDS", out[2].Type)
|
|
}
|
|
// Idempotent: a second pass changes nothing.
|
|
if _, changed2 := Migrate(out); changed2 {
|
|
t.Error("migration should be idempotent")
|
|
}
|
|
}
|
|
|
|
// Regression: a predefined reference whose own DXCC differs from the QSO's
|
|
// entity must still count when the field code matches and the award-level
|
|
// DXCC filter allows it (WAS Alaska: state AK, but DXCC entity 6, not 291).
|
|
func TestComputePredefinedCrossDXCC(t *testing.T) {
|
|
def := Def{Code: "WAS", Type: TypeQSOFields, Field: "state", ExactMatch: true,
|
|
DXCCFilter: []int{291, 110, 6}, Confirm: []string{"lotw", "qsl"}, Valid: true}
|
|
qsos := []qso.QSO{
|
|
{Callsign: "KL5DX", Band: "20m", DXCC: ip(6), State: "AK", LOTWRcvd: "Y"}, // Alaska
|
|
{Callsign: "K1ABC", Band: "20m", DXCC: ip(291), State: "MA"}, // continental
|
|
}
|
|
refMetas := map[string][]RefMeta{"WAS": {
|
|
{Code: "AK", Name: "Alaska", DXCCList: []int{291}, Valid: true}, // wrong DXCC on purpose
|
|
{Code: "MA", Name: "Massachusetts", DXCCList: []int{291}, Valid: true},
|
|
}}
|
|
r := Compute([]Def{def}, qsos, refMetas, nil)[0]
|
|
if r.Worked != 2 {
|
|
t.Errorf("WAS worked = %d, want 2 (Alaska must count despite DXCC 6) %v", r.Worked, refCodes(r))
|
|
}
|
|
}
|
|
|
|
// A predefined award only counts references present in its list, lists the
|
|
// unworked ones too, and uses the list size as the denominator.
|
|
func TestComputePredefinedList(t *testing.T) {
|
|
def := Def{Code: "RAC", Name: "RAC", Type: TypeQSOFields, Field: "state", ExactMatch: true,
|
|
DXCCFilter: []int{1}, Confirm: []string{"lotw", "qsl"}, Validate: []string{"lotw"}, Valid: true}
|
|
qsos := []qso.QSO{
|
|
{Callsign: "VE3AAA", Band: "20m", DXCC: ip(1), State: "ON", LOTWRcvd: "Y"}, // worked+confirmed+validated
|
|
{Callsign: "VE7BBB", Band: "40m", DXCC: ip(1), State: "BC", QSLRcvd: "Y"}, // worked+confirmed (not validated)
|
|
{Callsign: "VE9CCC", Band: "20m", DXCC: ip(1), State: "ZZ"}, // not a real province → ignored
|
|
{Callsign: "K1ABC", Band: "20m", DXCC: ip(291), State: "MA"}, // wrong DXCC → ignored
|
|
}
|
|
refMetas := map[string][]RefMeta{"RAC": {
|
|
{Code: "ON", Name: "Ontario", Valid: true},
|
|
{Code: "BC", Name: "British Columbia", Valid: true},
|
|
{Code: "AB", Name: "Alberta", Valid: true},
|
|
}}
|
|
r := Compute([]Def{def}, qsos, refMetas, nil)[0]
|
|
if r.Worked != 2 {
|
|
t.Errorf("RAC worked = %d, want 2 (%v)", r.Worked, refCodes(r))
|
|
}
|
|
if r.Confirmed != 2 {
|
|
t.Errorf("RAC confirmed = %d, want 2", r.Confirmed)
|
|
}
|
|
if r.Validated != 1 { // only ON via LoTW
|
|
t.Errorf("RAC validated = %d, want 1", r.Validated)
|
|
}
|
|
if r.Total != 3 { // denominator = list size
|
|
t.Errorf("RAC total = %d, want 3", r.Total)
|
|
}
|
|
if len(r.Refs) != 3 { // ON, BC worked + AB unworked
|
|
t.Errorf("RAC refs = %d, want 3 (%v)", len(r.Refs), refCodes(r))
|
|
}
|
|
}
|