diff --git a/.golangci.yml b/.golangci.yml index bee01fe..9d6ab37 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -25,7 +25,6 @@ linters: - asciicheck - bodyclose - deadcode - - depguard - durationcheck - errcheck - errorlint @@ -69,9 +68,6 @@ linters: - whitespace issues: - exclude: - - Using the variable on range scope `tt` in function literal - - Using the variable on range scope `tc` in function literal exclude-rules: - path: "_test\\.go" linters: diff --git a/Makefile b/Makefile index 7d8be79..9359274 100644 --- a/Makefile +++ b/Makefile @@ -34,24 +34,19 @@ SHELL := env \ # Tools # -TOOLS += $(TOOLDIR)/gobin -$(TOOLDIR)/gobin: - GO111MODULE=off go get -u github.com/myitcv/gobin - # external tool define tool # 1: binary-name, 2: go-import-path TOOLS += $(TOOLDIR)/$(1) -$(TOOLDIR)/$(1): $(TOOLDIR)/gobin Makefile - gobin $(V) "$(2)" +$(TOOLDIR)/$(1): Makefile + GOBIN="$(CURDIR)/$(TOOLDIR)" go install "$(2)" endef -$(eval $(call tool,godoc,golang.org/x/tools/cmd/godoc)) -$(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)) +$(eval $(call tool,godoc,golang.org/x/tools/cmd/godoc@latest)) +$(eval $(call tool,gofumpt,mvdan.cc/gofumpt@latest)) +$(eval $(call tool,goimports,golang.org/x/tools/cmd/goimports@latest)) +$(eval $(call tool,golangci-lint,github.com/golangci/golangci-lint/cmd/golangci-lint@v1.56)) +$(eval $(call tool,gomod,github.com/Helcaraxan/gomod@latest)) .PHONY: tools tools: $(TOOLS) diff --git a/fs.go b/fs.go new file mode 100644 index 0000000..442b99e --- /dev/null +++ b/fs.go @@ -0,0 +1,46 @@ +package golden + +import "os" + +type FS interface { + // MkdirAll creates a directory named path, along with any necessary + // parents, and returns nil, or else returns an error. The permission bits + // perm (before umask) are used for all directories that MkdirAll creates. + MkdirAll(path string, perm os.FileMode) error + + // ReadFile reads the named file and returns the contents. A successful call + // returns err == nil, not err == EOF. Because ReadFile reads the whole + // file, it does not treat an EOF from Read as an error to be reported. + ReadFile(filename string) ([]byte, error) + + // WriteFile writes data to a file named by filename. If the file does not + // exist, WriteFile creates it with permissions perm; otherwise WriteFile + // truncates it before writing, without changing permissions. + WriteFile(name string, data []byte, perm os.FileMode) error +} + +type fsImpl struct{} + +var _ FS = fsImpl{} + +// NewFS returns a new FS instance which operates against the host file system +// via calls to functions in the os package. +func NewFS() FS { + return fsImpl{} +} + +// DefaultFS is the default FS instance used by all top-level package functions, +// including the Default Golden instance, and also the New function. +var DefaultFS = NewFS() + +func (fsImpl) MkdirAll(path string, perm os.FileMode) error { + return os.MkdirAll(path, perm) +} + +func (fsImpl) ReadFile(filename string) ([]byte, error) { + return os.ReadFile(filename) +} + +func (fsImpl) WriteFile(filename string, data []byte, perm os.FileMode) error { + return os.WriteFile(filename, data, perm) +} diff --git a/fs_test.go b/fs_test.go new file mode 100644 index 0000000..273da49 --- /dev/null +++ b/fs_test.go @@ -0,0 +1,131 @@ +package golden + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestMkdirAll(t *testing.T) { + tempDir := t.TempDir() + + tests := []struct { + name string + path string + perm os.FileMode + wantErr bool + }{ + {"create new dir", "newdir", 0o755, false}, + {"create nested dirs", "nested/dir/structure", 0o755, false}, + {"invalid path", string([]byte{0, 0}), 0o755, true}, + } + + fs := NewFS() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + path := filepath.Join(tempDir, tt.path) + err := fs.MkdirAll(path, tt.perm) + + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + _, err := os.Stat(path) + assert.NoError(t, err) + } + }) + } +} + +func TestReadFile(t *testing.T) { + tempDir := t.TempDir() + + sampleFilePath := filepath.Join(tempDir, "sample.txt") + sampleContent := []byte("Hello, world!") + err := os.WriteFile(sampleFilePath, sampleContent, 0o600) + require.NoError(t, err) + + tests := []struct { + name string + filename string + want []byte + wantErr bool + }{ + {"read existing file", sampleFilePath, sampleContent, false}, + {"file does not exist", "nonexistent.txt", nil, true}, + } + + fs := NewFS() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := fs.ReadFile(tt.filename) + + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, string(tt.want), string(got)) + } + }) + } +} + +func TestWriteFile(t *testing.T) { + tempDir := t.TempDir() + + tests := []struct { + name string + filename string + data []byte + perm os.FileMode + wantErr bool + }{ + { + "write to new file", + "newfile.txt", + []byte("new content"), + 0o644, + false, + }, + { + "overwrite existing file", + "existing.txt", + []byte("overwritten content"), + 0o644, + false, + }, + { + "invalid filename", + string([]byte{0, 0}), + []byte("invalid filename"), + 0o644, + true, + }, + { + "non-existent directory", + "nonexistentdir/newfile.txt", + []byte("this will fail"), + 0o644, + true, + }, + } + + fs := NewFS() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + filePath := filepath.Join(tempDir, tt.filename) + err := fs.WriteFile(filePath, tt.data, tt.perm) + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + content, err := os.ReadFile(filePath) + assert.NoError(t, err) + assert.Equal(t, tt.data, content) + } + }) + } +} diff --git a/go.mod b/go.mod index 3d5c7dd..76fdeca 100644 --- a/go.mod +++ b/go.mod @@ -1,12 +1,14 @@ module github.com/jimeh/go-golden -go 1.15 +go 1.18 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 - github.com/stretchr/testify v1.7.0 - gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect + github.com/stretchr/testify v1.9.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 1ba3e2d..27652b7 100644 --- a/go.sum +++ b/go.sum @@ -1,51 +1,16 @@ -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/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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= -github.com/jimeh/go-mocktesting v0.1.0/go.mod h1:xnekQ6yP/ull2ewkOp1CbgH7Dym7nbKa/t96XWrIiH8= -github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/spf13/afero v1.6.0 h1:xoax2sJ2DT8S8xA2paPFjDCScCNeWsg75VG0DLRreiY= -github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 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= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 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= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/gold.go b/gold.go new file mode 100644 index 0000000..10739c9 --- /dev/null +++ b/gold.go @@ -0,0 +1,173 @@ +package golden + +import ( + "os" + "path/filepath" + "strings" + + "github.com/jimeh/go-golden/sanitize" +) + +// gold is the underlying struct that implements the Golden interface. +type gold 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 be "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 + + // fs is used for all file system operations. This enables providing custom + // afero.fs instances which can be useful for testing purposes. + fs FS + + // logOnWrite determines if a message is logged with t.Logf when a golden + // file is written to with either of the set methods. + logOnWrite bool +} + +// Ensure golden satisfies Golden interface. +var _ Golden = &gold{} + +func (g *gold) Do(t TestingT, data []byte) []byte { + t.Helper() + + if g.Update() { + g.Set(t, data) + } + + return g.Get(t) +} + +func (g *gold) DoP(t TestingT, name string, data []byte) []byte { + t.Helper() + + if g.Update() { + g.SetP(t, name, data) + } + + return g.GetP(t, name) +} + +func (g *gold) File(t TestingT) string { + t.Helper() + + return g.file(t, "") +} + +func (g *gold) FileP(t TestingT, name string) string { + t.Helper() + + if name == "" { + t.Fatalf("golden: name cannot be empty") + } + + return g.file(t, name) +} + +func (g *gold) file(t TestingT, name string) string { + t.Helper() + + if t.Name() == "" { + t.Fatalf("golden: could not determine filename for TestingT instance") + } + + base := []string{g.dirname, filepath.FromSlash(t.Name())} + if name != "" { + base = append(base, name) + } + + f := filepath.Clean(filepath.Join(base...) + g.suffix) + + dirty := strings.Split(f, string(os.PathSeparator)) + clean := make([]string, 0, len(dirty)) + for _, s := range dirty { + clean = append(clean, sanitize.Filename(s)) + } + + return strings.Join(clean, string(os.PathSeparator)) +} + +func (g *gold) Get(t TestingT) []byte { + t.Helper() + + return g.get(t, "") +} + +func (g *gold) GetP(t TestingT, name string) []byte { + t.Helper() + + if name == "" { + t.Fatalf("golden: name cannot be empty") + } + + return g.get(t, name) +} + +func (g *gold) get(t TestingT, name string) []byte { + t.Helper() + + f := g.file(t, name) + + b, err := g.fs.ReadFile(f) + if err != nil { + t.Fatalf("golden: %s", err.Error()) + } + + return b +} + +func (g *gold) Set(t TestingT, data []byte) { + t.Helper() + + g.set(t, "", data) +} + +func (g *gold) SetP(t TestingT, name string, data []byte) { + t.Helper() + + if name == "" { + t.Fatalf("golden: name cannot be empty") + } + + g.set(t, name, data) +} + +func (g *gold) set(t TestingT, name string, data []byte) { + t.Helper() + + f := g.file(t, name) + dir := filepath.Dir(f) + + if g.logOnWrite { + t.Logf("golden: writing golden file: %s", f) + } + + err := g.fs.MkdirAll(dir, g.dirMode) + if err != nil { + t.Fatalf("golden: failed to create directory: %s", err.Error()) + } + + err = g.fs.WriteFile(f, data, g.fileMode) + if err != nil { + t.Fatalf("golden: filed to write file: %s", err.Error()) + } +} + +func (g *gold) Update() bool { + return g.updateFunc() +} diff --git a/gold_test.go b/gold_test.go new file mode 100644 index 0000000..89bf340 --- /dev/null +++ b/gold_test.go @@ -0,0 +1,528 @@ +package golden + +// func Test_gold_File(t *testing.T) { +// type fields struct { +// suffix *string +// dirname *string +// } +// tests := []struct { +// name string +// testName string +// fields fields +// want string +// wantFatals []string +// }{ +// { +// name: "top-level", +// testName: "TestFooBar", +// want: filepath.Join("testdata", "TestFooBar.golden"), +// }, +// { +// name: "sub-test", +// testName: "TestFooBar/it_is_here", +// want: filepath.Join( +// "testdata", "TestFooBar", "it_is_here.golden", +// ), +// }, +// { +// name: "blank test name", +// testName: "", +// wantFatals: []string{ +// "golden: could not determine filename for TestingT instance", +// }, +// }, +// { +// name: "custom dirname", +// testName: "TestFozBar", +// fields: fields{ +// dirname: stringPtr("goldenfiles"), +// }, +// want: filepath.Join("goldenfiles", "TestFozBar.golden"), +// }, +// { +// name: "custom suffix", +// testName: "TestFozBaz", +// fields: fields{ +// suffix: stringPtr(".goldfile"), +// }, +// want: filepath.Join("testdata", "TestFozBaz.goldfile"), +// }, +// { +// name: "custom dirname and suffix", +// testName: "TestFozBar", +// fields: fields{ +// dirname: stringPtr("goldenfiles"), +// suffix: stringPtr(".goldfile"), +// }, +// want: filepath.Join("goldenfiles", "TestFozBar.goldfile"), +// }, +// { +// name: "invalid chars in test name", +// testName: `TestFooBar/foo?<>:*|"bar`, +// want: filepath.Join( +// "testdata", "TestFooBar", "foo_______bar.golden", +// ), +// }, +// } +// for _, tt := range tests { +// t.Run(tt.name, func(t *testing.T) { +// if tt.fields.suffix == nil { +// tt.fields.suffix = stringPtr(".golden") +// } +// if tt.fields.dirname == nil { +// tt.fields.dirname = stringPtr("testdata") +// } + +// g := &gold{ +// suffix: *tt.fields.suffix, +// dirname: *tt.fields.dirname, +// } + +// ft := &fakeTestingT{name: tt.testName} + +// var got string +// testInGoroutine(t, func() { +// got = g.File(ft) +// }) + +// assert.Equal(t, tt.want, got) +// assert.Equal(t, tt.wantFatals, ft.fatals) +// }) +// } +// } + +// func Test_gold_FileP(t *testing.T) { +// type fields struct { +// suffix *string +// dirname *string +// } +// tests := []struct { +// name string +// testName string +// goldenName string +// fields fields +// want string +// wantFatals []string +// }{ +// { +// name: "top-level", +// testName: "TestFooBar", +// goldenName: "yaml", +// want: filepath.Join("testdata", "TestFooBar", "yaml.golden"), +// }, +// { +// name: "sub-test", +// testName: "TestFooBar/it_is_here", +// goldenName: "json", +// want: filepath.Join( +// "testdata", "TestFooBar", "it_is_here", "json.golden", +// ), +// }, +// { +// name: "blank test name", +// testName: "", +// goldenName: "json", +// wantFatals: []string{ +// "golden: could not determine filename for TestintT instance", +// }, +// }, +// { +// name: "custom dirname", +// testName: "TestFozBar", +// goldenName: "xml", +// fields: fields{ +// dirname: stringPtr("goldenfiles"), +// }, +// want: filepath.Join("goldenfiles", "TestFozBar", "xml.golden"), +// }, +// { +// name: "custom suffix", +// testName: "TestFozBaz", +// goldenName: "toml", +// fields: fields{ +// suffix: stringPtr(".goldfile"), +// }, +// want: filepath.Join("testdata", "TestFozBaz", "toml.goldfile"), +// }, +// { +// name: "custom dirname and suffix", +// testName: "TestFozBar", +// goldenName: "json", +// fields: fields{ +// dirname: stringPtr("goldenfiles"), +// suffix: stringPtr(".goldfile"), +// }, +// want: filepath.Join("goldenfiles", "TestFozBar", "json.goldfile"), +// }, +// { +// name: "invalid chars in test name", +// testName: `TestFooBar/foo?<>:*|"bar`, +// goldenName: "yml", +// want: filepath.Join( +// "testdata", "TestFooBar", "foo_______bar", "yml.golden", +// ), +// }, +// } +// for _, tt := range tests { +// t.Run(tt.name, func(t *testing.T) { +// if tt.fields.suffix == nil { +// tt.fields.suffix = stringPtr(".golden") +// } +// if tt.fields.dirname == nil { +// tt.fields.dirname = stringPtr("testdata") +// } + +// g := &gold{ +// suffix: *tt.fields.suffix, +// dirname: *tt.fields.dirname, +// } + +// ft := &fakeTestingT{name: tt.testName} + +// var got string +// testInGoroutine(t, func() { +// got = g.FileP(ft, tt.goldenName) +// }) + +// assert.Equal(t, tt.want, got) +// assert.Equal(t, tt.wantFatals, ft.fatals) +// }) +// } +// } + +// func Test_gold_Get(t *testing.T) { +// type fields struct { +// suffix *string +// dirname *string +// } +// tests := []struct { +// name string +// testName string +// fields fields +// files map[string][]byte +// want []byte +// wantAborted bool +// wantFailCount int +// wantTestOutput []string +// }{ +// { +// name: "file exists", +// testName: "TestFooBar", +// files: map[string][]byte{ +// filepath.Join("testdata", "TestFooBar.golden"): []byte( +// "foo: bar\nhello: world", +// ), +// }, +// want: []byte("foo: bar\nhello: world"), +// }, +// { +// name: "file is missing", +// testName: "TestFooBar", +// files: map[string][]byte{}, +// wantAborted: true, +// wantFailCount: 1, +// wantTestOutput: []string{ +// "golden: open " + filepath.Join( +// "testdata", "TestFooBar.golden", +// ) + ": file does not exist\n", +// }, +// }, +// { +// name: "sub-test file exists", +// testName: "TestFooBar/it_is_here", +// files: map[string][]byte{ +// filepath.Join( +// "testdata", "TestFooBar", "it_is_here.golden", +// ): []byte("this is really here ^_^\n"), +// }, +// want: []byte("this is really here ^_^\n"), +// }, +// { +// name: "sub-test file is missing", +// testName: "TestFooBar/not_really_here", +// files: map[string][]byte{}, +// wantAborted: true, +// wantFailCount: 1, +// wantTestOutput: []string{ +// "golden: open " + filepath.Join( +// "testdata", "TestFooBar", "not_really_here.golden", +// ) + ": file does not exist\n", +// }, +// }, +// { +// name: "blank test name", +// testName: "", +// wantAborted: true, +// wantFailCount: 1, +// wantTestOutput: []string{ +// "golden: could not determine filename for given " + +// "*mocktesting.T instance\n", +// }, +// }, +// { +// name: "custom dirname", +// testName: "TestFozBar", +// fields: fields{ +// dirname: stringPtr("goldenfiles"), +// }, +// files: map[string][]byte{ +// filepath.Join("goldenfiles", "TestFozBar.golden"): []byte( +// "foo: bar\nhello: world", +// ), +// }, +// want: []byte("foo: bar\nhello: world"), +// }, +// { +// name: "custom suffix", +// testName: "TestFozBaz", +// fields: fields{ +// suffix: stringPtr(".goldfile"), +// }, +// files: map[string][]byte{ +// filepath.Join("testdata", "TestFozBaz.goldfile"): []byte( +// "foo: bar\nhello: world", +// ), +// }, +// want: []byte("foo: bar\nhello: world"), +// }, +// { +// name: "custom dirname and suffix", +// testName: "TestFozBar", +// fields: fields{ +// dirname: stringPtr("goldenfiles"), +// suffix: stringPtr(".goldfile"), +// }, +// files: map[string][]byte{ +// filepath.Join("goldenfiles", "TestFozBar.goldfile"): []byte( +// "foo: bar\nhello: world", +// ), +// }, +// want: []byte("foo: bar\nhello: world"), +// }, +// { +// name: "invalid chars in test name", +// testName: `TestFooBar/foo?<>:*|"bar`, +// files: map[string][]byte{ +// filepath.Join( +// "testdata", "TestFooBar", "foo_______bar.golden", +// ): []byte("foo: bar\nhello: world"), +// }, +// want: []byte("foo: bar\nhello: world"), +// }, +// } +// for _, tt := range tests { +// t.Run(tt.name, func(t *testing.T) { +// fs := NewFS() // TODO: Replace with in-memory stub FS. +// for f, b := range tt.files { +// _ = fs.WriteFile(f, b, 0o644) +// } + +// if tt.fields.suffix == nil { +// tt.fields.suffix = stringPtr(".golden") +// } +// if tt.fields.dirname == nil { +// tt.fields.dirname = stringPtr("testdata") +// } + +// g := &gold{ +// suffix: *tt.fields.suffix, +// dirname: *tt.fields.dirname, +// fs: fs, +// } + +// mt := mocktesting.NewT(tt.testName) + +// var got []byte +// mocktesting.Go(func() { +// got = g.Get(mt) +// }) + +// assert.Equal(t, tt.want, got) +// assert.Equal(t, tt.wantAborted, mt.Aborted(), "aborted") +// assert.Equal(t, +// tt.wantFailCount, mt.FailedCount(), "failed count", +// ) +// assert.Equal(t, tt.wantTestOutput, mt.Output(), "test output") +// }) +// } +// } + +// func Test_gold_GetP(t *testing.T) { +// type args struct { +// name string +// } +// type fields struct { +// suffix *string +// dirname *string +// } +// tests := []struct { +// name string +// testName string +// args args +// fields fields +// files map[string][]byte +// want []byte +// wantAborted bool +// wantFailCount int +// wantTestOutput []string +// }{ +// { +// name: "file exists", +// testName: "TestFooBar", +// args: args{name: "yaml"}, +// files: map[string][]byte{ +// filepath.Join("testdata", "TestFooBar", "yaml.golden"): []byte( +// "foo: bar\nhello: world", +// ), +// }, +// want: []byte("foo: bar\nhello: world"), +// }, +// { +// name: "file is missing", +// testName: "TestFooBar", +// args: args{name: "yaml"}, +// files: map[string][]byte{}, +// wantAborted: true, +// wantFailCount: 1, +// wantTestOutput: []string{ +// "golden: open " + filepath.Join( +// "testdata", "TestFooBar", "yaml.golden", +// ) + ": file does not exist\n", +// }, +// }, +// { +// name: "sub-test file exists", +// testName: "TestFooBar/it_is_here", +// args: args{name: "plain"}, +// files: map[string][]byte{ +// filepath.Join( +// "testdata", "TestFooBar", "it_is_here", "plain.golden", +// ): []byte("this is really here ^_^\n"), +// }, +// want: []byte("this is really here ^_^\n"), +// }, +// { +// name: "sub-test file is missing", +// testName: "TestFooBar/not_really_here", +// args: args{name: "plain"}, +// files: map[string][]byte{}, +// wantAborted: true, +// wantFailCount: 1, +// wantTestOutput: []string{ +// "golden: open " + filepath.Join( +// "testdata", "TestFooBar", "not_really_here", "plain.golden", +// ) + ": file does not exist\n", +// }, +// }, +// { +// name: "blank test name", +// testName: "", +// args: args{name: "plain"}, +// wantAborted: true, +// wantFailCount: 1, +// wantTestOutput: []string{ +// "golden: could not determine filename for given " + +// "*mocktesting.T instance\n", +// }, +// }, +// { +// name: "blank name", +// testName: "TestFooBar", +// args: args{name: ""}, +// wantAborted: true, +// wantFailCount: 1, +// wantTestOutput: []string{ +// "golden: name cannot be empty\n", +// }, +// }, +// { +// name: "custom dirname", +// testName: "TestFozBar", +// args: args{name: "yaml"}, +// fields: fields{ +// dirname: stringPtr("goldenfiles"), +// }, +// files: map[string][]byte{ +// filepath.Join( +// "goldenfiles", "TestFozBar", "yaml.golden", +// ): []byte("foo: bar\nhello: world"), +// }, +// want: []byte("foo: bar\nhello: world"), +// }, +// { +// name: "custom suffix", +// testName: "TestFozBaz", +// args: args{name: "yaml"}, +// fields: fields{ +// suffix: stringPtr(".goldfile"), +// }, +// files: map[string][]byte{ +// filepath.Join( +// "testdata", "TestFozBaz", "yaml.goldfile", +// ): []byte("foo: bar\nhello: world"), +// }, +// want: []byte("foo: bar\nhello: world"), +// }, +// { +// name: "custom dirname and suffix", +// testName: "TestFozBar", +// args: args{name: "yaml"}, +// fields: fields{ +// dirname: stringPtr("goldenfiles"), +// suffix: stringPtr(".goldfile"), +// }, +// files: map[string][]byte{ +// filepath.Join( +// "goldenfiles", "TestFozBar", "yaml.goldfile", +// ): []byte("foo: bar\nhello: world"), +// }, +// want: []byte("foo: bar\nhello: world"), +// }, +// { +// name: "invalid chars in test name", +// testName: `TestFooBar/foo?<>:*|"bar`, +// args: args{name: "trash"}, +// files: map[string][]byte{ +// filepath.Join( +// "testdata", "TestFooBar", "foo_______bar", "trash.golden", +// ): []byte("foo: bar\nhello: world"), +// }, +// want: []byte("foo: bar\nhello: world"), +// }, +// } +// for _, tt := range tests { +// t.Run(tt.name, func(t *testing.T) { +// fs := NewFS() // TODO: Replace with in-memory stub FS +// for f, b := range tt.files { +// _ = fs.WriteFile(f, b, 0o644) +// } + +// if tt.fields.suffix == nil { +// tt.fields.suffix = stringPtr(".golden") +// } +// if tt.fields.dirname == nil { +// tt.fields.dirname = stringPtr("testdata") +// } + +// g := &gold{ +// suffix: *tt.fields.suffix, +// dirname: *tt.fields.dirname, +// fs: fs, +// } + +// mt := mocktesting.NewT(tt.testName) + +// var got []byte +// mocktesting.Go(func() { +// got = g.GetP(mt, tt.args.name) +// }) + +// assert.Equal(t, tt.want, got) +// assert.Equal(t, tt.wantAborted, mt.Aborted(), "aborted") +// assert.Equal(t, +// tt.wantFailCount, mt.FailedCount(), "failed count", +// ) +// assert.Equal(t, tt.wantTestOutput, mt.Output(), "test output") +// }) +// } +// } diff --git a/golden.go b/golden.go index 3ce2b8f..f3b611d 100644 --- a/golden.go +++ b/golden.go @@ -118,119 +118,37 @@ // testdata/TestExampleMyStructTabularP/full_struct/xml.golden package golden -import ( - "os" - "path/filepath" - "strings" - "testing" +import "os" - "github.com/jimeh/go-golden/sanitize" - "github.com/spf13/afero" +var ( + // DefaultGolden is the default Golden instance used by all top-level + // package functions. + DefaultGolden = New() + + // DefaultDirMode is the default file system permissions used for any + // created directories to hold golden files. + DefaultDirMode = os.FileMode(0o755) + + // DefaultFileMode is the default file system permissions used for any + // created or updated golden files written to disk. + DefaultFileMode = os.FileMode(0o644) + + // DefaultSuffix is the default filename suffix used for all golden files. + DefaultSuffix = ".golden" + + // DefaultDirname is the default name of the top-level directory used to + // hold golden files. + DefaultDirname = "testdata" + + // DefaultUpdateFunc is the default function used to determine if golden + // files should be updated or not. It is called by Update(). + DefaultUpdateFunc = EnvUpdateFunc + + // DefaultLogOnWrite is the default value for logOnWrite on all Golden + // instances. + DefaultLogOnWrite = true ) -//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 { - Fatal(args ...interface{}) - Fatalf(format string, args ...interface{}) - Helper() - Log(args ...interface{}) - Logf(format string, args ...interface{}) - Name() string -} - -// 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 TestingT) string { - t.Helper() - - 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 TestingT) []byte { - t.Helper() - - return Default.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) { - t.Helper() - - 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 TestingT, name string) string { - t.Helper() - - return Default.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 { - t.Helper() - - return Default.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) { - t.Helper() - - 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 -// to determine if golden.Set() or golden.SetP() 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 Default.Update() -} - // Golden handles all interactions with golden files. The top-level package // functions proxy through to a default global Golden instance. type Golden interface { @@ -291,14 +209,14 @@ type Golden interface { // New returns a new Golden instance. Used to create custom Golden instances. // See the the various Option functions for details of what can be customized. func New(options ...Option) Golden { - g := &golden{ - dirMode: 0o755, - fileMode: 0o644, - suffix: ".golden", - dirname: "testdata", - updateFunc: EnvUpdateFunc, - fs: afero.NewOsFs(), - logOnWrite: true, + g := &gold{ + dirMode: DefaultDirMode, + fileMode: DefaultFileMode, + suffix: DefaultSuffix, + dirname: DefaultDirname, + updateFunc: DefaultUpdateFunc, + fs: DefaultFS, + logOnWrite: DefaultLogOnWrite, } for _, opt := range options { @@ -308,245 +226,88 @@ func New(options ...Option) Golden { return g } -type Option interface { - apply(*golden) +// 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 DefaultGolden.Do(t, data) } -type optionFunc func(*golden) +// 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() -func (fn optionFunc) apply(g *golden) { - fn(g) + return DefaultGolden.DoP(t, name, data) } -// WithDirMode sets the file system permissions used for any folders created to -// hold golden files. +// File returns the filename of the golden file for the given *testing.T +// instance as determined by t.Name(). +func File(t TestingT) string { + t.Helper() + + return DefaultGolden.File(t) +} + +// FileP returns the filename of the specifically named golden file for the +// given *testing.T instance as determined by t.Name(). +func FileP(t TestingT, name string) string { + t.Helper() + + return DefaultGolden.FileP(t, name) +} + +// 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 TestingT) []byte { + t.Helper() + + return DefaultGolden.Get(t) +} + +// 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(). // -// When this option is not provided, the default value is 0o755. -func WithDirMode(mode os.FileMode) Option { - return optionFunc(func(g *golden) { - g.dirMode = mode - }) +// 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 TestingT, name string) []byte { + t.Helper() + + return DefaultGolden.GetP(t, name) } -// WithFileMode sets the file system permissions used for any created or updated -// golden files written to. +// 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 TestingT, data []byte) { + t.Helper() + + DefaultGolden.Set(t, data) +} + +// 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. // -// When this option is not provided, the default value is 0o644. -func WithFileMode(mode os.FileMode) Option { - return optionFunc(func(g *golden) { - g.fileMode = mode - }) +// 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 TestingT, name string, data []byte) { + t.Helper() + + DefaultGolden.SetP(t, name, data) } -// WithSuffix sets the filename suffix used for all golden files. +// 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. // -// When this option is not provided, the default value is ".golden". -func WithSuffix(suffix string) Option { - return optionFunc(func(g *golden) { - g.suffix = suffix - }) -} - -// WithDirname sets the name of the top-level directory used to hold golden -// files. -// -// When this option is not provided, the default value is "testdata". -func WithDirname(name string) Option { - return optionFunc(func(g *golden) { - g.dirname = name - }) -} - -// WithUpdateFunc sets the function used to determine if golden files should be -// updated or not. Essentially the provided UpdateFunc is called by Update(). -// -// When this option is not provided, the default value is EnvUpdateFunc. -func WithUpdateFunc(fn UpdateFunc) Option { - return optionFunc(func(g *golden) { - g.updateFunc = fn - }) -} - -// 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 { - return optionFunc(func(g *golden) { - g.fs = fs - }) -} - -// WithSilentWrites silences the "golden: writing [...]" log messages whenever -// set functions write a golden file to disk. -func WithSilentWrites() Option { - return optionFunc(func(g *golden) { - g.logOnWrite = false - }) -} - -// golden is the underlying struct that implements the Golden interface. -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 be "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 - - // fs is used for all file system operations. This enables providing custom - // afero.fs instances which can be useful for testing purposes. - fs afero.Fs - - // logOnWrite determines if a message is logged with t.Logf when a golden - // file is written to with either of the set methods. - logOnWrite bool -} - -// Ensure golden satisfies Golden interface. -var _ Golden = &golden{} - -func (g *golden) File(t TestingT) string { - t.Helper() - - return g.file(t, "") -} - -func (g *golden) Get(t TestingT) []byte { - t.Helper() - - return g.get(t, "") -} - -func (g *golden) Set(t TestingT, data []byte) { - t.Helper() - - 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 (g *golden) FileP(t TestingT, name string) string { - t.Helper() - - if name == "" { - t.Fatalf("golden: test name cannot be empty") - } - - return g.file(t, name) -} - -func (g *golden) GetP(t TestingT, name string) []byte { - t.Helper() - - if name == "" { - t.Fatal("golden: name cannot be empty") - } - - return g.get(t, name) -} - -func (g *golden) SetP(t TestingT, name string, data []byte) { - t.Helper() - - if name == "" { - t.Fatal("golden: name cannot be empty") - } - - 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 (g *golden) file(t TestingT, name string) string { - t.Helper() - - if t.Name() == "" { - t.Fatalf( - "golden: could not determine filename for given %T instance", t, - ) - } - - base := []string{g.dirname, filepath.FromSlash(t.Name())} - if name != "" { - base = append(base, name) - } - - f := filepath.Clean(filepath.Join(base...) + g.suffix) - - dirty := strings.Split(f, string(os.PathSeparator)) - clean := make([]string, 0, len(dirty)) - for _, s := range dirty { - clean = append(clean, sanitize.Filename(s)) - } - - return strings.Join(clean, string(os.PathSeparator)) -} - -func (g *golden) get(t TestingT, name string) []byte { - t.Helper() - - f := g.file(t, name) - - b, err := afero.ReadFile(g.fs, f) - if err != nil { - t.Fatalf("golden: %s", err.Error()) - } - - return b -} - -func (g *golden) set(t TestingT, name string, data []byte) { - t.Helper() - - f := g.file(t, name) - dir := filepath.Dir(f) - - if g.logOnWrite { - t.Logf("golden: writing golden file: %s", f) - } - - err := g.fs.MkdirAll(dir, g.dirMode) - if err != nil { - t.Fatalf("golden: failed to create directory: %s", err.Error()) - } - - err = afero.WriteFile(g.fs, f, data, g.fileMode) - if err != nil { - t.Fatalf("golden: filed to write file: %s", err.Error()) - } -} - -func (g *golden) Update() bool { - return g.updateFunc() +// 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 DefaultGolden.Update() } diff --git a/golden_example_test.go b/golden_example_test.go index d60f678..39c5384 100644 --- a/golden_example_test.go +++ b/golden_example_test.go @@ -16,8 +16,7 @@ type MyStruct struct { // TestExampleMyStruct reads/writes the following golden file: // -// testdata/TestExampleMyStruct.golden -// +// testdata/TestExampleMyStruct.golden func TestExampleMyStruct(t *testing.T) { got, err := json.Marshal(&MyStruct{Foo: "Bar"}) require.NoError(t, err) @@ -32,9 +31,8 @@ func TestExampleMyStruct(t *testing.T) { // TestExampleMyStructTabular reads/writes the following golden files: // -// testdata/TestExampleMyStructTabular/empty_struct.golden -// testdata/TestExampleMyStructTabular/full_struct.golden -// +// testdata/TestExampleMyStructTabular/empty_struct.golden +// testdata/TestExampleMyStructTabular/full_struct.golden func TestExampleMyStructTabular(t *testing.T) { tests := []struct { name string @@ -60,9 +58,8 @@ func TestExampleMyStructTabular(t *testing.T) { // TestExampleMyStructP reads/writes the following golden file: // -// testdata/TestExampleMyStructP/json.golden -// testdata/TestExampleMyStructP/xml.golden -// +// 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"}) @@ -78,11 +75,10 @@ func TestExampleMyStructP(t *testing.T) { // 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 -// +// 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 diff --git a/golden_mock_test.go b/golden_mock_test.go deleted file mode 100644 index 0b7b82d..0000000 --- a/golden_mock_test.go +++ /dev/null @@ -1,306 +0,0 @@ -// 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 5b51d85..fd54a88 100644 --- a/golden_test.go +++ b/golden_test.go @@ -1,361 +1,921 @@ package golden import ( + "fmt" "io/fs" - "io/ioutil" - "os" "path/filepath" "reflect" "runtime" + "sync" "testing" - "github.com/golang/mock/gomock" "github.com/jimeh/envctl" - "github.com/jimeh/go-mocktesting" - "github.com/spf13/afero" + "github.com/jimeh/go-golden/test/testfs" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func stringPtr(s string) *string { - return &s -} +// +// Test Helpers +// func funcID(f interface{}) string { return runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name() } -func setupDefaultMock( - t *testing.T, - ctrl *gomock.Controller, -) (*MockTestingT, *MockGolden) { - t.Helper() +func prepareDefaultGoldenForTests(t *testing.T) *testfs.FS { + realDefault := DefaultGolden + t.Cleanup(func() { DefaultGolden = realDefault }) - mt := NewMockTestingT(ctrl) - mg := NewMockGolden(ctrl) + fs := testfs.New() + DefaultGolden = New(WithFS(fs)) - originalDefault := Default - Default = mg - t.Cleanup(func() { - Default = originalDefault - }) - - return mt, mg + return fs } -func TestDefault(t *testing.T) { - require.IsType(t, &golden{}, Default) +func testInGoroutine(t *testing.T, f func()) { + t.Helper() - dg := Default.(*golden) + wg := sync.WaitGroup{} + wg.Add(1) + go func() { + defer wg.Done() + f() + }() + wg.Wait() +} + +// +// Tests +// + +func TestDefault(t *testing.T) { + require.IsType(t, &gold{}, DefaultGolden) + + dg := DefaultGolden.(*gold) 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, NewFS(), dg.fs) assert.Equal(t, true, dg.logOnWrite) } -func TestFile(t *testing.T) { - ctrl := gomock.NewController(t) - mt, mg := setupDefaultMock(t, ctrl) - - want := filepath.Join("testdata", t.Name()+".golden") - - mt.EXPECT().Helper() - mg.EXPECT().File(mt).Return(want) - - 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) { - t.Cleanup(func() { - t.Log("cleaning up golden files") - err := os.RemoveAll(filepath.Join("testdata", "TestSet")) - require.NoError(t, err) - err = os.Remove(filepath.Join("testdata", "TestSet.golden")) - require.NoError(t, err) - }) - - content := []byte("This is the default golden file for TestSet ^_^") - Set(t, content) - - b, err := ioutil.ReadFile(filepath.Join("testdata", "TestSet.golden")) - require.NoError(t, err) - - assert.Equal(t, content, b) - +func TestDo(t *testing.T) { tests := []struct { - name string - file string - content []byte + name string + testName string + content []byte + existing []byte + wantFilepath string + wantNoUpdateLogs []string + wantNoUpdateFatals []string + wantUpdateLogs []string + wantUpdateFatals []string }{ { - name: "", - file: filepath.Join("testdata", "TestSet", "#00.golden"), - content: []byte("number double-zero strikes again"), + name: "empty test name", + testName: "", + wantUpdateFatals: []string{ + "golden: could not determine filename for TestingT instance", + }, + wantNoUpdateFatals: []string{ + "golden: could not determine filename for TestingT instance", + }, }, { - name: "foobar", - file: filepath.Join("testdata", "TestSet", "foobar.golden"), - content: []byte("foobar here"), + name: "without slashes", + testName: "TestFoo", + content: []byte("new content"), + existing: []byte("old content"), + wantFilepath: filepath.Join("testdata", "TestFoo.golden"), + wantUpdateLogs: []string{ + fmt.Sprintf( + "golden: writing golden file: %s", + filepath.Join("testdata", "TestFoo.golden"), + ), + }, }, { - name: "foo/bar", - file: filepath.Join("testdata", "TestSet", "foo", "bar.golden"), - content: []byte("foo/bar style sub-sub-folders works too"), + name: "with slashes", + testName: "TestFoo/bar", + content: []byte("new stuff with slashes"), + existing: []byte("old stuff"), + wantFilepath: filepath.Join("testdata", "TestFoo", "bar.golden"), + wantUpdateLogs: []string{ + fmt.Sprintf( + "golden: writing golden file: %s", + filepath.Join("testdata", "TestFoo", "bar.golden"), + ), + }, }, { - name: "john's lost flip-flop", - file: filepath.Join( - "testdata", "TestSet", "john's_lost_flip-flop.golden", + name: "with spaces and special characters", + testName: `TestFoo/John's "lost" flip-flop?<>:*|"`, + content: []byte("Did John lose his flip-flop again?"), + existing: []byte("Where is the flip-flop?"), + wantFilepath: filepath.Join( + "testdata", "TestFoo", "John's__lost__flip-flop_______.golden", ), - content: []byte("Did John lose his flip-flop again?"), + wantUpdateLogs: []string{ + fmt.Sprintf( + "golden: writing golden file: %s", + filepath.Join( + "testdata", "TestFoo", + "John's__lost__flip-flop_______.golden", + ), + ), + }, }, { - name: "thing: it's a thing!", - file: filepath.Join( - "testdata", "TestSet", "thing__it's_a_thing!.golden", + name: "does not exist", + testName: "TestFoo/nope", + content: []byte("new stuff with slashes"), + existing: nil, + wantFilepath: filepath.Join("testdata", "TestFoo", "nope.golden"), + wantUpdateLogs: []string{ + fmt.Sprintf( + "golden: writing golden file: %s", + filepath.Join("testdata", "TestFoo", "nope.golden"), + ), + }, + wantNoUpdateFatals: []string{ + fmt.Sprintf( + "golden: open %s: no such file or directory", + filepath.Join( + "/root", "testdata", "TestFoo", "nope.golden", + ), + ), + }, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name+"/no update", func(t *testing.T) { + require.False(t, Update()) + + fs := prepareDefaultGoldenForTests(t) + ft := &fakeTestingT{name: tt.testName} + + if tt.existing != nil { + err := fs.MkdirAll(filepath.Dir(tt.wantFilepath), 0o755) + require.NoError(t, err) + + err = fs.WriteFile(tt.wantFilepath, tt.existing, 0o600) + require.NoError(t, err) + } + + var got []byte + testInGoroutine(t, func() { + got = Do(ft, tt.content) + }) + + if tt.existing == nil { + assert.Equal(t, tt.existing, got) + } else { + assert.GreaterOrEqual(t, 1, len(ft.fatals)) + } + + assert.Equal(t, tt.wantNoUpdateFatals, ft.fatals) + assert.Equal(t, tt.wantNoUpdateLogs, ft.logs) + }) + t.Run(tt.name+"/update", func(t *testing.T) { + envctl.WithClean(map[string]string{"GOLDEN_UPDATE": "1"}, func() { + require.True(t, Update()) + + fs := prepareDefaultGoldenForTests(t) + ft := &fakeTestingT{name: tt.testName} + + if tt.existing != nil { + err := fs.MkdirAll(filepath.Dir(tt.wantFilepath), 0o755) + require.NoError(t, err) + + err = fs.WriteFile(tt.wantFilepath, tt.existing, 0o600) + require.NoError(t, err) + } + + var got []byte + testInGoroutine(t, func() { + got = Do(ft, tt.content) + }) + + assert.Equal(t, tt.content, got) + assert.Equal(t, tt.wantUpdateFatals, ft.fatals) + assert.Equal(t, tt.wantUpdateLogs, ft.logs) + }) + }) + } +} + +func TestDoP(t *testing.T) { + tests := []struct { + name string + testName string + goldenName string + content []byte + existing []byte + wantFilepath string + wantNoUpdateLogs []string + wantNoUpdateFatals []string + wantUpdateLogs []string + wantUpdateFatals []string + }{ + { + name: "empty test name", + testName: "", + goldenName: "junk", + wantUpdateFatals: []string{ + "golden: could not determine filename for TestingT instance", + }, + wantNoUpdateFatals: []string{ + "golden: could not determine filename for TestingT instance", + }, + }, + { + name: "empty golden name", + testName: "TestBar", + goldenName: "", + wantUpdateFatals: []string{ + "golden: name cannot be empty", + }, + wantNoUpdateFatals: []string{ + "golden: name cannot be empty", + }, + }, + { + name: "without slashes", + testName: "TestBar", + goldenName: "foo", + content: []byte("new content"), + existing: []byte("old content"), + wantFilepath: filepath.Join("testdata", "TestBar", "foo.golden"), + wantUpdateLogs: []string{ + fmt.Sprintf( + "golden: writing golden file: %s", + filepath.Join("testdata", "TestBar", "foo.golden"), + ), + }, + }, + { + name: "with slashes in test name", + testName: "TestBar/foo", + goldenName: "junk", + content: []byte("new stuff with slashes"), + existing: []byte("old stuff"), + wantFilepath: filepath.Join( + "testdata", "TestBar", "foo", "junk.golden", + ), + wantUpdateLogs: []string{ + fmt.Sprintf( + "golden: writing golden file: %s", + filepath.Join("testdata", "TestBar", "foo", "junk.golden"), + ), + }, + }, + { + name: "with slashes in golden name", + testName: "TestBar", + goldenName: "foo/junk", + content: []byte("new stuff with slashes"), + existing: []byte("old stuff"), + wantFilepath: filepath.Join( + "testdata", "TestBar", "foo", "junk.golden", + ), + wantUpdateLogs: []string{ + fmt.Sprintf( + "golden: writing golden file: %s", + filepath.Join("testdata", "TestBar", "foo", "junk.golden"), + ), + }, + }, + { + name: "with spaces and special characters", + testName: `TestBar/John's "lost" flip-flop?<>:*|"`, + goldenName: "junk/*plastic*", + content: []byte("Did John lose his flip-flop again?"), + existing: []byte("Where is the flip-flop?"), + wantFilepath: filepath.Join( + "testdata", "TestBar", "John's__lost__flip-flop_______", + "junk", "_plastic_.golden", + ), + wantUpdateLogs: []string{ + fmt.Sprintf( + "golden: writing golden file: %s", + filepath.Join( + "testdata", "TestBar", "John's__lost__flip-flop_______", + "junk", "_plastic_.golden", + ), + ), + }, + }, + { + name: "does not exist", + testName: "TestBar", + goldenName: "junk", + content: []byte("new stuff with slashes"), + existing: nil, + wantFilepath: filepath.Join("testdata", "TestBar", "junk.golden"), + wantUpdateLogs: []string{ + fmt.Sprintf( + "golden: writing golden file: %s", + filepath.Join("testdata", "TestBar", "junk.golden"), + ), + }, + wantNoUpdateFatals: []string{ + fmt.Sprintf( + "golden: open %s: no such file or directory", + filepath.Join( + "/root", "testdata", "TestBar", "junk.golden", + ), + ), + }, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name+"/no update", func(t *testing.T) { + require.False(t, Update()) + + fs := prepareDefaultGoldenForTests(t) + ft := &fakeTestingT{name: tt.testName} + + if tt.existing != nil { + err := fs.MkdirAll(filepath.Dir(tt.wantFilepath), 0o755) + require.NoError(t, err) + + err = fs.WriteFile(tt.wantFilepath, tt.existing, 0o600) + require.NoError(t, err) + } + + var got []byte + testInGoroutine(t, func() { + got = DoP(ft, tt.goldenName, tt.content) + }) + + if tt.existing == nil { + assert.Equal(t, tt.existing, got) + } else { + assert.GreaterOrEqual(t, 1, len(ft.fatals)) + } + + assert.Equal(t, tt.wantNoUpdateFatals, ft.fatals) + assert.Equal(t, tt.wantNoUpdateLogs, ft.logs) + }) + t.Run(tt.name+"/update", func(t *testing.T) { + envctl.WithClean(map[string]string{"GOLDEN_UPDATE": "1"}, func() { + require.True(t, Update()) + + fs := prepareDefaultGoldenForTests(t) + ft := &fakeTestingT{name: tt.testName} + + if tt.existing != nil { + err := fs.MkdirAll(filepath.Dir(tt.wantFilepath), 0o755) + require.NoError(t, err) + + err = fs.WriteFile(tt.wantFilepath, tt.existing, 0o600) + require.NoError(t, err) + } + + var got []byte + testInGoroutine(t, func() { + got = DoP(ft, tt.goldenName, tt.content) + }) + + assert.Equal(t, tt.content, got) + assert.Equal(t, tt.wantUpdateFatals, ft.fatals) + assert.Equal(t, tt.wantUpdateLogs, ft.logs) + }) + }) + } +} + +func TestFile(t *testing.T) { + tests := []struct { + name string + testName string + want string + wantFatals []string + }{ + { + name: "empty test name", + testName: "", + wantFatals: []string{ + "golden: could not determine filename for TestingT instance", + }, + }, + { + name: "without slashes", + testName: "TestFoo", + want: filepath.Join("testdata", "TestFoo.golden"), + }, + { + name: "with slashes", + testName: "TestFoo/bar", + want: filepath.Join("testdata", "TestFoo", "bar.golden"), + }, + { + name: "with slashes and special characters", + testName: `TestFoo/John's "lost" flip-flop?<>:*|"`, + want: filepath.Join( + "testdata", "TestFoo", "John's__lost__flip-flop_______.golden", ), - content: []byte("A thing? Really? Are we getting lazy? :P"), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - f := File(t) + ft := &fakeTestingT{name: tt.testName} - Set(t, tt.content) + var got string + testInGoroutine(t, func() { + got = File(ft) + }) - got, err := ioutil.ReadFile(f) - require.NoError(t, err) - - assert.Equal(t, tt.file, f) - assert.Equal(t, tt.content, got) + assert.Equal(t, tt.want, got) + assert.Equal(t, tt.wantFatals, ft.fatals) }) } } func TestFileP(t *testing.T) { - ctrl := gomock.NewController(t) - mt, mg := setupDefaultMock(t, ctrl) - - want := filepath.Join("testdata", t.Name(), "foobar.golden") - - mt.EXPECT().Helper() - mg.EXPECT().FileP(mt, "foobar").Return(want) - - got := FileP(mt, "foobar") - - assert.Equal(t, want, got) -} - -func TestGetP(t *testing.T) { - t.Cleanup(func() { - err := os.RemoveAll(filepath.Join("testdata", "TestGetP")) - require.NoError(t, err) - }) - - err := os.MkdirAll(filepath.Join("testdata", "TestGetP"), 0o755) - require.NoError(t, err) - - content := []byte("this is the named golden file for TestGetP") - err = ioutil.WriteFile( //nolint:gosec - filepath.Join("testdata", "TestGetP", "sub-name.golden"), - content, 0o644, - ) - require.NoError(t, err) - - got := GetP(t, "sub-name") - assert.Equal(t, content, got) - tests := []struct { - name string - named string - file string - want []byte + name string + testName string + goldenName string + want string + wantFatals []string }{ { - name: "", - named: "sub-zero-one", - file: filepath.Join( - "testdata", "TestGetP", "#00", "sub-zero-one.golden", - ), - want: []byte("number zero-one here"), + name: "empty test name", + testName: "", + goldenName: "junk", + wantFatals: []string{ + "golden: could not determine filename for TestingT instance", + }, }, { - name: "foobar", - named: "email", - file: filepath.Join( - "testdata", "TestGetP", "foobar", "email.golden", - ), - want: []byte("foobar email here"), + name: "empty golden name", + testName: "TestFoo", + goldenName: "", + wantFatals: []string{ + "golden: name cannot be empty", + }, }, { - name: "foobar", - named: "json", - file: filepath.Join( - "testdata", "TestGetP", "foobar#01", "json.golden", - ), - want: []byte("foobar json here"), + name: "without slashes", + testName: "TestFoo", + goldenName: "bar", + want: filepath.Join("testdata", "TestFoo", "bar.golden"), }, { - name: "foo/bar", - named: "hello/world", - file: filepath.Join( - "testdata", "TestGetP", - "foo", "bar", - "hello", "world.golden", + name: "slashes in test name", + testName: "TestFoo/bar", + goldenName: "junk", + want: filepath.Join( + "testdata", "TestFoo", "bar", "junk.golden", ), - want: []byte("foo/bar style sub-sub-folders works too"), }, { - name: "john's lost flip-flop", - named: "left", - file: filepath.Join( - "testdata", "TestGetP", "john's_lost_flip-flop", - "left.golden", + name: "slashes in golden name", + testName: "TestFoo", + goldenName: "bar/junk", + want: filepath.Join( + "testdata", "TestFoo", "bar", "junk.golden", ), - want: []byte("Did John lose his left flip-flop again?"), }, { - name: "john's lost flip-flop", - named: "right", - file: filepath.Join( - "testdata", "TestGetP", "john's_lost_flip-flop#01", - "right.golden", + name: "slashes in test and golden name", + testName: "TestFoo/bar", + goldenName: "junk/plastic", + want: filepath.Join( + "testdata", "TestFoo", "bar", "junk", "plastic.golden", ), - want: []byte("Did John lose his right flip-flop again?"), }, { - name: "thing: it's", - named: "a thing!", - file: filepath.Join( - "testdata", "TestGetP", "thing__it's", "a_thing!.golden", + name: "slashes and special characters", + testName: `TestFoo/John's "lost" flip-flop?<>:*|"`, + goldenName: "junk/*plastic*", + want: filepath.Join( + "testdata", "TestFoo", "John's__lost__flip-flop_______", + "junk", "_plastic_.golden", ), - want: []byte("A thing? Really? Are we getting lazy? :P"), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - f := FileP(t, tt.named) - dir := filepath.Dir(f) + ft := &fakeTestingT{name: tt.testName} - err := os.MkdirAll(dir, 0o755) - require.NoError(t, err) + var got string + testInGoroutine(t, func() { + got = FileP(ft, tt.goldenName) + }) - err = ioutil.WriteFile(f, tt.want, 0o644) //nolint:gosec - require.NoError(t, err) - - got := GetP(t, tt.named) - - assert.Equal(t, filepath.FromSlash(tt.file), f) assert.Equal(t, tt.want, got) + assert.Equal(t, tt.wantFatals, ft.fatals) + }) + } +} + +func TestGet(t *testing.T) { + tests := []struct { + name string + testName string + files map[string][]byte + want []byte + wantFatals []string + }{ + { + name: "empty test name", + testName: "", + wantFatals: []string{ + "golden: could not determine filename for TestingT instance", + }, + }, + { + name: "without slashes", + testName: "TestFoo", + files: map[string][]byte{ + filepath.Join("testdata", "TestFoo.golden"): []byte("bar\n"), + }, + want: []byte("bar\n"), + }, + { + name: "with slashes", + testName: "TestFoo/bar", + files: map[string][]byte{ + filepath.Join("testdata", "TestFoo", "bar.golden"): []byte( + "bar\n", + ), + }, + want: []byte("bar\n"), + }, + { + name: "with slashes and special characters", + testName: `TestFoo/John's "lost" flip-flop?<>:*|"`, + files: map[string][]byte{ + filepath.Join( + "testdata", "TestFoo", + "John's__lost__flip-flop_______.golden", + ): []byte("bar nope\n"), + }, + want: []byte("bar nope\n"), + }, + { + name: "file does not exist", + testName: "TestFoo", + files: map[string][]byte{}, + wantFatals: []string{ + fmt.Sprintf( + "golden: open %s: no such file or directory", + filepath.Join("/root", "testdata", "TestFoo.golden"), + ), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fs := prepareDefaultGoldenForTests(t) + ft := &fakeTestingT{name: tt.testName} + + for file, content := range tt.files { + err := fs.MkdirAll(filepath.Dir(file), 0o755) + require.NoError(t, err) + + err = fs.WriteFile(file, content, 0o600) + require.NoError(t, err) + } + + var got []byte + testInGoroutine(t, func() { + got = Get(ft) + }) + + assert.Equal(t, tt.want, got) + assert.Equal(t, tt.wantFatals, ft.fatals) + }) + } +} + +func TestGetP(t *testing.T) { + tests := []struct { + name string + testName string + goldenName string + files map[string][]byte + want []byte + wantFatals []string + }{ + { + name: "empty test name", + testName: "", + goldenName: "junk", + wantFatals: []string{ + "golden: could not determine filename for TestingT instance", + }, + }, + { + name: "empty golden name", + testName: "TestBar", + goldenName: "", + wantFatals: []string{ + "golden: name cannot be empty", + }, + }, + { + name: "without slashes", + testName: "TestBar", + goldenName: "junk", + files: map[string][]byte{ + filepath.Join("testdata", "TestBar", "junk.golden"): []byte( + "foo junk\n", + ), + }, + want: []byte("foo junk\n"), + }, + { + name: "with slashes in test name", + testName: "TestBar/foo", + goldenName: "junk", + files: map[string][]byte{ + filepath.Join( + "testdata", "TestBar", "foo", "junk.golden", + ): []byte("foo\n"), + }, + want: []byte("foo\n"), + }, + { + name: "with slashes in golden name", + testName: "TestBar", + goldenName: "foo/junk", + files: map[string][]byte{ + filepath.Join( + "testdata", "TestBar", "foo", "junk.golden", + ): []byte("foo\n"), + }, + want: []byte("foo\n"), + }, + { + name: "slashes and special characters", + testName: `TestFoo/John's "lost" flip-flop?<>:*|"`, + goldenName: "junk/*plastic*", + files: map[string][]byte{ + filepath.Join( + "testdata", "TestFoo", "John's__lost__flip-flop_______", + "junk", "_plastic_.golden", + ): []byte("junk here\n"), + }, + want: []byte("junk here\n"), + }, + { + name: "file does not exist", + testName: "TestBar", + goldenName: "junk", + files: map[string][]byte{}, + wantFatals: []string{ + fmt.Sprintf( + "golden: open %s: no such file or directory", + filepath.Join( + "/root", "testdata", "TestBar", "junk.golden", + ), + ), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fs := prepareDefaultGoldenForTests(t) + ft := &fakeTestingT{name: tt.testName} + + for file, content := range tt.files { + err := fs.MkdirAll(filepath.Dir(file), 0o755) + require.NoError(t, err) + + err = fs.WriteFile(file, content, 0o600) + require.NoError(t, err) + } + + var got []byte + testInGoroutine(t, func() { + got = GetP(ft, tt.goldenName) + }) + + assert.Equal(t, tt.want, got) + assert.Equal(t, tt.wantFatals, ft.fatals) + }) + } +} + +func TestSet(t *testing.T) { + tests := []struct { + name string + testName string + wantFilepath string + content []byte + wantLogs []string + wantFatals []string + }{ + { + name: "empty test name", + testName: "", + wantFatals: []string{ + "golden: could not determine filename for TestingT instance", + }, + }, + { + name: "without slashes", + testName: "TestFoo", + content: []byte("foobar here"), + wantFilepath: filepath.Join("testdata", "TestFoo.golden"), + wantLogs: []string{ + fmt.Sprintf( + "golden: writing golden file: %s", + filepath.Join("testdata", "TestFoo.golden"), + ), + }, + }, + { + name: "with slashes", + testName: "TestFoo/bar", + content: []byte("foo/bar style sub-sub-folders works too"), + wantFilepath: filepath.Join("testdata", "TestFoo", "bar.golden"), + wantLogs: []string{ + fmt.Sprintf( + "golden: writing golden file: %s", + filepath.Join("testdata", "TestFoo", "bar.golden"), + ), + }, + }, + { + name: "with spaces and special characters", + testName: `TestFoo/John's "lost" flip-flop?<>:*|"`, + content: []byte("Did John lose his flip-flop again?"), + wantFilepath: filepath.Join( + "testdata", "TestFoo", "John's__lost__flip-flop_______.golden", + ), + wantLogs: []string{ + fmt.Sprintf( + "golden: writing golden file: %s", + filepath.Join( + "testdata", "TestFoo", + "John's__lost__flip-flop_______.golden", + ), + ), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fs := prepareDefaultGoldenForTests(t) + ft := &fakeTestingT{name: tt.testName} + + testInGoroutine(t, func() { + Set(ft, tt.content) + }) + + assert.Equal(t, tt.wantLogs, ft.logs) + if len(tt.wantFatals) == 0 { + got, err := fs.ReadFile(tt.wantFilepath) + require.NoError(t, err) + + assert.Equal(t, tt.content, got) + + filePerms, err := fs.FileMode(tt.wantFilepath) + require.NoError(t, err) + + dirPerms, err := fs.FileMode(filepath.Dir(tt.wantFilepath)) + require.NoError(t, err) + + assert.Equal(t, filePerms, DefaultFileMode) + assert.Equal(t, dirPerms, DefaultDirMode) + } else { + assert.Equal(t, tt.wantFatals, ft.fatals) + assert.False(t, + fs.Exists(tt.wantFilepath), + "file should not exist", + ) + } }) } } func TestSetP(t *testing.T) { - t.Cleanup(func() { - t.Log("cleaning up golden files") - err := os.RemoveAll(filepath.Join("testdata", "TestSetP")) - require.NoError(t, err) - }) - - content := []byte("This is the named golden file for TestSetP ^_^") - SetP(t, "sub-name", content) - - b, err := ioutil.ReadFile( - filepath.Join("testdata", "TestSetP", "sub-name.golden"), - ) - require.NoError(t, err) - - assert.Equal(t, content, b) - tests := []struct { - name string - named string - file string - content []byte + name string + testName string + goldenName string + wantFilepath string + content []byte + wantLogs []string + wantFatals []string }{ { - name: "", - named: "sub-zero-one", - file: filepath.Join( - "testdata", "TestSetP", "#00", "sub-zero-one.golden", - ), - content: []byte("number zero-one sub-zero-one strikes again"), + name: "empty test name", + testName: "", + goldenName: "junk", + wantFatals: []string{ + "golden: could not determine filename for TestingT instance", + }, }, { - name: "foobar", - named: "email", - file: filepath.Join( - "testdata", "TestSetP", "foobar", "email.golden", - ), - content: []byte("foobar here"), + name: "empty golden name", + testName: "TestBar", + goldenName: "", + wantFatals: []string{ + "golden: name cannot be empty", + }, }, { - name: "foobar", - named: "json", - file: filepath.Join( - "testdata", "TestSetP", "foobar#01", "json.golden", - ), - content: []byte("foobar here"), + name: "without slashes", + testName: "TestBar", + goldenName: "junk", + content: []byte("junk here"), + wantFilepath: filepath.Join("testdata", "TestBar", "junk.golden"), + wantLogs: []string{ + fmt.Sprintf( + "golden: writing golden file: %s", + filepath.Join("testdata", "TestBar", "junk.golden"), + ), + }, }, { - name: "john's lost flip-flop", - named: "left", - file: filepath.Join( - "testdata", "TestSetP", "john's_lost_flip-flop", - "left.golden", + name: "with slashes in test name", + testName: "TestBar/foo", + goldenName: "junk", + content: []byte("foo/bar style sub-sub-folders works too"), + wantFilepath: filepath.Join( + "testdata", "TestBar", "foo", "junk.golden", ), - content: []byte("Did John lose his left flip-flop again?"), + wantLogs: []string{ + fmt.Sprintf( + "golden: writing golden file: %s", + filepath.Join("testdata", "TestBar", "foo", "junk.golden"), + ), + }, }, { - name: "john's lost flip-flop", - named: "right", - file: filepath.Join( - "testdata", "TestSetP", "john's_lost_flip-flop#01", - "right.golden", + name: "with slashes in golden name", + testName: "TestBar", + goldenName: "foo/junk", + content: []byte("foo/bar style sub-sub-folders works too"), + wantFilepath: filepath.Join( + "testdata", "TestBar", "foo", "junk.golden", ), - content: []byte("Did John lose his right flip-flop again?"), + wantLogs: []string{ + fmt.Sprintf( + "golden: writing golden file: %s", + filepath.Join("testdata", "TestBar", "foo", "junk.golden"), + ), + }, }, { - name: "thing: it's", - named: "a thing!", - file: filepath.Join( - "testdata", "TestSetP", "thing__it's", "a_thing!.golden", + name: "slashes and special characters", + testName: `TestFoo/John's "lost" flip-flop?<>:*|"`, + goldenName: "junk/*plastic*", + content: []byte("Did John lose his flip-flop again?"), + wantFilepath: filepath.Join( + "testdata", "TestFoo", "John's__lost__flip-flop_______", + "junk", "_plastic_.golden", ), - content: []byte("A thing? Really? Are we getting lazy? :P"), + wantLogs: []string{ + fmt.Sprintf( + "golden: writing golden file: %s", + filepath.Join( + "testdata", "TestFoo", "John's__lost__flip-flop_______", + "junk", "_plastic_.golden", + ), + ), + }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - f := FileP(t, tt.named) + fs := prepareDefaultGoldenForTests(t) + ft := &fakeTestingT{name: tt.testName} - SetP(t, tt.named, tt.content) + testInGoroutine(t, func() { + SetP(ft, tt.goldenName, tt.content) + }) - got, err := ioutil.ReadFile(f) - require.NoError(t, err) + assert.Equal(t, tt.wantLogs, ft.logs) + if len(tt.wantFatals) == 0 { + got, err := fs.ReadFile(tt.wantFilepath) + require.NoError(t, err) - assert.Equal(t, tt.file, f) - assert.Equal(t, tt.content, got) + assert.Equal(t, tt.content, got) + + filePerms, err := fs.FileMode(tt.wantFilepath) + require.NoError(t, err) + + dirPerms, err := fs.FileMode(filepath.Dir(tt.wantFilepath)) + require.NoError(t, err) + + assert.Equal(t, filePerms, DefaultFileMode) + assert.Equal(t, dirPerms, DefaultDirMode) + } else { + assert.Equal(t, tt.wantFatals, ft.fatals) + assert.False(t, fs.Exists(tt.wantFilepath)) + } }) } } @@ -381,18 +941,18 @@ func TestNew(t *testing.T) { tests := []struct { name string args args - want *golden + want *gold }{ { name: "no options", args: args{options: nil}, - want: &golden{ + want: &gold{ dirMode: 0o755, fileMode: 0o644, suffix: ".golden", dirname: "testdata", updateFunc: EnvUpdateFunc, - fs: afero.NewOsFs(), + fs: NewFS(), logOnWrite: true, }, }, @@ -405,17 +965,17 @@ func TestNew(t *testing.T) { WithSuffix(".gold"), WithDirname("goldstuff"), WithUpdateFunc(myUpdateFunc), - WithFs(afero.NewMemMapFs()), + WithFS(testfs.New()), WithSilentWrites(), }, }, - want: &golden{ + want: &gold{ dirMode: 0o777, fileMode: 0o666, suffix: ".gold", dirname: "goldstuff", updateFunc: myUpdateFunc, - fs: afero.NewMemMapFs(), + fs: testfs.New(), logOnWrite: false, }, }, @@ -423,564 +983,16 @@ func TestNew(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { g := New(tt.args.options...) - got, ok := g.(*golden) - require.True(t, ok, "New did not returns a *golden instance") + got, ok := g.(*gold) + require.True(t, ok, "New did not returns a *gold type") 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, funcID(tt.want.updateFunc), funcID(got.updateFunc)) assert.IsType(t, tt.want.fs, got.fs) - }) - } -} - -func Test_golden_File(t *testing.T) { - type fields struct { - suffix *string - dirname *string - } - tests := []struct { - name string - testName string - fields fields - want string - wantAborted bool - wantFailCount int - wantTestOutput []string - }{ - { - name: "top-level", - testName: "TestFooBar", - want: filepath.Join("testdata", "TestFooBar.golden"), - }, - { - name: "sub-test", - testName: "TestFooBar/it_is_here", - want: filepath.Join( - "testdata", "TestFooBar", "it_is_here.golden", - ), - }, - { - name: "blank test name", - testName: "", - wantAborted: true, - wantFailCount: 1, - wantTestOutput: []string{ - "golden: could not determine filename for given " + - "*mocktesting.T instance\n", - }, - }, - { - name: "custom dirname", - testName: "TestFozBar", - fields: fields{ - dirname: stringPtr("goldenfiles"), - }, - want: filepath.Join("goldenfiles", "TestFozBar.golden"), - }, - { - name: "custom suffix", - testName: "TestFozBaz", - fields: fields{ - suffix: stringPtr(".goldfile"), - }, - want: filepath.Join("testdata", "TestFozBaz.goldfile"), - }, - { - name: "custom dirname and suffix", - testName: "TestFozBar", - fields: fields{ - dirname: stringPtr("goldenfiles"), - suffix: stringPtr(".goldfile"), - }, - want: filepath.Join("goldenfiles", "TestFozBar.goldfile"), - }, - { - name: "invalid chars in test name", - testName: `TestFooBar/foo?<>:*|"bar`, - want: filepath.Join( - "testdata", "TestFooBar", "foo_______bar.golden", - ), - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if tt.fields.suffix == nil { - tt.fields.suffix = stringPtr(".golden") - } - if tt.fields.dirname == nil { - tt.fields.dirname = stringPtr("testdata") - } - - g := &golden{ - suffix: *tt.fields.suffix, - dirname: *tt.fields.dirname, - } - - mt := mocktesting.NewT(tt.testName) - - var got string - mocktesting.Go(func() { - got = g.File(mt) - }) - - assert.Equal(t, tt.want, got) - assert.Equal(t, tt.wantAborted, mt.Aborted(), "aborted") - assert.Equal(t, - tt.wantFailCount, mt.FailedCount(), "failed count", - ) - assert.Equal(t, tt.wantTestOutput, mt.Output(), "test output") - }) - } -} - -func Test_golden_Get(t *testing.T) { - type fields struct { - suffix *string - dirname *string - } - tests := []struct { - name string - testName string - fields fields - files map[string][]byte - want []byte - wantAborted bool - wantFailCount int - wantTestOutput []string - }{ - { - name: "file exists", - testName: "TestFooBar", - files: map[string][]byte{ - filepath.Join("testdata", "TestFooBar.golden"): []byte( - "foo: bar\nhello: world", - ), - }, - want: []byte("foo: bar\nhello: world"), - }, - { - name: "file is missing", - testName: "TestFooBar", - files: map[string][]byte{}, - wantAborted: true, - wantFailCount: 1, - wantTestOutput: []string{ - "golden: open " + filepath.Join( - "testdata", "TestFooBar.golden", - ) + ": file does not exist\n", - }, - }, - { - name: "sub-test file exists", - testName: "TestFooBar/it_is_here", - files: map[string][]byte{ - filepath.Join( - "testdata", "TestFooBar", "it_is_here.golden", - ): []byte("this is really here ^_^\n"), - }, - want: []byte("this is really here ^_^\n"), - }, - { - name: "sub-test file is missing", - testName: "TestFooBar/not_really_here", - files: map[string][]byte{}, - wantAborted: true, - wantFailCount: 1, - wantTestOutput: []string{ - "golden: open " + filepath.Join( - "testdata", "TestFooBar", "not_really_here.golden", - ) + ": file does not exist\n", - }, - }, - { - name: "blank test name", - testName: "", - wantAborted: true, - wantFailCount: 1, - wantTestOutput: []string{ - "golden: could not determine filename for given " + - "*mocktesting.T instance\n", - }, - }, - { - name: "custom dirname", - testName: "TestFozBar", - fields: fields{ - dirname: stringPtr("goldenfiles"), - }, - files: map[string][]byte{ - filepath.Join("goldenfiles", "TestFozBar.golden"): []byte( - "foo: bar\nhello: world", - ), - }, - want: []byte("foo: bar\nhello: world"), - }, - { - name: "custom suffix", - testName: "TestFozBaz", - fields: fields{ - suffix: stringPtr(".goldfile"), - }, - files: map[string][]byte{ - filepath.Join("testdata", "TestFozBaz.goldfile"): []byte( - "foo: bar\nhello: world", - ), - }, - want: []byte("foo: bar\nhello: world"), - }, - { - name: "custom dirname and suffix", - testName: "TestFozBar", - fields: fields{ - dirname: stringPtr("goldenfiles"), - suffix: stringPtr(".goldfile"), - }, - files: map[string][]byte{ - filepath.Join("goldenfiles", "TestFozBar.goldfile"): []byte( - "foo: bar\nhello: world", - ), - }, - want: []byte("foo: bar\nhello: world"), - }, - { - name: "invalid chars in test name", - testName: `TestFooBar/foo?<>:*|"bar`, - files: map[string][]byte{ - filepath.Join( - "testdata", "TestFooBar", "foo_______bar.golden", - ): []byte("foo: bar\nhello: world"), - }, - want: []byte("foo: bar\nhello: world"), - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - fs := afero.NewMemMapFs() - for f, b := range tt.files { - _ = afero.WriteFile(fs, f, b, 0o644) - } - - if tt.fields.suffix == nil { - tt.fields.suffix = stringPtr(".golden") - } - if tt.fields.dirname == nil { - tt.fields.dirname = stringPtr("testdata") - } - - g := &golden{ - suffix: *tt.fields.suffix, - dirname: *tt.fields.dirname, - fs: fs, - } - - mt := mocktesting.NewT(tt.testName) - - var got []byte - mocktesting.Go(func() { - got = g.Get(mt) - }) - - assert.Equal(t, tt.want, got) - assert.Equal(t, tt.wantAborted, mt.Aborted(), "aborted") - assert.Equal(t, - tt.wantFailCount, mt.FailedCount(), "failed count", - ) - assert.Equal(t, tt.wantTestOutput, mt.Output(), "test output") - }) - } -} - -func Test_golden_FileP(t *testing.T) { - type args struct { - name string - } - type fields struct { - suffix *string - dirname *string - } - tests := []struct { - name string - testName string - args args - fields fields - want string - wantAborted bool - wantFailCount int - wantTestOutput []string - }{ - { - name: "top-level", - testName: "TestFooBar", - args: args{name: "yaml"}, - want: filepath.Join("testdata", "TestFooBar", "yaml.golden"), - }, - { - name: "sub-test", - testName: "TestFooBar/it_is_here", - args: args{name: "json"}, - want: filepath.Join( - "testdata", "TestFooBar", "it_is_here", "json.golden", - ), - }, - { - name: "blank test name", - testName: "", - args: args{name: "json"}, - wantAborted: true, - wantFailCount: 1, - wantTestOutput: []string{ - "golden: could not determine filename for given " + - "*mocktesting.T instance\n", - }, - }, - { - name: "custom dirname", - testName: "TestFozBar", - args: args{name: "xml"}, - fields: fields{ - dirname: stringPtr("goldenfiles"), - }, - want: filepath.Join("goldenfiles", "TestFozBar", "xml.golden"), - }, - { - name: "custom suffix", - testName: "TestFozBaz", - args: args{name: "toml"}, - fields: fields{ - suffix: stringPtr(".goldfile"), - }, - want: filepath.Join("testdata", "TestFozBaz", "toml.goldfile"), - }, - { - name: "custom dirname and suffix", - testName: "TestFozBar", - args: args{name: "json"}, - fields: fields{ - dirname: stringPtr("goldenfiles"), - suffix: stringPtr(".goldfile"), - }, - want: filepath.Join("goldenfiles", "TestFozBar", "json.goldfile"), - }, - { - name: "invalid chars in test name", - testName: `TestFooBar/foo?<>:*|"bar`, - args: args{name: "yml"}, - want: filepath.Join( - "testdata", "TestFooBar", "foo_______bar", "yml.golden", - ), - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if tt.fields.suffix == nil { - tt.fields.suffix = stringPtr(".golden") - } - if tt.fields.dirname == nil { - tt.fields.dirname = stringPtr("testdata") - } - - g := &golden{ - suffix: *tt.fields.suffix, - dirname: *tt.fields.dirname, - } - - mt := mocktesting.NewT(tt.testName) - - var got string - mocktesting.Go(func() { - got = g.FileP(mt, tt.args.name) - }) - - assert.Equal(t, tt.want, got) - assert.Equal(t, tt.wantAborted, mt.Aborted(), "aborted") - assert.Equal(t, - tt.wantFailCount, mt.FailedCount(), "failed count", - ) - assert.Equal(t, tt.wantTestOutput, mt.Output(), "test output") - }) - } -} - -func Test_golden_GetP(t *testing.T) { - type args struct { - name string - } - type fields struct { - suffix *string - dirname *string - } - tests := []struct { - name string - testName string - args args - fields fields - files map[string][]byte - want []byte - wantAborted bool - wantFailCount int - wantTestOutput []string - }{ - { - name: "file exists", - testName: "TestFooBar", - args: args{name: "yaml"}, - files: map[string][]byte{ - filepath.Join("testdata", "TestFooBar", "yaml.golden"): []byte( - "foo: bar\nhello: world", - ), - }, - want: []byte("foo: bar\nhello: world"), - }, - { - name: "file is missing", - testName: "TestFooBar", - args: args{name: "yaml"}, - files: map[string][]byte{}, - wantAborted: true, - wantFailCount: 1, - wantTestOutput: []string{ - "golden: open " + filepath.Join( - "testdata", "TestFooBar", "yaml.golden", - ) + ": file does not exist\n", - }, - }, - { - name: "sub-test file exists", - testName: "TestFooBar/it_is_here", - args: args{name: "plain"}, - files: map[string][]byte{ - filepath.Join( - "testdata", "TestFooBar", "it_is_here", "plain.golden", - ): []byte("this is really here ^_^\n"), - }, - want: []byte("this is really here ^_^\n"), - }, - { - name: "sub-test file is missing", - testName: "TestFooBar/not_really_here", - args: args{name: "plain"}, - files: map[string][]byte{}, - wantAborted: true, - wantFailCount: 1, - wantTestOutput: []string{ - "golden: open " + filepath.Join( - "testdata", "TestFooBar", "not_really_here", "plain.golden", - ) + ": file does not exist\n", - }, - }, - { - name: "blank test name", - testName: "", - args: args{name: "plain"}, - wantAborted: true, - wantFailCount: 1, - wantTestOutput: []string{ - "golden: could not determine filename for given " + - "*mocktesting.T instance\n", - }, - }, - { - name: "blank name", - testName: "TestFooBar", - args: args{name: ""}, - wantAborted: true, - wantFailCount: 1, - wantTestOutput: []string{ - "golden: name cannot be empty\n", - }, - }, - { - name: "custom dirname", - testName: "TestFozBar", - args: args{name: "yaml"}, - fields: fields{ - dirname: stringPtr("goldenfiles"), - }, - files: map[string][]byte{ - filepath.Join( - "goldenfiles", "TestFozBar", "yaml.golden", - ): []byte("foo: bar\nhello: world"), - }, - want: []byte("foo: bar\nhello: world"), - }, - { - name: "custom suffix", - testName: "TestFozBaz", - args: args{name: "yaml"}, - fields: fields{ - suffix: stringPtr(".goldfile"), - }, - files: map[string][]byte{ - filepath.Join( - "testdata", "TestFozBaz", "yaml.goldfile", - ): []byte("foo: bar\nhello: world"), - }, - want: []byte("foo: bar\nhello: world"), - }, - { - name: "custom dirname and suffix", - testName: "TestFozBar", - args: args{name: "yaml"}, - fields: fields{ - dirname: stringPtr("goldenfiles"), - suffix: stringPtr(".goldfile"), - }, - files: map[string][]byte{ - filepath.Join( - "goldenfiles", "TestFozBar", "yaml.goldfile", - ): []byte("foo: bar\nhello: world"), - }, - want: []byte("foo: bar\nhello: world"), - }, - { - name: "invalid chars in test name", - testName: `TestFooBar/foo?<>:*|"bar`, - args: args{name: "trash"}, - files: map[string][]byte{ - filepath.Join( - "testdata", "TestFooBar", "foo_______bar", "trash.golden", - ): []byte("foo: bar\nhello: world"), - }, - want: []byte("foo: bar\nhello: world"), - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - fs := afero.NewMemMapFs() - for f, b := range tt.files { - _ = afero.WriteFile(fs, f, b, 0o644) - } - - if tt.fields.suffix == nil { - tt.fields.suffix = stringPtr(".golden") - } - if tt.fields.dirname == nil { - tt.fields.dirname = stringPtr("testdata") - } - - g := &golden{ - suffix: *tt.fields.suffix, - dirname: *tt.fields.dirname, - fs: fs, - } - - mt := mocktesting.NewT(tt.testName) - - var got []byte - mocktesting.Go(func() { - got = g.GetP(mt, tt.args.name) - }) - - assert.Equal(t, tt.want, got) - assert.Equal(t, tt.wantAborted, mt.Aborted(), "aborted") - assert.Equal(t, - tt.wantFailCount, mt.FailedCount(), "failed count", - ) - assert.Equal(t, tt.wantTestOutput, mt.Output(), "test output") + assert.Equal(t, tt.want.logOnWrite, got.logOnWrite) }) } } diff --git a/options.go b/options.go new file mode 100644 index 0000000..60cd9ca --- /dev/null +++ b/options.go @@ -0,0 +1,80 @@ +package golden + +import "os" + +type Option interface { + apply(*gold) +} + +type optionFunc func(*gold) + +func (fn optionFunc) apply(g *gold) { + fn(g) +} + +// WithDirMode sets the file system permissions used for any folders created to +// hold golden files. +// +// When this option is not provided, the default value is 0o755. +func WithDirMode(mode os.FileMode) Option { + return optionFunc(func(g *gold) { + g.dirMode = mode + }) +} + +// WithFileMode sets the file system permissions used for any created or updated +// golden files written to. +// +// When this option is not provided, the default value is 0o644. +func WithFileMode(mode os.FileMode) Option { + return optionFunc(func(g *gold) { + g.fileMode = mode + }) +} + +// WithSuffix sets the filename suffix used for all golden files. +// +// When this option is not provided, the default value is ".golden". +func WithSuffix(suffix string) Option { + return optionFunc(func(g *gold) { + g.suffix = suffix + }) +} + +// WithDirname sets the name of the top-level directory used to hold golden +// files. +// +// When this option is not provided, the default value is "testdata". +func WithDirname(name string) Option { + return optionFunc(func(g *gold) { + g.dirname = name + }) +} + +// WithUpdateFunc sets the function used to determine if golden files should be +// updated or not. Essentially the provided UpdateFunc is called by Update(). +// +// When this option is not provided, the default value is EnvUpdateFunc. +func WithUpdateFunc(fn UpdateFunc) Option { + return optionFunc(func(g *gold) { + g.updateFunc = fn + }) +} + +// 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 FS) Option { + return optionFunc(func(g *gold) { + g.fs = fs + }) +} + +// WithSilentWrites silences the "golden: writing [...]" log messages whenever +// set functions write a golden file to disk. +func WithSilentWrites() Option { + return optionFunc(func(g *gold) { + g.logOnWrite = false + }) +} diff --git a/test/testfs/testfs.go b/test/testfs/testfs.go new file mode 100644 index 0000000..a7e8d90 --- /dev/null +++ b/test/testfs/testfs.go @@ -0,0 +1,275 @@ +package testfs + +import ( + "errors" + "os" + "path" + "strings" +) + +type Node struct { + data []byte + perm os.FileMode + isDir bool +} + +type FS struct { + Pwd string + Nodes map[string]*Node +} + +func New() *FS { + return &FS{ + Pwd: "/root", + Nodes: map[string]*Node{ + "/": {perm: 0o755, isDir: true}, + "/root": {perm: 0o700, isDir: true}, + }, + } +} + +func (fs *FS) MkdirAll(name string, perm os.FileMode) error { + if !path.IsAbs(name) && name != "" { + name = path.Join(fs.Pwd, name) + } + + dirs := []string{name} + for d := path.Dir(name); d != "/"; d = path.Dir(d) { + dirs = append(dirs, d) + } + dirs = append(dirs, "/") + + for i := len(dirs) - 1; i >= 0; i-- { + dir := dirs[i] + parent := path.Dir(dir) + + if info, ok := fs.Nodes[dir]; ok { + if !info.isDir { + return &os.PathError{ + Op: "mkdir", + Path: dir, + Err: errors.New("not a directory"), + } + } + + continue + } + + parentInfo, ok := fs.Nodes[parent] + if !ok { + return &os.PathError{ + Op: "mkdir", + Path: parent, + Err: errors.New("no such file or directory"), + } + } + if !parentInfo.isDir { + return &os.PathError{ + Op: "mkdir", + Path: parent, + Err: errors.New("not a directory"), + } + } + // Ensure all parent directories have execute permissions, and direct + // parent also has write permission. + if parentInfo.perm&0o100 == 0 || i == 1 && parentInfo.perm&0o200 == 0 { + return &os.PathError{ + Op: "mkdir", + Path: dir, + Err: errors.New("permission denied"), + } + } + + fs.Nodes[dir] = &Node{perm: perm, isDir: true} + } + + return nil +} + +func (fs *FS) ReadFile(name string) ([]byte, error) { + if !path.IsAbs(name) && name != "" { + name = path.Join(fs.Pwd, name) + } + + _, err := fs.checkParents(name, false) + if err != nil { + return nil, err + } + + info, ok := fs.Nodes[name] + if !ok { + return nil, &os.PathError{ + Op: "open", + Path: name, + Err: errors.New("no such file or directory"), + } + } + if info.isDir { + return nil, &os.PathError{ + Op: "open", + Path: name, + Err: errors.New("is a directory"), + } + } + if info.perm&0o400 == 0 { + return nil, &os.PathError{ + Op: "open", + Path: name, + Err: errors.New("permission denied"), + } + } + + return info.data, nil +} + +func (fs *FS) WriteFile(name string, data []byte, perm os.FileMode) error { + if !path.IsAbs(name) && name != "" { + name = path.Join(fs.Pwd, name) + } + + parent, err := fs.checkParents(name, true) + if err != nil { + return err + } + + info, ok := fs.Nodes[name] + if ok { + if info.isDir { + return &os.PathError{ + Op: "open", + Path: name, + Err: errors.New("is a directory"), + } + } + } + // Return error if file exists and has no write permission, or if the file + // does not exist and the direct parent has no write permission. + if ok && info.perm&0o200 == 0 || !ok && parent.perm&0o200 == 0 { + return &os.PathError{ + Op: "open", + Path: name, + Err: errors.New("permission denied"), + } + } + + fs.Nodes[name] = &Node{data: data, perm: perm} + + return nil +} + +func (fs *FS) Remove(name string) error { + if !path.IsAbs(name) && name != "" { + name = path.Join(fs.Pwd, name) + } + + parent, err := fs.checkParents(name, false) + if err != nil { + return err + } + + if parent != nil && parent.perm&0o200 == 0 { + return &os.PathError{ + Op: "remove", + Path: name, + Err: errors.New("permission denied"), + } + } + + info, ok := fs.Nodes[name] + if !ok { + return &os.PathError{ + Op: "remove", + Path: name, + Err: errors.New("no such file or directory"), + } + } + if info.perm&0o200 == 0 { + return &os.PathError{ + Op: "remove", + Path: name, + Err: errors.New("permission denied"), + } + } + if info.isDir { + for p := range fs.Nodes { + if strings.HasPrefix(p, name) && p != name { + return &os.PathError{ + Op: "remove", + Path: name, + Err: errors.New("directory not empty"), + } + } + } + } + + delete(fs.Nodes, name) + + return nil +} + +func (fs *FS) Exists(name string) bool { + if !path.IsAbs(name) && name != "" { + name = path.Join(fs.Pwd, name) + } + + _, ok := fs.Nodes[name] + + return ok +} + +func (fs *FS) FileMode(name string) (os.FileMode, error) { + if !path.IsAbs(name) && name != "" { + name = path.Join(fs.Pwd, name) + } + + if info, ok := fs.Nodes[name]; ok { + return info.perm, nil + } + + return 0, &os.PathError{ + Op: "open", + Path: name, + Err: os.ErrNotExist, + } +} + +func (fs *FS) checkParents(absPath string, noExistError bool) (*Node, error) { + var parents []string + for d := path.Dir(absPath); d != "/"; d = path.Dir(d) { + parents = append(parents, d) + } + parents = append(parents, "/") + var directParent *Node + + for i := 0; i < len(parents); i++ { + dir := parents[i] + info, ok := fs.Nodes[dir] + if !ok && noExistError { + return nil, &os.PathError{ + Op: "open", + Path: dir, + Err: errors.New("no such file or directory"), + } + } + if info != nil && !info.isDir { + return nil, &os.PathError{ + Op: "open", + Path: dir, + Err: errors.New("not a directory"), + } + } + // Ensure all parent directories have execute permissions. + if info != nil && info.perm&0o100 == 0 { + return nil, &os.PathError{ + Op: "open", + Path: dir, + Err: errors.New("permission denied"), + } + } + if i == 0 { + directParent = info + } + } + + return directParent, nil +} diff --git a/test/testfs/testfs_test.go b/test/testfs/testfs_test.go new file mode 100644 index 0000000..db975b2 --- /dev/null +++ b/test/testfs/testfs_test.go @@ -0,0 +1,670 @@ +package testfs + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestFSMkdirAll(t *testing.T) { + type args struct { + path string + perm os.FileMode + } + + tests := []struct { + name string + args args + nodes map[string]*Node + want map[string]*Node + wantErr bool + }{ + { + name: "create relative new dir", + args: args{path: "newdir", perm: 0o755}, + want: map[string]*Node{ + "/root/newdir": {perm: 0o755, isDir: true}, + }, + }, + { + name: "create absolute new dir", + args: args{path: "/opt/newdir", perm: 0o755}, + want: map[string]*Node{ + "/opt": {perm: 0o755, isDir: true}, + "/opt/newdir": {perm: 0o755, isDir: true}, + }, + }, + + { + name: "create relative nested dirs", + args: args{path: "nested/dir/structure", perm: 0o755}, + want: map[string]*Node{ + "/root/nested": {perm: 0o755, isDir: true}, + "/root/nested/dir": {perm: 0o755, isDir: true}, + "/root/nested/dir/structure": {perm: 0o755, isDir: true}, + }, + }, + { + name: "create absolute nested dirs", + args: args{path: "/opt/nested/dir/structure", perm: 0o755}, + want: map[string]*Node{ + "/opt": {perm: 0o755, isDir: true}, + "/opt/nested": {perm: 0o755, isDir: true}, + "/opt/nested/dir": {perm: 0o755, isDir: true}, + "/opt/nested/dir/structure": {perm: 0o755, isDir: true}, + }, + }, + { + name: "create relative nested dirs with other perms", + args: args{path: "nested/dir/structure", perm: 0o750}, + want: map[string]*Node{ + "/root/nested": {perm: 0o750, isDir: true}, + "/root/nested/dir": {perm: 0o750, isDir: true}, + "/root/nested/dir/structure": {perm: 0o750, isDir: true}, + }, + }, + { + name: "create absolute nested dirs with other perms", + args: args{path: "/opt/nested/dir/structure", perm: 0o750}, + want: map[string]*Node{ + "/opt": {perm: 0o750, isDir: true}, + "/opt/nested": {perm: 0o750, isDir: true}, + "/opt/nested/dir": {perm: 0o750, isDir: true}, + "/opt/nested/dir/structure": {perm: 0o750, isDir: true}, + }, + }, + { + name: "create relative nested dirs with existing dirs", + args: args{path: "nested/dir/structure", perm: 0o755}, + want: map[string]*Node{ + "/root/nested": {perm: 0o755, isDir: true}, + "/root/nested/dir": {perm: 0o755, isDir: true}, + "/root/nested/dir/structure": {perm: 0o755, isDir: true}, + }, + }, + { + name: "create absolute nested dirs with existing dirs", + args: args{path: "/root/nested/dir/structure", perm: 0o755}, + want: map[string]*Node{ + "/root/nested": {perm: 0o755, isDir: true}, + "/root/nested/dir": {perm: 0o755, isDir: true}, + "/root/nested/dir/structure": {perm: 0o755, isDir: true}, + }, + }, + { + name: "create relative under file", + args: args{path: "file/newdir", perm: 0o755}, + nodes: map[string]*Node{ + "/root/file": {perm: 0o644}, + }, + wantErr: true, + }, + { + name: "create absolute under file", + args: args{path: "/root/file/newdir", perm: 0o755}, + nodes: map[string]*Node{ + "/root/file": {perm: 0o644}, + }, + wantErr: true, + }, + { + name: "create relative directory without execute permission", + args: args{path: "dir/newdir", perm: 0o755}, + nodes: map[string]*Node{ + "/root": {perm: 0o644}, + }, + wantErr: true, + }, + { + name: "create absolute directory without execute permission", + args: args{path: "/root/dir/newdir", perm: 0o755}, + nodes: map[string]*Node{ + "/root": {perm: 0o644}, + }, + wantErr: true, + }, + { + name: "create relative directory without write permission", + args: args{path: "dir/newdir", perm: 0o755}, + nodes: map[string]*Node{ + "/root": {perm: 0o444}, + }, + wantErr: true, + }, + { + name: "create absolute directory without write permission", + args: args{path: "/root/dir/newdir", perm: 0o755}, + nodes: map[string]*Node{ + "/root": {perm: 0o444}, + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fs := &FS{ + Pwd: "/root", + Nodes: map[string]*Node{ + "/": {perm: 0o755, isDir: true}, + "/root": {perm: 0o700, isDir: true}, + }, + } + + for fp, info := range tt.nodes { + fs.Nodes[fp] = info + } + + err := fs.MkdirAll(tt.args.path, tt.args.perm) + + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + + for fp, info := range tt.want { + got := fs.Nodes[fp] + assert.Equal(t, info, got, "path: %s", fp) + } + } + }) + } +} + +func TestFSReadFile(t *testing.T) { + type args struct { + name string + } + + tests := []struct { + name string + args args + nodes map[string]*Node + want []byte + wantErr bool + }{ + { + name: "relative read existing file", + args: args{name: "file.txt"}, + nodes: map[string]*Node{ + "/": {perm: 0o755, isDir: true}, + "/root": {perm: 0o755, isDir: true}, + "/root/file.txt": {data: []byte("file content"), perm: 0o644}, + }, + want: []byte("file content"), + }, + { + name: "absolute read existing file", + args: args{name: "/opt/file.txt"}, + nodes: map[string]*Node{ + "/": {perm: 0o755, isDir: true}, + "/opt": {perm: 0o755, isDir: true}, + "/opt/file.txt": {data: []byte("file content"), perm: 0o644}, + }, + want: []byte("file content"), + }, + { + name: "relative file does not exist", + args: args{name: "nonexistent.txt"}, + nodes: map[string]*Node{ + "/": {perm: 0o755, isDir: true}, + "/root": {perm: 0o755, isDir: true}, + }, + wantErr: true, + }, + { + name: "absolute file does not exist", + args: args{name: "/opt/nonexistent.txt"}, + nodes: map[string]*Node{ + "/": {perm: 0o755, isDir: true}, + "/opt": {perm: 0o755, isDir: true}, + }, + wantErr: true, + }, + { + name: "relative file is a directory", + args: args{name: "dir"}, + nodes: map[string]*Node{ + "/": {perm: 0o755, isDir: true}, + "/root": {perm: 0o755, isDir: true}, + "/root/dir": {perm: 0o755, isDir: true}, + }, + wantErr: true, + }, + { + name: "absolute file is a directory", + args: args{name: "/opt/dir"}, + nodes: map[string]*Node{ + "/": {perm: 0o755, isDir: true}, + "/opt": {perm: 0o755, isDir: true}, + "/opt/dir": {perm: 0o755, isDir: true}, + }, + wantErr: true, + }, + { + name: "relative file permission denied", + args: args{name: "file.txt"}, + nodes: map[string]*Node{ + "/": {perm: 0o755, isDir: true}, + "/root": {perm: 0o755, isDir: true}, + "/root/file.txt": {data: []byte("file content"), perm: 0o200}, + }, + wantErr: true, + }, + { + name: "relative no directory read permission", + args: args{name: "file.txt"}, + nodes: map[string]*Node{ + "/": {perm: 0o755, isDir: true}, + "/root": {perm: 0o355, isDir: true}, + "/root/file.txt": {data: []byte("file content"), perm: 0o644}, + }, + want: []byte("file content"), + }, + { + name: "relative no directory execute permission", + args: args{name: "file.txt"}, + nodes: map[string]*Node{ + "/": {perm: 0o755, isDir: true}, + "/root": {perm: 0o655, isDir: true}, + "/root/file.txt": {data: []byte("file content"), perm: 0o200}, + }, + wantErr: true, + }, + { + name: "relative no grandparent directory execute permission", + args: args{name: "foo/file.txt"}, + nodes: map[string]*Node{ + "/": {perm: 0o755, isDir: true}, + "/root": {perm: 0o655, isDir: true}, + "/root/foo": {perm: 0o755, isDir: true}, + "/root/foo/file.txt": {data: []byte("hello"), perm: 0o200}, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fs := &FS{ + Pwd: "/root", + Nodes: tt.nodes, + } + + got, err := fs.ReadFile(tt.args.name) + + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.want, got) + } + }) + } +} + +func TestFSWriteFile(t *testing.T) { + type args struct { + name string + data []byte + perm os.FileMode + } + + tests := []struct { + name string + args args + nodes map[string]*Node + wantPath string + wantErr bool + }{ + { + name: "relative write to new file", + args: args{ + name: "newfile.txt", + data: []byte("new content"), + perm: 0o644, + }, + wantPath: "/tmp/newfile.txt", + nodes: map[string]*Node{ + "/": {perm: 0o755, isDir: true}, + "/tmp": {perm: 0o755, isDir: true}, + }, + }, + { + name: "absolute write to new file", + args: args{ + name: "/opt/newfile.txt", + data: []byte("new content"), + perm: 0o644, + }, + wantPath: "/opt/newfile.txt", + nodes: map[string]*Node{ + "/": {perm: 0o755, isDir: true}, + "/opt": {perm: 0o755, isDir: true}, + }, + }, + { + name: "relative overwrite existing file", + args: args{ + name: "existing.txt", + data: []byte("overwritten"), + perm: 0o644, + }, + wantPath: "/tmp/existing.txt", + nodes: map[string]*Node{ + "/": {perm: 0o755, isDir: true}, + "/tmp": {perm: 0o755, isDir: true}, + "/tmp/existing": {data: []byte("existing"), perm: 0o644}, + }, + }, + { + name: "absolute overwrite existing file", + args: args{ + name: "/opt/existing.txt", + data: []byte("overwritten"), + perm: 0o644, + }, + wantPath: "/opt/existing.txt", + nodes: map[string]*Node{ + "/": {perm: 0o755, isDir: true}, + "/opt": {perm: 0o755, isDir: true}, + "/opt/existing": {data: []byte("existing"), perm: 0o644}, + }, + }, + { + name: "relative overwrite file permissions denied", + args: args{ + name: "existing.txt", + data: []byte("overwritten"), + perm: 0o644, + }, + wantPath: "/tmp/existing.txt", + nodes: map[string]*Node{ + "/": {perm: 0o755, isDir: true}, + "/tmp": {perm: 0o755, isDir: true}, + "/tmp/existing.txt": {data: []byte("existing"), perm: 0o400}, + }, + wantErr: true, + }, + { + name: "absolute overwrite file permissions denied", + args: args{ + name: "/opt/existing.txt", + data: []byte("overwritten"), + perm: 0o644, + }, + wantPath: "/opt/existing.txt", + nodes: map[string]*Node{ + "/": {perm: 0o755, isDir: true}, + "/opt": {perm: 0o755, isDir: true}, + "/opt/existing.txt": {data: []byte("existing"), perm: 0o400}, + }, + wantErr: true, + }, + { + name: "relative overwrite directory", + args: args{ + name: "dir", + data: []byte("overwritten"), + perm: 0o644, + }, + wantPath: "/tmp/dir", + nodes: map[string]*Node{ + "/": {perm: 0o755, isDir: true}, + "/tmp": {perm: 0o755, isDir: true}, + "/tmp/dir": {perm: 0o644, isDir: true}, + }, + wantErr: true, + }, + { + name: "absolute overwrite directory", + args: args{ + name: "/opt/dir", + data: []byte("overwritten"), + perm: 0o644, + }, + wantPath: "/opt/dir", + nodes: map[string]*Node{ + "/": {perm: 0o755, isDir: true}, + "/opt": {perm: 0o755, isDir: true}, + "/opt/dir": {perm: 0o644, isDir: true}, + }, + wantErr: true, + }, + { + name: "relative write to non-existent directory", + args: args{ + name: "nonexistentdir/newfile.txt", + data: []byte("this will fail"), + perm: 0o644, + }, + wantPath: "/tmp/nonexistentdir/newfile.txt", + nodes: map[string]*Node{ + "/": {perm: 0o755, isDir: true}, + "/tmp": {perm: 0o755, isDir: true}, + }, + wantErr: true, + }, + { + name: "absolute write to non-existent directory", + args: args{ + name: "/opt/nonexistentdir/newfile.txt", + data: []byte("this will fail"), + perm: 0o644, + }, + wantPath: "/opt/nonexistentdir/newfile.txt", + nodes: map[string]*Node{ + "/": {perm: 0o755, isDir: true}, + "/opt": {perm: 0o755, isDir: true}, + }, + wantErr: true, + }, + { + name: "relative write parent directory is a file", + args: args{ + name: "file/newfile.txt", + data: []byte("this will fail"), + perm: 0o644, + }, + wantPath: "/tmp/file/newfile.txt", + nodes: map[string]*Node{ + "/": {perm: 0o755, isDir: true}, + "/tmp": {perm: 0o755, isDir: true}, + "/tmp/file": {data: []byte("file content"), perm: 0o644}, + }, + wantErr: true, + }, + { + name: "relative no parent directory write permission denied", + args: args{ + name: "dir/newfile.txt", + data: []byte("this will fail"), + perm: 0o644, + }, + wantPath: "/tmp/dir/newfile.txt", + nodes: map[string]*Node{ + "/": {perm: 0o755, isDir: true}, + "/tmp": {perm: 0o755, isDir: true}, + "/tmp/dir": {perm: 0o500, isDir: true}, + }, + wantErr: true, + }, + { + name: "relative no parent directory execute permission denied", + args: args{ + name: "dir/newfile.txt", + data: []byte("this will fail"), + perm: 0o644, + }, + wantPath: "/tmp/dir/newfile.txt", + nodes: map[string]*Node{ + "/": {perm: 0o755, isDir: true}, + "/tmp": {perm: 0o755, isDir: true}, + "/tmp/dir": {perm: 0o600, isDir: true}, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fs := &FS{ + Pwd: "/tmp", + Nodes: tt.nodes, + } + + err := fs.WriteFile(tt.args.name, tt.args.data, tt.args.perm) + + if tt.wantErr { + assert.Error(t, err) + if _, ok := tt.nodes[tt.wantPath]; ok { + assert.Equal(t, + tt.nodes[tt.wantPath], + fs.Nodes[tt.wantPath], + ) + } else { + assert.NotContains(t, fs.Nodes, tt.wantPath) + } + } else { + assert.NoError(t, err) + + got := fs.Nodes[tt.wantPath] + assert.Equal(t, tt.args.data, got.data) + assert.Equal(t, tt.args.perm, got.perm) + assert.Equal(t, false, got.isDir) + } + }) + } +} + +func TestFSRemove(t *testing.T) { + type args struct { + name string + } + + tests := []struct { + name string + args args + nodes map[string]*Node + wantErr bool + }{ + { + name: "relative remove existing file", + args: args{name: "file.txt"}, + nodes: map[string]*Node{ + "/": {perm: 0o755, isDir: true}, + "/root": {perm: 0o755, isDir: true}, + "/root/file.txt": {data: []byte("file content"), perm: 0o644}, + }, + }, + { + name: "absolute remove existing file", + args: args{name: "/opt/file.txt"}, + nodes: map[string]*Node{ + "/": {perm: 0o755, isDir: true}, + "/opt": {perm: 0o755, isDir: true}, + "/opt/file.txt": {data: []byte("file content"), perm: 0o644}, + }, + }, + { + name: "relative file does not exist", + args: args{name: "nonexistent.txt"}, + nodes: map[string]*Node{ + "/": {perm: 0o755, isDir: true}, + "/root": {perm: 0o755, isDir: true}, + }, + wantErr: true, + }, + { + name: "absolute file does not exist", + args: args{name: "/opt/nonexistent.txt"}, + nodes: map[string]*Node{ + "/": {perm: 0o755, isDir: true}, + "/opt": {perm: 0o755, isDir: true}, + }, + wantErr: true, + }, + { + name: "relative file is a directory", + args: args{name: "dir"}, + nodes: map[string]*Node{ + "/": {perm: 0o755, isDir: true}, + "/root": {perm: 0o755, isDir: true}, + "/root/dir": {perm: 0o755, isDir: true}, + }, + }, + { + name: "absolute file is a directory", + args: args{name: "/opt/dir"}, + nodes: map[string]*Node{ + "/": {perm: 0o755, isDir: true}, + "/opt": {perm: 0o755, isDir: true}, + "/opt/dir": {perm: 0o755, isDir: true}, + }, + }, + { + name: "relative file permission denied", + args: args{name: "file.txt"}, + nodes: map[string]*Node{ + "/root": {perm: 0o755, isDir: true}, + "/root/file.txt": {data: []byte("file content"), perm: 0o400}, + }, + wantErr: true, + }, + { + name: "absolute file permission denied", + args: args{name: "/opt/file.txt"}, + nodes: map[string]*Node{ + "/": {perm: 0o755, isDir: true}, + "/opt": {perm: 0o755, isDir: true}, + "/opt/file.txt": {data: []byte("file content"), perm: 0o400}, + }, + wantErr: true, + }, + { + name: "relative no directory write permission", + args: args{name: "file.txt"}, + nodes: map[string]*Node{ + "/root": {perm: 0o555, isDir: true}, + "/root/file.txt": {data: []byte("file content"), perm: 0o644}, + }, + wantErr: true, + }, + { + name: "relative no directory execute permission", + args: args{name: "file.txt"}, + nodes: map[string]*Node{ + "/root": {perm: 0o655, isDir: true}, + "/root/file.txt": {data: []byte("file content"), perm: 0o644}, + }, + wantErr: true, + }, + { + name: "relative no grandparent directory execute permission", + args: args{name: "file.txt"}, + nodes: map[string]*Node{ + "/root": {perm: 0o655, isDir: true}, + "/root/dir": {perm: 0o755, isDir: true}, + "/root/dir/file.txt": { + data: []byte("file content"), perm: 0o644, + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fs := &FS{ + Pwd: "/root", + Nodes: tt.nodes, + } + + err := fs.Remove(tt.args.name) + + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/testingt.go b/testingt.go new file mode 100644 index 0000000..e0a74c6 --- /dev/null +++ b/testingt.go @@ -0,0 +1,10 @@ +package golden + +// TestingT is a interface describing a sub-set of methods of *testing.T which +// golden uses. +type TestingT interface { + Fatalf(format string, args ...interface{}) + Helper() + Logf(format string, args ...interface{}) + Name() string +} diff --git a/testingt_test.go b/testingt_test.go new file mode 100644 index 0000000..90013b1 --- /dev/null +++ b/testingt_test.go @@ -0,0 +1,30 @@ +package golden + +import ( + "fmt" + "runtime" +) + +type fakeTestingT struct { + helper bool + name string + logs []string + fatals []string +} + +func (m *fakeTestingT) Helper() { + m.helper = true +} + +func (m *fakeTestingT) Fatalf(format string, args ...interface{}) { + m.fatals = append(m.fatals, fmt.Sprintf(format, args...)) + runtime.Goexit() +} + +func (m *fakeTestingT) Logf(format string, args ...interface{}) { + m.logs = append(m.logs, fmt.Sprintf(format, args...)) +} + +func (m *fakeTestingT) Name() string { + return m.name +}