Public source validation / validate (push) Failing after 3m8s
108 lines
4.9 KiB
Go
108 lines
4.9 KiB
Go
package storagescenarios
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/itworx/pulse/internal/array"
|
|
"github.com/itworx/pulse/internal/datasource"
|
|
"github.com/itworx/pulse/internal/disk"
|
|
"github.com/itworx/pulse/internal/pool"
|
|
"github.com/itworx/pulse/internal/reconciliation"
|
|
)
|
|
|
|
type scenario struct {
|
|
ID string `json:"id"`
|
|
Timeline []json.RawMessage `json:"timeline"`
|
|
ExpectedOutcomes []json.RawMessage `json:"expectedOutcomes"`
|
|
}
|
|
|
|
func fixture(t *testing.T, name string) scenario {
|
|
t.Helper()
|
|
_, file, _, ok := runtime.Caller(0)
|
|
if !ok {
|
|
t.Fatal("could not locate scenario test")
|
|
}
|
|
content, err := os.ReadFile(filepath.Join(filepath.Dir(file), "..", "..", "fixtures", "scenarios", name))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var got scenario
|
|
if err := json.Unmarshal(content, &got); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(got.Timeline) == 0 || len(got.ExpectedOutcomes) == 0 {
|
|
t.Fatalf("fixture %s has no executable expectations", name)
|
|
}
|
|
return got
|
|
}
|
|
|
|
func TestStorageFailureScenarioSuite(t *testing.T) {
|
|
for _, name := range []string{"array-degraded.json", "smart-warning.json", "disk-temperature.json", "pool-capacity-pressure.json", "prometheus-stale.json"} {
|
|
scenario := fixture(t, name)
|
|
if scenario.ID == "" {
|
|
t.Fatalf("fixture %s has no stable id", name)
|
|
}
|
|
}
|
|
|
|
now := time.Date(2026, 8, 1, 12, 0, 0, 0, time.UTC)
|
|
arraySnapshot, err := array.Normalize(array.RawSnapshot{
|
|
Source: array.Source{ID: "fixture-array", Type: "fixture"}, State: array.StateDegraded,
|
|
Parity: array.RawParity{Present: true, State: "failed"},
|
|
Members: []array.RawMember{{ID: "disk-1", Name: "Disk 1", Role: "data", State: "emulated", CapacityBytes: 100}},
|
|
ObservedAt: now, ReceivedAt: now,
|
|
}, now, array.Limits{}, array.Policy{})
|
|
if err != nil || arraySnapshot.State != array.StateDegraded || arraySnapshot.Parity.State != "failed" {
|
|
t.Fatalf("array scenario=%+v err=%v", arraySnapshot, err)
|
|
}
|
|
|
|
diskSnapshot, err := disk.Normalize(disk.RawSnapshot{
|
|
Source: disk.Source{ID: "fixture-disks", Type: "fixture"},
|
|
Disks: []disk.RawDisk{{ID: "disk-smart", Name: "Disk SMART", SizeBytes: 100, SMART: &disk.RawSMART{Available: true, Overall: disk.SMARTPassed, ObservedAt: now, Attributes: []disk.RawSMARTAttribute{{ID: "197", Name: "Current Pending Sector", RawValue: 2}}}, Temperature: &disk.RawTemperature{Available: true, Celsius: 52, ObservedAt: now}}},
|
|
ObservedAt: now, ReceivedAt: now,
|
|
}, now, disk.Limits{}, disk.Policy{})
|
|
if err != nil || diskSnapshot.Disks[0].SMART.Overall != disk.SMARTAttention || diskSnapshot.Disks[0].Temperature.Status != "attention" {
|
|
t.Fatalf("disk scenario=%+v err=%v", diskSnapshot, err)
|
|
}
|
|
|
|
poolSnapshot, err := pool.Normalize(pool.RawSnapshot{
|
|
Source: pool.Source{ID: "fixture-pools", Type: "fixture"},
|
|
Pools: []pool.RawPool{{ID: "cache", Name: "Cache", Filesystem: "btrfs", State: pool.StateHealthy, UsableBytes: 1000, UsedBytes: 950, Capabilities: pool.Capabilities{Capacity: pool.CapabilityAvailable}}},
|
|
ObservedAt: now, ReceivedAt: now,
|
|
}, now, pool.Limits{}, pool.Policy{})
|
|
if err != nil || poolSnapshot.Pools[0].FreeBytes != 50 || poolSnapshot.Pools[0].UtilizationPercent != 95 {
|
|
t.Fatalf("pool scenario=%+v err=%v", poolSnapshot, err)
|
|
}
|
|
|
|
previous := []reconciliation.Existing{{EntityID: reconciliation.StableEntityID("source", "container", "abc"), SourceID: "source", ExternalType: "container", ExternalID: "abc"}}
|
|
result, err := reconciliation.Reconcile("source", datasource.HealthUnknown, nil, previous, nil, now)
|
|
if err != nil || len(result.Entities) != 1 || len(result.Tombstoned) != 0 {
|
|
t.Fatalf("source outage removed inventory: %+v err=%v", result, err)
|
|
}
|
|
}
|
|
|
|
func TestStorageFalseGreenAndCancellationScenarios(t *testing.T) {
|
|
now := time.Date(2026, 8, 1, 12, 0, 0, 0, time.UTC)
|
|
stale, err := disk.Normalize(disk.RawSnapshot{Source: disk.Source{ID: "fixture-disks", Type: "fixture"}, Disks: []disk.RawDisk{{ID: "disk", Name: "Disk", SizeBytes: 100, UsedBytes: 40, SMART: &disk.RawSMART{Available: true, Overall: disk.SMARTPassed, ObservedAt: now.Add(-2 * time.Hour)}}}, ObservedAt: now.Add(-2 * time.Hour), ReceivedAt: now.Add(-2 * time.Hour)}, now, disk.Limits{}, disk.Policy{FreshnessMaxAge: time.Minute, SMARTFreshnessMaxAge: time.Hour})
|
|
if err != nil || stale.Source.State != disk.StateUnknown || stale.Disks[0].SMART.State != disk.SMARTUnknown {
|
|
t.Fatalf("stale data became green: %+v err=%v", stale, err)
|
|
}
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
if _, err := (array.Adapter{}).Snapshot(ctx); !errors.Is(err, context.Canceled) {
|
|
t.Fatalf("array cancellation err=%v", err)
|
|
}
|
|
if _, err := (disk.Adapter{}).Snapshot(ctx); !errors.Is(err, context.Canceled) {
|
|
t.Fatalf("disk cancellation err=%v", err)
|
|
}
|
|
if _, err := (pool.Adapter{}).Snapshot(ctx); !errors.Is(err, context.Canceled) {
|
|
t.Fatalf("pool cancellation err=%v", err)
|
|
}
|
|
}
|