7 Commits

25 changed files with 3326 additions and 677 deletions

View File

@@ -7,11 +7,15 @@ jobs:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: golangci-lint
uses: golangci/golangci-lint-action@v2
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
version: v1.42
go-version: "1.18"
cache: false
- name: golangci-lint
uses: golangci/golangci-lint-action@v4
with:
version: v1.56
env:
VERBOSE: "true"
@@ -19,16 +23,10 @@ jobs:
name: Tidy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-go@v2
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: 1.15
- uses: actions/cache@v2
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
go-version: "1.18"
- name: Check if mods are tidy
run: make check-tidy
@@ -36,18 +34,12 @@ jobs:
name: Coverage
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-go@v2
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: 1.15
- uses: actions/cache@v2
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
go-version: "1.18"
- name: Publish coverage
uses: paambaati/codeclimate-action@v2.7.4
uses: paambaati/codeclimate-action@v5.0.0
env:
VERBOSE: "true"
GOMAXPROCS: 4
@@ -68,20 +60,16 @@ jobs:
- macos-latest
- windows-latest
go_version:
- "1.15"
- "1.16"
- "1.17"
- "1.18"
- "1.19"
- "1.20"
- "1.21"
- "1.22"
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- uses: actions/setup-go@v2
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go_version }}
- uses: actions/cache@v2
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
go-version: "1.22"
- name: Run tests
run: go test -v -count=1 -race ./...

5
.gitignore vendored
View File

@@ -1,5 +1,6 @@
bin/*
coverage.out
testdata/*
!testdata/TestExample*
testdata/TestFile*
testdata/TestGet*
testdata/TestSet*

View File

@@ -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:

View File

@@ -34,23 +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.42))
$(eval $(call tool,gomod,github.com/Helcaraxan/gomod))
$(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)

View File

@@ -70,4 +70,4 @@ for documentation and examples.
## License
[MIT](https://github.com/jimeh/go-golden/blob/master/LICENSE)
[MIT](https://github.com/jimeh/go-golden/blob/main/LICENSE)

46
fs.go Normal file
View File

@@ -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)
}

131
fs_test.go Normal file
View File

@@ -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)
}
})
}
}

10
go.mod
View File

@@ -1,8 +1,14 @@
module github.com/jimeh/go-golden
go 1.15
go 1.18
require (
github.com/jimeh/envctl v0.1.0
github.com/stretchr/testify v1.7.0
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
)

10
go.sum
View File

@@ -1,14 +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/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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
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/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.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/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=

173
gold.go Normal file
View File

@@ -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()
}

528
gold_test.go Normal file
View File

@@ -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")
// })
// }
// }

502
golden.go
View File

@@ -2,177 +2,271 @@
// with a focus on simplicity through it's default behavior.
//
// Golden file names are based on the name of the test function and any subtest
// names by calling t.Name(). File names are sanitized to ensure they're
// compatible with Linux, macOS and Windows systems regardless of what crazy
// names by calling t.Name(). File names are sanitized to ensure they are
// compatible with Linux, macOS and Windows systems regardless of what
// characters might be in a subtest's name.
//
// Usage
// # Usage
//
// Typical usage should look something like this:
//
// func TestExampleMyStruct(t *testing.T) {
// got, err := json.Marshal(&MyStruct{Foo: "Bar"})
// require.NoError(t, err)
// func TestExampleMyStruct(t *testing.T) {
// got, err := json.Marshal(&MyStruct{Foo: "Bar"})
// require.NoError(t, err)
//
// if golden.Update() {
// golden.Set(t, got)
// }
// want := golden.Get(t)
// want := golden.Do(t, got)
//
// assert.Equal(t, want, got)
// }
// assert.Equal(t, want, got)
// }
//
// The above example will read/write to:
// The above example will attempt to read/write to:
//
// testdata/TestExampleMyStruct.golden
// testdata/TestExampleMyStruct.golden
//
// The call to golden.Do() is equivalent to:
//
// if golden.Update() {
// golden.Set(t, got)
// }
// want := golden.Get(t)
//
// To update the golden file (have golden.Update() return true), simply set the
// GOLDEN_UPDATE environment variable to one of "1", "y", "t", "yes", "on", or
// "true" when running tests.
//
// Sub-Tests
// # Sub-Tests
//
// As the golden filename is based on t.Name(), it works with sub-tests too,
// ensuring each sub-test gets it's own golden file. For example:
//
// func TestExampleMyStructTabular(t *testing.T) {
// tests := []struct {
// name string
// obj *MyStruct
// }{
// {name: "empty struct", obj: &MyStruct{}},
// {name: "full struct", obj: &MyStruct{Foo: "Bar"}},
// }
// for _, tt := range tests {
// t.Run(tt.name, func(t *testing.T) {
// got, err := json.Marshal(tt.obj)
// require.NoError(t, err)
// func TestExampleMyStructTabular(t *testing.T) {
// tests := []struct {
// name string
// obj *MyStruct
// }{
// {name: "empty struct", obj: &MyStruct{}},
// {name: "full struct", obj: &MyStruct{Foo: "Bar"}},
// }
// for _, tt := range tests {
// t.Run(tt.name, func(t *testing.T) {
// got, err := json.Marshal(tt.obj)
// require.NoError(t, err)
//
// if golden.Update() {
// golden.Set(t, got)
// }
// want := golden.Get(t)
// want := golden.Do(t, got)
//
// assert.Equal(t, want, got)
// })
// }
// }
// assert.Equal(t, want, got)
// })
// }
// }
//
// The above example will read/write to:
//
// testdata/TestExampleMyStructTabular/empty_struct.golden
// testdata/TestExampleMyStructTabular/full_struct.golden
// testdata/TestExampleMyStructTabular/empty_struct.golden
// testdata/TestExampleMyStructTabular/full_struct.golden
//
// Multiple Golden Files in a Single Test
// # Multiple Golden Files in a Single Test
//
// The "P" suffixed methods, GetP(), SetP(), and FileP(), all take a name
// The "P" suffixed methods, GetP(), SetP(), DoP(), and FileP(), all take a name
// argument which allows using specific golden files within a given *testing.T
// instance.
//
// func TestExampleMyStructP(t *testing.T) {
// gotJSON, _ := json.Marshal(&MyStruct{Foo: "Bar"})
// gotXML, _ := xml.Marshal(&MyStruct{Foo: "Bar"})
// func TestExampleMyStructP(t *testing.T) {
// gotJSON, _ := json.Marshal(&MyStruct{Foo: "Bar"})
// gotXML, _ := xml.Marshal(&MyStruct{Foo: "Bar"})
//
// if golden.Update() {
// golden.SetP(t, "json", gotJSON)
// golden.SetP(t, "xml", gotXML)
// }
// wantJSON := golden.DoP(t, "json", gotJSON)
// wantXML := golden.DoP(t, "xml", gotXML)
//
// assert.Equal(t, golden.GetP(t, "json"), gotJSON)
// assert.Equal(t, golden.GetP(t, "xml"), gotXML)
// }
// assert.Equal(t, wantJSON, gotJSON)
// assert.Equal(t, wantXML, gotXML)
// }
//
// The above example will read/write to:
//
// testdata/TestExampleMyStructP/json.golden
// testdata/TestExampleMyStructP/xml.golden
// testdata/TestExampleMyStructP/json.golden
// testdata/TestExampleMyStructP/xml.golden
//
// This works with tabular tests too of course:
//
// func TestExampleMyStructTabularP(t *testing.T) {
// tests := []struct {
// name string
// obj *MyStruct
// }{
// {name: "empty struct", obj: &MyStruct{}},
// {name: "full struct", obj: &MyStruct{Foo: "Bar"}},
// }
// for _, tt := range tests {
// t.Run(tt.name, func(t *testing.T) {
// gotJSON, _ := json.Marshal(tt.obj)
// gotXML, _ := xml.Marshal(tt.obj)
// func TestExampleMyStructTabularP(t *testing.T) {
// tests := []struct {
// name string
// obj *MyStruct
// }{
// {name: "empty struct", obj: &MyStruct{}},
// {name: "full struct", obj: &MyStruct{Foo: "Bar"}},
// }
// for _, tt := range tests {
// t.Run(tt.name, func(t *testing.T) {
// gotJSON, _ := json.Marshal(tt.obj)
// gotXML, _ := xml.Marshal(tt.obj)
//
// if golden.Update() {
// golden.SetP(t, "json", gotJSON)
// golden.SetP(t, "xml", gotXML)
// }
// wantJSON := golden.DoP(t, "json", gotJSON)
// wantXML := golden.DoP(t, "xml", gotXML)
//
// assert.Equal(t, golden.GetP(t, "json"), gotJSON)
// assert.Equal(t, golden.GetP(t, "xml"), gotXML)
// })
// }
// }
// assert.Equal(t, wantJSON, gotJSON)
// assert.Equal(t, wantXML, gotXML)
// })
// }
// }
//
// The above example will read/write to:
//
// testdata/TestExampleMyStructTabularP/empty_struct/json.golden
// testdata/TestExampleMyStructTabularP/empty_struct/xml.golden
// testdata/TestExampleMyStructTabularP/full_struct/json.golden
// testdata/TestExampleMyStructTabularP/full_struct/xml.golden
//
// testdata/TestExampleMyStructTabularP/empty_struct/json.golden
// testdata/TestExampleMyStructTabularP/empty_struct/xml.golden
// testdata/TestExampleMyStructTabularP/full_struct/json.golden
// testdata/TestExampleMyStructTabularP/full_struct/xml.golden
package golden
import (
"io/ioutil"
"os"
"path/filepath"
"strings"
"testing"
import "os"
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
)
const (
DefaultDirMode = 0o755
DefaultFileMode = 0o644
DefaultSuffix = ".golden"
DefaultDirname = "testdata"
)
// Golden handles all interactions with golden files. The top-level package
// functions proxy through to a default global Golden instance.
type Golden interface {
// File returns the filename of the golden file for the given testing.TB
// instance as determined by t.Name().
File(t TestingT) string
var DefaultUpdateFunc = EnvUpdateFunc
// Get returns the content of the golden file for the given TestingT
// instance as determined by t.Name(). If no golden file can be found/read,
// it will fail the test by calling t.Fatal().
Get(t TestingT) []byte
var global = New()
// Set writes given data to the golden file for the given TestingT
// instance as determined by t.Name(). If writing fails it will fail the
// test by calling t.Fatal() with error details.
Set(t TestingT, data []byte)
// Do is a convenience function for calling Update(), Set(), and Get() in a
// single call. If Update() returns true, data will be written to the golden
// file using Set(), before reading it back with Get().
Do(t TestingT, data []byte) []byte
// FileP returns the filename of the specifically named golden file for the
// given TestingT instance as determined by t.Name().
FileP(t TestingT, name string) string
// GetP returns the content of the specifically named golden file belonging
// to the given TestingT 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 TestingT instance.
GetP(t TestingT, name string) []byte
// SetP writes given data of the specifically named golden file belonging to
// the given TestingT 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 TestingT instance.
SetP(t TestingT, name string, data []byte)
// DoP is a convenience function for calling Update(), SetP(), and GetP() in
// a single call. If Update() returns true, data will be written to the
// golden file using SetP(), before reading it back with GetP().
DoP(t TestingT, name string, data []byte) []byte
// Update returns true when golden is set to update golden files. Should be
// used to determine if golden.Set() or golden.SetP() should be called or
// not.
//
// Default behavior uses EnvUpdateFunc() to check if the "GOLDEN_UPDATE"
// environment variable is set to a truthy value. To customize set a new
// UpdateFunc value on *Golden.
Update() bool
}
// 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 := &gold{
dirMode: DefaultDirMode,
fileMode: DefaultFileMode,
suffix: DefaultSuffix,
dirname: DefaultDirname,
updateFunc: DefaultUpdateFunc,
fs: DefaultFS,
logOnWrite: DefaultLogOnWrite,
}
for _, opt := range options {
opt.apply(g)
}
return g
}
// 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)
}
// 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 DefaultGolden.DoP(t, name, data)
}
// File returns the filename of the golden file for the given *testing.T
// instance as determined by t.Name().
func File(t *testing.T) string {
func File(t TestingT) string {
t.Helper()
return global.File(t)
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 *testing.T) []byte {
func Get(t TestingT) []byte {
t.Helper()
return global.Get(t)
}
// Set writes given data to the golden file for the given *testing.T instance as
// determined by t.Name(). If writing fails it will fail the test by calling
// t.Fatal() with error details.
func Set(t *testing.T, data []byte) {
t.Helper()
global.Set(t, data)
}
// FileP returns the filename of the specifically named golden file for the
// given *testing.T instance as determined by t.Name().
func FileP(t *testing.T, name string) string {
t.Helper()
return global.FileP(t, name)
return DefaultGolden.Get(t)
}
// GetP returns the content of the specifically named golden file belonging
@@ -181,10 +275,19 @@ func FileP(t *testing.T, name string) string {
//
// 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 {
func GetP(t TestingT, name string) []byte {
t.Helper()
return global.GetP(t, name)
return DefaultGolden.GetP(t, name)
}
// 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
@@ -193,10 +296,10 @@ func GetP(t *testing.T, name string) []byte {
//
// 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) {
func SetP(t TestingT, name string, data []byte) {
t.Helper()
global.SetP(t, name, data)
DefaultGolden.SetP(t, name, data)
}
// Update returns true when golden is set to update golden files. Should be used
@@ -206,170 +309,5 @@ func SetP(t *testing.T, name string, data []byte) {
// environment variable is set to a truthy value. To customize create a custom
// *Golden instance with New() and set a new UpdateFunc value.
func Update() bool {
return global.Update()
}
// Golden handles all interactions with golden files. The top-level package
// functions all just proxy through to a default global *Golden instance.
type Golden struct {
// DirMode determines the file system permissions of any folders created to
// hold golden files.
DirMode os.FileMode
// FileMode determines the file system permissions of any created or updated
// golden files written to disk.
FileMode os.FileMode
// Suffix determines the filename suffix for all golden files. Typically
// this should be ".golden", but can be changed here if needed.
Suffix string
// Dirname is the name of the top-level directory at the root of the package
// which holds all golden files. Typically this should "testdata", but can
// be changed here if needed.
Dirname string
// UpdateFunc is used to determine if golden files should be updated or
// not. Its boolean return value is returned by Update().
UpdateFunc UpdateFunc
}
// New returns a new *Golden instance with default values correctly
// populated. This is ideally how you should create a custom *Golden, and then
// modify the relevant fields as you see fit.
func New() *Golden {
return &Golden{
DirMode: DefaultDirMode,
FileMode: DefaultFileMode,
Suffix: DefaultSuffix,
Dirname: DefaultDirname,
UpdateFunc: DefaultUpdateFunc,
}
}
// File returns the filename of the golden file for the given *testing.T
// instance as determined by t.Name().
func (s *Golden) File(t *testing.T) string {
return s.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 (s *Golden) Get(t *testing.T) []byte {
return s.get(t, "")
}
// Set writes given data to the golden file for the given *testing.T instance as
// determined by t.Name(). If writing fails it will fail the test by calling
// t.Fatal() with error details.
func (s *Golden) Set(t *testing.T, data []byte) {
s.set(t, "", data)
}
// FileP returns the filename of the specifically named golden file for the
// given *testing.T instance as determined by t.Name().
func (s *Golden) FileP(t *testing.T, name string) string {
if name == "" {
if t != nil {
t.Fatal("golden: name cannot be empty")
}
return ""
}
return s.file(t, name)
}
// GetP returns the content of the specifically named golden file belonging
// to the given *testing.T instance as determined by t.Name(). If no golden file
// can be found/read, it will fail the test with t.Fatal().
//
// This is very similar to Get(), but it allows multiple different golden files
// to be used within the same one *testing.T instance.
func (s *Golden) GetP(t *testing.T, name string) []byte {
if name == "" {
t.Fatal("golden: name cannot be empty")
return nil
}
return s.get(t, name)
}
// SetP writes given data of the specifically named golden file belonging to
// the given *testing.T instance as determined by t.Name(). If writing fails it
// will fail the test with t.Fatal() detailing the error.
//
// This is very similar to Set(), but it allows multiple different golden files
// to be used within the same one *testing.T instance.
func (s *Golden) SetP(t *testing.T, name string, data []byte) {
if name == "" {
t.Fatal("golden: name cannot be empty")
}
s.set(t, name, data)
}
func (s *Golden) file(t *testing.T, name string) string {
if t.Name() == "" {
t.Fatalf("golden: could not determine filename for: %+v", t)
return ""
}
base := []string{s.Dirname, filepath.FromSlash(t.Name())}
if name != "" {
base = append(base, name)
}
f := filepath.Clean(filepath.Join(base...) + s.Suffix)
dirty := strings.Split(f, string(os.PathSeparator))
clean := make([]string, 0, len(dirty))
for _, s := range dirty {
clean = append(clean, sanitizeFilename(s))
}
return strings.Join(clean, string(os.PathSeparator))
}
func (s *Golden) get(t *testing.T, name string) []byte {
f := s.file(t, name)
b, err := ioutil.ReadFile(f)
if err != nil {
t.Fatalf("golden: failed reading %s: %s", f, err.Error())
}
return b
}
func (s *Golden) set(t *testing.T, name string, data []byte) {
f := s.file(t, name)
dir := filepath.Dir(f)
t.Logf("golden: writing .golden file: %s", f)
err := os.MkdirAll(dir, s.DirMode)
if err != nil {
t.Fatalf("golden: failed to create directory: %s", err.Error())
return
}
err = ioutil.WriteFile(f, data, s.FileMode)
if err != nil {
t.Fatalf("golden: filed to write file: %s", err.Error())
}
}
// 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 set a new
// UpdateFunc value on *Golden.
func (s *Golden) Update() bool {
return s.UpdateFunc()
return DefaultGolden.Update()
}

View File

@@ -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

File diff suppressed because it is too large Load Diff

80
options.go Normal file
View File

@@ -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
})
}

View File

@@ -1,4 +1,4 @@
package golden
package sanitize
import (
"regexp"
@@ -15,7 +15,7 @@ var (
)
)
func sanitizeFilename(name string) string {
func Filename(name string) string {
if reservedNames.MatchString(name) || winReserved.MatchString(name) {
var b []byte
for i := 0; i < len(name); i++ {

View File

@@ -1,12 +1,13 @@
package golden
package sanitize_test
import (
"testing"
"github.com/jimeh/go-golden/sanitize"
"github.com/stretchr/testify/assert"
)
func Test_sanitizeFilename(t *testing.T) {
func TestFilename(t *testing.T) {
tests := []struct {
name string
filename string
@@ -69,6 +70,7 @@ func Test_sanitizeFilename(t *testing.T) {
filename: "foobar.golden .. .. .. ",
want: "foobar.golden",
},
// Protected Windows filenames.
{name: "con", filename: "con", want: "___"},
{name: "prn", filename: "prn", want: "___"},
{name: "aux", filename: "aux", want: "___"},
@@ -116,7 +118,7 @@ func Test_sanitizeFilename(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := sanitizeFilename(tt.filename)
got := sanitize.Filename(tt.filename)
assert.Equal(t, tt.want, got)
})

21
sanitize/line_breaks.go Normal file
View File

@@ -0,0 +1,21 @@
package sanitize
import "bytes"
var (
lf = []byte{10}
cr = []byte{13}
crlf = []byte{13, 10}
)
// LineBreaks replaces Windows CRLF (\r\n) and MacOS Classic CR (\r)
// line-breaks with Unix LF (\n) line breaks.
func LineBreaks(data []byte) []byte {
// Replace Windows CRLF (\r\n) with Unix LF (\n)
result := bytes.ReplaceAll(data, crlf, lf)
// Replace Classic MacOS CR (\r) with Unix LF (\n)
result = bytes.ReplaceAll(result, cr, lf)
return result
}

View File

@@ -0,0 +1,67 @@
package sanitize_test
import (
"testing"
"github.com/jimeh/go-golden/sanitize"
"github.com/stretchr/testify/assert"
)
func TestLineBreaks(t *testing.T) {
type args struct {
data []byte
}
tests := []struct {
name string
args args
want []byte
}{
{
name: "nil",
args: args{data: nil},
want: nil,
},
{
name: "empty",
args: args{data: []byte{}},
want: nil,
},
{
name: "no line breaks",
args: args{data: []byte("hello world")},
want: []byte("hello world"),
},
{
name: "UNIX line breaks",
args: args{data: []byte("hello\nworld\nhow are you?")},
want: []byte("hello\nworld\nhow are you?"),
},
{
name: "Windows line breaks",
args: args{data: []byte("hello\r\nworld\r\nhow are you?")},
want: []byte("hello\nworld\nhow are you?"),
},
{
name: "MacOS Classic line breaks",
args: args{data: []byte("hello\rworld\rhow are you?")},
want: []byte("hello\nworld\nhow are you?"),
},
{
name: "Windows and MacOS Classic line breaks",
args: args{data: []byte("hello\r\nworld\rhow are you?")},
want: []byte("hello\nworld\nhow are you?"),
},
{
name: "Windows, MacOS Classic, and UNIX line breaks",
args: args{data: []byte("hello\r\nworld\rhow are you?\nGood!")},
want: []byte("hello\nworld\nhow are you?\nGood!"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := sanitize.LineBreaks(tt.args.data)
assert.Equal(t, tt.want, got)
})
}
}

275
test/testfs/testfs.go Normal file
View File

@@ -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
}

670
test/testfs/testfs_test.go Normal file
View File

@@ -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)
}
})
}
}

10
testingt.go Normal file
View File

@@ -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
}

30
testingt_test.go Normal file
View File

@@ -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
}

View File

@@ -1,6 +1,10 @@
package golden
import "os"
import (
"flag"
"os"
"strings"
)
var truthyStrings = []string{"1", "y", "t", "yes", "on", "true"}
@@ -14,10 +18,29 @@ type UpdateFunc func() bool
func EnvUpdateFunc() bool {
env := os.Getenv("GOLDEN_UPDATE")
for _, v := range truthyStrings {
if env == v {
if strings.ToLower(env) == v {
return true
}
}
return false
}
var (
updateFlagSet *flag.FlagSet
updateFlag bool
)
// UpdateFunc returns a function that checks a -update flag is set.
func FlagUpdateFunc() bool {
if updateFlagSet == nil {
updateFlagSet = flag.NewFlagSet("golden", flag.ContinueOnError)
updateFlagSet.BoolVar(&updateFlag,
"update", false, "update golden files",
)
}
_ = updateFlagSet.Parse(os.Args[1:])
return false
}

View File

@@ -41,56 +41,171 @@ var envUpdateFuncTestCases = []struct {
env: map[string]string{"GOLDEN_UPDATE": "y"},
want: true,
},
{
name: "GOLDEN_UPDATE set to Y",
env: map[string]string{"GOLDEN_UPDATE": "Y"},
want: true,
},
{
name: "GOLDEN_UPDATE set to n",
env: map[string]string{"GOLDEN_UPDATE": "n"},
want: false,
},
{
name: "GOLDEN_UPDATE set to N",
env: map[string]string{"GOLDEN_UPDATE": "N"},
want: false,
},
{
name: "GOLDEN_UPDATE set to t",
env: map[string]string{"GOLDEN_UPDATE": "t"},
want: true,
},
{
name: "GOLDEN_UPDATE set to T",
env: map[string]string{"GOLDEN_UPDATE": "T"},
want: true,
},
{
name: "GOLDEN_UPDATE set to f",
env: map[string]string{"GOLDEN_UPDATE": "f"},
want: false,
},
{
name: "GOLDEN_UPDATE set to F",
env: map[string]string{"GOLDEN_UPDATE": "F"},
want: false,
},
{
name: "GOLDEN_UPDATE set to yes",
env: map[string]string{"GOLDEN_UPDATE": "yes"},
want: true,
},
{
name: "GOLDEN_UPDATE set to Yes",
env: map[string]string{"GOLDEN_UPDATE": "Yes"},
want: true,
},
{
name: "GOLDEN_UPDATE set to YeS",
env: map[string]string{"GOLDEN_UPDATE": "YeS"},
want: true,
},
{
name: "GOLDEN_UPDATE set to YES",
env: map[string]string{"GOLDEN_UPDATE": "YES"},
want: true,
},
{
name: "GOLDEN_UPDATE set to no",
env: map[string]string{"GOLDEN_UPDATE": "no"},
want: false,
},
{
name: "GOLDEN_UPDATE set to No",
env: map[string]string{"GOLDEN_UPDATE": "No"},
want: false,
},
{
name: "GOLDEN_UPDATE set to nO",
env: map[string]string{"GOLDEN_UPDATE": "nO"},
want: false,
},
{
name: "GOLDEN_UPDATE set to NO",
env: map[string]string{"GOLDEN_UPDATE": "NO"},
want: false,
},
{
name: "GOLDEN_UPDATE set to on",
env: map[string]string{"GOLDEN_UPDATE": "on"},
want: true,
},
{
name: "GOLDEN_UPDATE set to oN",
env: map[string]string{"GOLDEN_UPDATE": "oN"},
want: true,
},
{
name: "GOLDEN_UPDATE set to On",
env: map[string]string{"GOLDEN_UPDATE": "On"},
want: true,
},
{
name: "GOLDEN_UPDATE set to ON",
env: map[string]string{"GOLDEN_UPDATE": "ON"},
want: true,
},
{
name: "GOLDEN_UPDATE set to off",
env: map[string]string{"GOLDEN_UPDATE": "off"},
want: false,
},
{
name: "GOLDEN_UPDATE set to Off",
env: map[string]string{"GOLDEN_UPDATE": "Off"},
want: false,
},
{
name: "GOLDEN_UPDATE set to oFF",
env: map[string]string{"GOLDEN_UPDATE": "oFF"},
want: false,
},
{
name: "GOLDEN_UPDATE set to OFF",
env: map[string]string{"GOLDEN_UPDATE": "OFF"},
want: false,
},
{
name: "GOLDEN_UPDATE set to true",
env: map[string]string{"GOLDEN_UPDATE": "true"},
want: true,
},
{
name: "GOLDEN_UPDATE set to True",
env: map[string]string{"GOLDEN_UPDATE": "True"},
want: true,
},
{
name: "GOLDEN_UPDATE set to TruE",
env: map[string]string{"GOLDEN_UPDATE": "TruE"},
want: true,
},
{
name: "GOLDEN_UPDATE set to TRUE",
env: map[string]string{"GOLDEN_UPDATE": "TRUE"},
want: true,
},
{
name: "GOLDEN_UPDATE set to false",
env: map[string]string{"GOLDEN_UPDATE": "false"},
want: false,
},
{
name: "GOLDEN_UPDATE set to False",
env: map[string]string{"GOLDEN_UPDATE": "False"},
want: false,
},
{
name: "GOLDEN_UPDATE set to FaLsE",
env: map[string]string{"GOLDEN_UPDATE": "FaLsE"},
want: false,
},
{
name: "GOLDEN_UPDATE set to FALSE",
env: map[string]string{"GOLDEN_UPDATE": "FALSE"},
want: false,
},
{
name: "GOLDEN_UPDATE set to foobarnopebbq",
env: map[string]string{"GOLDEN_UPDATE": "foobarnopebbq"},
want: false,
},
{
name: "GOLDEN_UPDATE set to FOOBARNOPEBBQ",
env: map[string]string{"GOLDEN_UPDATE": "FOOBARNOPEBBQ"},
want: false,
},
}
func TestEnvUpdateFunc(t *testing.T) {