Files
OpsLog/internal/dxped/dxped_test.go
T
rouggy 187ac9aa84 feat(dxped): the announced DX, judged against your own log
A DXpeditions tab holding the two feeds the DX world announces itself
on: NG3K's ADXO (structured — dates, entity, calls, bands, modes, QSL
route) and DX-World's headlines. What a logger can say that a news
reader cannot is whether the operation is worth chasing, so every
announcement is put through the SAME verdict the cluster paints on a
spot — one badge, strongest wins, dimmed when the need is only a missing
QSL — with an 'only what I need' filter. One click watches every
callsign of an operation; the news headlines are mined for callsigns so
they can be watched the same way.

Parsers ported from DXHunter's and pinned with table tests against real
feed text: the 'as PJ2/W2APF' mining, the DXCC-prefix-first
normalisation without which no spot ever matches, both ADXO date forms,
and the '160-6m' span whose unit is written once (DXHunter read that as
6m alone and lost the low end).

Watchlist membership now announces itself app-wide, on the same card as
a new version: the bindings emit watchlist:changed, so a call added from
the cluster or this tab is confirmed where the operator is actually
looking — the panel's own inline message never was.
2026-08-31 17:51:45 +02:00

87 lines
3.0 KiB
Go

package dxped
import (
"reflect"
"testing"
"time"
)
// Pinned against REAL feed text: the parser reads a fixed sentence, and a
// wrong split silently empties a DXpedition list rather than failing loudly.
func TestParseActivation(t *testing.T) {
desc := "Aug 24-31, 2026 -- St Kitts and Nevis -- V47JA -- QSL: LoTW -- " +
"Source: W5JON (Aug 1, 2026) -- By W5JON as V47JA fm Calypso Bay; 160-6m; SSB FT8; yagi, verticals"
a := parseActivation(desc, "https://www.qrz.com/lookup/v47ja")
if a == nil {
t.Fatal("parseActivation returned nil on a real ADXO description")
}
if a.DXCC != "St Kitts and Nevis" {
t.Errorf("DXCC = %q", a.DXCC)
}
if a.Callsign != "V47JA" {
t.Errorf("Callsign = %q, want V47JA (mined from 'as')", a.Callsign)
}
if a.QSL != "LoTW" {
t.Errorf("QSL = %q", a.QSL)
}
if a.StartDate != "Aug 24, 2026" || a.EndDate != "Aug 31, 2026" {
t.Errorf("dates = %q..%q", a.StartDate, a.EndDate)
}
if want := []string{"160m", "6m"}; !reflect.DeepEqual(a.Bands, want) {
t.Errorf("Bands = %v, want %v", a.Bands, want)
}
if want := []string{"SSB", "FT8"}; !reflect.DeepEqual(a.Modes, want) {
t.Errorf("Modes = %v, want %v", a.Modes, want)
}
}
// The callsign column often holds only the prefix; the operators prose holds
// the real thing, and a slashed call must lead with the DXCC prefix or no spot
// will ever match it.
func TestCallsAfterAsAndNormalise(t *testing.T) {
if got := callsAfterAs("SQ2RAD as VP2EAD, M0PLX as VP2ELX"); !reflect.DeepEqual(got, []string{"VP2EAD", "VP2ELX"}) {
t.Errorf("callsAfterAs = %v", got)
}
if got := normalizeCall("JG8NQJ/JD1", "JD1"); got != "JD1/JG8NQJ" {
t.Errorf("normalizeCall = %q, want JD1/JG8NQJ", got)
}
if got := normalizeCall("PJ2/W2APF", "PJ2"); got != "PJ2/W2APF" {
t.Errorf("normalizeCall rewrote an already-correct call: %q", got)
}
}
func TestActivationStatus(t *testing.T) {
now := time.Date(2026, 8, 27, 12, 0, 0, 0, time.UTC)
cases := []struct{ start, end, want string }{
{"Aug 24, 2026", "Aug 31, 2026", "active"},
{"Sep 10, 2026", "Sep 20, 2026", "upcoming"},
{"Aug 1, 2026", "Aug 10, 2026", "ended"},
{"Aug 24, 2026", "Aug 27, 2026", "active"}, // ends TODAY: still on
{"garbage", "garbage", "upcoming"},
}
for _, c := range cases {
if got := activationStatus(c.start, c.end, now); got != c.want {
t.Errorf("activationStatus(%q,%q) = %q, want %q", c.start, c.end, got, c.want)
}
}
}
// Mining a headline must find calls without inventing them out of jargon.
func TestCallsInHeadline(t *testing.T) {
cases := []struct {
title string
want []string
}{
{"3B7M, St Brandon", []string{"3B7M"}},
{"TX5S team lands on Clipperton", []string{"TX5S"}},
{"FT8 activity on 160m in 2026", nil},
{"VP6D QSL via OQRS, LoTW", []string{"VP6D"}},
{"JD1/JG8NQJ from Minami Torishima", []string{"JD1/JG8NQJ"}},
}
for _, c := range cases {
if got := callsInHeadline(c.title); !reflect.DeepEqual(got, c.want) {
t.Errorf("callsInHeadline(%q) = %v, want %v", c.title, got, c.want)
}
}
}