github.com/blend/go-sdk@v1.20220411.3/testutil/golden.go (about) 1 /* 2 3 Copyright (c) 2022 - Present. Blend Labs, Inc. All rights reserved 4 Use of this source code is governed by a MIT license that can be found in the LICENSE file. 5 6 */ 7 8 package testutil 9 10 import ( 11 "bytes" 12 "flag" 13 "fmt" 14 "os" 15 "path/filepath" 16 "sync" 17 18 "github.com/blend/go-sdk/assert" 19 ) 20 21 const ( 22 // DefaultFixtureDirectory is the default directory where test fixture 23 // files will be retrieved. Since `go test` sets the present working directory 24 // to the current directory under test, this is a relative path. 25 DefaultFixtureDirectory = "./testdata" 26 // DefaultGoldenFilePrefix is the prefix that will be prepended to suffixes 27 // for golden filenames. 28 DefaultGoldenFilePrefix = "golden." 29 // DefaultUpdateGoldenFlag is the flag that this package will use to check 30 // if golden files should be updated. 31 DefaultUpdateGoldenFlag = "update-golden" 32 33 defaultNewFilePermissions = 0644 34 ) 35 36 var ( 37 goldenFileFlags = map[string]*bool{} 38 goldenFileFlagsLock = sync.Mutex{} 39 ) 40 41 // GetTestFixture opens a file in the test fixtures directory. This 42 // relies on the present working directory being set to the current 43 // directory under test and will default to `./testdata` when 44 // reading files. 45 func GetTestFixture(it *assert.Assertions, filename string, opts ...FixtureOption) []byte { 46 fc := NewFixtureConfig(opts...) 47 path := filepath.Join(fc.Directory, filename) 48 data, err := os.ReadFile(path) 49 it.Nil(err, fmt.Sprintf("Failed reading Test Fixture File %q", path)) 50 return data 51 } 52 53 // AssertGoldenFile checks that a "golden file" matches the expected contents. 54 // The golden file will use the test fixtures directory and will use the 55 // filename suffix **after** a prefix of `golden.`, e.g. 56 // `./testdata/golden.config.yml`. 57 // 58 // Managing golden files can be tedious, so test suites can optionally specify 59 // a boolean flag (e.g. `go test --update-golden`) that can be propagated to 60 // this function; in which case the golden file will just be overwritten instead 61 // of compared against `expected`. 62 func AssertGoldenFile(it *assert.Assertions, expected []byte, filenameSuffix string, opts ...FixtureOption) { 63 fc := NewFixtureConfig(opts...) 64 update := getUpdateGoldenFlag(it, fc.UpdateGoldenFlag) 65 filename := fmt.Sprintf("%s%s", fc.GoldenFilePrefix, filenameSuffix) 66 path := filepath.Join(fc.Directory, filename) 67 68 if update { 69 // NOTE: `defaultNewFilePermissions` will only be used for **new** 70 // files, otherwise `os.WriteFile()` will preserve permissions. 71 err := os.WriteFile(path, expected, defaultNewFilePermissions) 72 it.Nil(err, fmt.Sprintf("Error writing Golden File %q", path)) 73 return 74 } 75 76 actual, err := os.ReadFile(path) 77 it.Nil(err, fmt.Sprintf("Failed reading Golden File %q", path)) 78 it.True(bytes.Equal(expected, actual), fmt.Sprintf("Golden File %q does not match expected, consider running with the '--%s' flag to update the Golden File", path, fc.UpdateGoldenFlag)) 79 } 80 81 // MarkUpdateGoldenFlag is intended to be used in a `TestMain()` to declare 82 // a flag **before** `go test` parses flags (if not, unknown flags will 83 // fail a test). 84 // 85 // This is expected to be used in two modes: 86 // > MarkUpdateGoldenFlag() 87 // which will mark `--update-golden` (via `DefaultUpdateGoldenFlag`) as a valid 88 // flag for tests and with a custom flag override: 89 // > MarkUpdateGoldenFlag(OptUpdateGoldenFlag(customFlag)) 90 func MarkUpdateGoldenFlag(opts ...FixtureOption) { 91 goldenFileFlagsLock.Lock() 92 defer goldenFileFlagsLock.Unlock() 93 94 fc := NewFixtureConfig(opts...) 95 b := flag.Bool(fc.UpdateGoldenFlag, false, "Update Golden Files") 96 goldenFileFlags[fc.UpdateGoldenFlag] = b 97 } 98 99 // getUpdateGoldenFlag returns a (parsed) command line flag that indicates if 100 // golden files should be updated. 101 // 102 // NOTE: This function is careful not to re-define a flag that has already 103 // been set. This can also be checked in `flag.CommandLine.` 104 // See: https://github.com/golang/go/blob/go1.17.1/src/flag/flag.go#L879 105 func getUpdateGoldenFlag(it *assert.Assertions, name string) bool { 106 goldenFileFlagsLock.Lock() 107 defer goldenFileFlagsLock.Unlock() 108 109 b, ok := goldenFileFlags[name] 110 // Careful not to re-define a flag that has already been set. 111 if !ok { 112 existing := flag.CommandLine.Lookup(name) 113 it.Nil(existing, fmt.Sprintf("Update Golden Flag '--%s' is already defined elsewhere", name)) 114 b = flag.Bool(name, false, "Update Golden Files") 115 goldenFileFlags[name] = b 116 } 117 flag.Parse() 118 it.NotNil(b, fmt.Sprintf("Parsed boolean flag '--%s' should not be nil", name)) 119 return *b 120 } 121 122 // FixtureConfig represents defaults used for working with test fixtures. 123 type FixtureConfig struct { 124 Directory string 125 GoldenFilePrefix string 126 UpdateGoldenFlag string 127 } 128 129 // NewFixtureConfig returns a new `FixtureConfig` and applies options. 130 func NewFixtureConfig(opts ...FixtureOption) FixtureConfig { 131 fc := FixtureConfig{ 132 Directory: DefaultFixtureDirectory, 133 GoldenFilePrefix: DefaultGoldenFilePrefix, 134 UpdateGoldenFlag: DefaultUpdateGoldenFlag, 135 } 136 for _, opt := range opts { 137 opt(&fc) 138 } 139 return fc 140 } 141 142 // FixtureOption is a mutator for a `FixtureConfig`. 143 type FixtureOption func(*FixtureConfig) 144 145 // OptTestFixtureDirectory sets the directory used to look up test fixture 146 // files. Both relative and absolute paths are supported. 147 func OptTestFixtureDirectory(name string) FixtureOption { 148 return func(fc *FixtureConfig) { 149 fc.Directory = name 150 } 151 } 152 153 // OptUpdateGoldenFlag sets the default flag used to determine if golden files 154 // should be updated (e.g. to use `--update-golden`, pass `"update-golden"` 155 // here). 156 func OptUpdateGoldenFlag(name string) FixtureOption { 157 return func(fc *FixtureConfig) { 158 fc.UpdateGoldenFlag = name 159 } 160 }