7 Commits

29 changed files with 3252 additions and 1176 deletions

View File

@@ -1,3 +0,0 @@
{
".": "0.2.1"
}

View File

@@ -1,15 +0,0 @@
{
"bootstrap-sha": "edb189f0863fddd94211a5b4c5b5bcf7a90fbd10",
"packages": {
".": {
"release-type": "go",
"changelog-path": "CHANGELOG.md",
"bump-minor-pre-major": true,
"bump-patch-for-minor-pre-major": true,
"always-update": true,
"draft": false,
"prerelease": false
}
},
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json"
}

View File

@@ -6,19 +6,18 @@ jobs:
lint:
name: Lint
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
checks: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: stable
go-version: "1.18"
cache: false
- name: golangci-lint
uses: golangci/golangci-lint-action@v6
uses: golangci/golangci-lint-action@v4
with:
version: v1.64
version: v1.56
env:
VERBOSE: "true"
tidy:
name: Tidy
@@ -27,7 +26,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
go-version: "1.18"
- name: Check if mods are tidy
run: make check-tidy
@@ -38,9 +37,9 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
go-version: "1.18"
- name: Publish coverage
uses: paambaati/codeclimate-action@v9.0.0
uses: paambaati/codeclimate-action@v5.0.0
env:
VERBOSE: "true"
GOMAXPROCS: 4
@@ -61,30 +60,16 @@ jobs:
- macos-latest
- windows-latest
go_version:
- "1.17"
- "1.18"
- "1.19"
- "1.20"
- "stable"
- "1.21"
- "1.22"
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go_version }}
check-latest: true
go-version: "1.22"
- name: Run tests
run: go test -v -count=1 -race ./...
release-please:
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
outputs:
release_created: ${{ steps.release-please.outputs.release_created }}
version: ${{ steps.release-please.outputs.version }}
steps:
- uses: jimeh/release-please-manifest-action@v2
id: release-please
with:
app-id: ${{ secrets.RELEASE_BOT_APP_ID }}
private-key: ${{ secrets.RELEASE_BOT_PRIVATE_KEY }}

5
.gitignore vendored
View File

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

View File

@@ -4,13 +4,18 @@ linters-settings:
statements: 150
gocyclo:
min-complexity: 20
golint:
min-confidence: 0
govet:
check-shadowing: true
enable-all: true
disable:
- fieldalignment
lll:
line-length: 80
tab-width: 4
maligned:
suggest-new: true
misspell:
locale: US
@@ -19,11 +24,12 @@ linters:
enable:
- asciicheck
- bodyclose
- copyloopvar
- deadcode
- durationcheck
- errcheck
- errorlint
- exhaustive
- exportloopref
- funlen
- gochecknoinits
- goconst
@@ -51,17 +57,17 @@ linters:
- rowserrcheck
- sqlclosecheck
- staticcheck
- structcheck
- tparallel
- typecheck
- unconvert
- unparam
- unused
- varcheck
- wastedassign
- 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

@@ -2,28 +2,6 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
## [0.2.1](https://github.com/jimeh/go-golden/compare/v0.2.0...v0.2.1) (2025-04-05)
### Features
* **options:** add optional Options arguments to New() function ([#14](https://github.com/jimeh/go-golden/issues/14)) ([550ba17](https://github.com/jimeh/go-golden/commit/550ba17fb0be76372eab83f2494ee4ffff804fa6))
## [0.2.0](https://github.com/jimeh/go-golden/compare/v0.1.0...v0.2.0) (2025-03-24)
### ⚠ BREAKING CHANGES
* **go:** Go 1.17 or later is now required, up from Go 1.15.
### Features
* **defaults:** expose a Default *Golden instance ([#9](https://github.com/jimeh/go-golden/issues/9)) ([e46dc12](https://github.com/jimeh/go-golden/commit/e46dc124ff22e52dfc050174c7b3de980c980912))
* **do:** add new Do function to simplify golden usage ([#11](https://github.com/jimeh/go-golden/issues/11)) ([bc86ba7](https://github.com/jimeh/go-golden/commit/bc86ba7a6d9f4374c9fb15f78e51eedf50df04d2))
* **go:** upgrade to Go 1.17 (from 1.15) ([#6](https://github.com/jimeh/go-golden/issues/6)) ([85ae6e9](https://github.com/jimeh/go-golden/commit/85ae6e9ae3c4222d68faee0c44a1fd105a2e04b4))
* **interface:** use TestingT interface intead of *testing.T in function signatures ([#13](https://github.com/jimeh/go-golden/issues/13)) ([62e8344](https://github.com/jimeh/go-golden/commit/62e8344ff33dccf0a5666e94361a143d34558bf0))
* **update:** make default GOLDEN_UPDATE env var check case-insensitive ([#10](https://github.com/jimeh/go-golden/issues/10)) ([6817ec6](https://github.com/jimeh/go-golden/commit/6817ec6101558b3914984d4ee3fe816d534f87bb))
## 0.1.0 (2021-10-28)

View File

@@ -45,7 +45,7 @@ endef
$(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.64))
$(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
@@ -182,3 +182,20 @@ check-tidy:
docs: $(TOOLDIR)/godoc
$(info serviing docs on http://127.0.0.1:6060/pkg/$(GOMODNAME)/)
@godoc -http=127.0.0.1:6060
#
# Release
#
.PHONY: new-version
new-version: check-npx
npx standard-version
.PHONY: next-version
next-version: check-npx
npx standard-version --dry-run
.PHONY: check-npx
check-npx:
$(if $(shell which npx),,\
$(error No npx found in PATH, please install NodeJS))

View File

@@ -10,12 +10,26 @@
</p>
<p align="center">
<a href="https://pkg.go.dev/github.com/jimeh/go-golden"><img src="https://img.shields.io/badge/%E2%80%8B-reference-387b97.svg?logo=go&logoColor=white" alt="Go Reference"></a>
<a href="https://github.com/jimeh/go-golden/actions"><img src="https://img.shields.io/github/actions/workflow/status/jimeh/go-golden/ci.yml?logo=github" alt="Actions Status"></a>
<a href="https://codeclimate.com/github/jimeh/go-golden"><img src="https://img.shields.io/codeclimate/coverage/jimeh/go-golden.svg?logo=code%20climate" alt="Coverage"></a>
<a href="https://github.com/jimeh/go-golden/issues"><img src="https://img.shields.io/github/issues-raw/jimeh/go-golden.svg?style=flat&logo=github&logoColor=white" alt="GitHub issues"></a>
<a href="https://github.com/jimeh/go-golden/pulls"><img src="https://img.shields.io/github/issues-pr-raw/jimeh/go-golden.svg?style=flat&logo=github&logoColor=white" alt="GitHub pull requests"></a>
<a href="https://github.com/jimeh/go-golden/blob/master/LICENSE"><img src="https://img.shields.io/github/license/jimeh/go-golden.svg?style=flat" alt="License Status"></a>
<a href="https://pkg.go.dev/github.com/jimeh/go-golden">
<img src="https://img.shields.io/badge/%E2%80%8B-reference-387b97.svg?logo=go&logoColor=white"
alt="Go Reference">
</a>
<a href="https://github.com/jimeh/go-golden/actions">
<img src="https://img.shields.io/github/workflow/status/jimeh/go-golden/CI.svg?logo=github" alt="Actions Status">
</a>
<a href="https://codeclimate.com/github/jimeh/go-golden">
<img src="https://img.shields.io/codeclimate/coverage/jimeh/go-golden.svg?logo=code%20climate" alt="Coverage">
</a>
<a href="https://github.com/jimeh/go-golden/issues">
<img src="https://img.shields.io/github/issues-raw/jimeh/go-golden.svg?style=flat&logo=github&logoColor=white"
alt="GitHub issues">
</a>
<a href="https://github.com/jimeh/go-golden/pulls">
<img src="https://img.shields.io/github/issues-pr-raw/jimeh/go-golden.svg?style=flat&logo=github&logoColor=white" alt="GitHub pull requests">
</a>
<a href="https://github.com/jimeh/go-golden/blob/master/LICENSE">
<img src="https://img.shields.io/github/license/jimeh/go-golden.svg?style=flat" alt="License Status">
</a>
</p>
## Import
@@ -28,12 +42,15 @@ import "github.com/jimeh/go-golden"
```go
func TestExampleMyStruct(t *testing.T) {
got, err := json.Marshal(&MyStruct{Foo: "Bar"})
require.NoError(t, err)
got, err := json.Marshal(&MyStruct{Foo: "Bar"})
require.NoError(t, err)
want := golden.Do(t, got)
if golden.Update() {
golden.Set(t, got)
}
want := golden.Get(t)
assert.Equal(t, want, got)
assert.Equal(t, want, got)
}
```
@@ -41,15 +58,6 @@ The above example will read/write to:
- `testdata/TestExampleMyStruct.golden`
The call to `golden.Do()` is equivalent to:
```go
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.
@@ -62,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)
}
})
}
}

7
go.mod
View File

@@ -1,8 +1,11 @@
module github.com/jimeh/go-golden
go 1.17
go 1.18
require github.com/stretchr/testify v1.10.0
require (
github.com/jimeh/envctl v0.1.0
github.com/stretchr/testify v1.9.0
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect

13
go.sum
View File

@@ -1,17 +1,14 @@
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/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.6.1/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/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")
// })
// }
// }

425
golden.go
View File

@@ -1,10 +1,10 @@
// Package golden is yet another package for working with *.golden test files,
// with a focus on simplicity through it's default behavior.
//
// Golden file names are based on the name of the test function and any sub-test
// names by calling t.Name(). File names are sanitized to ensure they're
// compatible with Linux, macOS and Windows systems regardless of what crazy
// characters might be in a sub-test's name.
// 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 are
// compatible with Linux, macOS and Windows systems regardless of what
// characters might be in a subtest's name.
//
// # Usage
//
@@ -19,7 +19,7 @@
// assert.Equal(t, want, got)
// }
//
// The above example will read/write to:
// The above example will attempt to read/write to:
//
// testdata/TestExampleMyStruct.golden
//
@@ -37,7 +37,7 @@
// # Sub-Tests
//
// As the golden filename is based on t.Name(), it works with sub-tests too,
// ensuring each sub-test gets its own golden file. For example:
// ensuring each sub-test gets it's own golden file. For example:
//
// func TestExampleMyStructTabular(t *testing.T) {
// tests := []struct {
@@ -66,7 +66,7 @@
//
// # 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.
//
@@ -88,27 +88,27 @@
//
// 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)
//
// wantJSON := golden.DoP(t, "json", gotJSON)
// wantXML := golden.DoP(t, "xml", gotXML)
// wantJSON := golden.DoP(t, "json", gotJSON)
// wantXML := golden.DoP(t, "xml", gotXML)
//
// assert.Equal(t, wantJSON, gotJSON)
// assert.Equal(t, wantXML, gotXML)
// })
// }
// }
// assert.Equal(t, wantJSON, gotJSON)
// assert.Equal(t, wantXML, gotXML)
// })
// }
// }
//
// The above example will read/write to:
//
@@ -118,66 +118,121 @@
// testdata/TestExampleMyStructTabularP/full_struct/xml.golden
package golden
import (
"os"
"path/filepath"
"strings"
)
import "os"
var (
// Default is the default *Golden instance. All package-level functions use
// the Default instance.
Default = New()
// DefaultGolden is the default Golden instance used by all top-level
// package functions.
DefaultGolden = New()
// DefaultDirMode is the default DirMode value used by 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 FileMode value used by New().
// 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 Suffix value used by New().
// DefaultSuffix is the default filename suffix used for all golden files.
DefaultSuffix = ".golden"
// DefaultDirname is the default Dirname value used by New().
// DefaultDirname is the default name of the top-level directory used to
// hold golden files.
DefaultDirname = "testdata"
// DefaultUpdateFunc is the default UpdateFunc value used by New().
// 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
)
// 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
// 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
// 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 Default.Do(t, data)
}
// 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 TestingT, data []byte) {
t.Helper()
Default.Set(t, data)
return DefaultGolden.Do(t, data)
}
// DoP is a convenience function for calling Update(), SetP(), and GetP() in a
@@ -186,7 +241,15 @@ func Set(t TestingT, data []byte) {
func DoP(t TestingT, name string, data []byte) []byte {
t.Helper()
return Default.DoP(t, name, data)
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 TestingT) string {
t.Helper()
return DefaultGolden.File(t)
}
// FileP returns the filename of the specifically named golden file for the
@@ -194,7 +257,16 @@ func DoP(t TestingT, name string, data []byte) []byte {
func FileP(t TestingT, name string) string {
t.Helper()
return Default.FileP(t, name)
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
@@ -206,7 +278,16 @@ func FileP(t TestingT, name string) string {
func GetP(t TestingT, name string) []byte {
t.Helper()
return Default.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
@@ -218,7 +299,7 @@ func GetP(t TestingT, name string) []byte {
func SetP(t TestingT, name string, data []byte) {
t.Helper()
Default.SetP(t, name, data)
DefaultGolden.SetP(t, name, data)
}
// Update returns true when golden is set to update golden files. Should be used
@@ -228,207 +309,5 @@ func SetP(t TestingT, 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 Default.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 would be ".golden".
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.
// It accepts zero or more Option functions that can modify the default values.
func New(opts ...Option) *Golden {
g := &Golden{
DirMode: DefaultDirMode,
FileMode: DefaultFileMode,
Suffix: DefaultSuffix,
Dirname: DefaultDirname,
UpdateFunc: DefaultUpdateFunc,
}
for _, opt := range opts {
opt(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 (s *Golden) Do(t TestingT, data []byte) []byte {
t.Helper()
if s.Update() {
s.Set(t, data)
}
return s.Get(t)
}
// File returns the filename of the golden file for the given *testing.T
// instance as determined by t.Name().
func (s *Golden) File(t TestingT) string {
t.Helper()
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 TestingT) []byte {
t.Helper()
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 TestingT, data []byte) {
t.Helper()
s.set(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 (s *Golden) DoP(t TestingT, name string, data []byte) []byte {
t.Helper()
if name == "" {
t.Fatalf("golden: name cannot be empty")
}
if s.Update() {
s.SetP(t, name, data)
}
return s.GetP(t, name)
}
// 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 TestingT, name string) string {
t.Helper()
if name == "" {
t.Fatalf("golden: name cannot be empty")
}
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 TestingT, name string) []byte {
t.Helper()
if name == "" {
t.Fatalf("golden: name cannot be empty")
}
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 TestingT, name string, data []byte) {
t.Helper()
if name == "" {
t.Fatalf("golden: name cannot be empty")
}
s.set(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 set a new
// UpdateFunc value on *Golden.
func (s *Golden) Update() bool {
return s.UpdateFunc()
}
func (s *Golden) file(t TestingT, name string) string {
if t.Name() == "" {
t.Fatalf("golden: could not determine filename")
}
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 TestingT, name string) []byte {
f := s.file(t, name)
b, err := os.ReadFile(f)
if err != nil {
t.Fatalf("golden: failed reading %s: %s", f, err.Error())
}
return b
}
func (s *Golden) set(t TestingT, 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())
}
err = os.WriteFile(f, data, s.FileMode)
if err != nil {
t.Fatalf("golden: filed to write file: %s", err.Error())
}
return DefaultGolden.Update()
}

View File

@@ -10,9 +10,6 @@ import (
"github.com/stretchr/testify/require"
)
// The tests in this file are examples from the README and the package-level Go
// documentation.
type MyStruct struct {
Foo string `json:"foo,omitempty"`
}
@@ -24,7 +21,10 @@ func TestExampleMyStruct(t *testing.T) {
got, err := json.Marshal(&MyStruct{Foo: "Bar"})
require.NoError(t, err)
want := golden.Do(t, got)
if golden.Update() {
golden.Set(t, got)
}
want := golden.Get(t)
assert.Equal(t, want, got)
}
@@ -46,7 +46,10 @@ func TestExampleMyStructTabular(t *testing.T) {
got, err := json.Marshal(tt.obj)
require.NoError(t, err)
want := golden.Do(t, got)
if golden.Update() {
golden.Set(t, got)
}
want := golden.Get(t)
assert.Equal(t, want, got)
})
@@ -61,11 +64,13 @@ func TestExampleMyStructP(t *testing.T) {
gotJSON, _ := json.Marshal(&MyStruct{Foo: "Bar"})
gotXML, _ := xml.Marshal(&MyStruct{Foo: "Bar"})
wantJSON := golden.DoP(t, "json", gotJSON)
wantXML := golden.DoP(t, "xml", gotXML)
if golden.Update() {
golden.SetP(t, "json", gotJSON)
golden.SetP(t, "xml", gotXML)
}
assert.Equal(t, wantJSON, gotJSON)
assert.Equal(t, wantXML, gotXML)
assert.Equal(t, golden.GetP(t, "json"), gotJSON)
assert.Equal(t, golden.GetP(t, "xml"), gotXML)
}
// TestExampleMyStructTabularP reads/writes the following golden file:
@@ -87,11 +92,13 @@ func TestExampleMyStructTabularP(t *testing.T) {
gotJSON, _ := json.Marshal(tt.obj)
gotXML, _ := xml.Marshal(tt.obj)
wantJSON := golden.DoP(t, "json", gotJSON)
wantXML := golden.DoP(t, "xml", gotXML)
if golden.Update() {
golden.SetP(t, "json", gotJSON)
golden.SetP(t, "xml", gotXML)
}
assert.Equal(t, wantJSON, gotJSON)
assert.Equal(t, wantXML, gotXML)
assert.Equal(t, golden.GetP(t, "json"), gotJSON)
assert.Equal(t, golden.GetP(t, "xml"), gotXML)
})
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,43 +1,80 @@
package golden
import (
"os"
)
import "os"
// Option is a function that modifies a Golden instance.
type Option func(*Golden)
type Option interface {
apply(*gold)
}
// WithDirMode sets the directory mode for a Golden instance.
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 func(g *Golden) {
g.DirMode = mode
}
return optionFunc(func(g *gold) {
g.dirMode = mode
})
}
// WithFileMode sets the file mode for a Golden instance.
// 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 func(g *Golden) {
g.FileMode = mode
}
return optionFunc(func(g *gold) {
g.fileMode = mode
})
}
// WithSuffix sets the file suffix for a Golden instance.
// 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 func(g *Golden) {
g.Suffix = suffix
}
return optionFunc(func(g *gold) {
g.suffix = suffix
})
}
// WithDirname sets the directory name for a Golden instance.
func WithDirname(dirname string) Option {
return func(g *Golden) {
g.Dirname = dirname
}
// 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 update function for a Golden instance.
func WithUpdateFunc(updateFunc UpdateFunc) Option {
return func(g *Golden) {
g.UpdateFunc = updateFunc
}
// 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,58 +0,0 @@
package golden
import (
"os"
"testing"
"github.com/stretchr/testify/assert"
)
func TestWithDirMode(t *testing.T) {
customMode := os.FileMode(0o700)
g := &Golden{}
opt := WithDirMode(customMode)
opt(g)
assert.Equal(t, customMode, g.DirMode)
}
func TestWithFileMode(t *testing.T) {
customMode := os.FileMode(0o600)
g := &Golden{}
opt := WithFileMode(customMode)
opt(g)
assert.Equal(t, customMode, g.FileMode)
}
func TestWithSuffix(t *testing.T) {
customSuffix := ".custom"
g := &Golden{}
opt := WithSuffix(customSuffix)
opt(g)
assert.Equal(t, customSuffix, g.Suffix)
}
func TestWithDirname(t *testing.T) {
customDirname := "custom-testdata"
g := &Golden{}
opt := WithDirname(customDirname)
opt(g)
assert.Equal(t, customDirname, g.Dirname)
}
func TestWithUpdateFunc(t *testing.T) {
customUpdateFunc := func() bool { return true }
g := &Golden{}
opt := WithUpdateFunc(customUpdateFunc)
opt(g)
assertSameFunc(t, customUpdateFunc, g.UpdateFunc)
}

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

View File

@@ -1,7 +1,8 @@
package golden
// TestingT is a interface describing a sub-set of methods of *testing.T which
// golden uses.
type TestingT interface {
Errorf(format string, args ...interface{})
Fatalf(format string, args ...interface{})
Helper()
Logf(format string, args ...interface{})

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,7 @@
package golden
import (
"flag"
"os"
"strings"
)
@@ -17,10 +18,29 @@ type UpdateFunc func() bool
func EnvUpdateFunc() bool {
env := os.Getenv("GOLDEN_UPDATE")
for _, v := range truthyStrings {
if strings.EqualFold(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

@@ -3,6 +3,7 @@ package golden
import (
"testing"
"github.com/jimeh/envctl"
"github.com/stretchr/testify/assert"
)
@@ -40,94 +41,181 @@ 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,
},
// Case-insensitive test cases
{
name: "GOLDEN_UPDATE set to Y (uppercase)",
env: map[string]string{"GOLDEN_UPDATE": "Y"},
want: true,
},
{
name: "GOLDEN_UPDATE set to TRUE (uppercase)",
env: map[string]string{"GOLDEN_UPDATE": "TRUE"},
want: true,
},
{
name: "GOLDEN_UPDATE set to Yes (mixed case)",
env: map[string]string{"GOLDEN_UPDATE": "Yes"},
want: true,
},
{
name: "GOLDEN_UPDATE set to ON (uppercase)",
env: map[string]string{"GOLDEN_UPDATE": "ON"},
want: true,
},
{
name: "GOLDEN_UPDATE set to TrUe (mixed case)",
env: map[string]string{"GOLDEN_UPDATE": "TrUe"},
want: true,
name: "GOLDEN_UPDATE set to FOOBARNOPEBBQ",
env: map[string]string{"GOLDEN_UPDATE": "FOOBARNOPEBBQ"},
want: false,
},
}
func TestEnvUpdateFunc(t *testing.T) {
for _, tt := range envUpdateFuncTestCases {
t.Run(tt.name, func(t *testing.T) {
for k, v := range tt.env {
t.Setenv(k, v)
}
envctl.WithClean(tt.env, func() {
got := EnvUpdateFunc()
got := EnvUpdateFunc()
assert.Equal(t, tt.want, got)
assert.Equal(t, tt.want, got)
})
})
}
}