Record each file's size on disk and print it in trees (closes #147)
check / check (push) Waiting to run
check / check (push) Waiting to run
The files table gets a disk_size column: the file's allocated blocks, st_blocks × 512, from the lstat the walk already does. scan writes it with every record it writes. When a rescan finds a file with its size and mtime unchanged but a different disk_size, it updates disk_size alone, without reading the file. trees adds disk_size up per directory and prints it as a column after size. On a compressed or sparse file it is less than size, so two copies of one tree can differ; each row shows the dupe copy's. Judgement calls: a row's disk_size is the dupe copy's, not the first's. A file whose record gets only a new disk_size counts as unchanged. Model: opus-5-5
This commit is contained in:
+40
-11
@@ -17,18 +17,24 @@ const treesOpenLine = "[1/4] open: opening the database, elapsed 0s, " +
|
||||
|
||||
// smokeTreeRecs mirrors the README smoke-test tree layout: /d/t1 and
|
||||
// /d/t2 are identical, /d/t3 differs from them only by one filename.
|
||||
// Each file takes one 4 KiB block on disk.
|
||||
func smokeTreeRecs() []scanRec {
|
||||
f1Head, f1Tail, f1Content := testHash("f1h"), testHash("f1t"), testHash("f1c")
|
||||
f2Head, f2Tail, f2Content := testHash("f2h"), testHash("f2t"), testHash("f2c")
|
||||
|
||||
return []scanRec{
|
||||
{size: 3000, head: f1Head, tail: f1Tail, content: f1Content, path: "/d/t1/f1"},
|
||||
{size: 100, head: f2Head, tail: f2Tail, content: f2Content, path: "/d/t1/sub/f2"},
|
||||
{size: 3000, head: f1Head, tail: f1Tail, content: f1Content, path: "/d/t2/f1"},
|
||||
{size: 100, head: f2Head, tail: f2Tail, content: f2Content, path: "/d/t2/sub/f2"},
|
||||
{size: 3000, head: f1Head, tail: f1Tail, content: f1Content, path: "/d/t3/f1"},
|
||||
{size: 100, head: f2Head, tail: f2Tail, content: f2Content,
|
||||
path: "/d/t3/sub/f2renamed"},
|
||||
{size: 3000, diskSize: 4096, head: f1Head, tail: f1Tail,
|
||||
content: f1Content, path: "/d/t1/f1"},
|
||||
{size: 100, diskSize: 4096, head: f2Head, tail: f2Tail,
|
||||
content: f2Content, path: "/d/t1/sub/f2"},
|
||||
{size: 3000, diskSize: 4096, head: f1Head, tail: f1Tail,
|
||||
content: f1Content, path: "/d/t2/f1"},
|
||||
{size: 100, diskSize: 4096, head: f2Head, tail: f2Tail,
|
||||
content: f2Content, path: "/d/t2/sub/f2"},
|
||||
{size: 3000, diskSize: 4096, head: f1Head, tail: f1Tail,
|
||||
content: f1Content, path: "/d/t3/f1"},
|
||||
{size: 100, diskSize: 4096, head: f2Head, tail: f2Tail,
|
||||
content: f2Content, path: "/d/t3/sub/f2renamed"},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,13 +204,33 @@ func TestRunTreesEscapesPaths(t *testing.T) {
|
||||
code, exitOK, stderr.String())
|
||||
}
|
||||
|
||||
want := "first\tdupe\tfiles\tsize\n" +
|
||||
`/d/\tone\ntwo\rthree\\four` + "\t/d/A\t1\t5\n"
|
||||
want := "first\tdupe\tfiles\tsize\tdisk_size\n" +
|
||||
`/d/\tone\ntwo\rthree\\four` + "\t/d/A\t1\t5\t4096\n"
|
||||
if got := stdout.String(); got != want {
|
||||
t.Errorf("stdout = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRunTreesDiskSize runs trees on two copies of a tree whose files
|
||||
// take different space on disk. The row's disk_size is the sum over the
|
||||
// dupe copy's files, those in its subdirectory included.
|
||||
func TestRunTreesDiskSize(t *testing.T) {
|
||||
f, g := testHash("f"), testHash("g")
|
||||
recs := []scanRec{
|
||||
{size: 10, diskSize: 4096, head: f, tail: f, content: f, path: "/a/f"},
|
||||
{size: 20, diskSize: 4096, head: g, tail: g, content: g, path: "/a/sub/g"},
|
||||
{size: 10, diskSize: 512, head: f, tail: f, content: f, path: "/b/f"},
|
||||
{size: 20, diskSize: 1024, head: g, tail: g, content: g, path: "/b/sub/g"},
|
||||
}
|
||||
|
||||
t.Setenv(databaseEnv, seedDatabase(t, recs))
|
||||
|
||||
want := "first\tdupe\tfiles\tsize\tdisk_size\n/a\t/b\t2\t30\t1536\n"
|
||||
if got := runStdout(t, cmdTrees); got != want {
|
||||
t.Errorf("stdout = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRunTreesProgressNamesEachPhase runs trees on the smoke-test
|
||||
// records with stderr not a terminal, and checks every line it prints
|
||||
// there. After the line naming the database, each phase prints a line
|
||||
@@ -226,7 +252,8 @@ func TestRunTreesProgressNamesEachPhase(t *testing.T) {
|
||||
code, exitOK, stderr())
|
||||
}
|
||||
|
||||
report := "first\tdupe\tfiles\tsize\n/d/t1\t/d/t2\t2\t3100\n"
|
||||
report := "first\tdupe\tfiles\tsize\tdisk_size\n" +
|
||||
"/d/t1\t/d/t2\t2\t3100\t8192\n"
|
||||
if got := stdout.String(); got != report {
|
||||
t.Errorf("stdout = %q, want %q", got, report)
|
||||
}
|
||||
@@ -291,7 +318,9 @@ func TestWriteTreeGroupsOnTerminal(t *testing.T) {
|
||||
shown = append(shown, line[strings.LastIndex(line, "\r")+1:])
|
||||
}
|
||||
|
||||
want := []string{"first\tdupe\tfiles\tsize", "/d/t1\t/d/t2\t2\t3100"}
|
||||
want := []string{
|
||||
"first\tdupe\tfiles\tsize\tdisk_size", "/d/t1\t/d/t2\t2\t3100\t8192",
|
||||
}
|
||||
if len(shown) < len(want) || !slices.Equal(shown[:len(want)], want) {
|
||||
t.Errorf("terminal shows lines %q, want %q first", shown, want)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user