Warn about and skip non-regular and .zfs operands, keeping their records (closes #9)
check / check (push) Successful in 1m11s

A symlink, socket, FIFO or device-node operand, or a directory operand
named .zfs, was ignored without a word and stayed in the scanned
operands, so the update phase deleted every record stored beneath it.
Such an operand now gets a one-line warning naming the path and what it
is, counts as skipped in the summary, and is dropped before the
database index is loaded, so the records beneath it count as outside
the scanned operands and are not deleted, unless it lies under another
operand. The exit status stays 0. An operand that turns into one of
these between that check and the walk is warned about and skipped by
the walk instead. README "scan mode" and "Rules for the walk" say so.

Model: opus-5-5
This commit is contained in:
2026-10-03 15:02:56 +00:00
parent d63d3cc7fc
commit cf34b59632
5 changed files with 215 additions and 42 deletions
+96 -9
View File
@@ -89,16 +89,25 @@ func makeReadOnly(t *testing.T, path string) {
func captureStdout(t *testing.T) func() string {
t.Helper()
f, err := os.Create(filepath.Join(t.TempDir(), "stdout"))
return captureStream(t, &os.Stdout)
}
// captureStream redirects *stream (os.Stdout or os.Stderr) to a file
// for the rest of the test and returns a function reading back
// everything written to it.
func captureStream(t *testing.T, stream **os.File) func() string {
t.Helper()
f, err := os.Create(filepath.Join(t.TempDir(), "capture"))
if err != nil {
t.Fatal(err)
}
saved := os.Stdout
os.Stdout = f
saved := *stream
*stream = f
t.Cleanup(func() {
os.Stdout = saved
*stream = saved
_ = f.Close()
})
@@ -358,21 +367,30 @@ func scanFixture(t *testing.T) []string {
t.Fatal(err)
}
var stderr bytes.Buffer
scanOK(t, dir)
return dupes
}
// scanOK runs scan over operands, fails the test unless it exits 0 with
// nothing on stdout, and returns everything it printed to stderr.
func scanOK(t *testing.T, operands ...string) string {
t.Helper()
stdout := captureStdout(t)
stderr := captureStream(t, &os.Stderr)
code := run([]string{cmdScan, dir}, &stderr)
code := run(append([]string{cmdScan}, operands...), os.Stderr)
if code != exitOK {
t.Fatalf("run(scan) = %d, want %d; stderr: %s",
code, exitOK, stderr.String())
t.Fatalf("run(scan %q) = %d, want %d; stderr: %s",
operands, code, exitOK, stderr())
}
if got := stdout(); got != "" {
t.Errorf("scan stdout = %q, want nothing (data only)", got)
}
return dupes
return stderr()
}
func TestRunScanSucceedsDespiteWarnings(t *testing.T) {
@@ -383,6 +401,75 @@ func TestRunScanSucceedsDespiteWarnings(t *testing.T) {
assertNoSidecars(t, path)
}
func TestRunScanSkipsSymlinkOperand(t *testing.T) {
path := testDBPath(t)
t.Setenv(databaseEnv, path)
dir := t.TempDir()
writeFile(t, dir, "target/sub/f", pattern(1, 10))
link := filepath.Join(dir, "link")
err := os.Symlink(filepath.Join(dir, "target"), link)
if err != nil {
t.Fatal(err)
}
// Scanning a directory through the symlink stores a record beneath
// the symlink's own path for a file beneath its target.
scanOK(t, filepath.Join(link, "sub"))
assertOperandSkipped(t, path, link, "symlink",
filepath.Join(link, "sub", "f"))
}
func TestRunScanSkipsZFSOperand(t *testing.T) {
path := testDBPath(t)
t.Setenv(databaseEnv, path)
zfs := filepath.Join(t.TempDir(), ".zfs")
snapshot := filepath.Join(zfs, "snapshot", "hourly")
f := writeFile(t, snapshot, "f", pattern(1, 10))
// An operand beneath a .zfs directory is walked, because it is not
// itself named .zfs.
scanOK(t, snapshot)
assertOperandSkipped(t, path, zfs, ".zfs directory", f)
}
// assertOperandSkipped scans operand alone and checks that it is skipped
// as kind: a warning naming it, one skip in the summary, exit 0, and the
// record for kept, which an earlier scan stored beneath operand, still
// in the database at dbPath.
func assertOperandSkipped(t *testing.T, dbPath, operand, kind,
kept string,
) {
t.Helper()
stderr := scanOK(t, operand)
warning := "walk " + operand + ": skipping " + kind + " operand\n"
if !strings.Contains(stderr, warning) {
t.Errorf("stderr = %q, want %q", stderr, warning)
}
summary := "scan: 0 files seen (0 added, 0 updated, 0 removed, " +
"0 unchanged), 1 skipped\n"
if !strings.Contains(stderr, summary) {
t.Errorf("stderr = %q, want %q", stderr, summary)
}
db, err := openDB(dbPath, reportParams)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = db.Close() })
recordByPath(t, dbRecords(t, db), kept)
}
func TestRunReportSucceeds(t *testing.T) {
path := testDBPath(t)
t.Setenv(databaseEnv, path)