243 lines
7.4 KiB
Go
243 lines
7.4 KiB
Go
package dispenser
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestStrictAPFailuresBecomeUnusablePreparationObservations(t *testing.T) {
|
|
transportAddress(t)
|
|
cases := []struct {
|
|
name string
|
|
chunks [][]byte
|
|
readErr error
|
|
}{
|
|
{"invalid ACK 10 06 30", [][]byte{{0x10, 0x06, 0x30}}, nil},
|
|
{"truncated", [][]byte{vendorACK, vendorAP[:8]}, nil},
|
|
{"no response", nil, nil},
|
|
{"timeout", nil, context.DeadlineExceeded},
|
|
{"read failure", nil, io.ErrClosedPipe},
|
|
}
|
|
for _, offset := range []int{0, 1, 2, 3, 4, 5, 6, 11, 12} {
|
|
frame := append([]byte(nil), vendorAP...)
|
|
frame[offset] ^= 0xff
|
|
if offset == 5 || offset == 6 || offset == 11 {
|
|
frame[12] = calculateBCC(frame[:12])
|
|
}
|
|
cases = append(cases, struct {
|
|
name string
|
|
chunks [][]byte
|
|
readErr error
|
|
}{fmt.Sprintf("frame byte %d", offset), [][]byte{vendorACK, frame}, nil})
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
p := &scriptedTransport{chunks: tc.chunks, readErr: tc.readErr}
|
|
st, err := queryStatus(context.Background(), p, vendorFrames[0].command, 4, 0)
|
|
if err == nil {
|
|
t.Fatal("strict queryStatus accepted invalid transaction")
|
|
}
|
|
c, d := newSequenceTestClient(t, cmdResp{status: st, err: err}, cmdResp{status: status(0x37)})
|
|
if _, err := c.PrepareCurrentCard(context.Background()); err != nil {
|
|
t.Errorf("preparation after %s = %v, want opportunity", tc.name, err)
|
|
}
|
|
if commandCount(d.commands, cmdToEncoder) != 1 || commandCount(d.commands, cmdReset) != 0 {
|
|
t.Errorf("commands=%v, want one FC7 and no RS", d.commands)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestUnusableObservationsReclassifyAndShareUncertainty(t *testing.T) {
|
|
for _, later := range []byte{0x38, 0x30, 0x37, 0x35, 0x40} {
|
|
t.Run(fmt.Sprintf("later %X", later), func(t *testing.T) {
|
|
responses := []cmdResp{{err: io.ErrUnexpectedEOF}, {status: status(0x40)}}
|
|
responses = append(responses, positionResponses(later, 25)...)
|
|
c, d := newSequenceTestClient(t, responses...)
|
|
_, err := c.PrepareCurrentCard(context.Background())
|
|
var want error
|
|
if later == 0x38 {
|
|
want = ErrCardWellEmpty
|
|
}
|
|
if later == 0x30 {
|
|
want = ErrPreparationExhausted
|
|
}
|
|
if !errors.Is(err, want) {
|
|
t.Errorf("later %X error=%v, want %v", later, err, want)
|
|
}
|
|
resets := 0
|
|
if later == 0x30 {
|
|
resets = 3
|
|
}
|
|
if commandCount(d.commands, cmdReset) != resets {
|
|
t.Errorf("later %X commands=%v, want %d RS", later, d.commands, resets)
|
|
}
|
|
})
|
|
}
|
|
c, d := newSequenceTestClient(t, cmdResp{err: io.EOF}, cmdResp{err: io.EOF}, cmdResp{status: status(0x35)}, cmdResp{status: status(0x40)}, cmdResp{err: io.EOF}, cmdResp{status: status(0x39)})
|
|
start := c.now()
|
|
if _, err := c.PrepareCurrentCard(context.Background()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if elapsed := c.now().Sub(start); elapsed != sequenceUncertainWait {
|
|
t.Errorf("mixed uncertainty elapsed=%v, want %v", elapsed, sequenceUncertainWait)
|
|
}
|
|
if commandCount(d.commands, cmdToEncoder) != 1 {
|
|
t.Errorf("mixed uncertainty commands=%v, want one FC7", d.commands)
|
|
}
|
|
}
|
|
|
|
func TestObservationAtPreparationDeadline(t *testing.T) {
|
|
for _, last := range []cmdResp{{err: io.EOF}, {status: status(0x40)}, {status: status(0x35)}, {status: status(0x30)}, {status: status(0x38)}} {
|
|
c, d := newSequenceTestClient(t, cmdResp{status: status(0x30)}, last)
|
|
now := c.now()
|
|
c.sequenceTiming.now = func() time.Time { return now }
|
|
d.onCommand = func(cmd cmdType) {
|
|
if cmd == cmdStatus && commandCount(d.commands, cmdStatus) == 2 {
|
|
now = now.Add(sequenceTimeout)
|
|
}
|
|
}
|
|
_, err := c.PrepareCurrentCard(context.Background())
|
|
var want error
|
|
if len(last.status) == 4 && last.status[3] == 0x30 {
|
|
want = ErrPreparationExhausted
|
|
}
|
|
if len(last.status) == 4 && last.status[3] == 0x38 {
|
|
want = ErrCardWellEmpty
|
|
}
|
|
if !errors.Is(err, want) {
|
|
t.Errorf("deadline observation=%v error=%v, want %v", last, err, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestWorkerClearanceFallbackAndMovement(t *testing.T) {
|
|
for _, mode := range []string{"unusable", "ambiguous", "movement", "movement then unusable", "empty", "cancel"} {
|
|
t.Run(mode, func(t *testing.T) {
|
|
p := &scriptedTransport{}
|
|
for i := 0; i < 6; i++ {
|
|
switch mode {
|
|
case "unusable", "cancel":
|
|
p.chunks = append(p.chunks, []byte{0x10, 0x06, 0x30})
|
|
case "ambiguous":
|
|
p.chunks = append(p.chunks, vendorACK, apReply(status(0x37)))
|
|
case "empty":
|
|
p.chunks = append(p.chunks, vendorACK, apReply(status(0x38)))
|
|
default:
|
|
if mode == "movement then unusable" && i > 0 {
|
|
p.chunks = append(p.chunks, []byte{0x10, 0x06, 0x30})
|
|
} else {
|
|
p.chunks = append(p.chunks, vendorACK, apReply([]byte{0x31, 0x30, 0x30, 0x34}))
|
|
}
|
|
}
|
|
}
|
|
c, now := deliveryWorkerClient(t, p)
|
|
c.sequenceTiming.wait = func(ctx context.Context, d time.Duration) error {
|
|
if ctx.Err() != nil {
|
|
return ctx.Err()
|
|
}
|
|
*now = now.Add(2 * d)
|
|
return nil
|
|
}
|
|
c.deliveryPending = true
|
|
c.deliveryStarted = c.now()
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
if mode == "cancel" {
|
|
p.afterRead = cancel
|
|
}
|
|
r := c.doResponse(ctx, cmdDeliveryClearance)
|
|
if mode == "cancel" {
|
|
if !errors.Is(r.err, context.Canceled) {
|
|
t.Errorf("cancel=%v", r.err)
|
|
}
|
|
return
|
|
}
|
|
var want error
|
|
if mode == "movement" {
|
|
want = context.DeadlineExceeded
|
|
}
|
|
if mode == "empty" {
|
|
want = ErrCardWellEmpty
|
|
}
|
|
if !errors.Is(r.err, want) {
|
|
t.Errorf("clearance %s error=%v want %v", mode, r.err, want)
|
|
}
|
|
if c.deliveryPending != (want != nil) {
|
|
t.Errorf("clearance %s pending=%t", mode, c.deliveryPending)
|
|
}
|
|
if mode != "empty" && now.Sub(time.Unix(0, 0)) != deliveryClearanceTimeout {
|
|
t.Errorf("clearance %s wait=%v want 6s", mode, now.Sub(time.Unix(0, 0)))
|
|
}
|
|
for _, command := range wireCommands(p) {
|
|
if command != "AP" {
|
|
t.Errorf("clearance issued %s, want AP only", command)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestWorkerAssumedClearanceAllowsOneFC7(t *testing.T) {
|
|
for _, prepare := range []bool{false, true} {
|
|
p := &scriptedTransport{}
|
|
dispatched := false
|
|
p.afterWrite = func() {
|
|
w := p.writes[len(p.writes)-1]
|
|
if len(w) <= 6 || w[0] != STX {
|
|
return
|
|
}
|
|
switch string(w[5 : len(w)-2]) {
|
|
case "AP":
|
|
if dispatched {
|
|
p.chunks = append(p.chunks, vendorACK, apReply(status(0x35)))
|
|
} else {
|
|
p.chunks = append(p.chunks, []byte{0x10, 0x06, 0x30})
|
|
}
|
|
case "FC7":
|
|
dispatched = true
|
|
p.chunks = append(p.chunks, vendorACK)
|
|
}
|
|
}
|
|
c, now := deliveryWorkerClient(t, p)
|
|
c.deliveryPending = true
|
|
c.deliveryStarted = c.now()
|
|
var err error
|
|
if prepare {
|
|
_, err = c.PrepareCurrentCard(context.Background())
|
|
} else {
|
|
err = c.BeginPrepareNextCard(context.Background())
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("prepare=%t error=%v", prepare, err)
|
|
}
|
|
wantElapsed := deliveryClearanceTimeout
|
|
if prepare {
|
|
wantElapsed += sequenceUncertainWait
|
|
}
|
|
if elapsed := now.Sub(time.Unix(0, 0)); elapsed != wantElapsed {
|
|
t.Errorf("prepare=%t elapsed=%v want %v", prepare, elapsed, wantElapsed)
|
|
}
|
|
commands := wireCommands(p)
|
|
fc7 := 0
|
|
for _, cmd := range commands {
|
|
if cmd == "FC7" {
|
|
fc7++
|
|
}
|
|
if cmd == "RS" {
|
|
t.Errorf("unexpected RS: %v", commands)
|
|
}
|
|
}
|
|
if fc7 != 1 || c.deliveryPending {
|
|
t.Errorf("prepare=%t commands=%v pending=%t, want one FC7 and cleared", prepare, commands, c.deliveryPending)
|
|
}
|
|
if !prepare && commands[len(commands)-1] != "FC7" {
|
|
t.Errorf("prestaging polled after FC7: %v", commands)
|
|
}
|
|
}
|
|
}
|