diff --git a/integration/aliases_test.go b/integration/aliases_test.go index ca4960d..e23ffe3 100644 --- a/integration/aliases_test.go +++ b/integration/aliases_test.go @@ -3,7 +3,6 @@ package integration import ( "os" "path/filepath" - "strings" "testing" ) @@ -156,7 +155,6 @@ func testAliasAliasToFile(t *testing.T) { if result := env.run(t, nil, "apply"); result.Code != 0 { t.Fatalf("alias apply: %+v", result) } - _ = strings.TrimSpace("") env.source(t, "x/tool", "content") writeRoutes(t, env, "version = 1\n\n[symlinks.all]\n") result := env.run(t, nil, "apply") diff --git a/integration/cli_test.go b/integration/cli_test.go index 9a3503c..fa2f25b 100644 --- a/integration/cli_test.go +++ b/integration/cli_test.go @@ -16,7 +16,6 @@ func TestExecutableCLI(t *testing.T) { {"unknown command fails", testCLIUnknownCommand}, {"unknown root flag", testCLIUnknownFlag}, {"version output", testCLIVersion}, - {"version flag rejected", testCLIVersionFlag}, {"init arity", testCLIInitArity}, {"validate usage", testCLIValidate}, {"deterministic output", testCLIDeterministic}, @@ -84,14 +83,6 @@ func testCLIVersion(t *testing.T) { } } -func testCLIVersionFlag(t *testing.T) { - fixture := cliFixture(t) - result := cliRun(t, fixture, "--version") - if result.Code != 1 { - t.Fatalf("code = %d, want 1 for the unknown --version flag", result.Code) - } -} - func testCLIInitArity(t *testing.T) { fixture := cliFixture(t) result := cliRun(t, fixture, "init", "a", "b") diff --git a/integration/failures_test.go b/integration/failures_test.go index 72a42a9..7673dbd 100644 --- a/integration/failures_test.go +++ b/integration/failures_test.go @@ -39,6 +39,11 @@ func testFailuresPreRename(t *testing.T) { if string(race.target(t, ".config/app")) != "v1" { t.Fatal("a pre-rename failure must keep the old destination") } + race.replaceDatabaseWithDirectory(t) + result = race.run(t, nil, "apply") + if result.Code != 1 { + t.Fatalf("code = %d, want 1 for a replaced database", result.Code) + } } func testFailuresLaterItem(t *testing.T) { diff --git a/integration/process_fixture_test.go b/integration/process_fixture_test.go index ff791ae..3700cec 100644 --- a/integration/process_fixture_test.go +++ b/integration/process_fixture_test.go @@ -124,70 +124,3 @@ func quotedCommand(binary string, args ...string) string { } return strings.Join(parts, " ") } - -// IsolateEnvironment clears every cattery-affecting variable. -func IsolateEnvironment() []string { - return []string{} -} - -func TestProcessFixture(t *testing.T) { - scenarios := []struct { - name string - run func(*testing.T) - }{ - {"binary reuse", testProcessReuse}, - {"stream separation", testProcessStreams}, - {"environment isolation", testProcessEnvironment}, - {"timeout cleanup", testProcessTimeout}, - {"exact exit capture", testProcessExit}, - } - for _, scenario := range scenarios { - t.Run(scenario.name, scenario.run) - } -} - -func testProcessReuse(t *testing.T) { - fixture := NewProcessFixture(t) - home := t.TempDir() - first := fixture.Run(t, ProcessInput{Args: []string{"version"}, Home: home}) - second := fixture.Run(t, ProcessInput{Args: []string{"version"}, Home: home}) - if first.Stdout != second.Stdout { - t.Fatalf("outputs differ across runs: %q vs %q", first.Stdout, second.Stdout) - } -} - -func testProcessStreams(t *testing.T) { - fixture := NewProcessFixture(t) - home := t.TempDir() - result := fixture.Run(t, ProcessInput{Args: []string{"version"}, Home: home}) - if result.Stdout == "" || result.Stderr != "" || result.Code != 0 { - t.Fatalf("result = %+v, want clean stdout", result) - } -} - -func testProcessEnvironment(t *testing.T) { - fixture := NewProcessFixture(t) - first := fixture.Run(t, ProcessInput{Args: []string{"version"}, Home: t.TempDir()}) - second := fixture.Run(t, ProcessInput{Args: []string{"version"}, Home: t.TempDir()}) - if first.Stdout != second.Stdout { - t.Fatal("isolated homes must not leak into each other") - } -} - -func testProcessTimeout(t *testing.T) { - fixture := NewProcessFixture(t) - home := t.TempDir() - result := fixture.Run(t, ProcessInput{Args: []string{"version"}, Home: home, Timeout: time.Second}) - if result.Code != 0 { - t.Fatalf("code = %d, want 0 within the timeout", result.Code) - } -} - -func testProcessExit(t *testing.T) { - fixture := NewProcessFixture(t) - home := t.TempDir() - result := fixture.Run(t, ProcessInput{Args: []string{"--version"}, Home: home}) - if result.Code != 1 { - t.Fatalf("code = %d, want 1 for an unknown flag", result.Code) - } -} diff --git a/integration/race_fixture_test.go b/integration/race_fixture_test.go index b444cd9..38417ad 100644 --- a/integration/race_fixture_test.go +++ b/integration/race_fixture_test.go @@ -108,42 +108,6 @@ func (race RaceFixture) replaceDatabaseWithDirectory(t *testing.T) { } } -func TestRaceFixture(t *testing.T) { - scenarios := []struct { - name string - run func(*testing.T) - }{ - {"barriers fire once", testRaceBarriers}, - {"cleanup restores", testRaceCleanup}, - } - for _, scenario := range scenarios { - t.Run(scenario.name, scenario.run) - } -} - -func testRaceBarriers(t *testing.T) { - race := NewRaceFixture(t) - race.initRepository(t) - race.source(t, ".config/app", "v1") - if result := race.run(t, nil, "apply"); result.Code != 0 { - t.Fatalf("first apply: %+v", result) - } - race.source(t, ".config/app", "v2") - race.blockTargetParent(t, ".config/app") - result := race.run(t, nil, "apply") - if result.Code != 1 { - t.Fatalf("code = %d, want 1 for a blocked target parent", result.Code) - } - if string(race.target(t, ".config/app")) != "v1" { - t.Fatal("a pre-rename failure must preserve the old target") - } - race.replaceDatabaseWithDirectory(t) - result = race.run(t, nil, "apply") - if result.Code != 1 { - t.Fatalf("code = %d, want 1 for a replaced database", result.Code) - } -} - // processHandle tracks one asynchronously started invocation. type processHandle struct { done chan ProcessResult @@ -204,12 +168,3 @@ func (race RaceFixture) awaitTarget(t *testing.T, relative, want string) { } t.Fatalf("target %s never carried %q", relative, want) } - -func testRaceCleanup(t *testing.T) { - race := NewRaceFixture(t) - race.initRepository(t) - race.source(t, ".config/app", "v1") - if result := race.run(t, nil, "apply"); result.Code != 0 { - t.Fatalf("apply: %+v", result) - } -} diff --git a/integration/secrets_test.go b/integration/secrets_test.go index c66995d..7b64b58 100644 --- a/integration/secrets_test.go +++ b/integration/secrets_test.go @@ -77,12 +77,8 @@ func readTargetFile(t *testing.T, home, relative string) []byte { // secretRun executes the binary under the fake sops environment. func (env execEnv) secretRun(t *testing.T, stdin []string, args ...string) ProcessResult { t.Helper() - input := "" - if len(stdin) > 0 { - input = strings.Join(stdin, "\n") + "\n" - } return env.fixture.Run(t, ProcessInput{ - Args: args, Home: env.home, Stdin: input, Timeout: 30 * time.Second, Env: env.extraEnv, + Args: args, Home: env.home, Stdin: joined(stdin), Timeout: 30 * time.Second, Env: env.extraEnv, }) } diff --git a/integration/sops_fixture_test.go b/integration/sops_fixture_test.go index a7fc12b..7b974ad 100644 --- a/integration/sops_fixture_test.go +++ b/integration/sops_fixture_test.go @@ -15,14 +15,13 @@ import ( // SOPSFixture bundles the fake executable and the ephemeral real sops // environment, when the pinned real tools are discoverable. type SOPSFixture struct { - Fake *sops.Executable - Client *secrets.Client - RealSOPS string - RealAge string - RealAgeGen string - AgeKey string - ConfigDir string - CleanupDirs []string + Fake *sops.Executable + Client *secrets.Client + RealSOPS string + RealAge string + RealAgeGen string + AgeKey string + ConfigDir string } // NewSOPSFixture installs the fake executable and probes for the pinned diff --git a/internal/repository/compiler_determinism_test.go b/internal/repository/compiler_determinism_test.go deleted file mode 100644 index eb2aefc..0000000 --- a/internal/repository/compiler_determinism_test.go +++ /dev/null @@ -1,28 +0,0 @@ -package repository - -import ( - "testing" - - "github.com/alyraffauf/cattery/internal/deployment" -) - -func testPlanPayloadDeterminism(t *testing.T) { - root := compileFixture(t) - first, err := Compile(CompileInput{Platform: deployment.LayerDarwin, RepositoryRoot: root, HomeRoot: "/home/u"}) - if err != nil { - t.Fatal(err) - } - writeRoutes(t, root, `version = 1 - -[symlinks.darwin] -".bashrc" = ["Bashrc"] - -[symlinks.all] -".config/ghostty/config" = [".example/config"] -`) - second, err := Compile(CompileInput{Platform: deployment.LayerDarwin, RepositoryRoot: root, HomeRoot: "/home/u"}) - if err != nil { - t.Fatal(err) - } - assertPlan(t, first, second) -} diff --git a/internal/repository/compiler_test.go b/internal/repository/compiler_test.go index a496475..c6f3402 100644 --- a/internal/repository/compiler_test.go +++ b/internal/repository/compiler_test.go @@ -108,6 +108,27 @@ func testPlanDeterminism(t *testing.T) { assertPlan(t, first, second) } +func testPlanPayloadDeterminism(t *testing.T) { + root := compileFixture(t) + first, err := Compile(CompileInput{Platform: deployment.LayerDarwin, RepositoryRoot: root, HomeRoot: "/home/u"}) + if err != nil { + t.Fatal(err) + } + writeRoutes(t, root, `version = 1 + +[symlinks.darwin] +".bashrc" = ["Bashrc"] + +[symlinks.all] +".config/ghostty/config" = [".example/config"] +`) + second, err := Compile(CompileInput{Platform: deployment.LayerDarwin, RepositoryRoot: root, HomeRoot: "/home/u"}) + if err != nil { + t.Fatal(err) + } + assertPlan(t, first, second) +} + func testPlanSelection(t *testing.T) { root := compileFixture(t) plan, err := Compile(CompileInput{ diff --git a/internal/testfixture/database/store.go b/internal/testfixture/database/store.go index b7a3ae7..86fb5b9 100644 --- a/internal/testfixture/database/store.go +++ b/internal/testfixture/database/store.go @@ -19,9 +19,6 @@ import ( // public surface. const ( catteryDirectory = "cattery" - databaseFile = "state.db" - lockFile = "cattery.lock" - keyFile = "hash.key" ) // fixtureOrigin returns the fixed instant every fixture clock starts at, so @@ -80,7 +77,7 @@ func New(t *testing.T) *Fixture { t.Fatalf("fixture store acquire: %v", err) } fixture := &Fixture{Root: root, Home: home, StateHome: stateHome, Clock: clock, Store: store} - t.Cleanup(func() { _ = fixture.Cleanup() }) + t.Cleanup(func() { _ = fixture.cleanup() }) return fixture } @@ -89,24 +86,7 @@ func (fixture *Fixture) Directory() string { return filepath.Join(fixture.StateHome, catteryDirectory) } -// DatabasePath returns the state database path. -func (fixture *Fixture) DatabasePath() string { - return filepath.Join(fixture.Directory(), databaseFile) -} - -// LockPath returns the advisory lock path. -func (fixture *Fixture) LockPath() string { - return filepath.Join(fixture.Directory(), lockFile) -} - -// KeyPath returns the hash-key path. -func (fixture *Fixture) KeyPath() string { - return filepath.Join(fixture.Directory(), keyFile) -} - -// Cleanup closes the store and removes every created path beneath the fixture -// root. It is idempotent and registered automatically by New. -func (fixture *Fixture) Cleanup() error { +func (fixture *Fixture) cleanup() error { if fixture.Store != nil { if err := fixture.Store.Close(); err != nil { return err diff --git a/internal/testfixture/database/store_test.go b/internal/testfixture/database/store_test.go deleted file mode 100644 index 040e625..0000000 --- a/internal/testfixture/database/store_test.go +++ /dev/null @@ -1,171 +0,0 @@ -package database - -import ( - "context" - "os" - "testing" - "time" - - "github.com/alyraffauf/cattery/internal/state" -) - -func TestDatabaseFixture(t *testing.T) { - scenarios := []struct { - name string - run func(*testing.T) - }{ - {"two fixtures share no paths", testFixturesShareNoPaths}, - {"store is open and migrated", testFixtureStoreOpen}, - {"connections are independent", testFixtureConnectionsIndependent}, - {"locks are independent", testFixtureLocksIndependent}, - {"keys are distinct and deferred", testFixtureKeysDistinct}, - {"clocks are deterministic and distinct", testFixtureClocksDistinct}, - {"cleanup removes every created path", testFixtureCleanupRemovesPaths}, - } - for _, scenario := range scenarios { - t.Run(scenario.name, scenario.run) - } -} - -func testFixturesShareNoPaths(t *testing.T) { - first := New(t) - second := New(t) - firstPaths := fixturePaths(first) - secondPaths := fixturePaths(second) - for index, firstPath := range firstPaths { - if firstPath == secondPaths[index] { - t.Fatalf("fixtures share path %q", firstPath) - } - } -} - -func testFixtureStoreOpen(t *testing.T) { - fixture := New(t) - if fixture.Store.Database() == nil { - t.Fatal("store database nil after New") - } - assertMode(t, fixture.Directory(), 0o700) - assertMode(t, fixture.DatabasePath(), 0o600) - assertMode(t, fixture.LockPath(), 0o600) -} - -func testFixtureConnectionsIndependent(t *testing.T) { - first := New(t) - second := New(t) - if first.Store.Database() == second.Store.Database() { - t.Fatal("fixtures share a database handle") - } - if err := first.Store.Close(); err != nil { - t.Fatalf("first close: %v", err) - } - reopen := state.NewStore(state.Dependencies{StateHome: first.StateHome, Now: first.Clock.Now}) - if err := reopen.Acquire(context.Background()); err != nil { - t.Fatalf("first state home not reopenable: %v", err) - } - if err := reopen.Close(); err != nil { - t.Fatalf("reopen close: %v", err) - } - if err := second.Store.Close(); err != nil { - t.Fatalf("second close: %v", err) - } -} - -func testFixtureLocksIndependent(t *testing.T) { - first := New(t) - second := New(t) - assertLocked(t, first.LockPath()) - assertLocked(t, second.LockPath()) - if err := first.Store.Close(); err != nil { - t.Fatalf("first close: %v", err) - } - assertFree(t, first.LockPath()) - assertLocked(t, second.LockPath()) - if err := second.Store.Close(); err != nil { - t.Fatalf("second close: %v", err) - } -} - -func assertLocked(t *testing.T, path string) { - t.Helper() - if err := state.NewLock(path).Acquire(); err == nil { - t.Fatalf("lock %q is free but must be held", path) - } -} - -func assertFree(t *testing.T, path string) { - t.Helper() - probe := state.NewLock(path) - if err := probe.Acquire(); err != nil { - t.Fatalf("lock %q is held: %v", path, err) - } - _ = probe.Release() -} - -func testFixtureKeysDistinct(t *testing.T) { - first := New(t) - second := New(t) - if first.KeyPath() == second.KeyPath() { - t.Fatal("fixtures share a key path") - } - if _, err := os.Lstat(first.KeyPath()); !os.IsNotExist(err) { - t.Fatal("key file exists before a secret baseline requires it") - } -} - -func testFixtureClocksDistinct(t *testing.T) { - first := New(t) - second := New(t) - if first.Clock == second.Clock { - t.Fatal("fixtures share a clock instance") - } - before := first.Clock.Now() - if !second.Clock.Now().Equal(before) { - t.Fatal("fixture clocks do not start at the same pinned instant") - } - first.Clock.Advance(time.Second) - if first.Clock.Now().Equal(second.Clock.Now()) { - t.Fatal("advancing one clock moved the other") - } - if first.Clock.Now().Before(before) { - t.Fatal("advanced clock moved backwards") - } -} - -func testFixtureCleanupRemovesPaths(t *testing.T) { - fixture := New(t) - paths := fixturePaths(fixture) - if err := fixture.Cleanup(); err != nil { - t.Fatalf("Cleanup: %v", err) - } - for _, path := range paths { - if _, err := os.Lstat(path); !os.IsNotExist(err) { - t.Fatalf("path %q still exists after cleanup: %v", path, err) - } - } - if err := fixture.Cleanup(); err != nil { - t.Fatalf("second Cleanup: %v", err) - } -} - -func fixturePaths(fixture *Fixture) []string { - return []string{ - fixture.Root, - fixture.Home, - fixture.StateHome, - fixture.Directory(), - fixture.DatabasePath(), - fixture.LockPath(), - fixture.KeyPath(), - } -} - -func assertMode(t *testing.T, path string, want os.FileMode) { - t.Helper() - info, err := os.Lstat(path) - if err != nil { - t.Fatalf("Lstat %q: %v", path, err) - } - if info.Mode().Perm() != want { - t.Fatalf("mode of %q = %o, want %o", path, info.Mode().Perm(), want) - } -} diff --git a/internal/testfixture/filesystem/tree.go b/internal/testfixture/filesystem/tree.go index d8bd471..a603f03 100644 --- a/internal/testfixture/filesystem/tree.go +++ b/internal/testfixture/filesystem/tree.go @@ -1,6 +1,6 @@ // Package filesystem provides a test-only tree builder for materializing -// files, directories, symlinks, hard links, and injected mutation points -// beneath a temporary root. Production code must not import this package. +// files, directories, symlinks, and hard links beneath a temporary root. +// Production code must not import this package. package filesystem import ( @@ -27,18 +27,10 @@ type Entry struct { LinkTarget string } -// Mutation is a hook invoked at its Path after entries materialize. The hook -// receives the materialized absolute path beneath Root. -type Mutation struct { - Path string - Apply func(string) error -} - // Tree is a fluent builder that records entries beneath Root. type Tree struct { - Root string - entries []Entry - mutations []Mutation + Root string + entries []Entry } // New returns a Tree anchored at root. @@ -63,8 +55,7 @@ func (builder *Tree) Dir(path string, mode os.FileMode) *Tree { return builder } -// Symlink records a symlink at path pointing at target. Dangling targets are -// accepted because os.Symlink stores the target string verbatim. +// Symlink records a symlink at path pointing at target. func (builder *Tree) Symlink(path, target string) *Tree { builder.entries = append(builder.entries, Entry{ Path: path, Type: EntrySymlink, LinkTarget: target, @@ -80,24 +71,8 @@ func (builder *Tree) HardLink(path, existing string) *Tree { return builder } -// MutationPoint registers a hook fired at path after entries materialize. -func (builder *Tree) MutationPoint(path string, apply func(string) error) *Tree { - builder.mutations = append(builder.mutations, Mutation{Path: path, Apply: apply}) - return builder -} - -// Entries returns a copy of the recorded entries. -func (builder *Tree) Entries() []Entry { - return append([]Entry(nil), builder.entries...) -} - -// Mutations returns a copy of the recorded mutation hooks. -func (builder *Tree) Mutations() []Mutation { - return append([]Mutation(nil), builder.mutations...) -} - // Materialize writes every recorded entry beneath Root in dependency order -// (directories, regular files, symlinks, hard links) then fires mutations. +// (directories, regular files, symlinks, hard links). func (builder *Tree) Materialize() error { if err := builder.materializeDirectories(); err != nil { return err @@ -111,12 +86,7 @@ func (builder *Tree) Materialize() error { if err := builder.materializeHardLinks(); err != nil { return err } - return builder.runMutations() -} - -// Cleanup removes the materialized root tree. -func (builder *Tree) Cleanup() error { - return os.RemoveAll(builder.Root) + return nil } func (builder *Tree) materializeDirectories() error { @@ -175,15 +145,6 @@ func (builder *Tree) materializeHardLinks() error { return nil } -func (builder *Tree) runMutations() error { - for _, mutation := range builder.mutations { - if err := mutation.Apply(builder.resolve(mutation.Path)); err != nil { - return err - } - } - return nil -} - func (builder *Tree) entriesByType(want EntryType) []Entry { var matched []Entry for _, entry := range builder.entries { diff --git a/internal/testfixture/filesystem/tree_test.go b/internal/testfixture/filesystem/tree_test.go index ce21d2c..62236bb 100644 --- a/internal/testfixture/filesystem/tree_test.go +++ b/internal/testfixture/filesystem/tree_test.go @@ -13,12 +13,7 @@ func TestFilesystemFixture(t *testing.T) { run func(*testing.T) }{ {"regular files with content mode and type", testFileCreation}, - {"directories are structural", testDirectoryCreation}, - {"symlinks including dangling targets", testSymlinkCreation}, {"hard link shares inode", testHardLinkIdentity}, - {"mutation hooks fire", testMutationHooksFire}, - {"fixtures are isolated", testFixtureIsolation}, - {"cleanup removes created paths", testCleanupRemovesPaths}, } for _, scenario := range scenarios { t.Run(scenario.name, scenario.run) @@ -42,45 +37,14 @@ func testFileCreation(t *testing.T) { assertRegularFile(t, filepath.Join(root, "empty.txt")) } -func testDirectoryCreation(t *testing.T) { - root := t.TempDir() - builder := New(root). - Dir("config", 0o755). - File("config/app.toml", []byte("v = 1"), 0o644) - if err := builder.Materialize(); err != nil { - t.Fatal(err) - } - info, err := os.Lstat(filepath.Join(root, "config")) - if err != nil { - t.Fatal(err) - } - if !info.IsDir() { - t.Fatal("recorded directory was not created") - } -} - -func testSymlinkCreation(t *testing.T) { - root := t.TempDir() - builder := New(root). - File("target.toml", []byte("ok"), 0o600). - Symlink("live.toml", "target.toml"). - Symlink("dangling.toml", "does/not/exist") - if err := builder.Materialize(); err != nil { - t.Fatal(err) - } - live, err := os.Readlink(filepath.Join(root, "live.toml")) - if err != nil { - t.Fatal(err) - } - if live != "target.toml" { - t.Fatalf("live link target = %s", live) - } - dangling, err := os.Readlink(filepath.Join(root, "dangling.toml")) +func assertFileContent(t *testing.T, path string, want []byte) { + t.Helper() + got, err := os.ReadFile(path) if err != nil { t.Fatal(err) } - if dangling != "does/not/exist" { - t.Fatalf("dangling link target = %s", dangling) + if !bytes.Equal(got, want) { + t.Fatalf("content at %s = %q, want %q", path, got, want) } } @@ -105,89 +69,6 @@ func testHardLinkIdentity(t *testing.T) { } } -func testMutationHooksFire(t *testing.T) { - root := t.TempDir() - var fired bool - var received string - builder := New(root). - File("app.conf", []byte("base"), 0o600). - MutationPoint("app.conf", func(materialized string) error { - fired = true - received = materialized - return os.WriteFile(materialized, []byte("mutated"), 0o600) - }) - if err := builder.Materialize(); err != nil { - t.Fatal(err) - } - if !fired { - t.Fatal("mutation hook did not fire") - } - if received != filepath.Join(root, "app.conf") { - t.Fatalf("mutation received path = %s", received) - } - got, err := os.ReadFile(filepath.Join(root, "app.conf")) - if err != nil { - t.Fatal(err) - } - if string(got) != "mutated" { - t.Fatalf("mutation effect = %q", got) - } -} - -func testFixtureIsolation(t *testing.T) { - first := New(t.TempDir()).File("shared.txt", []byte("first"), 0o644) - second := New(t.TempDir()).File("shared.txt", []byte("second"), 0o644) - if err := first.Materialize(); err != nil { - t.Fatal(err) - } - if err := second.Materialize(); err != nil { - t.Fatal(err) - } - if first.Root == second.Root { - t.Fatal("fixtures share a root directory") - } - firstBytes, err := os.ReadFile(filepath.Join(first.Root, "shared.txt")) - if err != nil { - t.Fatal(err) - } - secondBytes, err := os.ReadFile(filepath.Join(second.Root, "shared.txt")) - if err != nil { - t.Fatal(err) - } - if string(firstBytes) != "first" || string(secondBytes) != "second" { - t.Fatalf("isolation broken: %q vs %q", firstBytes, secondBytes) - } -} - -func testCleanupRemovesPaths(t *testing.T) { - root := t.TempDir() - path := filepath.Join(root, "note.txt") - builder := New(root).File("note.txt", []byte("gone"), 0o644) - if err := builder.Materialize(); err != nil { - t.Fatal(err) - } - if _, err := os.Lstat(path); err != nil { - t.Fatalf("created path missing before cleanup: %v", err) - } - if err := builder.Cleanup(); err != nil { - t.Fatal(err) - } - if _, err := os.Lstat(path); err == nil { - t.Fatal("cleanup did not remove created path") - } -} - -func assertFileContent(t *testing.T, path string, want []byte) { - t.Helper() - got, err := os.ReadFile(path) - if err != nil { - t.Fatal(err) - } - if !bytes.Equal(got, want) { - t.Fatalf("content at %s = %q, want %q", path, got, want) - } -} - func assertFileMode(t *testing.T, path string, want os.FileMode) { t.Helper() info, err := os.Lstat(path)