package steppir import ( "testing" "time" ) // The controller says nothing until its own poll comes round, so a move has to // be reported from the moment it is COMMANDED. Without it the app's "block TX // while the elements travel" had a hole: the grace covering the command ran out // before the first poll that would have seen the movement, and the transmitter // was released in the middle of a move. func TestAJustCommandedMoveReportsMotion(t *testing.T) { c := &Client{} c.lastStatus = &Status{Connected: true} // idle, as the controller last said if st, _ := c.GetStatus(); st.MotorsMoving != 0 { t.Fatal("an idle antenna reports motion") } c.statusMu.Lock() c.moveUntil = time.Now().Add(moveOptimisticWindow) c.statusMu.Unlock() st, _ := c.GetStatus() if st.MotorsMoving == 0 { t.Error("a move commanded a moment ago is not reported as motion") } if c.lastStatus.MotorsMoving != 0 { t.Error("the optimistic flag leaked into the cached status the poll owns") } // Bounded: an antenna that never reports motion must not latch the transmit // inhibit on for ever. c.statusMu.Lock() c.moveUntil = time.Now().Add(-time.Second) c.statusMu.Unlock() if st, _ := c.GetStatus(); st.MotorsMoving != 0 { t.Error("the optimistic window never expires") } } // Retract and Calibrate move every element, and they were the two commands that // reported nothing at all: no "moving" on screen, the poll left on its // two-second idle cadence instead of speeding up to watch, and no transmit // inhibit while the elements travelled. Reported by an operator whose retract // looked inert next to an Ultrabeam's. func TestRetractAndCalibrateReportMotion(t *testing.T) { for _, tc := range []struct { name string call func(*Client) error }{ {"retract", (*Client).Retract}, {"calibrate", (*Client).Calibrate}, } { c := &Client{} c.lastStatus = &Status{Connected: true, Frequency: 14074} // No connection, so the write fails and the command returns an error — // which is the point: a command that did NOT reach the controller must // not claim the antenna is moving. if err := tc.call(c); err == nil { t.Fatalf("%s: expected an error with no connection", tc.name) } if st, _ := c.GetStatus(); st.MotorsMoving != 0 { t.Errorf("%s: a command that failed to send reports motion", tc.name) } // And with the write accepted, motion is reported at once. c.markMoving(retractOptimisticWindow) if st, _ := c.GetStatus(); st.MotorsMoving == 0 { t.Errorf("%s: a commanded move is not reported as motion", tc.name) } } } // The retract bridge is longer than the tune bridge: winding every element into // its hub takes tens of seconds, and a retract drops the controller out of // AUTOTRACK, where its motor-bit reporting is less predictable. func TestRetractBridgeOutlastsTheTuneBridge(t *testing.T) { if retractOptimisticWindow <= moveOptimisticWindow { t.Errorf("retract window %v is not longer than the tune window %v", retractOptimisticWindow, moveOptimisticWindow) } // Bounded all the same — see the note on the constant. if retractOptimisticWindow > time.Minute { t.Errorf("retract window %v could latch the transmit inhibit on", retractOptimisticWindow) } } // markMoving extends, never shortens: a tune issued a moment after a retract // must not cut the retract's bridge down to the tune's. func TestMarkMovingOnlyExtends(t *testing.T) { c := &Client{} c.markMoving(retractOptimisticWindow) far := c.moveUntil c.markMoving(moveOptimisticWindow) if c.moveUntil.Before(far) { t.Error("a shorter bridge shortened a longer one") } }