Keep records on other filesystems under an operand with -x (closes #99)
check / check (push) Canceled after 0s
check / check (push) Canceled after 0s
With -x the walk does not enter a directory on another filesystem than its operand, but the update phase still deleted every record below it. A separate scan of a filesystem mounted under the operand therefore lost its records on every -x scan and re-hashed its whole tree. The walk now sends an event naming each directory it declines this way, and the walk phase treats the records below those directories like records outside the operands: they are not deleted, and their sizes join the size census. README "scan mode" and "Rules for the walk" state the rule. Model: opus-5-5
This commit was merged in pull request #105.
This commit is contained in:
+88
-3
@@ -997,7 +997,7 @@ func dirEntryFor(t *testing.T, dir, name string) fs.DirEntry {
|
||||
}
|
||||
|
||||
// callSubdirJob runs subdirJob against e under parent, collecting any
|
||||
// warning events it emits (subdirJob emits at most one).
|
||||
// events it emits (subdirJob emits at most one).
|
||||
func callSubdirJob(t *testing.T, p string, e fs.DirEntry,
|
||||
parent dirJob, oneFS bool,
|
||||
) (dirJob, bool, []walkEvent) {
|
||||
@@ -1075,8 +1075,15 @@ func TestSubdirJobOneFilesystem(t *testing.T) {
|
||||
t.Fatalf("accepted = %v, want %v", ok, tc.wantOK)
|
||||
}
|
||||
|
||||
if len(evs) != 0 {
|
||||
t.Fatalf("unexpected events: %+v", evs)
|
||||
// A refused descent names the directory, so the scan keeps
|
||||
// the records below it.
|
||||
var wantEvs []walkEvent
|
||||
if !ok {
|
||||
wantEvs = []walkEvent{{otherFSDir: sub}}
|
||||
}
|
||||
|
||||
if !slices.Equal(evs, wantEvs) {
|
||||
t.Fatalf("events = %+v, want %+v", evs, wantEvs)
|
||||
}
|
||||
|
||||
// The accepted job must carry the operand's device down, or -x
|
||||
@@ -1128,6 +1135,84 @@ func TestSubdirJobStatError(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestScanOneFilesystemKeepsOtherFilesystemRecords checks that with -x a
|
||||
// record below a directory the walk does not enter, because it is on
|
||||
// another filesystem than its operand, counts as outside the operands:
|
||||
// it is not deleted, and its size joins the size census. The record of
|
||||
// a gone file that is not below that directory is still deleted, even
|
||||
// when its path begins with the directory's name. The walk starts from
|
||||
// an operand device that no directory here is on, so no second real
|
||||
// filesystem is needed; the phases after it run as runPhases runs them.
|
||||
func TestScanOneFilesystemKeepsOtherFilesystemRecords(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dir := t.TempDir()
|
||||
db := openTestDB(t)
|
||||
|
||||
// A scan without -x records the file below mnt, and mnt2, which is
|
||||
// beside mnt, not below it.
|
||||
mounted := writeFile(t, dir, "mnt/f", pattern(1, 100))
|
||||
gone := writeFile(t, dir, "mnt2", pattern(3, 50))
|
||||
syncTree(t, db, dir)
|
||||
|
||||
err := os.Remove(gone)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// The same size as mnt/f, so it is hashed only if mnt/f's size is
|
||||
// in the census.
|
||||
added := writeFile(t, dir, "added", pattern(2, 100))
|
||||
|
||||
info, err := os.Lstat(dir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
dev, ok := deviceOfInfo(info)
|
||||
if !ok {
|
||||
t.Skip("platform exposes no device id")
|
||||
}
|
||||
|
||||
ctx := t.Context()
|
||||
s := &scanState{db: db}
|
||||
|
||||
err = s.loadIndex(ctx, []string{dir})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
jobs, subdirs, events := startWalkWorkers(ctx, 2, true)
|
||||
root := dirJob{path: dir, rootDev: dev + 1, rootDevOK: true}
|
||||
dispatchDirs(ctx, []dirJob{root}, jobs, subdirs)
|
||||
|
||||
changed, unhashed := s.walkPhase(events)
|
||||
s.partition(changed, unhashed)
|
||||
|
||||
err = s.hashPhase(ctx, 2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = s.updatePhase(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
recs := dbRecords(t, db)
|
||||
recordByPath(t, recs, mounted)
|
||||
|
||||
if recordByPath(t, recs, added).head == "" {
|
||||
t.Errorf("%s not hashed: the census lacks the size of %s",
|
||||
added, mounted)
|
||||
}
|
||||
|
||||
if slices.Contains(recordPaths(recs), gone) {
|
||||
t.Errorf("record for %s kept: the file is gone and not below mnt",
|
||||
gone)
|
||||
}
|
||||
}
|
||||
|
||||
// buildSmokeTree recreates the README smoke-test filesystem layout
|
||||
// with deterministic content and returns the tree root.
|
||||
func buildSmokeTree(t *testing.T) string {
|
||||
|
||||
Reference in New Issue
Block a user