From ed031f6f8d05d96af3edc7fa3154ce3e818370e9 Mon Sep 17 00:00:00 2001 From: Adrian Dombeck Date: Mon, 11 Nov 2024 19:05:41 +0100 Subject: [PATCH 1/9] Remove obsolete comment The function does not do any dconf database filtering --- internal/testutils/golden.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/testutils/golden.go b/internal/testutils/golden.go index 279ce5f..61d40c9 100644 --- a/internal/testutils/golden.go +++ b/internal/testutils/golden.go @@ -122,7 +122,6 @@ func GoldenPath(t *testing.T) string { } // CompareTreesWithFiltering allows comparing a goldPath directory to p. Those can be updated via the dedicated flag. -// It will filter dconf database and not commit it in the new golden directory. func CompareTreesWithFiltering(t *testing.T, p, goldPath string, update bool) { t.Helper() From 0404bb664c05a3adf40ffe61bdc02c9a4bff7176 Mon Sep 17 00:00:00 2001 From: Adrian Dombeck Date: Mon, 11 Nov 2024 19:07:37 +0100 Subject: [PATCH 2/9] Rename CompareTreesWithFiltering -> CheckOrUpdateGoldenFileTree * Remove the "Filtering" part from the name because the function doesn't do any filtering * Mention that it can also update the golden files --- internal/broker/broker_test.go | 4 ++-- internal/broker/config_test.go | 2 +- internal/testutils/golden.go | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/broker/broker_test.go b/internal/broker/broker_test.go index d44d620..14bd3f5 100644 --- a/internal/broker/broker_test.go +++ b/internal/broker/broker_test.go @@ -668,7 +668,7 @@ func TestIsAuthenticated(t *testing.T) { } } - testutils.CompareTreesWithFiltering(t, outDir, testutils.GoldenPath(t), testutils.UpdateEnabled()) + testutils.CheckOrUpdateGoldenFileTree(t, outDir, testutils.GoldenPath(t), testutils.UpdateEnabled()) }) } } @@ -794,7 +794,7 @@ func TestConcurrentIsAuthenticated(t *testing.T) { t.Logf("Failed to rename issuer data directory: %v", err) } } - testutils.CompareTreesWithFiltering(t, outDir, testutils.GoldenPath(t), testutils.UpdateEnabled()) + testutils.CheckOrUpdateGoldenFileTree(t, outDir, testutils.GoldenPath(t), testutils.UpdateEnabled()) }) } } diff --git a/internal/broker/config_test.go b/internal/broker/config_test.go index aee7521..4685c9d 100644 --- a/internal/broker/config_test.go +++ b/internal/broker/config_test.go @@ -106,7 +106,7 @@ func TestParseConfig(t *testing.T) { err = os.WriteFile(filepath.Join(outDir, "config.txt"), []byte(strings.Join(fields, "\n")), 0600) require.NoError(t, err) - testutils.CompareTreesWithFiltering(t, outDir, testutils.GoldenPath(t), testutils.UpdateEnabled()) + testutils.CheckOrUpdateGoldenFileTree(t, outDir, testutils.GoldenPath(t), testutils.UpdateEnabled()) }) } } diff --git a/internal/testutils/golden.go b/internal/testutils/golden.go index 61d40c9..d930b42 100644 --- a/internal/testutils/golden.go +++ b/internal/testutils/golden.go @@ -121,8 +121,8 @@ func GoldenPath(t *testing.T) string { return path } -// CompareTreesWithFiltering allows comparing a goldPath directory to p. Those can be updated via the dedicated flag. -func CompareTreesWithFiltering(t *testing.T, p, goldPath string, update bool) { +// CheckOrUpdateGoldenFileTree allows comparing a goldPath directory to p. Those can be updated via the dedicated flag. +func CheckOrUpdateGoldenFileTree(t *testing.T, p, goldPath string, update bool) { t.Helper() // UpdateEnabled golden file From f638255354ce70f6353ed53511333cc4120b0c35 Mon Sep 17 00:00:00 2001 From: Adrian Dombeck Date: Mon, 11 Nov 2024 19:09:50 +0100 Subject: [PATCH 3/9] Remove "update" argument from CheckOrUpdateGoldenFileTree We don't need to pass it, the function can access the package variable instead. --- internal/broker/broker_test.go | 4 ++-- internal/broker/config_test.go | 2 +- internal/testutils/golden.go | 3 +-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/internal/broker/broker_test.go b/internal/broker/broker_test.go index 14bd3f5..67b0b4a 100644 --- a/internal/broker/broker_test.go +++ b/internal/broker/broker_test.go @@ -668,7 +668,7 @@ func TestIsAuthenticated(t *testing.T) { } } - testutils.CheckOrUpdateGoldenFileTree(t, outDir, testutils.GoldenPath(t), testutils.UpdateEnabled()) + testutils.CheckOrUpdateGoldenFileTree(t, outDir, testutils.GoldenPath(t)) }) } } @@ -794,7 +794,7 @@ func TestConcurrentIsAuthenticated(t *testing.T) { t.Logf("Failed to rename issuer data directory: %v", err) } } - testutils.CheckOrUpdateGoldenFileTree(t, outDir, testutils.GoldenPath(t), testutils.UpdateEnabled()) + testutils.CheckOrUpdateGoldenFileTree(t, outDir, testutils.GoldenPath(t)) }) } } diff --git a/internal/broker/config_test.go b/internal/broker/config_test.go index 4685c9d..bd02d62 100644 --- a/internal/broker/config_test.go +++ b/internal/broker/config_test.go @@ -106,7 +106,7 @@ func TestParseConfig(t *testing.T) { err = os.WriteFile(filepath.Join(outDir, "config.txt"), []byte(strings.Join(fields, "\n")), 0600) require.NoError(t, err) - testutils.CheckOrUpdateGoldenFileTree(t, outDir, testutils.GoldenPath(t), testutils.UpdateEnabled()) + testutils.CheckOrUpdateGoldenFileTree(t, outDir, testutils.GoldenPath(t)) }) } } diff --git a/internal/testutils/golden.go b/internal/testutils/golden.go index d930b42..b9cf6c7 100644 --- a/internal/testutils/golden.go +++ b/internal/testutils/golden.go @@ -122,10 +122,9 @@ func GoldenPath(t *testing.T) string { } // CheckOrUpdateGoldenFileTree allows comparing a goldPath directory to p. Those can be updated via the dedicated flag. -func CheckOrUpdateGoldenFileTree(t *testing.T, p, goldPath string, update bool) { +func CheckOrUpdateGoldenFileTree(t *testing.T, p, goldPath string) { t.Helper() - // UpdateEnabled golden file if update { t.Logf("updating golden file %s", goldPath) require.NoError(t, os.RemoveAll(goldPath), "Cannot remove target golden directory") From e7de8bb98093b3f8f550121cfbbefb0a2de2643e Mon Sep 17 00:00:00 2001 From: Adrian Dombeck Date: Mon, 11 Nov 2024 19:11:16 +0100 Subject: [PATCH 4/9] Don't nest function calls in require.NoError Move function calls out of require.NoError for improved readability. --- internal/testutils/golden.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/internal/testutils/golden.go b/internal/testutils/golden.go index b9cf6c7..8b0dc2a 100644 --- a/internal/testutils/golden.go +++ b/internal/testutils/golden.go @@ -127,7 +127,8 @@ func CheckOrUpdateGoldenFileTree(t *testing.T, p, goldPath string) { if update { t.Logf("updating golden file %s", goldPath) - require.NoError(t, os.RemoveAll(goldPath), "Cannot remove target golden directory") + err := os.RemoveAll(goldPath) + require.NoError(t, err, "Cannot remove target golden directory") // check the source directory exists before trying to copy it info, err := os.Stat(p) @@ -140,7 +141,8 @@ func CheckOrUpdateGoldenFileTree(t *testing.T, p, goldPath string) { // copy file data, err := os.ReadFile(p) require.NoError(t, err, "Cannot read new generated file file %s", p) - require.NoError(t, os.WriteFile(goldPath, data, info.Mode()), "Cannot write golden file") + err = os.WriteFile(goldPath, data, info.Mode()) + require.NoError(t, err, "Cannot write golden file") } else { err := addEmptyMarker(p) require.NoError(t, err, "Cannot add empty marker to directory %s", p) From 126b619725800025f0cab688a1fd9766d0ee6da5 Mon Sep 17 00:00:00 2001 From: Adrian Dombeck Date: Mon, 11 Nov 2024 19:14:58 +0100 Subject: [PATCH 5/9] Rename arguments --- internal/testutils/golden.go | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/internal/testutils/golden.go b/internal/testutils/golden.go index 8b0dc2a..d2dadf8 100644 --- a/internal/testutils/golden.go +++ b/internal/testutils/golden.go @@ -122,47 +122,47 @@ func GoldenPath(t *testing.T) string { } // CheckOrUpdateGoldenFileTree allows comparing a goldPath directory to p. Those can be updated via the dedicated flag. -func CheckOrUpdateGoldenFileTree(t *testing.T, p, goldPath string) { +func CheckOrUpdateGoldenFileTree(t *testing.T, path, goldenPath string) { t.Helper() if update { - t.Logf("updating golden file %s", goldPath) - err := os.RemoveAll(goldPath) + t.Logf("updating golden path %s", goldenPath) + err := os.RemoveAll(goldenPath) require.NoError(t, err, "Cannot remove target golden directory") // check the source directory exists before trying to copy it - info, err := os.Stat(p) + info, err := os.Stat(path) if errors.Is(err, fs.ErrNotExist) { return } - require.NoErrorf(t, err, "Error on checking %q", p) + require.NoErrorf(t, err, "Error on checking %q", path) if !info.IsDir() { // copy file - data, err := os.ReadFile(p) - require.NoError(t, err, "Cannot read new generated file file %s", p) - err = os.WriteFile(goldPath, data, info.Mode()) + data, err := os.ReadFile(path) + require.NoError(t, err, "Cannot read new generated file file %s", path) + err = os.WriteFile(goldenPath, data, info.Mode()) require.NoError(t, err, "Cannot write golden file") } else { - err := addEmptyMarker(p) - require.NoError(t, err, "Cannot add empty marker to directory %s", p) + err := addEmptyMarker(path) + require.NoError(t, err, "Cannot add empty marker to directory %s", path) - err = cp.Copy(p, goldPath) + err = cp.Copy(path, goldenPath) require.NoError(t, err, "Can’t update golden directory") } } var gotContent map[string]treeAttrs - if _, err := os.Stat(p); err == nil { - gotContent, err = treeContentAndAttrs(t, p, nil) + if _, err := os.Stat(path); err == nil { + gotContent, err = treeContentAndAttrs(t, path, nil) if err != nil { t.Fatalf("No generated content: %v", err) } } var goldContent map[string]treeAttrs - if _, err := os.Stat(goldPath); err == nil { - goldContent, err = treeContentAndAttrs(t, goldPath, nil) + if _, err := os.Stat(goldenPath); err == nil { + goldContent, err = treeContentAndAttrs(t, goldenPath, nil) if err != nil { t.Fatalf("No golden directory found: %v", err) } @@ -175,8 +175,8 @@ func CheckOrUpdateGoldenFileTree(t *testing.T, p, goldPath string) { } require.Empty(t, gotContent, "Some files are missing in the golden directory") - // No more verification on p if it doesn’t exists - if _, err := os.Stat(p); errors.Is(err, fs.ErrNotExist) { + // No more verification on path if it doesn’t exists + if _, err := os.Stat(path); errors.Is(err, fs.ErrNotExist) { return } } From d994038d511fc48eae23e6c3d0cb98703eb65fe1 Mon Sep 17 00:00:00 2001 From: Adrian Dombeck Date: Mon, 11 Nov 2024 18:46:11 +0100 Subject: [PATCH 6/9] Print golden file path on mismatch --- internal/broker/broker_test.go | 12 +-- internal/testutils/golden.go | 177 +++++++++++++++++---------------- 2 files changed, 96 insertions(+), 93 deletions(-) diff --git a/internal/broker/broker_test.go b/internal/broker/broker_test.go index 67b0b4a..4061bb1 100644 --- a/internal/broker/broker_test.go +++ b/internal/broker/broker_test.go @@ -259,8 +259,7 @@ func TestGetAuthenticationModes(t *testing.T) { } require.NoError(t, err, "GetAuthenticationModes should not have returned an error") - want := testutils.LoadWithUpdateFromGoldenYAML(t, got) - require.Equal(t, want, got, "GetAuthenticationModes should have returned the expected value") + testutils.CheckOrUpdateGoldenYAML(t, got, nil) }) } } @@ -375,8 +374,7 @@ func TestSelectAuthenticationMode(t *testing.T) { } require.NoError(t, err, "SelectAuthenticationMode should not have returned an error") - want := testutils.LoadWithUpdateFromGoldenYAML(t, got) - require.Equal(t, want, got, "SelectAuthenticationMode should have returned the expected layout") + testutils.CheckOrUpdateGoldenYAML(t, got, nil) }) } } @@ -871,8 +869,7 @@ func TestFetchUserInfo(t *testing.T) { } require.NoError(t, err, "FetchUserInfo should not have returned an error") - want := testutils.LoadWithUpdateFromGoldenYAML(t, got) - require.Equal(t, want, got, "FetchUserInfo should have returned the expected value") + testutils.CheckOrUpdateGoldenYAML(t, got, nil) }) } } @@ -976,8 +973,7 @@ func TestUserPreCheck(t *testing.T) { } require.NoError(t, err, "UserPreCheck should not have returned an error") - want := testutils.LoadWithUpdateFromGolden(t, got) - require.Equal(t, want, got, "UserPreCheck should have returned the expected value") + testutils.CheckOrUpdateGolden(t, got, nil) }) } } diff --git a/internal/testutils/golden.go b/internal/testutils/golden.go index d2dadf8..55e15d8 100644 --- a/internal/testutils/golden.go +++ b/internal/testutils/golden.go @@ -1,11 +1,11 @@ package testutils import ( - "bytes" "errors" "io/fs" "os" "path/filepath" + "strconv" "strings" "testing" @@ -28,44 +28,63 @@ func init() { } } -type goldenOptions struct { - goldenPath string +// GoldenOptions are options for functions that work with golden files. +type GoldenOptions struct { + Path string } -// GoldenOption is a supported option reference to change the golden files comparison. -type GoldenOption func(*goldenOptions) +func updateGoldenFile(t *testing.T, path string, data []byte) { + t.Logf("updating golden file %s", path) + err := os.MkdirAll(filepath.Dir(path), 0750) + require.NoError(t, err, "Cannot create directory for updating golden files") + err = os.WriteFile(path, data, 0600) + require.NoError(t, err, "Cannot write golden file") +} -// WithGoldenPath overrides the default path for golden files used. -func WithGoldenPath(path string) GoldenOption { - return func(o *goldenOptions) { - if path != "" { - o.goldenPath = path - } +// CheckOrUpdateGolden compares the provided string with the content of the golden file. If the update environment +// variable is set, the golden file is updated with the provided string. +func CheckOrUpdateGolden(t *testing.T, got string, opts *GoldenOptions) { + t.Helper() + + if opts == nil { + opts = &GoldenOptions{} + } + if opts.Path == "" { + opts.Path = GoldenPath(t) } + + want := LoadWithUpdateFromGolden(t, got, opts) + require.Equal(t, want, got, "Output does not match golden file %s", opts.Path) +} + +// CheckOrUpdateGoldenYAML compares the provided object with the content of the golden file. If the update environment +// variable is set, the golden file is updated with the provided object serialized as YAML. +func CheckOrUpdateGoldenYAML[E any](t *testing.T, got E, opts *GoldenOptions) { + t.Helper() + + data, err := yaml.Marshal(got) + require.NoError(t, err, "Cannot serialize provided object") + + CheckOrUpdateGolden(t, string(data), opts) } // 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 { +func LoadWithUpdateFromGolden(t *testing.T, data string, opts *GoldenOptions) string { t.Helper() - o := goldenOptions{ - goldenPath: GoldenPath(t), + if opts == nil { + opts = &GoldenOptions{} } - - for _, opt := range opts { - opt(&o) + if opts.Path == "" { + opts.Path = GoldenPath(t) } if update { - t.Logf("updating golden file %s", o.goldenPath) - err := os.MkdirAll(filepath.Dir(o.goldenPath), 0750) - require.NoError(t, err, "Cannot create directory for updating golden files") - err = os.WriteFile(o.goldenPath, []byte(data), 0600) - require.NoError(t, err, "Cannot write golden file") + updateGoldenFile(t, opts.Path, []byte(data)) } - want, err := os.ReadFile(o.goldenPath) + want, err := os.ReadFile(opts.Path) require.NoError(t, err, "Cannot load golden file") return string(want) @@ -73,13 +92,13 @@ func LoadWithUpdateFromGolden(t *testing.T, data string, opts ...GoldenOption) s // LoadWithUpdateFromGoldenYAML load the generic element from a YAML serialized golden file. // It will update the file if the update flag is used prior to deserializing it. -func LoadWithUpdateFromGoldenYAML[E any](t *testing.T, got E, opts ...GoldenOption) E { +func LoadWithUpdateFromGoldenYAML[E any](t *testing.T, got E, opts *GoldenOptions) E { t.Helper() t.Logf("Serializing object for golden file") data, err := yaml.Marshal(got) require.NoError(t, err, "Cannot serialize provided object") - want := LoadWithUpdateFromGolden(t, string(data), opts...) + want := LoadWithUpdateFromGolden(t, string(data), opts) var wantDeserialized E err = yaml.Unmarshal([]byte(want), &wantDeserialized) @@ -128,7 +147,7 @@ func CheckOrUpdateGoldenFileTree(t *testing.T, path, goldenPath string) { if update { t.Logf("updating golden path %s", goldenPath) err := os.RemoveAll(goldenPath) - require.NoError(t, err, "Cannot remove target golden directory") + require.NoError(t, err, "Cannot remove golden path %s", goldenPath) // check the source directory exists before trying to copy it info, err := os.Stat(path) @@ -140,7 +159,7 @@ func CheckOrUpdateGoldenFileTree(t *testing.T, path, goldenPath string) { if !info.IsDir() { // copy file data, err := os.ReadFile(path) - require.NoError(t, err, "Cannot read new generated file file %s", path) + require.NoError(t, err, "Cannot read file %s", path) err = os.WriteFile(goldenPath, data, info.Mode()) require.NoError(t, err, "Cannot write golden file") } else { @@ -152,86 +171,74 @@ func CheckOrUpdateGoldenFileTree(t *testing.T, path, goldenPath string) { } } - var gotContent map[string]treeAttrs - if _, err := os.Stat(path); err == nil { - gotContent, err = treeContentAndAttrs(t, path, nil) + // Compare the content and attributes of the files in the directories. + err := filepath.WalkDir(path, func(p string, de fs.DirEntry, err error) error { if err != nil { - t.Fatalf("No generated content: %v", err) + return err } - } - var goldContent map[string]treeAttrs - if _, err := os.Stat(goldenPath); err == nil { - goldContent, err = treeContentAndAttrs(t, goldenPath, nil) - if err != nil { - t.Fatalf("No golden directory found: %v", err) - } - } + relPath, err := filepath.Rel(path, p) + require.NoError(t, err, "Cannot get relative path for %s", p) + goldenFilePath := filepath.Join(goldenPath, relPath) - // Maps are not ordered, so we need to compare the content and attributes of each file - for key, value := range goldContent { - require.Equal(t, value, gotContent[key], "Content or attributes are different for %s", key) - delete(gotContent, key) - } - require.Empty(t, gotContent, "Some files are missing in the golden directory") + if de.IsDir() { + return nil + } - // No more verification on path if it doesn’t exists - if _, err := os.Stat(path); errors.Is(err, fs.ErrNotExist) { - return - } -} + goldenFile, err := os.Stat(goldenFilePath) + if errors.Is(err, fs.ErrNotExist) { + require.Failf(t, "Unexpected file %s", p) + } + require.NoError(t, err, "Cannot get golden file %s", goldenFilePath) -// treeAttrs are the attributes to take into consideration when comparing each file. -type treeAttrs struct { - content string - path string - executable bool -} + file, err := os.Stat(p) + require.NoError(t, err, "Cannot get file %s", p) -const fileForEmptyDir = ".empty" + // Compare executable bit + a := strconv.FormatInt(int64(goldenFile.Mode().Perm()&0o111), 8) + b := strconv.FormatInt(int64(file.Mode().Perm()&0o111), 8) + require.Equal(t, a, b, "Executable bit does not match.\nFile: %s\nGolden file: %s", p, goldenFilePath) -// treeContentAndAttrs builds a recursive file list of dir with their content and other attributes. -// It can ignore files starting with ignoreHeaders. -func treeContentAndAttrs(t *testing.T, dir string, ignoreHeaders []byte) (map[string]treeAttrs, error) { - t.Helper() + // Compare content + fileContent, err := os.ReadFile(p) + require.NoError(t, err, "Cannot read file %s", p) + goldenContent, err := os.ReadFile(goldenFilePath) + require.NoError(t, err, "Cannot read golden file %s", goldenFilePath) + require.Equal(t, string(fileContent), string(goldenContent), "Content does not match.\nFile: %s\nGolden file: %s", p, goldenFilePath) - r := make(map[string]treeAttrs) + return nil + }) + require.NoError(t, err, "Cannot walk through directory %s", path) - err := filepath.WalkDir(dir, func(path string, de fs.DirEntry, err error) error { + // Check if there are files in the golden directory that are not in the source directory. + err = filepath.WalkDir(goldenPath, func(p string, de fs.DirEntry, err error) error { if err != nil { return err } - // Ignore markers for empty directories - if filepath.Base(path) == fileForEmptyDir { + // Ignore the ".empty" file + if de.Name() == fileForEmptyDir { return nil } - content := "" - info, err := os.Stat(path) - require.NoError(t, err, "Cannot stat %s", path) - if !de.IsDir() { - d, err := os.ReadFile(path) - if err != nil { - return err - } - // ignore given header - if ignoreHeaders != nil && bytes.HasPrefix(d, ignoreHeaders) { - return nil - } - content = string(d) + relPath, err := filepath.Rel(goldenPath, p) + require.NoError(t, err, "Cannot get relative path for %s", p) + filePath := filepath.Join(path, relPath) + + if de.IsDir() { + return nil } - trimmedPath := strings.TrimPrefix(path, dir) - r[trimmedPath] = treeAttrs{content, strings.TrimPrefix(path, dir), info.Mode()&0111 != 0} + + _, err = os.Stat(filePath) + require.NoError(t, err, "Missing expected file %s", filePath) + return nil }) - if err != nil { - return nil, err - } - - return r, nil + require.NoError(t, err, "Cannot walk through directory %s", goldenPath) } +const fileForEmptyDir = ".empty" + // addEmptyMarker adds to any empty directory, fileForEmptyDir to it. // That allows git to commit it. func addEmptyMarker(p string) error { From d1376a088dfb23acfb77d611aa4cf1ca7ae3f658 Mon Sep 17 00:00:00 2001 From: Adrian Dombeck Date: Tue, 12 Nov 2024 10:27:05 +0100 Subject: [PATCH 7/9] Improve the diff of mismatching golden files Print the content in multiple lines instead of a single line. --- go.mod | 2 +- internal/testutils/golden.go | 67 ++++++++++++++++++++++++++++++++---- 2 files changed, 61 insertions(+), 8 deletions(-) diff --git a/go.mod b/go.mod index 97e74f3..730545a 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/microsoftgraph/msgraph-sdk-go v1.48.0 github.com/microsoftgraph/msgraph-sdk-go-core v1.2.1 github.com/otiai10/copy v1.14.0 + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 github.com/spf13/cobra v1.8.1 github.com/spf13/viper v1.19.0 github.com/stretchr/testify v1.9.0 @@ -48,7 +49,6 @@ require ( github.com/microsoft/kiota-serialization-text-go v1.0.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect - github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect diff --git a/internal/testutils/golden.go b/internal/testutils/golden.go index 55e15d8..3d49cd4 100644 --- a/internal/testutils/golden.go +++ b/internal/testutils/golden.go @@ -2,6 +2,7 @@ package testutils import ( "errors" + "fmt" "io/fs" "os" "path/filepath" @@ -10,6 +11,7 @@ import ( "testing" cp "github.com/otiai10/copy" + "github.com/pmezard/go-difflib/difflib" "github.com/stretchr/testify/require" "gopkg.in/yaml.v3" ) @@ -53,8 +55,11 @@ func CheckOrUpdateGolden(t *testing.T, got string, opts *GoldenOptions) { opts.Path = GoldenPath(t) } - want := LoadWithUpdateFromGolden(t, got, opts) - require.Equal(t, want, got, "Output does not match golden file %s", opts.Path) + if update { + updateGoldenFile(t, opts.Path, []byte(got)) + } + + checkGoldenFileEqualsString(t, got, opts.Path) } // CheckOrUpdateGoldenYAML compares the provided object with the content of the golden file. If the update environment @@ -140,6 +145,58 @@ func GoldenPath(t *testing.T) string { return path } +// checkFileContent compares the content of the actual and golden files and reports any differences. +func checkFileContent(t *testing.T, actual, expected, actualPath, expectedPath string) { + if actual == expected { + return + } + + diff := difflib.UnifiedDiff{ + A: difflib.SplitLines(expected), + B: difflib.SplitLines(actual), + FromFile: "Expected (golden)", + ToFile: "Actual", + Context: 3, + } + diffStr, err := difflib.GetUnifiedDiffString(diff) + require.NoError(t, err, "Cannot get unified diff") + + msg := fmt.Sprintf("Golden file: %s", expectedPath) + if actualPath != "Actual" { + msg += fmt.Sprintf("\nFile: %s", actualPath) + } + + require.Failf(t, strings.Join([]string{ + "Golden file content mismatch", + "\nExpected (golden):", + strings.Repeat("-", 50), + strings.TrimSuffix(expected, "\n"), + strings.Repeat("-", 50), + "\nActual: ", + strings.Repeat("-", 50), + strings.TrimSuffix(actual, "\n"), + strings.Repeat("-", 50), + "\nDiff:", + diffStr, + }, "\n"), msg) +} + +func checkGoldenFileEqualsFile(t *testing.T, path, goldenPath string) { + fileContent, err := os.ReadFile(path) + require.NoError(t, err, "Cannot read file %s", path) + goldenContent, err := os.ReadFile(goldenPath) + require.NoError(t, err, "Cannot read golden file %s", goldenPath) + + checkFileContent(t, string(fileContent), string(goldenContent), path, goldenPath) +} + +func checkGoldenFileEqualsString(t *testing.T, got, goldenPath string) { + goldenContent, err := os.ReadFile(goldenPath) + require.NoError(t, err, "Cannot read golden file %s", goldenPath) + + checkFileContent(t, got, string(goldenContent), "Actual", goldenPath) +} + // CheckOrUpdateGoldenFileTree allows comparing a goldPath directory to p. Those can be updated via the dedicated flag. func CheckOrUpdateGoldenFileTree(t *testing.T, path, goldenPath string) { t.Helper() @@ -200,11 +257,7 @@ func CheckOrUpdateGoldenFileTree(t *testing.T, path, goldenPath string) { require.Equal(t, a, b, "Executable bit does not match.\nFile: %s\nGolden file: %s", p, goldenFilePath) // Compare content - fileContent, err := os.ReadFile(p) - require.NoError(t, err, "Cannot read file %s", p) - goldenContent, err := os.ReadFile(goldenFilePath) - require.NoError(t, err, "Cannot read golden file %s", goldenFilePath) - require.Equal(t, string(fileContent), string(goldenContent), "Content does not match.\nFile: %s\nGolden file: %s", p, goldenFilePath) + checkGoldenFileEqualsFile(t, p, goldenFilePath) return nil }) From 9487cd7460a42f6140a7320d31fb205e1469c09b Mon Sep 17 00:00:00 2001 From: Adrian Dombeck Date: Tue, 12 Nov 2024 10:58:51 +0100 Subject: [PATCH 8/9] Colorize diff of mismatching golden files Use delta to colorize the diff of mismatching golden files if delta is found in the PATH. --- internal/testutils/golden.go | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/internal/testutils/golden.go b/internal/testutils/golden.go index 3d49cd4..6396688 100644 --- a/internal/testutils/golden.go +++ b/internal/testutils/golden.go @@ -1,10 +1,12 @@ package testutils import ( + "bytes" "errors" "fmt" "io/fs" "os" + "os/exec" "path/filepath" "strconv" "strings" @@ -145,6 +147,22 @@ func GoldenPath(t *testing.T) string { return path } +// runDelta pipes the unified diff through the `delta` command for word-level diff and coloring. +func runDelta(diff string) (string, error) { + cmd := exec.Command("delta", "--diff-so-fancy", "--hunk-header-style", "omit") + cmd.Stdin = strings.NewReader(diff) + + var out bytes.Buffer + cmd.Stdout = &out + cmd.Stderr = &out + + err := cmd.Run() + if err != nil { + return "", fmt.Errorf("failed to run delta: %w", err) + } + return out.String(), nil +} + // checkFileContent compares the content of the actual and golden files and reports any differences. func checkFileContent(t *testing.T, actual, expected, actualPath, expectedPath string) { if actual == expected { @@ -161,6 +179,15 @@ func checkFileContent(t *testing.T, actual, expected, actualPath, expectedPath s diffStr, err := difflib.GetUnifiedDiffString(diff) require.NoError(t, err, "Cannot get unified diff") + // Check if the `delta` command is available and use it to colorize the diff. + _, err = exec.LookPath("delta") + if err == nil { + diffStr, err = runDelta(diffStr) + require.NoError(t, err, "Cannot run delta") + } else { + diffStr = "\nDiff:\n" + diffStr + } + msg := fmt.Sprintf("Golden file: %s", expectedPath) if actualPath != "Actual" { msg += fmt.Sprintf("\nFile: %s", actualPath) @@ -176,7 +203,6 @@ func checkFileContent(t *testing.T, actual, expected, actualPath, expectedPath s strings.Repeat("-", 50), strings.TrimSuffix(actual, "\n"), strings.Repeat("-", 50), - "\nDiff:", diffStr, }, "\n"), msg) } From 838e863291455ddd77a901ab38f8100600b3897d Mon Sep 17 00:00:00 2001 From: Adrian Dombeck Date: Wed, 13 Nov 2024 16:54:21 +0100 Subject: [PATCH 9/9] Use variadic options --- internal/broker/broker_test.go | 8 ++--- internal/testutils/golden.go | 53 ++++++++++++++++++++-------------- 2 files changed, 36 insertions(+), 25 deletions(-) diff --git a/internal/broker/broker_test.go b/internal/broker/broker_test.go index 4061bb1..5574afc 100644 --- a/internal/broker/broker_test.go +++ b/internal/broker/broker_test.go @@ -259,7 +259,7 @@ func TestGetAuthenticationModes(t *testing.T) { } require.NoError(t, err, "GetAuthenticationModes should not have returned an error") - testutils.CheckOrUpdateGoldenYAML(t, got, nil) + testutils.CheckOrUpdateGoldenYAML(t, got) }) } } @@ -374,7 +374,7 @@ func TestSelectAuthenticationMode(t *testing.T) { } require.NoError(t, err, "SelectAuthenticationMode should not have returned an error") - testutils.CheckOrUpdateGoldenYAML(t, got, nil) + testutils.CheckOrUpdateGoldenYAML(t, got) }) } } @@ -869,7 +869,7 @@ func TestFetchUserInfo(t *testing.T) { } require.NoError(t, err, "FetchUserInfo should not have returned an error") - testutils.CheckOrUpdateGoldenYAML(t, got, nil) + testutils.CheckOrUpdateGoldenYAML(t, got) }) } } @@ -973,7 +973,7 @@ func TestUserPreCheck(t *testing.T) { } require.NoError(t, err, "UserPreCheck should not have returned an error") - testutils.CheckOrUpdateGolden(t, got, nil) + testutils.CheckOrUpdateGolden(t, got) }) } } diff --git a/internal/testutils/golden.go b/internal/testutils/golden.go index 6396688..9a693d2 100644 --- a/internal/testutils/golden.go +++ b/internal/testutils/golden.go @@ -32,9 +32,20 @@ func init() { } } -// GoldenOptions are options for functions that work with golden files. -type GoldenOptions struct { - Path string +type goldenOptions struct { + path string +} + +// GoldenOption is a supported option reference to change the golden files comparison. +type GoldenOption func(*goldenOptions) + +// WithGoldenPath overrides the default path for golden files used. +func WithGoldenPath(path string) GoldenOption { + return func(o *goldenOptions) { + if path != "" { + o.path = path + } + } } func updateGoldenFile(t *testing.T, path string, data []byte) { @@ -47,51 +58,51 @@ func updateGoldenFile(t *testing.T, path string, data []byte) { // CheckOrUpdateGolden compares the provided string with the content of the golden file. If the update environment // variable is set, the golden file is updated with the provided string. -func CheckOrUpdateGolden(t *testing.T, got string, opts *GoldenOptions) { +func CheckOrUpdateGolden(t *testing.T, got string, options ...GoldenOption) { t.Helper() - if opts == nil { - opts = &GoldenOptions{} + opts := goldenOptions{ + path: GoldenPath(t), } - if opts.Path == "" { - opts.Path = GoldenPath(t) + for _, f := range options { + f(&opts) } if update { - updateGoldenFile(t, opts.Path, []byte(got)) + updateGoldenFile(t, opts.path, []byte(got)) } - checkGoldenFileEqualsString(t, got, opts.Path) + checkGoldenFileEqualsString(t, got, opts.path) } // CheckOrUpdateGoldenYAML compares the provided object with the content of the golden file. If the update environment // variable is set, the golden file is updated with the provided object serialized as YAML. -func CheckOrUpdateGoldenYAML[E any](t *testing.T, got E, opts *GoldenOptions) { +func CheckOrUpdateGoldenYAML[E any](t *testing.T, got E, options ...GoldenOption) { t.Helper() data, err := yaml.Marshal(got) require.NoError(t, err, "Cannot serialize provided object") - CheckOrUpdateGolden(t, string(data), opts) + CheckOrUpdateGolden(t, string(data), options...) } // 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 *GoldenOptions) string { +func LoadWithUpdateFromGolden(t *testing.T, data string, options ...GoldenOption) string { t.Helper() - if opts == nil { - opts = &GoldenOptions{} + opts := goldenOptions{ + path: GoldenPath(t), } - if opts.Path == "" { - opts.Path = GoldenPath(t) + for _, f := range options { + f(&opts) } if update { - updateGoldenFile(t, opts.Path, []byte(data)) + updateGoldenFile(t, opts.path, []byte(data)) } - want, err := os.ReadFile(opts.Path) + want, err := os.ReadFile(opts.path) require.NoError(t, err, "Cannot load golden file") return string(want) @@ -99,13 +110,13 @@ func LoadWithUpdateFromGolden(t *testing.T, data string, opts *GoldenOptions) st // LoadWithUpdateFromGoldenYAML load the generic element from a YAML serialized golden file. // It will update the file if the update flag is used prior to deserializing it. -func LoadWithUpdateFromGoldenYAML[E any](t *testing.T, got E, opts *GoldenOptions) E { +func LoadWithUpdateFromGoldenYAML[E any](t *testing.T, got E, options ...GoldenOption) E { t.Helper() t.Logf("Serializing object for golden file") data, err := yaml.Marshal(got) require.NoError(t, err, "Cannot serialize provided object") - want := LoadWithUpdateFromGolden(t, string(data), opts) + want := LoadWithUpdateFromGolden(t, string(data), options...) var wantDeserialized E err = yaml.Unmarshal([]byte(want), &wantDeserialized)