Skip to content

Commit

Permalink
testutils/golden: Add GoldenTracker to track the used golden files
Browse files Browse the repository at this point in the history
If a test using a golden file gets renamed or removed we don't have any
strategy to check if the related golden file(s) got updated as well.

To make this possible, add a new struct that allows tests to track their
test cases and check if the related golden files are being used, or fail
otherwise
  • Loading branch information
3v1n0 committed Oct 21, 2024
1 parent 010e587 commit 40c00f1
Showing 1 changed file with 109 additions and 4 deletions.
113 changes: 109 additions & 4 deletions internal/testutils/golden.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package testutils

import (
"io/fs"
"maps"
"os"
"path/filepath"
"slices"
"strings"
"sync"
"testing"

"github.com/stretchr/testify/require"
Expand All @@ -25,7 +29,8 @@ func init() {
}

type goldenOptions struct {
goldenPath string
goldenPath string
goldenTracker *GoldenTracker
}

// GoldenOption is a supported option reference to change the golden files comparison.
Expand All @@ -40,9 +45,16 @@ func WithGoldenPath(path string) GoldenOption {
}
}

// LoadWithUpdateFromGolden loads the element from a plaintext golden file.
// It will update the file if the update flag is used prior to loading it.
func LoadWithUpdateFromGolden(t *testing.T, data string, opts ...GoldenOption) string {
// WithGoldenTracker sets the golden tracker to mark the golden as used.
func WithGoldenTracker(gt *GoldenTracker) GoldenOption {
return func(o *goldenOptions) {
if gt != nil {
o.goldenTracker = gt
}
}
}

func parseOptions(t *testing.T, opts ...GoldenOption) goldenOptions {
t.Helper()

o := goldenOptions{
Expand All @@ -53,6 +65,16 @@ func LoadWithUpdateFromGolden(t *testing.T, data string, opts ...GoldenOption) s
opt(&o)
}

return o
}

// LoadWithUpdateFromGolden loads the element from a plaintext golden file.
// It will update the file if the update flag is used prior to loading it.
func LoadWithUpdateFromGolden(t *testing.T, data string, opts ...GoldenOption) string {
t.Helper()

o := parseOptions(t, opts...)

if update {
t.Logf("updating golden file %s", o.goldenPath)
err := os.MkdirAll(filepath.Dir(o.goldenPath), 0750)
Expand All @@ -64,6 +86,10 @@ func LoadWithUpdateFromGolden(t *testing.T, data string, opts ...GoldenOption) s
want, err := os.ReadFile(o.goldenPath)
require.NoError(t, err, "Cannot load golden file")

if o.goldenTracker != nil {
o.goldenTracker.MarkUsed(t, WithGoldenPath(o.goldenPath))
}

return string(want)
}

Expand Down Expand Up @@ -121,3 +147,82 @@ func GoldenPath(t *testing.T) string {
func UpdateEnabled() bool {
return update
}

// GoldenTracker is a structure to track used golden files in tests.
type GoldenTracker struct {
mu *sync.Mutex
used map[string]struct{}
}

// NewGoldenTracker create a new [GoldenTracker] that checks if golden files are used.
func NewGoldenTracker(t *testing.T) GoldenTracker {
t.Helper()

gt := GoldenTracker{
mu: &sync.Mutex{},
used: make(map[string]struct{}),
}

require.False(t, strings.Contains(t.Name(), "/"),
"Setup: %T should be used from a parent test, %s is not", gt, t.Name())

if slices.ContainsFunc(RunningTests(), func(r string) bool {
prefix := t.Name() + "/"
return strings.HasPrefix(r, prefix) && len(r) > len(prefix)
}) {
t.Logf("%T disabled, can't work on partial tests", gt)
return gt
}

t.Cleanup(func() {
if t.Failed() {
return
}

goldenPath := GoldenPath(t)

var entries []string
err := filepath.WalkDir(goldenPath, func(path string, entry fs.DirEntry, err error) error {
require.NoError(t, err, "TearDown: Reading test golden files %s", path)
if path == goldenPath {
return nil
}
entries = append(entries, path)
return nil
})
require.NoError(t, err, "TearDown: Walking test golden files %s", goldenPath)

gt.mu.Lock()
defer gt.mu.Unlock()

t.Log("Checking golden files in", goldenPath)
var unused []string
for _, e := range entries {
if _, ok := gt.used[e]; ok {
continue
}
unused = append(unused, e)
}
require.Empty(t, unused, "TearDown: Unused golden files have been found, known are %#v",
slices.Collect(maps.Keys(gt.used)))
})

return gt
}

// MarkUsed marks a golden file as being used.
func (gt *GoldenTracker) MarkUsed(t *testing.T, opts ...GoldenOption) {
t.Helper()

gt.mu.Lock()
defer gt.mu.Unlock()

o := parseOptions(t, opts...)
require.Nil(t, o.goldenTracker, "Setup: GoldenTracker option is not supported")
gt.used[o.goldenPath] = struct{}{}

basePath := filepath.Dir(o.goldenPath)
if basePath == GoldenPath(t) {
gt.used[basePath] = struct{}{}
}
}

0 comments on commit 40c00f1

Please sign in to comment.