package space import ( "context" "os" "path/filepath" "testing" "time" ) func mk(t *testing.T, dir string, files map[string]int) { for p, n := range files { full := filepath.Join(dir, p) if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(full, make([]byte, n), 0o644); err != nil { t.Fatal(err) } } } func TestRefreshAndResume(t *testing.T) { dir := t.TempDir() mk(t, dir, map[string]int{"a/big1": 300 << 10, "a/small": 100, "b/c/big2": 500 << 10, "b/d/big3": 200 << 10}) res, err := Walk(context.Background(), []string{dir}, Options{}, nil) if err != nil { t.Fatal(err) } root := Find(res.Root, dir) if root == nil || root.Files != 4 { t.Fatalf("full walk: %+v", root) } want := root.Size // grow a file in place (its directory's mtime does not change) and add a file elsewhere time.Sleep(20 * time.Millisecond) if err := os.WriteFile(filepath.Join(dir, "a/big1"), make([]byte, 600<<10), 0o644); err != nil { t.Fatal(err) } mk(t, dir, map[string]int{"b/d/big4": 400 << 10}) res2, err := Walk(context.Background(), []string{dir}, Options{Prev: res.Root, Modes: map[string]Mode{dir: ModeMTime}}, nil) if err != nil { t.Fatal(err) } r2 := Find(res2.Root, dir) if r2.Files != 5 || r2.Size != want+(300<<10)+(400<<10) { t.Fatalf("mtime refresh: files %d size %d want %d (reused %d read %d)", r2.Files, r2.Size, want+(300<<10)+(400<<10), res2.Reused, res2.Read) } if res2.Reused == 0 || res2.Read == 0 { t.Fatalf("mtime refresh should reuse some and read some: reused %d read %d", res2.Reused, res2.Read) } // diff mode: only b/d named dirty; a's growth must NOT be seen (that is the contract), b/d's new file must dirty := map[string]bool{filepath.Join(dir, "b/d"): true} touched := map[string]bool{dir: true, filepath.Join(dir, "b"): true} res3, err := Walk(context.Background(), []string{dir}, Options{Prev: res.Root, Modes: map[string]Mode{dir: ModeDiff}, Dirty: dirty, Touched: touched}, nil) if err != nil { t.Fatal(err) } r3 := Find(res3.Root, dir) if r3.Files != 5 || r3.Size != want+(400<<10) || res3.Read != 1 { t.Fatalf("diff refresh: files %d size %d want %d read %d", r3.Files, r3.Size, want+(400<<10), res3.Read) } // a cancelled walk marks what it did not read; resuming reads those ctx, cancel := context.WithCancel(context.Background()) cancel() res4, err := Walk(ctx, []string{dir}, Options{}, nil) if err == nil || res4.Complete { t.Fatal("cancelled walk should say so") } if len(partialPaths(res4.Root, "/")) == 0 { t.Fatal("cancelled walk should leave partial directories") } res5, err := Walk(context.Background(), []string{dir}, Options{Prev: res4.Root, Modes: map[string]Mode{dir: ModeMTime}}, nil) if err != nil { t.Fatal(err) } if r5 := Find(res5.Root, dir); r5.Files != 5 || len(partialPaths(res5.Root, "/")) != 0 { t.Fatalf("resume: files %d partial %v", r5.Files, partialPaths(res5.Root, "/")) } // the codec round-trips the partial flag pr, pw, _ := os.Pipe() go func() { _ = Encode(pw, res4.Root); pw.Close() }() back, err := Decode(pr) if err != nil || len(partialPaths(back, "/")) != len(partialPaths(res4.Root, "/")) { t.Fatalf("codec: %v", err) } }