Skip to content

Commit b7759d9

Browse files
committed
testutils/golden: Add GoldenTracker to track the used golden files
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
1 parent 3139201 commit b7759d9

File tree

1 file changed

+109
-4
lines changed

1 file changed

+109
-4
lines changed

internal/testutils/golden.go

+109-4
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
package testutils
22

33
import (
4+
"io/fs"
5+
"maps"
46
"os"
57
"path/filepath"
8+
"slices"
69
"strings"
10+
"sync"
711
"testing"
812

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

2731
type goldenOptions struct {
28-
goldenPath string
32+
goldenPath string
33+
goldenTracker *GoldenTracker
2934
}
3035

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

43-
// LoadWithUpdateFromGolden loads the element from a plaintext golden file.
44-
// It will update the file if the update flag is used prior to loading it.
45-
func LoadWithUpdateFromGolden(t *testing.T, data string, opts ...GoldenOption) string {
48+
// WithGoldenTracker sets the golden tracker to mark the golden as used.
49+
func WithGoldenTracker(gt *GoldenTracker) GoldenOption {
50+
return func(o *goldenOptions) {
51+
if gt != nil {
52+
o.goldenTracker = gt
53+
}
54+
}
55+
}
56+
57+
func parseOptions(t *testing.T, opts ...GoldenOption) goldenOptions {
4658
t.Helper()
4759

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

68+
return o
69+
}
70+
71+
// LoadWithUpdateFromGolden loads the element from a plaintext golden file.
72+
// It will update the file if the update flag is used prior to loading it.
73+
func LoadWithUpdateFromGolden(t *testing.T, data string, opts ...GoldenOption) string {
74+
t.Helper()
75+
76+
o := parseOptions(t, opts...)
77+
5678
if update {
5779
t.Logf("updating golden file %s", o.goldenPath)
5880
err := os.MkdirAll(filepath.Dir(o.goldenPath), 0750)
@@ -64,6 +86,10 @@ func LoadWithUpdateFromGolden(t *testing.T, data string, opts ...GoldenOption) s
6486
want, err := os.ReadFile(o.goldenPath)
6587
require.NoError(t, err, "Cannot load golden file")
6688

89+
if o.goldenTracker != nil {
90+
o.goldenTracker.MarkUsed(t, WithGoldenPath(o.goldenPath))
91+
}
92+
6793
return string(want)
6894
}
6995

@@ -121,3 +147,82 @@ func GoldenPath(t *testing.T) string {
121147
func UpdateEnabled() bool {
122148
return update
123149
}
150+
151+
// GoldenTracker is a structure to track used golden files in tests.
152+
type GoldenTracker struct {
153+
mu *sync.Mutex
154+
used map[string]struct{}
155+
}
156+
157+
// NewGoldenTracker create a new [GoldenTracker] that checks if golden files are used.
158+
func NewGoldenTracker(t *testing.T) GoldenTracker {
159+
t.Helper()
160+
161+
gt := GoldenTracker{
162+
mu: &sync.Mutex{},
163+
used: make(map[string]struct{}),
164+
}
165+
166+
require.False(t, strings.Contains(t.Name(), "/"),
167+
"Setup: %T should be used from a parent test, %s is not", gt, t.Name())
168+
169+
if slices.ContainsFunc(RunningTests(), func(r string) bool {
170+
prefix := t.Name() + "/"
171+
return strings.HasPrefix(r, prefix) && len(r) > len(prefix)
172+
}) {
173+
t.Logf("%T disabled, can't work on partial tests", gt)
174+
return gt
175+
}
176+
177+
t.Cleanup(func() {
178+
if t.Failed() {
179+
return
180+
}
181+
182+
goldenPath := GoldenPath(t)
183+
184+
var entries []string
185+
err := filepath.WalkDir(goldenPath, func(path string, entry fs.DirEntry, err error) error {
186+
require.NoError(t, err, "TearDown: Reading test golden files %s", path)
187+
if path == goldenPath {
188+
return nil
189+
}
190+
entries = append(entries, path)
191+
return nil
192+
})
193+
require.NoError(t, err, "TearDown: Walking test golden files %s", goldenPath)
194+
195+
gt.mu.Lock()
196+
defer gt.mu.Unlock()
197+
198+
t.Log("Checking golden files in", goldenPath)
199+
var unused []string
200+
for _, e := range entries {
201+
if _, ok := gt.used[e]; ok {
202+
continue
203+
}
204+
unused = append(unused, e)
205+
}
206+
require.Empty(t, unused, "TearDown: Unused golden files have been found, known are %#v",
207+
slices.Collect(maps.Keys(gt.used)))
208+
})
209+
210+
return gt
211+
}
212+
213+
// MarkUsed marks a golden file as being used.
214+
func (gt *GoldenTracker) MarkUsed(t *testing.T, opts ...GoldenOption) {
215+
t.Helper()
216+
217+
gt.mu.Lock()
218+
defer gt.mu.Unlock()
219+
220+
o := parseOptions(t, opts...)
221+
require.Nil(t, o.goldenTracker, "Setup: GoldenTracker option is not supported")
222+
gt.used[o.goldenPath] = struct{}{}
223+
224+
basePath := filepath.Dir(o.goldenPath)
225+
if basePath == GoldenPath(t) {
226+
gt.used[basePath] = struct{}{}
227+
}
228+
}

0 commit comments

Comments
 (0)