7ace2cc602
Backend (Go 1.25 / Wails v2): - QSO storage on SQLite (modernc) with embedded migrations (0001..0005) - Streaming ADIF import (batch insert) + WorkedBefore per callsign and DXCC - Callsign lookup with QRZ.com + HamQTH providers (primary/failsafe routing) and SQLite-backed TTL cache - DXCC resolver from cty.dat (auto-download, longest-prefix-match) - Multi-profile operator identities (home/portable/SOTA/contest) — every QSO stamps MY_* from the active profile - CAT control via OmniRig COM on a single OS-locked goroutine, with bidirectional sync (freq/mode/band/split/VFOs) and Rig1/Rig2 hot-swap - Settings store (key/value), CAT debug log at %APPDATA%/HamLog/cat.log Frontend (React 18 + TypeScript + Tailwind v4 + shadcn-style): - Single-row entry strip with CAT-aware band/mode/freq, RST, Start/End UTC, per-field locks (band/mode/freq/start/end) for backdated QSOs - Topbar: live freq (MHz.kHz.Hz dotted), live UTC, band/mode/SPLIT badges, CAT pill with rig selector and clickable Azimuth pill (rotor TODO) - Settings tree: Profiles (Log4OM-style manager), Station Information (edits the active profile), unified Callsign Lookup with Test buttons, Bands/Modes lists, CAT - Worked-before matrix (band × mode × class) with new-DXCC highlighting - ADIF import from menu + Maintenance > Refresh cty.dat Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
85 lines
2.0 KiB
Go
85 lines
2.0 KiB
Go
package adif
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestParseSimple(t *testing.T) {
|
|
src := `Header text here
|
|
Generated by HamLog
|
|
<EOH>
|
|
<CALL:5>F4XYZ<BAND:3>20m<MODE:3>SSB<QSO_DATE:8>20240101<TIME_ON:6>123456<EOR>
|
|
<call:4>K1AB<band:3>40m<mode:2>CW<qso_date:8>20240102<time_on:4>0930<eor>
|
|
`
|
|
var got []Record
|
|
err := Parse(strings.NewReader(src), func(r Record) error {
|
|
got = append(got, r)
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("parse: %v", err)
|
|
}
|
|
if len(got) != 2 {
|
|
t.Fatalf("want 2 records, got %d", len(got))
|
|
}
|
|
if got[0]["call"] != "F4XYZ" || got[0]["band"] != "20m" || got[0]["mode"] != "SSB" {
|
|
t.Errorf("record 0 mismatch: %+v", got[0])
|
|
}
|
|
if got[1]["call"] != "K1AB" || got[1]["time_on"] != "0930" {
|
|
t.Errorf("record 1 mismatch: %+v", got[1])
|
|
}
|
|
}
|
|
|
|
func TestParseValueWithAngleBracket(t *testing.T) {
|
|
// Length-prefixed value can contain '<' and '>' bytes.
|
|
src := `<EOH><CALL:5>F4XYZ<COMMENT:7>a<b>c<d<EOR>`
|
|
var got []Record
|
|
err := Parse(strings.NewReader(src), func(r Record) error {
|
|
got = append(got, r)
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("parse: %v", err)
|
|
}
|
|
if len(got) != 1 {
|
|
t.Fatalf("want 1, got %d", len(got))
|
|
}
|
|
if got[0]["comment"] != "a<b>c<d" {
|
|
t.Errorf("comment mismatch: %q", got[0]["comment"])
|
|
}
|
|
}
|
|
|
|
func TestParseNoHeader(t *testing.T) {
|
|
// Some loggers omit the header entirely — records before <EOH> are
|
|
// discarded by design. Verify nothing is emitted in that case.
|
|
src := `<CALL:5>F4XYZ<EOR>`
|
|
var got int
|
|
err := Parse(strings.NewReader(src), func(r Record) error {
|
|
got++
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("parse: %v", err)
|
|
}
|
|
if got != 0 {
|
|
t.Errorf("expected 0 records without <EOH>, got %d", got)
|
|
}
|
|
}
|
|
|
|
func TestParseTypedField(t *testing.T) {
|
|
// <FIELD:LEN:TYPE> form (e.g. <FREQ:6:N>).
|
|
src := `<EOH><CALL:5>F4XYZ<FREQ:6:N>14.250<EOR>`
|
|
var got Record
|
|
err := Parse(strings.NewReader(src), func(r Record) error {
|
|
got = r
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("parse: %v", err)
|
|
}
|
|
if got["freq"] != "14.250" {
|
|
t.Errorf("freq mismatch: %q", got["freq"])
|
|
}
|
|
}
|