This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user