Files
mesh-catalog/modules/disk-load/cmd/disk-load/folders_test.go
T
jschoubben 4ef2fa2dff
mesh/merge-gate waiting in the build queue since 01:13
mesh/delivery superseded: a newer head of the same pull request
mesh/check failed: unit: diskload/cmd/disk-load: # diskload/cmd/disk-load [diskload/cmd/disk-load.test] | 4ef2fa2dff1c on 3bbae41037e3 | unit 4096 run…
disk_folders: bound the walk at 25 s, below the tool runner's 30 s, and say findings as lines (issue 1804)
2026-10-22 01:13:29 +02:00

171 lines
5.6 KiB
Go

package main
import (
"fmt"
"io/fs"
"os"
"strings"
"testing"
"time"
)
const mb = 1 << 20
// megabytes writes a file of n MiB under the fake root.
func (f *fake) megabytes(path string, n int) { f.file(path, strings.Repeat("x", n*mb)) }
func aFullDisk(t *testing.T) *fake {
f := newFake(t)
f.megabytes("var/lib/docker/overlay2/a/layer", 8)
f.megabytes("var/lib/docker/overlay2/b/layer", 4)
f.megabytes("var/log/big.log", 3)
f.megabytes("home/jo/video.mkv", 6)
f.megabytes("home/jo/deep/er/est/file", 1)
f.dir("empty")
return f
}
func folders(t *testing.T, m *Machine, args map[string]any) FolderReport {
t.Helper()
out, err := tool(t, m, "disk_folders").Run(args)
if err != nil {
t.Fatal(err)
}
return out.(FolderReport)
}
func paths(r FolderReport) string {
var p []string
for _, f := range r.Largest {
p = append(p, f.Path)
}
return strings.Join(p, ",")
}
// 1804/AC1: given a disk with folders of known sizes, when disk_folders runs with the defaults, then it answers
// the largest folders to depth 2, biggest first, with bytes and GiB, and the total of the path.
func TestFoldersAreRankedBiggestFirstToDepthTwoByDefault(t *testing.T) {
r := folders(t, aFullDisk(t).machine(), nil)
if got := paths(r); !strings.HasPrefix(got, "/var,/var/lib,") || !strings.Contains(got, "/home,/home/jo") {
t.Fatalf("ranked %s", got)
}
if r.Path != "/" || r.Partial || r.Unreadable != 0 {
t.Errorf("%+v", r)
}
top := r.Largest[0]
if top.Path != "/var" || top.Depth != 1 || top.Bytes < 15*mb || top.Bytes > 16*mb || top.GiB <= 0 {
t.Errorf("top %+v", top)
}
for _, f := range r.Largest {
if f.Depth > 2 || f.Depth < 1 {
t.Errorf("%+v is outside depth 2", f)
}
if strings.Contains(f.Path, "overlay2") {
t.Errorf("%s is deeper than 2", f.Path)
}
}
if r.Bytes < 22*mb || r.Bytes > 24*mb {
t.Errorf("total %d", r.Bytes)
}
}
// 1804/AC2: given a path, a depth and a limit, then only that folder is measured, depth is held at 3 and limit at 100.
func TestFoldersTakeAPathADepthAndALimitHeldAtThreeAndAHundred(t *testing.T) {
f := aFullDisk(t)
r := folders(t, f.machine(), map[string]any{"path": "/var/lib/docker", "depth": 1.0})
if r.Path != "/var/lib/docker" || paths(r) != "/var/lib/docker/overlay2" {
t.Errorf("%s: %s", r.Path, paths(r))
}
r = folders(t, f.machine(), map[string]any{"path": "/home", "depth": 9.0, "limit": 2.0})
if len(r.Largest) != 2 {
t.Errorf("limit 2 gave %d", len(r.Largest))
}
r = folders(t, f.machine(), map[string]any{"path": "/home", "depth": 9.0})
if got := paths(r); !strings.Contains(got, "/home/jo/deep/er") || strings.Contains(got, "/home/jo/deep/er/est") {
t.Errorf("depth 3 under /home is /home/jo/deep/er: %s", got)
}
tl := tool(t, f.machine(), "disk_folders")
for _, bad := range []map[string]any{{"depth": 0.0}, {"limit": 0.0}, {"depth": "deep"}, {"path": "relative/dir"}, {"path": "/nope"}, {"path": "/var/log/big.log"}} {
if _, err := tl.Run(bad); err == nil {
t.Errorf("%v was accepted", bad)
}
}
for i := 0; i < 120; i++ {
f.file(fmt.Sprintf("many/d%03d/f", i), "x")
}
if r := folders(t, f.machine(), map[string]any{"path": "/many", "limit": 500.0}); len(r.Largest) != 100 {
t.Errorf("limit held at %d, not 100", len(r.Largest))
}
}
// 1804/AC3: given a mount below the path, then it is never entered or counted, and is named.
func TestFoldersNeverCrossIntoAnotherFilesystem(t *testing.T) {
f := aFullDisk(t)
m := f.machine()
m.DevOf = func(path string, info fs.FileInfo) uint64 {
if path == "/var/lib/docker" {
return 7
}
return 1
}
r := folders(t, m, nil)
if strings.Contains(paths(r), "docker") || len(r.Skipped) != 1 || r.Skipped[0] != "/var/lib/docker" {
t.Errorf("%s, skipped %v", paths(r), r.Skipped)
}
if r.Bytes > 14*mb {
t.Errorf("total %d includes the mount", r.Bytes)
}
}
// 1804/AC4: given folders it cannot read, then they are counted and named and the rest is still answered.
func TestUnreadableFoldersAreCountedAndNamed(t *testing.T) {
f := aFullDisk(t)
m := f.machine()
real := os.ReadDir
m.ReadDir = func(p string) ([]os.DirEntry, error) {
if strings.HasSuffix(p, "/var/log") || strings.HasSuffix(p, "/overlay2/b") {
return nil, fmt.Errorf("open %s: permission denied", p)
}
return real(p)
}
r := folders(t, m, map[string]any{"depth": 3.0})
if r.Unreadable != 2 || strings.Join(r.UnreadableFolders, ",") != "/var/lib/docker/overlay2/b,/var/log" {
t.Errorf("%d: %v", r.Unreadable, r.UnreadableFolders)
}
if r.Partial || !strings.Contains(strings.Join(r.Said, "\n"), "2 folders could not be read") {
t.Errorf("%+v", r)
}
}
// 1804/AC5: given a walk that outlasts its bound, then it answers what it measured, partial, with where it stopped.
func TestALongWalkAnswersPartialAndSaysWhereItStopped(t *testing.T) {
f := aFullDisk(t)
m := f.machine()
now := time.Unix(0, 0)
m.Clock = func() time.Time { now = now.Add(10 * time.Second); return now }
r := folders(t, m, nil)
if !r.Partial || r.StoppedAt == "" || len(r.Said) < 2 || !strings.HasPrefix(r.Said[0], "PARTIAL") || !strings.Contains(r.Said[0], r.StoppedAt) {
t.Errorf("%+v", r)
}
}
// 1804/AC6: a hard link is counted once, and the tool changes nothing.
func TestAHardLinkIsCountedOnce(t *testing.T) {
f := newFake(t)
f.megabytes("a/big", 5)
if err := os.Link(f.root+"/a/big", f.root+"/a/again"); err != nil {
t.Skip(err)
}
r := folders(t, f.machine(), nil)
if r.Bytes > 6*mb {
t.Errorf("counted twice: %d", r.Bytes)
}
}
// 1804/AC7: the bound sits below the 30 s the tool runner gives a call, so a partial answer reaches its caller.
func TestTheWalkBoundIsBelowTheToolRunnersCallLimit(t *testing.T) {
if FolderBudget >= 30*time.Second {
t.Errorf("the budget %s is not below the 30 s a call gets", FolderBudget)
}
}