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.

233 lines
7.4 KiB

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