wip: refactor Golden to a interface, improve testability of main implementation

This commit is contained in:
2022-03-21 23:09:43 +00:00
parent edb189f086
commit 138981b7d4
9 changed files with 987 additions and 114 deletions

3
go.mod
View File

@@ -4,5 +4,8 @@ go 1.15
require (
github.com/jimeh/envctl v0.1.0
github.com/jimeh/go-mocktesting v0.1.0
github.com/spf13/afero v1.6.0
github.com/stretchr/testify v1.7.0
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
)

21
go.sum
View File

@@ -2,13 +2,32 @@ 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/jimeh/go-mocktesting v0.1.0 h1:y0tLABo3V4i9io7m6TiXdXbU3IVMjtPvWkr+A0+aLTM=
github.com/jimeh/go-mocktesting v0.1.0/go.mod h1:xnekQ6yP/ull2ewkOp1CbgH7Dym7nbKa/t96XWrIiH8=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
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/spf13/afero v1.6.0 h1:xoax2sJ2DT8S8xA2paPFjDCScCNeWsg75VG0DLRreiY=
github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
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=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
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.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

347
golden.go
View File

@@ -3,7 +3,7 @@
//
// 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
// compatible with Linux, macOS and Windows systems regardless of what
// characters might be in a subtest's name.
//
// Usage
@@ -123,30 +123,37 @@
package golden
import (
"io/ioutil"
"os"
"path/filepath"
"strings"
"testing"
"github.com/jimeh/go-golden/sanitize"
"github.com/spf13/afero"
)
const (
DefaultDirMode = 0o755
DefaultFileMode = 0o644
DefaultSuffix = ".golden"
DefaultDirname = "testdata"
)
// TestingT is a interface describing a sub-set of methods of *testing.T which
// golden uses.
type TestingT interface {
Error(args ...interface{})
Errorf(format string, args ...interface{})
FailNow()
Fatal(args ...interface{})
Fatalf(format string, args ...interface{})
Helper()
Log(args ...interface{})
Logf(format string, args ...interface{})
Name() string
}
var DefaultUpdateFunc = EnvUpdateFunc
var global = New()
var defaultGolden = 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 {
t.Helper()
return global.File(t)
return defaultGolden.File(t)
}
// Get returns the content of the golden file for the given *testing.T instance
@@ -155,7 +162,7 @@ func File(t *testing.T) string {
func Get(t *testing.T) []byte {
t.Helper()
return global.Get(t)
return defaultGolden.Get(t)
}
// Set writes given data to the golden file for the given *testing.T instance as
@@ -164,7 +171,7 @@ func Get(t *testing.T) []byte {
func Set(t *testing.T, data []byte) {
t.Helper()
global.Set(t, data)
defaultGolden.Set(t, data)
}
// FileP returns the filename of the specifically named golden file for the
@@ -172,7 +179,7 @@ func Set(t *testing.T, data []byte) {
func FileP(t *testing.T, name string) string {
t.Helper()
return global.FileP(t, name)
return defaultGolden.FileP(t, name)
}
// GetP returns the content of the specifically named golden file belonging
@@ -184,7 +191,7 @@ func FileP(t *testing.T, name string) string {
func GetP(t *testing.T, name string) []byte {
t.Helper()
return global.GetP(t, name)
return defaultGolden.GetP(t, name)
}
// SetP writes given data of the specifically named golden file belonging to
@@ -196,7 +203,7 @@ func GetP(t *testing.T, name string) []byte {
func SetP(t *testing.T, name string, data []byte) {
t.Helper()
global.SetP(t, name, data)
defaultGolden.SetP(t, name, data)
}
// Update returns true when golden is set to update golden files. Should be used
@@ -206,104 +213,228 @@ func SetP(t *testing.T, name string, data []byte) {
// environment variable is set to a truthy value. To customize create a custom
// *Golden instance with New() and set a new UpdateFunc value.
func Update() bool {
return global.Update()
return defaultGolden.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
// functions proxy through to a default global Golden instance.
type Golden interface {
// File returns the filename of the golden file for the given testing.TB
// instance as determined by t.Name().
File(t TestingT) string
// FileMode determines the file system permissions of any created or updated
// golden files written to disk.
FileMode os.FileMode
// Get returns the content of the golden file for the given TestingT
// instance as determined by t.Name(). If no golden file can be found/read,
// it will fail the test by calling t.Fatal().
Get(t TestingT) []byte
// Suffix determines the filename suffix for all golden files. Typically
// this should be ".golden", but can be changed here if needed.
Suffix string
// Set writes given data to the golden file for the given TestingT
// instance as determined by t.Name(). If writing fails it will fail the
// test by calling t.Fatal() with error details.
Set(t TestingT, data []byte)
// 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
// FileP returns the filename of the specifically named golden file for the
// given TestingT instance as determined by t.Name().
FileP(t TestingT, name string) string
// UpdateFunc is used to determine if golden files should be updated or
// not. Its boolean return value is returned by Update().
UpdateFunc UpdateFunc
// GetP returns the content of the specifically named golden file belonging
// to the given TestingT instance as determined by t.Name(). If no golden
// file can be found/read, it will fail the test with t.Fatal().
//
// This is very similar to Get(), but it allows multiple different golden
// files to be used within the same one TestingT instance.
GetP(t TestingT, name string) []byte
// SetP writes given data of the specifically named golden file belonging to
// the given TestingT instance as determined by t.Name(). If writing fails
// it will fail the test with t.Fatal() detailing the error.
//
// This is very similar to Set(), but it allows multiple different golden
// files to be used within the same one TestingT instance.
SetP(t TestingT, name string, data []byte)
// 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 with default values correctly
// populated. This is ideally how you should create a custom *Golden, and then
// modify the relevant fields as you see fit.
func New() *Golden {
return &Golden{
DirMode: DefaultDirMode,
FileMode: DefaultFileMode,
Suffix: DefaultSuffix,
Dirname: DefaultDirname,
UpdateFunc: DefaultUpdateFunc,
// 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 := &golden{
dirMode: 0o755,
fileMode: 0o644,
suffix: ".golden",
dirname: "testdata",
updateFunc: EnvUpdateFunc,
fs: afero.NewOsFs(),
logOnWrite: true,
}
for _, opt := range options {
opt.apply(g)
}
return g
}
// 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 {
type Option interface {
apply(*golden)
}
type optionFunc func(*golden)
func (fn optionFunc) apply(g *golden) {
fn(g)
}
// WithDirMode sets the file system permissions used for any folders created to
// hold golden files.
//
// When this option is not provided, the default value is 0o755.
func WithDirMode(mode os.FileMode) Option {
return optionFunc(func(g *golden) {
g.dirMode = mode
})
}
// WithFileMode sets the file system permissions used for any created or updated
// golden files written to.
//
// When this option is not provided, the default value is 0o644.
func WithFileMode(mode os.FileMode) Option {
return optionFunc(func(g *golden) {
g.fileMode = mode
})
}
// WithSuffix sets the filename suffix used for all golden files.
//
// When this option is not provided, the default value is ".golden".
func WithSuffix(suffix string) Option {
return optionFunc(func(g *golden) {
g.suffix = suffix
})
}
// WithDirname sets the name of the top-level directory used to hold golden
// files.
//
// When this option is not provided, the default value is "testdata".
func WithDirname(name string) Option {
return optionFunc(func(g *golden) {
g.dirname = name
})
}
// WithUpdateFunc sets the function used to determine if golden files should be
// updated or not. Essentially the provided UpdateFunc is called by Update().
//
// When this option is not provided, the default value is EnvUpdateFunc.
func WithUpdateFunc(fn UpdateFunc) Option {
return optionFunc(func(g *golden) {
g.updateFunc = fn
})
}
// WithFs sets s afero.Fs instance which is used to read/write all golden files.
//
// When this option is not provided, the default value is afero.NewOsFs().
func WithFs(fs afero.Fs) Option {
return optionFunc(func(g *golden) {
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 *golden) {
g.logOnWrite = false
})
}
// golden is the underlying struct that implements the Golden interface.
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 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 afero.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 = &golden{}
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 *testing.T) []byte {
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 *testing.T, data []byte) {
func (s *golden) Set(t TestingT, data []byte) {
t.Helper()
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")
}
func (s *golden) FileP(t TestingT, name string) string {
t.Helper()
return ""
if name == "" {
t.Fatalf("golden: test 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 *testing.T, name string) []byte {
func (s *golden) GetP(t TestingT, name string) []byte {
t.Helper()
if name == "" {
t.Fatal("golden: name cannot be empty")
return nil
}
return s.get(t, name)
}
// SetP writes given data of the specifically named golden file belonging to
// the given *testing.T instance as determined by t.Name(). If writing fails it
// will fail the test with t.Fatal() detailing the error.
//
// This is very similar to Set(), but it allows multiple different golden files
// to be used within the same one *testing.T instance.
func (s *Golden) SetP(t *testing.T, name string, data []byte) {
func (s *golden) SetP(t TestingT, name string, data []byte) {
t.Helper()
if name == "" {
t.Fatal("golden: name cannot be empty")
}
@@ -311,65 +442,65 @@ func (s *Golden) SetP(t *testing.T, name string, data []byte) {
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)
func (s *golden) file(t TestingT, name string) string {
t.Helper()
return ""
if t.Name() == "" {
t.Fatalf(
"golden: could not determine filename for given %T instance", t,
)
}
base := []string{s.Dirname, filepath.FromSlash(t.Name())}
base := []string{s.dirname, filepath.FromSlash(t.Name())}
if name != "" {
base = append(base, name)
}
f := filepath.Clean(filepath.Join(base...) + s.Suffix)
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))
clean = append(clean, sanitize.Filename(s))
}
return strings.Join(clean, string(os.PathSeparator))
}
func (s *Golden) get(t *testing.T, name string) []byte {
func (s *golden) get(t TestingT, name string) []byte {
t.Helper()
f := s.file(t, name)
b, err := ioutil.ReadFile(f)
b, err := afero.ReadFile(s.fs, f)
if err != nil {
t.Fatalf("golden: failed reading %s: %s", f, err.Error())
t.Fatalf("golden: %s", err.Error())
}
return b
}
func (s *Golden) set(t *testing.T, name string, data []byte) {
func (s *golden) set(t TestingT, name string, data []byte) {
t.Helper()
f := s.file(t, name)
dir := filepath.Dir(f)
t.Logf("golden: writing .golden file: %s", f)
err := os.MkdirAll(dir, s.DirMode)
if err != nil {
t.Fatalf("golden: failed to create directory: %s", err.Error())
return
if s.logOnWrite {
t.Logf("golden: writing golden file: %s", f)
}
err = ioutil.WriteFile(f, data, s.FileMode)
err := s.fs.MkdirAll(dir, s.dirMode)
if err != nil {
t.Fatalf("golden: failed to create directory: %s", err.Error())
}
err = afero.WriteFile(s.fs, f, data, s.fileMode)
if err != nil {
t.Fatalf("golden: filed to write file: %s", err.Error())
}
}
// Update returns true when golden is set to update golden files. Should be used
// to determine if golden.Set() or golden.SetP() should be called or not.
//
// Default behavior uses EnvUpdateFunc() to check if the "GOLDEN_UPDATE"
// environment variable is set to a truthy value. To customize set a new
// UpdateFunc value on *Golden.
func (s *Golden) Update() bool {
return s.UpdateFunc()
func (s *golden) Update() bool {
return s.updateFunc()
}

View File

@@ -4,13 +4,21 @@ import (
"io/ioutil"
"os"
"path/filepath"
"reflect"
"runtime"
"testing"
"github.com/jimeh/envctl"
"github.com/jimeh/go-mocktesting"
"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func stringPtr(s string) *string {
return &s
}
func TestFile(t *testing.T) {
got := File(t)
@@ -122,6 +130,7 @@ func TestGet(t *testing.T) {
func TestSet(t *testing.T) {
t.Cleanup(func() {
t.Log("cleaning up golden files")
err := os.RemoveAll(filepath.Join("testdata", "TestSet"))
require.NoError(t, err)
err = os.Remove(filepath.Join("testdata", "TestSet.golden"))
@@ -344,6 +353,7 @@ func TestGetP(t *testing.T) {
func TestSetP(t *testing.T) {
t.Cleanup(func() {
t.Log("cleaning up golden files")
err := os.RemoveAll(filepath.Join("testdata", "TestSetP"))
require.NoError(t, err)
})
@@ -441,3 +451,623 @@ func TestUpdate(t *testing.T) {
})
}
}
func TestNew(t *testing.T) {
myUpdateFunc := func() bool { return false }
type args struct {
options []Option
}
tests := []struct {
name string
args args
want *golden
}{
{
name: "no options",
args: args{options: nil},
want: &golden{
dirMode: 0o755,
fileMode: 0o644,
suffix: ".golden",
dirname: "testdata",
updateFunc: EnvUpdateFunc,
fs: afero.NewOsFs(),
logOnWrite: true,
},
},
{
name: "all options",
args: args{
options: []Option{
WithDirMode(0o777),
WithFileMode(0o666),
WithSuffix(".gold"),
WithDirname("goldstuff"),
WithUpdateFunc(myUpdateFunc),
WithFs(afero.NewMemMapFs()),
WithSilentWrites(),
},
},
want: &golden{
dirMode: 0o777,
fileMode: 0o666,
suffix: ".gold",
dirname: "goldstuff",
updateFunc: myUpdateFunc,
fs: afero.NewMemMapFs(),
logOnWrite: false,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := New(tt.args.options...)
got, ok := g.(*golden)
require.True(t, ok, "New did not returns a *golden instance")
gotUpdateFunc := runtime.FuncForPC(
reflect.ValueOf(got.updateFunc).Pointer(),
).Name()
wantUpdateFunc := runtime.FuncForPC(
reflect.ValueOf(tt.want.updateFunc).Pointer(),
).Name()
assert.Equal(t, tt.want.dirMode, got.dirMode)
assert.Equal(t, tt.want.fileMode, got.fileMode)
assert.Equal(t, tt.want.suffix, got.suffix)
assert.Equal(t, tt.want.dirname, got.dirname)
assert.Equal(t, tt.want.logOnWrite, got.logOnWrite)
assert.Equal(t, wantUpdateFunc, gotUpdateFunc)
assert.IsType(t, tt.want.fs, got.fs)
})
}
}
func Test_golden_File(t *testing.T) {
type fields struct {
suffix *string
dirname *string
}
tests := []struct {
name string
testName string
fields fields
want string
wantAborted bool
wantFailCount int
wantTestOutput []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: "",
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"),
},
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 := &golden{
suffix: *tt.fields.suffix,
dirname: *tt.fields.dirname,
}
mt := mocktesting.NewT(tt.testName)
var got string
mocktesting.Go(func() {
got = g.File(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_golden_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 := afero.NewMemMapFs()
for f, b := range tt.files {
_ = afero.WriteFile(fs, f, b, 0o644)
}
if tt.fields.suffix == nil {
tt.fields.suffix = stringPtr(".golden")
}
if tt.fields.dirname == nil {
tt.fields.dirname = stringPtr("testdata")
}
g := &golden{
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_golden_FileP(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
want string
wantAborted bool
wantFailCount int
wantTestOutput []string
}{
{
name: "top-level",
testName: "TestFooBar",
args: args{name: "yaml"},
want: filepath.Join("testdata", "TestFooBar", "yaml.golden"),
},
{
name: "sub-test",
testName: "TestFooBar/it_is_here",
args: args{name: "json"},
want: filepath.Join(
"testdata", "TestFooBar", "it_is_here", "json.golden",
),
},
{
name: "blank test name",
testName: "",
args: args{name: "json"},
wantAborted: true,
wantFailCount: 1,
wantTestOutput: []string{
"golden: could not determine filename for given " +
"*mocktesting.T instance\n",
},
},
{
name: "custom dirname",
testName: "TestFozBar",
args: args{name: "xml"},
fields: fields{
dirname: stringPtr("goldenfiles"),
},
want: filepath.Join("goldenfiles", "TestFozBar", "xml.golden"),
},
{
name: "custom suffix",
testName: "TestFozBaz",
args: args{name: "toml"},
fields: fields{
suffix: stringPtr(".goldfile"),
},
want: filepath.Join("testdata", "TestFozBaz", "toml.goldfile"),
},
{
name: "custom dirname and suffix",
testName: "TestFozBar",
args: args{name: "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`,
args: args{name: "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 := &golden{
suffix: *tt.fields.suffix,
dirname: *tt.fields.dirname,
}
mt := mocktesting.NewT(tt.testName)
var got string
mocktesting.Go(func() {
got = g.FileP(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")
})
}
}
func Test_golden_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 := afero.NewMemMapFs()
for f, b := range tt.files {
_ = afero.WriteFile(fs, f, b, 0o644)
}
if tt.fields.suffix == nil {
tt.fields.suffix = stringPtr(".golden")
}
if tt.fields.dirname == nil {
tt.fields.dirname = stringPtr("testdata")
}
g := &golden{
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")
})
}
}

View File

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

View File

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

21
sanitize/line_breaks.go Normal file
View File

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

View File

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