package ultrabeam import ( "bufio" "bytes" "net" "testing" "time" ) // sendCommand must flush any bytes already in the stream (a reply left by an // earlier timed-out command) before reading, then return the reply to THIS // command. The antenna does not echo our sequence number, so keeping the stream // clean is the only way to stay in sync. func TestSendCommandFlushesStaleThenReadsReply(t *testing.T) { srvConn, cliConn := net.Pipe() defer srvConn.Close() defer cliConn.Close() c := &Client{conn: cliConn, reader: bufio.NewReader(cliConn)} srv := &Client{reader: bufio.NewReader(srvConn)} errc := make(chan error, 1) go func() { // Leftover from a "previous" command still sitting in the stream. The Write // blocks until drainStale consumes it, so it is guaranteed flushed before // the real exchange. if _, err := srvConn.Write(c.buildPacket(9, UB_OK, []byte{0xDE, 0xAD})); err != nil { errc <- err return } // Now serve the actual request. req, err := srv.readPacket() if err != nil { errc <- err return } seq, _, _, err := parsePacket(req) if err != nil { errc <- err return } _, err = srvConn.Write(c.buildPacket(seq, UB_OK, []byte{0x11, 0x22, 0x33})) errc <- err }() payload, err := c.sendCommand(CMD_STATUS, nil) if err != nil { t.Fatalf("sendCommand: %v", err) } if !bytes.Equal(payload, []byte{0x11, 0x22, 0x33}) { t.Fatalf("payload = % X, want 11 22 33 — stale reply not flushed", payload) } if err := <-errc; err != nil { t.Fatalf("server: %v", err) } } // readPacket must resynchronise to the next STX, dropping any partial/garbage // bytes left in the stream, so one corrupt frame can't misalign every frame // after it. func TestReadPacketResyncsToSTX(t *testing.T) { frame := (&Client{}).buildPacket(5, UB_OK, []byte{0x01, 0x02}) stream := append([]byte{0x11, 0x22, 0x33}, frame...) // leading garbage, then a real frame c := &Client{reader: bufio.NewReader(bytes.NewReader(stream))} got, err := c.readPacket() if err != nil { t.Fatalf("readPacket: %v", err) } seq, cmd, payload, err := parsePacket(got) if err != nil { t.Fatalf("parsePacket: %v", err) } if seq != 5 || cmd != UB_OK || !bytes.Equal(payload, []byte{0x01, 0x02}) { t.Fatalf("seq=%d cmd=%d payload=% X, want 5 / OK / 01 02", seq, cmd, payload) } } // A commanded move must report motion immediately (before the next status poll), // then fall back to the real motor state once the window elapses. func TestOptimisticMotionAfterCommand(t *testing.T) { c := &Client{lastStatus: &Status{Connected: true, MotorsMoving: 0}} c.moveCmdAt = time.Now() if st, _ := c.GetStatus(); st.MotorsMoving == 0 { t.Fatal("just after a move command, GetStatus should report motion") } c.moveCmdAt = time.Now().Add(-ubMoveOptimisticWindow - time.Second) if st, _ := c.GetStatus(); st.MotorsMoving != 0 { t.Fatal("past the window with motors idle, GetStatus must report no motion") } }