|
@@ -0,0 +1,439 @@
|
|
|
+package cli_test
|
|
|
+
|
|
|
+import (
|
|
|
+ "bytes"
|
|
|
+ "os"
|
|
|
+ "path/filepath"
|
|
|
+ "sort"
|
|
|
+ "strings"
|
|
|
+ "testing"
|
|
|
+
|
|
|
+ "github.com/stretchr/testify/require"
|
|
|
+
|
|
|
+ "github.com/kopia/kopia/internal/testutil"
|
|
|
+ "github.com/kopia/kopia/repo/content"
|
|
|
+ "github.com/kopia/kopia/repo/content/index"
|
|
|
+ "github.com/kopia/kopia/repo/object"
|
|
|
+ "github.com/kopia/kopia/snapshot"
|
|
|
+ "github.com/kopia/kopia/snapshot/snapshotfs"
|
|
|
+ "github.com/kopia/kopia/tests/testenv"
|
|
|
+)
|
|
|
+
|
|
|
+// nolint:maintidx
|
|
|
+func TestSnapshotFix(t *testing.T) {
|
|
|
+ srcDir1 := testutil.TempDirectory(t)
|
|
|
+
|
|
|
+ if testutil.ShouldReduceTestComplexity() {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // 300 bytes
|
|
|
+ mustWriteFileWithRepeatedData(t, filepath.Join(srcDir1, "small-file1"), 1, bytes.Repeat([]byte{1, 2, 3}, 100))
|
|
|
+ mustWriteFileWithRepeatedData(t, filepath.Join(srcDir1, "small-file1-dup"), 1, bytes.Repeat([]byte{1, 2, 3}, 100))
|
|
|
+ mustWriteFileWithRepeatedData(t, filepath.Join(srcDir1, "small-file2"), 1, bytes.Repeat([]byte{1, 2, 4}, 100))
|
|
|
+
|
|
|
+ require.NoError(t, os.MkdirAll(filepath.Join(srcDir1, "dir1"), 0o700))
|
|
|
+ require.NoError(t, os.MkdirAll(filepath.Join(srcDir1, "dir2"), 0o700))
|
|
|
+
|
|
|
+ // 3 x 3 x 1_000_000 bytes = 9 MB
|
|
|
+ mustWriteFileWithRepeatedData(t, filepath.Join(srcDir1, "large-file1"), 3, bytes.Repeat([]byte{1, 2, 3}, 1000000))
|
|
|
+ mustWriteFileWithRepeatedData(t, filepath.Join(srcDir1, "large-file1-dup"), 3, bytes.Repeat([]byte{1, 2, 3}, 1000000))
|
|
|
+ mustWriteFileWithRepeatedData(t, filepath.Join(srcDir1, "large-file2"), 3, bytes.Repeat([]byte{1, 2, 4}, 1000000))
|
|
|
+
|
|
|
+ mustWriteFileWithRepeatedData(t, filepath.Join(srcDir1, "dir1", "small-file1"), 1, bytes.Repeat([]byte{1, 1, 2, 3}, 100))
|
|
|
+ mustWriteFileWithRepeatedData(t, filepath.Join(srcDir1, "dir1", "small-file1-dup"), 1, bytes.Repeat([]byte{1, 1, 2, 3}, 100))
|
|
|
+ mustWriteFileWithRepeatedData(t, filepath.Join(srcDir1, "dir1", "small-file2"), 1, bytes.Repeat([]byte{1, 1, 2, 4}, 100))
|
|
|
+ mustWriteFileWithRepeatedData(t, filepath.Join(srcDir1, "dir1", "large-file1"), 3, bytes.Repeat([]byte{1, 1, 2, 3}, 1000000))
|
|
|
+ mustWriteFileWithRepeatedData(t, filepath.Join(srcDir1, "dir1", "large-file1-dup"), 3, bytes.Repeat([]byte{1, 1, 2, 3}, 1000000))
|
|
|
+ mustWriteFileWithRepeatedData(t, filepath.Join(srcDir1, "dir1", "large-file2"), 3, bytes.Repeat([]byte{1, 1, 2, 4}, 1000000))
|
|
|
+
|
|
|
+ mustWriteFileWithRepeatedData(t, filepath.Join(srcDir1, "dir2", "small-file1"), 1, bytes.Repeat([]byte{2, 1, 2, 3}, 100))
|
|
|
+ mustWriteFileWithRepeatedData(t, filepath.Join(srcDir1, "dir2", "small-file1-dup"), 1, bytes.Repeat([]byte{2, 1, 2, 3}, 100))
|
|
|
+ mustWriteFileWithRepeatedData(t, filepath.Join(srcDir1, "dir2", "small-file2"), 1, bytes.Repeat([]byte{2, 1, 2, 4}, 100))
|
|
|
+ mustWriteFileWithRepeatedData(t, filepath.Join(srcDir1, "dir2", "large-file1"), 3, bytes.Repeat([]byte{2, 1, 2, 3}, 1000000))
|
|
|
+ mustWriteFileWithRepeatedData(t, filepath.Join(srcDir1, "dir2", "large-file1-dup"), 3, bytes.Repeat([]byte{2, 1, 2, 3}, 1000000))
|
|
|
+ mustWriteFileWithRepeatedData(t, filepath.Join(srcDir1, "dir2", "large-file2"), 3, bytes.Repeat([]byte{2, 1, 2, 4}, 1000000))
|
|
|
+
|
|
|
+ cases := []struct {
|
|
|
+ name string
|
|
|
+ flags []string
|
|
|
+ modifyRepoAfterSnapshot func(env *testenv.CLITest, man *snapshot.Manifest, fileMap map[string]*snapshot.DirEntry)
|
|
|
+ initiallyCorrupted bool
|
|
|
+ wantRecoveredFiles []string
|
|
|
+ wantRootStub bool
|
|
|
+ wantFixFail bool
|
|
|
+ wantFailVerify bool
|
|
|
+ }{
|
|
|
+ {
|
|
|
+ name: "FixInvalidFiles_NoOp",
|
|
|
+ modifyRepoAfterSnapshot: func(env *testenv.CLITest, man *snapshot.Manifest, fileMap map[string]*snapshot.DirEntry) {},
|
|
|
+ flags: []string{"invalid-files"},
|
|
|
+ wantRecoveredFiles: []string{
|
|
|
+ "dir1",
|
|
|
+ "dir1/large-file1",
|
|
|
+ "dir1/large-file1-dup",
|
|
|
+ "dir1/large-file2",
|
|
|
+ "dir1/small-file1",
|
|
|
+ "dir1/small-file1-dup",
|
|
|
+ "dir1/small-file2",
|
|
|
+ "dir2",
|
|
|
+ "dir2/large-file1",
|
|
|
+ "dir2/large-file1-dup",
|
|
|
+ "dir2/large-file2",
|
|
|
+ "dir2/small-file1",
|
|
|
+ "dir2/small-file1-dup",
|
|
|
+ "dir2/small-file2",
|
|
|
+ "large-file1",
|
|
|
+ "large-file1-dup",
|
|
|
+ "large-file2",
|
|
|
+ "small-file1",
|
|
|
+ "small-file1-dup",
|
|
|
+ "small-file2",
|
|
|
+ },
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "FixInvalidFiles_MissingRootDirStub",
|
|
|
+ modifyRepoAfterSnapshot: func(env *testenv.CLITest, man *snapshot.Manifest, fileMap map[string]*snapshot.DirEntry) {
|
|
|
+ forgetContents(t, env,
|
|
|
+ string(man.RootObjectID()))
|
|
|
+ },
|
|
|
+ initiallyCorrupted: true,
|
|
|
+ flags: []string{"invalid-files"},
|
|
|
+ wantRootStub: true,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "FixInvalidFiles_MissingRootDirFail",
|
|
|
+ modifyRepoAfterSnapshot: func(env *testenv.CLITest, man *snapshot.Manifest, fileMap map[string]*snapshot.DirEntry) {
|
|
|
+ forgetContents(t, env,
|
|
|
+ string(man.RootObjectID()))
|
|
|
+ },
|
|
|
+ initiallyCorrupted: true,
|
|
|
+ flags: []string{"invalid-files", "--invalid-directory-handling=fail"},
|
|
|
+ wantFixFail: true,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "FixInvalidFiles_MissingRootDirKeep",
|
|
|
+ modifyRepoAfterSnapshot: func(env *testenv.CLITest, man *snapshot.Manifest, fileMap map[string]*snapshot.DirEntry) {
|
|
|
+ forgetContents(t, env,
|
|
|
+ string(man.RootObjectID()))
|
|
|
+ },
|
|
|
+ initiallyCorrupted: true,
|
|
|
+ flags: []string{"invalid-files", "--invalid-directory-handling=keep"},
|
|
|
+ wantFailVerify: true,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "FixInvalidFiles_MissingShortContentFileRemove",
|
|
|
+ modifyRepoAfterSnapshot: func(env *testenv.CLITest, man *snapshot.Manifest, fileMap map[string]*snapshot.DirEntry) {
|
|
|
+ forgetContents(t, env,
|
|
|
+ string(fileMap["small-file1"].ObjectID),
|
|
|
+ string(fileMap["dir1/small-file1"].ObjectID))
|
|
|
+ },
|
|
|
+ initiallyCorrupted: true,
|
|
|
+ // recovered files
|
|
|
+ flags: []string{"invalid-files", "--invalid-file-handling=remove"},
|
|
|
+ wantRecoveredFiles: []string{
|
|
|
+ "dir1",
|
|
|
+ "dir1/large-file1",
|
|
|
+ "dir1/large-file1-dup",
|
|
|
+ "dir1/large-file2",
|
|
|
+ "dir1/small-file2",
|
|
|
+ "dir2",
|
|
|
+ "dir2/large-file1",
|
|
|
+ "dir2/large-file1-dup",
|
|
|
+ "dir2/large-file2",
|
|
|
+ "dir2/small-file1",
|
|
|
+ "dir2/small-file1-dup",
|
|
|
+ "dir2/small-file2",
|
|
|
+ "large-file1",
|
|
|
+ "large-file1-dup",
|
|
|
+ "large-file2",
|
|
|
+ "small-file2",
|
|
|
+ },
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "FixInvalidFiles_MissingShortContentFileStub",
|
|
|
+ modifyRepoAfterSnapshot: func(env *testenv.CLITest, man *snapshot.Manifest, fileMap map[string]*snapshot.DirEntry) {
|
|
|
+ forgetContents(t, env,
|
|
|
+ string(fileMap["small-file1"].ObjectID),
|
|
|
+ string(fileMap["dir1/small-file1"].ObjectID))
|
|
|
+ },
|
|
|
+ initiallyCorrupted: true,
|
|
|
+ flags: []string{"invalid-files"},
|
|
|
+ // recovered files
|
|
|
+ wantRecoveredFiles: []string{
|
|
|
+ ".INVALID.small-file1",
|
|
|
+ ".INVALID.small-file1-dup",
|
|
|
+ "dir1",
|
|
|
+ "dir1/.INVALID.small-file1",
|
|
|
+ "dir1/.INVALID.small-file1-dup",
|
|
|
+ "dir1/large-file1",
|
|
|
+ "dir1/large-file1-dup",
|
|
|
+ "dir1/large-file2",
|
|
|
+ "dir1/small-file2",
|
|
|
+ "dir2",
|
|
|
+ "dir2/large-file1",
|
|
|
+ "dir2/large-file1-dup",
|
|
|
+ "dir2/large-file2",
|
|
|
+ "dir2/small-file1",
|
|
|
+ "dir2/small-file1-dup",
|
|
|
+ "dir2/small-file2",
|
|
|
+ "large-file1",
|
|
|
+ "large-file1-dup",
|
|
|
+ "large-file2",
|
|
|
+ "small-file2",
|
|
|
+ },
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "FixInvalidFiles_MissingShortContentFileKeep",
|
|
|
+ modifyRepoAfterSnapshot: func(env *testenv.CLITest, man *snapshot.Manifest, fileMap map[string]*snapshot.DirEntry) {
|
|
|
+ forgetContents(t, env,
|
|
|
+ string(fileMap["small-file1"].ObjectID),
|
|
|
+ string(fileMap["dir1/small-file1"].ObjectID))
|
|
|
+ },
|
|
|
+ initiallyCorrupted: true,
|
|
|
+ flags: []string{"invalid-files", "--invalid-file-handling=keep"},
|
|
|
+ wantFailVerify: true,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "FixInvalidFiles_MissingShortContentDir",
|
|
|
+ modifyRepoAfterSnapshot: func(env *testenv.CLITest, man *snapshot.Manifest, fileMap map[string]*snapshot.DirEntry) {
|
|
|
+ forgetContents(t, env,
|
|
|
+ string(fileMap["dir1"].ObjectID))
|
|
|
+ },
|
|
|
+ initiallyCorrupted: true,
|
|
|
+ flags: []string{"invalid-files", "--invalid-directory-handling=stub"},
|
|
|
+ wantRecoveredFiles: []string{
|
|
|
+ ".INVALID.dir1",
|
|
|
+ "dir2",
|
|
|
+ "dir2/large-file1",
|
|
|
+ "dir2/large-file1-dup",
|
|
|
+ "dir2/large-file2",
|
|
|
+ "dir2/small-file1",
|
|
|
+ "dir2/small-file1-dup",
|
|
|
+ "dir2/small-file2",
|
|
|
+ "large-file1",
|
|
|
+ "large-file1-dup",
|
|
|
+ "large-file2",
|
|
|
+ "small-file1",
|
|
|
+ "small-file1-dup",
|
|
|
+ "small-file2",
|
|
|
+ },
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "FixInvalidFiles_MissingLargeFileIndex",
|
|
|
+ modifyRepoAfterSnapshot: func(env *testenv.CLITest, man *snapshot.Manifest, fileMap map[string]*snapshot.DirEntry) {
|
|
|
+ forgetContents(t, env,
|
|
|
+ strings.TrimPrefix(string(fileMap["large-file1"].ObjectID), "I"),
|
|
|
+ strings.TrimPrefix(string(fileMap["dir1/large-file1"].ObjectID), "I"))
|
|
|
+ },
|
|
|
+ initiallyCorrupted: true,
|
|
|
+ flags: []string{"invalid-files", "--invalid-file-handling=remove"},
|
|
|
+ wantRecoveredFiles: []string{
|
|
|
+ "dir1",
|
|
|
+ "dir1/large-file2",
|
|
|
+ "dir1/small-file1",
|
|
|
+ "dir1/small-file1-dup",
|
|
|
+ "dir1/small-file2",
|
|
|
+ "dir2",
|
|
|
+ "dir2/large-file1",
|
|
|
+ "dir2/large-file1-dup",
|
|
|
+ "dir2/large-file2",
|
|
|
+ "dir2/small-file1",
|
|
|
+ "dir2/small-file1-dup",
|
|
|
+ "dir2/small-file2",
|
|
|
+ "large-file2",
|
|
|
+ "small-file1",
|
|
|
+ "small-file1-dup",
|
|
|
+ "small-file2",
|
|
|
+ },
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "FixRemoveFiles_ByFileName",
|
|
|
+ modifyRepoAfterSnapshot: func(env *testenv.CLITest, man *snapshot.Manifest, fileMap map[string]*snapshot.DirEntry) {},
|
|
|
+ flags: []string{"remove-files", "--filename=small-file2", "--filename=large-file1-dup"},
|
|
|
+ wantRecoveredFiles: []string{
|
|
|
+ "dir1",
|
|
|
+ "dir1/large-file1",
|
|
|
+ "dir1/large-file2",
|
|
|
+ "dir1/small-file1",
|
|
|
+ "dir1/small-file1-dup",
|
|
|
+ "dir2",
|
|
|
+ "dir2/large-file1",
|
|
|
+ "dir2/large-file2",
|
|
|
+ "dir2/small-file1",
|
|
|
+ "dir2/small-file1-dup",
|
|
|
+ "large-file1",
|
|
|
+ "large-file2",
|
|
|
+ "small-file1",
|
|
|
+ "small-file1-dup",
|
|
|
+ },
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "FixRemoveFiles_ByWildcard",
|
|
|
+ modifyRepoAfterSnapshot: func(env *testenv.CLITest, man *snapshot.Manifest, fileMap map[string]*snapshot.DirEntry) {},
|
|
|
+ flags: []string{"remove-files", "--filename=small-*", "--filename=*-dup"},
|
|
|
+ wantRecoveredFiles: []string{
|
|
|
+ "dir1",
|
|
|
+ "dir1/large-file1",
|
|
|
+ "dir1/large-file2",
|
|
|
+ "dir2",
|
|
|
+ "dir2/large-file1",
|
|
|
+ "dir2/large-file2",
|
|
|
+ "large-file1",
|
|
|
+ "large-file2",
|
|
|
+ },
|
|
|
+ },
|
|
|
+ }
|
|
|
+
|
|
|
+ for _, tc := range cases {
|
|
|
+ tc := tc
|
|
|
+
|
|
|
+ t.Run(tc.name, func(t *testing.T) {
|
|
|
+ runner := testenv.NewInProcRunner(t)
|
|
|
+ env := testenv.NewCLITest(t, testenv.RepoFormatNotImportant, runner)
|
|
|
+
|
|
|
+ env.RunAndExpectSuccess(t, "repo", "create", "filesystem", "--path", env.RepoDir)
|
|
|
+
|
|
|
+ var man1, man2 snapshot.Manifest
|
|
|
+
|
|
|
+ testutil.MustParseJSONLines(t, env.RunAndExpectSuccess(t, "snapshot", "create", srcDir1, "--json"), &man1)
|
|
|
+ testutil.MustParseJSONLines(t, env.RunAndExpectSuccess(t, "snapshot", "create", srcDir1, "--json"), &man2)
|
|
|
+
|
|
|
+ fileMap := mustGetFileMap(t, env, man1.RootObjectID())
|
|
|
+
|
|
|
+ tc.modifyRepoAfterSnapshot(env, &man1, fileMap)
|
|
|
+
|
|
|
+ if tc.initiallyCorrupted {
|
|
|
+ env.RunAndExpectFailure(t, "snapshot", "verify")
|
|
|
+ } else {
|
|
|
+ env.RunAndExpectFailure(t, "snapshot", "success")
|
|
|
+ }
|
|
|
+
|
|
|
+ if tc.wantFixFail {
|
|
|
+ env.RunAndExpectFailure(t, append([]string{"snapshot", "fix"}, tc.flags...)...)
|
|
|
+ env.RunAndExpectFailure(t, append(append([]string{"snapshot", "fix"}, tc.flags...), "--commit")...)
|
|
|
+ env.RunAndExpectFailure(t, "snapshot", "verify")
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // this does not commit fixes
|
|
|
+ env.RunAndExpectSuccess(t, append([]string{"snapshot", "fix"}, tc.flags...)...)
|
|
|
+
|
|
|
+ if tc.initiallyCorrupted {
|
|
|
+ // snapshot verify still fails
|
|
|
+ env.RunAndExpectFailure(t, "snapshot", "verify")
|
|
|
+ } else {
|
|
|
+ env.RunAndExpectFailure(t, "snapshot", "success")
|
|
|
+ }
|
|
|
+
|
|
|
+ env.RunAndExpectSuccess(t, append(append([]string{"snapshot", "fix"}, tc.flags...), "--commit")...)
|
|
|
+
|
|
|
+ if tc.wantFailVerify {
|
|
|
+ env.RunAndExpectFailure(t, "snapshot", "verify")
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ env.RunAndExpectSuccess(t, "snapshot", "verify")
|
|
|
+
|
|
|
+ var manifests []snapshot.Manifest
|
|
|
+
|
|
|
+ testutil.MustParseJSONLines(t, env.RunAndExpectSuccess(t, "snapshot", "list", "--json"), &manifests)
|
|
|
+ require.Len(t, manifests, 2)
|
|
|
+
|
|
|
+ // make sure all root entries have been fixed the same way
|
|
|
+ require.Equal(t, manifests[0].RootEntry, manifests[1].RootEntry)
|
|
|
+
|
|
|
+ switch {
|
|
|
+ case tc.wantRecoveredFiles != nil:
|
|
|
+ var remainingFiles []string
|
|
|
+
|
|
|
+ for f := range mustGetFileMap(t, env, manifests[0].RootObjectID()) {
|
|
|
+ remainingFiles = append(remainingFiles, f)
|
|
|
+ }
|
|
|
+
|
|
|
+ sort.Strings(remainingFiles)
|
|
|
+ require.Equal(t, tc.wantRecoveredFiles, remainingFiles)
|
|
|
+
|
|
|
+ case tc.wantRootStub:
|
|
|
+ var stub snapshotfs.UnreadableDirEntryReplacement
|
|
|
+
|
|
|
+ testutil.MustParseJSONLines(t, env.RunAndExpectSuccess(t, "show", string(manifests[0].RootObjectID())), &stub)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// forgetContents rewrites contents into a new blob and deletes the blob
|
|
|
+// making index entries dangling.
|
|
|
+func forgetContents(t *testing.T, env *testenv.CLITest, contentIDs ...string) {
|
|
|
+ t.Helper()
|
|
|
+
|
|
|
+ before := mustGetContentMap(t, env)
|
|
|
+
|
|
|
+ env.RunAndExpectSuccess(t, append([]string{"content", "rewrite", "--safety=none"}, contentIDs...)...)
|
|
|
+
|
|
|
+ after := mustGetContentMap(t, env)
|
|
|
+
|
|
|
+ var blobIDs []string
|
|
|
+
|
|
|
+ for _, cid := range contentIDs {
|
|
|
+ require.NotEqual(t, before[content.ID(cid)].PackBlobID, after[content.ID(cid)].PackBlobID)
|
|
|
+ blobIDs = append(blobIDs, string(after[index.ID(cid)].PackBlobID))
|
|
|
+ }
|
|
|
+
|
|
|
+ env.RunAndExpectSuccess(t, append([]string{"blob", "rm"}, blobIDs...)...)
|
|
|
+}
|
|
|
+
|
|
|
+func mustGetContentMap(t *testing.T, env *testenv.CLITest) map[content.ID]content.InfoStruct {
|
|
|
+ t.Helper()
|
|
|
+
|
|
|
+ var contents1 []content.InfoStruct
|
|
|
+
|
|
|
+ testutil.MustParseJSONLines(t, env.RunAndExpectSuccess(t, "content", "ls", "--json"), &contents1)
|
|
|
+
|
|
|
+ contentMap := map[content.ID]content.InfoStruct{}
|
|
|
+ for _, v := range contents1 {
|
|
|
+ contentMap[v.ContentID] = v
|
|
|
+ }
|
|
|
+
|
|
|
+ return contentMap
|
|
|
+}
|
|
|
+
|
|
|
+func mustGetFileMap(t *testing.T, env *testenv.CLITest, root object.ID) map[string]*snapshot.DirEntry {
|
|
|
+ t.Helper()
|
|
|
+
|
|
|
+ fileMap := map[string]*snapshot.DirEntry{}
|
|
|
+ mustListDirEntries(t, env, fileMap, root, "")
|
|
|
+
|
|
|
+ return fileMap
|
|
|
+}
|
|
|
+
|
|
|
+func mustListDirEntries(t *testing.T, env *testenv.CLITest, out map[string]*snapshot.DirEntry, root object.ID, prefix string) {
|
|
|
+ t.Helper()
|
|
|
+
|
|
|
+ var dir1 snapshot.DirManifest
|
|
|
+
|
|
|
+ testutil.MustParseJSONLines(t, env.RunAndExpectSuccess(t, "show", string(root)), &dir1)
|
|
|
+
|
|
|
+ for _, v := range dir1.Entries {
|
|
|
+ out[prefix+v.Name] = v
|
|
|
+
|
|
|
+ if v.Type == snapshot.EntryTypeDirectory {
|
|
|
+ mustListDirEntries(t, env, out, v.ObjectID, prefix+v.Name+"/")
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func mustWriteFileWithRepeatedData(t *testing.T, fname string, repeat int, data []byte) {
|
|
|
+ t.Helper()
|
|
|
+
|
|
|
+ f, err := os.Create(fname)
|
|
|
+ require.NoError(t, err)
|
|
|
+
|
|
|
+ defer f.Close()
|
|
|
+
|
|
|
+ for i := 0; i < repeat; i++ {
|
|
|
+ _, err := f.Write(data)
|
|
|
+ require.NoError(t, err)
|
|
|
+ }
|
|
|
+}
|