From 1d50225ce647ff318247881d79fe258c4244a0d8 Mon Sep 17 00:00:00 2001 From: Jim Myhrberg Date: Thu, 16 Sep 2021 02:30:47 +0100 Subject: [PATCH] feat(golden): initial commit --- .github/workflows/ci.yml | 61 +++++ .gitignore | 3 + .golangci.yml | 95 +++++++ LICENSE | 20 ++ Makefile | 193 ++++++++++++++ README.md | 5 + global.go | 49 ++++ global_test.go | 561 +++++++++++++++++++++++++++++++++++++++ go.mod | 8 + go.sum | 14 + golden.go | 109 ++++++++ updating.go | 20 ++ 12 files changed, 1138 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 .golangci.yml create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 global.go create mode 100644 global_test.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 golden.go create mode 100644 updating.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..b8a1393 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,61 @@ +--- +name: CI +on: [push] + +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: golangci-lint + uses: golangci/golangci-lint-action@v2 + with: + version: v1.42 + env: + VERBOSE: "true" + + tidy: + name: Tidy + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-go@v2 + with: + go-version: 1.15 + - uses: actions/cache@v2 + with: + path: ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + - name: Check if mods are tidy + run: make check-tidy + + test: + name: Test + strategy: + fail-fast: false + matrix: + os: + - ubuntu-latest + - macos-latest + - windows-latest + go_version: + - "1.15" + - "1.16" + - "1.17" + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-go@v2 + with: + go-version: ${{ matrix.go_version }} + - uses: actions/cache@v2 + with: + path: ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + - name: Run tests + run: go test -v -count=1 -race ./... diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ede5c9f --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +bin/* +testdata/* +coverage.out diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..92e57d7 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,95 @@ +linters-settings: + funlen: + lines: 100 + 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 + +linters: + disable-all: true + enable: + - asciicheck + - bodyclose + - deadcode + - depguard + - durationcheck + - errcheck + - errorlint + - exhaustive + - exportloopref + - funlen + - gochecknoinits + - goconst + - gocritic + - gocyclo + - godot + - gofumpt + - goimports + - goprintffuncname + - gosec + - gosimple + - govet + - importas + - ineffassign + - lll + - misspell + - nakedret + - nilerr + - noctx + - nolintlint + - prealloc + - predeclared + - revive + - 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: + - funlen + - dupl + - source: "^//go:generate " + linters: + - lll + - source: "`json:" + linters: + - lll + - source: "`xml:" + linters: + - lll + - source: "`yaml:" + linters: + - lll + +run: + timeout: 2m + allow-parallel-runners: true + modules-download-mode: readonly diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9dcb8ab --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2021 Jim Myhrberg + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..c57f9fc --- /dev/null +++ b/Makefile @@ -0,0 +1,193 @@ +GOMODNAME := $(shell grep 'module' go.mod | sed -e 's/^module //') +SOURCES := $(shell find . -name "*.go" -or -name "go.mod" -or -name "go.sum" \ + -or -name "Makefile") + +# Verbose output +ifdef VERBOSE +V = -v +endif + +# +# Environment +# + +BINDIR := bin +TOOLDIR := $(BINDIR)/tools + +# Global environment variables for all targets +SHELL ?= /bin/bash +SHELL := env \ + GO111MODULE=on \ + GOBIN=$(CURDIR)/$(TOOLDIR) \ + CGO_ENABLED=1 \ + PATH='$(CURDIR)/$(BINDIR):$(CURDIR)/$(TOOLDIR):$(PATH)' \ + $(SHELL) + +# +# Defaults +# + +# Default target +.DEFAULT_GOAL := test + +# +# Tools +# + +TOOLS += $(TOOLDIR)/gobin +$(TOOLDIR)/gobin: + GO111MODULE=off go get -u github.com/myitcv/gobin + +# external tool +define tool # 1: binary-name, 2: go-import-path +TOOLS += $(TOOLDIR)/$(1) + +$(TOOLDIR)/$(1): $(TOOLDIR)/gobin Makefile + gobin $(V) "$(2)" +endef + +$(eval $(call tool,godoc,golang.org/x/tools/cmd/godoc)) +$(eval $(call tool,gofumpt,mvdan.cc/gofumpt)) +$(eval $(call tool,goimports,golang.org/x/tools/cmd/goimports)) +$(eval $(call tool,golangci-lint,github.com/golangci/golangci-lint/cmd/golangci-lint@v1.42)) +$(eval $(call tool,gomod,github.com/Helcaraxan/gomod)) + +.PHONY: tools +tools: $(TOOLS) + +# +# Development +# + +BENCH ?= . +TESTARGS ?= + +.PHONY: clean +clean: + rm -f $(TOOLS) + rm -f ./coverage.out ./go.mod.tidy-check ./go.sum.tidy-check + +.PHONY: test +test: + go test $(V) -count=1 -race $(TESTARGS) ./... + +.PHONY: test-deps +test-deps: + go test all + +.PHONY: lint +lint: $(TOOLDIR)/golangci-lint + golangci-lint $(V) run + +.PHONY: format +format: $(TOOLDIR)/goimports $(TOOLDIR)/gofumpt + goimports -w . && gofumpt -w . + +.SILENT: bench +.PHONY: bench +bench: + go test $(V) -count=1 -bench=$(BENCH) $(TESTARGS) ./... + +# +# Code Generation +# + +.PHONY: generate +generate: + go generate ./... + +.PHONY: check-generate +check-generate: + $(eval CHKDIR := $(shell mktemp -d)) + cp -av . "$(CHKDIR)" + make -C "$(CHKDIR)/" generate + ( diff -rN . "$(CHKDIR)" && rm -rf "$(CHKDIR)" ) || \ + ( rm -rf "$(CHKDIR)" && exit 1 ) + +# +# Coverage +# + +.PHONY: cov +cov: coverage.out + +.PHONY: cov-html +cov-html: coverage.out + go tool cover -html=./coverage.out + +.PHONY: cov-func +cov-func: coverage.out + go tool cover -func=./coverage.out + +coverage.out: $(SOURCES) + go test $(V) -covermode=count -coverprofile=./coverage.out ./... + +# +# Dependencies +# + +.PHONY: deps +deps: + go mod download + +.PHONY: deps-update +deps-update: + go get -u -t ./... + +.PHONY: deps-analyze +deps-analyze: $(TOOLDIR)/gomod + gomod analyze + +.PHONY: tidy +tidy: + go mod tidy $(V) + +.PHONY: verify +verify: + go mod verify + +.SILENT: check-tidy +.PHONY: check-tidy +check-tidy: + cp go.mod go.mod.tidy-check + cp go.sum go.sum.tidy-check + go mod tidy + ( \ + diff go.mod go.mod.tidy-check && \ + diff go.sum go.sum.tidy-check && \ + rm -f go.mod go.sum && \ + mv go.mod.tidy-check go.mod && \ + mv go.sum.tidy-check go.sum \ + ) || ( \ + rm -f go.mod go.sum && \ + mv go.mod.tidy-check go.mod && \ + mv go.sum.tidy-check go.sum; \ + exit 1 \ + ) + +# +# Documentation +# + +# Serve docs +.PHONY: docs +docs: 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)) diff --git a/README.md b/README.md new file mode 100644 index 0000000..12ab269 --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# go-golden + +Yet another Go package for working with `*.golden` test files. + +Currently a work in progress. diff --git a/global.go b/global.go new file mode 100644 index 0000000..3787797 --- /dev/null +++ b/global.go @@ -0,0 +1,49 @@ +package golden + +import "testing" + +var global = New() + +// Updating returns true when golden is set to update golden files. Used to +// determine if golden.Set() should be called or not. +func Updating() bool { + return global.Updating() +} + +// Get returns the content of the default 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 with t.Fatal(). +func Get(t *testing.T) []byte { + return global.Get(t) +} + +// Set writes given data of the default golden file for the given *testing.T +// instance as determined by t.Name(). If writing fails it will fail the test +// with t.Fatal() detailing the error. +func Set(t *testing.T, data []byte) { + global.Set(t, data) +} + +// File returns the filename for the default golden file for the given +// *testing.T instance as determined by t.Name(). +func File(t *testing.T) string { + return global.File(t) +} + +// GetNamed return 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(). +func GetNamed(t *testing.T, name string) []byte { + return global.GetNamed(t, name) +} + +// SetNamed 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. +func SetNamed(t *testing.T, name string, data []byte) { + global.SetNamed(t, name, data) +} + +func NamedFile(t *testing.T, name string) string { + return global.NamedFile(t, name) +} diff --git a/global_test.go b/global_test.go new file mode 100644 index 0000000..6a5c8bd --- /dev/null +++ b/global_test.go @@ -0,0 +1,561 @@ +package golden + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" + + "github.com/jimeh/envctl" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestUpdating(t *testing.T) { + tests := []struct { + name string + env map[string]string + want bool + }{ + { + name: "GOLDEN_UPDATE not set", + want: false, + }, + { + name: "GOLDEN_UPDATE set to 0", + env: map[string]string{"GOLDEN_UPDATE": "0"}, + want: false, + }, + { + name: "GOLDEN_UPDATE set to 1", + env: map[string]string{"GOLDEN_UPDATE": "1"}, + want: true, + }, + { + name: "GOLDEN_UPDATE set to 2", + env: map[string]string{"GOLDEN_UPDATE": "2"}, + want: false, + }, + { + 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 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 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 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 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 foobarnopebbq", + env: map[string]string{"GOLDEN_UPDATE": "foobarnopebbq"}, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + envctl.WithClean(tt.env, func() { + got := Updating() + + assert.Equal(t, tt.want, got) + }) + }) + } +} + +func TestFile(t *testing.T) { + got := File(t) + + assert.Equal(t, filepath.Join("testdata", "TestFile.golden"), got) + + tests := []struct { + name string + want string + }{ + { + name: "", + want: filepath.Join("testdata", "TestFile", "#00.golden"), + }, + { + name: "foobar", + want: filepath.Join("testdata", "TestFile", "foobar.golden"), + }, + { + name: "foo/bar", + want: filepath.Join("testdata", "TestFile", "foo/bar.golden"), + }, + { + name: `"foobar"`, + want: filepath.Join("testdata", "TestFile", "\"foobar\".golden"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := File(t) + + assert.Equal(t, tt.want, got) + }) + } +} + +func TestGet(t *testing.T) { + t.Cleanup(func() { + err := os.RemoveAll(filepath.Join("testdata", t.Name())) + require.NoError(t, err) + err = os.Remove(filepath.Join("testdata", t.Name()+".golden")) + require.NoError(t, err) + }) + + err := os.MkdirAll("testdata", 0o755) + require.NoError(t, err) + + content := []byte("foobar\nhello world :)") + err = ioutil.WriteFile( //nolint:gosec + filepath.Join("testdata", "TestGet.golden"), content, 0o644, + ) + require.NoError(t, err) + + got := Get(t) + assert.Equal(t, content, got) + + tests := []struct { + name string + file string + want []byte + }{ + { + name: "", + file: filepath.Join("testdata", "TestGet", "#00.golden"), + want: []byte("number double-zero here"), + }, + { + name: "foobar", + file: filepath.Join("testdata", "TestGet", "foobar.golden"), + want: []byte("foobar here"), + }, + { + name: "foo/bar", + file: filepath.Join("testdata", "TestGet", "foo", "bar.golden"), + want: []byte("foo/bar style sub-sub-folders works too"), + }, + { + name: "john's lost flip-flop", + file: filepath.Join( + "testdata", "TestGet", "john's_lost_flip-flop.golden", + ), + want: []byte("Did John lose his flip-flop again?"), + }, + { + name: "thing: it's a thing!", + file: filepath.Join( + "testdata", "TestGet", "thing:_it's_a_thing!.golden", + ), + want: []byte("A thing? Really? Are we getting lazy? :P"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f := File(t) + dir := filepath.Dir(f) + + err := os.MkdirAll(dir, 0o755) + require.NoError(t, err) + + err = ioutil.WriteFile(f, tt.want, 0o644) //nolint:gosec + require.NoError(t, err) + + got := Get(t) + + assert.Equal(t, tt.file, f) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestSet(t *testing.T) { + t.Cleanup(func() { + err := os.RemoveAll(filepath.Join("testdata", t.Name())) + require.NoError(t, err) + err = os.Remove(filepath.Join("testdata", t.Name()+".golden")) + require.NoError(t, err) + }) + + content := []byte("This is the default golden file for TestSet ^_^") + Set(t, content) + + b, err := ioutil.ReadFile(filepath.Join("testdata", "TestSet.golden")) + require.NoError(t, err) + + assert.Equal(t, content, b) + + tests := []struct { + name string + file string + content []byte + }{ + { + name: "", + file: filepath.Join("testdata", "TestSet", "#00.golden"), + content: []byte("number double-zero strikes again"), + }, + { + name: "foobar", + file: filepath.Join("testdata", "TestSet", "foobar.golden"), + content: []byte("foobar here"), + }, + { + name: "foo/bar", + file: filepath.Join("testdata", "TestSet", "foo", "bar.golden"), + content: []byte("foo/bar style sub-sub-folders works too"), + }, + { + name: "john's lost flip-flop", + file: filepath.Join( + "testdata", "TestSet", "john's_lost_flip-flop.golden", + ), + content: []byte("Did John lose his flip-flop again?"), + }, + { + name: "thing: it's a thing!", + file: filepath.Join( + "testdata", "TestSet", "thing:_it's_a_thing!.golden", + ), + content: []byte("A thing? Really? Are we getting lazy? :P"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f := File(t) + + Set(t, tt.content) + + got, err := ioutil.ReadFile(f) + require.NoError(t, err) + + assert.Equal(t, tt.file, f) + assert.Equal(t, tt.content, got) + }) + } +} + +func TestGetNamed(t *testing.T) { + t.Cleanup(func() { + err := os.RemoveAll(filepath.Join("testdata", t.Name())) + require.NoError(t, err) + err = os.Remove(filepath.Join("testdata", t.Name()+".golden")) + require.NoError(t, err) + }) + + err := os.MkdirAll(filepath.Join("testdata", "TestGetNamed"), 0o755) + require.NoError(t, err) + + content := []byte("this is the default golden file for TestGetNamed") + err = ioutil.WriteFile( //nolint:gosec + filepath.Join("testdata", "TestGetNamed.golden"), content, 0o644, + ) + require.NoError(t, err) + + got := GetNamed(t, "") + assert.Equal(t, content, got) + + content = []byte("this is the named golden file for TestGetNamed") + err = ioutil.WriteFile( //nolint:gosec + filepath.Join("testdata", "TestGetNamed", "sub-name.golden"), + content, 0o644, + ) + require.NoError(t, err) + + got = GetNamed(t, "sub-name") + assert.Equal(t, content, got) + + tests := []struct { + name string + named string + file string + want []byte + }{ + { + name: "", + file: filepath.Join("testdata", "TestGetNamed", "#00.golden"), + want: []byte("number double-zero here"), + }, + { + name: "", + named: "sub-zero-one", + file: filepath.Join( + "testdata", "TestGetNamed", "#01/sub-zero-one.golden", + ), + want: []byte("number zero-one here"), + }, + { + name: "foobar", + named: "email", + file: filepath.Join( + "testdata", "TestGetNamed", "foobar/email.golden", + ), + want: []byte("foobar email here"), + }, + { + name: "foobar", + named: "json", + file: filepath.Join( + "testdata", "TestGetNamed", "foobar#01/json.golden", + ), + want: []byte("foobar json here"), + }, + { + name: "foo/bar", + named: "hello/world", + file: filepath.Join( + "testdata", "TestGetNamed", + "foo", "bar", + "hello", "world.golden", + ), + want: []byte("foo/bar style sub-sub-folders works too"), + }, + { + name: "john's lost flip-flop", + named: "left", + file: filepath.Join( + "testdata", "TestGetNamed", "john's_lost_flip-flop", + "left.golden", + ), + want: []byte("Did John lose his left flip-flop again?"), + }, + { + name: "john's lost flip-flop", + named: "right", + file: filepath.Join( + "testdata", "TestGetNamed", "john's_lost_flip-flop#01", + "right.golden", + ), + want: []byte("Did John lose his right flip-flop again?"), + }, + { + name: "thing: it's", + named: "a thing!", + file: filepath.Join( + "testdata", "TestGetNamed", "thing:_it's", "a thing!.golden", + ), + want: []byte("A thing? Really? Are we getting lazy? :P"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f := NamedFile(t, tt.named) + dir := filepath.Dir(f) + + err := os.MkdirAll(dir, 0o755) + require.NoError(t, err) + + err = ioutil.WriteFile(f, tt.want, 0o644) //nolint:gosec + require.NoError(t, err) + + got := GetNamed(t, tt.named) + + assert.Equal(t, filepath.FromSlash(tt.file), f) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestSetNamed(t *testing.T) { + t.Cleanup(func() { + err := os.RemoveAll(filepath.Join("testdata", t.Name())) + require.NoError(t, err) + err = os.Remove(filepath.Join("testdata", t.Name()+".golden")) + require.NoError(t, err) + }) + + content := []byte("This is the default golden file for TestSetNamed ^_^") + SetNamed(t, "", content) + + b, err := ioutil.ReadFile(filepath.Join("testdata", "TestSetNamed.golden")) + require.NoError(t, err) + + assert.Equal(t, content, b) + + content = []byte("This is the named golden file for TestSetNamed ^_^") + SetNamed(t, "sub-name", content) + + b, err = ioutil.ReadFile( + filepath.Join("testdata", "TestSetNamed", "sub-name.golden"), + ) + require.NoError(t, err) + + assert.Equal(t, content, b) + + tests := []struct { + name string + named string + file string + content []byte + }{ + { + name: "", + file: filepath.Join("testdata", "TestSetNamed", "#00.golden"), + content: []byte("number double-zero strikes again"), + }, + { + name: "", + named: "sub-zero-one", + file: filepath.Join( + "testdata", "TestSetNamed", "#01", "sub-zero-one.golden", + ), + content: []byte("number zero-one sub-zero-one strikes again"), + }, + { + name: "foobar", + named: "email", + file: filepath.Join( + "testdata", "TestSetNamed", "foobar", "email.golden", + ), + content: []byte("foobar here"), + }, + { + name: "foobar", + named: "json", + file: filepath.Join( + "testdata", "TestSetNamed", "foobar#01", "json.golden", + ), + content: []byte("foobar here"), + }, + { + name: "foo/bar", + file: filepath.Join( + "testdata", "TestSetNamed", "foo", "bar.golden", + ), + content: []byte("foo/bar style sub-sub-folders works too"), + }, + { + name: "john's lost flip-flop", + named: "left", + file: filepath.Join( + "testdata", "TestSetNamed", "john's_lost_flip-flop", + "left.golden", + ), + content: []byte("Did John lose his left flip-flop again?"), + }, + { + name: "john's lost flip-flop", + named: "right", + file: filepath.Join( + "testdata", "TestSetNamed", "john's_lost_flip-flop#01", + "right.golden", + ), + content: []byte("Did John lose his right flip-flop again?"), + }, + { + name: "thing: it's", + named: "a thing!", + file: filepath.Join( + "testdata", "TestSetNamed", "thing:_it's", "a thing!.golden", + ), + content: []byte("A thing? Really? Are we getting lazy? :P"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f := NamedFile(t, tt.named) + + SetNamed(t, tt.named, tt.content) + + got, err := ioutil.ReadFile(f) + require.NoError(t, err) + + assert.Equal(t, tt.file, f) + assert.Equal(t, tt.content, got) + }) + } +} + +func TestNamedFile(t *testing.T) { + got := NamedFile(t, "") + assert.Equal(t, "testdata/TestNamedFile.golden", got) + + got = NamedFile(t, "sub-name") + assert.Equal(t, "testdata/TestNamedFile/sub-name.golden", got) + + tests := []struct { + name string + named string + want string + }{ + { + name: "", + named: "", + want: "testdata/TestNamedFile/#00.golden", + }, + { + name: "", + named: "sub-thing", + want: "testdata/TestNamedFile/#01/sub-thing.golden", + }, + { + name: "foobar", + want: "testdata/TestNamedFile/foobar.golden", + }, + { + name: "fozbaz", + named: "email", + want: "testdata/TestNamedFile/fozbaz/email.golden", + }, + { + name: "fozbaz", + named: "json", + want: "testdata/TestNamedFile/fozbaz#01/json.golden", + }, + { + name: "foo/bar", + named: "hello/world", + want: "testdata/TestNamedFile/foo/bar/hello/world.golden", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := NamedFile(t, tt.named) + + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..6182b53 --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module github.com/jimeh/go-golden + +go 1.15 + +require ( + github.com/jimeh/envctl v0.1.0 + github.com/stretchr/testify v1.7.0 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..341eccf --- /dev/null +++ b/go.sum @@ -0,0 +1,14 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/jimeh/envctl v0.1.0 h1:KTv3D+pi5M4/PgFVE/W8ssWqiZP3pDJ8Cga50L+1avo= +github.com/jimeh/envctl v0.1.0/go.mod h1:aM27ffBbO1yUBKUzgJGCUorS4z+wyh+qhQe1ruxXZZo= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/golden.go b/golden.go new file mode 100644 index 0000000..2e7d266 --- /dev/null +++ b/golden.go @@ -0,0 +1,109 @@ +// Package golden is yet another package for working with *.golden test files. +package golden + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" +) + +const ( + DefaultDirMode = 0o755 + DefaultFileMode = 0o644 + DefaultSuffix = ".golden" + DefaultDirname = "testdata" +) + +type Golden struct { + DirMode os.FileMode + FileMode os.FileMode + Suffix string + Dirname string + UpdatingFunc UpdatingFunc +} + +func New() *Golden { + return &Golden{ + DirMode: DefaultDirMode, + FileMode: DefaultFileMode, + Suffix: DefaultSuffix, + Dirname: DefaultDirname, + UpdatingFunc: EnvVarUpdatingFunc, + } +} + +// Updating returns true when the function assigned to UpdatingFunc returns +// true. +func (s *Golden) Updating() bool { + return s.UpdatingFunc() +} + +// Get returns the content of the default 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 with t.Fatal(). +func (s *Golden) Get(t *testing.T) []byte { + return s.GetNamed(t, "") +} + +// Set writes given data of the default golden file for the given *testing.T +// instance as determined by t.Name(). If writing fails it will fail the test +// with t.Fatal() detailing the error. +func (s *Golden) Set(t *testing.T, data []byte) { + s.SetNamed(t, "", data) +} + +func (s *Golden) File(t *testing.T) string { + return s.NamedFile(t, "") +} + +func (s *Golden) GetNamed(t *testing.T, name string) []byte { + if t == nil { + return nil + } + + f := s.NamedFile(t, name) + + b, err := ioutil.ReadFile(f) + if err != nil { + t.Fatalf("golden: failed reading %s: %s", f, err.Error()) + } + + return b +} + +func (s *Golden) SetNamed(t *testing.T, name string, data []byte) { + if t == nil { + return + } + + f := s.NamedFile(t, name) + dir := filepath.Dir(f) + + t.Logf("golden: writing .golden file: %s", f) + + err := os.MkdirAll(dir, s.DirMode) + if err != nil { + t.Fatalf("golden: failed to create directory: %s", err.Error()) + return + } + + err = ioutil.WriteFile(f, data, s.FileMode) + if err != nil { + t.Fatalf("golden: filed to write file: %s", err.Error()) + } +} + +func (s *Golden) NamedFile(t *testing.T, name string) string { + if t == nil || t.Name() == "" { + t.Fatalf("golden: could not determine filename for: %+v", t) + return "" + } + + base := []string{s.Dirname, filepath.FromSlash(t.Name())} + if name != "" { + base = append(base, name) + } + + return filepath.Clean(filepath.Join(base...) + s.Suffix) +} diff --git a/updating.go b/updating.go new file mode 100644 index 0000000..94dd6e2 --- /dev/null +++ b/updating.go @@ -0,0 +1,20 @@ +package golden + +import "os" + +var truthyStrings = []string{"1", "y", "t", "yes", "on", "true"} + +type UpdatingFunc func() bool + +// EnvVarUpdateFunc checks if the GOLDEN_UPDATE environment variable is set to +// one of "1", "y", "t", "yes", "on", or "true". +func EnvVarUpdatingFunc() bool { + env := os.Getenv("GOLDEN_UPDATE") + for _, v := range truthyStrings { + if env == v { + return true + } + } + + return false +}