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  }