From 3185b09d0961134ac946e391f9640a4478fa5d72 Mon Sep 17 00:00:00 2001 From: Jim Myhrberg Date: Fri, 17 Sep 2021 02:08:09 +0100 Subject: [PATCH] refactor(golden): major refactor and improvements This includes sanitizing golden file names to avoid characters and names which are invalid on some operating systems. So this should now work on Linux, macOS, and Window. --- .gitignore | 4 +- Makefile | 14 +- example_test.go | 108 ++++++ global.go | 49 --- golden.go | 358 +++++++++++++++--- global_test.go => golden_test.go | 306 +++++---------- sanitize.go | 34 ++ sanitize_test.go | 124 ++++++ testdata/TestExampleMyStruct.golden | 1 + testdata/TestExampleMyStructP/json.golden | 1 + testdata/TestExampleMyStructP/xml.golden | 1 + .../empty_struct.golden | 1 + .../full_struct.golden | 1 + .../empty_struct/json.golden | 1 + .../empty_struct/xml.golden | 1 + .../full_struct/json.golden | 1 + .../full_struct/xml.golden | 1 + updating.go => update.go | 9 +- update_test.go | 106 ++++++ 19 files changed, 801 insertions(+), 320 deletions(-) create mode 100644 example_test.go delete mode 100644 global.go rename global_test.go => golden_test.go (56%) create mode 100644 sanitize.go create mode 100644 sanitize_test.go create mode 100644 testdata/TestExampleMyStruct.golden create mode 100644 testdata/TestExampleMyStructP/json.golden create mode 100644 testdata/TestExampleMyStructP/xml.golden create mode 100644 testdata/TestExampleMyStructTabular/empty_struct.golden create mode 100644 testdata/TestExampleMyStructTabular/full_struct.golden create mode 100644 testdata/TestExampleMyStructTabularP/empty_struct/json.golden create mode 100644 testdata/TestExampleMyStructTabularP/empty_struct/xml.golden create mode 100644 testdata/TestExampleMyStructTabularP/full_struct/json.golden create mode 100644 testdata/TestExampleMyStructTabularP/full_struct/xml.golden rename updating.go => update.go (54%) create mode 100644 update_test.go diff --git a/.gitignore b/.gitignore index ede5c9f..f65385f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ bin/* -testdata/* coverage.out + +testdata/* +!testdata/TestExample* diff --git a/Makefile b/Makefile index c57f9fc..15b676b 100644 --- a/Makefile +++ b/Makefile @@ -88,6 +88,18 @@ format: $(TOOLDIR)/goimports $(TOOLDIR)/gofumpt bench: go test $(V) -count=1 -bench=$(BENCH) $(TESTARGS) ./... +.PHONY: golden-update +golden-update: + GOLDEN_UPDATE=1 $(MAKE) test + +.PHONY: golden-clean +golden-clean: + find . -type f -name '*.golden' -path '*/testdata/*' -delete + find . -type d -empty -path '*/testdata/*' -delete + +.PHONY: golden-regen +golden-regen: golden-clean golden-update + # # Code Generation # @@ -171,7 +183,7 @@ check-tidy: # Serve docs .PHONY: docs -docs: godoc +docs: $(TOOLDIR)/godoc $(info serviing docs on http://127.0.0.1:6060/pkg/$(GOMODNAME)/) @godoc -http=127.0.0.1:6060 diff --git a/example_test.go b/example_test.go new file mode 100644 index 0000000..d60f678 --- /dev/null +++ b/example_test.go @@ -0,0 +1,108 @@ +package golden_test + +import ( + "encoding/json" + "encoding/xml" + "testing" + + "github.com/jimeh/go-golden" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type MyStruct struct { + Foo string `json:"foo,omitempty"` +} + +// TestExampleMyStruct reads/writes the following golden file: +// +// testdata/TestExampleMyStruct.golden +// +func TestExampleMyStruct(t *testing.T) { + got, err := json.Marshal(&MyStruct{Foo: "Bar"}) + require.NoError(t, err) + + if golden.Update() { + golden.Set(t, got) + } + want := golden.Get(t) + + assert.Equal(t, want, got) +} + +// TestExampleMyStructTabular reads/writes the following golden files: +// +// testdata/TestExampleMyStructTabular/empty_struct.golden +// testdata/TestExampleMyStructTabular/full_struct.golden +// +func TestExampleMyStructTabular(t *testing.T) { + tests := []struct { + name string + obj *MyStruct + }{ + {name: "empty struct", obj: &MyStruct{}}, + {name: "full struct", obj: &MyStruct{Foo: "Bar"}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := json.Marshal(tt.obj) + require.NoError(t, err) + + if golden.Update() { + golden.Set(t, got) + } + want := golden.Get(t) + + assert.Equal(t, want, got) + }) + } +} + +// TestExampleMyStructP reads/writes the following golden file: +// +// testdata/TestExampleMyStructP/json.golden +// testdata/TestExampleMyStructP/xml.golden +// +func TestExampleMyStructP(t *testing.T) { + gotJSON, _ := json.Marshal(&MyStruct{Foo: "Bar"}) + gotXML, _ := xml.Marshal(&MyStruct{Foo: "Bar"}) + + if golden.Update() { + golden.SetP(t, "json", gotJSON) + golden.SetP(t, "xml", gotXML) + } + + assert.Equal(t, golden.GetP(t, "json"), gotJSON) + assert.Equal(t, golden.GetP(t, "xml"), gotXML) +} + +// TestExampleMyStructTabularP reads/writes the following golden file: +// +// testdata/TestExampleMyStructTabularP/empty_struct/json.golden +// testdata/TestExampleMyStructTabularP/empty_struct/xml.golden +// testdata/TestExampleMyStructTabularP/full_struct/json.golden +// testdata/TestExampleMyStructTabularP/full_struct/xml.golden +// +func TestExampleMyStructTabularP(t *testing.T) { + tests := []struct { + name string + obj *MyStruct + }{ + {name: "empty struct", obj: &MyStruct{}}, + {name: "full struct", obj: &MyStruct{Foo: "Bar"}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotJSON, _ := json.Marshal(tt.obj) + gotXML, _ := xml.Marshal(tt.obj) + + if golden.Update() { + golden.SetP(t, "json", gotJSON) + golden.SetP(t, "xml", gotXML) + } + + assert.Equal(t, golden.GetP(t, "json"), gotJSON) + assert.Equal(t, golden.GetP(t, "xml"), gotXML) + }) + } +} diff --git a/global.go b/global.go deleted file mode 100644 index 3787797..0000000 --- a/global.go +++ /dev/null @@ -1,49 +0,0 @@ -package golden - -import "testing" - -var global = New() - -// Updating returns true when golden is set to update golden files. Used to -// determine if golden.Set() should be called or not. -func Updating() bool { - return global.Updating() -} - -// Get returns the content of the default golden file for the given *testing.T -// instance as determined by t.Name(). If no golden file can be found/read, it -// will fail the test with t.Fatal(). -func Get(t *testing.T) []byte { - return global.Get(t) -} - -// Set writes given data of the default golden file for the given *testing.T -// instance as determined by t.Name(). If writing fails it will fail the test -// with t.Fatal() detailing the error. -func Set(t *testing.T, data []byte) { - global.Set(t, data) -} - -// File returns the filename for the default golden file for the given -// *testing.T instance as determined by t.Name(). -func File(t *testing.T) string { - return global.File(t) -} - -// GetNamed return the content of the specifically named golden file belonging -// to the given *testing.T instance as determined by t.Name(). If no golden file -// can be found/read, it will fail the test with t.Fatal(). -func GetNamed(t *testing.T, name string) []byte { - return global.GetNamed(t, name) -} - -// SetNamed writes given data of the specifically named golden file belonging to -// the given *testing.T instance as determined by t.Name(). If writing fails it -// will fail the test with t.Fatal() detailing the error. -func SetNamed(t *testing.T, name string, data []byte) { - global.SetNamed(t, name, data) -} - -func NamedFile(t *testing.T, name string) string { - return global.NamedFile(t, name) -} diff --git a/golden.go b/golden.go index 2e7d266..3e3c5f8 100644 --- a/golden.go +++ b/golden.go @@ -1,10 +1,132 @@ -// Package golden is yet another package for working with *.golden test files. +// Package golden is yet another package for working with *.golden test files, +// with a focus on simplicity through it's default behavior. +// +// Golden file names are based on the name of the test function and any subtest +// names by calling t.Name(). File names are sanitized to ensure they're +// compatible with Linux, macOS and Windows systems regardless of what crazy +// characters might be in a subtest's name. +// +// Usage +// +// Typical usage should look something like this: +// +// func TestExampleMyStruct(t *testing.T) { +// got, err := json.Marshal(&MyStruct{Foo: "Bar"}) +// require.NoError(t, err) +// +// if golden.Update() { +// golden.Set(t, got) +// } +// want := golden.Get(t) +// +// assert.Equal(t, want, got) +// } +// +// The above example will read/write to: +// +// testdata/TestExampleMyStruct.golden +// +// To update the golden file (have golden.Update() return true), simply set the +// GOLDEN_UPDATE environment variable to one of "1", "y", "t", "yes", "on", or +// "true" when running tests. +// +// Sub-Tests +// +// As the golden filename is based on t.Name(), it works with sub-tests too, +// ensuring each sub-test gets it's own golden file. For example: +// +// func TestExampleMyStructTabular(t *testing.T) { +// tests := []struct { +// name string +// obj *MyStruct +// }{ +// {name: "empty struct", obj: &MyStruct{}}, +// {name: "full struct", obj: &MyStruct{Foo: "Bar"}}, +// } +// for _, tt := range tests { +// t.Run(tt.name, func(t *testing.T) { +// got, err := json.Marshal(tt.obj) +// require.NoError(t, err) +// +// if golden.Update() { +// golden.Set(t, got) +// } +// want := golden.Get(t) +// +// assert.Equal(t, want, got) +// }) +// } +// } +// +// The above example will read/write to: +// +// testdata/TestExampleMyStructTabular/empty_struct.golden +// testdata/TestExampleMyStructTabular/full_struct.golden +// +// Multiple Golden Files in a Single Test +// +// The "P" suffixed methods, GetP(), SetP(), and FileP(), all take a name +// argument which allows using specific golden files within a given *testing.T +// instance. +// +// func TestExampleMyStructP(t *testing.T) { +// gotJSON, _ := json.Marshal(&MyStruct{Foo: "Bar"}) +// gotXML, _ := xml.Marshal(&MyStruct{Foo: "Bar"}) +// +// if golden.Update() { +// golden.SetP(t, "json", gotJSON) +// golden.SetP(t, "xml", gotXML) +// } +// +// assert.Equal(t, golden.GetP(t, "json"), gotJSON) +// assert.Equal(t, golden.GetP(t, "xml"), gotXML) +// } +// +// The above example will read/write to: +// +// testdata/TestExampleMyStructP/json.golden +// testdata/TestExampleMyStructP/xml.golden +// +// This works with tabular tests too of course: +// +// func TestExampleMyStructTabularP(t *testing.T) { +// tests := []struct { +// name string +// obj *MyStruct +// }{ +// {name: "empty struct", obj: &MyStruct{}}, +// {name: "full struct", obj: &MyStruct{Foo: "Bar"}}, +// } +// for _, tt := range tests { +// t.Run(tt.name, func(t *testing.T) { +// gotJSON, _ := json.Marshal(tt.obj) +// gotXML, _ := xml.Marshal(tt.obj) +// +// if golden.Update() { +// golden.SetP(t, "json", gotJSON) +// golden.SetP(t, "xml", gotXML) +// } +// +// assert.Equal(t, golden.GetP(t, "json"), gotJSON) +// assert.Equal(t, golden.GetP(t, "xml"), gotXML) +// }) +// } +// } +// +// The above example will read/write to: +// +// testdata/TestExampleMyStructTabularP/empty_struct/json.golden +// testdata/TestExampleMyStructTabularP/empty_struct/xml.golden +// testdata/TestExampleMyStructTabularP/full_struct/json.golden +// testdata/TestExampleMyStructTabularP/full_struct/xml.golden +// package golden import ( "io/ioutil" "os" "path/filepath" + "strings" "testing" ) @@ -15,54 +137,190 @@ const ( DefaultDirname = "testdata" ) -type Golden struct { - DirMode os.FileMode - FileMode os.FileMode - Suffix string - Dirname string - UpdatingFunc UpdatingFunc +var DefaultUpdateFunc = EnvUpdateFunc + +var global = New() + +// File returns the filename of the golden file for the given *testing.T +// instance as determined by t.Name(). +func File(t *testing.T) string { + return global.File(t) } +// Get returns the content of the golden file for the given *testing.T instance +// as determined by t.Name(). If no golden file can be found/read, it will fail +// the test by calling t.Fatal(). +func Get(t *testing.T) []byte { + return global.Get(t) +} + +// Set writes given data to the golden file for the given *testing.T instance as +// determined by t.Name(). If writing fails it will fail the test by calling +// t.Fatal() with error details. +func Set(t *testing.T, data []byte) { + global.Set(t, data) +} + +// FileP returns the filename of the specifically named golden file for the +// given *testing.T instance as determined by t.Name(). +func FileP(t *testing.T, name string) string { + return global.FileP(t, name) +} + +// GetP returns the content of the specifically named golden file belonging +// to the given *testing.T instance as determined by t.Name(). If no golden file +// can be found/read, it will fail the test with t.Fatal(). +// +// This is very similar to Get(), but it allows multiple different golden files +// to be used within the same one *testing.T instance. +func GetP(t *testing.T, name string) []byte { + return global.GetP(t, name) +} + +// SetP writes given data of the specifically named golden file belonging to +// the given *testing.T instance as determined by t.Name(). If writing fails it +// will fail the test with t.Fatal() detailing the error. +// +// This is very similar to Set(), but it allows multiple different golden files +// to be used within the same one *testing.T instance. +func SetP(t *testing.T, name string, data []byte) { + global.SetP(t, name, data) +} + +// Update returns true when golden is set to update golden files. Used to +// determine if golden.Set() or golden.Write() should be called or not. +// +// Default behavior uses EnvUpdateFunc() to check if the "GOLDEN_UPDATE" +// environment variable is set to a truthy value. To customize create a custom +// *Golden instance with New() and set a new UpdateFunc value. +func Update() bool { + return global.Update() +} + +// Golden handles all interactions with golden files. The top-level package +// functions all just proxy through to a default global *Golden instance. +type Golden struct { + // DirMode determines the file system permissions of any folders created to + // hold golden files. + DirMode os.FileMode + + // FileMode determines the file system permissions of any created or updated + // golden files written to disk. + FileMode os.FileMode + + // Suffix determines the filename suffix for all golden files. Typically + // this should be ".golden", but can be changed here if needed. + Suffix string + + // Dirname is the name of the top-level directory at the root of the package + // which holds all golden files. Typically this should "testdata", but can + // be changed here if needed. + Dirname string + + // UpdateFunc is used to determine if golden files should be updated or + // not. Its boolean return value is returned by Update(). + UpdateFunc UpdateFunc +} + +// New returns a new *Golden instance with default values correctly +// populated. This is ideally how you should create a custom *Golden, and then +// modify the relevant fields as you see fit. func New() *Golden { return &Golden{ - DirMode: DefaultDirMode, - FileMode: DefaultFileMode, - Suffix: DefaultSuffix, - Dirname: DefaultDirname, - UpdatingFunc: EnvVarUpdatingFunc, + DirMode: DefaultDirMode, + FileMode: DefaultFileMode, + Suffix: DefaultSuffix, + Dirname: DefaultDirname, + UpdateFunc: DefaultUpdateFunc, } } -// Updating returns true when the function assigned to UpdatingFunc returns -// true. -func (s *Golden) Updating() bool { - return s.UpdatingFunc() -} - -// Get returns the content of the default golden file for the given *testing.T -// instance as determined by t.Name(). If no golden file can be found/read, it -// will fail the test with t.Fatal(). -func (s *Golden) Get(t *testing.T) []byte { - return s.GetNamed(t, "") -} - -// Set writes given data of the default golden file for the given *testing.T -// instance as determined by t.Name(). If writing fails it will fail the test -// with t.Fatal() detailing the error. -func (s *Golden) Set(t *testing.T, data []byte) { - s.SetNamed(t, "", data) -} - +// File returns the filename of the golden file for the given *testing.T +// instance as determined by t.Name(). func (s *Golden) File(t *testing.T) string { - return s.NamedFile(t, "") + return s.file(t, "") } -func (s *Golden) GetNamed(t *testing.T, name string) []byte { - if t == nil { +// Get returns the content of the golden file for the given *testing.T instance +// as determined by t.Name(). If no golden file can be found/read, it will fail +// the test by calling t.Fatal(). +func (s *Golden) Get(t *testing.T) []byte { + return s.get(t, "") +} + +// Set writes given data to the golden file for the given *testing.T instance as +// determined by t.Name(). If writing fails it will fail the test by calling +// t.Fatal() with error details. +func (s *Golden) Set(t *testing.T, data []byte) { + s.set(t, "", data) +} + +// FileP returns the filename of the specifically named golden file for the +// given *testing.T instance as determined by t.Name(). +func (s *Golden) FileP(t *testing.T, name string) string { + if name == "" { + if t != nil { + t.Fatal("golden: name cannot be empty") + } + return "" + } + + return s.file(t, name) +} + +// GetP returns the content of the specifically named golden file belonging +// to the given *testing.T instance as determined by t.Name(). If no golden file +// can be found/read, it will fail the test with t.Fatal(). +// +// This is very similar to Get(), but it allows multiple different golden files +// to be used within the same one *testing.T instance. +func (s *Golden) GetP(t *testing.T, name string) []byte { + if name == "" { + t.Fatal("golden: name cannot be empty") return nil } - f := s.NamedFile(t, name) + return s.get(t, name) +} + +// SetP writes given data of the specifically named golden file belonging to +// the given *testing.T instance as determined by t.Name(). If writing fails it +// will fail the test with t.Fatal() detailing the error. +// +// This is very similar to Set(), but it allows multiple different golden files +// to be used within the same one *testing.T instance. +func (s *Golden) SetP(t *testing.T, name string, data []byte) { + if name == "" { + t.Fatal("golden: name cannot be empty") + } + + s.set(t, name, data) +} + +func (s *Golden) file(t *testing.T, name string) string { + if t.Name() == "" { + t.Fatalf("golden: could not determine filename for: %+v", t) + return "" + } + + base := []string{s.Dirname, filepath.FromSlash(t.Name())} + if name != "" { + base = append(base, name) + } + + f := filepath.Clean(filepath.Join(base...) + s.Suffix) + + dirty := strings.Split(f, string(os.PathSeparator)) + clean := make([]string, 0, len(dirty)) + for _, s := range dirty { + clean = append(clean, sanitizeFilename(s)) + } + + return strings.Join(clean, string(os.PathSeparator)) +} + +func (s *Golden) get(t *testing.T, name string) []byte { + f := s.file(t, name) b, err := ioutil.ReadFile(f) if err != nil { @@ -72,12 +330,8 @@ func (s *Golden) GetNamed(t *testing.T, name string) []byte { return b } -func (s *Golden) SetNamed(t *testing.T, name string, data []byte) { - if t == nil { - return - } - - f := s.NamedFile(t, name) +func (s *Golden) set(t *testing.T, name string, data []byte) { + f := s.file(t, name) dir := filepath.Dir(f) t.Logf("golden: writing .golden file: %s", f) @@ -94,16 +348,12 @@ func (s *Golden) SetNamed(t *testing.T, name string, data []byte) { } } -func (s *Golden) NamedFile(t *testing.T, name string) string { - if t == nil || t.Name() == "" { - t.Fatalf("golden: could not determine filename for: %+v", t) - return "" - } - - base := []string{s.Dirname, filepath.FromSlash(t.Name())} - if name != "" { - base = append(base, name) - } - - return filepath.Clean(filepath.Join(base...) + s.Suffix) +// Update returns true when golden is set to update golden files. Used to +// determine if golden.Set() or golden.Write() should be called or not. +// +// Default behavior uses EnvUpdateFunc() to check if the "GOLDEN_UPDATE" +// environment variable is set to a truthy value. To customize set a new +// UpdateFunc value on *Golden. +func (s *Golden) Update() bool { + return s.UpdateFunc() } diff --git a/global_test.go b/golden_test.go similarity index 56% rename from global_test.go rename to golden_test.go index 6a5c8bd..a077a32 100644 --- a/global_test.go +++ b/golden_test.go @@ -11,98 +11,6 @@ import ( "github.com/stretchr/testify/require" ) -func TestUpdating(t *testing.T) { - tests := []struct { - name string - env map[string]string - want bool - }{ - { - name: "GOLDEN_UPDATE not set", - want: false, - }, - { - name: "GOLDEN_UPDATE set to 0", - env: map[string]string{"GOLDEN_UPDATE": "0"}, - want: false, - }, - { - name: "GOLDEN_UPDATE set to 1", - env: map[string]string{"GOLDEN_UPDATE": "1"}, - want: true, - }, - { - name: "GOLDEN_UPDATE set to 2", - env: map[string]string{"GOLDEN_UPDATE": "2"}, - want: false, - }, - { - name: "GOLDEN_UPDATE set to y", - env: map[string]string{"GOLDEN_UPDATE": "y"}, - want: true, - }, - { - name: "GOLDEN_UPDATE set to n", - env: map[string]string{"GOLDEN_UPDATE": "n"}, - want: false, - }, - { - name: "GOLDEN_UPDATE set to t", - env: map[string]string{"GOLDEN_UPDATE": "t"}, - want: true, - }, - { - name: "GOLDEN_UPDATE set to f", - env: map[string]string{"GOLDEN_UPDATE": "f"}, - want: false, - }, - { - name: "GOLDEN_UPDATE set to yes", - env: map[string]string{"GOLDEN_UPDATE": "yes"}, - want: true, - }, - { - name: "GOLDEN_UPDATE set to no", - env: map[string]string{"GOLDEN_UPDATE": "no"}, - want: false, - }, - { - name: "GOLDEN_UPDATE set to on", - env: map[string]string{"GOLDEN_UPDATE": "on"}, - want: true, - }, - { - name: "GOLDEN_UPDATE set to off", - env: map[string]string{"GOLDEN_UPDATE": "off"}, - want: false, - }, - { - name: "GOLDEN_UPDATE set to true", - env: map[string]string{"GOLDEN_UPDATE": "true"}, - want: true, - }, - { - name: "GOLDEN_UPDATE set to false", - env: map[string]string{"GOLDEN_UPDATE": "false"}, - want: false, - }, - { - name: "GOLDEN_UPDATE set to foobarnopebbq", - env: map[string]string{"GOLDEN_UPDATE": "foobarnopebbq"}, - want: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - envctl.WithClean(tt.env, func() { - got := Updating() - - assert.Equal(t, tt.want, got) - }) - }) - } -} - func TestFile(t *testing.T) { got := File(t) @@ -122,11 +30,11 @@ func TestFile(t *testing.T) { }, { name: "foo/bar", - want: filepath.Join("testdata", "TestFile", "foo/bar.golden"), + want: filepath.Join("testdata", "TestFile", "foo", "bar.golden"), }, { name: `"foobar"`, - want: filepath.Join("testdata", "TestFile", "\"foobar\".golden"), + want: filepath.Join("testdata", "TestFile", "_foobar_.golden"), }, } for _, tt := range tests { @@ -140,9 +48,9 @@ func TestFile(t *testing.T) { func TestGet(t *testing.T) { t.Cleanup(func() { - err := os.RemoveAll(filepath.Join("testdata", t.Name())) + err := os.RemoveAll(filepath.Join("testdata", "TestGet")) require.NoError(t, err) - err = os.Remove(filepath.Join("testdata", t.Name()+".golden")) + err = os.Remove(filepath.Join("testdata", "TestGet.golden")) require.NoError(t, err) }) @@ -188,7 +96,7 @@ func TestGet(t *testing.T) { { name: "thing: it's a thing!", file: filepath.Join( - "testdata", "TestGet", "thing:_it's_a_thing!.golden", + "testdata", "TestGet", "thing__it's_a_thing!.golden", ), want: []byte("A thing? Really? Are we getting lazy? :P"), }, @@ -214,9 +122,9 @@ func TestGet(t *testing.T) { func TestSet(t *testing.T) { t.Cleanup(func() { - err := os.RemoveAll(filepath.Join("testdata", t.Name())) + err := os.RemoveAll(filepath.Join("testdata", "TestSet")) require.NoError(t, err) - err = os.Remove(filepath.Join("testdata", t.Name()+".golden")) + err = os.Remove(filepath.Join("testdata", "TestSet.golden")) require.NoError(t, err) }) @@ -258,7 +166,7 @@ func TestSet(t *testing.T) { { name: "thing: it's a thing!", file: filepath.Join( - "testdata", "TestSet", "thing:_it's_a_thing!.golden", + "testdata", "TestSet", "thing__it's_a_thing!.golden", ), content: []byte("A thing? Really? Are we getting lazy? :P"), }, @@ -278,34 +186,74 @@ func TestSet(t *testing.T) { } } -func TestGetNamed(t *testing.T) { +func TestFileP(t *testing.T) { + got := FileP(t, "sub-name") + assert.Equal(t, + filepath.Join("testdata", "TestFileP", "sub-name.golden"), got, + ) + + tests := []struct { + name string + named string + want string + }{ + { + name: "", + named: "sub-thing", + want: filepath.Join( + "testdata", "TestFileP", "#00", "sub-thing.golden", + ), + }, + { + name: "fozbaz", + named: "email", + want: filepath.Join( + "testdata", "TestFileP", "fozbaz", "email.golden", + ), + }, + { + name: "fozbaz", + named: "json", + want: filepath.Join( + "testdata", "TestFileP", "fozbaz#01", "json.golden", + ), + }, + { + name: "foo/bar", + named: "hello/world", + want: filepath.Join( + "testdata", "TestFileP", + "foo", "bar", + "hello", "world.golden", + ), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := FileP(t, tt.named) + + assert.Equal(t, tt.want, got) + }) + } +} + +func TestGetP(t *testing.T) { t.Cleanup(func() { - err := os.RemoveAll(filepath.Join("testdata", t.Name())) - require.NoError(t, err) - err = os.Remove(filepath.Join("testdata", t.Name()+".golden")) + err := os.RemoveAll(filepath.Join("testdata", "TestGetP")) require.NoError(t, err) }) - err := os.MkdirAll(filepath.Join("testdata", "TestGetNamed"), 0o755) + err := os.MkdirAll(filepath.Join("testdata", "TestGetP"), 0o755) require.NoError(t, err) - content := []byte("this is the default golden file for TestGetNamed") + content := []byte("this is the named golden file for TestGetP") err = ioutil.WriteFile( //nolint:gosec - filepath.Join("testdata", "TestGetNamed.golden"), content, 0o644, - ) - require.NoError(t, err) - - got := GetNamed(t, "") - assert.Equal(t, content, got) - - content = []byte("this is the named golden file for TestGetNamed") - err = ioutil.WriteFile( //nolint:gosec - filepath.Join("testdata", "TestGetNamed", "sub-name.golden"), + filepath.Join("testdata", "TestGetP", "sub-name.golden"), content, 0o644, ) require.NoError(t, err) - got = GetNamed(t, "sub-name") + got := GetP(t, "sub-name") assert.Equal(t, content, got) tests := []struct { @@ -314,16 +262,11 @@ func TestGetNamed(t *testing.T) { file string want []byte }{ - { - name: "", - file: filepath.Join("testdata", "TestGetNamed", "#00.golden"), - want: []byte("number double-zero here"), - }, { name: "", named: "sub-zero-one", file: filepath.Join( - "testdata", "TestGetNamed", "#01/sub-zero-one.golden", + "testdata", "TestGetP", "#00", "sub-zero-one.golden", ), want: []byte("number zero-one here"), }, @@ -331,7 +274,7 @@ func TestGetNamed(t *testing.T) { name: "foobar", named: "email", file: filepath.Join( - "testdata", "TestGetNamed", "foobar/email.golden", + "testdata", "TestGetP", "foobar", "email.golden", ), want: []byte("foobar email here"), }, @@ -339,7 +282,7 @@ func TestGetNamed(t *testing.T) { name: "foobar", named: "json", file: filepath.Join( - "testdata", "TestGetNamed", "foobar#01/json.golden", + "testdata", "TestGetP", "foobar#01", "json.golden", ), want: []byte("foobar json here"), }, @@ -347,7 +290,7 @@ func TestGetNamed(t *testing.T) { name: "foo/bar", named: "hello/world", file: filepath.Join( - "testdata", "TestGetNamed", + "testdata", "TestGetP", "foo", "bar", "hello", "world.golden", ), @@ -357,7 +300,7 @@ func TestGetNamed(t *testing.T) { name: "john's lost flip-flop", named: "left", file: filepath.Join( - "testdata", "TestGetNamed", "john's_lost_flip-flop", + "testdata", "TestGetP", "john's_lost_flip-flop", "left.golden", ), want: []byte("Did John lose his left flip-flop again?"), @@ -366,7 +309,7 @@ func TestGetNamed(t *testing.T) { name: "john's lost flip-flop", named: "right", file: filepath.Join( - "testdata", "TestGetNamed", "john's_lost_flip-flop#01", + "testdata", "TestGetP", "john's_lost_flip-flop#01", "right.golden", ), want: []byte("Did John lose his right flip-flop again?"), @@ -375,14 +318,14 @@ func TestGetNamed(t *testing.T) { name: "thing: it's", named: "a thing!", file: filepath.Join( - "testdata", "TestGetNamed", "thing:_it's", "a thing!.golden", + "testdata", "TestGetP", "thing__it's", "a_thing!.golden", ), want: []byte("A thing? Really? Are we getting lazy? :P"), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - f := NamedFile(t, tt.named) + f := FileP(t, tt.named) dir := filepath.Dir(f) err := os.MkdirAll(dir, 0o755) @@ -391,7 +334,7 @@ func TestGetNamed(t *testing.T) { err = ioutil.WriteFile(f, tt.want, 0o644) //nolint:gosec require.NoError(t, err) - got := GetNamed(t, tt.named) + got := GetP(t, tt.named) assert.Equal(t, filepath.FromSlash(tt.file), f) assert.Equal(t, tt.want, got) @@ -399,27 +342,17 @@ func TestGetNamed(t *testing.T) { } } -func TestSetNamed(t *testing.T) { +func TestSetP(t *testing.T) { t.Cleanup(func() { - err := os.RemoveAll(filepath.Join("testdata", t.Name())) - require.NoError(t, err) - err = os.Remove(filepath.Join("testdata", t.Name()+".golden")) + err := os.RemoveAll(filepath.Join("testdata", "TestSetP")) require.NoError(t, err) }) - content := []byte("This is the default golden file for TestSetNamed ^_^") - SetNamed(t, "", content) + content := []byte("This is the named golden file for TestSetP ^_^") + SetP(t, "sub-name", content) - b, err := ioutil.ReadFile(filepath.Join("testdata", "TestSetNamed.golden")) - require.NoError(t, err) - - assert.Equal(t, content, b) - - content = []byte("This is the named golden file for TestSetNamed ^_^") - SetNamed(t, "sub-name", content) - - b, err = ioutil.ReadFile( - filepath.Join("testdata", "TestSetNamed", "sub-name.golden"), + b, err := ioutil.ReadFile( + filepath.Join("testdata", "TestSetP", "sub-name.golden"), ) require.NoError(t, err) @@ -431,16 +364,11 @@ func TestSetNamed(t *testing.T) { file string content []byte }{ - { - name: "", - file: filepath.Join("testdata", "TestSetNamed", "#00.golden"), - content: []byte("number double-zero strikes again"), - }, { name: "", named: "sub-zero-one", file: filepath.Join( - "testdata", "TestSetNamed", "#01", "sub-zero-one.golden", + "testdata", "TestSetP", "#00", "sub-zero-one.golden", ), content: []byte("number zero-one sub-zero-one strikes again"), }, @@ -448,7 +376,7 @@ func TestSetNamed(t *testing.T) { name: "foobar", named: "email", file: filepath.Join( - "testdata", "TestSetNamed", "foobar", "email.golden", + "testdata", "TestSetP", "foobar", "email.golden", ), content: []byte("foobar here"), }, @@ -456,22 +384,15 @@ func TestSetNamed(t *testing.T) { name: "foobar", named: "json", file: filepath.Join( - "testdata", "TestSetNamed", "foobar#01", "json.golden", + "testdata", "TestSetP", "foobar#01", "json.golden", ), content: []byte("foobar here"), }, - { - name: "foo/bar", - file: filepath.Join( - "testdata", "TestSetNamed", "foo", "bar.golden", - ), - content: []byte("foo/bar style sub-sub-folders works too"), - }, { name: "john's lost flip-flop", named: "left", file: filepath.Join( - "testdata", "TestSetNamed", "john's_lost_flip-flop", + "testdata", "TestSetP", "john's_lost_flip-flop", "left.golden", ), content: []byte("Did John lose his left flip-flop again?"), @@ -480,7 +401,7 @@ func TestSetNamed(t *testing.T) { name: "john's lost flip-flop", named: "right", file: filepath.Join( - "testdata", "TestSetNamed", "john's_lost_flip-flop#01", + "testdata", "TestSetP", "john's_lost_flip-flop#01", "right.golden", ), content: []byte("Did John lose his right flip-flop again?"), @@ -489,16 +410,16 @@ func TestSetNamed(t *testing.T) { name: "thing: it's", named: "a thing!", file: filepath.Join( - "testdata", "TestSetNamed", "thing:_it's", "a thing!.golden", + "testdata", "TestSetP", "thing__it's", "a_thing!.golden", ), content: []byte("A thing? Really? Are we getting lazy? :P"), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - f := NamedFile(t, tt.named) + f := FileP(t, tt.named) - SetNamed(t, tt.named, tt.content) + SetP(t, tt.named, tt.content) got, err := ioutil.ReadFile(f) require.NoError(t, err) @@ -509,53 +430,14 @@ func TestSetNamed(t *testing.T) { } } -func TestNamedFile(t *testing.T) { - got := NamedFile(t, "") - assert.Equal(t, "testdata/TestNamedFile.golden", got) - - got = NamedFile(t, "sub-name") - assert.Equal(t, "testdata/TestNamedFile/sub-name.golden", got) - - tests := []struct { - name string - named string - want string - }{ - { - name: "", - named: "", - want: "testdata/TestNamedFile/#00.golden", - }, - { - name: "", - named: "sub-thing", - want: "testdata/TestNamedFile/#01/sub-thing.golden", - }, - { - name: "foobar", - want: "testdata/TestNamedFile/foobar.golden", - }, - { - name: "fozbaz", - named: "email", - want: "testdata/TestNamedFile/fozbaz/email.golden", - }, - { - name: "fozbaz", - named: "json", - want: "testdata/TestNamedFile/fozbaz#01/json.golden", - }, - { - name: "foo/bar", - named: "hello/world", - want: "testdata/TestNamedFile/foo/bar/hello/world.golden", - }, - } - for _, tt := range tests { +func TestUpdate(t *testing.T) { + for _, tt := range envUpdateFuncTestCases { t.Run(tt.name, func(t *testing.T) { - got := NamedFile(t, tt.named) + envctl.WithClean(tt.env, func() { + got := Update() - assert.Equal(t, tt.want, got) + assert.Equal(t, tt.want, got) + }) }) } } diff --git a/sanitize.go b/sanitize.go new file mode 100644 index 0000000..c77d741 --- /dev/null +++ b/sanitize.go @@ -0,0 +1,34 @@ +package golden + +import ( + "regexp" + "strings" +) + +var ( + whitespaceChars = regexp.MustCompile(`\s`) + illegalChars = regexp.MustCompile(`[\/\?<>\\:\*\|"]`) + controlChars = regexp.MustCompile(`[\x00-\x1f\x80-\x9f]`) + reservedNames = regexp.MustCompile(`^\.+$`) + winReserved = regexp.MustCompile( + `(?i)^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\..*)?$`, + ) +) + +func sanitizeFilename(name string) string { + if reservedNames.MatchString(name) || winReserved.MatchString(name) { + var b []byte + for i := 0; i < len(name); i++ { + b = append(b, byte('_')) + } + + return string(b) + } + + r := strings.TrimRight(name, ". ") + r = whitespaceChars.ReplaceAllString(r, "_") + r = illegalChars.ReplaceAllString(r, "_") + r = controlChars.ReplaceAllString(r, "_") + + return r +} diff --git a/sanitize_test.go b/sanitize_test.go new file mode 100644 index 0000000..c98b93b --- /dev/null +++ b/sanitize_test.go @@ -0,0 +1,124 @@ +package golden + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_sanitizeFilename(t *testing.T) { + tests := []struct { + name string + filename string + want string + }{ + { + name: "empty", + filename: "", + want: "", + }, + { + name: ".", + filename: ".", + want: "_", + }, + { + name: "..", + filename: "..", + want: "__", + }, + { + name: "...", + filename: "...", + want: "___", + }, + { + name: "clean", + filename: "foo-bar-nope.golden", + want: "foo-bar-nope.golden", + }, + { + name: "with spaces", + filename: "foo bar nope.golden", + want: "foo__bar_nope.golden", + }, + { + name: "illegal chars", + filename: `foo/?<>\:*|"bar.golden`, + want: "foo_________bar.golden", + }, + { + name: "control chars", + filename: "foo\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b" + + "\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a" + + "\x1b\x1c\x1d\x1e\x1fbar.golden", + want: "foo________________________________bar.golden", + }, + { + name: "trailing whitespace", + filename: "foobar.golden ", + want: "foobar.golden", + }, + { + name: "trailing dots", + filename: "foobar.golden......", + want: "foobar.golden", + }, + { + name: "trailing whitespace and dots", + filename: "foobar.golden .. .. .. ", + want: "foobar.golden", + }, + {name: "con", filename: "con", want: "___"}, + {name: "prn", filename: "prn", want: "___"}, + {name: "aux", filename: "aux", want: "___"}, + {name: "nul", filename: "nul", want: "___"}, + {name: "com1", filename: "com1", want: "____"}, + {name: "com2", filename: "com2", want: "____"}, + {name: "com3", filename: "com3", want: "____"}, + {name: "com4", filename: "com4", want: "____"}, + {name: "com5", filename: "com5", want: "____"}, + {name: "com6", filename: "com6", want: "____"}, + {name: "com7", filename: "com7", want: "____"}, + {name: "com8", filename: "com8", want: "____"}, + {name: "com9", filename: "com9", want: "____"}, + {name: "lpt1", filename: "lpt1", want: "____"}, + {name: "lpt2", filename: "lpt2", want: "____"}, + {name: "lpt3", filename: "lpt3", want: "____"}, + {name: "lpt4", filename: "lpt4", want: "____"}, + {name: "lpt5", filename: "lpt5", want: "____"}, + {name: "lpt6", filename: "lpt6", want: "____"}, + {name: "lpt7", filename: "lpt7", want: "____"}, + {name: "lpt8", filename: "lpt8", want: "____"}, + {name: "lpt9", filename: "lpt9", want: "____"}, + {name: "CON", filename: "CON", want: "___"}, + {name: "PRN", filename: "PRN", want: "___"}, + {name: "AUX", filename: "AUX", want: "___"}, + {name: "NUL", filename: "NUL", want: "___"}, + {name: "COM1", filename: "COM1", want: "____"}, + {name: "COM2", filename: "COM2", want: "____"}, + {name: "COM3", filename: "COM3", want: "____"}, + {name: "COM4", filename: "COM4", want: "____"}, + {name: "COM5", filename: "COM5", want: "____"}, + {name: "COM6", filename: "COM6", want: "____"}, + {name: "COM7", filename: "COM7", want: "____"}, + {name: "COM8", filename: "COM8", want: "____"}, + {name: "COM9", filename: "COM9", want: "____"}, + {name: "LPT1", filename: "LPT1", want: "____"}, + {name: "LPT2", filename: "LPT2", want: "____"}, + {name: "LPT3", filename: "LPT3", want: "____"}, + {name: "LPT4", filename: "LPT4", want: "____"}, + {name: "LPT5", filename: "LPT5", want: "____"}, + {name: "LPT6", filename: "LPT6", want: "____"}, + {name: "LPT7", filename: "LPT7", want: "____"}, + {name: "LPT8", filename: "LPT8", want: "____"}, + {name: "LPT9", filename: "LPT9", want: "____"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := sanitizeFilename(tt.filename) + + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/testdata/TestExampleMyStruct.golden b/testdata/TestExampleMyStruct.golden new file mode 100644 index 0000000..7129afe --- /dev/null +++ b/testdata/TestExampleMyStruct.golden @@ -0,0 +1 @@ +{"foo":"Bar"} \ No newline at end of file diff --git a/testdata/TestExampleMyStructP/json.golden b/testdata/TestExampleMyStructP/json.golden new file mode 100644 index 0000000..7129afe --- /dev/null +++ b/testdata/TestExampleMyStructP/json.golden @@ -0,0 +1 @@ +{"foo":"Bar"} \ No newline at end of file diff --git a/testdata/TestExampleMyStructP/xml.golden b/testdata/TestExampleMyStructP/xml.golden new file mode 100644 index 0000000..4e49261 --- /dev/null +++ b/testdata/TestExampleMyStructP/xml.golden @@ -0,0 +1 @@ +Bar \ No newline at end of file diff --git a/testdata/TestExampleMyStructTabular/empty_struct.golden b/testdata/TestExampleMyStructTabular/empty_struct.golden new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/testdata/TestExampleMyStructTabular/empty_struct.golden @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/testdata/TestExampleMyStructTabular/full_struct.golden b/testdata/TestExampleMyStructTabular/full_struct.golden new file mode 100644 index 0000000..7129afe --- /dev/null +++ b/testdata/TestExampleMyStructTabular/full_struct.golden @@ -0,0 +1 @@ +{"foo":"Bar"} \ No newline at end of file diff --git a/testdata/TestExampleMyStructTabularP/empty_struct/json.golden b/testdata/TestExampleMyStructTabularP/empty_struct/json.golden new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/testdata/TestExampleMyStructTabularP/empty_struct/json.golden @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/testdata/TestExampleMyStructTabularP/empty_struct/xml.golden b/testdata/TestExampleMyStructTabularP/empty_struct/xml.golden new file mode 100644 index 0000000..554a1bf --- /dev/null +++ b/testdata/TestExampleMyStructTabularP/empty_struct/xml.golden @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/testdata/TestExampleMyStructTabularP/full_struct/json.golden b/testdata/TestExampleMyStructTabularP/full_struct/json.golden new file mode 100644 index 0000000..7129afe --- /dev/null +++ b/testdata/TestExampleMyStructTabularP/full_struct/json.golden @@ -0,0 +1 @@ +{"foo":"Bar"} \ No newline at end of file diff --git a/testdata/TestExampleMyStructTabularP/full_struct/xml.golden b/testdata/TestExampleMyStructTabularP/full_struct/xml.golden new file mode 100644 index 0000000..4e49261 --- /dev/null +++ b/testdata/TestExampleMyStructTabularP/full_struct/xml.golden @@ -0,0 +1 @@ +Bar \ No newline at end of file diff --git a/updating.go b/update.go similarity index 54% rename from updating.go rename to update.go index 94dd6e2..e7d7ce9 100644 --- a/updating.go +++ b/update.go @@ -4,11 +4,14 @@ import "os" var truthyStrings = []string{"1", "y", "t", "yes", "on", "true"} -type UpdatingFunc func() bool +type UpdateFunc func() bool -// EnvVarUpdateFunc checks if the GOLDEN_UPDATE environment variable is set to +// EnvUpdateFunc checks if the GOLDEN_UPDATE environment variable is set to // one of "1", "y", "t", "yes", "on", or "true". -func EnvVarUpdatingFunc() bool { +// +// This is also the default UpdateFunc used to determine the return value of +// Update(). +func EnvUpdateFunc() bool { env := os.Getenv("GOLDEN_UPDATE") for _, v := range truthyStrings { if env == v { diff --git a/update_test.go b/update_test.go new file mode 100644 index 0000000..26937a7 --- /dev/null +++ b/update_test.go @@ -0,0 +1,106 @@ +package golden + +import ( + "testing" + + "github.com/jimeh/envctl" + "github.com/stretchr/testify/assert" +) + +var envUpdateFuncTestCases = []struct { + name string + env map[string]string + want bool +}{ + { + name: "GOLDEN_UPDATE not set", + want: false, + }, + { + name: "GOLDEN_UPDATE set to empty string", + env: map[string]string{"GOLDEN_UPDATE": ""}, + want: false, + }, + { + name: "GOLDEN_UPDATE set to 0", + env: map[string]string{"GOLDEN_UPDATE": "0"}, + want: false, + }, + { + name: "GOLDEN_UPDATE set to 1", + env: map[string]string{"GOLDEN_UPDATE": "1"}, + want: true, + }, + { + name: "GOLDEN_UPDATE set to 2", + env: map[string]string{"GOLDEN_UPDATE": "2"}, + want: false, + }, + { + name: "GOLDEN_UPDATE set to y", + env: map[string]string{"GOLDEN_UPDATE": "y"}, + want: true, + }, + { + name: "GOLDEN_UPDATE set to n", + env: map[string]string{"GOLDEN_UPDATE": "n"}, + want: false, + }, + { + name: "GOLDEN_UPDATE set to t", + env: map[string]string{"GOLDEN_UPDATE": "t"}, + want: true, + }, + { + name: "GOLDEN_UPDATE set to f", + env: map[string]string{"GOLDEN_UPDATE": "f"}, + want: false, + }, + { + name: "GOLDEN_UPDATE set to yes", + env: map[string]string{"GOLDEN_UPDATE": "yes"}, + want: true, + }, + { + name: "GOLDEN_UPDATE set to no", + env: map[string]string{"GOLDEN_UPDATE": "no"}, + want: false, + }, + { + name: "GOLDEN_UPDATE set to on", + env: map[string]string{"GOLDEN_UPDATE": "on"}, + want: true, + }, + { + name: "GOLDEN_UPDATE set to off", + env: map[string]string{"GOLDEN_UPDATE": "off"}, + want: false, + }, + { + name: "GOLDEN_UPDATE set to true", + env: map[string]string{"GOLDEN_UPDATE": "true"}, + want: true, + }, + { + name: "GOLDEN_UPDATE set to false", + env: map[string]string{"GOLDEN_UPDATE": "false"}, + want: false, + }, + { + name: "GOLDEN_UPDATE set to foobarnopebbq", + env: map[string]string{"GOLDEN_UPDATE": "foobarnopebbq"}, + want: false, + }, +} + +func TestEnvUpdateFunc(t *testing.T) { + for _, tt := range envUpdateFuncTestCases { + t.Run(tt.name, func(t *testing.T) { + envctl.WithClean(tt.env, func() { + got := EnvUpdateFunc() + + assert.Equal(t, tt.want, got) + }) + }) + } +}