package main import ( "slices" "testing" ) // Signature hashes shared by the smoke-test records. const ( f1Head = "f1h" f1Tail = "f1t" f2Head = "f2h" f2Tail = "f2t" ) // smokeTreeRecs mirrors the README smoke-test tree layout: /d/t1 and // /d/t2 are identical, /d/t3 differs from them only by one filename. func smokeTreeRecs() []scanRec { return []scanRec{ {size: 3000, head: f1Head, tail: f1Tail, path: "/d/t1/f1"}, {size: 100, head: f2Head, tail: f2Tail, path: "/d/t1/sub/f2"}, {size: 3000, head: f1Head, tail: f1Tail, path: "/d/t2/f1"}, {size: 100, head: f2Head, tail: f2Tail, path: "/d/t2/sub/f2"}, {size: 3000, head: f1Head, tail: f1Tail, path: "/d/t3/f1"}, {size: 100, head: f2Head, tail: f2Tail, path: "/d/t3/sub/f2renamed"}, } } // nodeByPath finds the directory node with the given path. func nodeByPath(t *testing.T, dirs []*treeNode, path string) *treeNode { t.Helper() for _, d := range dirs { if d.path == path { return d } } t.Fatalf("no directory node with path %q", path) return nil } // groupPaths flattens tree groups into their member path lists. func groupPaths(groups [][]*treeNode) [][]string { out := make([][]string, 0, len(groups)) for _, g := range groups { paths := make([]string, 0, len(g)) for _, n := range g { paths = append(paths, n.path) } out = append(out, paths) } return out } func TestBuildHierarchyCounts(t *testing.T) { t.Parallel() super, dirs := buildHierarchy(smokeTreeRecs()) super.compute() d := nodeByPath(t, dirs, "/d") if d.fileCount != 6 || d.totalSize != 9300 { t.Errorf("/d: fileCount %d size %d, want 6 9300", d.fileCount, d.totalSize) } t1 := nodeByPath(t, dirs, "/d/t1") if t1.fileCount != 2 || t1.totalSize != 3100 { t.Errorf("/d/t1: fileCount %d size %d, want 2 3100", t1.fileCount, t1.totalSize) } sub := nodeByPath(t, dirs, "/d/t1/sub") if sub.fileCount != 1 || sub.totalSize != 100 { t.Errorf("/d/t1/sub: fileCount %d size %d, want 1 100", sub.fileCount, sub.totalSize) } } func TestTreeDigests(t *testing.T) { t.Parallel() super, dirs := buildHierarchy(smokeTreeRecs()) super.compute() t1 := nodeByPath(t, dirs, "/d/t1") t2 := nodeByPath(t, dirs, "/d/t2") t3 := nodeByPath(t, dirs, "/d/t3") if t1.digest != t2.digest { t.Error("identical trees /d/t1 and /d/t2 have different digests") } // t3 differs only in a filename; names are part of the digest. if t1.digest == t3.digest { t.Error("/d/t3 digest equals /d/t1 despite a renamed file") } sub1 := nodeByPath(t, dirs, "/d/t1/sub") sub3 := nodeByPath(t, dirs, "/d/t3/sub") if sub1.digest == sub3.digest { t.Error("subdirs with differently-named files share a digest") } } func TestTreeDigestContentSensitivity(t *testing.T) { t.Parallel() const sharedTail = "same" recs := []scanRec{ {size: 10, head: sharedTail, tail: sharedTail, path: "/r/a/f"}, {size: 10, head: "DIFF", tail: sharedTail, path: "/r/b/f"}, } super, dirs := buildHierarchy(recs) super.compute() a := nodeByPath(t, dirs, "/r/a") b := nodeByPath(t, dirs, "/r/b") if a.digest == b.digest { t.Error("trees with different file content share a digest") } } func TestCollectTreeGroupsMaximal(t *testing.T) { t.Parallel() super, dirs := buildHierarchy(smokeTreeRecs()) super.compute() groups := collectTreeGroups(dirs, super) want := [][]string{{"/d/t1", "/d/t2"}} if got := groupPaths(groups); !slices.EqualFunc(got, want, slices.Equal) { t.Fatalf("groups = %v, want %v", got, want) } // The /d/t1/sub vs /d/t2/sub group must be suppressed as implied // by its parents' group; the winning group reports one copy's // recursive totals. if groups[0][0].fileCount != 2 || groups[0][0].totalSize != 3100 { t.Errorf("group totals: %d files %d bytes, want 2 3100", groups[0][0].fileCount, groups[0][0].totalSize) } } func TestCollectTreeGroupsDeterministic(t *testing.T) { t.Parallel() recs := smokeTreeRecs() super, dirs := buildHierarchy(recs) super.compute() forward := groupPaths(collectTreeGroups(dirs, super)) reversed := slices.Clone(recs) slices.Reverse(reversed) superR, dirsR := buildHierarchy(reversed) superR.compute() backward := groupPaths(collectTreeGroups(dirsR, superR)) if !slices.EqualFunc(forward, backward, slices.Equal) { t.Fatalf("output depends on record order: %v vs %v", forward, backward) } } func TestCollectTreeGroupsSiblings(t *testing.T) { t.Parallel() // Identical sibling dirs share a parent, so their group cannot be // implied by a parent group and must be reported. recs := []scanRec{ {size: 10, head: "h", tail: "t", path: "/p/x1/f"}, {size: 10, head: "h", tail: "t", path: "/p/x2/f"}, } super, dirs := buildHierarchy(recs) super.compute() got := groupPaths(collectTreeGroups(dirs, super)) want := [][]string{{"/p/x1", "/p/x2"}} if !slices.EqualFunc(got, want, slices.Equal) { t.Fatalf("groups = %v, want %v", got, want) } } func TestCollectTreeGroupsDifferingParents(t *testing.T) { t.Parallel() // /p/a and /q/b contain identical x subtrees, but /p/a has an // extra file, so the parents' digests differ and the x group must // be reported. recs := []scanRec{ {size: 10, head: "h", tail: "t", path: "/p/a/x/f"}, {size: 99, head: "e", tail: "e", path: "/p/a/extra"}, {size: 10, head: "h", tail: "t", path: "/q/b/x/f"}, } super, dirs := buildHierarchy(recs) super.compute() got := groupPaths(collectTreeGroups(dirs, super)) want := [][]string{{"/p/a/x", "/q/b/x"}} if !slices.EqualFunc(got, want, slices.Equal) { t.Fatalf("groups = %v, want %v", got, want) } }