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.
242 lines
8.3 KiB
242 lines
8.3 KiB
package serial
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
type fakeLineDevice struct {
|
|
path string
|
|
closed bool
|
|
}
|
|
|
|
func (*fakeLineDevice) Fd() uintptr { return 1 }
|
|
|
|
func (d *fakeLineDevice) Close() error {
|
|
d.closed = true
|
|
return nil
|
|
}
|
|
|
|
type fakeLineOperations struct {
|
|
current map[string]LineConfig
|
|
applyFailure map[string]error
|
|
readbackOverride map[string]LineConfig
|
|
applyCalls []string
|
|
restoreCalls []string
|
|
opened []*fakeLineDevice
|
|
}
|
|
|
|
func (f *fakeLineOperations) Open(path string) (lineDevice, error) {
|
|
device := &fakeLineDevice{path: path}
|
|
f.opened = append(f.opened, device)
|
|
return device, nil
|
|
}
|
|
|
|
func (f *fakeLineOperations) Snapshot(device lineDevice) (any, error) {
|
|
path := device.(*fakeLineDevice).path
|
|
return f.current[path], nil
|
|
}
|
|
|
|
func (f *fakeLineOperations) Apply(device lineDevice, config LineConfig) error {
|
|
path := device.(*fakeLineDevice).path
|
|
f.applyCalls = append(f.applyCalls, path)
|
|
f.current[path] = config
|
|
return f.applyFailure[path]
|
|
}
|
|
|
|
func (f *fakeLineOperations) Read(device lineDevice, path string) (LineConfig, error) {
|
|
if override, ok := f.readbackOverride[path]; ok {
|
|
return override, nil
|
|
}
|
|
return f.current[path], nil
|
|
}
|
|
|
|
func (f *fakeLineOperations) Restore(device lineDevice, snapshot any) error {
|
|
path := device.(*fakeLineDevice).path
|
|
f.restoreCalls = append(f.restoreCalls, path)
|
|
f.current[path] = snapshot.(LineConfig)
|
|
return nil
|
|
}
|
|
|
|
func TestMappingControllerApplyReturnsFullLineReadback(t *testing.T) {
|
|
controller, operations := transactionController(t)
|
|
before, err := controller.States(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("states before apply: %v", err)
|
|
}
|
|
if before[0].ProfileApplied || before[0].LineSettings != nil {
|
|
t.Fatalf("unexpected applied profile before transaction: %#v", before[0])
|
|
}
|
|
requested := ChannelConfig{
|
|
Channel: "CH01", BaudRate: 115200, DataBits: 7,
|
|
Parity: "even", StopBits: 2, FlowControl: "rtscts",
|
|
}
|
|
|
|
states, err := controller.Apply(context.Background(), []ChannelConfig{requested})
|
|
if err != nil {
|
|
t.Fatalf("apply: %v", err)
|
|
}
|
|
if len(states) != 1 || states[0].LineSettings == nil {
|
|
t.Fatalf("states = %#v", states)
|
|
}
|
|
want := LineSettings{
|
|
BaudRate: 115200, DataBits: 7, Parity: "even", StopBits: 2, StopBitsMode: "2",
|
|
FlowControl: "rtscts", RTSControl: "auto", DTRControl: "auto",
|
|
}
|
|
if !reflect.DeepEqual(*states[0].LineSettings, want) {
|
|
t.Fatalf("readback = %#v, want %#v", *states[0].LineSettings, want)
|
|
}
|
|
if len(operations.restoreCalls) != 0 {
|
|
t.Fatalf("unexpected rollback = %#v", operations.restoreCalls)
|
|
}
|
|
cached, err := controller.States(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("states after apply: %v", err)
|
|
}
|
|
if !cached[0].ProfileApplied || cached[0].LineSettings == nil || !reflect.DeepEqual(*cached[0].LineSettings, want) {
|
|
t.Fatalf("cached state = %#v, want applied %#v", cached[0], want)
|
|
}
|
|
}
|
|
|
|
func TestMappingControllerKeepsAppliedDeviceOpenUntilClose(t *testing.T) {
|
|
controller, operations := transactionController(t)
|
|
path := controller.byChannel["CH01"].Device
|
|
|
|
_, err := controller.Apply(context.Background(), []ChannelConfig{{
|
|
Channel: "CH01", BaudRate: 9600, DataBits: 8, Parity: "none", StopBits: 1,
|
|
FlowControl: "none", RTSControl: "on", DTRControl: "on",
|
|
}})
|
|
if err != nil {
|
|
t.Fatalf("apply: %v", err)
|
|
}
|
|
if len(operations.opened) != 1 || operations.opened[0].closed {
|
|
t.Fatalf("applied device was not retained: %#v", operations.opened)
|
|
}
|
|
if controller.heldDevices["CH01"] != operations.opened[0] {
|
|
t.Fatalf("held device for CH01 = %#v, want %s", controller.heldDevices["CH01"], path)
|
|
}
|
|
if err := controller.Close(); err != nil {
|
|
t.Fatalf("close controller: %v", err)
|
|
}
|
|
if !operations.opened[0].closed || len(controller.heldDevices) != 0 {
|
|
t.Fatalf("held device was not closed: device=%#v holds=%#v", operations.opened[0], controller.heldDevices)
|
|
}
|
|
}
|
|
|
|
func TestMappingControllerApplyRollsBackEveryOpenedChannelOnFailure(t *testing.T) {
|
|
controller, operations := transactionController(t)
|
|
firstPath := controller.byChannel["CH01"].Device
|
|
secondPath := controller.byChannel["CH02"].Device
|
|
beforeFirst := operations.current[firstPath]
|
|
beforeSecond := operations.current[secondPath]
|
|
operations.applyFailure[secondPath] = errors.New("injected apply failure")
|
|
|
|
_, err := controller.Apply(context.Background(), []ChannelConfig{
|
|
{Channel: "CH01", BaudRate: 115200, DataBits: 8, Parity: "none", StopBits: 1, FlowControl: "none"},
|
|
{Channel: "CH02", BaudRate: 57600, DataBits: 7, Parity: "odd", StopBits: 2, FlowControl: "rtscts"},
|
|
})
|
|
if err == nil || !strings.Contains(err.Error(), "rolled back") {
|
|
t.Fatalf("apply error = %v", err)
|
|
}
|
|
if !reflect.DeepEqual(operations.current[firstPath], beforeFirst) || !reflect.DeepEqual(operations.current[secondPath], beforeSecond) {
|
|
t.Fatalf("current configs were not restored: %#v", operations.current)
|
|
}
|
|
wantRestore := []string{secondPath, firstPath}
|
|
if !reflect.DeepEqual(operations.restoreCalls, wantRestore) {
|
|
t.Fatalf("restore calls = %#v, want %#v", operations.restoreCalls, wantRestore)
|
|
}
|
|
for _, device := range operations.opened {
|
|
if !device.closed {
|
|
t.Fatalf("device %s was not closed", device.path)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMappingControllerApplyRollsBackOnReadbackMismatch(t *testing.T) {
|
|
controller, operations := transactionController(t)
|
|
path := controller.byChannel["CH01"].Device
|
|
before := operations.current[path]
|
|
operations.readbackOverride[path] = LineConfig{
|
|
Device: path, BaudRate: 9600, DataBits: 8, Parity: "none", StopBits: 1, FlowControl: "none",
|
|
}
|
|
|
|
_, err := controller.Apply(context.Background(), []ChannelConfig{{
|
|
Channel: "CH01", BaudRate: 115200, DataBits: 8, Parity: "none", StopBits: 1, FlowControl: "none",
|
|
}})
|
|
if err == nil || !strings.Contains(err.Error(), "readback") || !strings.Contains(err.Error(), "rolled back") {
|
|
t.Fatalf("apply error = %v", err)
|
|
}
|
|
if !reflect.DeepEqual(operations.current[path], before) {
|
|
t.Fatalf("config = %#v, want restored %#v", operations.current[path], before)
|
|
}
|
|
}
|
|
|
|
func TestMappingControllerProbeCH01AlwaysRestoresPriorState(t *testing.T) {
|
|
controller, operations := transactionController(t)
|
|
path := controller.byChannel["CH01"].Device
|
|
before := operations.current[path]
|
|
|
|
result, err := controller.Probe(context.Background(), ChannelConfig{
|
|
Channel: "CH01", BaudRate: 115200, DataBits: 8, Parity: "none", StopBits: 1, FlowControl: "none",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("probe: %v", err)
|
|
}
|
|
if !result.Restored || result.Applied.BaudRate != 115200 || !reflect.DeepEqual(operations.current[path], before) {
|
|
t.Fatalf("result = %#v, current = %#v", result, operations.current[path])
|
|
}
|
|
if len(operations.restoreCalls) != 1 || operations.restoreCalls[0] != path {
|
|
t.Fatalf("restore calls = %#v", operations.restoreCalls)
|
|
}
|
|
}
|
|
|
|
func TestMappingControllerProbeRejectsNonRepresentativeLiveChannel(t *testing.T) {
|
|
controller, operations := transactionController(t)
|
|
|
|
_, err := controller.Probe(context.Background(), ChannelConfig{
|
|
Channel: "CH02", BaudRate: 115200, DataBits: 8, Parity: "none", StopBits: 1, FlowControl: "none",
|
|
})
|
|
if err == nil || !strings.Contains(err.Error(), "CH01") {
|
|
t.Fatalf("probe error = %v", err)
|
|
}
|
|
if len(operations.applyCalls) != 0 {
|
|
t.Fatalf("unexpected apply calls = %#v", operations.applyCalls)
|
|
}
|
|
}
|
|
|
|
func transactionController(t *testing.T) (*MappingController, *fakeLineOperations) {
|
|
t.Helper()
|
|
controller, err := NewMappingController(testMapping())
|
|
if err != nil {
|
|
t.Fatalf("new mapping controller: %v", err)
|
|
}
|
|
operations := &fakeLineOperations{
|
|
current: make(map[string]LineConfig),
|
|
applyFailure: make(map[string]error),
|
|
readbackOverride: make(map[string]LineConfig),
|
|
}
|
|
for channel, entry := range controller.byChannel {
|
|
operations.current[entry.Device] = LineConfig{
|
|
Device: entry.Device, BaudRate: 9600, DataBits: 8,
|
|
Parity: "none", StopBits: 1, FlowControl: "none",
|
|
}
|
|
_ = channel
|
|
}
|
|
controller.lineOps = operations
|
|
controller.stateProbe = func(entry MappingChannel) State {
|
|
return State{
|
|
Channel: entry.Channel, Device: entry.Device, PCIBDF: entry.PCIBDF,
|
|
HardwareProfile: controller.mapping.HardwareProfile,
|
|
MappingVersion: controller.mapping.MappingVersion, Available: true,
|
|
}
|
|
}
|
|
return controller, operations
|
|
}
|
|
|
|
func (f *fakeLineOperations) String() string {
|
|
return fmt.Sprintf("apply=%v restore=%v", f.applyCalls, f.restoreCalls)
|
|
}
|
|
|