refactor(golden): major refactor and improvements

This includes sanitizing golden file names to avoid characters and names
which are invalid on some operating systems. So this should now work on
Linux, macOS, and Window.
This commit is contained in:
2021-09-17 02:08:09 +01:00
parent 1d50225ce6
commit 3185b09d09
19 changed files with 801 additions and 320 deletions

4
.gitignore vendored
View File

@@ -1,3 +1,5 @@
bin/*
testdata/*
coverage.out
testdata/*
!testdata/TestExample*

View File

@@ -88,6 +88,18 @@ format: $(TOOLDIR)/goimports $(TOOLDIR)/gofumpt
bench:
go test $(V) -count=1 -bench=$(BENCH) $(TESTARGS) ./...
.PHONY: golden-update
golden-update:
GOLDEN_UPDATE=1 $(MAKE) test
.PHONY: golden-clean
golden-clean:
find . -type f -name '*.golden' -path '*/testdata/*' -delete
find . -type d -empty -path '*/testdata/*' -delete
.PHONY: golden-regen
golden-regen: golden-clean golden-update
#
# Code Generation
#
@@ -171,7 +183,7 @@ check-tidy:
# Serve docs
.PHONY: docs
docs: godoc
docs: $(TOOLDIR)/godoc
$(info serviing docs on http://127.0.0.1:6060/pkg/$(GOMODNAME)/)
@godoc -http=127.0.0.1:6060

108
example_test.go Normal file
View File

@@ -0,0 +1,108 @@
package golden_test
import (
"encoding/json"
"encoding/xml"
"testing"
"github.com/jimeh/go-golden"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type MyStruct struct {
Foo string `json:"foo,omitempty"`
}
// TestExampleMyStruct reads/writes the following golden file:
//
// testdata/TestExampleMyStruct.golden
//
func TestExampleMyStruct(t *testing.T) {
got, err := json.Marshal(&MyStruct{Foo: "Bar"})
require.NoError(t, err)
if golden.Update() {
golden.Set(t, got)
}
want := golden.Get(t)
assert.Equal(t, want, got)
}
// TestExampleMyStructTabular reads/writes the following golden files:
//
// testdata/TestExampleMyStructTabular/empty_struct.golden
// testdata/TestExampleMyStructTabular/full_struct.golden
//
func TestExampleMyStructTabular(t *testing.T) {
tests := []struct {
name string
obj *MyStruct
}{
{name: "empty struct", obj: &MyStruct{}},
{name: "full struct", obj: &MyStruct{Foo: "Bar"}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := json.Marshal(tt.obj)
require.NoError(t, err)
if golden.Update() {
golden.Set(t, got)
}
want := golden.Get(t)
assert.Equal(t, want, got)
})
}
}
// TestExampleMyStructP reads/writes the following golden file:
//
// testdata/TestExampleMyStructP/json.golden
// testdata/TestExampleMyStructP/xml.golden
//
func TestExampleMyStructP(t *testing.T) {
gotJSON, _ := json.Marshal(&MyStruct{Foo: "Bar"})
gotXML, _ := xml.Marshal(&MyStruct{Foo: "Bar"})
if golden.Update() {
golden.SetP(t, "json", gotJSON)
golden.SetP(t, "xml", gotXML)
}
assert.Equal(t, golden.GetP(t, "json"), gotJSON)
assert.Equal(t, golden.GetP(t, "xml"), gotXML)
}
// TestExampleMyStructTabularP reads/writes the following golden file:
//
// testdata/TestExampleMyStructTabularP/empty_struct/json.golden
// testdata/TestExampleMyStructTabularP/empty_struct/xml.golden
// testdata/TestExampleMyStructTabularP/full_struct/json.golden
// testdata/TestExampleMyStructTabularP/full_struct/xml.golden
//
func TestExampleMyStructTabularP(t *testing.T) {
tests := []struct {
name string
obj *MyStruct
}{
{name: "empty struct", obj: &MyStruct{}},
{name: "full struct", obj: &MyStruct{Foo: "Bar"}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotJSON, _ := json.Marshal(tt.obj)
gotXML, _ := xml.Marshal(tt.obj)
if golden.Update() {
golden.SetP(t, "json", gotJSON)
golden.SetP(t, "xml", gotXML)
}
assert.Equal(t, golden.GetP(t, "json"), gotJSON)
assert.Equal(t, golden.GetP(t, "xml"), gotXML)
})
}
}

View File

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

350
golden.go
View File

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

View File

@@ -11,98 +11,6 @@ import (
"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)
@@ -122,11 +30,11 @@ func TestFile(t *testing.T) {
},
{
name: "foo/bar",
want: filepath.Join("testdata", "TestFile", "foo/bar.golden"),
want: filepath.Join("testdata", "TestFile", "foo", "bar.golden"),
},
{
name: `"foobar"`,
want: filepath.Join("testdata", "TestFile", "\"foobar\".golden"),
want: filepath.Join("testdata", "TestFile", "_foobar_.golden"),
},
}
for _, tt := range tests {
@@ -140,9 +48,9 @@ func TestFile(t *testing.T) {
func TestGet(t *testing.T) {
t.Cleanup(func() {
err := os.RemoveAll(filepath.Join("testdata", t.Name()))
err := os.RemoveAll(filepath.Join("testdata", "TestGet"))
require.NoError(t, err)
err = os.Remove(filepath.Join("testdata", t.Name()+".golden"))
err = os.Remove(filepath.Join("testdata", "TestGet.golden"))
require.NoError(t, err)
})
@@ -188,7 +96,7 @@ func TestGet(t *testing.T) {
{
name: "thing: it's a thing!",
file: filepath.Join(
"testdata", "TestGet", "thing:_it's_a_thing!.golden",
"testdata", "TestGet", "thing__it's_a_thing!.golden",
),
want: []byte("A thing? Really? Are we getting lazy? :P"),
},
@@ -214,9 +122,9 @@ func TestGet(t *testing.T) {
func TestSet(t *testing.T) {
t.Cleanup(func() {
err := os.RemoveAll(filepath.Join("testdata", t.Name()))
err := os.RemoveAll(filepath.Join("testdata", "TestSet"))
require.NoError(t, err)
err = os.Remove(filepath.Join("testdata", t.Name()+".golden"))
err = os.Remove(filepath.Join("testdata", "TestSet.golden"))
require.NoError(t, err)
})
@@ -258,7 +166,7 @@ func TestSet(t *testing.T) {
{
name: "thing: it's a thing!",
file: filepath.Join(
"testdata", "TestSet", "thing:_it's_a_thing!.golden",
"testdata", "TestSet", "thing__it's_a_thing!.golden",
),
content: []byte("A thing? Really? Are we getting lazy? :P"),
},
@@ -278,34 +186,74 @@ func TestSet(t *testing.T) {
}
}
func TestGetNamed(t *testing.T) {
func TestFileP(t *testing.T) {
got := FileP(t, "sub-name")
assert.Equal(t,
filepath.Join("testdata", "TestFileP", "sub-name.golden"), got,
)
tests := []struct {
name string
named string
want string
}{
{
name: "",
named: "sub-thing",
want: filepath.Join(
"testdata", "TestFileP", "#00", "sub-thing.golden",
),
},
{
name: "fozbaz",
named: "email",
want: filepath.Join(
"testdata", "TestFileP", "fozbaz", "email.golden",
),
},
{
name: "fozbaz",
named: "json",
want: filepath.Join(
"testdata", "TestFileP", "fozbaz#01", "json.golden",
),
},
{
name: "foo/bar",
named: "hello/world",
want: filepath.Join(
"testdata", "TestFileP",
"foo", "bar",
"hello", "world.golden",
),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := FileP(t, tt.named)
assert.Equal(t, tt.want, got)
})
}
}
func TestGetP(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"))
err := os.RemoveAll(filepath.Join("testdata", "TestGetP"))
require.NoError(t, err)
})
err := os.MkdirAll(filepath.Join("testdata", "TestGetNamed"), 0o755)
err := os.MkdirAll(filepath.Join("testdata", "TestGetP"), 0o755)
require.NoError(t, err)
content := []byte("this is the default golden file for TestGetNamed")
content := []byte("this is the named golden file for TestGetP")
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"),
filepath.Join("testdata", "TestGetP", "sub-name.golden"),
content, 0o644,
)
require.NoError(t, err)
got = GetNamed(t, "sub-name")
got := GetP(t, "sub-name")
assert.Equal(t, content, got)
tests := []struct {
@@ -314,16 +262,11 @@ func TestGetNamed(t *testing.T) {
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",
"testdata", "TestGetP", "#00", "sub-zero-one.golden",
),
want: []byte("number zero-one here"),
},
@@ -331,7 +274,7 @@ func TestGetNamed(t *testing.T) {
name: "foobar",
named: "email",
file: filepath.Join(
"testdata", "TestGetNamed", "foobar/email.golden",
"testdata", "TestGetP", "foobar", "email.golden",
),
want: []byte("foobar email here"),
},
@@ -339,7 +282,7 @@ func TestGetNamed(t *testing.T) {
name: "foobar",
named: "json",
file: filepath.Join(
"testdata", "TestGetNamed", "foobar#01/json.golden",
"testdata", "TestGetP", "foobar#01", "json.golden",
),
want: []byte("foobar json here"),
},
@@ -347,7 +290,7 @@ func TestGetNamed(t *testing.T) {
name: "foo/bar",
named: "hello/world",
file: filepath.Join(
"testdata", "TestGetNamed",
"testdata", "TestGetP",
"foo", "bar",
"hello", "world.golden",
),
@@ -357,7 +300,7 @@ func TestGetNamed(t *testing.T) {
name: "john's lost flip-flop",
named: "left",
file: filepath.Join(
"testdata", "TestGetNamed", "john's_lost_flip-flop",
"testdata", "TestGetP", "john's_lost_flip-flop",
"left.golden",
),
want: []byte("Did John lose his left flip-flop again?"),
@@ -366,7 +309,7 @@ func TestGetNamed(t *testing.T) {
name: "john's lost flip-flop",
named: "right",
file: filepath.Join(
"testdata", "TestGetNamed", "john's_lost_flip-flop#01",
"testdata", "TestGetP", "john's_lost_flip-flop#01",
"right.golden",
),
want: []byte("Did John lose his right flip-flop again?"),
@@ -375,14 +318,14 @@ func TestGetNamed(t *testing.T) {
name: "thing: it's",
named: "a thing!",
file: filepath.Join(
"testdata", "TestGetNamed", "thing:_it's", "a thing!.golden",
"testdata", "TestGetP", "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)
f := FileP(t, tt.named)
dir := filepath.Dir(f)
err := os.MkdirAll(dir, 0o755)
@@ -391,7 +334,7 @@ func TestGetNamed(t *testing.T) {
err = ioutil.WriteFile(f, tt.want, 0o644) //nolint:gosec
require.NoError(t, err)
got := GetNamed(t, tt.named)
got := GetP(t, tt.named)
assert.Equal(t, filepath.FromSlash(tt.file), f)
assert.Equal(t, tt.want, got)
@@ -399,27 +342,17 @@ func TestGetNamed(t *testing.T) {
}
}
func TestSetNamed(t *testing.T) {
func TestSetP(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"))
err := os.RemoveAll(filepath.Join("testdata", "TestSetP"))
require.NoError(t, err)
})
content := []byte("This is the default golden file for TestSetNamed ^_^")
SetNamed(t, "", content)
content := []byte("This is the named golden file for TestSetP ^_^")
SetP(t, "sub-name", 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"),
b, err := ioutil.ReadFile(
filepath.Join("testdata", "TestSetP", "sub-name.golden"),
)
require.NoError(t, err)
@@ -431,16 +364,11 @@ func TestSetNamed(t *testing.T) {
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",
"testdata", "TestSetP", "#00", "sub-zero-one.golden",
),
content: []byte("number zero-one sub-zero-one strikes again"),
},
@@ -448,7 +376,7 @@ func TestSetNamed(t *testing.T) {
name: "foobar",
named: "email",
file: filepath.Join(
"testdata", "TestSetNamed", "foobar", "email.golden",
"testdata", "TestSetP", "foobar", "email.golden",
),
content: []byte("foobar here"),
},
@@ -456,22 +384,15 @@ func TestSetNamed(t *testing.T) {
name: "foobar",
named: "json",
file: filepath.Join(
"testdata", "TestSetNamed", "foobar#01", "json.golden",
"testdata", "TestSetP", "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",
"testdata", "TestSetP", "john's_lost_flip-flop",
"left.golden",
),
content: []byte("Did John lose his left flip-flop again?"),
@@ -480,7 +401,7 @@ func TestSetNamed(t *testing.T) {
name: "john's lost flip-flop",
named: "right",
file: filepath.Join(
"testdata", "TestSetNamed", "john's_lost_flip-flop#01",
"testdata", "TestSetP", "john's_lost_flip-flop#01",
"right.golden",
),
content: []byte("Did John lose his right flip-flop again?"),
@@ -489,16 +410,16 @@ func TestSetNamed(t *testing.T) {
name: "thing: it's",
named: "a thing!",
file: filepath.Join(
"testdata", "TestSetNamed", "thing:_it's", "a thing!.golden",
"testdata", "TestSetP", "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)
f := FileP(t, tt.named)
SetNamed(t, tt.named, tt.content)
SetP(t, tt.named, tt.content)
got, err := ioutil.ReadFile(f)
require.NoError(t, err)
@@ -509,53 +430,14 @@ func TestSetNamed(t *testing.T) {
}
}
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 {
func TestUpdate(t *testing.T) {
for _, tt := range envUpdateFuncTestCases {
t.Run(tt.name, func(t *testing.T) {
got := NamedFile(t, tt.named)
envctl.WithClean(tt.env, func() {
got := Update()
assert.Equal(t, tt.want, got)
})
})
}
}

34
sanitize.go Normal file
View File

@@ -0,0 +1,34 @@
package golden
import (
"regexp"
"strings"
)
var (
whitespaceChars = regexp.MustCompile(`\s`)
illegalChars = regexp.MustCompile(`[\/\?<>\\:\*\|"]`)
controlChars = regexp.MustCompile(`[\x00-\x1f\x80-\x9f]`)
reservedNames = regexp.MustCompile(`^\.+$`)
winReserved = regexp.MustCompile(
`(?i)^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\..*)?$`,
)
)
func sanitizeFilename(name string) string {
if reservedNames.MatchString(name) || winReserved.MatchString(name) {
var b []byte
for i := 0; i < len(name); i++ {
b = append(b, byte('_'))
}
return string(b)
}
r := strings.TrimRight(name, ". ")
r = whitespaceChars.ReplaceAllString(r, "_")
r = illegalChars.ReplaceAllString(r, "_")
r = controlChars.ReplaceAllString(r, "_")
return r
}

124
sanitize_test.go Normal file
View File

@@ -0,0 +1,124 @@
package golden
import (
"testing"
"github.com/stretchr/testify/assert"
)
func Test_sanitizeFilename(t *testing.T) {
tests := []struct {
name string
filename string
want string
}{
{
name: "empty",
filename: "",
want: "",
},
{
name: ".",
filename: ".",
want: "_",
},
{
name: "..",
filename: "..",
want: "__",
},
{
name: "...",
filename: "...",
want: "___",
},
{
name: "clean",
filename: "foo-bar-nope.golden",
want: "foo-bar-nope.golden",
},
{
name: "with spaces",
filename: "foo bar nope.golden",
want: "foo__bar_nope.golden",
},
{
name: "illegal chars",
filename: `foo/?<>\:*|"bar.golden`,
want: "foo_________bar.golden",
},
{
name: "control chars",
filename: "foo\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b" +
"\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a" +
"\x1b\x1c\x1d\x1e\x1fbar.golden",
want: "foo________________________________bar.golden",
},
{
name: "trailing whitespace",
filename: "foobar.golden ",
want: "foobar.golden",
},
{
name: "trailing dots",
filename: "foobar.golden......",
want: "foobar.golden",
},
{
name: "trailing whitespace and dots",
filename: "foobar.golden .. .. .. ",
want: "foobar.golden",
},
{name: "con", filename: "con", want: "___"},
{name: "prn", filename: "prn", want: "___"},
{name: "aux", filename: "aux", want: "___"},
{name: "nul", filename: "nul", want: "___"},
{name: "com1", filename: "com1", want: "____"},
{name: "com2", filename: "com2", want: "____"},
{name: "com3", filename: "com3", want: "____"},
{name: "com4", filename: "com4", want: "____"},
{name: "com5", filename: "com5", want: "____"},
{name: "com6", filename: "com6", want: "____"},
{name: "com7", filename: "com7", want: "____"},
{name: "com8", filename: "com8", want: "____"},
{name: "com9", filename: "com9", want: "____"},
{name: "lpt1", filename: "lpt1", want: "____"},
{name: "lpt2", filename: "lpt2", want: "____"},
{name: "lpt3", filename: "lpt3", want: "____"},
{name: "lpt4", filename: "lpt4", want: "____"},
{name: "lpt5", filename: "lpt5", want: "____"},
{name: "lpt6", filename: "lpt6", want: "____"},
{name: "lpt7", filename: "lpt7", want: "____"},
{name: "lpt8", filename: "lpt8", want: "____"},
{name: "lpt9", filename: "lpt9", want: "____"},
{name: "CON", filename: "CON", want: "___"},
{name: "PRN", filename: "PRN", want: "___"},
{name: "AUX", filename: "AUX", want: "___"},
{name: "NUL", filename: "NUL", want: "___"},
{name: "COM1", filename: "COM1", want: "____"},
{name: "COM2", filename: "COM2", want: "____"},
{name: "COM3", filename: "COM3", want: "____"},
{name: "COM4", filename: "COM4", want: "____"},
{name: "COM5", filename: "COM5", want: "____"},
{name: "COM6", filename: "COM6", want: "____"},
{name: "COM7", filename: "COM7", want: "____"},
{name: "COM8", filename: "COM8", want: "____"},
{name: "COM9", filename: "COM9", want: "____"},
{name: "LPT1", filename: "LPT1", want: "____"},
{name: "LPT2", filename: "LPT2", want: "____"},
{name: "LPT3", filename: "LPT3", want: "____"},
{name: "LPT4", filename: "LPT4", want: "____"},
{name: "LPT5", filename: "LPT5", want: "____"},
{name: "LPT6", filename: "LPT6", want: "____"},
{name: "LPT7", filename: "LPT7", want: "____"},
{name: "LPT8", filename: "LPT8", want: "____"},
{name: "LPT9", filename: "LPT9", want: "____"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := sanitizeFilename(tt.filename)
assert.Equal(t, tt.want, got)
})
}
}

1
testdata/TestExampleMyStruct.golden vendored Normal file
View File

@@ -0,0 +1 @@
{"foo":"Bar"}

View File

@@ -0,0 +1 @@
{"foo":"Bar"}

View File

@@ -0,0 +1 @@
<MyStruct><Foo>Bar</Foo></MyStruct>

View File

@@ -0,0 +1 @@
{}

View File

@@ -0,0 +1 @@
{"foo":"Bar"}

View File

@@ -0,0 +1 @@
{}

View File

@@ -0,0 +1 @@
<MyStruct><Foo></Foo></MyStruct>

View File

@@ -0,0 +1 @@
{"foo":"Bar"}

View File

@@ -0,0 +1 @@
<MyStruct><Foo>Bar</Foo></MyStruct>

View File

@@ -4,11 +4,14 @@ import "os"
var truthyStrings = []string{"1", "y", "t", "yes", "on", "true"}
type UpdatingFunc func() bool
type UpdateFunc func() bool
// EnvVarUpdateFunc checks if the GOLDEN_UPDATE environment variable is set to
// EnvUpdateFunc checks if the GOLDEN_UPDATE environment variable is set to
// one of "1", "y", "t", "yes", "on", or "true".
func EnvVarUpdatingFunc() bool {
//
// This is also the default UpdateFunc used to determine the return value of
// Update().
func EnvUpdateFunc() bool {
env := os.Getenv("GOLDEN_UPDATE")
for _, v := range truthyStrings {
if env == v {

106
update_test.go Normal file
View File

@@ -0,0 +1,106 @@
package golden
import (
"testing"
"github.com/jimeh/envctl"
"github.com/stretchr/testify/assert"
)
var envUpdateFuncTestCases = []struct {
name string
env map[string]string
want bool
}{
{
name: "GOLDEN_UPDATE not set",
want: false,
},
{
name: "GOLDEN_UPDATE set to empty string",
env: map[string]string{"GOLDEN_UPDATE": ""},
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,
},
}
func TestEnvUpdateFunc(t *testing.T) {
for _, tt := range envUpdateFuncTestCases {
t.Run(tt.name, func(t *testing.T) {
envctl.WithClean(tt.env, func() {
got := EnvUpdateFunc()
assert.Equal(t, tt.want, got)
})
})
}
}