package serial import ( "context" "fmt" "io" "net" "testing" "time" ) func TestNormalizeCRProbeGroupsPreservesPCITopology(t *testing.T) { configured := []CRProbeGroup{{ID: "0000:01:00.0"}, {ID: "0002:21:00.0"}} for index := 1; index <= ChannelCount; index++ { group := 0 if index > 24 { group = 1 } configured[group].Channels = append(configured[group].Channels, fmt.Sprintf("CH%02d", index)) } groups, err := normalizeCRProbeGroups(configured, 8) if err != nil { t.Fatal(err) } if len(groups) != 2 || len(groups[0].Channels) != 24 || len(groups[1].Channels) != 24 { t.Fatalf("groups = %#v", groups) } if groups[0].Channels[0] != "CH01" || groups[0].Channels[23] != "CH24" || groups[1].Channels[0] != "CH25" || groups[1].Channels[23] != "CH48" { t.Fatalf("channel topology = %#v", groups) } } func TestNormalizeCRProbeGroupsRejectsDuplicateChannel(t *testing.T) { configured := []CRProbeGroup{{ID: "group-a"}, {ID: "group-b"}} for index := 1; index <= ChannelCount; index++ { configured[0].Channels = append(configured[0].Channels, fmt.Sprintf("CH%02d", index)) } configured[1].Channels = []string{"CH48"} if _, err := normalizeCRProbeGroups(configured, 2); err == nil { t.Fatal("duplicate CR probe channel was accepted") } } func TestCRProbeOffsetsDistributeTwentyFourChannelsAcrossFourSeconds(t *testing.T) { const channels = 24 sweep := 4 * time.Second previous := time.Duration(-1) for index := 0; index < channels; index++ { offset := crProbeOffset(sweep, channels, index) if offset <= previous { t.Fatalf("offset[%d]=%s, previous=%s", index, offset, previous) } if offset >= sweep { t.Fatalf("offset[%d]=%s must remain inside %s sweep", index, offset, sweep) } if index > 0 && offset-previous > 167*time.Millisecond { t.Fatalf("gap[%d]=%s exceeds 167ms", index, offset-previous) } previous = offset } } func TestShouldConfirmOnlyFirstMissFromPreviouslyUpPort(t *testing.T) { firstMiss := CRProbeState{ State: CRProbeStateNoResponse, EverUp: true, Up: true, ConsecutiveMisses: 1, } if !shouldConfirmCRProbeMiss(firstMiss, 2) { t.Fatal("first miss from an UP port must be confirmed immediately") } if shouldConfirmCRProbeMiss(firstMiss, 1) { t.Fatal("single-miss policy must not schedule a redundant confirmation") } if shouldConfirmCRProbeMiss(CRProbeState{State: CRProbeStateNoResponse, ConsecutiveMisses: 1}, 2) { t.Fatal("an Unknown port must not receive an extra confirmation probe") } } func TestCRProbeDetectsRemoteResponseWithoutCountingProbeTraffic(t *testing.T) { resolver := newFakeStreamResolver() serialSide, peer := net.Pipe() defer peer.Close() manager, err := NewStreamLeaseManager(resolver, StreamLeaseOptions{ Opener: streamOpenFunc(func(string) (io.ReadWriteCloser, error) { return serialSide, nil }), }) if err != nil { t.Fatal(err) } probe, err := NewCRProbeManager(manager, CRProbeOptions{Enabled: true, ResponseTimeout: 200 * time.Millisecond, ConfirmResponses: 1}) if err != nil { t.Fatal(err) } received := make(chan byte, 1) go func() { buffer := make([]byte, 1) if _, err := io.ReadFull(peer, buffer); err == nil { received <- buffer[0] _, _ = peer.Write([]byte("router>")) } }() state, err := probe.ProbeOnce(context.Background(), "CH01") if err != nil { t.Fatalf("probe: %v", err) } if state.State != CRProbeStateResponded || state.ResponseBytes == 0 || state.LastResponseAt == nil { t.Fatalf("state = %#v", state) } if value := <-received; value != '\r' { t.Fatalf("sent byte = %#x, want CR", value) } snapshot := manager.Snapshot() if snapshot.RXBytes != 0 || snapshot.TXBytes != 0 || snapshot.ActiveLeases != 0 { t.Fatalf("internal probe leaked into session counters: %#v", snapshot) } } func TestCRProbeRequiresRepeatedResponsesAndMisses(t *testing.T) { manager, err := NewStreamLeaseManager(newFakeStreamResolver(), StreamLeaseOptions{}) if err != nil { t.Fatal(err) } probe, err := NewCRProbeManager(manager, CRProbeOptions{ Enabled: true, ConfirmResponses: 2, DownMisses: 3, }) if err != nil { t.Fatal(err) } now := time.Now().UTC() responseAt := now state := probe.updateAt("CH06", CRProbeStateResponded, 7, &responseAt, now, "response") if state.State != CRProbeStateConfirming || state.Up || state.EverUp || state.ConsecutiveResponses != 1 { t.Fatalf("first response = %#v", state) } responseAt = now.Add(time.Second) state = probe.updateAt("CH06", CRProbeStateResponded, 7, &responseAt, responseAt, "response") if state.State != CRProbeStateResponded || !state.Up || !state.EverUp || state.LastConfirmedAt == nil { t.Fatalf("confirmed response = %#v", state) } for miss := 1; miss <= 2; miss++ { state = probe.updateAt("CH06", CRProbeStateNoResponse, 0, nil, now.Add(time.Duration(miss+1)*time.Second), "miss") if !state.Up || state.ConsecutiveMisses != miss { t.Fatalf("miss %d = %#v", miss, state) } } state = probe.updateAt("CH06", CRProbeStateNoResponse, 0, nil, now.Add(4*time.Second), "miss") if state.Up || !state.EverUp || state.ConsecutiveMisses != 3 { t.Fatalf("third miss = %#v", state) } } func TestMeaningfulCRResponseRejectsLocalEchoOnly(t *testing.T) { if meaningfulCRResponse([]byte{'\r', '\n', 0x00}) { t.Fatal("CR/LF/NUL-only payload was accepted as a remote response") } if !meaningfulCRResponse([]byte("\r\nrouter>")) { t.Fatal("remote prompt was rejected") } } func TestCRProbeNoResponseRemainsIndeterminate(t *testing.T) { resolver := newFakeStreamResolver() serialSide, peer := net.Pipe() defer peer.Close() manager, err := NewStreamLeaseManager(resolver, StreamLeaseOptions{ Opener: streamOpenFunc(func(string) (io.ReadWriteCloser, error) { return serialSide, nil }), }) if err != nil { t.Fatal(err) } probe, err := NewCRProbeManager(manager, CRProbeOptions{Enabled: true, ResponseTimeout: 50 * time.Millisecond}) if err != nil { t.Fatal(err) } go func() { _, _ = io.ReadFull(peer, make([]byte, 1)) }() state, err := probe.ProbeOnce(context.Background(), "CH01") if err != nil { t.Fatalf("probe: %v", err) } if state.State != CRProbeStateNoResponse || state.ConsecutiveMisses != 1 || state.LastResponseAt != nil { t.Fatalf("state = %#v", state) } } func TestCRProbeSkipsChannelOwnedBySession(t *testing.T) { resolver := newFakeStreamResolver() serialSide, peer := net.Pipe() defer peer.Close() manager, err := NewStreamLeaseManager(resolver, StreamLeaseOptions{ Opener: streamOpenFunc(func(string) (io.ReadWriteCloser, error) { return serialSide, nil }), }) if err != nil { t.Fatal(err) } lease, err := manager.Acquire(context.Background(), "CH01", "sessiond-test") if err != nil { t.Fatal(err) } defer lease.Close() probe, err := NewCRProbeManager(manager, CRProbeOptions{Enabled: true}) if err != nil { t.Fatal(err) } state, err := probe.ProbeOnce(context.Background(), "CH01") if err != nil { t.Fatalf("busy probe: %v", err) } if state.State != CRProbeStateBusy { t.Fatalf("state = %#v", state) } } func TestDisabledCRProbeDoesNotOpenDevice(t *testing.T) { manager, err := NewStreamLeaseManager(newFakeStreamResolver(), StreamLeaseOptions{ Opener: streamOpenFunc(func(string) (io.ReadWriteCloser, error) { t.Fatal("disabled probe opened serial device") return nil, nil }), }) if err != nil { t.Fatal(err) } probe, err := NewCRProbeManager(manager, CRProbeOptions{}) if err != nil { t.Fatal(err) } state, err := probe.ProbeOnce(context.Background(), "CH01") if err != nil || state.State != CRProbeStateDisabled { t.Fatalf("state = %#v, err = %v", state, err) } }