package steppir import ( "errors" "io" "testing" "time" ) // silentPort is a serial port that has stopped answering: every read times out. // On Windows that is reported as (0, nil) — a timeout is not an error on this // transport — which is precisely what io.ReadFull cannot survive. type silentPort struct{ reads int } func (p *silentPort) Read(b []byte) (int, error) { p.reads++ time.Sleep(5 * time.Millisecond) // stand in for the port's read timeout return 0, nil } func (p *silentPort) Write(b []byte) (int, error) { return len(b), nil } func (p *silentPort) Close() error { return nil } // A controller that goes quiet must make the read FAIL, not hang. Hanging held // the io mutex, so the poll loop never reported a fault and every operator // command blocked behind it: the antenna stopped responding and the log had // nothing in it. func TestReadFrameGivesUpOnASilentController(t *testing.T) { done := make(chan error, 1) go func() { done <- readFrame(&silentPort{}, make([]byte, 11), 100*time.Millisecond) }() select { case err := <-done: if err == nil { t.Fatal("a silent controller was reported as a good frame") } case <-time.After(3 * time.Second): t.Fatal("readFrame never returned — the driver is wedged exactly as it was in the field") } } // dribblePort delivers the frame a few bytes at a time, with empty reads in // between — a slow 4800-baud link, which must still assemble one frame. type dribblePort struct { data []byte step int idle int // empty reads before each chunk n int } func (p *dribblePort) Read(b []byte) (int, error) { if p.n < p.idle { p.n++ return 0, nil } p.n = 0 if len(p.data) == 0 { return 0, nil } k := p.step if k > len(p.data) { k = len(p.data) } if k > len(b) { k = len(b) } copy(b, p.data[:k]) p.data = p.data[k:] return k, nil } func (p *dribblePort) Write(b []byte) (int, error) { return len(b), nil } func (p *dribblePort) Close() error { return nil } func TestReadFrameAssemblesASlowFrame(t *testing.T) { want := []byte{'@', 'A', 0x00, 0x20, 0x1E, 0xA8, 0x00, 0x05, 0x30, 0x37, 0x0D} p := &dribblePort{data: append([]byte(nil), want...), step: 3, idle: 2} buf := make([]byte, 11) if err := readFrame(p, buf, time.Second); err != nil { t.Fatalf("readFrame: %v", err) } for i := range want { if buf[i] != want[i] { t.Fatalf("read % X, want % X", buf, want) } } } // A truncated frame is a failure, not a frame. The controller sending 5 bytes // and stopping used to spin forever on the missing 6. func TestReadFrameRejectsATruncatedFrame(t *testing.T) { p := &dribblePort{data: []byte{'@', 'A', 0x00, 0x20, 0x1E}, step: 5} err := readFrame(p, make([]byte, 11), 100*time.Millisecond) if err == nil { t.Fatal("a 5-byte frame was accepted as 11") } } // A real error still comes straight back. func TestReadFrameReturnsPortErrors(t *testing.T) { want := errors.New("port closed") p := errPort{err: want} if err := readFrame(p, make([]byte, 11), time.Second); !errors.Is(err, want) { t.Fatalf("err = %v, want %v", err, want) } } type errPort struct{ err error } func (p errPort) Read([]byte) (int, error) { return 0, p.err } func (p errPort) Write(b []byte) (int, error) { return len(b), nil } func (p errPort) Close() error { return nil } var _ io.ReadWriteCloser = errPort{}