vendor: gotest.tools v3.3.0

full diff: https://github.com/gotestyourself/gotest.tools/compare/v3.1.0...v3.3.0

- golden: accept -update for updating files
- assert: golden variables

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
This commit is contained in:
Sebastiaan van Stijn 2022-09-22 15:38:19 +02:00
parent 28b0aa9f1a
commit ef2a826636
No known key found for this signature in database
GPG Key ID: 76698F39D527CE8C
21 changed files with 492 additions and 235 deletions

View File

@ -40,7 +40,7 @@ require (
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
golang.org/x/text v0.3.7
gopkg.in/yaml.v2 v2.4.0
gotest.tools/v3 v3.1.0
gotest.tools/v3 v3.3.0
)
require (

View File

@ -749,8 +749,8 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA=
gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
gotest.tools/v3 v3.1.0 h1:rVV8Tcg/8jHUkPUorwjaMTtemIMVXfIPKiOqnhEhakk=
gotest.tools/v3 v3.1.0/go.mod h1:fHy7eyTmJFO5bQbUsEGQ1v4m2J3Jz9eWL54TP2/ZuYQ=
gotest.tools/v3 v3.3.0 h1:MfDY1b1/0xN1CyMlQDac0ziEy9zJQd9CXBRRDHw2jJo=
gotest.tools/v3 v3.3.0/go.mod h1:Mcr9QNxkg0uMvy/YElmo4SpXgJKWgQvYrT7Kw5RzJ1A=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View File

@ -1,51 +1,80 @@
/*Package assert provides assertions for comparing expected values to actual
values. When an assertion fails a helpful error message is printed.
Assert and Check
Assert() and Check() both accept a Comparison, and fail the test when the
comparison fails. The one difference is that Assert() will end the test execution
immediately (using t.FailNow()) whereas Check() will fail the test (using t.Fail()),
return the value of the comparison, then proceed with the rest of the test case.
values in tests. When an assertion fails a helpful error message is printed.
Example usage
The example below shows assert used with some common types.
All the assertions in this package use testing.T.Helper to mark themselves as
test helpers. This allows the testing package to print the filename and line
number of the file function that failed.
assert.NilError(t, err)
// filename_test.go:212: assertion failed: error is not nil: file not found
import (
"testing"
If any assertion is called from a helper function, make sure to call t.Helper
from the helper function so that the filename and line number remain correct.
"gotest.tools/assert"
is "gotest.tools/assert/cmp"
)
The examples below show assert used with some common types and the failure
messages it produces. The filename and line number portion of the failure
message is omitted from these examples for brevity.
func TestEverything(t *testing.T) {
// booleans
assert.Assert(t, ok)
// assertion failed: ok is false
assert.Assert(t, !missing)
// assertion failed: missing is true
// primitives
assert.Equal(t, count, 1)
// assertion failed: 0 (count int) != 1 (int)
assert.Equal(t, msg, "the message")
assert.Assert(t, total != 10) // NotEqual
// assertion failed: my message (msg string) != the message (string)
assert.Assert(t, total != 10) // use Assert for NotEqual
// assertion failed: total is 10
assert.Assert(t, count > 20, "count=%v", count)
// assertion failed: count is <= 20: count=1
// errors
assert.NilError(t, closer.Close())
// assertion failed: error is not nil: close /file: errno 11
assert.Error(t, err, "the exact error message")
// assertion failed: expected error "the exact error message", got "oops"
assert.ErrorContains(t, err, "includes this")
assert.ErrorType(t, err, os.IsNotExist)
// assertion failed: expected error to contain "includes this", got "oops"
assert.ErrorIs(t, err, os.ErrNotExist)
// assertion failed: error is "oops", not "file does not exist" (os.ErrNotExist)
// complex types
assert.DeepEqual(t, result, myStruct{Name: "title"})
// assertion failed: ... (diff of the two structs)
assert.Assert(t, is.Len(items, 3))
assert.Assert(t, len(sequence) != 0) // NotEmpty
// assertion failed: expected [] (length 0) to have length 3
assert.Assert(t, len(sequence) != 0) // use Assert for NotEmpty
// assertion failed: len(sequence) is 0
assert.Assert(t, is.Contains(mapping, "key"))
// assertion failed: map[other:1] does not contain key
// pointers and interface
assert.Assert(t, is.Nil(ref))
assert.Assert(t, ref != nil) // NotNil
}
assert.Assert(t, ref == nil)
// assertion failed: ref is not nil
assert.Assert(t, ref != nil) // use Assert for NotNil
// assertion failed: ref is nil
Assert and Check
Assert and Check are very similar, they both accept a Comparison, and fail
the test when the comparison fails. The one difference is that Assert uses
testing.T.FailNow to fail the test, which will end the test execution immediately.
Check uses testing.T.Fail to fail the test, which allows it to return the
result of the comparison, then proceed with the rest of the test case.
Like testing.T.FailNow, Assert must be called from the goroutine running the test,
not from other goroutines created during the test. Check is safe to use from any
goroutine.
Comparisons
@ -70,7 +99,8 @@ import (
"gotest.tools/v3/internal/assert"
)
// BoolOrComparison can be a bool, or cmp.Comparison. See Assert() for usage.
// BoolOrComparison can be a bool, cmp.Comparison, or error. See Assert for
// details about how this type is used.
type BoolOrComparison interface{}
// TestingT is the subset of testing.T used by the assert package.
@ -88,16 +118,28 @@ type helperT interface {
// failed, a failure message is logged, and execution is stopped immediately.
//
// The comparison argument may be one of three types:
//
// bool
// True is success. False is a failure.
// The failure message will contain the literal source code of the expression.
// True is success. False is a failure. The failure message will contain
// the literal source code of the expression.
//
// cmp.Comparison
// Uses cmp.Result.Success() to check for success of failure.
// Uses cmp.Result.Success() to check for success or failure.
// The comparison is responsible for producing a helpful failure message.
// http://pkg.go.dev/gotest.tools/v3/assert/cmp provides many common comparisons.
//
// error
// A nil value is considered success.
// A non-nil error is a failure, err.Error() is used as the failure message.
// A nil value is considered success, and a non-nil error is a failure.
// The return value of error.Error is used as the failure message.
//
//
// Extra details can be added to the failure message using msgAndArgs. msgAndArgs
// may be either a single string, or a format string and args that will be
// passed to fmt.Sprintf.
//
// Assert uses t.FailNow to fail the test. Like t.FailNow, Assert must be called
// from the goroutine running the test function, not from other
// goroutines created during the test. Use Check from other goroutines.
func Assert(t TestingT, comparison BoolOrComparison, msgAndArgs ...interface{}) {
if ht, ok := t.(helperT); ok {
ht.Helper()
@ -108,8 +150,8 @@ func Assert(t TestingT, comparison BoolOrComparison, msgAndArgs ...interface{})
}
// Check performs a comparison. If the comparison fails the test is marked as
// failed, a failure message is logged, and Check returns false. Otherwise returns
// true.
// failed, a failure message is printed, and Check returns false. If the comparison
// is successful Check returns true. Check may be called from any goroutine.
//
// See Assert for details about the comparison arg and failure messages.
func Check(t TestingT, comparison BoolOrComparison, msgAndArgs ...interface{}) bool {
@ -123,8 +165,12 @@ func Check(t TestingT, comparison BoolOrComparison, msgAndArgs ...interface{}) b
return true
}
// NilError fails the test immediately if err is not nil.
// This is equivalent to Assert(t, err)
// NilError fails the test immediately if err is not nil, and includes err.Error
// in the failure message.
//
// NilError uses t.FailNow to fail the test. Like t.FailNow, NilError must be
// called from the goroutine running the test function, not from other
// goroutines created during the test. Use Check from other goroutines.
func NilError(t TestingT, err error, msgAndArgs ...interface{}) {
if ht, ok := t.(helperT); ok {
ht.Helper()
@ -137,15 +183,22 @@ func NilError(t TestingT, err error, msgAndArgs ...interface{}) {
// Equal uses the == operator to assert two values are equal and fails the test
// if they are not equal.
//
// If the comparison fails Equal will use the variable names for x and y as part
// of the failure message to identify the actual and expected values.
// If the comparison fails Equal will use the variable names and types of
// x and y as part of the failure message to identify the actual and expected
// values.
//
// assert.Equal(t, actual, expected)
// // main_test.go:41: assertion failed: 1 (actual int) != 21 (expected int32)
//
// If either x or y are a multi-line string the failure message will include a
// unified diff of the two values. If the values only differ by whitespace
// the unified diff will be augmented by replacing whitespace characters with
// visible characters to identify the whitespace difference.
//
// This is equivalent to Assert(t, cmp.Equal(x, y)).
// Equal uses t.FailNow to fail the test. Like t.FailNow, Equal must be
// called from the goroutine running the test function, not from other
// goroutines created during the test. Use Check with cmp.Equal from other
// goroutines.
func Equal(t TestingT, x, y interface{}, msgAndArgs ...interface{}) {
if ht, ok := t.(helperT); ok {
ht.Helper()
@ -161,7 +214,10 @@ func Equal(t TestingT, x, y interface{}, msgAndArgs ...interface{}) {
// Package http://pkg.go.dev/gotest.tools/v3/assert/opt provides some additional
// commonly used Options.
//
// This is equivalent to Assert(t, cmp.DeepEqual(x, y)).
// DeepEqual uses t.FailNow to fail the test. Like t.FailNow, DeepEqual must be
// called from the goroutine running the test function, not from other
// goroutines created during the test. Use Check with cmp.DeepEqual from other
// goroutines.
func DeepEqual(t TestingT, x, y interface{}, opts ...gocmp.Option) {
if ht, ok := t.(helperT); ok {
ht.Helper()
@ -171,21 +227,33 @@ func DeepEqual(t TestingT, x, y interface{}, opts ...gocmp.Option) {
}
}
// Error fails the test if err is nil, or the error message is not the expected
// message.
// Equivalent to Assert(t, cmp.Error(err, message)).
func Error(t TestingT, err error, message string, msgAndArgs ...interface{}) {
// Error fails the test if err is nil, or if err.Error is not equal to expected.
// Both err.Error and expected will be included in the failure message.
// Error performs an exact match of the error text. Use ErrorContains if only
// part of the error message is relevant. Use ErrorType or ErrorIs to compare
// errors by type.
//
// Error uses t.FailNow to fail the test. Like t.FailNow, Error must be
// called from the goroutine running the test function, not from other
// goroutines created during the test. Use Check with cmp.Error from other
// goroutines.
func Error(t TestingT, err error, expected string, msgAndArgs ...interface{}) {
if ht, ok := t.(helperT); ok {
ht.Helper()
}
if !assert.Eval(t, assert.ArgsAfterT, cmp.Error(err, message), msgAndArgs...) {
if !assert.Eval(t, assert.ArgsAfterT, cmp.Error(err, expected), msgAndArgs...) {
t.FailNow()
}
}
// ErrorContains fails the test if err is nil, or the error message does not
// contain the expected substring.
// Equivalent to Assert(t, cmp.ErrorContains(err, substring)).
// ErrorContains fails the test if err is nil, or if err.Error does not
// contain the expected substring. Both err.Error and the expected substring
// will be included in the failure message.
//
// ErrorContains uses t.FailNow to fail the test. Like t.FailNow, ErrorContains
// must be called from the goroutine running the test function, not from other
// goroutines created during the test. Use Check with cmp.ErrorContains from other
// goroutines.
func ErrorContains(t TestingT, err error, substring string, msgAndArgs ...interface{}) {
if ht, ok := t.(helperT); ok {
ht.Helper()
@ -196,19 +264,29 @@ func ErrorContains(t TestingT, err error, substring string, msgAndArgs ...interf
}
// ErrorType fails the test if err is nil, or err is not the expected type.
// Equivalent to Assert(t, cmp.ErrorType(err, expected)).
// Most new code should use ErrorIs instead. ErrorType may be deprecated in the
// future.
//
// Expected can be one of:
//
// func(error) bool
// Function should return true if the error is the expected type.
// type struct{}, type &struct{}
// A struct or a pointer to a struct.
// Fails if the error is not of the same type as expected.
// type &interface{}
// A pointer to an interface type.
// Fails if err does not implement the interface.
// The function should return true if the error is the expected type.
//
// struct{} or *struct{}
// A struct or a pointer to a struct. The assertion fails if the error is
// not of the same type.
//
// *interface{}
// A pointer to an interface type. The assertion fails if err does not
// implement the interface.
//
// reflect.Type
// Fails if err does not implement the reflect.Type
// The assertion fails if err does not implement the reflect.Type.
//
// ErrorType uses t.FailNow to fail the test. Like t.FailNow, ErrorType
// must be called from the goroutine running the test function, not from other
// goroutines created during the test. Use Check with cmp.ErrorType from other
// goroutines.
func ErrorType(t TestingT, err error, expected interface{}, msgAndArgs ...interface{}) {
if ht, ok := t.(helperT); ok {
ht.Helper()
@ -217,3 +295,20 @@ func ErrorType(t TestingT, err error, expected interface{}, msgAndArgs ...interf
t.FailNow()
}
}
// ErrorIs fails the test if err is nil, or the error does not match expected
// when compared using errors.Is. See https://golang.org/pkg/errors/#Is for
// accepted arguments.
//
// ErrorIs uses t.FailNow to fail the test. Like t.FailNow, ErrorIs
// must be called from the goroutine running the test function, not from other
// goroutines created during the test. Use Check with cmp.ErrorIs from other
// goroutines.
func ErrorIs(t TestingT, err error, expected error, msgAndArgs ...interface{}) {
if ht, ok := t.(helperT); ok {
ht.Helper()
}
if !assert.Eval(t, assert.ArgsAfterT, cmp.ErrorIs(err, expected), msgAndArgs...) {
t.FailNow()
}
}

View File

@ -1,20 +0,0 @@
// +build go1.13
package assert
import (
"gotest.tools/v3/assert/cmp"
"gotest.tools/v3/internal/assert"
)
// ErrorIs fails the test if err is nil, or the error does not match expected
// when compared using errors.Is. See https://golang.org/pkg/errors/#Is for
// accepted argument values.
func ErrorIs(t TestingT, err error, expected error, msgAndArgs ...interface{}) {
if ht, ok := t.(helperT); ok {
ht.Helper()
}
if !assert.Eval(t, assert.ArgsAfterT, cmp.ErrorIs(err, expected), msgAndArgs...) {
t.FailNow()
}
}

View File

@ -2,6 +2,7 @@
package cmp // import "gotest.tools/v3/assert/cmp"
import (
"errors"
"fmt"
"reflect"
"regexp"
@ -34,7 +35,7 @@ func DeepEqual(x, y interface{}, opts ...cmp.Option) Comparison {
if diff == "" {
return ResultSuccess
}
return multiLineDiffResult(diff)
return multiLineDiffResult(diff, x, y)
}
}
@ -101,7 +102,7 @@ func Equal(x, y interface{}) Comparison {
return ResultSuccess
case isMultiLineStringCompare(x, y):
diff := format.UnifiedDiff(format.DiffConfig{A: x.(string), B: y.(string)})
return multiLineDiffResult(diff)
return multiLineDiffResult(diff, x, y)
}
return ResultFailureTemplate(`
{{- printf "%v" .Data.x}} (
@ -127,12 +128,12 @@ func isMultiLineStringCompare(x, y interface{}) bool {
return strings.Contains(strX, "\n") || strings.Contains(strY, "\n")
}
func multiLineDiffResult(diff string) Result {
func multiLineDiffResult(diff string, x, y interface{}) Result {
return ResultFailureTemplate(`
--- {{ with callArg 0 }}{{ formatNode . }}{{else}}{{end}}
+++ {{ with callArg 1 }}{{ formatNode . }}{{else}}{{end}}
{{ .Data.diff }}`,
map[string]interface{}{"diff": diff})
map[string]interface{}{"diff": diff, "x": x, "y": y})
}
// Len succeeds if the sequence has the expected length.
@ -165,7 +166,7 @@ func Contains(collection interface{}, item interface{}) Comparison {
return func() Result {
colValue := reflect.ValueOf(collection)
if !colValue.IsValid() {
return ResultFailure(fmt.Sprintf("nil does not contain items"))
return ResultFailure("nil does not contain items")
}
msg := fmt.Sprintf("%v does not contain %v", collection, item)
@ -247,6 +248,7 @@ type causer interface {
}
func formatErrorMessage(err error) string {
// nolint: errorlint // unwrapping is not appropriate here
if _, ok := err.(causer); ok {
return fmt.Sprintf("%q\n%+v", err, err)
}
@ -307,7 +309,7 @@ func ErrorType(err error, expected interface{}) Comparison {
}
return cmpErrorTypeEqualType(err, expectedType)
case nil:
return ResultFailure(fmt.Sprintf("invalid type for expected: nil"))
return ResultFailure("invalid type for expected: nil")
}
expectedType := reflect.TypeOf(expected)
@ -363,3 +365,30 @@ func isPtrToInterface(typ reflect.Type) bool {
func isPtrToStruct(typ reflect.Type) bool {
return typ.Kind() == reflect.Ptr && typ.Elem().Kind() == reflect.Struct
}
var (
stdlibErrorNewType = reflect.TypeOf(errors.New(""))
stdlibFmtErrorType = reflect.TypeOf(fmt.Errorf("%w", fmt.Errorf("")))
)
// ErrorIs succeeds if errors.Is(actual, expected) returns true. See
// https://golang.org/pkg/errors/#Is for accepted argument values.
func ErrorIs(actual error, expected error) Comparison {
return func() Result {
if errors.Is(actual, expected) {
return ResultSuccess
}
// The type of stdlib errors is excluded because the type is not relevant
// in those cases. The type is only important when it is a user defined
// custom error type.
return ResultFailureTemplate(`error is
{{- if not .Data.a }} nil,{{ else }}
{{- printf " \"%v\"" .Data.a }}
{{- if notStdlibErrorType .Data.a }} ({{ printf "%T" .Data.a }}){{ end }},
{{- end }} not {{ printf "\"%v\"" .Data.x }} (
{{- with callArg 1 }}{{ formatNode . }}{{ end }}
{{- if notStdlibErrorType .Data.x }}{{ printf " %T" .Data.x }}{{ end }})`,
map[string]interface{}{"a": actual, "x": expected})
}
}

View File

@ -1,29 +0,0 @@
// +build go1.13
package cmp
import (
"errors"
)
// ErrorIs succeeds if errors.Is(actual, expected) returns true. See
// https://golang.org/pkg/errors/#Is for accepted argument values.
func ErrorIs(actual error, expected error) Comparison {
return func() Result {
if errors.Is(actual, expected) {
return ResultSuccess
}
return ResultFailureTemplate(`error is
{{- if not .Data.a }} nil,{{ else }}
{{- printf " \"%v\"" .Data.a}} (
{{- with callArg 0 }}{{ formatNode . }} {{end -}}
{{- printf "%T" .Data.a -}}
),
{{- end }} not {{ printf "\"%v\"" .Data.x}} (
{{- with callArg 1 }}{{ formatNode . }} {{end -}}
{{- printf "%T" .Data.x -}}
)`,
map[string]interface{}{"a": actual, "x": expected})
}
}

View File

@ -4,6 +4,7 @@ import (
"bytes"
"fmt"
"go/ast"
"reflect"
"text/template"
"gotest.tools/v3/internal/source"
@ -68,6 +69,11 @@ func (r templatedResult) FailureMessage(args []ast.Expr) string {
return msg
}
func (r templatedResult) UpdatedExpected(stackIndex int) error {
// TODO: would be nice to have structured data instead of a map
return source.UpdateExpectedValue(stackIndex+1, r.data["x"], r.data["y"])
}
// ResultFailureTemplate returns a Result with a template string and data which
// can be used to format a failure message. The template may access data from .Data,
// the comparison args with the callArg function, and the formatNode function may
@ -85,6 +91,11 @@ func renderMessage(result templatedResult, args []ast.Expr) (string, error) {
}
return args[index]
},
// TODO: any way to include this from ErrorIS instead of here?
"notStdlibErrorType": func(typ interface{}) bool {
r := reflect.TypeOf(typ)
return r != stdlibFmtErrorType && r != stdlibErrorNewType
},
})
var err error
tmpl, err = tmpl.Parse(result.template)

View File

@ -21,6 +21,8 @@ type helperT interface {
//
// When used with Go 1.14+ the unpatch function will be called automatically
// when the test ends, unless the TEST_NOCLEANUP env var is set to true.
//
// Deprecated: use t.SetEnv
func Patch(t assert.TestingT, key, value string) func() {
if ht, ok := t.(helperT); ok {
ht.Helper()

View File

@ -1,12 +1,12 @@
package fs
import (
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"github.com/pkg/errors"
"gotest.tools/v3/assert"
)
@ -75,7 +75,7 @@ func manifestFromDir(path string) (Manifest, error) {
case err != nil:
return Manifest{}, err
case !info.IsDir():
return Manifest{}, errors.Errorf("path %s must be a directory", path)
return Manifest{}, fmt.Errorf("path %s must be a directory", path)
}
directory, err := newDirectory(path, info)

View File

@ -2,6 +2,7 @@ package fs
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"os"
@ -9,7 +10,6 @@ import (
"strings"
"time"
"github.com/pkg/errors"
"gotest.tools/v3/assert"
)
@ -137,7 +137,7 @@ func WithFiles(files map[string]string) PathOp {
func FromDir(source string) PathOp {
return func(path Path) error {
if _, ok := path.(manifestDirectory); ok {
return errors.New("use manifest.FromDir")
return fmt.Errorf("use manifest.FromDir")
}
return copyDirectory(source, path.Path())
}
@ -257,7 +257,7 @@ func WithSymlink(path, target string) PathOp {
func WithHardlink(path, target string) PathOp {
return func(root Path) error {
if _, ok := root.(manifestDirectory); ok {
return errors.New("WithHardlink not implemented for manifests")
return fmt.Errorf("WithHardlink not implemented for manifests")
}
return os.Link(filepath.Join(root.Path(), target), filepath.Join(root.Path(), path))
}
@ -268,7 +268,7 @@ func WithHardlink(path, target string) PathOp {
func WithTimestamps(atime, mtime time.Time) PathOp {
return func(root Path) error {
if _, ok := root.(manifestDirectory); ok {
return errors.New("WithTimestamp not implemented for manifests")
return fmt.Errorf("WithTimestamp not implemented for manifests")
}
return os.Chtimes(root.Path(), atime, mtime)
}

View File

@ -72,7 +72,6 @@ func removeCarriageReturn(in []byte) []byte {
return bytes.Replace(in, []byte("\r\n"), []byte("\n"), -1)
}
// nolint: gocyclo
func eqFile(x, y *file) []problem {
p := eqResource(x.resource, y.resource)
@ -159,7 +158,7 @@ func eqSymlink(x, y *symlink) []problem {
func eqDirectory(path string, x, y *directory) []failure {
p := eqResource(x.resource, y.resource)
var f []failure // nolint: prealloc
var f []failure
matchedFiles := make(map[string]bool)
for _, name := range sortedKeys(x.items) {

View File

@ -2,7 +2,7 @@
Golden files are files in the ./testdata/ subdirectory of the package under test.
Golden files can be automatically updated to match new values by running
`go test pkgname -test.update-golden`. To ensure the update is correct
`go test pkgname -update`. To ensure the update is correct
compare the diff of the old expected value to the new expected value.
*/
package golden // import "gotest.tools/v3/golden"
@ -18,9 +18,12 @@ import (
"gotest.tools/v3/assert"
"gotest.tools/v3/assert/cmp"
"gotest.tools/v3/internal/format"
"gotest.tools/v3/internal/source"
)
var flagUpdate = flag.Bool("test.update-golden", false, "update golden file")
func init() {
flag.BoolVar(&source.Update, "test.update-golden", false, "deprecated flag")
}
type helperT interface {
Helper()
@ -28,7 +31,7 @@ type helperT interface {
// NormalizeCRLFToLF enables end-of-line normalization for actual values passed
// to Assert and String, as well as the values saved to golden files with
// -test.update-golden.
// -update.
//
// Defaults to true. If you use the core.autocrlf=true git setting on windows
// you will need to set this to false.
@ -39,9 +42,9 @@ type helperT interface {
// The default value may change in a future major release.
var NormalizeCRLFToLF = os.Getenv("GOTESTTOOLS_GOLDEN_NormalizeCRLFToLF") != "false"
// FlagUpdate returns true when the -test.update-golden flag has been set.
// FlagUpdate returns true when the -update flag has been set.
func FlagUpdate() bool {
return *flagUpdate
return source.Update
}
// Open opens the file in ./testdata
@ -81,7 +84,7 @@ func removeCarriageReturn(in []byte) []byte {
// Assert compares actual to the expected value in the golden file.
//
// Running `go test pkgname -test.update-golden` will write the value of actual
// Running `go test pkgname -update` will write the value of actual
// to the golden file.
//
// This is equivalent to assert.Assert(t, String(actual, filename))
@ -95,7 +98,7 @@ func Assert(t assert.TestingT, actual string, filename string, msgAndArgs ...int
// String compares actual to the contents of filename and returns success
// if the strings are equal.
//
// Running `go test pkgname -test.update-golden` will write the value of actual
// Running `go test pkgname -update` will write the value of actual
// to the golden file.
//
// Any \r\n substrings in actual are converted to a single \n character
@ -122,13 +125,13 @@ func String(actual string, filename string) cmp.Comparison {
func failurePostamble(filename string) string {
return fmt.Sprintf(`
You can run 'go test . -test.update-golden' to automatically update %s to the new expected value.'
You can run 'go test . -update' to automatically update %s to the new expected value.'
`, Path(filename))
}
// AssertBytes compares actual to the expected value in the golden.
//
// Running `go test pkgname -test.update-golden` will write the value of actual
// Running `go test pkgname -update` will write the value of actual
// to the golden file.
//
// This is equivalent to assert.Assert(t, Bytes(actual, filename))
@ -147,7 +150,7 @@ func AssertBytes(
// Bytes compares actual to the contents of filename and returns success
// if the bytes are equal.
//
// Running `go test pkgname -test.update-golden` will write the value of actual
// Running `go test pkgname -update` will write the value of actual
// to the golden file.
func Bytes(actual []byte, filename string) cmp.Comparison {
return func() cmp.Result {
@ -175,7 +178,7 @@ func compare(actual []byte, filename string) (cmp.Result, []byte) {
}
func update(filename string, actual []byte) error {
if !*flagUpdate {
if !source.Update {
return nil
}
if dir := filepath.Dir(Path(filename)); dir != "." {

View File

@ -77,7 +77,6 @@ func (r *Result) Compare(exp Expected) error {
return r.match(exp)
}
// nolint: gocyclo
func (r *Result) match(exp Expected) error {
errors := []string{}
add := func(format string, args ...interface{}) {

View File

@ -1,32 +1,24 @@
package icmd
import (
"syscall"
"errors"
"github.com/pkg/errors"
exec "golang.org/x/sys/execabs"
)
// getExitCode returns the ExitStatus of a process from the error returned by
// exec.Run(). If the exit status could not be parsed an error is returned.
func getExitCode(err error) (int, error) {
if exiterr, ok := err.(*exec.ExitError); ok {
if procExit, ok := exiterr.Sys().(syscall.WaitStatus); ok {
return procExit.ExitStatus(), nil
}
}
return 0, errors.Wrap(err, "failed to get exit code")
}
func processExitCode(err error) (exitCode int) {
func processExitCode(err error) int {
if err == nil {
return 0
}
exitCode, exiterr := getExitCode(err)
if exiterr != nil {
// TODO: Fix this so we check the error's text.
// we've failed to retrieve exit code, so we set it to 127
return 127
var exitErr *exec.ExitError
if errors.As(err, &exitErr) {
if exitErr.ProcessState == nil {
return 0
}
return exitCode
if code := exitErr.ProcessState.ExitCode(); code != -1 {
return code
}
}
return 127
}

View File

@ -23,7 +23,6 @@ type helperT interface {
const failureMessage = "assertion failed: "
// Eval the comparison and print a failure messages if the comparison has failed.
// nolint: gocyclo
func Eval(
t LogT,
argSelector argSelector,
@ -115,7 +114,7 @@ func failureMsgFromError(err error) string {
}
func boolFailureMessage(expr ast.Expr) (string, error) {
if binaryExpr, ok := expr.(*ast.BinaryExpr); ok && binaryExpr.Op == token.NEQ {
if binaryExpr, ok := expr.(*ast.BinaryExpr); ok {
x, err := source.FormatNode(binaryExpr.X)
if err != nil {
return "", err
@ -124,7 +123,21 @@ func boolFailureMessage(expr ast.Expr) (string, error) {
if err != nil {
return "", err
}
switch binaryExpr.Op {
case token.NEQ:
return x + " is " + y, nil
case token.EQL:
return x + " is not " + y, nil
case token.GTR:
return x + " is <= " + y, nil
case token.LSS:
return x + " is >= " + y, nil
case token.GEQ:
return x + " is less than " + y, nil
case token.LEQ:
return x + " is greater than " + y, nil
}
}
if unaryExpr, ok := expr.(*ast.UnaryExpr); ok && unaryExpr.Op == token.NOT {
@ -135,6 +148,10 @@ func boolFailureMessage(expr ast.Expr) (string, error) {
return x + " is true", nil
}
if ident, ok := expr.(*ast.Ident); ok {
return ident.Name + " is false", nil
}
formatted, err := source.FormatNode(expr)
if err != nil {
return "", err

View File

@ -1,6 +1,7 @@
package assert
import (
"errors"
"fmt"
"go/ast"
@ -25,6 +26,22 @@ func RunComparison(
return true
}
if source.Update {
if updater, ok := result.(updateExpected); ok {
const stackIndex = 3 // Assert/Check, assert, RunComparison
err := updater.UpdatedExpected(stackIndex)
switch {
case err == nil:
return true
case errors.Is(err, source.ErrNotFound):
// do nothing, fallthrough to regular failure message
default:
t.Log("failed to update source", err)
return false
}
}
}
var message string
switch typed := result.(type) {
case resultWithComparisonArgs:
@ -52,6 +69,10 @@ type resultBasic interface {
FailureMessage() string
}
type updateExpected interface {
UpdatedExpected(stackIndex int) error
}
// filterPrintableExpr filters the ast.Expr slice to only include Expr that are
// easy to read when printed and contain relevant information to an assertion.
//

View File

@ -1,10 +1,9 @@
package source
import (
"fmt"
"go/ast"
"go/token"
"github.com/pkg/errors"
)
func scanToDeferLine(fileset *token.FileSet, node ast.Node, lineNum int) ast.Node {
@ -29,11 +28,11 @@ func guessDefer(node ast.Node) (ast.Node, error) {
defers := collectDefers(node)
switch len(defers) {
case 0:
return nil, errors.New("failed to expression in defer")
return nil, fmt.Errorf("failed to find expression in defer")
case 1:
return defers[0].Call, nil
default:
return nil, errors.Errorf(
return nil, fmt.Errorf(
"ambiguous call expression: multiple (%d) defers in call block",
len(defers))
}

View File

@ -2,6 +2,7 @@ package source // import "gotest.tools/v3/internal/source"
import (
"bytes"
"errors"
"fmt"
"go/ast"
"go/format"
@ -9,14 +10,8 @@ import (
"go/token"
"os"
"runtime"
"strconv"
"strings"
"github.com/pkg/errors"
)
const baseStackIndex = 1
// FormattedCallExprArg returns the argument from an ast.CallExpr at the
// index in the call stack. The argument is formatted using FormatNode.
func FormattedCallExprArg(stackIndex int, argPos int) (string, error) {
@ -33,28 +28,26 @@ func FormattedCallExprArg(stackIndex int, argPos int) (string, error) {
// CallExprArgs returns the ast.Expr slice for the args of an ast.CallExpr at
// the index in the call stack.
func CallExprArgs(stackIndex int) ([]ast.Expr, error) {
_, filename, lineNum, ok := runtime.Caller(baseStackIndex + stackIndex)
_, filename, line, ok := runtime.Caller(stackIndex + 1)
if !ok {
return nil, errors.New("failed to get call stack")
}
debug("call stack position: %s:%d", filename, lineNum)
debug("call stack position: %s:%d", filename, line)
node, err := getNodeAtLine(filename, lineNum)
if err != nil {
return nil, err
}
debug("found node: %s", debugFormatNode{node})
return getCallExprArgs(node)
}
func getNodeAtLine(filename string, lineNum int) (ast.Node, error) {
fileset := token.NewFileSet()
astFile, err := parser.ParseFile(fileset, filename, nil, parser.AllErrors)
if err != nil {
return nil, errors.Wrapf(err, "failed to parse source file: %s", filename)
return nil, fmt.Errorf("failed to parse source file %s: %w", filename, err)
}
expr, err := getCallExprArgs(fileset, astFile, line)
if err != nil {
return nil, fmt.Errorf("call from %s:%d: %w", filename, line, err)
}
return expr, nil
}
func getNodeAtLine(fileset *token.FileSet, astFile ast.Node, lineNum int) (ast.Node, error) {
if node := scanToLine(fileset, astFile, lineNum); node != nil {
return node, nil
}
@ -64,8 +57,7 @@ func getNodeAtLine(filename string, lineNum int) (ast.Node, error) {
return node, err
}
}
return nil, errors.Errorf(
"failed to find an expression on line %d in %s", lineNum, filename)
return nil, nil
}
func scanToLine(fileset *token.FileSet, node ast.Node, lineNum int) ast.Node {
@ -74,7 +66,7 @@ func scanToLine(fileset *token.FileSet, node ast.Node, lineNum int) ast.Node {
switch {
case node == nil || matchedNode != nil:
return false
case nodePosition(fileset, node).Line == lineNum:
case fileset.Position(node.Pos()).Line == lineNum:
matchedNode = node
return false
}
@ -83,46 +75,17 @@ func scanToLine(fileset *token.FileSet, node ast.Node, lineNum int) ast.Node {
return matchedNode
}
// In golang 1.9 the line number changed from being the line where the statement
// ended to the line where the statement began.
func nodePosition(fileset *token.FileSet, node ast.Node) token.Position {
if goVersionBefore19 {
return fileset.Position(node.End())
func getCallExprArgs(fileset *token.FileSet, astFile ast.Node, line int) ([]ast.Expr, error) {
node, err := getNodeAtLine(fileset, astFile, line)
switch {
case err != nil:
return nil, err
case node == nil:
return nil, fmt.Errorf("failed to find an expression")
}
return fileset.Position(node.Pos())
}
// GoVersionLessThan returns true if runtime.Version() is semantically less than
// version major.minor. Returns false if a release version can not be parsed from
// runtime.Version().
func GoVersionLessThan(major, minor int64) bool {
version := runtime.Version()
// not a release version
if !strings.HasPrefix(version, "go") {
return false
}
version = strings.TrimPrefix(version, "go")
parts := strings.Split(version, ".")
if len(parts) < 2 {
return false
}
rMajor, err := strconv.ParseInt(parts[0], 10, 32)
if err != nil {
return false
}
if rMajor != major {
return rMajor < major
}
rMinor, err := strconv.ParseInt(parts[1], 10, 32)
if err != nil {
return false
}
return rMinor < minor
}
debug("found node: %s", debugFormatNode{node})
var goVersionBefore19 = GoVersionLessThan(1, 9)
func getCallExprArgs(node ast.Node) ([]ast.Expr, error) {
visitor := &callExprVisitor{}
ast.Walk(visitor, node)
if visitor.expr == nil {
@ -173,6 +136,9 @@ type debugFormatNode struct {
}
func (n debugFormatNode) String() string {
if n.Node == nil {
return "none"
}
out, err := FormatNode(n.Node)
if err != nil {
return fmt.Sprintf("failed to format %s: %s", n.Node, err)

View File

@ -0,0 +1,138 @@
package source
import (
"bytes"
"errors"
"flag"
"fmt"
"go/ast"
"go/format"
"go/parser"
"go/token"
"os"
"runtime"
"strings"
)
// Update is set by the -update flag. It indicates the user running the tests
// would like to update any golden values.
var Update bool
func init() {
flag.BoolVar(&Update, "update", false, "update golden values")
}
// ErrNotFound indicates that UpdateExpectedValue failed to find the
// variable to update, likely because it is not a package level variable.
var ErrNotFound = fmt.Errorf("failed to find variable for update of golden value")
// UpdateExpectedValue looks for a package-level variable with a name that
// starts with expected in the arguments to the caller. If the variable is
// found, the value of the variable will be updated to value of the other
// argument to the caller.
func UpdateExpectedValue(stackIndex int, x, y interface{}) error {
_, filename, line, ok := runtime.Caller(stackIndex + 1)
if !ok {
return errors.New("failed to get call stack")
}
debug("call stack position: %s:%d", filename, line)
fileset := token.NewFileSet()
astFile, err := parser.ParseFile(fileset, filename, nil, parser.AllErrors|parser.ParseComments)
if err != nil {
return fmt.Errorf("failed to parse source file %s: %w", filename, err)
}
expr, err := getCallExprArgs(fileset, astFile, line)
if err != nil {
return fmt.Errorf("call from %s:%d: %w", filename, line, err)
}
if len(expr) < 3 {
debug("not enough arguments %d: %v",
len(expr), debugFormatNode{Node: &ast.CallExpr{Args: expr}})
return ErrNotFound
}
argIndex, varName := getVarNameForExpectedValueArg(expr)
if argIndex < 0 || varName == "" {
debug("no arguments started with the word 'expected': %v",
debugFormatNode{Node: &ast.CallExpr{Args: expr}})
return ErrNotFound
}
value := x
if argIndex == 1 {
value = y
}
strValue, ok := value.(string)
if !ok {
debug("value must be type string, got %T", value)
return ErrNotFound
}
return UpdateVariable(filename, fileset, astFile, varName, strValue)
}
// UpdateVariable writes to filename the contents of astFile with the value of
// the variable updated to value.
func UpdateVariable(
filename string,
fileset *token.FileSet,
astFile *ast.File,
varName string,
value string,
) error {
obj := astFile.Scope.Objects[varName]
if obj == nil {
return ErrNotFound
}
if obj.Kind != ast.Con && obj.Kind != ast.Var {
debug("can only update var and const, found %v", obj.Kind)
return ErrNotFound
}
spec, ok := obj.Decl.(*ast.ValueSpec)
if !ok {
debug("can only update *ast.ValueSpec, found %T", obj.Decl)
return ErrNotFound
}
if len(spec.Names) != 1 {
debug("more than one name in ast.ValueSpec")
return ErrNotFound
}
spec.Values[0] = &ast.BasicLit{
Kind: token.STRING,
Value: "`" + value + "`",
}
var buf bytes.Buffer
if err := format.Node(&buf, fileset, astFile); err != nil {
return fmt.Errorf("failed to format file after update: %w", err)
}
fh, err := os.Create(filename)
if err != nil {
return fmt.Errorf("failed to open file %v: %w", filename, err)
}
if _, err = fh.Write(buf.Bytes()); err != nil {
return fmt.Errorf("failed to write file %v: %w", filename, err)
}
if err := fh.Sync(); err != nil {
return fmt.Errorf("failed to sync file %v: %w", filename, err)
}
return nil
}
func getVarNameForExpectedValueArg(expr []ast.Expr) (int, string) {
for i := 1; i < 3; i++ {
switch e := expr[i].(type) {
case *ast.Ident:
if strings.HasPrefix(strings.ToLower(e.Name), "expected") {
return i, e.Name
}
}
}
return -1, ""
}

View File

@ -0,0 +1,35 @@
package source
import (
"runtime"
"strconv"
"strings"
)
// GoVersionLessThan returns true if runtime.Version() is semantically less than
// version major.minor. Returns false if a release version can not be parsed from
// runtime.Version().
func GoVersionLessThan(major, minor int64) bool {
version := runtime.Version()
// not a release version
if !strings.HasPrefix(version, "go") {
return false
}
version = strings.TrimPrefix(version, "go")
parts := strings.Split(version, ".")
if len(parts) < 2 {
return false
}
rMajor, err := strconv.ParseInt(parts[0], 10, 32)
if err != nil {
return false
}
if rMajor != major {
return rMajor < major
}
rMinor, err := strconv.ParseInt(parts[1], 10, 32)
if err != nil {
return false
}
return rMinor < minor
}

4
vendor/modules.txt vendored
View File

@ -379,8 +379,8 @@ google.golang.org/protobuf/types/known/timestamppb
# gopkg.in/yaml.v2 v2.4.0
## explicit; go 1.15
gopkg.in/yaml.v2
# gotest.tools/v3 v3.1.0
## explicit; go 1.11
# gotest.tools/v3 v3.3.0
## explicit; go 1.13
gotest.tools/v3/assert
gotest.tools/v3/assert/cmp
gotest.tools/v3/env