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.
178 lines
7.2 KiB
178 lines
7.2 KiB
package logruntime
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"tera-clone/services/tera-api/internal/domain"
|
|
"tera-clone/services/tera-api/internal/serial"
|
|
)
|
|
|
|
type fakeBackend struct {
|
|
policy domain.LogRetentionSettings
|
|
keepalives []domain.SystemLogEvent
|
|
links []domain.LinkStateObservation
|
|
}
|
|
|
|
func (b *fakeBackend) LogRetention(context.Context) (domain.LogRetentionSettings, error) {
|
|
return b.policy, nil
|
|
}
|
|
|
|
func (b *fakeBackend) RecordSystemLog(_ context.Context, event domain.SystemLogEvent) (domain.SystemLogEvent, error) {
|
|
b.keepalives = append(b.keepalives, event)
|
|
return event, nil
|
|
}
|
|
|
|
func (b *fakeBackend) RecordLinkState(_ context.Context, event domain.LinkStateObservation) (*domain.LinkLogEvent, error) {
|
|
b.links = append(b.links, event)
|
|
return nil, nil
|
|
}
|
|
|
|
type fakeObserver struct{ values []domain.LinkStateObservation }
|
|
|
|
type partialObserver struct{ value domain.LinkStateObservation }
|
|
|
|
func (o partialObserver) Observe(context.Context, time.Time) ([]domain.LinkStateObservation, error) {
|
|
return []domain.LinkStateObservation{o.value}, errors.New("one source unavailable")
|
|
}
|
|
|
|
type fakeCRProbeController struct{ values []serial.CRProbeState }
|
|
|
|
func (c fakeCRProbeController) CRProbeStates(context.Context) ([]serial.CRProbeState, error) {
|
|
return append([]serial.CRProbeState(nil), c.values...), nil
|
|
}
|
|
|
|
func (o fakeObserver) Observe(_ context.Context, now time.Time) ([]domain.LinkStateObservation, error) {
|
|
result := append([]domain.LinkStateObservation(nil), o.values...)
|
|
for index := range result {
|
|
result[index].ObservedAt = now
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func TestSchedulerRunsConfiguredKeepaliveAndLinkCadences(t *testing.T) {
|
|
backend := &fakeBackend{policy: domain.LogRetentionSettings{
|
|
KeepaliveEnabled: true, KeepaliveIntervalMinutes: 3,
|
|
SinglePortStatusEnabled: true, SinglePortStatusIntervalMinutes: 1,
|
|
AllPortStatusEnabled: true, AllPortStatusIntervalMinutes: 30,
|
|
}}
|
|
scheduler, err := NewScheduler(Options{Backend: backend, Observer: fakeObserver{values: []domain.LinkStateObservation{{Kind: "ethernet", Resource: "Interface-1", State: "up", Signal: "carrier"}}}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
start := time.Date(2026, 7, 29, 0, 0, 0, 0, time.UTC)
|
|
scheduler.step(context.Background(), start)
|
|
scheduler.step(context.Background(), start.Add(30*time.Second))
|
|
scheduler.step(context.Background(), start.Add(time.Minute))
|
|
scheduler.step(context.Background(), start.Add(3*time.Minute))
|
|
if len(backend.keepalives) != 2 || backend.keepalives[0].Category != "status" {
|
|
t.Fatalf("keepalives = %+v", backend.keepalives)
|
|
}
|
|
if len(backend.links) != 3 {
|
|
t.Fatalf("link observations = %d, want 3", len(backend.links))
|
|
}
|
|
}
|
|
|
|
func TestSchedulerUsesAllPortIntervalWhenSinglePortDisabled(t *testing.T) {
|
|
backend := &fakeBackend{policy: domain.LogRetentionSettings{AllPortStatusEnabled: true, AllPortStatusIntervalMinutes: 30}}
|
|
scheduler, _ := NewScheduler(Options{Backend: backend, Observer: fakeObserver{values: []domain.LinkStateObservation{{Kind: "serial", Resource: "CH01", State: "unknown", Signal: "unknown"}}}})
|
|
start := time.Date(2026, 7, 29, 0, 0, 0, 0, time.UTC)
|
|
scheduler.step(context.Background(), start)
|
|
scheduler.step(context.Background(), start.Add(29*time.Minute))
|
|
scheduler.step(context.Background(), start.Add(30*time.Minute))
|
|
if len(backend.links) != 2 {
|
|
t.Fatalf("link observations = %d, want 2", len(backend.links))
|
|
}
|
|
}
|
|
|
|
func TestSchedulerContinuesObservationAndKeepaliveWithoutSnapshots(t *testing.T) {
|
|
backend := &fakeBackend{policy: domain.LogRetentionSettings{
|
|
AllPortStatusEnabled: false,
|
|
SinglePortStatusEnabled: true, SinglePortStatusIntervalMinutes: 1,
|
|
KeepaliveEnabled: true, KeepaliveIntervalMinutes: 3,
|
|
}}
|
|
scheduler, err := NewScheduler(Options{Backend: backend, Observer: fakeObserver{values: []domain.LinkStateObservation{{
|
|
Kind: "ethernet", Resource: "Interface-1", State: "up", Signal: "carrier",
|
|
}}}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for minute := 0; minute <= 3; minute++ {
|
|
scheduler.step(context.Background(), time.Date(2026, 9, 7, 0, minute, 0, 0, time.UTC))
|
|
}
|
|
if len(backend.links) != 4 || len(backend.keepalives) != 2 {
|
|
t.Fatalf("disabling snapshots stopped observation or keepalive: links=%d keepalives=%d", len(backend.links), len(backend.keepalives))
|
|
}
|
|
}
|
|
|
|
func TestEthernetObserverReadsCarrierWithoutGuessingMissingState(t *testing.T) {
|
|
root := t.TempDir()
|
|
for device, value := range map[string]string{"eth0": "1\n", "eth1": "0\n"} {
|
|
path := filepath.Join(root, device)
|
|
if err := os.Mkdir(path, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(path, "carrier"), []byte(value), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
observer := EthernetObserver{SysfsRoot: root, Interfaces: map[string]string{"Interface-1": "eth0", "Interface-2": "eth1", "Interface-3": "missing"}}
|
|
values, err := observer.Observe(context.Background(), time.Now())
|
|
if err != nil || len(values) != 3 {
|
|
t.Fatalf("Observe = %+v, %v", values, err)
|
|
}
|
|
if values[0].State != "up" || values[1].State != "down" || values[2].State != "unknown" || values[2].Signal != "unknown" {
|
|
t.Fatalf("carrier values = %+v", values)
|
|
}
|
|
}
|
|
|
|
func TestParseEthernetInterfacesIsBoundedToDeviceNames(t *testing.T) {
|
|
interfaces, err := ParseEthernetInterfaces("Interface-1=eth0,Interface-2=eth1")
|
|
if err != nil || interfaces["Interface-2"] != "eth1" {
|
|
t.Fatalf("ParseEthernetInterfaces = %+v, %v", interfaces, err)
|
|
}
|
|
if _, err := ParseEthernetInterfaces("Interface-1=../../secret"); err == nil {
|
|
t.Fatal("ParseEthernetInterfaces accepted a path")
|
|
}
|
|
}
|
|
|
|
func TestSerialObserverUsesCRProbeForUnifiedConnectionTransitions(t *testing.T) {
|
|
now := time.Date(2026, 8, 22, 0, 0, 0, 0, time.UTC)
|
|
observer := SerialObserver{Probes: fakeCRProbeController{values: []serial.CRProbeState{{
|
|
Channel: "CH06", Enabled: true, State: serial.CRProbeStateResponded, Up: true, EverUp: true,
|
|
}}}}
|
|
values, err := observer.Observe(context.Background(), now)
|
|
if err != nil || len(values) != 1 || values[0].State != "up" || values[0].Signal != "cr" {
|
|
t.Fatalf("CR up observations = %+v, %v", values, err)
|
|
}
|
|
|
|
observer.Probes = fakeCRProbeController{values: []serial.CRProbeState{{
|
|
Channel: "CH06", Enabled: true, State: serial.CRProbeStateNoResponse, Up: false, EverUp: true,
|
|
}}}
|
|
values, err = observer.Observe(context.Background(), now.Add(time.Second))
|
|
if err != nil || len(values) != 1 || values[0].State != "unknown" || values[0].Signal != "cr" {
|
|
t.Fatalf("CR no response must remain indeterminate = %+v, %v", values, err)
|
|
}
|
|
observer.Probes = fakeCRProbeController{values: []serial.CRProbeState{{
|
|
Channel: "CH06", Enabled: true, EverUp: true, LastResponseAt: &now,
|
|
State: serial.CRProbeStateNoResponse,
|
|
}}}
|
|
values, err = observer.Observe(context.Background(), now.Add(4*time.Second))
|
|
if err != nil || len(values) != 1 || values[0].State != "up" {
|
|
t.Fatalf("transient CR miss must use shared grace period = %+v, %v", values, err)
|
|
}
|
|
}
|
|
|
|
func TestCompositeObserverKeepsPartialObservations(t *testing.T) {
|
|
observer := CompositeObserver{partialObserver{value: domain.LinkStateObservation{
|
|
Kind: "serial", Resource: "CH06", State: "up", Signal: "cr",
|
|
}}}
|
|
values, err := observer.Observe(context.Background(), time.Now())
|
|
if err == nil || len(values) != 1 || values[0].Resource != "CH06" {
|
|
t.Fatalf("partial observations = %+v, %v", values, err)
|
|
}
|
|
}
|
|
|