From 73a741a8713d92049763b36c5df0541d6c7bf2c6 Mon Sep 17 00:00:00 2001 From: Brandon Dyck Date: Tue, 20 Aug 2024 17:27:59 -0600 Subject: [PATCH] util: add TempFile helper --- README.md | 4 ++ must/must_test.go | 41 ++++--------- test_test.go | 41 ++++--------- util/tempfile.go | 117 ++++++++++++++++++++++++++++++++++++ util/tempfile_test.go | 135 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 276 insertions(+), 62 deletions(-) create mode 100644 util/tempfile.go create mode 100644 util/tempfile_test.go diff --git a/README.md b/README.md index 6a47694..42d9848 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,10 @@ There are five key packages, ### Changes +:ballot_box_with_check: v1.10.0 adds a `util` package for helpers that return values + + - Adds ability to create and automatically clean up temporary files + :ballot_box_with_check: v1.9.0 substantially improves filesystem tests - Greater compatibility with Windows diff --git a/must/must_test.go b/must/must_test.go index 3e16817..d54ef6a 100644 --- a/must/must_test.go +++ b/must/must_test.go @@ -17,6 +17,7 @@ import ( "testing/fstest" "time" + "github.com/shoenig/test/util" "github.com/shoenig/test/wait" ) @@ -1356,15 +1357,6 @@ func TestMapNotContainsValueEqual(t *testing.T) { MapNotContainsValueEqual(tc, m, &Person{ID: 200, Name: "Daisy"}) } -func writeTempFile(t *testing.T, name, data string) (path string) { - path = filepath.Join(t.TempDir(), name) - err := os.WriteFile(path, []byte(data), os.ModePerm) - if err != nil { - t.Fatal("failed to create temp file") - } - return path -} - func TestFileExistsFS(t *testing.T) { t.Run("file does not exist", func(t *testing.T) { tc := newCase(t, `expected file to exist`) @@ -1391,7 +1383,7 @@ func TestFileExists(t *testing.T) { tc := newCase(t, "") t.Cleanup(tc.assertNot) - FileExists(tc, writeTempFile(t, "real", "")) + FileExists(tc, util.TempFile(t, util.Pattern("real"))) }) } @@ -1415,7 +1407,7 @@ func TestFileNotExists(t *testing.T) { tc := newCase(t, `expected file to not exist`) t.Cleanup(tc.assert) - FileNotExists(tc, writeTempFile(t, "real", "")) + FileNotExists(tc, util.TempFile(t, util.Pattern("real"))) }) t.Run("file does not exist", func(t *testing.T) { tc := newCase(t, "") @@ -1513,21 +1505,6 @@ func TestFileModeFS(t *testing.T) { }) } -func createFileWithPerm(t *testing.T, perm fs.FileMode) (path string) { - t.Helper() - f, err := os.CreateTemp(t.TempDir(), "") - if err != nil { - t.Fatal("failed to created temp file") - } - f.Close() - err = os.Chmod(f.Name(), perm) - if err != nil { - t.Fatal("failed to set file permissions") - } - t.Log("created temp file", f.Name()) - return f.Name() -} - func createDirWithPerm(t *testing.T, perm fs.FileMode) (path string) { t.Helper() path, err := os.MkdirTemp(t.TempDir(), "") @@ -1550,7 +1527,7 @@ func TestFileMode(t *testing.T) { tc := newCase(t, `expected different file permissions`) t.Cleanup(tc.assert) - path := createFileWithPerm(t, 0666) + path := util.TempFile(t, util.Mode(0666)) FileMode(tc, path, 0755) }) @@ -1559,7 +1536,7 @@ func TestFileMode(t *testing.T) { t.Cleanup(tc.assertNot) const perm fs.FileMode = 0666 - path := createFileWithPerm(t, perm) + path := util.TempFile(t, util.Mode(perm)) FileMode(tc, path, perm) }) } @@ -1637,7 +1614,7 @@ func TestDirMode(t *testing.T) { t.Cleanup(tc.assert) const perm fs.FileMode = 0777 - path := createFileWithPerm(t, perm) + path := util.TempFile(t, util.Mode(perm)) DirMode(tc, path, perm) }) } @@ -1662,13 +1639,15 @@ func TestFileContains(t *testing.T) { tc := newCase(t, `expected file contents`) t.Cleanup(tc.assert) - FileContains(tc, writeTempFile(t, "test", "real data"), "fake") + path := util.TempFile(t, util.Pattern("test"), util.StringData("real data")) + FileContains(tc, path, "fake") }) t.Run("file contains data", func(t *testing.T) { tc := newCase(t, "") t.Cleanup(tc.assertNot) - FileContains(tc, writeTempFile(t, "test", "real data"), "real") + path := util.TempFile(t, util.Pattern("test"), util.StringData("real data")) + FileContains(tc, path, "real") }) } diff --git a/test_test.go b/test_test.go index 579f0ac..bd15cd9 100644 --- a/test_test.go +++ b/test_test.go @@ -15,6 +15,7 @@ import ( "testing/fstest" "time" + "github.com/shoenig/test/util" "github.com/shoenig/test/wait" ) @@ -1354,15 +1355,6 @@ func TestMapNotContainsValueEqual(t *testing.T) { MapNotContainsValueEqual(tc, m, &Person{ID: 200, Name: "Daisy"}) } -func writeTempFile(t *testing.T, name, data string) (path string) { - path = filepath.Join(t.TempDir(), name) - err := os.WriteFile(path, []byte(data), os.ModePerm) - if err != nil { - t.Fatal("failed to create temp file") - } - return path -} - func TestFileExistsFS(t *testing.T) { t.Run("file does not exist", func(t *testing.T) { tc := newCase(t, `expected file to exist`) @@ -1389,7 +1381,7 @@ func TestFileExists(t *testing.T) { tc := newCase(t, "") t.Cleanup(tc.assertNot) - FileExists(tc, writeTempFile(t, "real", "")) + FileExists(tc, util.TempFile(t, util.Pattern("real"))) }) } @@ -1413,7 +1405,7 @@ func TestFileNotExists(t *testing.T) { tc := newCase(t, `expected file to not exist`) t.Cleanup(tc.assert) - FileNotExists(tc, writeTempFile(t, "real", "")) + FileNotExists(tc, util.TempFile(t, util.Pattern("real"))) }) t.Run("file does not exist", func(t *testing.T) { tc := newCase(t, "") @@ -1511,21 +1503,6 @@ func TestFileModeFS(t *testing.T) { }) } -func createFileWithPerm(t *testing.T, perm fs.FileMode) (path string) { - t.Helper() - f, err := os.CreateTemp(t.TempDir(), "") - if err != nil { - t.Fatal("failed to created temp file") - } - f.Close() - err = os.Chmod(f.Name(), perm) - if err != nil { - t.Fatal("failed to set file permissions") - } - t.Log("created temp file", f.Name()) - return f.Name() -} - func createDirWithPerm(t *testing.T, perm fs.FileMode) (path string) { t.Helper() path, err := os.MkdirTemp(t.TempDir(), "") @@ -1548,7 +1525,7 @@ func TestFileMode(t *testing.T) { tc := newCase(t, `expected different file permissions`) t.Cleanup(tc.assert) - path := createFileWithPerm(t, 0666) + path := util.TempFile(t, util.Mode(0666)) FileMode(tc, path, 0755) }) @@ -1557,7 +1534,7 @@ func TestFileMode(t *testing.T) { t.Cleanup(tc.assertNot) const perm fs.FileMode = 0666 - path := createFileWithPerm(t, perm) + path := util.TempFile(t, util.Mode(perm)) FileMode(tc, path, perm) }) } @@ -1635,7 +1612,7 @@ func TestDirMode(t *testing.T) { t.Cleanup(tc.assert) const perm fs.FileMode = 0777 - path := createFileWithPerm(t, perm) + path := util.TempFile(t, util.Mode(perm)) DirMode(tc, path, perm) }) } @@ -1660,13 +1637,15 @@ func TestFileContains(t *testing.T) { tc := newCase(t, `expected file contents`) t.Cleanup(tc.assert) - FileContains(tc, writeTempFile(t, "test", "real data"), "fake") + path := util.TempFile(t, util.Pattern("test"), util.StringData("real data")) + FileContains(tc, path, "fake") }) t.Run("file contains data", func(t *testing.T) { tc := newCase(t, "") t.Cleanup(tc.assertNot) - FileContains(tc, writeTempFile(t, "test", "real data"), "real") + path := util.TempFile(t, util.Pattern("test"), util.StringData("real data")) + FileContains(tc, path, "real") }) } diff --git a/util/tempfile.go b/util/tempfile.go new file mode 100644 index 0000000..fa15e57 --- /dev/null +++ b/util/tempfile.go @@ -0,0 +1,117 @@ +// Copyright (c) The Test Authors +// SPDX-License-Identifier: MPL-2.0 + +package util + +import ( + "io/fs" + "os" +) + +type T interface { + TempDir() string + Helper() + Errorf(format string, args ...any) + Fatalf(format string, args ...any) + Cleanup(func()) +} + +type TempFileSettings struct { + data []byte + mode *fs.FileMode + namePattern string + path *string +} + +type TempFileSetting func(s *TempFileSettings) + +// Pattern sets the filename to pattern with a random string appended. +// If pattern contains a '*', the last '*' will be replaced by the +// random string. +func Pattern(pattern string) TempFileSetting { + return func(s *TempFileSettings) { + s.namePattern = pattern + } +} + +// Mode sets the temporary file's mode. +func Mode(mode fs.FileMode) TempFileSetting { + return func(s *TempFileSettings) { + s.mode = &mode + } +} + +// StringData writes data to the temporary file. +func StringData(data string) TempFileSetting { + return func(s *TempFileSettings) { + s.data = []byte(data) + } +} + +// ByteData writes data to the temporary file. +func ByteData(data []byte) TempFileSetting { + return func(s *TempFileSettings) { + s.data = data + } +} + +// Path specifies a directory path to contain the temporary file. +// A temporary file created in a custom directory will still be deleted +// after the test runs, though the directory may not. +func Path(path string) TempFileSetting { + return func(s *TempFileSettings) { + s.path = &path + } +} + +// TempFile creates a temporary file that is deleted after the test is +// completed. If the file cannot be deleted, the test fails with a message +// containing its path. TempFile creates a new file every time it is called. +// By default, each file thus created is in a unique directory as +// created by (*testing.T).TempDir(); this directory is also deleted +// after the test is completed. +func TempFile(t T, settings ...TempFileSetting) (path string) { + t.Helper() + var allSettings TempFileSettings + for _, setting := range settings { + setting(&allSettings) + } + if allSettings.mode == nil { + allSettings.mode = new(fs.FileMode) + *allSettings.mode = 0600 + } + if allSettings.path == nil { + allSettings.path = new(string) + *allSettings.path = t.TempDir() + } + + var err error + crash := func() { + t.Fatalf("%s: %v", "TempFile", err) + } + file, err := os.CreateTemp(*allSettings.path, allSettings.namePattern) + if err != nil { + crash() + } + path = file.Name() + t.Cleanup(func() { + err := os.Remove(path) + if err != nil { + t.Fatalf("failed to clean up temp file: %s", path) + } + }) + _, err = file.Write(allSettings.data) + if err != nil { + file.Close() + crash() + } + err = file.Close() + if err != nil { + crash() + } + err = os.Chmod(path, *allSettings.mode) + if err != nil { + crash() + } + return file.Name() +} diff --git a/util/tempfile_test.go b/util/tempfile_test.go new file mode 100644 index 0000000..d1ec5ec --- /dev/null +++ b/util/tempfile_test.go @@ -0,0 +1,135 @@ +// Copyright (c) The Test Authors +// SPDX-License-Identifier: MPL-2.0 + +package util_test + +import ( + "bytes" + "io/fs" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/shoenig/test/util" +) + +func trackHelper(t util.T) *helperTracker { + return &helperTracker{t: t} +} + +type helperTracker struct { + helperCalled bool + t util.T +} + +func (t *helperTracker) TempDir() string { + t.t.Helper() + return t.t.TempDir() +} + +func (t *helperTracker) Helper() { + t.t.Helper() + t.helperCalled = true +} + +func (t *helperTracker) Errorf(s string, args ...any) { + t.t.Helper() + t.t.Errorf(s, args) +} + +func (t *helperTracker) Fatalf(s string, args ...any) { + t.t.Helper() + t.t.Fatalf(s, args...) +} + +func (t *helperTracker) Cleanup(f func()) { + t.t.Helper() + t.t.Cleanup(f) +} + +func TestTempFile(t *testing.T) { + t.Run("creates a read/write temp file by default", func(t *testing.T) { + th := trackHelper(t) + path := util.TempFile(th) + if !th.helperCalled { + t.Errorf("expected TempFile to call Helper") + } + info, err := os.Stat(path) + if err != nil { + t.Fatalf("failed to stat temp file: %v", err) + } + mode := info.Mode() + if mode&0400 == 0 || mode&0200 == 0 { + t.Fatalf("expected at least u+rw permission, got %03o", mode) + } + }) + t.Run("sets a custom file mode", func(t *testing.T) { + var expectedMode fs.FileMode = 0444 + path := util.TempFile(t, util.Mode(expectedMode)) + info, err := os.Stat(path) + if err != nil { + t.Fatalf("failed to stat temp file: %v", err) + } + actualMode := info.Mode() + if expectedMode != actualMode { + t.Fatalf("file has wrong mode\nexpected %03o\ngot %03o", expectedMode, actualMode) + } + }) + t.Run("sets a name pattern", func(t *testing.T) { + prefix := "harvey-" + pattern := prefix + "*" + path := util.TempFile(t, util.Pattern(pattern)) + if !strings.Contains(path, prefix) { + t.Fatalf("filename does not match pattern\nexpected to contain %s\ngot %s", prefix, path) + } + }) + t.Run("sets string data", func(t *testing.T) { + expectedData := "important data" + path := util.TempFile(t, util.StringData(expectedData)) + actualData, err := os.ReadFile(path) + if err != nil { + t.Fatalf("failed to read temp file: %v", err) + } + if expectedData != string(actualData) { + t.Fatalf("temp file contains wrong data\nexpected %q\ngot %q", expectedData, string(actualData)) + } + }) + t.Run("sets binary data", func(t *testing.T) { + expectedData := []byte("important data") + path := util.TempFile(t, util.ByteData(expectedData)) + actualData, err := os.ReadFile(path) + if err != nil { + t.Fatalf("failed to read temp file: %v", err) + } + if !bytes.Equal(expectedData, actualData) { + t.Fatalf("temp file contains wrong data\nexpected %q\ngot %q", string(expectedData), actualData) + } + }) + + t.Run("file is deleted after test", func(t *testing.T) { + dirpath := t.TempDir() + var path string + + t.Run("uses custom path", func(t *testing.T) { + path = util.TempFile(t, util.Path(dirpath)) + entries, err := os.ReadDir(dirpath) + if err != nil { + t.Fatalf("failed to read directory: %v", err) + } + if entries[0].Name() != filepath.Base(path) { + t.Fatalf("did not find temporary file in %s", dirpath) + } + }) + + _, err := os.Stat(path) + if err == nil { + t.Errorf("expected temp file not to exist: %s", path) + err := os.Remove(path) + if err != nil { + t.Errorf("failed to clean up temp file: %s", path) + } + t.FailNow() + } + }) +}