You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
431 lines
12 KiB
431 lines
12 KiB
|
2 months ago
|
package serial
|
||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"errors"
|
||
|
|
"io"
|
||
|
|
"net"
|
||
|
|
"reflect"
|
||
|
|
"sync"
|
||
|
|
"testing"
|
||
|
|
"time"
|
||
|
|
)
|
||
|
|
|
||
|
|
type fakeStreamResolver struct {
|
||
|
|
mu sync.Mutex
|
||
|
|
devices map[string]string
|
||
|
|
active map[string]bool
|
||
|
|
released map[string]int
|
||
|
|
}
|
||
|
|
|
||
|
|
func newFakeStreamResolver() *fakeStreamResolver {
|
||
|
|
return &fakeStreamResolver{
|
||
|
|
devices: map[string]string{"CH01": "/dev/ttyWCH4", "CH02": "/dev/ttyWCH5"},
|
||
|
|
active: make(map[string]bool),
|
||
|
|
released: make(map[string]int),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func (r *fakeStreamResolver) ReserveStream(ctx context.Context, channel string) (*StreamTarget, error) {
|
||
|
|
if err := ctx.Err(); err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
r.mu.Lock()
|
||
|
|
defer r.mu.Unlock()
|
||
|
|
device, ok := r.devices[channel]
|
||
|
|
if !ok {
|
||
|
|
return nil, &UnavailableError{Reason: "channel is not mapped"}
|
||
|
|
}
|
||
|
|
if r.active[channel] {
|
||
|
|
return nil, ErrStreamLeaseBusy
|
||
|
|
}
|
||
|
|
r.active[channel] = true
|
||
|
|
return newStreamTarget(channel, device, "fake-pci-group", func() {
|
||
|
|
r.mu.Lock()
|
||
|
|
defer r.mu.Unlock()
|
||
|
|
delete(r.active, channel)
|
||
|
|
r.released[channel]++
|
||
|
|
}), nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (r *fakeStreamResolver) releaseCount(channel string) int {
|
||
|
|
r.mu.Lock()
|
||
|
|
defer r.mu.Unlock()
|
||
|
|
return r.released[channel]
|
||
|
|
}
|
||
|
|
|
||
|
|
type streamOpenFunc func(string) (io.ReadWriteCloser, error)
|
||
|
|
|
||
|
|
func (f streamOpenFunc) OpenStream(path string) (io.ReadWriteCloser, error) { return f(path) }
|
||
|
|
|
||
|
|
func TestStreamOpenIsSerializedWithinPCIGroup(t *testing.T) {
|
||
|
|
resolver := newFakeStreamResolver()
|
||
|
|
var mu sync.Mutex
|
||
|
|
activeOpens := 0
|
||
|
|
maximumOpens := 0
|
||
|
|
peers := make([]io.Closer, 0, 2)
|
||
|
|
manager, err := NewStreamLeaseManager(resolver, StreamLeaseOptions{
|
||
|
|
Opener: streamOpenFunc(func(string) (io.ReadWriteCloser, error) {
|
||
|
|
mu.Lock()
|
||
|
|
activeOpens++
|
||
|
|
if activeOpens > maximumOpens {
|
||
|
|
maximumOpens = activeOpens
|
||
|
|
}
|
||
|
|
mu.Unlock()
|
||
|
|
time.Sleep(25 * time.Millisecond)
|
||
|
|
serialSide, peer := net.Pipe()
|
||
|
|
mu.Lock()
|
||
|
|
peers = append(peers, peer)
|
||
|
|
activeOpens--
|
||
|
|
mu.Unlock()
|
||
|
|
return serialSide, nil
|
||
|
|
}),
|
||
|
|
})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
|
||
|
|
start := make(chan struct{})
|
||
|
|
type result struct {
|
||
|
|
lease *StreamLease
|
||
|
|
err error
|
||
|
|
}
|
||
|
|
results := make(chan result, 2)
|
||
|
|
for _, channel := range []string{"CH01", "CH02"} {
|
||
|
|
channel := channel
|
||
|
|
go func() {
|
||
|
|
<-start
|
||
|
|
lease, acquireErr := manager.Acquire(context.Background(), channel, "sessiond-open-test-"+channel)
|
||
|
|
results <- result{lease: lease, err: acquireErr}
|
||
|
|
}()
|
||
|
|
}
|
||
|
|
close(start)
|
||
|
|
for index := 0; index < 2; index++ {
|
||
|
|
got := <-results
|
||
|
|
if got.err != nil {
|
||
|
|
t.Fatal(got.err)
|
||
|
|
}
|
||
|
|
if err := got.lease.Close(); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
for _, peer := range peers {
|
||
|
|
_ = peer.Close()
|
||
|
|
}
|
||
|
|
if maximumOpens != 1 {
|
||
|
|
t.Fatalf("same PCI group had %d concurrent opens, want 1", maximumOpens)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestStreamLeaseIsExclusiveAndReleasesDevice(t *testing.T) {
|
||
|
|
resolver := newFakeStreamResolver()
|
||
|
|
serialSide, peer := net.Pipe()
|
||
|
|
defer peer.Close()
|
||
|
|
opened := make([]string, 0, 1)
|
||
|
|
manager, err := NewStreamLeaseManager(resolver, StreamLeaseOptions{
|
||
|
|
Opener: streamOpenFunc(func(path string) (io.ReadWriteCloser, error) {
|
||
|
|
opened = append(opened, path)
|
||
|
|
return serialSide, nil
|
||
|
|
}),
|
||
|
|
})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("new manager: %v", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
lease, err := manager.Acquire(context.Background(), "CH01", "sessiond-test")
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("acquire: %v", err)
|
||
|
|
}
|
||
|
|
if _, err := manager.Acquire(context.Background(), "CH01", "sessiond-test-2"); !errors.Is(err, ErrStreamLeaseBusy) {
|
||
|
|
t.Fatalf("second acquire error = %v, want busy", err)
|
||
|
|
}
|
||
|
|
if !reflect.DeepEqual(opened, []string{"/dev/ttyWCH4"}) {
|
||
|
|
t.Fatalf("opened = %#v", opened)
|
||
|
|
}
|
||
|
|
|
||
|
|
if err := lease.Close(); err != nil {
|
||
|
|
t.Fatalf("close lease: %v", err)
|
||
|
|
}
|
||
|
|
if err := lease.Close(); err != nil {
|
||
|
|
t.Fatalf("idempotent close: %v", err)
|
||
|
|
}
|
||
|
|
if resolver.releaseCount("CH01") != 1 {
|
||
|
|
t.Fatalf("release count = %d", resolver.releaseCount("CH01"))
|
||
|
|
}
|
||
|
|
if got := manager.Snapshot(); got.ActiveLeases != 0 {
|
||
|
|
t.Fatalf("snapshot = %#v", got)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestStreamReconnectKeepsCanonicalDeviceIdentity(t *testing.T) {
|
||
|
|
resolver := newFakeStreamResolver()
|
||
|
|
opened := make([]string, 0, 2)
|
||
|
|
manager, err := NewStreamLeaseManager(resolver, StreamLeaseOptions{
|
||
|
|
Opener: streamOpenFunc(func(path string) (io.ReadWriteCloser, error) {
|
||
|
|
opened = append(opened, path)
|
||
|
|
serialSide, peer := net.Pipe()
|
||
|
|
_ = peer.Close()
|
||
|
|
return serialSide, nil
|
||
|
|
}),
|
||
|
|
})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
for index := 0; index < 2; index++ {
|
||
|
|
lease, err := manager.Acquire(context.Background(), "CH01", "sessiond-reconnect")
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("acquire %d: %v", index, err)
|
||
|
|
}
|
||
|
|
if err := lease.Close(); err != nil {
|
||
|
|
t.Fatalf("close %d: %v", index, err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if !reflect.DeepEqual(opened, []string{"/dev/ttyWCH4", "/dev/ttyWCH4"}) {
|
||
|
|
t.Fatalf("reconnect opened = %#v", opened)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestStreamLeaseCountsBytesAndDropsOnlyMetadataWhenSinkIsFull(t *testing.T) {
|
||
|
|
resolver := newFakeStreamResolver()
|
||
|
|
serialSide, peer := net.Pipe()
|
||
|
|
defer peer.Close()
|
||
|
|
sink := NewBoundedStreamEventSink(1)
|
||
|
|
manager, err := NewStreamLeaseManager(resolver, StreamLeaseOptions{
|
||
|
|
Opener: streamOpenFunc(func(string) (io.ReadWriteCloser, error) { return serialSide, nil }),
|
||
|
|
Events: sink,
|
||
|
|
})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
lease, err := manager.Acquire(context.Background(), "CH01", "sessiond-test")
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
defer lease.Close()
|
||
|
|
|
||
|
|
written := make(chan error, 1)
|
||
|
|
go func() {
|
||
|
|
_, err := lease.Write([]byte{0x00, 0xff, 0x3a})
|
||
|
|
written <- err
|
||
|
|
}()
|
||
|
|
buf := make([]byte, 3)
|
||
|
|
if _, err := io.ReadFull(peer, buf); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if err := <-written; err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if !reflect.DeepEqual(buf, []byte{0x00, 0xff, 0x3a}) {
|
||
|
|
t.Fatalf("payload = %v", buf)
|
||
|
|
}
|
||
|
|
|
||
|
|
readDone := make(chan error, 1)
|
||
|
|
go func() {
|
||
|
|
_, err := peer.Write([]byte{0x10, 0x00})
|
||
|
|
readDone <- err
|
||
|
|
}()
|
||
|
|
read := make([]byte, 2)
|
||
|
|
if _, err := io.ReadFull(lease, read); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if err := <-readDone; err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
|
||
|
|
snapshot := manager.Snapshot()
|
||
|
|
if snapshot.TXBytes != 3 || snapshot.RXBytes != 2 || sink.Dropped() == 0 {
|
||
|
|
t.Fatalf("snapshot = %#v, dropped = %d", snapshot, sink.Dropped())
|
||
|
|
}
|
||
|
|
if len(snapshot.Channels) != 1 || snapshot.Channels[0].Channel != "CH01" ||
|
||
|
|
!snapshot.Channels[0].Active || snapshot.Channels[0].TXBytes != 3 || snapshot.Channels[0].RXBytes != 2 || snapshot.Channels[0].LastRXAt == nil {
|
||
|
|
t.Fatalf("channel snapshot = %#v", snapshot.Channels)
|
||
|
|
}
|
||
|
|
select {
|
||
|
|
case event := <-sink.Events():
|
||
|
|
if event.Channel != "CH01" || event.LeaseID == "" {
|
||
|
|
t.Fatalf("event = %#v", event)
|
||
|
|
}
|
||
|
|
case <-time.After(time.Second):
|
||
|
|
t.Fatal("expected bounded metadata event")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestStreamPayloadCaptureRespectsDirectionAndCopiesBytes(t *testing.T) {
|
||
|
|
resolver := newFakeStreamResolver()
|
||
|
|
serialSide, peer := net.Pipe()
|
||
|
|
defer peer.Close()
|
||
|
|
sink := NewBoundedStreamEventSink(16)
|
||
|
|
manager, err := NewStreamLeaseManager(resolver, StreamLeaseOptions{
|
||
|
|
Opener: streamOpenFunc(func(string) (io.ReadWriteCloser, error) { return serialSide, nil }),
|
||
|
|
Events: sink,
|
||
|
|
})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if err := manager.ApplyCapture(context.Background(), []CaptureConfig{{Channel: "CH01", Mode: CaptureModeRX}}); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
lease, err := manager.Acquire(context.Background(), "CH01", "sessiond-capture")
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
defer lease.Close()
|
||
|
|
<-sink.Events() // lifecycle open
|
||
|
|
|
||
|
|
txDone := make(chan error, 1)
|
||
|
|
go func() { _, err := lease.Write([]byte{0xaa}); txDone <- err }()
|
||
|
|
if _, err := io.ReadFull(peer, make([]byte, 1)); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if err := <-txDone; err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
select {
|
||
|
|
case event := <-sink.Events():
|
||
|
|
t.Fatalf("TX event bypassed RX capture policy: %#v", event)
|
||
|
|
case <-time.After(20 * time.Millisecond):
|
||
|
|
}
|
||
|
|
|
||
|
|
rxSource := []byte{0x00, 0xff, 0x3a}
|
||
|
|
rxDone := make(chan error, 1)
|
||
|
|
go func() { _, err := peer.Write(rxSource); rxDone <- err }()
|
||
|
|
buffer := make([]byte, len(rxSource))
|
||
|
|
if _, err := io.ReadFull(lease, buffer); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if err := <-rxDone; err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
buffer[0] = 0x99
|
||
|
|
event := <-sink.Events()
|
||
|
|
if event.Direction != CaptureModeRX || !reflect.DeepEqual(event.Payload, rxSource) || event.Bytes != uint64(len(rxSource)) || event.Timestamp.IsZero() {
|
||
|
|
t.Fatalf("captured event = %#v", event)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestStreamPayloadCaptureSplitsBoundedChunks(t *testing.T) {
|
||
|
|
resolver := newFakeStreamResolver()
|
||
|
|
serialSide, peer := net.Pipe()
|
||
|
|
defer peer.Close()
|
||
|
|
sink := NewBoundedStreamEventSink(8)
|
||
|
|
manager, err := NewStreamLeaseManager(resolver, StreamLeaseOptions{
|
||
|
|
Opener: streamOpenFunc(func(string) (io.ReadWriteCloser, error) { return serialSide, nil }),
|
||
|
|
Events: sink,
|
||
|
|
})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
lease, err := manager.Acquire(context.Background(), "CH01", "sessiond-chunk")
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
defer lease.Close()
|
||
|
|
<-sink.Events()
|
||
|
|
|
||
|
|
payload := make([]byte, MaxCapturedPayloadBytes+17)
|
||
|
|
for index := range payload {
|
||
|
|
payload[index] = byte(index)
|
||
|
|
}
|
||
|
|
done := make(chan error, 1)
|
||
|
|
go func() { _, err := lease.Write(payload); done <- err }()
|
||
|
|
if _, err := io.ReadFull(peer, make([]byte, len(payload))); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if err := <-done; err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
first, second := <-sink.Events(), <-sink.Events()
|
||
|
|
if len(first.Payload) != MaxCapturedPayloadBytes || len(second.Payload) != 17 {
|
||
|
|
t.Fatalf("chunk sizes = %d, %d", len(first.Payload), len(second.Payload))
|
||
|
|
}
|
||
|
|
combined := append(append([]byte(nil), first.Payload...), second.Payload...)
|
||
|
|
if !reflect.DeepEqual(combined, payload) {
|
||
|
|
t.Fatal("captured chunks did not preserve payload")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestStreamPayloadCaptureFramesDelimiterAndTimeoutWithDisplayPolicy(t *testing.T) {
|
||
|
|
resolver := newFakeStreamResolver()
|
||
|
|
serialSide, peer := net.Pipe()
|
||
|
|
defer peer.Close()
|
||
|
|
sink := NewBoundedStreamEventSink(8)
|
||
|
|
manager, err := NewStreamLeaseManager(resolver, StreamLeaseOptions{
|
||
|
|
Opener: streamOpenFunc(func(string) (io.ReadWriteCloser, error) { return serialSide, nil }),
|
||
|
|
Events: sink,
|
||
|
|
})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
config := CaptureConfig{
|
||
|
|
Channel: "CH01", Mode: CaptureModeTX,
|
||
|
|
DataPackage: CapturePackageConfig{Mode: "delimiter", Length: 128, TimeoutMillis: 30, DelimiterProcess: "remove", Delimiter1: 13, Delimiter2: 10},
|
||
|
|
DataLog: CaptureLogConfig{
|
||
|
|
Send: true, ShowTimestamp: true, ShowDirection: true, ShowUsername: true, ShowIPAddress: true, ShowPort: true, Format: "hex",
|
||
|
|
ReadTrigger: CaptureTriggerConfig{Process: "keep", Character1: -1, Character2: -1},
|
||
|
|
SendTrigger: CaptureTriggerConfig{Process: "keep", Character1: -1, Character2: -1},
|
||
|
|
},
|
||
|
|
}
|
||
|
|
if err := manager.ApplyCapture(context.Background(), []CaptureConfig{config}); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
lease, err := manager.Acquire(context.Background(), "CH01", "sessiond-framing")
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
defer lease.Close()
|
||
|
|
<-sink.Events()
|
||
|
|
|
||
|
|
write := func(payload []byte) {
|
||
|
|
t.Helper()
|
||
|
|
done := make(chan error, 1)
|
||
|
|
go func() { _, err := lease.Write(payload); done <- err }()
|
||
|
|
if _, err := io.ReadFull(peer, make([]byte, len(payload))); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if err := <-done; err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
write([]byte("show version\r"))
|
||
|
|
select {
|
||
|
|
case event := <-sink.Events():
|
||
|
|
t.Fatalf("partial delimiter emitted early: %#v", event)
|
||
|
|
case <-time.After(10 * time.Millisecond):
|
||
|
|
}
|
||
|
|
write([]byte("\nnext"))
|
||
|
|
first := <-sink.Events()
|
||
|
|
if string(first.Payload) != "show version" || first.LogFormat != "hex" || !first.ShowUsername || !first.ShowIPAddress || !first.ShowPort {
|
||
|
|
t.Fatalf("delimiter event = %#v", first)
|
||
|
|
}
|
||
|
|
select {
|
||
|
|
case second := <-sink.Events():
|
||
|
|
if string(second.Payload) != "next" {
|
||
|
|
t.Fatalf("timeout event = %#v", second)
|
||
|
|
}
|
||
|
|
case <-time.After(time.Second):
|
||
|
|
t.Fatal("timed capture frame was not flushed")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestMappingControllerReservationBlocksLineMutationUntilRelease(t *testing.T) {
|
||
|
|
controller, _ := transactionController(t)
|
||
|
|
target, err := controller.ReserveStream(context.Background(), "CH01")
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("reserve: %v", err)
|
||
|
|
}
|
||
|
|
if target.Channel != "CH01" || target.Device != controller.byChannel["CH01"].Device {
|
||
|
|
t.Fatalf("target = %#v", target)
|
||
|
|
}
|
||
|
|
request := []ChannelConfig{{
|
||
|
|
Channel: "CH01", BaudRate: 115200, DataBits: 8,
|
||
|
|
Parity: "none", StopBits: 1, FlowControl: "none",
|
||
|
|
}}
|
||
|
|
if _, err := controller.Apply(context.Background(), request); !errors.Is(err, ErrStreamLeaseBusy) {
|
||
|
|
t.Fatalf("apply while leased error = %v, want busy", err)
|
||
|
|
}
|
||
|
|
target.Release()
|
||
|
|
if _, err := controller.Apply(context.Background(), request); err != nil {
|
||
|
|
t.Fatalf("apply after release: %v", err)
|
||
|
|
}
|
||
|
|
}
|