Files
restic-manager/internal/restic/runner_test.go
T
steve 7fd29427a0 restic: tighten RunCheck lock sniff + RunStats zero-snapshot test
Narrow the LockPresent predicate from bare "locked" (too broad) to
"stale lock" and "already locked" — the two phrases restic actually
emits. Replace TestRunCheckParsesLock with table-driven
TestRunCheckLockSniff covering both trigger phrases and a benign
"locked-file" line that must not set LockPresent. Add
TestRunStatsZeroSnapshots to pin that RunStats accepts zero-snapshot
JSON without error.
2026-05-04 10:19:15 +01:00

186 lines
5.4 KiB
Go

package restic
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"testing"
)
// setupScriptBin writes a small shell script to a temp directory,
// makes it executable, and returns its path. scriptBody is the
// complete script content (without the shebang line — that's added
// automatically).
func setupScriptBin(t *testing.T, scriptBody string) string {
t.Helper()
dir := t.TempDir()
p := filepath.Join(dir, "restic")
content := "#!/bin/sh\n" + scriptBody + "\n"
if err := os.WriteFile(p, []byte(content), 0o755); err != nil {
t.Fatalf("setupScriptBin: %v", err)
}
return p
}
// captureLines returns a LineHandler that appends "stream:line" into
// the returned slice pointer (safe for single-goroutine test use).
func captureLines() (*[]string, LineHandler) {
var lines []string
h := func(stream, line string, _ any) {
lines = append(lines, fmt.Sprintf("%s:%s", stream, line))
}
return &lines, h
}
// --- B1: RunPrune + B2: RunCheck ---
func TestRunPruneInvokesPrune(t *testing.T) {
// Shell script that echoes its args; "prune" should appear in output.
bin := setupScriptBin(t, `echo "$@"`)
env := Env{Bin: bin}
lines, h := captureLines()
if err := env.RunPrune(context.Background(), h); err != nil {
t.Fatalf("RunPrune returned error: %v", err)
}
for _, l := range *lines {
if strings.Contains(l, "prune") {
return
}
}
t.Fatalf("expected 'prune' in captured output; got: %v", *lines)
}
// --- B2: RunCheck ---
func TestRunCheckLockSniff(t *testing.T) {
cases := []struct {
name string
stderrLine string
wantLocked bool
}{
{"stale lock", "Found stale lock from PID 1234", true},
{"already locked", "repository is already locked exclusively", true},
{"benign mention", "subdir/locked-file ok", false},
{"empty", "", false},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
// Script emits the line on stderr, then exits 0.
script := fmt.Sprintf(`printf '%%s\n' %q >&2`, c.stderrLine)
bin := setupScriptBin(t, script)
env := Env{Bin: bin}
res, err := env.RunCheck(context.Background(), 0, nil)
if err != nil {
t.Fatalf("RunCheck returned unexpected error: %v", err)
}
if res.LockPresent != c.wantLocked {
t.Fatalf("LockPresent: got %v, want %v (line: %q)", res.LockPresent, c.wantLocked, c.stderrLine)
}
if res.ErrorsFound {
t.Fatal("expected ErrorsFound=false")
}
})
}
}
func TestRunCheckErrorsFoundOnExit1(t *testing.T) {
bin := setupScriptBin(t, `exit 1`)
env := Env{Bin: bin}
res, err := env.RunCheck(context.Background(), 0, nil)
if err != nil {
t.Fatalf("RunCheck returned unexpected error (should have absorbed exit 1): %v", err)
}
if !res.ErrorsFound {
t.Fatal("expected ErrorsFound=true for exit 1")
}
}
func TestRunCheckSubsetArg(t *testing.T) {
bin := setupScriptBin(t, `echo "$@"`)
env := Env{Bin: bin}
lines, h := captureLines()
if _, err := env.RunCheck(context.Background(), 25, h); err != nil {
t.Fatalf("RunCheck: %v", err)
}
want := "--read-data-subset 25%"
for _, l := range *lines {
if strings.Contains(l, want) {
return
}
}
t.Fatalf("expected %q in captured output; got: %v", want, *lines)
}
// --- B3: RunUnlock + RunStats ---
func TestRunUnlockInvokesUnlock(t *testing.T) {
bin := setupScriptBin(t, `echo "$@"`)
env := Env{Bin: bin}
lines, h := captureLines()
if err := env.RunUnlock(context.Background(), h); err != nil {
t.Fatalf("RunUnlock: %v", err)
}
for _, l := range *lines {
if strings.Contains(l, "unlock") {
return
}
}
t.Fatalf("expected 'unlock' in captured output; got: %v", *lines)
}
func TestRunStatsParsesJSON(t *testing.T) {
bin := setupScriptBin(t, `echo '{"total_size":1234,"total_uncompressed_size":5678,"snapshots_count":3,"total_file_count":100,"total_blob_count":50}'`)
env := Env{Bin: bin}
stats, err := env.RunStats(context.Background(), nil)
if err != nil {
t.Fatalf("RunStats: %v", err)
}
if stats.TotalSize != 1234 {
t.Fatalf("TotalSize: got %d, want 1234", stats.TotalSize)
}
if stats.TotalUncompressed != 5678 {
t.Fatalf("TotalUncompressed: got %d, want 5678", stats.TotalUncompressed)
}
if stats.SnapshotsCount != 3 {
t.Fatalf("SnapshotsCount: got %d, want 3", stats.SnapshotsCount)
}
if stats.TotalFileCount != 100 {
t.Fatalf("TotalFileCount: got %d, want 100", stats.TotalFileCount)
}
if stats.TotalBlobCount != 50 {
t.Fatalf("TotalBlobCount: got %d, want 50", stats.TotalBlobCount)
}
}
func TestRunStatsErrorsWithoutJSON(t *testing.T) {
bin := setupScriptBin(t, `echo "no json here"`)
env := Env{Bin: bin}
_, err := env.RunStats(context.Background(), nil)
if err == nil {
t.Fatal("expected error when no JSON in output")
}
if !strings.Contains(err.Error(), "no JSON in output") {
t.Fatalf("unexpected error: %v", err)
}
}
func TestRunStatsZeroSnapshots(t *testing.T) {
// Confirms RunStats succeeds and returns a valid *RepoStats when the
// repo has no snapshots (snapshots_count=0). A regression that
// re-added a "SnapshotsCount > 0" guard would return an error here.
bin := setupScriptBin(t, `echo '{"total_size":0,"total_uncompressed_size":0,"snapshots_count":0,"total_file_count":0,"total_blob_count":0}'`)
env := Env{Bin: bin}
stats, err := env.RunStats(context.Background(), nil)
if err != nil {
t.Fatalf("RunStats with zero snapshots returned unexpected error: %v", err)
}
if stats == nil {
t.Fatal("expected non-nil *RepoStats, got nil")
}
if stats.SnapshotsCount != 0 {
t.Fatalf("SnapshotsCount: got %d, want 0", stats.SnapshotsCount)
}
}