sendCommand discarded the reply's sequence number and accepted whatever frame came back. On a slow remote link a command that timed out left its late reply in the stream, and the NEXT command read it as its own — crossing STATUS with READ_BANDS/PROGRESS. That surfaced as phantom frequency jumps (a spurious follow-loop re-tune), intermittent "reply too short" disconnects, and wrong element-length readings (READ_BANDS getting the status frame). Now every reply is matched to its request seq and stale/malformed frames are drained (bounded), with readPacket resyncing to the next STX. Tested with a net.Pipe that injects a stale reply ahead of the real one.
81 lines
2.3 KiB
Go
81 lines
2.3 KiB
Go
package ultrabeam
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"net"
|
|
"testing"
|
|
)
|
|
|
|
// A stale reply left in the stream by an earlier timed-out command must be
|
|
// DRAINED, not taken for this command's reply. Without seq matching, a STATUS
|
|
// query would return a lingering READ_BANDS/PROGRESS payload — which showed up
|
|
// on a remote link as phantom frequencies (a spurious follow-loop re-tune) and
|
|
// dropped connections.
|
|
func TestSendCommandDrainsStaleReplyAndMatchesSeq(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() {
|
|
// Read the request, learn its seq, then answer with a stale reply (wrong
|
|
// seq) FIRST, then the real one.
|
|
req, err := srv.readPacket()
|
|
if err != nil {
|
|
errc <- err
|
|
return
|
|
}
|
|
seq, _, _, err := parsePacket(req)
|
|
if err != nil {
|
|
errc <- err
|
|
return
|
|
}
|
|
staleSeq := (seq + 1) % 128
|
|
if _, err := srvConn.Write(c.buildPacket(staleSeq, UB_OK, []byte{0xAA, 0xBB})); err != nil {
|
|
errc <- err
|
|
return
|
|
}
|
|
if _, err := srvConn.Write(c.buildPacket(seq, UB_OK, []byte{0x11, 0x22, 0x33})); err != nil {
|
|
errc <- err
|
|
return
|
|
}
|
|
errc <- nil
|
|
}()
|
|
|
|
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 drained or seq not matched", 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)
|
|
}
|
|
}
|