Covers splitNUL framing, parseScanStream (malformed records, tabs and newlines in paths, unterminated trailing record), duplicate grouping with ordering/tie-break/determinism, humanBytes units, Merkle digest equality and name/content sensitivity, maximal-tree suppression (including sibling and differing-parent cases), hashHeadTail edge sizes and error paths, walkPass .zfs/symlink skipping, statPass vanished files, and an end-to-end walk/stat/hash pipeline test of the README smoke-test scenario. 64% statement coverage.
205 lines
5.0 KiB
Go
205 lines
5.0 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// mkRecord serializes one scan record in the on-the-wire format.
|
|
func mkRecord(size, mtime int64, head, tail, path string) string {
|
|
return fmt.Sprintf("%d\t%d\t%s\t%s\t%s\x00",
|
|
size, mtime, head, tail, path)
|
|
}
|
|
|
|
func TestSplitNULScanner(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
sc := bufio.NewScanner(strings.NewReader("a\x00bb\x00\x00tail"))
|
|
sc.Split(splitNUL)
|
|
|
|
var got []string
|
|
for sc.Scan() {
|
|
got = append(got, sc.Text())
|
|
}
|
|
|
|
err := sc.Err()
|
|
if err != nil {
|
|
t.Fatalf("scanner error: %v", err)
|
|
}
|
|
|
|
want := []string{"a", "bb", "", "tail"}
|
|
if !slices.Equal(got, want) {
|
|
t.Fatalf("tokens = %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestParseScanStream(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
stream := mkRecord(10, 1, "h1", "t1", "/a/x") +
|
|
"garbage-without-tabs\x00" +
|
|
"notanumber\t1\th\tt\t/a/bad\x00" +
|
|
mkRecord(20, 2, "h2", "t2", "/a/tab\tin\tname") +
|
|
"30\t3\th3\tt3\t/trailing/no-nul"
|
|
|
|
recs, malformed := parseScanStream(strings.NewReader(stream), "test")
|
|
|
|
if malformed != 2 {
|
|
t.Errorf("malformed = %d, want 2", malformed)
|
|
}
|
|
|
|
want := []scanRec{
|
|
{size: 10, head: "h1", tail: "t1", path: "/a/x"},
|
|
{size: 20, head: "h2", tail: "t2", path: "/a/tab\tin\tname"},
|
|
{size: 30, head: "h3", tail: "t3", path: "/trailing/no-nul"},
|
|
}
|
|
if !slices.Equal(recs, want) {
|
|
t.Fatalf("recs = %+v, want %+v", recs, want)
|
|
}
|
|
}
|
|
|
|
func TestParseScanStreamPathWithNewline(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
in := strings.NewReader(mkRecord(5, 9, "h", "t", "/a/new\nline"))
|
|
|
|
recs, malformed := parseScanStream(in, "test")
|
|
if malformed != 0 || len(recs) != 1 {
|
|
t.Fatalf("got %d recs, %d malformed, want 1, 0",
|
|
len(recs), malformed)
|
|
}
|
|
|
|
if recs[0].path != "/a/new\nline" {
|
|
t.Fatalf("path = %q, want %q", recs[0].path, "/a/new\nline")
|
|
}
|
|
}
|
|
|
|
func TestParseScanStreamEmpty(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
recs, malformed := parseScanStream(strings.NewReader(""), "test")
|
|
if len(recs) != 0 || malformed != 0 {
|
|
t.Fatalf("got %d recs, %d malformed, want 0, 0",
|
|
len(recs), malformed)
|
|
}
|
|
}
|
|
|
|
func TestCollectDupeGroups(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
recs := []scanRec{
|
|
{size: 100, head: "h", tail: "t", path: "/z/b"},
|
|
{size: 100, head: "h", tail: "t", path: "/z/a"},
|
|
{size: 100, head: "h", tail: "t", path: "/z/c"},
|
|
{size: 4000, head: "H", tail: "T", path: "/big/2"},
|
|
{size: 4000, head: "H", tail: "T", path: "/big/1"},
|
|
// Same size as the /z group but a different head hash.
|
|
{size: 100, head: "other", tail: "t", path: "/z/d"},
|
|
// A singleton signature must not form a group.
|
|
{size: 7, head: "u", tail: "u", path: "/lonely"},
|
|
}
|
|
|
|
groups := collectDupeGroups(recs)
|
|
if len(groups) != 2 {
|
|
t.Fatalf("len(groups) = %d, want 2", len(groups))
|
|
}
|
|
|
|
if groups[0].size != 4000 ||
|
|
!slices.Equal(groups[0].paths, []string{"/big/1", "/big/2"}) {
|
|
t.Errorf("groups[0] = %+v, want size 4000, paths /big/1 /big/2",
|
|
groups[0])
|
|
}
|
|
|
|
if groups[1].size != 100 ||
|
|
!slices.Equal(groups[1].paths, []string{"/z/a", "/z/b", "/z/c"}) {
|
|
t.Errorf("groups[1] = %+v, want size 100, paths /z/a /z/b /z/c",
|
|
groups[1])
|
|
}
|
|
}
|
|
|
|
func TestCollectDupeGroupsTieBreak(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
recs := []scanRec{
|
|
{size: 50, head: "b", tail: "b", path: "/beta/2"},
|
|
{size: 50, head: "b", tail: "b", path: "/beta/1"},
|
|
{size: 50, head: "a", tail: "a", path: "/alpha/2"},
|
|
{size: 50, head: "a", tail: "a", path: "/alpha/1"},
|
|
}
|
|
|
|
groups := collectDupeGroups(recs)
|
|
if len(groups) != 2 {
|
|
t.Fatalf("len(groups) = %d, want 2", len(groups))
|
|
}
|
|
|
|
// Equal sizes: ordered by first path ascending.
|
|
if groups[0].paths[0] != "/alpha/1" || groups[1].paths[0] != "/beta/1" {
|
|
t.Fatalf("tie-break order wrong: %q then %q",
|
|
groups[0].paths[0], groups[1].paths[0])
|
|
}
|
|
}
|
|
|
|
func TestCollectDupeGroupsDeterministic(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
recs := []scanRec{
|
|
{size: 1, head: "a", tail: "a", path: "/p/1"},
|
|
{size: 1, head: "a", tail: "a", path: "/p/2"},
|
|
{size: 2, head: "b", tail: "b", path: "/q/1"},
|
|
{size: 2, head: "b", tail: "b", path: "/q/2"},
|
|
}
|
|
|
|
forward := collectDupeGroups(recs)
|
|
|
|
reversed := slices.Clone(recs)
|
|
slices.Reverse(reversed)
|
|
|
|
backward := collectDupeGroups(reversed)
|
|
if !slices.EqualFunc(forward, backward, func(a, b dupeGroup) bool {
|
|
return a.size == b.size && slices.Equal(a.paths, b.paths)
|
|
}) {
|
|
t.Fatalf("output depends on record order: %+v vs %+v",
|
|
forward, backward)
|
|
}
|
|
}
|
|
|
|
func TestHumanBytes(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cases := []struct {
|
|
n int64
|
|
want string
|
|
}{
|
|
{0, "0 B"},
|
|
{1, "1 B"},
|
|
{1023, "1023 B"},
|
|
{1024, "1.0 KiB"},
|
|
{1536, "1.5 KiB"},
|
|
{1 << 20, "1.0 MiB"},
|
|
{5 << 30, "5.0 GiB"},
|
|
{1 << 40, "1.0 TiB"},
|
|
{1 << 50, "1.0 PiB"},
|
|
{1 << 60, "1.0 EiB"},
|
|
}
|
|
for _, c := range cases {
|
|
if got := humanBytes(c.n); got != c.want {
|
|
t.Errorf("humanBytes(%d) = %q, want %q", c.n, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMalformedNote(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
if got := malformedNote(0); got != "" {
|
|
t.Errorf("malformedNote(0) = %q, want empty", got)
|
|
}
|
|
|
|
if got := malformedNote(3); got != " (3 malformed, skipped)" {
|
|
t.Errorf("malformedNote(3) = %q", got)
|
|
}
|
|
}
|