This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
package forecast
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/itworx/pulse/internal/pool"
|
||||
"github.com/itworx/pulse/internal/share"
|
||||
)
|
||||
|
||||
type staticShares struct{ snapshot share.Snapshot }
|
||||
|
||||
func (s staticShares) Snapshot(context.Context) (share.Snapshot, error) { return s.snapshot, nil }
|
||||
|
||||
type staticPools struct{ snapshot pool.Snapshot }
|
||||
|
||||
func (s staticPools) Snapshot(context.Context) (pool.Snapshot, error) { return s.snapshot, nil }
|
||||
|
||||
type fullHistory struct{ points []Point }
|
||||
|
||||
func (h fullHistory) Points(_ context.Context, _, _ string, _ time.Time, limit int) ([]Point, error) {
|
||||
if limit != len(h.points) {
|
||||
return nil, errors.New("history query did not reserve the current-point slot")
|
||||
}
|
||||
return h.points, nil
|
||||
}
|
||||
|
||||
func TestStorageProviderProjectsQualifiedShareHistory(t *testing.T) {
|
||||
now := time.Date(2026, 8, 12, 1, 0, 0, 0, time.UTC)
|
||||
shareSnapshot := share.Snapshot{Source: share.Source{Freshness: share.Fresh}, Shares: []share.Share{{
|
||||
ID: "media", Name: "Media", UsedBytes: 400, SizeObservedAt: now, SizeState: share.SizeAvailable,
|
||||
Placements: []share.Placement{{PoolID: "cache"}}, GrowthHistory: []share.GrowthPoint{
|
||||
{ObservedAt: now.Add(-14 * 24 * time.Hour), UsedBytes: 200},
|
||||
{ObservedAt: now.Add(-7 * 24 * time.Hour), UsedBytes: 300},
|
||||
{ObservedAt: now, UsedBytes: 400},
|
||||
},
|
||||
}}}
|
||||
poolSnapshot := pool.Snapshot{Pools: []pool.Pool{{ID: " CACHE ", UsableBytes: 1000}}}
|
||||
provider := StorageProvider{Shares: staticShares{shareSnapshot}, Pools: staticPools{poolSnapshot}, Policy: Policy{Enabled: true}, Now: func() time.Time { return now }}
|
||||
|
||||
snapshot, err := provider.Snapshot(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if snapshot.QualifiedCount != 1 || len(snapshot.Items) != 1 || snapshot.Items[0].EntityID != "media" || snapshot.Items[0].ProjectedAt == nil || snapshot.Items[0].CapacityBytes != 1000 {
|
||||
t.Fatalf("qualified storage forecast missing: %+v", snapshot)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStorageProviderDoesNotCountInsufficientOrStaleHistory(t *testing.T) {
|
||||
now := time.Date(2026, 8, 12, 1, 0, 0, 0, time.UTC)
|
||||
base := share.Share{ID: "media", Name: "Media", UsedBytes: 400, SizeObservedAt: now, SizeState: share.SizeAvailable, Placements: []share.Placement{{PoolID: "cache"}}, GrowthHistory: []share.GrowthPoint{{ObservedAt: now, UsedBytes: 400}}}
|
||||
provider := StorageProvider{Shares: staticShares{share.Snapshot{Source: share.Source{Freshness: share.Fresh}, Shares: []share.Share{base}}}, Pools: staticPools{pool.Snapshot{Pools: []pool.Pool{{ID: "cache", UsableBytes: 1000}}}}, Policy: Policy{Enabled: true}, Now: func() time.Time { return now }}
|
||||
|
||||
insufficient, err := provider.Snapshot(context.Background())
|
||||
if err != nil || insufficient.QualifiedCount != 0 || insufficient.Items[0].Reason != "insufficient_points" {
|
||||
t.Fatalf("insufficient history counted as forecast: %+v, %v", insufficient, err)
|
||||
}
|
||||
base.SizeState = share.SizeStale
|
||||
provider.Shares = staticShares{share.Snapshot{Source: share.Source{Freshness: share.Stale}, Shares: []share.Share{base}}}
|
||||
stale, err := provider.Snapshot(context.Background())
|
||||
if err != nil || stale.QualifiedCount != 0 || stale.Items[0].Reason != "history_stale" || stale.Items[0].ProjectedAt != nil {
|
||||
t.Fatalf("stale history was not explicit: %+v, %v", stale, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStorageProviderEmptySourceNeverCreatesZeroByteEntity(t *testing.T) {
|
||||
now := time.Date(2026, 8, 12, 1, 0, 0, 0, time.UTC)
|
||||
provider := StorageProvider{Shares: staticShares{share.UnknownSnapshot(now, "shares", "unraid", "source_unavailable")}, Pools: staticPools{pool.Snapshot{}}, Policy: Policy{Enabled: true}, Now: func() time.Time { return now }}
|
||||
snapshot, err := provider.Snapshot(context.Background())
|
||||
if err != nil || len(snapshot.Items) != 0 || snapshot.QualifiedCount != 0 || snapshot.Reason != "source_unavailable" {
|
||||
t.Fatalf("empty source created a forecast entity: %+v, %v", snapshot, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStorageProviderKeepsMatureHistoryInsidePointBound(t *testing.T) {
|
||||
now := time.Date(2026, 8, 12, 1, 0, 0, 0, time.UTC)
|
||||
history := make([]Point, 0, 4)
|
||||
for day := 4; day > 0; day-- {
|
||||
history = append(history, Point{ObservedAt: now.Add(-time.Duration(day) * 24 * time.Hour), UsedBytes: uint64((5 - day) * 100)})
|
||||
}
|
||||
provider := StorageProvider{
|
||||
Shares: staticShares{share.Snapshot{Source: share.Source{Freshness: share.Fresh}, Shares: []share.Share{{ID: "media", Name: "Media", UsedBytes: 500, SizeObservedAt: now, SizeState: share.SizeAvailable, Placements: []share.Placement{{PoolID: "cache"}}}}}},
|
||||
Pools: staticPools{pool.Snapshot{Pools: []pool.Pool{{ID: "cache", UsableBytes: 1000}}}},
|
||||
History: fullHistory{points: history}, Policy: Policy{Enabled: true, MaxPoints: 5}, Now: func() time.Time { return now },
|
||||
}
|
||||
snapshot, err := provider.Snapshot(context.Background())
|
||||
if err != nil || snapshot.QualifiedCount != 1 || snapshot.Items[0].DataPoints != 5 {
|
||||
t.Fatalf("mature history exceeded its bound: %+v, %v", snapshot, err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user