418 lines
14 KiB
Go
418 lines
14 KiB
Go
package dispenser
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"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 TestACKScannerFragmentation(t *testing.T) {
|
|
transportAddress(t)
|
|
for _, address := range []string{"00", "15"} {
|
|
Address = []byte(address)
|
|
wrong := []byte("15")
|
|
if address == "15" {
|
|
wrong = []byte("00")
|
|
}
|
|
for _, prefix := range [][]byte{nil, {0x30}, {0x10}, {address[1]}, {ACK, wrong[0], wrong[1], 0x30}, {NAK, wrong[0], wrong[1]}} {
|
|
for _, token := range []byte{ACK, NAK} {
|
|
wire := append(append([]byte(nil), prefix...), token, Address[0], Address[1])
|
|
for mask := 0; mask < 1<<(len(wire)-1); mask++ {
|
|
t.Run(fmt.Sprintf("%s/%X/%d", address, wire, mask), func(t *testing.T) {
|
|
p := &scriptedTransport{}
|
|
start := 0
|
|
for i := 1; i < len(wire); i++ {
|
|
if mask&(1<<(i-1)) != 0 {
|
|
p.chunks = append(p.chunks, wire[start:i])
|
|
start = i
|
|
}
|
|
}
|
|
p.chunks = append(p.chunks, wire[start:])
|
|
err := dispatchCommand(context.Background(), p, commandFC7, 0)
|
|
if (err != nil) != (token == NAK) {
|
|
t.Fatalf("dispatchCommand(% X)=%v", wire, err)
|
|
}
|
|
want := [][]byte{createPacket(Address, commandFC7)}
|
|
if token == ACK {
|
|
want = append(want, append([]byte{ENQ}, Address...))
|
|
}
|
|
if len(p.writes) != len(want) {
|
|
t.Fatalf("writes=% X want=% X", p.writes, want)
|
|
}
|
|
for i := range want {
|
|
if !bytes.Equal(p.writes[i], want[i]) {
|
|
t.Errorf("write=% X want=% X", p.writes[i], want[i])
|
|
}
|
|
}
|
|
if len(p.chunks) != 0 {
|
|
t.Errorf("unread token bytes=% X", p.chunks)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestACKScannerBoundsAndFollowingTransaction(t *testing.T) {
|
|
transportAddress(t)
|
|
for _, address := range []string{"00", "15"} {
|
|
Address = []byte(address)
|
|
ack := append([]byte{ACK}, Address...)
|
|
for _, leading := range []int{61, 62, 64} {
|
|
wire := append(bytes.Repeat([]byte{0xff}, leading), ack...)
|
|
p := &scriptedTransport{chunks: [][]byte{wire}}
|
|
err := dispatchCommand(context.Background(), p, commandRS, 0)
|
|
if (err == nil) != (leading == 61) {
|
|
t.Errorf("leading=%d error=%v", leading, err)
|
|
}
|
|
if leading != 61 && (len(p.writes) != 1 || len(bytes.Join(p.chunks, nil)) != len(wire)-64) {
|
|
t.Fatalf("scan exceeded bound: writes=% X remaining=% X", p.writes, p.chunks)
|
|
}
|
|
}
|
|
wire := append(append(append([]byte{0x10}, ack...), ack...), 0xfe)
|
|
p := &scriptedTransport{chunks: [][]byte{wire}}
|
|
for i := 0; i < 2; i++ {
|
|
if err := dispatchCommand(context.Background(), p, commandFC7, 0); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if len(p.writes) != 4 || !bytes.Equal(bytes.Join(p.chunks, nil), []byte{0xfe}) {
|
|
t.Fatalf("transaction boundary lost: writes=% X remaining=% X", p.writes, p.chunks)
|
|
}
|
|
}
|
|
}
|
|
|
|
type ackReadErrorTransport struct {
|
|
*scriptedTransport
|
|
reads, failAt int
|
|
err error
|
|
}
|
|
|
|
func (p *ackReadErrorTransport) Read(b []byte) (int, error) {
|
|
p.reads++
|
|
n, err := p.scriptedTransport.Read(b)
|
|
if p.reads == p.failAt {
|
|
return n, p.err
|
|
}
|
|
return n, err
|
|
}
|
|
|
|
func TestACKScannerReadErrors(t *testing.T) {
|
|
transportAddress(t)
|
|
for _, address := range []string{"00", "15"} {
|
|
Address = []byte(address)
|
|
for _, token := range []byte{ACK, NAK} {
|
|
for _, readErr := range []error{io.EOF, os.ErrDeadlineExceeded, io.ErrClosedPipe} {
|
|
for _, failAt := range []int{1, 2} {
|
|
p := &ackReadErrorTransport{scriptedTransport: &scriptedTransport{chunks: [][]byte{{token, Address[0], Address[1]}}}, failAt: failAt, err: readErr}
|
|
err := dispatchCommand(context.Background(), p, commandFC7, 0)
|
|
if failAt == 1 && !errors.Is(err, readErr) {
|
|
t.Fatalf("incomplete token error=%v want=%v", err, readErr)
|
|
}
|
|
if failAt == 2 && ((err == nil) != (token == ACK) || errors.Is(err, readErr)) {
|
|
t.Fatalf("complete token %02X with read error returned %v", token, err)
|
|
}
|
|
wantWrites := 1
|
|
if failAt == 2 && token == ACK {
|
|
wantWrites = 2
|
|
}
|
|
if len(p.writes) != wantWrites || p.reads != failAt {
|
|
t.Fatalf("writes=%d reads=%d want=%d/%d", len(p.writes), p.reads, wantWrites, failAt)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
for _, wire := range [][]byte{nil, {0x30, ACK, Address[0]}, {0xff, 0xfe}, {ACK, Address[0]}} {
|
|
p := &scriptedTransport{chunks: [][]byte{wire}}
|
|
if err := dispatchCommand(context.Background(), p, commandFC7, 0); err == nil || len(p.writes) != 1 {
|
|
t.Fatalf("incomplete/garbage % X: err=%v writes=% X", wire, err, p.writes)
|
|
}
|
|
}
|
|
for _, cancelAt := range []int{1, 2} {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
p := &ackReadErrorTransport{scriptedTransport: &scriptedTransport{chunks: [][]byte{{ACK, Address[0], Address[1]}}}, failAt: 2, err: io.EOF}
|
|
p.afterRead = func() {
|
|
if p.reads == cancelAt {
|
|
cancel()
|
|
}
|
|
}
|
|
err := dispatchCommand(ctx, p, commandFC7, 0)
|
|
cancel()
|
|
if !errors.Is(err, context.Canceled) || len(p.writes) != 1 || p.reads != cancelAt {
|
|
t.Fatalf("cancel read %d: err=%v writes=%d reads=%d", cancelAt, err, len(p.writes), p.reads)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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 = func() {
|
|
if len(p.chunks) == 1 {
|
|
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 == 3 {
|
|
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))
|
|
}
|
|
}
|