From 237a86dee50b6e246020a78cb3e92d94dde37eaf Mon Sep 17 00:00:00 2001 From: Steve Cliff Date: Sun, 3 May 2026 22:29:09 +0100 Subject: [PATCH] restic: tighten RunCheck lock sniff + RunStats zero-snapshot test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- internal/restic/runner.go | 2 +- internal/restic/runner_test.go | 56 +++++++++++++++++++++++++++------- 2 files changed, 46 insertions(+), 12 deletions(-) diff --git a/internal/restic/runner.go b/internal/restic/runner.go index 4d1ab59..b54ac71 100644 --- a/internal/restic/runner.go +++ b/internal/restic/runner.go @@ -367,7 +367,7 @@ func (e Env) RunCheck(ctx context.Context, subsetPct int, handle LineHandler) (C var res CheckResult sniff := func(stream, line string, ev any) { if stream == "stderr" { - if strings.Contains(line, "Found stale lock") || strings.Contains(line, "locked") { + if strings.Contains(line, "stale lock") || strings.Contains(line, "already locked") { res.LockPresent = true } } diff --git a/internal/restic/runner_test.go b/internal/restic/runner_test.go index 70870fd..de24ef2 100644 --- a/internal/restic/runner_test.go +++ b/internal/restic/runner_test.go @@ -54,18 +54,34 @@ func TestRunPruneInvokesPrune(t *testing.T) { // --- B2: RunCheck --- -func TestRunCheckParsesLock(t *testing.T) { - bin := setupScriptBin(t, `echo "Found stale lock" >&2`) - env := Env{Bin: bin} - res, err := env.RunCheck(context.Background(), 0, nil) - if err != nil { - t.Fatalf("RunCheck returned unexpected error: %v", err) +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}, } - if !res.LockPresent { - t.Fatal("expected LockPresent=true") - } - if res.ErrorsFound { - t.Fatal("expected ErrorsFound=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") + } + }) } } @@ -149,3 +165,21 @@ func TestRunStatsErrorsWithoutJSON(t *testing.T) { 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) + } +}