Files
ITWorx Pulse release export bd774932d5
Public source validation / validate (push) Failing after 3m8s
Publish ITWorx Pulse source
2026-09-03 02:09:19 +02:00

94 lines
4.8 KiB
Go

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)
}
}