Six faults from an evening on 60 m, all in the same family: the engine judging a station by what the log wants from it and forgetting what is already under way. - An exchange was abandoned mid-QSO. The reply lands in the same period the ladder is re-read, and that period was judged before the reply was taken into account, so a better-ranked caller took the slot from a station that had just come back to us. The answer is settled first now, and our own report counts as being inside the exchange too — which also protects a QSO the operator started by hand. - A station just picked started with misses against it. Its transmit slot was unknown until a second decode, and with the parity unknown every period counted, including the one spent transmitting to it. - The freed slot after "it is working somebody else" was thrown away: the period's decodes are in hand, so the next station is picked from them rather than fifteen seconds later. Never mid-over. - Auto-call is never armed from a stored setting — not at launch, not on a profile switch. It is the one feature that puts the station on the air by itself and OpsLog starts with Windows. - It says what it is waiting for: a wanted station in a QSO with somebody else now shows beside the Auto button instead of looking idle. - Switching profile left the previous logbook's verdicts on screen. The worked-index, chase-new and the frontend's cached verdicts are dropped when the logbook changes. FT decodes: distance column, a message addressed to you set whole in green (the station you are calling keeps a tint — most of what it sends goes to other people), badge order L / Wkd / WL, list cleared when the RIG changes band. Rotor: new world-map compass from EC1KD's design, with the Ultrabeam boom and second lobe restored and the compact form preserved; the classic dial is kept and Settings → Rotator chooses between them. Stop no longer flickers on a rotor standing still — movement was inferred from a degree, less than the jitter a controller reports at rest.
920 lines
36 KiB
Go
920 lines
36 KiB
Go
package autocall
|
||
|
||
import (
|
||
"fmt"
|
||
"strings"
|
||
"testing"
|
||
"time"
|
||
)
|
||
|
||
const me = "F4BPO"
|
||
|
||
// base is a slot boundary, so slot parity in the tests is the real arithmetic.
|
||
var base = time.Date(2026, 9, 6, 12, 0, 0, 0, time.UTC)
|
||
|
||
func at(period int) time.Time { return base.Add(time.Duration(period) * 15 * time.Second) }
|
||
|
||
func cq(call string, need Need, snr int, opts ...func(*Candidate)) Candidate {
|
||
c := Candidate{
|
||
Decode: Decode{Call: call, Band: "20m", Mode: "FT8", SNR: snr, CQ: true,
|
||
Msg: "CQ " + call + " JN36", TRPeriod: 15, IsNew: true},
|
||
Need: need,
|
||
}
|
||
for _, o := range opts {
|
||
o(&c)
|
||
}
|
||
return c
|
||
}
|
||
|
||
func watched(c *Candidate) { c.Watched = true }
|
||
func worked(c *Candidate) { c.Worked = true }
|
||
|
||
// busy is a station in the middle of an exchange with somebody else.
|
||
func busy(call string, need Need, snr int) Candidate {
|
||
c := cq(call, need, snr)
|
||
c.CQ = false
|
||
c.Msg = "VP6D " + call + " JN36"
|
||
return c
|
||
}
|
||
|
||
// callsMe is a station answering us.
|
||
func callsMe(call string, need Need, snr int) Candidate {
|
||
c := cq(call, need, snr)
|
||
c.CQ = false
|
||
c.Msg = me + " " + call + " -12"
|
||
return c
|
||
}
|
||
|
||
func period(n int, decodes ...Candidate) Period {
|
||
for i := range decodes {
|
||
decodes[i].At = at(n)
|
||
}
|
||
return Period{Key: fmt.Sprintf("p%d", n), At: at(n), TRPeriod: 15, Decodes: decodes, MyCall: me}
|
||
}
|
||
|
||
func on() *Engine { return New(Settings{Enabled: true}) }
|
||
|
||
// ── The ladder ────────────────────────────────────────────────────────────
|
||
|
||
func TestLadderOrder(t *testing.T) {
|
||
// Every rung. The watched ones come FIRST, ordered among themselves by what
|
||
// is needed — the list is the operator's answer, not a tie-breaker.
|
||
order := []Candidate{
|
||
cq("A", NeedDXCC, 0, watched), cq("C", NeedBand, 0, watched),
|
||
cq("E", NeedMode, 0, watched), cq("G", NeedSlot, 0, watched),
|
||
cq("I", NeedNone, 0, watched),
|
||
cq("B", NeedDXCC, 0), cq("D", NeedBand, 0),
|
||
cq("F", NeedMode, 0), cq("H", NeedSlot, 0),
|
||
}
|
||
for i := 1; i < len(order); i++ {
|
||
if rank(order[i-1]) <= rank(order[i]) {
|
||
t.Errorf("%s (%d) does not outrank %s (%d)",
|
||
order[i-1].Call, rank(order[i-1]), order[i].Call, rank(order[i]))
|
||
}
|
||
}
|
||
// A station with nothing needed and not watched is not called at all.
|
||
if rank(cq("Z", NeedNone, 0)) != 0 {
|
||
t.Error("a station with nothing to gain from it ranks above zero")
|
||
}
|
||
// And the pick agrees with the ladder, whatever order the period lists them.
|
||
e := on()
|
||
a := e.OnPeriod(period(0, order[8], order[5], order[0], order[6]))
|
||
if a.Kind != DoReply || a.Decode.Call != "A" {
|
||
t.Fatalf("picked %+v, want the watched new entity", a)
|
||
}
|
||
// A watched station with NOTHING needed still beats a new entity that is not
|
||
// watched — the case that sent an operator hunting: a watched DXpedition sat
|
||
// on the band all evening while the engine worked what the log wanted.
|
||
e = on()
|
||
if a := e.OnPeriod(period(2, cq("RARE", NeedDXCC, 0), cq("WATCHED", NeedNone, -20, watched))); a.Decode.Call != "WATCHED" {
|
||
t.Errorf("picked %q, want the watched callsign", a.Decode.Call)
|
||
}
|
||
}
|
||
|
||
func TestStrongestWinsBetweenEquals(t *testing.T) {
|
||
e := on()
|
||
a := e.OnPeriod(period(0, cq("WEAK", NeedBand, -20), cq("LOUD", NeedBand, -5)))
|
||
if a.Decode.Call != "LOUD" {
|
||
t.Errorf("picked %q, want the strongest of two equal needs", a.Decode.Call)
|
||
}
|
||
}
|
||
|
||
// ── The busy station ──────────────────────────────────────────────────────
|
||
|
||
func TestBusyStationIsNeverCalled(t *testing.T) {
|
||
e := on()
|
||
// The new entity is answering a DXpedition; the new band is calling CQ.
|
||
a := e.OnPeriod(period(0, busy("RARE", NeedDXCC, -3), cq("DL1XX", NeedBand, -15)))
|
||
if a.Kind != DoReply || a.Decode.Call != "DL1XX" {
|
||
t.Fatalf("called %+v — a station in mid-QSO cannot answer and must not be called", a)
|
||
}
|
||
// It is not banned: the moment it calls CQ it takes the slot back, once the
|
||
// QSO in hand is over.
|
||
e2 := on()
|
||
if a := e2.OnPeriod(period(0, cq("RARE", NeedDXCC, -3))); a.Decode.Call != "RARE" {
|
||
t.Errorf("the same station calling CQ was not called: %+v", a)
|
||
}
|
||
}
|
||
|
||
func TestFinalFrameIsCallable(t *testing.T) {
|
||
e := on()
|
||
c := busy("RARE", NeedDXCC, -3)
|
||
c.Msg = "IK2AAA RARE RR73" // one frame from being free
|
||
if a := e.OnPeriod(period(0, c)); a.Kind != DoReply {
|
||
t.Errorf("a station sending its last frame is free next period: %+v", a)
|
||
}
|
||
}
|
||
|
||
// ── The brakes ────────────────────────────────────────────────────────────
|
||
|
||
func TestAttemptsCapAtSevenAndFifteen(t *testing.T) {
|
||
for _, tc := range []struct {
|
||
name string
|
||
opt func(*Candidate)
|
||
want int
|
||
}{{"plain", func(*Candidate) {}, 7}, {"watched", watched, 15}} {
|
||
e := on()
|
||
e.OnPeriod(period(0, cq("DX", NeedDXCC, -5, tc.opt)))
|
||
tx := TXState{Transmitting: true, Msg: "DX " + me + " JN36"}
|
||
for i := 1; i < tc.want; i++ {
|
||
if a := e.NoteTX(tx); a.Kind != DoNothing {
|
||
t.Fatalf("%s: gave up at call %d of %d", tc.name, i, tc.want)
|
||
}
|
||
}
|
||
a := e.NoteTX(tx)
|
||
if a.Kind != DoHalt {
|
||
t.Errorf("%s: still calling after %d attempts", tc.name, tc.want)
|
||
}
|
||
if e.Target() != "" {
|
||
t.Errorf("%s: target still held after giving up", tc.name)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestOnlyFreshCallsCount(t *testing.T) {
|
||
e := on()
|
||
e.OnPeriod(period(0, cq("DX", NeedDXCC, -5)))
|
||
// The rest of the exchange is not a call: without this the seven were spent
|
||
// on one QSO in progress.
|
||
for _, msg := range []string{"DX " + me + " -12", "DX " + me + " R-12", "DX " + me + " RR73"} {
|
||
if a := e.NoteTX(TXState{Transmitting: true, Msg: msg}); a.Kind != DoNothing {
|
||
t.Fatalf("%q ended the series", msg)
|
||
}
|
||
}
|
||
if e.Status().Attempts != 0 {
|
||
t.Errorf("attempts = %d after three QSO frames, want 0", e.Status().Attempts)
|
||
}
|
||
// A transmission aimed at somebody else counts for nothing either.
|
||
e.NoteTX(TXState{Transmitting: true, Msg: "OTHER " + me + " JN36"})
|
||
if e.Status().Attempts != 0 {
|
||
t.Errorf("a call to another station was counted against the target")
|
||
}
|
||
}
|
||
|
||
func TestMissesOnlyCountTheStationsOwnPeriods(t *testing.T) {
|
||
e := on()
|
||
e.OnPeriod(period(0, cq("DX", NeedDXCC, -5))) // learns nothing yet
|
||
e.OnPeriod(period(2, cq("DX", NeedDXCC, -5))) // seen: it transmits on even periods
|
||
// Its listening periods say nothing about it. Ten of them must not add up
|
||
// to a give-up.
|
||
for _, p := range []int{3, 5, 7, 9, 11} {
|
||
if a := e.OnPeriod(period(p)); a.Kind != DoNothing {
|
||
t.Fatalf("gave up during the station's own listening period %d", p)
|
||
}
|
||
}
|
||
if e.Status().Misses != 0 {
|
||
t.Errorf("misses = %d over five listening periods, want 0", e.Status().Misses)
|
||
}
|
||
// Absent from two of its transmit periods: still holding.
|
||
e.OnPeriod(period(4))
|
||
e.OnPeriod(period(6))
|
||
if e.Target() == "" {
|
||
t.Fatal("gave up after two misses, the limit is three")
|
||
}
|
||
if a := e.OnPeriod(period(8)); a.Kind != DoHalt {
|
||
t.Errorf("still holding after three missed transmit periods: %+v", a)
|
||
}
|
||
}
|
||
|
||
func TestOneMissPerPeriodEvenIfTheHandlerRunsTwice(t *testing.T) {
|
||
e := on()
|
||
e.OnPeriod(period(0, cq("DX", NeedDXCC, -5)))
|
||
e.OnPeriod(period(2, cq("DX", NeedDXCC, -5)))
|
||
p := period(4)
|
||
e.OnPeriod(p)
|
||
e.OnPeriod(p)
|
||
e.OnPeriod(p)
|
||
if e.Status().Misses != 1 {
|
||
t.Errorf("misses = %d after one period handled three times, want 1", e.Status().Misses)
|
||
}
|
||
}
|
||
|
||
func TestClockBackstopReleasesAStuckTarget(t *testing.T) {
|
||
e := on()
|
||
e.OnPeriod(period(0, cq("DX", NeedDXCC, -5)))
|
||
// Decoded every one of its periods and never answering, with the decoder
|
||
// never reporting a transmission: no counter can advance.
|
||
for p := 2; p <= 16; p += 2 {
|
||
e.OnPeriod(period(p, cq("DX", NeedDXCC, -5)))
|
||
}
|
||
if a := e.OnPeriod(period(18, cq("DX", NeedDXCC, -5))); a.Kind != DoHalt {
|
||
t.Errorf("a target held past MaxHold with no counter moving was never released: %+v", a)
|
||
}
|
||
}
|
||
|
||
// ── After a series ────────────────────────────────────────────────────────
|
||
|
||
func TestAReleasedStationYieldsToAnythingBetter(t *testing.T) {
|
||
e := on()
|
||
e.OnPeriod(period(0, cq("DX", NeedBand, -5)))
|
||
tx := TXState{Transmitting: true, Msg: "DX " + me + " JN36"}
|
||
for i := 0; i < 7; i++ {
|
||
e.NoteTX(tx)
|
||
}
|
||
if e.Target() != "" {
|
||
t.Fatal("still holding after seven calls")
|
||
}
|
||
// It is still there, and so is a new entity: the entity takes the slot.
|
||
a := e.OnPeriod(period(2, cq("DX", NeedBand, -5), cq("RARE", NeedDXCC, -20)))
|
||
if a.Decode.Call != "RARE" {
|
||
t.Errorf("picked %q, want the higher priority over the station just released", a.Decode.Call)
|
||
}
|
||
}
|
||
|
||
func TestAReleasedStationRestsBeforeItIsCalledAgain(t *testing.T) {
|
||
e := on()
|
||
e.OnPeriod(period(0, cq("DX", NeedBand, -5)))
|
||
tx := TXState{Transmitting: true, Msg: "DX " + me + " JN36"}
|
||
for i := 0; i < 7; i++ {
|
||
e.NoteTX(tx)
|
||
}
|
||
// Still the only thing on the air, and still resting: seven calls, a halt,
|
||
// and the same station called again four seconds later is not a rest.
|
||
for _, n := range []int{2, 4, 6} { // all inside the two minutes
|
||
if a := e.OnPeriod(period(n, cq("DX", NeedBand, -5))); a.Kind != DoNothing {
|
||
t.Fatalf("period %d: called again during the rest: %+v", n, a)
|
||
}
|
||
}
|
||
// Two minutes later (period 8 × 15 s = 2 min past the give-up) it may go
|
||
// again — the station is still there and nothing better is.
|
||
if a := e.OnPeriod(period(9, cq("DX", NeedBand, -5))); a.Kind != DoReply {
|
||
t.Errorf("after the rest: %+v, want the station called again", a)
|
||
}
|
||
}
|
||
|
||
func TestTheAttemptsCapLetsTheOverFinish(t *testing.T) {
|
||
e := on()
|
||
e.OnPeriod(period(0, cq("DX", NeedDXCC, -5)))
|
||
tx := TXState{Transmitting: true, Msg: "DX " + me + " JN36"}
|
||
var a Action
|
||
for i := 0; i < 7; i++ {
|
||
a = e.NoteTX(tx)
|
||
}
|
||
if a.Kind != DoHalt {
|
||
t.Fatalf("no halt after seven calls: %+v", a)
|
||
}
|
||
// The cap trips WHILE the seventh call is going out — cutting the carrier
|
||
// there sends half a call. It asks the decoder to finish the over first.
|
||
if !a.Soft {
|
||
t.Error("the attempts cap cut the transmission that counted it")
|
||
}
|
||
}
|
||
|
||
func TestAStationIsParkedAfterItsRounds(t *testing.T) {
|
||
e := on()
|
||
tx := TXState{Transmitting: true, Msg: "DX " + me + " JN36"}
|
||
// Rounds are spaced past the rest (2 min = 8 periods) or the station would
|
||
// simply be resting rather than parked.
|
||
for round := 1; round <= 3; round++ {
|
||
a := e.OnPeriod(period(round*10, cq("DX", NeedBand, -5)))
|
||
if a.Kind != DoReply {
|
||
t.Fatalf("round %d: not called (%+v)", round, a)
|
||
}
|
||
for i := 0; i < 7; i++ {
|
||
e.NoteTX(tx)
|
||
}
|
||
}
|
||
// Three series of seven is twenty-one calls. That is the end of it for this
|
||
// session — the whole point of the exercise is that it cannot reach fifty.
|
||
if a := e.OnPeriod(period(60, cq("DX", NeedBand, -5))); a.Kind != DoNothing {
|
||
t.Errorf("a fourth series was started: %+v", a)
|
||
}
|
||
}
|
||
|
||
// ── Handing over ──────────────────────────────────────────────────────────
|
||
|
||
func TestFinishedQSOMovesToTheNextPriority(t *testing.T) {
|
||
e := on()
|
||
e.OnPeriod(period(0, cq("DX", NeedDXCC, -5)))
|
||
done := callsMe("DX", NeedDXCC, -5)
|
||
done.Msg = me + " DX RR73"
|
||
// The period the QSO ends in belongs to our own 73 — nothing else is called.
|
||
if a := e.OnPeriod(period(2, done, cq("NEXT", NeedBand, -10))); a.Kind != DoNothing {
|
||
t.Fatalf("took the slot our 73 goes out in: %+v", a)
|
||
}
|
||
a := e.OnPeriod(period(4, cq("NEXT", NeedBand, -10)))
|
||
if a.Kind != DoReply || a.Decode.Call != "NEXT" {
|
||
t.Errorf("next period: %+v, want the next priority", a)
|
||
}
|
||
}
|
||
|
||
func TestFinishedQSOWithNoPriorityAnswersWhoeverIsCallingUs(t *testing.T) {
|
||
e := on()
|
||
e.OnPeriod(period(0, cq("DX", NeedDXCC, -5)))
|
||
done := callsMe("DX", NeedDXCC, -5)
|
||
done.Msg = me + " DX RR73"
|
||
// Two stations calling us, nothing needed from either: the strongest wins —
|
||
// on the period after the one our 73 goes out in.
|
||
weak := callsMe("WEAK", NeedNone, -18)
|
||
weak.Watched = true
|
||
loud := callsMe("LOUD", NeedNone, -4)
|
||
loud.Watched = true
|
||
e.OnPeriod(period(2, done))
|
||
a := e.OnPeriod(period(4, weak, loud))
|
||
if a.Kind != DoReply || a.Decode.Call != "LOUD" {
|
||
t.Errorf("answered %+v, want the strongest of the stations calling us", a)
|
||
}
|
||
}
|
||
|
||
func TestAlreadyWorkedIsNeverCalled(t *testing.T) {
|
||
e := on()
|
||
a := e.OnPeriod(period(0, cq("DX", NeedDXCC, -5, worked)))
|
||
if a.Kind != DoNothing {
|
||
t.Errorf("called a station already in the log on this band and mode: %+v", a)
|
||
}
|
||
}
|
||
|
||
func TestReplayedHistoryIsNeverCalled(t *testing.T) {
|
||
e := on()
|
||
old := cq("DX", NeedDXCC, -5)
|
||
old.IsNew = false
|
||
if a := e.OnPeriod(period(0, old)); a.Kind != DoNothing {
|
||
t.Errorf("answered a replayed decode: %+v", a)
|
||
}
|
||
}
|
||
|
||
func TestNoCallStartsOverATransmissionInProgress(t *testing.T) {
|
||
e := on()
|
||
p := period(0, cq("DX", NeedDXCC, -5))
|
||
p.TX = TXState{Transmitting: true}
|
||
if a := e.OnPeriod(p); a.Kind != DoNothing {
|
||
t.Errorf("started a call while the decoder was transmitting: %+v", a)
|
||
}
|
||
}
|
||
|
||
// ── The "call this station" field ─────────────────────────────────────────
|
||
|
||
func TestOnlyCallsThatStation(t *testing.T) {
|
||
e := New(Settings{Enabled: true, Only: "VP6D"})
|
||
a := e.OnPeriod(period(0, cq("RARE", NeedDXCC, -5), cq("VP6D", NeedSlot, -20)))
|
||
if a.Kind != DoReply || a.Decode.Call != "VP6D" {
|
||
t.Fatalf("picked %+v, want the station named in the field", a)
|
||
}
|
||
// Same brakes, then a hard stop: there is nothing else it was asked to do.
|
||
tx := TXState{Transmitting: true, Msg: "VP6D " + me + " JN36"}
|
||
for i := 0; i < 7; i++ {
|
||
e.NoteTX(tx)
|
||
}
|
||
if !e.Status().Stopped {
|
||
t.Error("an explicit target ran out of attempts and the feature did not stop")
|
||
}
|
||
if a := e.OnPeriod(period(2, cq("VP6D", NeedSlot, -20))); a.Kind != DoNothing {
|
||
t.Errorf("kept calling after the stop: %+v", a)
|
||
}
|
||
e.Reset()
|
||
if a := e.OnPeriod(period(4, cq("VP6D", NeedSlot, -20))); a.Kind != DoReply {
|
||
t.Errorf("the operator restarted it and nothing happened: %+v", a)
|
||
}
|
||
}
|
||
|
||
func TestDisabledDoesNothingAtAll(t *testing.T) {
|
||
e := New(Settings{})
|
||
if a := e.OnPeriod(period(0, cq("DX", NeedDXCC, 0))); a.Kind != DoNothing {
|
||
t.Errorf("switched off and still calling: %+v", a)
|
||
}
|
||
if a := e.NoteTX(TXState{Transmitting: true, Msg: "DX " + me + " JN36"}); a.Kind != DoNothing {
|
||
t.Errorf("switched off and still counting: %+v", a)
|
||
}
|
||
}
|
||
|
||
func TestOnlyStopsOnceThatStationIsWorked(t *testing.T) {
|
||
e := New(Settings{Enabled: true, Only: "VP6D"})
|
||
e.OnPeriod(period(0, cq("VP6D", NeedDXCC, -10)))
|
||
done := callsMe("VP6D", NeedDXCC, -10)
|
||
done.Msg = me + " VP6D RR73"
|
||
e.OnPeriod(period(2, done))
|
||
// The station is still on the air calling CQ. It has been worked: an
|
||
// explicit request is for one QSO, not for the whole evening.
|
||
if a := e.OnPeriod(period(4, cq("VP6D", NeedDXCC, -10))); a.Kind != DoNothing {
|
||
t.Errorf("called the named station again after working it: %+v", a)
|
||
}
|
||
}
|
||
|
||
func TestAStationJustWorkedIsNotCalledBackWhileTheLogCatchesUp(t *testing.T) {
|
||
e := on()
|
||
e.OnPeriod(period(0, cq("DX", NeedDXCC, -5)))
|
||
done := callsMe("DX", NeedDXCC, -5)
|
||
done.Msg = me + " DX RR73"
|
||
e.OnPeriod(period(2, done))
|
||
// Still flagged as a new entity — the QSO is not in the log yet — and still
|
||
// calling CQ. It must not be answered again.
|
||
if a := e.OnPeriod(period(4, cq("DX", NeedDXCC, -5))); a.Kind != DoNothing {
|
||
t.Errorf("called back a station worked two periods ago: %+v", a)
|
||
}
|
||
}
|
||
|
||
func TestChaseListTakesSeveralCallsigns(t *testing.T) {
|
||
// Typed the way an operator types a list: commas, spaces, or both.
|
||
for _, field := range []string{"VP6D 3Y0J", "vp6d,3y0j", "VP6D, 3Y0J", " VP6D ;3Y0J "} {
|
||
if got := onlyList(field); len(got) != 2 || got[0] != "VP6D" || got[1] != "3Y0J" {
|
||
t.Errorf("%q parsed as %v, want [VP6D 3Y0J]", field, got)
|
||
}
|
||
}
|
||
|
||
e := New(Settings{Enabled: true, Only: "VP6D, 3Y0J"})
|
||
// Nothing off the list is called, however rare it is.
|
||
if a := e.OnPeriod(period(0, cq("RARE", NeedDXCC, -1))); a.Kind != DoNothing {
|
||
t.Errorf("called a station that is not on the chase list: %+v", a)
|
||
}
|
||
// Between two listed stations the ladder still decides: the new entity over
|
||
// the new slot, whatever their order in the period.
|
||
a := e.OnPeriod(period(2, cq("3Y0J", NeedSlot, -1), cq("VP6D", NeedDXCC, -22)))
|
||
if a.Kind != DoReply || a.Decode.Call != "VP6D" {
|
||
t.Fatalf("picked %+v, want the new entity of the two listed", a)
|
||
}
|
||
// Working one of them leaves the other callable — the list is a hunt, not a
|
||
// single request.
|
||
done := callsMe("VP6D", NeedDXCC, -22)
|
||
done.Msg = me + " VP6D RR73"
|
||
e.OnPeriod(period(4, done, cq("3Y0J", NeedSlot, -1)))
|
||
a = e.OnPeriod(period(6, cq("3Y0J", NeedSlot, -1)))
|
||
if a.Kind != DoReply || a.Decode.Call != "3Y0J" {
|
||
t.Errorf("after working VP6D: %+v, want the other station on the list", a)
|
||
}
|
||
}
|
||
|
||
// ── Two decoders at once (the split view) ─────────────────────────────────
|
||
|
||
func onInst(inst string, p Period) Period {
|
||
p.Instance = inst
|
||
for i := range p.Decodes {
|
||
p.Decodes[i].Instance = inst
|
||
}
|
||
return p
|
||
}
|
||
|
||
func TestTheOtherReceiverCannotBreakTheQSOInProgress(t *testing.T) {
|
||
e := on()
|
||
// Calling a new band on receiver A.
|
||
if a := e.OnPeriod(onInst("A", period(0, cq("DL1XX", NeedBand, -10)))); a.Decode.Call != "DL1XX" {
|
||
t.Fatalf("first call went to %+v", a)
|
||
}
|
||
// Receiver B now hears a new ENTITY — a better catch by every rule. It must
|
||
// still not be called: one station at a time, and the QSO in hand is the
|
||
// one already under way.
|
||
if a := e.OnPeriod(onInst("B", period(1, cq("RARE", NeedDXCC, -1)))); a.Kind != DoNothing {
|
||
t.Errorf("the other receiver started a second QSO: %+v", a)
|
||
}
|
||
// And B's periods count no misses against A's target: the station is not
|
||
// absent from B, it was never on that band.
|
||
e.OnPeriod(onInst("B", period(3, cq("RARE", NeedDXCC, -1))))
|
||
e.OnPeriod(onInst("B", period(5, cq("RARE", NeedDXCC, -1))))
|
||
e.OnPeriod(onInst("B", period(7, cq("RARE", NeedDXCC, -1))))
|
||
if e.Status().Misses != 0 || e.Target() != "DL1XX" {
|
||
t.Errorf("misses = %d, target = %q — the other receiver's periods were counted",
|
||
e.Status().Misses, e.Target())
|
||
}
|
||
// B transmitting its own QSO is not us calling DL1XX either.
|
||
for i := 0; i < 9; i++ {
|
||
e.NoteTX(TXState{Transmitting: true, Instance: "B", Msg: "DL1XX " + me + " JN36"})
|
||
}
|
||
if e.Status().Attempts != 0 {
|
||
t.Errorf("attempts = %d from the other receiver's transmissions, want 0", e.Status().Attempts)
|
||
}
|
||
// A's own transmissions do count.
|
||
e.NoteTX(TXState{Transmitting: true, Instance: "A", Msg: "DL1XX " + me + " JN36"})
|
||
if e.Status().Attempts != 1 {
|
||
t.Errorf("attempts = %d after one call from the calling receiver, want 1", e.Status().Attempts)
|
||
}
|
||
// Once the QSO is over, the other receiver's station is free to be taken.
|
||
done := callsMe("DL1XX", NeedBand, -10)
|
||
done.Msg = me + " DL1XX RR73"
|
||
e.OnPeriod(onInst("A", period(9, done)))
|
||
if a := e.OnPeriod(onInst("B", period(11, cq("RARE", NeedDXCC, -1)))); a.Decode.Call != "RARE" {
|
||
t.Errorf("after the QSO ended, the other receiver was still locked out: %+v", a)
|
||
}
|
||
}
|
||
|
||
// The one reported from the air: his RRR arrives, we are sending our 73, and
|
||
// the engine picked the next station in the same instant. WSJT-X acts on a
|
||
// reply at once — it dropped the exchange and started calling the new station,
|
||
// cutting the 73 a second in.
|
||
func TestNothingIsCalledOverOurOwnTransmission(t *testing.T) {
|
||
e := on()
|
||
e.OnPeriod(period(0, cq("DX", NeedDXCC, -5)))
|
||
done := callsMe("DX", NeedDXCC, -5)
|
||
done.Msg = me + " DX RRR"
|
||
p := period(2, done, cq("NEXT", NeedBand, -10))
|
||
p.TX = TXState{Transmitting: true, Msg: "DX " + me + " 73"}
|
||
if a := e.OnPeriod(p); a.Kind != DoNothing {
|
||
t.Fatalf("called %+v while our own over was still going out", a)
|
||
}
|
||
// The QSO is still released — only the next call waits.
|
||
if e.Target() != "" {
|
||
t.Errorf("target = %q after the QSO finished, want none", e.Target())
|
||
}
|
||
// Next period, carrier down: now it may take the next station.
|
||
if a := e.OnPeriod(period(4, cq("NEXT", NeedBand, -10))); a.Kind != DoReply || a.Decode.Call != "NEXT" {
|
||
t.Errorf("once the transmission ended: %+v, want the next station called", a)
|
||
}
|
||
}
|
||
|
||
func TestTheHoldClockRestartsWhenTheStationAnswers(t *testing.T) {
|
||
e := on()
|
||
e.OnPeriod(period(0, cq("DX", NeedDXCC, -5)))
|
||
// Called for a long time — nearly the backstop — and then he answers.
|
||
for p := 2; p <= 14; p += 2 {
|
||
e.OnPeriod(period(p, cq("DX", NeedDXCC, -5)))
|
||
}
|
||
e.OnPeriod(period(16, callsMe("DX", NeedDXCC, -5)))
|
||
// The exchange must not be halted by a backstop that was counting the wait.
|
||
for _, p := range []int{18, 20} {
|
||
if a := e.OnPeriod(period(p, callsMe("DX", NeedDXCC, -5))); a.Kind == DoHalt {
|
||
t.Fatalf("period %d: halted an exchange in progress (%s)", p, a.Reason)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestARealNeedOutranksAnUnconfirmedOne(t *testing.T) {
|
||
real := cq("REAL", NeedBand, -20)
|
||
unconf := cq("UNCONF", NeedBand, -2)
|
||
unconf.Unconfirmed = true
|
||
// Same need, and the unconfirmed one is 18 dB louder. The band never worked
|
||
// is still the catch: the other is a QSL to chase, not a QSO to make.
|
||
e := on()
|
||
if a := e.OnPeriod(period(0, unconf, real)); a.Decode.Call != "REAL" {
|
||
t.Errorf("picked %q, want the band never worked", a.Decode.Call)
|
||
}
|
||
// Watched changes that, and is meant to: the list is the operator's answer.
|
||
unconf.Watched = true
|
||
e = on()
|
||
if a := e.OnPeriod(period(0, unconf, real)); a.Decode.Call != "UNCONF" {
|
||
t.Errorf("picked %q, want the watched station", a.Decode.Call)
|
||
}
|
||
unconf.Watched = false
|
||
// Between two unwatched stations, an unconfirmed BAND still beats a real
|
||
// SLOT: the band is the bigger prize whatever state it is in.
|
||
slot := cq("SLOT", NeedSlot, 0)
|
||
e = on()
|
||
if a := e.OnPeriod(period(0, slot, unconf)); a.Decode.Call != "UNCONF" {
|
||
t.Errorf("picked %q, want the unconfirmed band over a real slot", a.Decode.Call)
|
||
}
|
||
}
|
||
|
||
// ── The operator's Halt ───────────────────────────────────────────────────
|
||
|
||
func TestHaltSetsTheStationAsideForTheSession(t *testing.T) {
|
||
e := on()
|
||
if a := e.OnPeriod(period(0, cq("DX", NeedDXCC, -5))); a.Decode.Call != "DX" {
|
||
t.Fatalf("not called: %+v", a)
|
||
}
|
||
// The operator stops it mid-call.
|
||
if got := e.Halt(); got != "DX" {
|
||
t.Fatalf("Halt returned %q, want the station being called", got)
|
||
}
|
||
if e.Target() != "" {
|
||
t.Error("the target survived a halt")
|
||
}
|
||
// Still the loudest new entity on the air, period after period. It is the
|
||
// operator's verdict, so it is not called again — not next period, not in
|
||
// ten minutes.
|
||
for _, n := range []int{2, 4, 40} {
|
||
if a := e.OnPeriod(period(n, cq("DX", NeedDXCC, -5))); a.Kind != DoNothing {
|
||
t.Errorf("period %d: called a station the operator had stopped: %+v", n, a)
|
||
}
|
||
}
|
||
// Naming it explicitly does not override the operator either.
|
||
e.SetSettings(Settings{Enabled: true, Only: "DX"})
|
||
if a := e.OnPeriod(period(42, cq("DX", NeedDXCC, -5))); a.Kind != DoNothing {
|
||
t.Errorf("the chase list overrode a halt: %+v", a)
|
||
}
|
||
// Switching auto-call off and on is what starts over.
|
||
e.Reset()
|
||
e.SetSettings(Settings{Enabled: true})
|
||
if a := e.OnPeriod(period(44, cq("DX", NeedDXCC, -5))); a.Kind != DoReply {
|
||
t.Errorf("after a restart the station is fair game again: %+v", a)
|
||
}
|
||
}
|
||
|
||
func TestHaltWithNothingBeingCalledIsJustAHalt(t *testing.T) {
|
||
e := on()
|
||
if got := e.Halt(); got != "" {
|
||
t.Errorf("Halt returned %q with no target", got)
|
||
}
|
||
if e.Greylisted() != 0 {
|
||
t.Errorf("greylisted %d stations from an idle halt", e.Greylisted())
|
||
}
|
||
}
|
||
|
||
func TestTraceSaysWhyNobodyWasCalled(t *testing.T) {
|
||
var lines []string
|
||
e := on()
|
||
e.SetTrace(func(f string, args ...any) { lines = append(lines, fmt.Sprintf(f, args...)) })
|
||
|
||
worked := cq("A", NeedDXCC, -5, worked)
|
||
nothing := cq("B", NeedNone, -5)
|
||
busyOne := busy("C", NeedBand, -5)
|
||
e.Halt() // nothing held: no-op, keeps the set empty
|
||
e.OnPeriod(period(0, worked, nothing, busyOne))
|
||
|
||
if len(lines) == 0 {
|
||
t.Fatal("tracing on and nothing was written")
|
||
}
|
||
got := lines[len(lines)-1]
|
||
for _, want := range []string{"decodes=3", "callable=0", "worked=1", "nothing-needed=1", "busy=1"} {
|
||
if !strings.Contains(got, want) {
|
||
t.Errorf("trace %q does not say %q", got, want)
|
||
}
|
||
}
|
||
|
||
// And when it DOES call, the line names the candidates it ranked.
|
||
lines = nil
|
||
e.OnPeriod(period(2, cq("DX", NeedBand, -9, watched)))
|
||
if len(lines) == 0 || !strings.Contains(lines[0], "DX(watched band,-9 dB,r114)") {
|
||
t.Errorf("trace %v does not describe the station it called", lines)
|
||
}
|
||
}
|
||
|
||
func TestAStationCallingUsIsAnsweredEvenWithNothingToGain(t *testing.T) {
|
||
e := on()
|
||
// A QSO has just ended and two stations are calling us. Neither is worth
|
||
// anything to the log — and both are worth answering: they have heard us,
|
||
// they are waiting, and the contact is one over away.
|
||
weak := callsMe("WEAK", NeedNone, -18)
|
||
loud := callsMe("LOUD", NeedNone, -3)
|
||
a := e.OnPeriod(period(0, weak, loud))
|
||
if a.Kind != DoReply || a.Decode.Call != "LOUD" {
|
||
t.Fatalf("answered %+v, want the strongest of the stations calling us", a)
|
||
}
|
||
// A caller already worked on this band and mode is a duplicate, not a QSO.
|
||
e = on()
|
||
done := callsMe("DUPE", NeedNone, -1)
|
||
done.Worked = true
|
||
if a := e.OnPeriod(period(2, done)); a.Kind != DoNothing {
|
||
t.Errorf("answered a station already in the log on this band and mode: %+v", a)
|
||
}
|
||
// And one the operator halted stays halted, however politely it calls.
|
||
e = on()
|
||
e.OnPeriod(period(4, cq("STOP", NeedDXCC, -5)))
|
||
e.Halt()
|
||
if a := e.OnPeriod(period(6, callsMe("STOP", NeedDXCC, -5))); a.Kind != DoNothing {
|
||
t.Errorf("answered a station the operator had stopped: %+v", a)
|
||
}
|
||
// A needed station still comes first: a caller with nothing to gain is
|
||
// answered when nothing better is on the air, which is what was asked for.
|
||
e = on()
|
||
if a := e.OnPeriod(period(8, callsMe("CALLER", NeedNone, 0), cq("RARE", NeedDXCC, -20))); a.Decode.Call != "RARE" {
|
||
t.Errorf("picked %q, want the new entity ahead of a caller with nothing needed", a.Decode.Call)
|
||
}
|
||
}
|
||
|
||
func TestATargetThatStartsWorkingSomebodyElseIsDropped(t *testing.T) {
|
||
e := on()
|
||
// Picked on its CQ.
|
||
if a := e.OnPeriod(period(0, cq("ON7GB", NeedSlot, +5))); a.Decode.Call != "ON7GB" {
|
||
t.Fatalf("not called: %+v", a)
|
||
}
|
||
// Next period it is answering somebody else. Calling it is pointless: it is
|
||
// committed, and the seven attempts would be spent transmitting at a
|
||
// station that cannot hear them.
|
||
busyNow := busy("ON7GB", NeedSlot, +5)
|
||
busyNow.Msg = "PY2SAD ON7GB JO21"
|
||
a := e.OnPeriod(period(2, busyNow))
|
||
if a.Kind != DoHalt {
|
||
t.Fatalf("kept calling a station in a QSO with someone else: %+v", a)
|
||
}
|
||
if !strings.Contains(a.Reason, "PY2SAD") {
|
||
t.Errorf("reason %q does not name the station it is working", a.Reason)
|
||
}
|
||
if e.Target() != "" {
|
||
t.Error("the target was not released")
|
||
}
|
||
// No verdict attached: the moment it CQs again it is fair game, with a full
|
||
// allowance — it was never given up on.
|
||
if a := e.OnPeriod(period(4, cq("ON7GB", NeedSlot, +5))); a.Kind != DoReply {
|
||
t.Errorf("not called again once free: %+v", a)
|
||
}
|
||
if e.Status().Attempts != 0 {
|
||
t.Errorf("attempts = %d on a fresh series, want 0", e.Status().Attempts)
|
||
}
|
||
}
|
||
|
||
func TestAFinalFrameToSomebodyElseIsNotABusyTarget(t *testing.T) {
|
||
e := on()
|
||
e.OnPeriod(period(0, cq("DX", NeedSlot, 0)))
|
||
// Sending RR73 to another station: one period from being free, and the best
|
||
// moment there is to be calling it.
|
||
wrap := busy("DX", NeedSlot, 0)
|
||
wrap.Msg = "PY2SAD DX RR73"
|
||
if a := e.OnPeriod(period(2, wrap)); a.Kind != DoNothing || e.Target() != "DX" {
|
||
t.Errorf("dropped a station that was finishing: %+v (target %q)", a, e.Target())
|
||
}
|
||
}
|
||
|
||
func TestNothingHiddenByThePanelIsCalled(t *testing.T) {
|
||
e := New(Settings{Enabled: true, OnScreenOnly: true})
|
||
// The operator has filtered the list down to CQs: what is not on the screen
|
||
// is not called, whatever the log makes of it.
|
||
hidden := cq("RARE", NeedDXCC, 0)
|
||
hidden.Hidden = true
|
||
shown := cq("PLAIN", NeedSlot, -20)
|
||
if a := e.OnPeriod(period(0, hidden, shown)); a.Decode.Call != "PLAIN" {
|
||
t.Errorf("picked %q, want the station the panel is showing", a.Decode.Call)
|
||
}
|
||
// With the panel publishing nothing, nothing is hidden and the ladder
|
||
// decides alone.
|
||
e = New(Settings{Enabled: true, OnScreenOnly: true})
|
||
free := cq("RARE", NeedDXCC, 0)
|
||
if a := e.OnPeriod(period(2, free, shown)); a.Decode.Call != "RARE" {
|
||
t.Errorf("picked %q with no filtering in force, want the new entity", a.Decode.Call)
|
||
}
|
||
}
|
||
|
||
// MSHV answers two stations in one transmission and the decoder prints them as
|
||
// one line. Reported from the air: the DX was answering us in the second half
|
||
// and the engine, reading only the first, dropped it as busy.
|
||
func TestAMultiAnswerLineIsReadWhole(t *testing.T) {
|
||
fox := cq("HK0/PY8WW", NeedDXCC, -17, watched)
|
||
fox.CQ = false
|
||
fox.Msg = "YV5ALI RR73; " + me + " <HK0/PY8WW> -08"
|
||
|
||
if !callingUs(fox, me) {
|
||
t.Error("the second half is a report to us and was not read as one")
|
||
}
|
||
if !callable(fox, me) {
|
||
t.Error("a station answering us was judged uncallable")
|
||
}
|
||
// Held as the target, that line must not release it — it is the answer we
|
||
// were waiting for, not a station working somebody else.
|
||
e := on()
|
||
e.OnPeriod(period(0, cq("HK0/PY8WW", NeedDXCC, -17, watched)))
|
||
if a := e.OnPeriod(period(2, fox)); a.Kind != DoNothing || e.Target() != "HK0/PY8WW" {
|
||
t.Errorf("dropped the DX as it answered us: %+v (target %q)", a, e.Target())
|
||
}
|
||
// The exchange is under way, so the attempt counter has done its job.
|
||
if e.Status().Attempts != 0 {
|
||
t.Errorf("attempts = %d while in QSO, want 0", e.Status().Attempts)
|
||
}
|
||
// And its final frame to us, in the same shape, ends the QSO.
|
||
done := fox
|
||
done.Msg = "IK2ABC RR73; " + me + " <HK0/PY8WW> RR73"
|
||
if !finished([]Candidate{done}, "HK0/PY8WW", me) {
|
||
t.Error("a 73 to us inside a multi-answer line did not end the QSO")
|
||
}
|
||
}
|
||
|
||
// Reported from the air twice: the DX sends RR73, our own 73 is about to go
|
||
// out, and the engine answered somebody else in that very slot — two
|
||
// transmissions in one period, and the station at the other end never got the
|
||
// frame that closes the contact.
|
||
func TestTheSlotAfterAQSOBelongsToOur73(t *testing.T) {
|
||
e := on()
|
||
e.OnPeriod(period(0, cq("HP/WE9G", NeedDXCC, -6)))
|
||
bye := callsMe("HP/WE9G", NeedDXCC, -6)
|
||
bye.Msg = "<" + me + "> HP/WE9G RR73"
|
||
// A new band is decoding in the same period and is not called.
|
||
a := e.OnPeriod(period(2, bye, cq("GW8DX", NeedBand, +2)))
|
||
if a.Kind != DoNothing {
|
||
t.Fatalf("called %+v in the slot our 73 goes out in", a)
|
||
}
|
||
if !strings.Contains(a.Reason, "73") {
|
||
t.Errorf("reason %q does not say why the slot was left alone", a.Reason)
|
||
}
|
||
// It is still there fifteen seconds later, which is the whole cost.
|
||
if a := e.OnPeriod(period(4, cq("GW8DX", NeedBand, +2))); a.Kind != DoReply || a.Decode.Call != "GW8DX" {
|
||
t.Errorf("next period: %+v, want the new band called", a)
|
||
}
|
||
}
|
||
|
||
func TestABetterStationTakesOverBeforeAnybodyHasAnswered(t *testing.T) {
|
||
e := on()
|
||
e.OnPeriod(period(0, cq("PLAIN", NeedSlot, +10)))
|
||
// A watched station comes on the air: nothing has answered us yet, so the
|
||
// call in progress is worth less than the one now possible.
|
||
a := e.OnPeriod(period(2, cq("PLAIN", NeedSlot, +10), cq("WATCHED", NeedNone, -15, watched)))
|
||
if a.Kind != DoReply || a.Decode.Call != "WATCHED" {
|
||
t.Fatalf("%+v — a better station did not take over", a)
|
||
}
|
||
|
||
// Equal rungs do NOT take over while the station being called is on the air.
|
||
e = on()
|
||
e.OnPeriod(period(4, cq("A", NeedBand, 0)))
|
||
if a := e.OnPeriod(period(6, cq("A", NeedBand, 0), cq("B", NeedBand, +20))); a.Kind != DoNothing {
|
||
t.Errorf("%+v — swapped between two stations of equal value", a)
|
||
}
|
||
// But they do when it is absent: seven calls into the void while a station
|
||
// of the same value is calling CQ is what the operator was watching.
|
||
if a := e.OnPeriod(period(8, cq("B", NeedBand, +20))); a.Kind != DoReply || a.Decode.Call != "B" {
|
||
t.Errorf("%+v — kept calling a station that was not on the air", a)
|
||
}
|
||
|
||
// Once the station has answered, nothing takes its place.
|
||
e = on()
|
||
e.OnPeriod(period(10, cq("DX", NeedSlot, 0)))
|
||
e.OnPeriod(period(12, callsMe("DX", NeedSlot, 0)))
|
||
if a := e.OnPeriod(period(14, callsMe("DX", NeedSlot, 0), cq("RARE", NeedDXCC, 0, watched))); a.Kind != DoNothing {
|
||
t.Errorf("%+v — abandoned an exchange in progress", a)
|
||
}
|
||
}
|
||
|
||
// TestAnExchangeInProgressIsNeverAbandoned is the shack report: mid-QSO with
|
||
// V31MA — its report decoded, our RR73 going out — and a station of a higher
|
||
// rung called us from the other side of the screen. The engine switched.
|
||
func TestAnExchangeInProgressIsNeverAbandoned(t *testing.T) {
|
||
e := on()
|
||
// V31MA is worth nothing on the ladder: worked before, nothing needed.
|
||
if a := e.OnPeriod(period(0, callsMe("V31MA", NeedNone, -12))); a.Kind != DoReply {
|
||
t.Fatalf("%+v — a station calling us was not answered", a)
|
||
}
|
||
// Its report arrives in the same period a better station calls us. That
|
||
// period used to be judged before the reply was taken into account.
|
||
a := e.OnPeriod(period(2,
|
||
callsMe("V31MA", NeedNone, -12),
|
||
callsMe("F5NNN", NeedSlot, -10)))
|
||
if a.Kind != DoNothing {
|
||
t.Fatalf("%+v — left V31MA mid-exchange", a)
|
||
}
|
||
if e.target == nil || e.target.Call != "V31MA" {
|
||
t.Fatalf("target is %v — the QSO in progress lost its place", e.target)
|
||
}
|
||
|
||
// Our own transmission settles it too: a report is not an opening call, so
|
||
// even a QSO the operator started by hand is protected.
|
||
e = on()
|
||
e.OnPeriod(period(4, cq("V31MA", NeedNone, -12, watched)))
|
||
e.NoteTX(TXState{Transmitting: true, Msg: "V31MA F4BPO RR73"})
|
||
if !e.answered {
|
||
t.Error("sending a report did not count as being inside the exchange")
|
||
}
|
||
if a := e.OnPeriod(period(6, cq("V31MA", NeedNone, -12, watched), callsMe("F5NNN", NeedDXCC, -10))); a.Kind != DoNothing {
|
||
t.Errorf("%+v — abandoned a QSO we were in the middle of", a)
|
||
}
|
||
if e.attempts != 0 {
|
||
t.Errorf("attempts=%d — an exchange frame was counted as a call", e.attempts)
|
||
}
|
||
}
|
||
|
||
func TestTheSlotIsNotWastedWhenTheTargetTurnsOutToBeBusy(t *testing.T) {
|
||
e := on()
|
||
if a := e.OnPeriod(period(0, cq("DX", NeedBand, -7))); a.Kind != DoReply {
|
||
t.Fatalf("%+v", a)
|
||
}
|
||
// It answers somebody else, and a station of the same value is calling CQ
|
||
// in the very same period. Waiting for the next one throws away a slot.
|
||
a := e.OnPeriod(period(2, busy("DX", NeedBand, -7), cq("ER1CW", NeedBand, -8)))
|
||
if a.Kind != DoReply || a.Decode.Call != "ER1CW" {
|
||
t.Fatalf("%+v — the freed slot was not used", a)
|
||
}
|
||
if !strings.Contains(a.Reason, "cannot answer") || !strings.Contains(a.Reason, "calling ER1CW") {
|
||
t.Errorf("reason %q says neither what was left nor what was taken", a.Reason)
|
||
}
|
||
|
||
// Mid-over, it still waits: cutting our own transmission in half is worse
|
||
// than losing the slot.
|
||
e = on()
|
||
e.OnPeriod(period(4, cq("DX", NeedBand, -7)))
|
||
pp := period(6, busy("DX", NeedBand, -7), cq("ER1CW", NeedBand, -8))
|
||
pp.TX = TXState{Transmitting: true}
|
||
if a := e.OnPeriod(pp); a.Kind != DoHalt || !a.Soft {
|
||
t.Errorf("%+v — replied over our own transmission", a)
|
||
}
|
||
}
|
||
|
||
// TestTheFirstCallStartsWithNoMisses is the shack report: a CQ answered for the
|
||
// first time, and the toolbar already reading two misses out of three.
|
||
func TestTheFirstCallStartsWithNoMisses(t *testing.T) {
|
||
e := on()
|
||
// It calls CQ on an odd slot; we answer.
|
||
if a := e.OnPeriod(period(1, cq("ER1CW", NeedBand, -2))); a.Kind != DoReply {
|
||
t.Fatalf("%+v", a)
|
||
}
|
||
// The next period is OURS: we are transmitting to it, and of course it is
|
||
// not decoded. That is not a miss.
|
||
pp := period(2)
|
||
pp.TX = TXState{Transmitting: true}
|
||
e.OnPeriod(pp)
|
||
if e.misses != 0 {
|
||
t.Fatalf("misses=%d after our own transmit period", e.misses)
|
||
}
|
||
// Nor is a period of the wrong parity with nothing in it.
|
||
e.OnPeriod(period(4))
|
||
if e.misses != 0 {
|
||
t.Fatalf("misses=%d — counted a period the station never transmits in", e.misses)
|
||
}
|
||
// ITS period, and it is not there: that is a miss.
|
||
e.OnPeriod(period(3))
|
||
if e.misses != 1 {
|
||
t.Fatalf("misses=%d — the station's own silent period was not counted", e.misses)
|
||
}
|
||
}
|