github.com/blend/go-sdk@v1.20240719.1/testutil/golden.go (about) 1 /* 2 3 Copyright (c) 2024 - 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 // 104 // been set. This can also be checked in `flag.CommandLine.` 105 // See: https://github.com/golang/go/blob/go1.17.1/src/flag/flag.go#L879 106 func getUpdateGoldenFlag(it *assert.Assertions, name string) bool { 107 goldenFileFlagsLock.Lock() 108 defer goldenFileFlagsLock.Unlock() 109 110 b, ok := goldenFileFlags[name] 111 // Careful not to re-define a flag that has already been set. 112 if !ok { 113 existing := flag.CommandLine.Lookup(name) 114 it.Nil(existing, fmt.Sprintf("Update Golden Flag '--%s' is already defined elsewhere", name)) 115 b = flag.Bool(name, false, "Update Golden Files") 116 goldenFileFlags[name] = b 117 } 118 flag.Parse() 119 it.NotNil(b, fmt.Sprintf("Parsed boolean flag '--%s' should not be nil", name)) 120 return *b 121 } 122 123 // FixtureConfig represents defaults used for working with test fixtures. 124 type FixtureConfig struct { 125 Directory string 126 GoldenFilePrefix string 127 UpdateGoldenFlag string 128 } 129 130 // NewFixtureConfig returns a new `FixtureConfig` and applies options. 131 func NewFixtureConfig(opts ...FixtureOption) FixtureConfig { 132 fc := FixtureConfig{ 133 Directory: DefaultFixtureDirectory, 134 GoldenFilePrefix: DefaultGoldenFilePrefix, 135 UpdateGoldenFlag: DefaultUpdateGoldenFlag, 136 } 137 for _, opt := range opts { 138 opt(&fc) 139 } 140 return fc 141 } 142 143 // FixtureOption is a mutator for a `FixtureConfig`. 144 type FixtureOption func(*FixtureConfig) 145 146 // OptTestFixtureDirectory sets the directory used to look up test fixture 147 // files. Both relative and absolute paths are supported. 148 func OptTestFixtureDirectory(name string) FixtureOption { 149 return func(fc *FixtureConfig) { 150 fc.Directory = name 151 } 152 } 153 154 // OptUpdateGoldenFlag sets the default flag used to determine if golden files 155 // should be updated (e.g. to use `--update-golden`, pass `"update-golden"` 156 // here). 157 func OptUpdateGoldenFlag(name string) FixtureOption { 158 return func(fc *FixtureConfig) { 159 fc.UpdateGoldenFlag = name 160 } 161 }