Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

util: TempFile improvements #169

Merged
merged 7 commits into from
Aug 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 40 additions & 23 deletions util/tempfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package util

import (
"errors"
"io/fs"
"os"
)
Expand All @@ -20,7 +21,7 @@ type TempFileSettings struct {
data []byte
mode *fs.FileMode
namePattern string
path *string
dir *string
}

type TempFileSetting func(s *TempFileSettings)
Expand Down Expand Up @@ -55,12 +56,14 @@ func ByteData(data []byte) TempFileSetting {
}
}

// Path specifies a directory path to contain the temporary file.
// Dir specifies a directory path to contain the temporary file.
// If dir is the empty string, the file will be created in the
// default directory for temporary files, as returned by os.TempDir.
// 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 {
func Dir(dir string) TempFileSetting {
return func(s *TempFileSettings) {
s.path = &path
s.dir = &dir
}
}

Expand All @@ -71,6 +74,28 @@ func Path(path string) TempFileSetting {
// 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()
path, err := tempFile(t, settings...)
t.Cleanup(func() {
err := os.Remove(path)
if err != nil {
t.Fatalf("failed to clean up temp file: %s", path)
}
})
if err != nil {
t.Fatalf("TempFile: %v", err)
}
return path
}

type tempFileT interface {
Helper()
TempDir() string
}

// tempFile returns errors instead of relying upon T to stop execution, for ease
// of testing TempFile.
func tempFile(t tempFileT, settings ...TempFileSetting) (path string, err error) {
t.Helper()
var allSettings TempFileSettings
for _, setting := range settings {
Expand All @@ -80,39 +105,31 @@ func TempFile(t T, settings ...TempFileSetting) (path string) {
allSettings.mode = new(fs.FileMode)
*allSettings.mode = 0600
}
if allSettings.path == nil {
allSettings.path = new(string)
*allSettings.path = t.TempDir()
if allSettings.dir == nil {
allSettings.dir = new(string)
*allSettings.dir = t.TempDir()
}

var err error
crash := func(t T) {
t.Helper()
t.Fatalf("%s: %v", "TempFile", err)
file, err := os.CreateTemp(*allSettings.dir, allSettings.namePattern)
if errors.Is(err, fs.ErrNotExist) {
return "", errors.New("directory does not exist")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Usually returning a sentinel could be useful. This way people could catch it

Another solution is to wrap the existing error, allowing people to catch the fs.ErrNotExist, if they have to.

Suggested change
return "", errors.New("directory does not exist")
return "", fmt.Errorf("directory does not exist: %w", err)

But these comments might be irrelevant, as you are building a test helper and not a function that people will call and will have to handle its error.

}
file, err := os.CreateTemp(*allSettings.path, allSettings.namePattern)
if err != nil {
crash(t)
return "", err
}
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(t)
return path, err
}
err = file.Close()
if err != nil {
crash(t)
return path, err
}
err = os.Chmod(path, *allSettings.mode)
if err != nil {
crash(t)
return path, err
}
return file.Name()
return file.Name(), nil
}
176 changes: 133 additions & 43 deletions util/tempfile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ package util_test

import (
"bytes"
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
Expand All @@ -24,7 +26,7 @@ type helperTracker struct {
}

func (t *helperTracker) TempDir() string {
t.t.Helper()
t.Helper()
return t.t.TempDir()
}

Expand All @@ -34,20 +36,67 @@ func (t *helperTracker) Helper() {
}

func (t *helperTracker) Errorf(s string, args ...any) {
t.t.Helper()
t.Helper()
t.t.Errorf(s, args)
}

func (t *helperTracker) Fatalf(s string, args ...any) {
t.t.Helper()
t.Helper()
t.t.Fatalf(s, args...)
}

func (t *helperTracker) Cleanup(f func()) {
t.Helper()
t.t.Cleanup(f)
}

func trackFailure(t util.T) *failureTracker {
return &failureTracker{t: t}
}

type failureTracker struct {
failed bool
log bytes.Buffer
t util.T
}

func (t *failureTracker) TempDir() string {
t.Helper()
return t.t.TempDir()
}

func (t *failureTracker) Helper() {
t.t.Helper()
}

func (t *failureTracker) Errorf(s string, args ...any) {
t.Helper()
t.failed = true
fmt.Fprintf(&t.log, s+"\n", args...)
}

func (t *failureTracker) Fatalf(s string, args ...any) {
t.Helper()
t.failed = true
fmt.Fprintf(&t.log, s+"\n", args...)
}

func (t *failureTracker) Cleanup(f func()) {
t.Helper()
t.t.Cleanup(f)
}

func (t *failureTracker) AssertFailedWith(msg string) {
t.Helper()
if !t.failed {
t.t.Fatalf("expected test to fail with message %q", msg)
}
strlog := t.log.String()
if !strings.Contains(strlog, msg) {
t.t.Fatalf("expected test to fail with message %q\ngot message %q", msg, strlog)
}
}

func TestTempFile(t *testing.T) {
t.Run("creates a read/write temp file by default", func(t *testing.T) {
th := trackHelper(t)
Expand All @@ -64,38 +113,44 @@ func TestTempFile(t *testing.T) {
t.Fatalf("expected at least u+rw permission, got %03o", mode)
}
})
t.Run("sets a custom file mode", func(t *testing.T) {

t.Run("using multiple options", 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) {
expectedData := "important data"
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))
}
path := util.TempFile(t,
util.Mode(expectedMode),
util.Pattern(pattern),
util.StringData(expectedData))

t.Run("Mode sets a custom file mode", func(t *testing.T) {
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) {
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) {
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) {

t.Run("ByteData sets binary data", func(t *testing.T) {
expectedData := []byte("important data")
path := util.TempFile(t, util.ByteData(expectedData))
actualData, err := os.ReadFile(path)
Expand All @@ -107,27 +162,62 @@ func TestTempFile(t *testing.T) {
}
})

t.Run("file is deleted after test", func(t *testing.T) {
dirpath := t.TempDir()
var path string
t.Run("cleans up file (and nothing else) in custom dir", func(t *testing.T) {
dir := t.TempDir()
existing, err := os.CreateTemp(dir, "")
if err != nil {
t.Fatalf("failed to create temporary file: %v", err)
}
existingPath := existing.Name()
existing.Close()
var newPath string

t.Run("uses custom path", func(t *testing.T) {
path = util.TempFile(t, util.Path(dirpath))
entries, err := os.ReadDir(dirpath)
t.Run("uses custom directory", func(t *testing.T) {
newPath = util.TempFile(t, util.Dir(dir))
entries, err := os.ReadDir(dir)
if err != nil {
t.Fatalf("failed to read directory: %v", err)
}
if len(entries) == 0 || entries[0].Name() != filepath.Base(path) {
t.Fatalf("did not find temporary file in %s", dirpath)
var found bool
for _, entry := range entries {
if entry.Name() == filepath.Base(newPath) {
found = true
brandondyck marked this conversation as resolved.
Show resolved Hide resolved
break
}
}
if !found {
t.Fatalf("did not find temporary file in %s", dir)
}
})

if path == "" {
if newPath == "" {
t.Fatal("expected non-empty path")
}
_, err := os.Stat(path)
if err == nil {
t.Fatalf("expected temp file not to exist: %s", path)
_, err = os.Stat(newPath)
if !errors.Is(err, fs.ErrNotExist) {
if err == nil {
t.Errorf("expected temp file not to exist: %s", newPath)
} else {
t.Errorf("unexpected error: %v", err)
}
}
_, err = os.Stat(existingPath)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
t.Error("expected pre-existing file not to be deleted")
} else {
t.Errorf("unexpected error statting pre-existing file: %v", err)
}
}
})

t.Run("fails if specified dir doesn't exist", func(t *testing.T) {
fakeDir := filepath.Join(t.TempDir(), "fake")
tracker := trackFailure(t)
path := util.TempFile(tracker, util.Dir(fakeDir))
if path != "" {
t.Errorf("expected empty path\ngot %q", path)
}
tracker.AssertFailedWith("TempFile: directory does not exist")
})
}
Loading