// SPDX-License-Identifier: GPL-2.0-or-later // Copyright (C) 2026 Bryan Joshua Pedini package main import ( "bytes" "context" "database/sql" "errors" "fmt" "io" "log/slog" "path/filepath" "sync" "testing" "time" "github.com/moby/moby/api/types/container" ) // syncBuffer is a goroutine-safe bytes.Buffer for capturing logs and output type syncBuffer struct { mu sync.Mutex b bytes.Buffer } func (s *syncBuffer) Write(p []byte) (int, error) { s.mu.Lock() defer s.mu.Unlock() return s.b.Write(p) } func (s *syncBuffer) String() string { s.mu.Lock() defer s.mu.Unlock() return s.b.String() } // newTestLogger returns a logger writing to a captured buffer func newTestLogger(level slog.Level) (*slog.Logger, *syncBuffer) { buf := &syncBuffer{} return NewLogger(buf, level), buf } // traceDiscard is a TRACE-level logger whose output is dropped (exercises every log call) func traceDiscard() *slog.Logger { return NewLogger(io.Discard, LevelTrace) } // fakeClock returns a fixed time advancing by step on every call type fakeClock struct { mu sync.Mutex t time.Time step time.Duration } func newFakeClock(start time.Time, step time.Duration) *fakeClock { return &fakeClock{t: start, step: step} } func (c *fakeClock) Now() time.Time { c.mu.Lock() defer c.mu.Unlock() t := c.t c.t = c.t.Add(c.step) return t } // seqGenerator returns the given tokens in order, then an error func seqGenerator(values ...string) func() (string, error) { var mu sync.Mutex i := 0 return func() (string, error) { mu.Lock() defer mu.Unlock() if i >= len(values) { return "", errors.New("generator exhausted") } v := values[i] i++ return v, nil } } // testDBPath returns a fresh database path inside the test's temporary directory func testDBPath(t *testing.T) string { t.Helper() return filepath.Join(t.TempDir(), "dpsapi.db") } // openTestDB opens and initializes a fresh database func openTestDB(t *testing.T) *sql.DB { t.Helper() db, err := OpenDatabase(context.Background(), testDBPath(t), traceDiscard()) if err != nil { t.Fatalf("OpenDatabase: %v", err) } t.Cleanup(func() { db.Close() }) return db } // mustExec executes a statement or fails the test func mustExec(t *testing.T, db *sql.DB, query string, args ...any) { t.Helper() if _, err := db.Exec(query, args...); err != nil { t.Fatalf("exec %q: %v", query, err) } } // countRows returns the number of rows in a table func countRows(t *testing.T, db *sql.DB, table string) int { t.Helper() var n int if err := db.QueryRow(`SELECT COUNT(*) FROM ` + table).Scan(&n); err != nil { t.Fatalf("count %s: %v", table, err) } return n } // fakeDocker is an in-memory ContainerLister type fakeDocker struct { mu sync.Mutex containers []container.Summary err error calls int closed bool onList func(ctx context.Context) } func (f *fakeDocker) ListRunningContainers(ctx context.Context) ([]container.Summary, error) { f.mu.Lock() f.calls++ hook := f.onList f.mu.Unlock() if hook != nil { hook(ctx) } f.mu.Lock() defer f.mu.Unlock() if f.err != nil { return nil, f.err } return f.containers, nil } func (f *fakeDocker) Close() error { f.mu.Lock() defer f.mu.Unlock() f.closed = true return nil } func (f *fakeDocker) Calls() int { f.mu.Lock() defer f.mu.Unlock() return f.calls } // makeContainers builds n distinct container summaries func makeContainers(n int) []container.Summary { out := make([]container.Summary, 0, n) for i := 0; i < n; i++ { out = append(out, container.Summary{ ID: fmt.Sprintf("%064x", i+1), Names: []string{fmt.Sprintf("/c%d", i)}, Image: "busybox:latest", State: container.StateRunning, Status: "Up 1 second", Labels: map[string]string{"index": fmt.Sprint(i)}, }) } return out } // envMap returns a lookupEnv function backed by a map func envMap(m map[string]string) func(string) (string, bool) { return func(k string) (string, bool) { v, ok := m[k] return v, ok } }