package serial import ( "context" "errors" "fmt" "sync/atomic" "testing" "time" ) func TestMappingControllerRequiresValidatedCompleteMapping(t *testing.T) { mapping := testMapping() mapping.Validated = false if _, err := NewMappingController(mapping); err == nil { t.Fatal("expected unvalidated mapping to be rejected") } mapping = testMapping() mapping.Channels = mapping.Channels[:47] if _, err := NewMappingController(mapping); err == nil { t.Fatal("expected incomplete mapping to be rejected") } } func TestMappingControllerRejectsConnectedChannelsMappedToNCUARTs(t *testing.T) { mapping := testMapping() mapping.Channels[0].Device = "/dev/ttyWCH0" if _, err := NewMappingController(mapping); err == nil { t.Fatal("expected CH01 mapped to NC ttyWCH0 to be rejected") } mapping = testMapping() mapping.Channels[24].Device = "/dev/ttyWCH28" if _, err := NewMappingController(mapping); err == nil { t.Fatal("expected CH25 mapped to NC ttyWCH28 to be rejected") } } func TestMappingControllerFailsClosedBeforeOpeningUnavailableDevice(t *testing.T) { controller, err := NewMappingController(testMapping()) if err != nil { t.Fatalf("new mapping controller: %v", err) } states, err := controller.States(context.Background()) if err != nil { t.Fatalf("states: %v", err) } if len(states) != ChannelCount || states[0].Channel != "CH01" || states[0].Available { t.Fatalf("states = %#v", states) } _, err = controller.Apply(context.Background(), []ChannelConfig{{Channel: "CH01", BaudRate: 115200}}) if !IsUnavailable(err) { t.Fatalf("apply error = %v, want unavailable error", err) } if !errors.Is(err, ErrSerialUnavailable) { t.Fatalf("apply error = %v, want ErrSerialUnavailable", err) } } func TestMappingControllerReportsUnknownUntilModemSignalIsValidated(t *testing.T) { controller, err := NewMappingController(testMapping()) if err != nil { t.Fatal(err) } states, err := controller.LinkStates(context.Background()) if err != nil || len(states) != ChannelCount { t.Fatalf("LinkStates = %+v, %v", states, err) } if states[0].Channel != "CH01" || states[0].State != "unknown" || states[0].Signal != "unknown" || states[0].Available { t.Fatalf("CH01 modem state = %+v", states[0]) } mapping := testMapping() mapping.Channels[0].LinkSignal = "ri" if _, err := NewMappingController(mapping); err == nil { t.Fatal("NewMappingController accepted an unapproved link signal") } } func TestMappingControllerCanObserveDCDDSRAndCTSIndependently(t *testing.T) { mapping := testMapping() mapping.Channels[0].LinkSignals = []string{"dcd", "dsr", "cts"} controller, err := NewMappingController(mapping) if err != nil { t.Fatal(err) } up := true controller.modemProbe = func(entry MappingChannel) LinkState { state := "down" if up { state = "up" } return LinkState{Channel: entry.Channel, State: state, Signal: entry.LinkSignal, Available: true} } states, err := controller.LinkStates(context.Background()) if err != nil { t.Fatal(err) } if len(states) != ChannelCount+2 || states[0].Signal != "dcd" || states[1].Signal != "dsr" || states[2].Signal != "cts" || states[0].Channel != "CH01" || states[1].Channel != "CH01" || states[2].Channel != "CH01" || !states[2].EverUp { t.Fatalf("multi-signal link states = %+v", states[:min(4, len(states))]) } up = false states, err = controller.LinkStates(context.Background()) if err != nil || states[2].State != "down" || !states[2].EverUp { t.Fatalf("CTS history after link loss = %+v, %v", states[2], err) } mapping.Channels[0].LinkSignals = []string{"dcd", "dcd"} if _, err := NewMappingController(mapping); err == nil { t.Fatal("duplicate modem signals were accepted") } } func TestMappingControllerBoundsParallelLinkProbes(t *testing.T) { controller, err := NewMappingController(testMapping()) if err != nil { t.Fatal(err) } controller.SetLinkWorkerCount(8) var active atomic.Int32 var maximum atomic.Int32 controller.modemProbe = func(entry MappingChannel) LinkState { current := active.Add(1) for { seen := maximum.Load() if current <= seen || maximum.CompareAndSwap(seen, current) { break } } time.Sleep(5 * time.Millisecond) active.Add(-1) return LinkState{Channel: entry.Channel, State: "down", Signal: entry.LinkSignal, Available: true} } states, err := controller.LinkStates(context.Background()) if err != nil || len(states) != ChannelCount { t.Fatalf("LinkStates = %d, %v", len(states), err) } if maximum.Load() < 2 || maximum.Load() > 8 { t.Fatalf("parallel link workers = %d, want 2..8", maximum.Load()) } } func testMapping() Mapping { channels := make([]MappingChannel, 0, ChannelCount) for index := 1; index <= ChannelCount; index++ { bdf := "0000:01:00.0" ttyNumber := index + 3 if index > 24 { bdf = "0002:21:00.0" ttyNumber = index + 7 } channels = append(channels, MappingChannel{ Channel: fmt.Sprintf("CH%02d", index), Device: fmt.Sprintf("/dev/ttyWCH%d", ttyNumber), PCIBDF: bdf, }) } return Mapping{ FormatVersion: 1, HardwareProfile: "fet3568j-c", MappingVersion: "validated-test", Evidence: "isolated-loopback-test", Validated: true, Channels: channels, } }