Reported with screenshots: a dozen records lifted out of wsjtx_log.adi into a new .adi imported as "0 imported, 0 ignored, 0 total". The parser only emitted records after <EOH>, and a hand-assembled file has no header — so the whole file was read as header text and thrown away. The count says the file was empty; there is nothing on screen to suggest a missing tag, which is why this was reported as "import does nothing". An <eor> is proof that a record ended, header or no header. Fields are now collected always and DISCARDED at <eoh> — that tag is exactly the statement "everything before this was the header", so real headers stay out of the data just as reliably, and a headerless file keeps its first record instead of losing it to the same rule. An existing test asserted the old behaviour. It encoded a defensible reading of the spec, but the file it rejects is one operators really make and the rejection is silent, so the test is rewritten with the evidence that changed it rather than deleted.
148 lines
4.5 KiB
Go
148 lines
4.5 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) {
|
|
// A file without a header is IMPORTED, not discarded.
|
|
//
|
|
// This test asserted the opposite until 2026-07-29, when an operator pasted a
|
|
// dozen lines out of wsjtx_log.adi into a new .adi and got "0 imported,
|
|
// 0 ignored, 0 total". Discarding records before <EOH> was a defensible
|
|
// reading of the spec, but the file it rejects is one people really make, and
|
|
// the rejection is silent: the count says the file was empty, not that a tag
|
|
// was missing. An <eor> is proof enough that a record ended.
|
|
src := `<CALL:5>F4XYZ<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("expected the record to be imported without <EOH>, got %d", len(got))
|
|
}
|
|
if got[0]["call"] != "F4XYZ" {
|
|
t.Errorf("call = %q, want F4XYZ", got[0]["call"])
|
|
}
|
|
}
|
|
|
|
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"])
|
|
}
|
|
}
|
|
|
|
// A file with NO header must still import.
|
|
//
|
|
// Reported from the field: an operator lifted a dozen lines out of wsjtx_log.adi
|
|
// into a new .adi and got "0 imported, 0 ignored, 0 total" — which reads as an
|
|
// empty file rather than as a missing header. Records were only emitted after
|
|
// <EOH>, so a hand-assembled file was invisible in its entirety.
|
|
func TestParseHeaderlessFile(t *testing.T) {
|
|
// The operator's exact shape: records straight from a WSJT-X log, no header.
|
|
in := "<call:5>PY2VE <gridsquare:4>GG67 <mode:3>FT8 <rst_sent:3>-18 <rst_rcvd:3>-15 " +
|
|
"<qso_date:8>20260728 <time_on:6>194200 <band:3>15m <eor>\n" +
|
|
"<call:5>W8OBX <gridsquare:4>FM25 <mode:3>FT8 <rst_sent:3>-19 <rst_rcvd:3>-12 " +
|
|
"<qso_date:8>20260728 <time_on:6>194500 <band:3>15m <eor>\n"
|
|
|
|
var got []Record
|
|
if err := Parse(strings.NewReader(in), func(r Record) error {
|
|
got = append(got, r)
|
|
return nil
|
|
}); err != nil {
|
|
t.Fatalf("Parse: %v", err)
|
|
}
|
|
if len(got) != 2 {
|
|
t.Fatalf("parsed %d records from a headerless file, want 2", len(got))
|
|
}
|
|
if got[0]["call"] != "PY2VE" || got[1]["call"] != "W8OBX" {
|
|
t.Errorf("callsigns = %q, %q — want PY2VE, W8OBX", got[0]["call"], got[1]["call"])
|
|
}
|
|
if got[0]["gridsquare"] != "GG67" {
|
|
t.Errorf("gridsquare = %q, want GG67", got[0]["gridsquare"])
|
|
}
|
|
}
|
|
|
|
// And a normal file must be unaffected: its header is still discarded, so header
|
|
// fields never leak into the first record.
|
|
func TestParseHeaderStillDiscarded(t *testing.T) {
|
|
in := "Generated by SomeLogger\n<adif_ver:5>3.1.4 <programid:6>OpsLog <eoh>\n" +
|
|
"<call:4>F4AA <mode:2>CW <eor>\n"
|
|
var got []Record
|
|
if err := Parse(strings.NewReader(in), func(r Record) error {
|
|
got = append(got, r)
|
|
return nil
|
|
}); err != nil {
|
|
t.Fatalf("Parse: %v", err)
|
|
}
|
|
if len(got) != 1 {
|
|
t.Fatalf("parsed %d records, want 1", len(got))
|
|
}
|
|
if _, leaked := got[0]["programid"]; leaked {
|
|
t.Error("a header field leaked into the record")
|
|
}
|
|
if got[0]["call"] != "F4AA" {
|
|
t.Errorf("call = %q, want F4AA", got[0]["call"])
|
|
}
|
|
}
|