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

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 != "down" || values[0].Signal != "cr" {
t.Fatalf("CR no response after UP must be DOWN = %+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)
}
}