diff --git a/.gitignore b/.gitignore index f65385f..98d92b5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ bin/* coverage.out -testdata/* -!testdata/TestExample* +testdata/TestFile* +testdata/TestGet* +testdata/TestSet* diff --git a/Makefile b/Makefile index 48c8b3c..7d8be79 100644 --- a/Makefile +++ b/Makefile @@ -51,6 +51,7 @@ $(eval $(call tool,gofumpt,mvdan.cc/gofumpt)) $(eval $(call tool,goimports,golang.org/x/tools/cmd/goimports)) $(eval $(call tool,golangci-lint,github.com/golangci/golangci-lint/cmd/golangci-lint@v1.44)) $(eval $(call tool,gomod,github.com/Helcaraxan/gomod)) +$(eval $(call tool,mockgen,mockgen,github.com/golang/mock/mockgen@v1.6.0)) .PHONY: tools tools: $(TOOLS) diff --git a/go.mod b/go.mod index 4da1858..3d5c7dd 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/jimeh/go-golden go 1.15 require ( + github.com/golang/mock v1.6.0 github.com/jimeh/envctl v0.1.0 github.com/jimeh/go-mocktesting v0.1.0 github.com/spf13/afero v1.6.0 diff --git a/go.sum b/go.sum index 33c0728..1ba3e2d 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/jimeh/envctl v0.1.0 h1:KTv3D+pi5M4/PgFVE/W8ssWqiZP3pDJ8Cga50L+1avo= github.com/jimeh/envctl v0.1.0/go.mod h1:aM27ffBbO1yUBKUzgJGCUorS4z+wyh+qhQe1ruxXZZo= github.com/jimeh/go-mocktesting v0.1.0 h1:y0tLABo3V4i9io7m6TiXdXbU3IVMjtPvWkr+A0+aLTM= @@ -16,15 +18,31 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/golden.go b/golden.go index 6f4e253..3ce2b8f 100644 --- a/golden.go +++ b/golden.go @@ -2,124 +2,120 @@ // 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 +// names by calling t.Name(). File names are sanitized to ensure they are // compatible with Linux, macOS and Windows systems regardless of what // characters might be in a subtest's name. // -// Usage +// # Usage // // Typical usage should look something like this: // -// func TestExampleMyStruct(t *testing.T) { -// got, err := json.Marshal(&MyStruct{Foo: "Bar"}) -// require.NoError(t, err) +// 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) +// want := golden.Do(t, got) // -// assert.Equal(t, want, got) -// } +// assert.Equal(t, want, got) +// } // -// The above example will read/write to: +// The above example will attempt to read/write to: +// +// testdata/TestExampleMyStruct.golden // -// testdata/TestExampleMyStruct.golden +// The call to golden.Do() is equivalent to: +// +// if golden.Update() { +// golden.Set(t, got) +// } +// want := golden.Get(t) // // 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 +// # 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) -// }) -// } -// } +// 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) +// +// want := golden.Do(t, got) +// +// assert.Equal(t, want, got) +// }) +// } +// } // // The above example will read/write to: // -// testdata/TestExampleMyStructTabular/empty_struct.golden -// testdata/TestExampleMyStructTabular/full_struct.golden +// testdata/TestExampleMyStructTabular/empty_struct.golden +// testdata/TestExampleMyStructTabular/full_struct.golden // -// Multiple Golden Files in a Single Test +// # Multiple Golden Files in a Single Test // -// The "P" suffixed methods, GetP(), SetP(), and FileP(), all take a name +// The "P" suffixed methods, GetP(), SetP(), DoP(), 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"}) +// 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) -// } +// wantJSON := golden.DoP(t, "json", gotJSON) +// wantXML := golden.DoP(t, "xml", gotXML) // -// assert.Equal(t, golden.GetP(t, "json"), gotJSON) -// assert.Equal(t, golden.GetP(t, "xml"), gotXML) -// } +// assert.Equal(t, wantJSON, gotJSON) +// assert.Equal(t, wantXML, gotXML) +// } // // The above example will read/write to: // -// testdata/TestExampleMyStructP/json.golden -// testdata/TestExampleMyStructP/xml.golden +// 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) -// }) -// } -// } +// 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) +// +// wantJSON := golden.DoP(t, "json", gotJSON) +// wantXML := golden.DoP(t, "xml", gotXML) +// +// assert.Equal(t, wantJSON, gotJSON) +// assert.Equal(t, wantXML, 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 -// +// 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 ( @@ -132,12 +128,11 @@ import ( "github.com/spf13/afero" ) +//go:generate mockgen -source=golden.go -destination=golden_mock_test.go -package golden -self_package github.com/jimeh/go-golden + // TestingT is a interface describing a sub-set of methods of *testing.T which // golden uses. type TestingT interface { - Error(args ...interface{}) - Errorf(format string, args ...interface{}) - FailNow() Fatal(args ...interface{}) Fatalf(format string, args ...interface{}) Helper() @@ -146,23 +141,25 @@ type TestingT interface { Name() string } -var defaultGolden = New() +// Default is the default Golden instance used by all top-level package +// functions. +var Default = 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 { +func File(t TestingT) string { t.Helper() - return defaultGolden.File(t) + return Default.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 { +func Get(t TestingT) []byte { t.Helper() - return defaultGolden.Get(t) + return Default.Get(t) } // Set writes given data to the golden file for the given *testing.T instance as @@ -171,15 +168,24 @@ func Get(t *testing.T) []byte { func Set(t *testing.T, data []byte) { t.Helper() - defaultGolden.Set(t, data) + Default.Set(t, data) +} + +// Do is a convenience function for calling Update(), Set(), and Get() in a +// single call. If Update() returns true, data will be written to the golden +// file using Set(), before reading it back with Get(). +func Do(t TestingT, data []byte) []byte { + t.Helper() + + return Default.Do(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 { +func FileP(t TestingT, name string) string { t.Helper() - return defaultGolden.FileP(t, name) + return Default.FileP(t, name) } // GetP returns the content of the specifically named golden file belonging @@ -191,7 +197,7 @@ func FileP(t *testing.T, name string) string { func GetP(t *testing.T, name string) []byte { t.Helper() - return defaultGolden.GetP(t, name) + return Default.GetP(t, name) } // SetP writes given data of the specifically named golden file belonging to @@ -203,7 +209,16 @@ func GetP(t *testing.T, name string) []byte { func SetP(t *testing.T, name string, data []byte) { t.Helper() - defaultGolden.SetP(t, name, data) + Default.SetP(t, name, data) +} + +// DoP is a convenience function for calling Update(), SetP(), and GetP() in a +// single call. If Update() returns true, data will be written to the golden +// file using SetP(), before reading it back with GetP(). +func DoP(t TestingT, name string, data []byte) []byte { + t.Helper() + + return Default.DoP(t, name, data) } // Update returns true when golden is set to update golden files. Should be used @@ -213,7 +228,7 @@ func SetP(t *testing.T, name string, data []byte) { // 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 defaultGolden.Update() + return Default.Update() } // Golden handles all interactions with golden files. The top-level package @@ -233,6 +248,11 @@ type Golden interface { // test by calling t.Fatal() with error details. Set(t TestingT, data []byte) + // Do is a convenience function for calling Update(), Set(), and Get() in a + // single call. If Update() returns true, data will be written to the golden + // file using Set(), before reading it back with Get(). + Do(t TestingT, data []byte) []byte + // FileP returns the filename of the specifically named golden file for the // given TestingT instance as determined by t.Name(). FileP(t TestingT, name string) string @@ -253,6 +273,11 @@ type Golden interface { // files to be used within the same one TestingT instance. SetP(t TestingT, name string, data []byte) + // DoP is a convenience function for calling Update(), SetP(), and GetP() in + // a single call. If Update() returns true, data will be written to the + // golden file using SetP(), before reading it back with GetP(). + DoP(t TestingT, name string, data []byte) []byte + // Update returns true when golden is set to update golden files. Should be // used to determine if golden.Set() or golden.SetP() should be called or // not. @@ -342,7 +367,8 @@ func WithUpdateFunc(fn UpdateFunc) Option { }) } -// WithFs sets s afero.Fs instance which is used to read/write all golden files. +// WithFs sets the afero.Fs instance which is used for all file system +// operations to read/write golden files. // // When this option is not provided, the default value is afero.NewOsFs(). func WithFs(fs afero.Fs) Option { @@ -394,55 +420,75 @@ type golden struct { // Ensure golden satisfies Golden interface. var _ Golden = &golden{} -func (s *golden) File(t TestingT) string { +func (g *golden) File(t TestingT) string { t.Helper() - return s.file(t, "") + return g.file(t, "") } -func (s *golden) Get(t TestingT) []byte { +func (g *golden) Get(t TestingT) []byte { t.Helper() - return s.get(t, "") + return g.get(t, "") } -func (s *golden) Set(t TestingT, data []byte) { +func (g *golden) Set(t TestingT, data []byte) { t.Helper() - s.set(t, "", data) + g.set(t, "", data) +} + +func (g *golden) Do(t TestingT, data []byte) []byte { + t.Helper() + + if g.Update() { + g.Set(t, data) + } + + return g.Get(t) } -func (s *golden) FileP(t TestingT, name string) string { +func (g *golden) FileP(t TestingT, name string) string { t.Helper() if name == "" { t.Fatalf("golden: test name cannot be empty") } - return s.file(t, name) + return g.file(t, name) } -func (s *golden) GetP(t TestingT, name string) []byte { +func (g *golden) GetP(t TestingT, name string) []byte { t.Helper() if name == "" { t.Fatal("golden: name cannot be empty") } - return s.get(t, name) + return g.get(t, name) } -func (s *golden) SetP(t TestingT, name string, data []byte) { +func (g *golden) SetP(t TestingT, name string, data []byte) { t.Helper() if name == "" { t.Fatal("golden: name cannot be empty") } - s.set(t, name, data) + g.set(t, name, data) +} + +func (g *golden) DoP(t TestingT, name string, data []byte) []byte { + t.Helper() + + if g.Update() { + g.SetP(t, name, data) + } + + return g.GetP(t, name) } -func (s *golden) file(t TestingT, name string) string { +func (g *golden) file(t TestingT, name string) string { t.Helper() if t.Name() == "" { @@ -451,12 +497,12 @@ func (s *golden) file(t TestingT, name string) string { ) } - base := []string{s.dirname, filepath.FromSlash(t.Name())} + base := []string{g.dirname, filepath.FromSlash(t.Name())} if name != "" { base = append(base, name) } - f := filepath.Clean(filepath.Join(base...) + s.suffix) + f := filepath.Clean(filepath.Join(base...) + g.suffix) dirty := strings.Split(f, string(os.PathSeparator)) clean := make([]string, 0, len(dirty)) @@ -467,12 +513,12 @@ func (s *golden) file(t TestingT, name string) string { return strings.Join(clean, string(os.PathSeparator)) } -func (s *golden) get(t TestingT, name string) []byte { +func (g *golden) get(t TestingT, name string) []byte { t.Helper() - f := s.file(t, name) + f := g.file(t, name) - b, err := afero.ReadFile(s.fs, f) + b, err := afero.ReadFile(g.fs, f) if err != nil { t.Fatalf("golden: %s", err.Error()) } @@ -480,27 +526,27 @@ func (s *golden) get(t TestingT, name string) []byte { return b } -func (s *golden) set(t TestingT, name string, data []byte) { +func (g *golden) set(t TestingT, name string, data []byte) { t.Helper() - f := s.file(t, name) + f := g.file(t, name) dir := filepath.Dir(f) - if s.logOnWrite { + if g.logOnWrite { t.Logf("golden: writing golden file: %s", f) } - err := s.fs.MkdirAll(dir, s.dirMode) + err := g.fs.MkdirAll(dir, g.dirMode) if err != nil { t.Fatalf("golden: failed to create directory: %s", err.Error()) } - err = afero.WriteFile(s.fs, f, data, s.fileMode) + err = afero.WriteFile(g.fs, f, data, g.fileMode) if err != nil { t.Fatalf("golden: filed to write file: %s", err.Error()) } } -func (s *golden) Update() bool { - return s.updateFunc() +func (g *golden) Update() bool { + return g.updateFunc() } diff --git a/golden_mock_test.go b/golden_mock_test.go new file mode 100644 index 0000000..0b7b82d --- /dev/null +++ b/golden_mock_test.go @@ -0,0 +1,306 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: golden.go + +// Package golden is a generated GoMock package. +package golden + +import ( + reflect "reflect" + + gomock "github.com/golang/mock/gomock" +) + +// MockTestingT is a mock of TestingT interface. +type MockTestingT struct { + ctrl *gomock.Controller + recorder *MockTestingTMockRecorder +} + +// MockTestingTMockRecorder is the mock recorder for MockTestingT. +type MockTestingTMockRecorder struct { + mock *MockTestingT +} + +// NewMockTestingT creates a new mock instance. +func NewMockTestingT(ctrl *gomock.Controller) *MockTestingT { + mock := &MockTestingT{ctrl: ctrl} + mock.recorder = &MockTestingTMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockTestingT) EXPECT() *MockTestingTMockRecorder { + return m.recorder +} + +// Fatal mocks base method. +func (m *MockTestingT) Fatal(args ...interface{}) { + m.ctrl.T.Helper() + varargs := []interface{}{} + for _, a := range args { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "Fatal", varargs...) +} + +// Fatal indicates an expected call of Fatal. +func (mr *MockTestingTMockRecorder) Fatal(args ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Fatal", reflect.TypeOf((*MockTestingT)(nil).Fatal), args...) +} + +// Fatalf mocks base method. +func (m *MockTestingT) Fatalf(format string, args ...interface{}) { + m.ctrl.T.Helper() + varargs := []interface{}{format} + for _, a := range args { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "Fatalf", varargs...) +} + +// Fatalf indicates an expected call of Fatalf. +func (mr *MockTestingTMockRecorder) Fatalf(format interface{}, args ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{format}, args...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Fatalf", reflect.TypeOf((*MockTestingT)(nil).Fatalf), varargs...) +} + +// Helper mocks base method. +func (m *MockTestingT) Helper() { + m.ctrl.T.Helper() + m.ctrl.Call(m, "Helper") +} + +// Helper indicates an expected call of Helper. +func (mr *MockTestingTMockRecorder) Helper() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Helper", reflect.TypeOf((*MockTestingT)(nil).Helper)) +} + +// Log mocks base method. +func (m *MockTestingT) Log(args ...interface{}) { + m.ctrl.T.Helper() + varargs := []interface{}{} + for _, a := range args { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "Log", varargs...) +} + +// Log indicates an expected call of Log. +func (mr *MockTestingTMockRecorder) Log(args ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Log", reflect.TypeOf((*MockTestingT)(nil).Log), args...) +} + +// Logf mocks base method. +func (m *MockTestingT) Logf(format string, args ...interface{}) { + m.ctrl.T.Helper() + varargs := []interface{}{format} + for _, a := range args { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "Logf", varargs...) +} + +// Logf indicates an expected call of Logf. +func (mr *MockTestingTMockRecorder) Logf(format interface{}, args ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{format}, args...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Logf", reflect.TypeOf((*MockTestingT)(nil).Logf), varargs...) +} + +// Name mocks base method. +func (m *MockTestingT) Name() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Name") + ret0, _ := ret[0].(string) + return ret0 +} + +// Name indicates an expected call of Name. +func (mr *MockTestingTMockRecorder) Name() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Name", reflect.TypeOf((*MockTestingT)(nil).Name)) +} + +// MockGolden is a mock of Golden interface. +type MockGolden struct { + ctrl *gomock.Controller + recorder *MockGoldenMockRecorder +} + +// MockGoldenMockRecorder is the mock recorder for MockGolden. +type MockGoldenMockRecorder struct { + mock *MockGolden +} + +// NewMockGolden creates a new mock instance. +func NewMockGolden(ctrl *gomock.Controller) *MockGolden { + mock := &MockGolden{ctrl: ctrl} + mock.recorder = &MockGoldenMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockGolden) EXPECT() *MockGoldenMockRecorder { + return m.recorder +} + +// Do mocks base method. +func (m *MockGolden) Do(t TestingT, data []byte) []byte { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Do", t, data) + ret0, _ := ret[0].([]byte) + return ret0 +} + +// Do indicates an expected call of Do. +func (mr *MockGoldenMockRecorder) Do(t, data interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Do", reflect.TypeOf((*MockGolden)(nil).Do), t, data) +} + +// DoP mocks base method. +func (m *MockGolden) DoP(t TestingT, name string, data []byte) []byte { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DoP", t, name, data) + ret0, _ := ret[0].([]byte) + return ret0 +} + +// DoP indicates an expected call of DoP. +func (mr *MockGoldenMockRecorder) DoP(t, name, data interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DoP", reflect.TypeOf((*MockGolden)(nil).DoP), t, name, data) +} + +// File mocks base method. +func (m *MockGolden) File(t TestingT) string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "File", t) + ret0, _ := ret[0].(string) + return ret0 +} + +// File indicates an expected call of File. +func (mr *MockGoldenMockRecorder) File(t interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "File", reflect.TypeOf((*MockGolden)(nil).File), t) +} + +// FileP mocks base method. +func (m *MockGolden) FileP(t TestingT, name string) string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FileP", t, name) + ret0, _ := ret[0].(string) + return ret0 +} + +// FileP indicates an expected call of FileP. +func (mr *MockGoldenMockRecorder) FileP(t, name interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FileP", reflect.TypeOf((*MockGolden)(nil).FileP), t, name) +} + +// Get mocks base method. +func (m *MockGolden) Get(t TestingT) []byte { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", t) + ret0, _ := ret[0].([]byte) + return ret0 +} + +// Get indicates an expected call of Get. +func (mr *MockGoldenMockRecorder) Get(t interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockGolden)(nil).Get), t) +} + +// GetP mocks base method. +func (m *MockGolden) GetP(t TestingT, name string) []byte { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetP", t, name) + ret0, _ := ret[0].([]byte) + return ret0 +} + +// GetP indicates an expected call of GetP. +func (mr *MockGoldenMockRecorder) GetP(t, name interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetP", reflect.TypeOf((*MockGolden)(nil).GetP), t, name) +} + +// Set mocks base method. +func (m *MockGolden) Set(t TestingT, data []byte) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "Set", t, data) +} + +// Set indicates an expected call of Set. +func (mr *MockGoldenMockRecorder) Set(t, data interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Set", reflect.TypeOf((*MockGolden)(nil).Set), t, data) +} + +// SetP mocks base method. +func (m *MockGolden) SetP(t TestingT, name string, data []byte) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetP", t, name, data) +} + +// SetP indicates an expected call of SetP. +func (mr *MockGoldenMockRecorder) SetP(t, name, data interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetP", reflect.TypeOf((*MockGolden)(nil).SetP), t, name, data) +} + +// Update mocks base method. +func (m *MockGolden) Update() bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Update") + ret0, _ := ret[0].(bool) + return ret0 +} + +// Update indicates an expected call of Update. +func (mr *MockGoldenMockRecorder) Update() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockGolden)(nil).Update)) +} + +// MockOption is a mock of Option interface. +type MockOption struct { + ctrl *gomock.Controller + recorder *MockOptionMockRecorder +} + +// MockOptionMockRecorder is the mock recorder for MockOption. +type MockOptionMockRecorder struct { + mock *MockOption +} + +// NewMockOption creates a new mock instance. +func NewMockOption(ctrl *gomock.Controller) *MockOption { + mock := &MockOption{ctrl: ctrl} + mock.recorder = &MockOptionMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockOption) EXPECT() *MockOptionMockRecorder { + return m.recorder +} + +// apply mocks base method. +func (m *MockOption) apply(arg0 *golden) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "apply", arg0) +} + +// apply indicates an expected call of apply. +func (mr *MockOptionMockRecorder) apply(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "apply", reflect.TypeOf((*MockOption)(nil).apply), arg0) +} diff --git a/golden_test.go b/golden_test.go index b78d2e2..5b51d85 100644 --- a/golden_test.go +++ b/golden_test.go @@ -1,6 +1,7 @@ package golden import ( + "io/fs" "io/ioutil" "os" "path/filepath" @@ -8,6 +9,7 @@ import ( "runtime" "testing" + "github.com/golang/mock/gomock" "github.com/jimeh/envctl" "github.com/jimeh/go-mocktesting" "github.com/spf13/afero" @@ -19,113 +21,68 @@ func stringPtr(s string) *string { return &s } -func TestFile(t *testing.T) { - got := File(t) - - assert.Equal(t, filepath.Join("testdata", "TestFile.golden"), got) +func funcID(f interface{}) string { + return runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name() +} - tests := []struct { - name string - want string - }{ - { - name: "", - want: filepath.Join("testdata", "TestFile", "#00.golden"), - }, - { - name: "foobar", - want: filepath.Join("testdata", "TestFile", "foobar.golden"), - }, - { - name: "foo/bar", - want: filepath.Join("testdata", "TestFile", "foo", "bar.golden"), - }, - { - name: `"foobar"`, - want: filepath.Join("testdata", "TestFile", "_foobar_.golden"), - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := File(t) +func setupDefaultMock( + t *testing.T, + ctrl *gomock.Controller, +) (*MockTestingT, *MockGolden) { + t.Helper() - assert.Equal(t, tt.want, got) - }) - } -} + mt := NewMockTestingT(ctrl) + mg := NewMockGolden(ctrl) -func TestGet(t *testing.T) { + originalDefault := Default + Default = mg t.Cleanup(func() { - err := os.RemoveAll(filepath.Join("testdata", "TestGet")) - require.NoError(t, err) - err = os.Remove(filepath.Join("testdata", "TestGet.golden")) - require.NoError(t, err) + Default = originalDefault }) - err := os.MkdirAll("testdata", 0o755) - require.NoError(t, err) + return mt, mg +} - content := []byte("foobar\nhello world :)") - err = ioutil.WriteFile( //nolint:gosec - filepath.Join("testdata", "TestGet.golden"), content, 0o644, - ) - require.NoError(t, err) +func TestDefault(t *testing.T) { + require.IsType(t, &golden{}, Default) - got := Get(t) - assert.Equal(t, content, got) + dg := Default.(*golden) - tests := []struct { - name string - file string - want []byte - }{ - { - name: "", - file: filepath.Join("testdata", "TestGet", "#00.golden"), - want: []byte("number double-zero here"), - }, - { - name: "foobar", - file: filepath.Join("testdata", "TestGet", "foobar.golden"), - want: []byte("foobar here"), - }, - { - name: "foo/bar", - file: filepath.Join("testdata", "TestGet", "foo", "bar.golden"), - want: []byte("foo/bar style sub-sub-folders works too"), - }, - { - name: "john's lost flip-flop", - file: filepath.Join( - "testdata", "TestGet", "john's_lost_flip-flop.golden", - ), - want: []byte("Did John lose his flip-flop again?"), - }, - { - name: "thing: it's a thing!", - file: filepath.Join( - "testdata", "TestGet", "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 := File(t) - dir := filepath.Dir(f) + assert.Equal(t, fs.FileMode(0o755), dg.dirMode) + assert.Equal(t, fs.FileMode(0o644), dg.fileMode) + assert.Equal(t, ".golden", dg.suffix) + assert.Equal(t, "testdata", dg.dirname) + assert.Equal(t, funcID(EnvUpdateFunc), funcID(dg.updateFunc)) + assert.Equal(t, afero.NewOsFs(), dg.fs) + assert.Equal(t, true, dg.logOnWrite) +} - err := os.MkdirAll(dir, 0o755) - require.NoError(t, err) +func TestFile(t *testing.T) { + ctrl := gomock.NewController(t) + mt, mg := setupDefaultMock(t, ctrl) - err = ioutil.WriteFile(f, tt.want, 0o644) //nolint:gosec - require.NoError(t, err) + want := filepath.Join("testdata", t.Name()+".golden") - got := Get(t) + mt.EXPECT().Helper() + mg.EXPECT().File(mt).Return(want) - assert.Equal(t, tt.file, f) - assert.Equal(t, tt.want, got) - }) - } + got := File(mt) + + assert.Equal(t, want, got) +} + +func TestGet(t *testing.T) { + ctrl := gomock.NewController(t) + mt, mg := setupDefaultMock(t, ctrl) + + want := []byte("foobar\nhello world :)") + + mt.EXPECT().Helper() + mg.EXPECT().Get(mt).Return(want) + + got := Get(mt) + + assert.Equal(t, want, got) } func TestSet(t *testing.T) { @@ -196,54 +153,17 @@ func TestSet(t *testing.T) { } func TestFileP(t *testing.T) { - got := FileP(t, "sub-name") - assert.Equal(t, - filepath.Join("testdata", "TestFileP", "sub-name.golden"), got, - ) + ctrl := gomock.NewController(t) + mt, mg := setupDefaultMock(t, ctrl) - 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) + want := filepath.Join("testdata", t.Name(), "foobar.golden") - assert.Equal(t, tt.want, got) - }) - } + mt.EXPECT().Helper() + mg.EXPECT().FileP(mt, "foobar").Return(want) + + got := FileP(mt, "foobar") + + assert.Equal(t, want, got) } func TestGetP(t *testing.T) { @@ -506,19 +426,12 @@ func TestNew(t *testing.T) { got, ok := g.(*golden) require.True(t, ok, "New did not returns a *golden instance") - gotUpdateFunc := runtime.FuncForPC( - reflect.ValueOf(got.updateFunc).Pointer(), - ).Name() - wantUpdateFunc := runtime.FuncForPC( - reflect.ValueOf(tt.want.updateFunc).Pointer(), - ).Name() - assert.Equal(t, tt.want.dirMode, got.dirMode) assert.Equal(t, tt.want.fileMode, got.fileMode) assert.Equal(t, tt.want.suffix, got.suffix) assert.Equal(t, tt.want.dirname, got.dirname) assert.Equal(t, tt.want.logOnWrite, got.logOnWrite) - assert.Equal(t, wantUpdateFunc, gotUpdateFunc) + assert.Equal(t, funcID(tt.want.updateFunc), funcID(got.updateFunc)) assert.IsType(t, tt.want.fs, got.fs) }) } diff --git a/update.go b/update.go index d655613..10e771b 100644 --- a/update.go +++ b/update.go @@ -1,6 +1,7 @@ package golden import ( + "flag" "os" "strings" ) @@ -24,3 +25,22 @@ func EnvUpdateFunc() bool { return false } + +var ( + updateFlagSet *flag.FlagSet + updateFlag bool +) + +// UpdateFunc returns a function that checks a -update flag is set. +func FlagUpdateFunc() bool { + if updateFlagSet == nil { + updateFlagSet = flag.NewFlagSet("golden", flag.ContinueOnError) + updateFlagSet.BoolVar(&updateFlag, + "update", false, "update golden files", + ) + } + + _ = updateFlagSet.Parse(os.Args[1:]) + + return false +}