12 Commits

Author SHA1 Message Date
jimehbot[bot]
e17b04717f chore(main): release 0.2.1 (#15)
Co-authored-by: jimehbot[bot] <132453784+jimehbot[bot]@users.noreply.github.com>
2025-04-05 12:34:00 +01:00
550ba17fb0 feat(options): add optional Options arguments to New() function (#14)
Enables simpler creation of custom *Golden instances, as you can just pass in the custom values to New(), rather than modifying fields after increating the Golden instance.
2025-04-05 12:28:57 +01:00
jimehbot[bot]
8a88e5174b chore(main): release 0.2.0 (#8)
Co-authored-by: jimehbot[bot] <132453784+jimehbot[bot]@users.noreply.github.com>
2025-03-24 13:19:39 +00:00
bc86ba7a6d feat(do): add new Do function to simplify golden usage (#11)
Add new Do function whihc ecapsulates the common pattern of writing to
golden file if the update mechnism evaluates to true, followed by
immediately reading the file. Compresses four lines into one.
2025-03-24 13:17:17 +00:00
62e8344ff3 feat(interface): use TestingT interface intead of *testing.T in function signatures (#13) 2025-03-24 13:04:34 +00:00
e46dc124ff feat(defaults): expose a Default *Golden instance (#9)
Also change the Default* constants to variables so they can be modified
as well.
2025-03-24 12:54:29 +00:00
6817ec6101 feat(update): make default GOLDEN_UPDATE env var check case-insensitive (#10) 2025-03-24 12:53:34 +00:00
8f4d3d4170 chore(deps/dev): upgrade development dependencies, including golangci-lint (#12) 2025-03-24 12:52:20 +00:00
b2112ca475 ci(release): add release-please config and workflow job (#7) 2025-03-24 11:36:39 +00:00
27e0134701 doc(readme): fix badges 2025-03-22 02:00:17 +00:00
85ae6e9ae3 feat(go)!: upgrade to Go 1.17 (from 1.15) (#6)
BREAKING CHANGE: Go 1.17 or later is now required, up from Go 1.15.
2025-03-22 01:37:06 +00:00
f5a03af9ce ci(deps): update actions and lint config to get them all running again (#5) 2025-03-22 01:23:12 +00:00
29 changed files with 1197 additions and 3273 deletions

3
.github/.release-please-manifest.json vendored Normal file
View File

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

15
.github/release-please-config.json vendored Normal file
View File

@@ -0,0 +1,15 @@
{
"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,18 +6,19 @@ jobs:
lint: lint:
name: Lint name: Lint
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
checks: write
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-go@v5 - uses: actions/setup-go@v5
with: with:
go-version: "1.18" go-version: stable
cache: false
- name: golangci-lint - name: golangci-lint
uses: golangci/golangci-lint-action@v4 uses: golangci/golangci-lint-action@v6
with: with:
version: v1.56 version: v1.64
env:
VERBOSE: "true"
tidy: tidy:
name: Tidy name: Tidy
@@ -26,7 +27,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-go@v5 - uses: actions/setup-go@v5
with: with:
go-version: "1.18" go-version-file: go.mod
- name: Check if mods are tidy - name: Check if mods are tidy
run: make check-tidy run: make check-tidy
@@ -37,9 +38,9 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-go@v5 - uses: actions/setup-go@v5
with: with:
go-version: "1.18" go-version-file: go.mod
- name: Publish coverage - name: Publish coverage
uses: paambaati/codeclimate-action@v5.0.0 uses: paambaati/codeclimate-action@v9.0.0
env: env:
VERBOSE: "true" VERBOSE: "true"
GOMAXPROCS: 4 GOMAXPROCS: 4
@@ -60,16 +61,30 @@ jobs:
- macos-latest - macos-latest
- windows-latest - windows-latest
go_version: go_version:
- "1.17"
- "1.18" - "1.18"
- "1.19" - "1.19"
- "1.20" - "1.20"
- "1.21" - "stable"
- "1.22"
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-go@v5 - uses: actions/setup-go@v5
with: with:
go-version: "1.22" go-version: ${{ matrix.go_version }}
check-latest: true
- name: Run tests - name: Run tests
run: go test -v -count=1 -race ./... 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,6 +1,5 @@
bin/* bin/*
coverage.out coverage.out
testdata/TestFile* testdata/*
testdata/TestGet* !testdata/TestExample*
testdata/TestSet*

View File

@@ -4,18 +4,13 @@ linters-settings:
statements: 150 statements: 150
gocyclo: gocyclo:
min-complexity: 20 min-complexity: 20
golint:
min-confidence: 0
govet: govet:
check-shadowing: true
enable-all: true enable-all: true
disable: disable:
- fieldalignment - fieldalignment
lll: lll:
line-length: 80 line-length: 80
tab-width: 4 tab-width: 4
maligned:
suggest-new: true
misspell: misspell:
locale: US locale: US
@@ -24,12 +19,11 @@ linters:
enable: enable:
- asciicheck - asciicheck
- bodyclose - bodyclose
- deadcode - copyloopvar
- durationcheck - durationcheck
- errcheck - errcheck
- errorlint - errorlint
- exhaustive - exhaustive
- exportloopref
- funlen - funlen
- gochecknoinits - gochecknoinits
- goconst - goconst
@@ -57,17 +51,17 @@ linters:
- rowserrcheck - rowserrcheck
- sqlclosecheck - sqlclosecheck
- staticcheck - staticcheck
- structcheck
- tparallel
- typecheck - typecheck
- unconvert - unconvert
- unparam - unparam
- unused - unused
- varcheck
- wastedassign - wastedassign
- whitespace - whitespace
issues: issues:
exclude:
- Using the variable on range scope `tt` in function literal
- Using the variable on range scope `tc` in function literal
exclude-rules: exclude-rules:
- path: "_test\\.go" - path: "_test\\.go"
linters: linters:

View File

@@ -2,6 +2,28 @@
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. 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) ## 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,godoc,golang.org/x/tools/cmd/godoc@latest))
$(eval $(call tool,gofumpt,mvdan.cc/gofumpt@latest)) $(eval $(call tool,gofumpt,mvdan.cc/gofumpt@latest))
$(eval $(call tool,goimports,golang.org/x/tools/cmd/goimports@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,golangci-lint,github.com/golangci/golangci-lint/cmd/golangci-lint@v1.64))
$(eval $(call tool,gomod,github.com/Helcaraxan/gomod@latest)) $(eval $(call tool,gomod,github.com/Helcaraxan/gomod@latest))
.PHONY: tools .PHONY: tools
@@ -182,20 +182,3 @@ check-tidy:
docs: $(TOOLDIR)/godoc docs: $(TOOLDIR)/godoc
$(info serviing docs on http://127.0.0.1:6060/pkg/$(GOMODNAME)/) $(info serviing docs on http://127.0.0.1:6060/pkg/$(GOMODNAME)/)
@godoc -http=127.0.0.1:6060 @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,26 +10,12 @@
</p> </p>
<p align="center"> <p align="center">
<a href="https://pkg.go.dev/github.com/jimeh/go-golden"> <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>
<img src="https://img.shields.io/badge/%E2%80%8B-reference-387b97.svg?logo=go&logoColor=white" <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>
alt="Go Reference"> <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> <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/actions"> <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>
<img src="https://img.shields.io/github/workflow/status/jimeh/go-golden/CI.svg?logo=github" alt="Actions Status"> <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>
<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> </p>
## Import ## Import
@@ -42,15 +28,12 @@ import "github.com/jimeh/go-golden"
```go ```go
func TestExampleMyStruct(t *testing.T) { func TestExampleMyStruct(t *testing.T) {
got, err := json.Marshal(&MyStruct{Foo: "Bar"}) got, err := json.Marshal(&MyStruct{Foo: "Bar"})
require.NoError(t, err) require.NoError(t, err)
if golden.Update() { want := golden.Do(t, got)
golden.Set(t, got)
}
want := golden.Get(t)
assert.Equal(t, want, got) assert.Equal(t, want, got)
} }
``` ```
@@ -58,6 +41,15 @@ The above example will read/write to:
- `testdata/TestExampleMyStruct.golden` - `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 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 `GOLDEN_UPDATE` environment variable to one of `1`, `y`, `t`, `yes`, `on`, or
`true` when running tests. `true` when running tests.
@@ -70,4 +62,4 @@ for documentation and examples.
## License ## License
[MIT](https://github.com/jimeh/go-golden/blob/main/LICENSE) [MIT](https://github.com/jimeh/go-golden/blob/master/LICENSE)

View File

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

46
fs.go
View File

@@ -1,46 +0,0 @@
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)
}

View File

@@ -1,131 +0,0 @@
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,11 +1,8 @@
module github.com/jimeh/go-golden module github.com/jimeh/go-golden
go 1.18 go 1.17
require ( require github.com/stretchr/testify v1.10.0
github.com/jimeh/envctl v0.1.0
github.com/stretchr/testify v1.9.0
)
require ( require (
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect

13
go.sum
View File

@@ -1,14 +1,17 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 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=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 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/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= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

173
gold.go
View File

@@ -1,173 +0,0 @@
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()
}

View File

@@ -1,528 +0,0 @@
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")
// })
// }
// }

437
golden.go
View File

@@ -1,10 +1,10 @@
// Package golden is yet another package for working with *.golden test files, // Package golden is yet another package for working with *.golden test files,
// with a focus on simplicity through it's default behavior. // 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 // 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 are // names by calling t.Name(). File names are sanitized to ensure they're
// compatible with Linux, macOS and Windows systems regardless of what // compatible with Linux, macOS and Windows systems regardless of what crazy
// characters might be in a subtest's name. // characters might be in a sub-test's name.
// //
// # Usage // # Usage
// //
@@ -19,7 +19,7 @@
// assert.Equal(t, want, got) // assert.Equal(t, want, got)
// } // }
// //
// The above example will attempt to read/write to: // The above example will read/write to:
// //
// testdata/TestExampleMyStruct.golden // testdata/TestExampleMyStruct.golden
// //
@@ -37,7 +37,7 @@
// # Sub-Tests // # Sub-Tests
// //
// As the golden filename is based on t.Name(), it works with sub-tests too, // 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: // ensuring each sub-test gets its own golden file. For example:
// //
// func TestExampleMyStructTabular(t *testing.T) { // func TestExampleMyStructTabular(t *testing.T) {
// tests := []struct { // tests := []struct {
@@ -66,7 +66,7 @@
// //
// # Multiple Golden Files in a Single Test // # Multiple Golden Files in a Single Test
// //
// The "P" suffixed methods, GetP(), SetP(), DoP(), and FileP(), all take a name // The "P" suffixed methods, GetP(), SetP(), and FileP(), all take a name
// argument which allows using specific golden files within a given *testing.T // argument which allows using specific golden files within a given *testing.T
// instance. // instance.
// //
@@ -88,27 +88,27 @@
// //
// This works with tabular tests too of course: // This works with tabular tests too of course:
// //
// func TestExampleMyStructTabularP(t *testing.T) { // func TestExampleMyStructTabularP(t *testing.T) {
// tests := []struct { // tests := []struct {
// name string // name string
// obj *MyStruct // obj *MyStruct
// }{ // }{
// {name: "empty struct", obj: &MyStruct{}}, // {name: "empty struct", obj: &MyStruct{}},
// {name: "full struct", obj: &MyStruct{Foo: "Bar"}}, // {name: "full struct", obj: &MyStruct{Foo: "Bar"}},
// } // }
// for _, tt := range tests { // for _, tt := range tests {
// t.Run(tt.name, func(t *testing.T) { // t.Run(tt.name, func(t *testing.T) {
// gotJSON, _ := json.Marshal(tt.obj) // gotJSON, _ := json.Marshal(tt.obj)
// gotXML, _ := xml.Marshal(tt.obj) // gotXML, _ := xml.Marshal(tt.obj)
// //
// wantJSON := golden.DoP(t, "json", gotJSON) // wantJSON := golden.DoP(t, "json", gotJSON)
// wantXML := golden.DoP(t, "xml", gotXML) // wantXML := golden.DoP(t, "xml", gotXML)
// //
// assert.Equal(t, wantJSON, gotJSON) // assert.Equal(t, wantJSON, gotJSON)
// assert.Equal(t, wantXML, gotXML) // assert.Equal(t, wantXML, gotXML)
// }) // })
// } // }
// } // }
// //
// The above example will read/write to: // The above example will read/write to:
// //
@@ -118,113 +118,32 @@
// testdata/TestExampleMyStructTabularP/full_struct/xml.golden // testdata/TestExampleMyStructTabularP/full_struct/xml.golden
package golden package golden
import "os" import (
"os"
var ( "path/filepath"
// DefaultGolden is the default Golden instance used by all top-level "strings"
// 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
) )
// Golden handles all interactions with golden files. The top-level package var (
// functions proxy through to a default global Golden instance. // Default is the default *Golden instance. All package-level functions use
type Golden interface { // the Default instance.
// File returns the filename of the golden file for the given testing.TB Default = New()
// instance as determined by t.Name().
File(t TestingT) string
// Get returns the content of the golden file for the given TestingT // DefaultDirMode is the default DirMode value used by New().
// instance as determined by t.Name(). If no golden file can be found/read, DefaultDirMode = os.FileMode(0o755)
// 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 // DefaultFileMode is the default FileMode value used by New().
// instance as determined by t.Name(). If writing fails it will fail the DefaultFileMode = os.FileMode(0o644)
// 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 // DefaultSuffix is the default Suffix value used by New().
// single call. If Update() returns true, data will be written to the golden DefaultSuffix = ".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 // DefaultDirname is the default Dirname value used by New().
// given TestingT instance as determined by t.Name(). DefaultDirname = "testdata"
FileP(t TestingT, name string) string
// GetP returns the content of the specifically named golden file belonging // DefaultUpdateFunc is the default UpdateFunc value used by New().
// to the given TestingT instance as determined by t.Name(). If no golden DefaultUpdateFunc = EnvUpdateFunc
// 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 // 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 // single call. If Update() returns true, data will be written to the golden
@@ -232,16 +151,7 @@ func New(options ...Option) Golden {
func Do(t TestingT, data []byte) []byte { func Do(t TestingT, data []byte) []byte {
t.Helper() t.Helper()
return DefaultGolden.Do(t, data) return Default.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 // File returns the filename of the golden file for the given *testing.T
@@ -249,15 +159,7 @@ func DoP(t TestingT, name string, data []byte) []byte {
func File(t TestingT) string { func File(t TestingT) string {
t.Helper() t.Helper()
return DefaultGolden.File(t) return Default.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 // Get returns the content of the golden file for the given *testing.T instance
@@ -266,7 +168,33 @@ func FileP(t TestingT, name string) string {
func Get(t TestingT) []byte { func Get(t TestingT) []byte {
t.Helper() t.Helper()
return DefaultGolden.Get(t) return Default.Get(t)
}
// Set writes given data to the golden file for the given *testing.T instance as
// 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)
}
// DoP is a convenience function for calling Update(), SetP(), and GetP() in a
// single call. If Update() returns true, data will be written to the golden
// file using SetP(), before reading it back with GetP().
func DoP(t TestingT, name string, data []byte) []byte {
t.Helper()
return Default.DoP(t, name, data)
}
// FileP returns the filename of the specifically named golden file for the
// given *testing.T instance as determined by t.Name().
func FileP(t TestingT, name string) string {
t.Helper()
return Default.FileP(t, name)
} }
// GetP returns the content of the specifically named golden file belonging // GetP returns the content of the specifically named golden file belonging
@@ -278,16 +206,7 @@ func Get(t TestingT) []byte {
func GetP(t TestingT, name string) []byte { func GetP(t TestingT, name string) []byte {
t.Helper() t.Helper()
return DefaultGolden.GetP(t, name) return Default.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 // SetP writes given data of the specifically named golden file belonging to
@@ -299,7 +218,7 @@ func Set(t TestingT, data []byte) {
func SetP(t TestingT, name string, data []byte) { func SetP(t TestingT, name string, data []byte) {
t.Helper() t.Helper()
DefaultGolden.SetP(t, name, data) Default.SetP(t, name, data)
} }
// Update returns true when golden is set to update golden files. Should be used // Update returns true when golden is set to update golden files. Should be used
@@ -309,5 +228,207 @@ func SetP(t TestingT, name string, data []byte) {
// environment variable is set to a truthy value. To customize create a custom // environment variable is set to a truthy value. To customize create a custom
// *Golden instance with New() and set a new UpdateFunc value. // *Golden instance with New() and set a new UpdateFunc value.
func Update() bool { func Update() bool {
return DefaultGolden.Update() 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())
}
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,80 +1,43 @@
package golden package golden
import "os" import (
"os"
)
type Option interface { // Option is a function that modifies a Golden instance.
apply(*gold) type Option func(*Golden)
}
type optionFunc func(*gold) // WithDirMode sets the directory mode for a Golden instance.
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 { func WithDirMode(mode os.FileMode) Option {
return optionFunc(func(g *gold) { return func(g *Golden) {
g.dirMode = mode g.DirMode = mode
}) }
} }
// WithFileMode sets the file system permissions used for any created or updated // WithFileMode sets the file mode for a Golden instance.
// golden files written to.
//
// When this option is not provided, the default value is 0o644.
func WithFileMode(mode os.FileMode) Option { func WithFileMode(mode os.FileMode) Option {
return optionFunc(func(g *gold) { return func(g *Golden) {
g.fileMode = mode g.FileMode = mode
}) }
} }
// WithSuffix sets the filename suffix used for all golden files. // WithSuffix sets the file suffix for a Golden instance.
//
// When this option is not provided, the default value is ".golden".
func WithSuffix(suffix string) Option { func WithSuffix(suffix string) Option {
return optionFunc(func(g *gold) { return func(g *Golden) {
g.suffix = suffix g.Suffix = suffix
}) }
} }
// WithDirname sets the name of the top-level directory used to hold golden // WithDirname sets the directory name for a Golden instance.
// files. func WithDirname(dirname string) Option {
// return func(g *Golden) {
// When this option is not provided, the default value is "testdata". g.Dirname = dirname
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 // WithUpdateFunc sets the update function for a Golden instance.
// updated or not. Essentially the provided UpdateFunc is called by Update(). func WithUpdateFunc(updateFunc UpdateFunc) Option {
// return func(g *Golden) {
// When this option is not provided, the default value is EnvUpdateFunc. g.UpdateFunc = updateFunc
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
})
} }

58
options_test.go Normal file
View File

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

View File

@@ -1,21 +0,0 @@
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

@@ -1,67 +0,0 @@
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)
})
}
}

View File

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

View File

@@ -1,275 +0,0 @@
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
}

View File

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

View File

@@ -1,30 +0,0 @@
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,7 +1,6 @@
package golden package golden
import ( import (
"flag"
"os" "os"
"strings" "strings"
) )
@@ -18,29 +17,10 @@ type UpdateFunc func() bool
func EnvUpdateFunc() bool { func EnvUpdateFunc() bool {
env := os.Getenv("GOLDEN_UPDATE") env := os.Getenv("GOLDEN_UPDATE")
for _, v := range truthyStrings { for _, v := range truthyStrings {
if strings.ToLower(env) == v { if strings.EqualFold(env, v) {
return true return true
} }
} }
return false 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,7 +3,6 @@ package golden
import ( import (
"testing" "testing"
"github.com/jimeh/envctl"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@@ -41,181 +40,94 @@ var envUpdateFuncTestCases = []struct {
env: map[string]string{"GOLDEN_UPDATE": "y"}, env: map[string]string{"GOLDEN_UPDATE": "y"},
want: true, want: true,
}, },
{
name: "GOLDEN_UPDATE set to Y",
env: map[string]string{"GOLDEN_UPDATE": "Y"},
want: true,
},
{ {
name: "GOLDEN_UPDATE set to n", name: "GOLDEN_UPDATE set to n",
env: map[string]string{"GOLDEN_UPDATE": "n"}, env: map[string]string{"GOLDEN_UPDATE": "n"},
want: false, want: false,
}, },
{
name: "GOLDEN_UPDATE set to N",
env: map[string]string{"GOLDEN_UPDATE": "N"},
want: false,
},
{ {
name: "GOLDEN_UPDATE set to t", name: "GOLDEN_UPDATE set to t",
env: map[string]string{"GOLDEN_UPDATE": "t"}, env: map[string]string{"GOLDEN_UPDATE": "t"},
want: true, want: true,
}, },
{
name: "GOLDEN_UPDATE set to T",
env: map[string]string{"GOLDEN_UPDATE": "T"},
want: true,
},
{ {
name: "GOLDEN_UPDATE set to f", name: "GOLDEN_UPDATE set to f",
env: map[string]string{"GOLDEN_UPDATE": "f"}, env: map[string]string{"GOLDEN_UPDATE": "f"},
want: false, want: false,
}, },
{
name: "GOLDEN_UPDATE set to F",
env: map[string]string{"GOLDEN_UPDATE": "F"},
want: false,
},
{ {
name: "GOLDEN_UPDATE set to yes", name: "GOLDEN_UPDATE set to yes",
env: map[string]string{"GOLDEN_UPDATE": "yes"}, env: map[string]string{"GOLDEN_UPDATE": "yes"},
want: true, 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", name: "GOLDEN_UPDATE set to no",
env: map[string]string{"GOLDEN_UPDATE": "no"}, env: map[string]string{"GOLDEN_UPDATE": "no"},
want: false, 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", name: "GOLDEN_UPDATE set to on",
env: map[string]string{"GOLDEN_UPDATE": "on"}, env: map[string]string{"GOLDEN_UPDATE": "on"},
want: true, 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", name: "GOLDEN_UPDATE set to off",
env: map[string]string{"GOLDEN_UPDATE": "off"}, env: map[string]string{"GOLDEN_UPDATE": "off"},
want: false, 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", name: "GOLDEN_UPDATE set to true",
env: map[string]string{"GOLDEN_UPDATE": "true"}, env: map[string]string{"GOLDEN_UPDATE": "true"},
want: 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", name: "GOLDEN_UPDATE set to false",
env: map[string]string{"GOLDEN_UPDATE": "false"}, env: map[string]string{"GOLDEN_UPDATE": "false"},
want: 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", name: "GOLDEN_UPDATE set to foobarnopebbq",
env: map[string]string{"GOLDEN_UPDATE": "foobarnopebbq"}, env: map[string]string{"GOLDEN_UPDATE": "foobarnopebbq"},
want: false, want: false,
}, },
// Case-insensitive test cases
{ {
name: "GOLDEN_UPDATE set to FOOBARNOPEBBQ", name: "GOLDEN_UPDATE set to Y (uppercase)",
env: map[string]string{"GOLDEN_UPDATE": "FOOBARNOPEBBQ"}, env: map[string]string{"GOLDEN_UPDATE": "Y"},
want: false, 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,
}, },
} }
func TestEnvUpdateFunc(t *testing.T) { func TestEnvUpdateFunc(t *testing.T) {
for _, tt := range envUpdateFuncTestCases { for _, tt := range envUpdateFuncTestCases {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
envctl.WithClean(tt.env, func() { for k, v := range tt.env {
got := EnvUpdateFunc() t.Setenv(k, v)
}
assert.Equal(t, tt.want, got) got := EnvUpdateFunc()
})
assert.Equal(t, tt.want, got)
}) })
} }
} }