273 lines
9.1 KiB
Go
273 lines
9.1 KiB
Go
package dispenser
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// Golden frames reconstructed from K720_Dll.dll's big-endian length and XOR
|
|
// algorithm (SendCmd 0x100050C0, Query 0x10005280, SensorQuery 0x10005420).
|
|
var vendorFrames = []struct {
|
|
name string
|
|
command []byte
|
|
frame []byte
|
|
}{
|
|
{"AP", []byte{2, 'A', 'P'}, []byte{2, 0x30, 0x30, 0, 2, 0x41, 0x50, 3, 0x12}},
|
|
{"RF", []byte{2, 'R', 'F'}, []byte{2, 0x30, 0x30, 0, 2, 0x52, 0x46, 3, 0x17}},
|
|
{"FC7", commandFC7, []byte{2, 0x30, 0x30, 0, 3, 0x46, 0x43, 0x37, 3, 0x30}},
|
|
{"FC0", commandFC0, []byte{2, 0x30, 0x30, 0, 3, 0x46, 0x43, 0x30, 3, 0x37}},
|
|
{"RS", commandRS, []byte{2, 0x30, 0x30, 0, 2, 0x52, 0x53, 3, 2}},
|
|
}
|
|
|
|
func TestVendorOutboundFrames(t *testing.T) {
|
|
for _, tc := range vendorFrames {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
if got := createPacket([]byte("00"), tc.command); !bytes.Equal(got, tc.frame) {
|
|
t.Errorf("createPacket(%s) = % X, want % X", tc.name, got, tc.frame)
|
|
}
|
|
if got := calculateBCC(tc.frame[:len(tc.frame)-1]); got != tc.frame[len(tc.frame)-1] {
|
|
t.Errorf("calculateBCC(%s) = %02X, want %02X", tc.name, got, tc.frame[len(tc.frame)-1])
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
type scriptedTransport struct {
|
|
chunks [][]byte
|
|
writes [][]byte
|
|
readErr error
|
|
writeErr error
|
|
shortWrite int
|
|
afterRead func()
|
|
afterWrite func()
|
|
}
|
|
|
|
func (p *scriptedTransport) Read(b []byte) (int, error) {
|
|
if len(p.chunks) == 0 {
|
|
if p.readErr != nil {
|
|
return 0, p.readErr
|
|
}
|
|
return 0, io.EOF
|
|
}
|
|
n := copy(b, p.chunks[0])
|
|
p.chunks[0] = p.chunks[0][n:]
|
|
if len(p.chunks[0]) == 0 {
|
|
p.chunks = p.chunks[1:]
|
|
}
|
|
if p.afterRead != nil {
|
|
p.afterRead()
|
|
}
|
|
return n, nil
|
|
}
|
|
func (p *scriptedTransport) Write(b []byte) (int, error) {
|
|
p.writes = append(p.writes, append([]byte(nil), b...))
|
|
if p.afterWrite != nil {
|
|
p.afterWrite()
|
|
}
|
|
if p.writeErr != nil {
|
|
return 0, p.writeErr
|
|
}
|
|
if p.shortWrite == len(p.writes) {
|
|
return len(b) - 1, nil
|
|
}
|
|
return len(b), nil
|
|
}
|
|
func transportAddress(t *testing.T) {
|
|
t.Helper()
|
|
old := Address
|
|
Address = []byte("00")
|
|
t.Cleanup(func() { Address = old })
|
|
}
|
|
|
|
// Independent SF response vectors: status is 30 30 30 [33].
|
|
var vendorAP = []byte{2, 0x30, 0x30, 0, 6, 'S', 'F', 0x30, 0x30, 0x30, 0x33, 3, 0x11}
|
|
var vendorRF = []byte{2, 0x30, 0x30, 0, 5, 'S', 'F', 0x30, 0x30, 0x30, 3, 0x21}
|
|
var vendorACK = []byte{6, 0x30, 0x30}
|
|
|
|
func TestVendorQueryFragmentation(t *testing.T) {
|
|
transportAddress(t)
|
|
for i, frame := range [][]byte{vendorAP, vendorRF} {
|
|
wire := append(append([]byte(nil), vendorACK...), frame...)
|
|
for split := 1; split < len(wire); split++ {
|
|
t.Run(fmt.Sprintf("%s/split%d", vendorFrames[i].name, split), func(t *testing.T) {
|
|
p := &scriptedTransport{chunks: [][]byte{wire[:split], wire[split:]}}
|
|
got, err := queryStatus(context.Background(), p, vendorFrames[i].command, 4-i, 0)
|
|
if err != nil || !bytes.Equal(got, frame[7:len(frame)-2]) {
|
|
t.Fatalf("queryStatus(split=%d) = % X, %v, want % X, nil", split, got, err, frame[7:len(frame)-2])
|
|
}
|
|
if len(p.writes) != 2 || !bytes.Equal(p.writes[0], vendorFrames[i].frame) || !bytes.Equal(p.writes[1], []byte{5, 0x30, 0x30}) {
|
|
t.Errorf("queryStatus writes = % X, want command then ENQ", p.writes)
|
|
}
|
|
})
|
|
}
|
|
t.Run(vendorFrames[i].name+"/one-byte", func(t *testing.T) {
|
|
p := &scriptedTransport{}
|
|
for _, b := range wire {
|
|
p.chunks = append(p.chunks, []byte{b})
|
|
}
|
|
if _, err := queryStatus(context.Background(), p, vendorFrames[i].command, 4-i, 0); err != nil {
|
|
t.Errorf("queryStatus(one-byte reads) = %v, want nil", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestVendorQueryRejectsInvalidFrames(t *testing.T) {
|
|
transportAddress(t)
|
|
for _, tc := range []struct {
|
|
name string
|
|
offset int
|
|
value byte
|
|
}{
|
|
{"STX", 0, 1}, {"address high", 1, '1'}, {"address low", 2, '1'},
|
|
{"zero length", 4, 0}, {"short length", 4, 5}, {"long length", 4, 7}, {"high length", 3, 0xff},
|
|
{"type S", 5, 'X'}, {"type F", 6, 'X'}, {"ETX", 11, 4}, {"BCC", 12, 0},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
frame := append([]byte(nil), vendorAP...)
|
|
frame[tc.offset] = tc.value
|
|
// Keep checksum valid when testing type/ETX to isolate those checks.
|
|
if tc.offset == 5 || tc.offset == 6 || tc.offset == 11 {
|
|
frame[12] = 0
|
|
for _, b := range frame[:12] {
|
|
frame[12] ^= b
|
|
}
|
|
}
|
|
p := &scriptedTransport{chunks: [][]byte{vendorACK, frame}}
|
|
if got, err := queryStatus(context.Background(), p, vendorFrames[0].command, 4, 0); err == nil || got != nil {
|
|
t.Errorf("queryStatus(%s) = % X, %v, want nil/error", tc.name, got, err)
|
|
}
|
|
if len(p.writes) != 2 {
|
|
t.Errorf("queryStatus(%s) writes=%d, want 2 without resend", tc.name, len(p.writes))
|
|
}
|
|
})
|
|
}
|
|
for n := 0; n < len(vendorAP); n++ {
|
|
t.Run(fmt.Sprintf("truncated%d", n), func(t *testing.T) {
|
|
p := &scriptedTransport{chunks: [][]byte{vendorACK, vendorAP[:n]}}
|
|
if _, err := queryStatus(context.Background(), p, vendorFrames[0].command, 4, 0); err == nil {
|
|
t.Errorf("queryStatus(%d-byte frame) succeeded, want error", n)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestTransportACKAndMechanicalDispatch(t *testing.T) {
|
|
transportAddress(t)
|
|
for _, tc := range vendorFrames[2:] {
|
|
p := &scriptedTransport{chunks: [][]byte{{6}, {'0'}, {'0'}}}
|
|
if err := dispatchCommand(context.Background(), p, tc.command, 0); err != nil {
|
|
t.Fatalf("dispatchCommand(%s)=%v, want nil", tc.name, err)
|
|
}
|
|
if len(p.writes) != 2 || !bytes.Equal(p.writes[0], tc.frame) || !bytes.Equal(p.writes[1], []byte{5, '0', '0'}) {
|
|
t.Errorf("dispatchCommand(%s) writes=% X, want command then ENQ", tc.name, p.writes)
|
|
}
|
|
}
|
|
for _, ack := range [][]byte{{0x15, '0', '0'}, {6, '1', '0'}, {6, '0', '1'}, {6}, {6, '0'}, {}} {
|
|
p := &scriptedTransport{chunks: [][]byte{ack}}
|
|
if err := dispatchCommand(context.Background(), p, commandFC7, 0); err == nil {
|
|
t.Errorf("dispatchCommand(ACK=% X) succeeded, want error", ack)
|
|
}
|
|
if len(p.writes) != 1 {
|
|
t.Errorf("dispatchCommand(ACK=% X) writes=%d, want 1", ack, len(p.writes))
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTransportShortWritesAndErrors(t *testing.T) {
|
|
transportAddress(t)
|
|
failure := errors.New("serial failure")
|
|
for _, stage := range []int{1, 2} {
|
|
p := &scriptedTransport{chunks: [][]byte{vendorACK}, shortWrite: stage}
|
|
if err := dispatchCommand(context.Background(), p, commandFC7, 0); !errors.Is(err, io.ErrShortWrite) {
|
|
t.Errorf("dispatchCommand(short write %d)=%v, want ErrShortWrite", stage, err)
|
|
}
|
|
if len(p.writes) != stage {
|
|
t.Errorf("short write %d writes=%d, want %d", stage, len(p.writes), stage)
|
|
}
|
|
}
|
|
for _, p := range []*scriptedTransport{{writeErr: failure}, {readErr: failure}, {chunks: [][]byte{{}}}} {
|
|
if err := dispatchCommand(context.Background(), p, commandRS, 0); err == nil {
|
|
t.Error("dispatchCommand(I/O failure) succeeded, want error")
|
|
}
|
|
if len(p.writes) != 1 {
|
|
t.Errorf("dispatchCommand(I/O failure) writes=%d, want 1", len(p.writes))
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTransportCancellation(t *testing.T) {
|
|
transportAddress(t)
|
|
for _, stage := range []string{"before write", "processing wait", "after ACK", "after ENQ", "after header"} {
|
|
t.Run(stage, func(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
p := &scriptedTransport{chunks: [][]byte{vendorACK, vendorAP}}
|
|
wantWrites := 1
|
|
switch stage {
|
|
case "before write":
|
|
cancel()
|
|
wantWrites = 0
|
|
case "processing wait":
|
|
p.afterWrite = cancel
|
|
case "after ACK":
|
|
p.afterRead = cancel
|
|
case "after ENQ":
|
|
wantWrites = 2
|
|
p.afterWrite = func() {
|
|
if len(p.writes) == 2 {
|
|
cancel()
|
|
}
|
|
}
|
|
case "after header":
|
|
wantWrites = 2
|
|
reads := 0
|
|
p.afterRead = func() {
|
|
reads++
|
|
if reads == 2 {
|
|
cancel()
|
|
}
|
|
}
|
|
}
|
|
if _, err := queryStatus(ctx, p, vendorFrames[0].command, 4, 0); !errors.Is(err, context.Canceled) {
|
|
t.Errorf("queryStatus(cancel %s)=%v, want Canceled", stage, err)
|
|
}
|
|
if len(p.writes) != wantWrites {
|
|
t.Errorf("queryStatus(cancel %s) writes=%d, want %d", stage, len(p.writes), wantWrites)
|
|
}
|
|
})
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
|
|
defer cancel()
|
|
p := &scriptedTransport{}
|
|
if err := dispatchCommand(ctx, p, commandRS, time.Second); !errors.Is(err, context.DeadlineExceeded) {
|
|
t.Errorf("dispatchCommand(deadline in wait)=%v, want DeadlineExceeded", err)
|
|
}
|
|
if len(p.writes) != 1 {
|
|
t.Errorf("dispatchCommand(deadline) writes=%d, want 1", len(p.writes))
|
|
}
|
|
}
|
|
|
|
func TestTransportTrailingDataNotConsumedAsStatus(t *testing.T) {
|
|
transportAddress(t)
|
|
frame := append(append([]byte(nil), vendorAP...), 0xff, 0xfe, 0xfd)
|
|
p := &scriptedTransport{chunks: [][]byte{vendorACK, frame}}
|
|
if _, err := queryStatus(context.Background(), p, vendorFrames[0].command, 4, 0); err != nil {
|
|
t.Fatalf("queryStatus(frame with trailing bytes)=%v, want nil", err)
|
|
}
|
|
if len(p.chunks) != 1 || !bytes.Equal(p.chunks[0], []byte{0xff, 0xfe, 0xfd}) {
|
|
t.Fatalf("remaining bytes=% X, want FF FE FD", p.chunks)
|
|
}
|
|
if err := dispatchCommand(context.Background(), p, commandFC7, 0); err == nil {
|
|
t.Error("dispatchCommand(trailing garbage) succeeded, want invalid ACK")
|
|
}
|
|
if len(p.writes) != 3 {
|
|
t.Errorf("writes after trailing garbage=%d, want 3 (no ENQ/resend)", len(p.writes))
|
|
}
|
|
}
|