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.
232 lines
7.4 KiB
232 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)
|
|
}
|
|
}
|
|
|