9bb4b09a04
CI / Lint + build + test (push) Has been cancelled
Post-repair hardware validation pipeline for Proxmox cluster hosts. Go orchestrator + in-image agent + mkosi live image + bundled dnsmasq PXE + SQLite + HTMX/SSE UI + notify registry + janitor + full docs.
134 lines
3.6 KiB
Go
134 lines
3.6 KiB
Go
package janitor
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"vetting/internal/store"
|
|
)
|
|
|
|
// fakeStores is a test double that records what the janitor asked for
|
|
// and hands back canned runs/artifacts. It lets us verify both the
|
|
// cleanup contract (files deleted, rows deleted) and that the janitor
|
|
// honours a zero retention as a no-op.
|
|
type fakeStores struct {
|
|
cutoffSeen time.Time
|
|
runsOlder []int64
|
|
artifactsByID map[int64][]store.Artifact
|
|
deleted map[int64]bool
|
|
logs map[int64]string
|
|
}
|
|
|
|
func (f *fakeStores) CompletedOlderThan(_ context.Context, cutoff time.Time) ([]int64, error) {
|
|
f.cutoffSeen = cutoff
|
|
return f.runsOlder, nil
|
|
}
|
|
|
|
func (f *fakeStores) DeleteArtifactsForRun(_ context.Context, runID int64) ([]store.Artifact, error) {
|
|
if f.deleted == nil {
|
|
f.deleted = map[int64]bool{}
|
|
}
|
|
f.deleted[runID] = true
|
|
return f.artifactsByID[runID], nil
|
|
}
|
|
|
|
func (f *fakeStores) LogPathFor(runID int64) string { return f.logs[runID] }
|
|
|
|
func writeTempFile(t *testing.T, dir, name string) string {
|
|
t.Helper()
|
|
p := filepath.Join(dir, name)
|
|
if err := os.WriteFile(p, []byte("x"), 0o644); err != nil {
|
|
t.Fatalf("write %s: %v", p, err)
|
|
}
|
|
return p
|
|
}
|
|
|
|
func TestSweepDeletesArtifactsAndLogs(t *testing.T) {
|
|
dir := t.TempDir()
|
|
p1 := writeTempFile(t, dir, "artifact-1.bin")
|
|
p2 := writeTempFile(t, dir, "artifact-2.json")
|
|
log1 := writeTempFile(t, dir, "run-1.log")
|
|
|
|
s := &fakeStores{
|
|
runsOlder: []int64{1},
|
|
artifactsByID: map[int64][]store.Artifact{
|
|
1: {{ID: 10, RunID: 1, Path: p1}, {ID: 11, RunID: 1, Path: p2}},
|
|
},
|
|
logs: map[int64]string{1: log1},
|
|
}
|
|
j := New(Config{
|
|
ArtifactRetention: 24 * time.Hour,
|
|
LogRetention: 24 * time.Hour,
|
|
Interval: time.Minute,
|
|
}, s)
|
|
if err := j.Sweep(context.Background(), time.Now().UTC()); err != nil {
|
|
t.Fatalf("sweep: %v", err)
|
|
}
|
|
if !s.deleted[1] {
|
|
t.Fatalf("run 1 not passed to DeleteArtifactsForRun")
|
|
}
|
|
for _, p := range []string{p1, p2, log1} {
|
|
if _, err := os.Stat(p); !os.IsNotExist(err) {
|
|
t.Errorf("file %s still exists (err=%v)", p, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSweepIsNoopWhenRetentionsAreZero(t *testing.T) {
|
|
dir := t.TempDir()
|
|
p := writeTempFile(t, dir, "keep.bin")
|
|
s := &fakeStores{
|
|
runsOlder: []int64{1},
|
|
artifactsByID: map[int64][]store.Artifact{
|
|
1: {{ID: 10, RunID: 1, Path: p}},
|
|
},
|
|
logs: map[int64]string{1: p},
|
|
}
|
|
j := New(Config{}, s) // all zero
|
|
if err := j.Sweep(context.Background(), time.Now().UTC()); err != nil {
|
|
t.Fatalf("sweep: %v", err)
|
|
}
|
|
if s.deleted[1] {
|
|
t.Fatalf("expected no deletion for zero retention")
|
|
}
|
|
if _, err := os.Stat(p); err != nil {
|
|
t.Fatalf("file should still exist: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSweepSkipsMissingFilesGracefully(t *testing.T) {
|
|
s := &fakeStores{
|
|
runsOlder: []int64{7},
|
|
artifactsByID: map[int64][]store.Artifact{
|
|
7: {{ID: 99, RunID: 7, Path: "/nonexistent/path.bin"}},
|
|
},
|
|
logs: map[int64]string{7: "/nonexistent/run-7.log"},
|
|
}
|
|
j := New(Config{ArtifactRetention: time.Hour, LogRetention: time.Hour}, s)
|
|
if err := j.Sweep(context.Background(), time.Now().UTC()); err != nil {
|
|
t.Fatalf("sweep: %v", err)
|
|
}
|
|
if !s.deleted[7] {
|
|
t.Fatalf("run 7 should have been processed")
|
|
}
|
|
}
|
|
|
|
func TestSweepUsesTheLongerCutoff(t *testing.T) {
|
|
s := &fakeStores{}
|
|
j := New(Config{
|
|
ArtifactRetention: 72 * time.Hour,
|
|
LogRetention: 24 * time.Hour,
|
|
}, s)
|
|
now := time.Date(2026, 4, 17, 12, 0, 0, 0, time.UTC)
|
|
if err := j.Sweep(context.Background(), now); err != nil {
|
|
t.Fatalf("sweep: %v", err)
|
|
}
|
|
want := now.Add(-72 * time.Hour)
|
|
if !s.cutoffSeen.Equal(want) {
|
|
t.Fatalf("cutoff = %v, want %v (the longer of the two retentions)", s.cutoffSeen, want)
|
|
}
|
|
}
|