Not the id: the primary key follows insertion order, so importing an old ADIF gives the oldest contacts the highest ids. This is a rank over qso_date. Computed over the WHOLE log, not the query result — ranking inside the result would renumber every contact the moment a filter is applied, and QSO #1 would change identity as the operator typed. Held as one id-to-rank map, built from a single ordered id query and dropped with the other derived indexes when the log changes. A contact logged from the entry form is the newest, so it takes the next number without rereading the log: AddQSO deliberately avoids full invalidation because a contest run would pay for it once per QSO. A contact entered with an OLDER date belongs in the middle of the order, so there the map is dropped and rebuilt rather than mis-numbered.
96 lines
2.8 KiB
Go
96 lines
2.8 KiB
Go
package main
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"hamlog/internal/qso"
|
|
)
|
|
|
|
func at(s string) time.Time {
|
|
t, err := time.Parse("2006-01-02 15:04", s)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return t
|
|
}
|
|
|
|
// The number is the contact's rank in the WHOLE log, not its position in what
|
|
// the grid happens to be showing. Ranking inside the result would renumber
|
|
// every contact the moment a filter is applied, and QSO #1 would change
|
|
// identity as the operator typed.
|
|
func TestQSONumberIsGlobalNotPerPage(t *testing.T) {
|
|
a := &App{}
|
|
// The log, oldest first: ids are deliberately NOT in date order, which is
|
|
// exactly what an imported ADIF produces.
|
|
a.qsoNumbers = map[int64]int{
|
|
770: 1, // oldest, imported last so it has the highest id
|
|
101: 2,
|
|
102: 3,
|
|
103: 4,
|
|
}
|
|
|
|
// A filtered page holding only two of them, newest first as the grid asks.
|
|
page := []qso.QSO{{ID: 103}, {ID: 770}}
|
|
a.stampQSONumbers(page)
|
|
|
|
if page[0].Number != 4 {
|
|
t.Errorf("id 103 numbered %d, want 4", page[0].Number)
|
|
}
|
|
if page[1].Number != 1 {
|
|
t.Errorf("id 770 numbered %d, want 1 — the oldest contact, whatever its id", page[1].Number)
|
|
}
|
|
}
|
|
|
|
// A contact logged now is the newest, so it takes the next number without
|
|
// rereading the log — a full scan per QSO would be felt in a contest run.
|
|
func TestNewQSOTakesTheNextNumber(t *testing.T) {
|
|
a := &App{}
|
|
a.qsoNumbers = map[int64]int{1: 1, 2: 2, 3: 3}
|
|
a.qsoNumMax = at("2026-08-13 10:00")
|
|
|
|
a.noteQSONumbered(9, at("2026-08-13 11:00"))
|
|
if got := a.qsoNumbers[9]; got != 4 {
|
|
t.Errorf("new QSO numbered %d, want 4", got)
|
|
}
|
|
if !a.qsoNumMax.Equal(at("2026-08-13 11:00")) {
|
|
t.Error("the newest date was not carried forward")
|
|
}
|
|
}
|
|
|
|
// A contact entered with an OLDER date belongs in the middle of the order.
|
|
// Appending it would number it last, which is wrong — so the map is dropped and
|
|
// rebuilt correctly instead.
|
|
func TestBackdatedQSOForcesARebuild(t *testing.T) {
|
|
a := &App{}
|
|
a.qsoNumbers = map[int64]int{1: 1, 2: 2, 3: 3}
|
|
a.qsoNumMax = at("2026-08-13 10:00")
|
|
|
|
a.noteQSONumbered(9, at("2020-01-01 09:00"))
|
|
if a.qsoNumbers != nil {
|
|
t.Errorf("a back-dated QSO was appended as the newest: %v", a.qsoNumbers)
|
|
}
|
|
}
|
|
|
|
// Nothing built yet: the lazy build will see the new contact anyway, so this
|
|
// must not create a one-entry map that then numbers the whole log wrongly.
|
|
func TestNoteBeforeAnyBuildDoesNothing(t *testing.T) {
|
|
a := &App{}
|
|
a.noteQSONumbered(9, at("2026-08-13 11:00"))
|
|
if a.qsoNumbers != nil {
|
|
t.Errorf("built a map from a single contact: %v", a.qsoNumbers)
|
|
}
|
|
}
|
|
|
|
// With no index available the column is simply empty — never wrong.
|
|
func TestStampWithoutIndexLeavesZero(t *testing.T) {
|
|
a := &App{} // no qso repo, so the index cannot be built
|
|
page := []qso.QSO{{ID: 1}, {ID: 2}}
|
|
a.stampQSONumbers(page)
|
|
for _, q := range page {
|
|
if q.Number != 0 {
|
|
t.Errorf("invented a number without an index: %d", q.Number)
|
|
}
|
|
}
|
|
}
|