Author SHA1 Message Date
sneak d58e9fe256 Stop the disk size tests failing at random on ZFS (closes #150)
check / check (push) Canceled after 0s
unix.Sync can return before ZFS has counted a file's blocks, so a test
that expected the size on disk to be settled after it failed at random.
The test that needs the size to change now calls unix.Sync in a loop
until it does, for at most 30 seconds. All three tests read the size
before and after the scan and accept either value.

Model: opus-5-5
2026-11-02 22:32:07 +00:00
2 changed files with 24 additions and 55 deletions

No files matched your search

+4 -6
View File
@@ -30,12 +30,10 @@ Newest first. An entry ends with its date and its issue or branch, where those
are known. are known.
- The three disk size tests in `scan_test.go` no longer fail at random on ZFS. - The three disk size tests in `scan_test.go` no longer fail at random on ZFS.
`unix.Sync` can return before ZFS has counted a file's blocks, and one ZFS `unix.Sync` can return before ZFS has counted a file's blocks. The test that
commit can take a file through several sizes. The test that needs the size to needs the size to change waits for it, for at most 30 seconds. Each test reads
change waits for it, for at most 30 seconds. Each test reads the size before the size before and after its scan and accepts either value (2026-11-02,
and after its scan. The rewrite test rescans until the two reads match, then https://git.eeqj.de/sneak/sfdupes/issues/150)
compares exactly. The other two fail only on a `disk_size` outside the two
reads (2026-11-02, https://git.eeqj.de/sneak/sfdupes/issues/150)
- `TODO.md` rewritten from scratch by the same rules as `README.md`, sentence by - `TODO.md` rewritten from scratch by the same rules as `README.md`, sentence by
sentence. Every completed step is kept, with its date and its issue or branch. sentence. Every completed step is kept, with its date and its issue or branch.
+20 -49
View File
@@ -44,15 +44,12 @@ func writeFile(t *testing.T, dir, name string, data []byte) string {
// diskSizeOf returns the bytes the file at path takes on disk, from its // diskSizeOf returns the bytes the file at path takes on disk, from its
// own lstat: st_blocks counts 512-byte units. ZFS counts a write's // own lstat: st_blocks counts 512-byte units. ZFS counts a write's
// blocks only when it commits the write, up to seconds later, and one // blocks only when it commits the write, seconds later and sometimes in
// commit can take the file through several growing sizes. So this value // steps, so this value can change at any moment. fsync does not hasten
// can change at any moment. Neither fsync nor unix.Sync hastens that, // that. unix.Sync usually does, but it can return before the blocks are
// but a unix.Sync call often takes long enough that the blocks are // counted. A test that needs the value to change therefore waits for
// counted when it returns. A test that needs the value to change waits // that, and a test that compares it with a scan's disk_size reads it
// for the change. A test that compares it with a scan's disk_size reads // before and after the scan and accepts either.
// it before and after the scan. The rewrite test rescans until the two
// reads match, then compares exactly. The other two tests fail only on
// a disk_size outside the two reads.
func diskSizeOf(t *testing.T, path string) int64 { func diskSizeOf(t *testing.T, path string) int64 {
t.Helper() t.Helper()
@@ -1452,8 +1449,6 @@ func TestScanStoresDiskSize(t *testing.T) {
p := sparseFile(t, t.TempDir(), "sparse", length) p := sparseFile(t, t.TempDir(), "sparse", length)
pokeAt(t, p, 0, []byte("x")) pokeAt(t, p, 0, []byte("x"))
unix.Sync() // see diskSizeOf
size := diskSizeOf(t, p) size := diskSizeOf(t, p)
if size == 0 || size >= length { if size == 0 || size >= length {
t.Fatalf("the file takes %d bytes on disk, want more than none "+ t.Fatalf("the file takes %d bytes on disk, want more than none "+
@@ -1471,8 +1466,8 @@ func TestScanStoresDiskSize(t *testing.T) {
after := diskSizeOf(t, p) after := diskSizeOf(t, p)
got := recordByPath(t, dbRecords(t, db), p).diskSize got := recordByPath(t, dbRecords(t, db), p).diskSize
if got < before || got > after { if got != before && got != after {
t.Errorf("scan of %s: disk_size = %d, want %d to %d", t.Errorf("scan of %s: disk_size = %d, want %d or %d",
operand, got, before, after) operand, got, before, after)
} }
} }
@@ -1515,42 +1510,20 @@ func TestSyncScanRewriteUpdatesDiskSize(t *testing.T) {
unix.Sync() unix.Sync()
} }
// ZFS may still be counting, so the size may change during a rescan. // ZFS may still be counting, so the size may change during the rescan.
// Only a rescan with the same size on both sides of it can be compared before := diskSizeOf(t, p)
// exactly. A rescan that keeps the old size still fails, because the
// wait made before differ from it.
for {
before := diskSizeOf(t, p)
st := syncTree(t, db, dir) st := syncTree(t, db, dir)
after := diskSizeOf(t, p) after := diskSizeOf(t, p)
if st != (scanStats{walked: 1, updated: 1}) { if st != (scanStats{walked: 1, updated: 1}) {
t.Fatalf("rescan stats = %+v, want 1 updated", st) t.Fatalf("rescan stats = %+v, want 1 updated", st)
} }
if before == after { got := recordByPath(t, dbRecords(t, db), p).diskSize
got := recordByPath(t, dbRecords(t, db), p).diskSize if got != before && got != after {
if got != after { t.Errorf("disk_size = %d, want %d or %d", got, before, after)
t.Errorf("disk_size = %d, want %d", got, after)
}
return
}
if time.Now().After(deadline) {
t.Fatalf("the size on disk was still changing 30 seconds after "+
"the write into its hole: %d, then %d", before, after)
}
// A later mtime makes the next rescan read the file again.
later = later.Add(time.Second)
err = os.Chtimes(p, later, later)
if err != nil {
t.Fatal(err)
}
} }
} }
@@ -1574,8 +1547,6 @@ func TestSyncScanUnchangedUpdatesDiskSize(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
unix.Sync() // see diskSizeOf
// ZFS may count the blocks during the rescan; see diskSizeOf. // ZFS may count the blocks during the rescan; see diskSizeOf.
before := diskSizeOf(t, p) before := diskSizeOf(t, p)
@@ -1589,8 +1560,8 @@ func TestSyncScanUnchangedUpdatesDiskSize(t *testing.T) {
} }
got := recordByPath(t, dbRecords(t, db), p).diskSize got := recordByPath(t, dbRecords(t, db), p).diskSize
if got < before || got > after { if got != before && got != after {
t.Errorf("disk_size = %d, want %d to %d", got, before, after) t.Errorf("disk_size = %d, want %d or %d", got, before, after)
} }
} }