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.

243 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)
}