From 40c00f1c11015bd4ae33a2794d312dd71a61bb67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= Date: Tue, 22 Oct 2024 00:39:06 +0200 Subject: [PATCH] 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 --- internal/testutils/golden.go | 113 +++++++++++++++++++++++++++++++++-- 1 file changed, 109 insertions(+), 4 deletions(-) diff --git a/internal/testutils/golden.go b/internal/testutils/golden.go index c0fcdf011..f7259c2e8 100644 --- a/internal/testutils/golden.go +++ b/internal/testutils/golden.go @@ -1,9 +1,13 @@ package testutils import ( + "io/fs" + "maps" "os" "path/filepath" + "slices" "strings" + "sync" "testing" "github.com/stretchr/testify/require" @@ -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. @@ -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{ @@ -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) @@ -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) } @@ -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{}{} + } +}