diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b8a1393..ad6a567 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,6 +32,32 @@ jobs: - name: Check if mods are tidy run: make check-tidy + cov: + name: Coverage + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-go@v2 + with: + go-version: 1.15 + - uses: actions/cache@v2 + with: + path: ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + - name: Publish coverage + uses: paambaati/codeclimate-action@v2.7.4 + env: + VERBOSE: "true" + GOMAXPROCS: 4 + CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} + with: + coverageCommand: make cov + prefix: github.com/${{ github.repository }} + coverageLocations: | + ${{ github.workspace }}/coverage.out:gocov + test: name: Test strategy: 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/README.md b/README.md index 12ab269..d78ae36 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,80 @@ -# go-golden +

+ go-golden +

-Yet another Go package for working with `*.golden` test files. +

+ + Yet another Go package for working with `*.golden` test files, with a focus + on simplicity through it's default behavior. + +

-Currently a work in progress. +

+ + Go Reference + + + Actions Status + + + Coverage + + + + + + GitHub pull requests + + + License Status + +

+ +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. + +## Import + +``` +import "github.com/jimeh/go-golden" +``` + +## Usage + +Typical usage should look something like this: + +```go +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. + +## Documentation + +Please see the +[Go Reference](https://pkg.go.dev/github.com/jimeh/go-golden#section-documentation) +for documentation and examples. + +## License + +[MIT](https://github.com/jimeh/go-golden/blob/master/LICENSE) 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) + }) + }) + } +}