package adif import ( "strings" "testing" ) func TestParseSimple(t *testing.T) { src := `Header text here Generated by HamLog F4XYZ20mSSB20240101123456 K1AB40mCW202401020930 ` 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 := `F4XYZac` 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"] != "ac 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 is proof enough that a record ended. src := `F4XYZ` 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 , got %d", len(got)) } if got[0]["call"] != "F4XYZ" { t.Errorf("call = %q, want F4XYZ", got[0]["call"]) } } func TestParseTypedField(t *testing.T) { // form (e.g. ). src := `F4XYZ14.250` 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 // , 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 := "PY2VE GG67 FT8 -18 -15 " + "20260728 194200 15m \n" + "W8OBX FM25 FT8 -19 -12 " + "20260728 194500 15m \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\n3.1.4 OpsLog \n" + "F4AA CW \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"]) } }