2018-02-28 10:11:02 -05:00
|
|
|
/*Package cmp provides Comparisons for Assert and Check*/
|
2020-02-22 12:12:14 -05:00
|
|
|
package cmp // import "gotest.tools/v3/assert/cmp"
|
2018-02-28 10:11:02 -05:00
|
|
|
|
|
|
|
import (
|
2022-09-22 09:38:19 -04:00
|
|
|
"errors"
|
2018-02-28 10:11:02 -05:00
|
|
|
"fmt"
|
|
|
|
"reflect"
|
2018-11-14 06:28:16 -05:00
|
|
|
"regexp"
|
2018-02-28 10:11:02 -05:00
|
|
|
"strings"
|
|
|
|
|
|
|
|
"github.com/google/go-cmp/cmp"
|
2020-02-22 12:12:14 -05:00
|
|
|
"gotest.tools/v3/internal/format"
|
2018-02-28 10:11:02 -05:00
|
|
|
)
|
|
|
|
|
|
|
|
// Comparison is a function which compares values and returns ResultSuccess if
|
|
|
|
// the actual value matches the expected value. If the values do not match the
|
|
|
|
// Result will contain a message about why it failed.
|
|
|
|
type Comparison func() Result
|
|
|
|
|
2020-02-22 12:12:14 -05:00
|
|
|
// DeepEqual compares two values using google/go-cmp
|
|
|
|
// (https://godoc.org/github.com/google/go-cmp/cmp)
|
2018-02-28 10:11:02 -05:00
|
|
|
// and succeeds if the values are equal.
|
|
|
|
//
|
|
|
|
// The comparison can be customized using comparison Options.
|
2022-03-01 09:50:32 -05:00
|
|
|
// Package http://pkg.go.dev/gotest.tools/v3/assert/opt provides some additional
|
2018-06-08 12:23:38 -04:00
|
|
|
// commonly used Options.
|
2018-02-28 10:11:02 -05:00
|
|
|
func DeepEqual(x, y interface{}, opts ...cmp.Option) Comparison {
|
|
|
|
return func() (result Result) {
|
|
|
|
defer func() {
|
|
|
|
if panicmsg, handled := handleCmpPanic(recover()); handled {
|
|
|
|
result = ResultFailure(panicmsg)
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
diff := cmp.Diff(x, y, opts...)
|
2018-06-08 12:23:38 -04:00
|
|
|
if diff == "" {
|
|
|
|
return ResultSuccess
|
|
|
|
}
|
2022-09-22 09:38:19 -04:00
|
|
|
return multiLineDiffResult(diff, x, y)
|
2018-02-28 10:11:02 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func handleCmpPanic(r interface{}) (string, bool) {
|
|
|
|
if r == nil {
|
|
|
|
return "", false
|
|
|
|
}
|
|
|
|
panicmsg, ok := r.(string)
|
|
|
|
if !ok {
|
|
|
|
panic(r)
|
|
|
|
}
|
|
|
|
switch {
|
|
|
|
case strings.HasPrefix(panicmsg, "cannot handle unexported field"):
|
|
|
|
return panicmsg, true
|
|
|
|
}
|
|
|
|
panic(r)
|
|
|
|
}
|
|
|
|
|
|
|
|
func toResult(success bool, msg string) Result {
|
|
|
|
if success {
|
|
|
|
return ResultSuccess
|
|
|
|
}
|
|
|
|
return ResultFailure(msg)
|
|
|
|
}
|
|
|
|
|
2018-11-14 06:28:16 -05:00
|
|
|
// RegexOrPattern may be either a *regexp.Regexp or a string that is a valid
|
|
|
|
// regexp pattern.
|
|
|
|
type RegexOrPattern interface{}
|
|
|
|
|
|
|
|
// Regexp succeeds if value v matches regular expression re.
|
|
|
|
//
|
|
|
|
// Example:
|
2022-11-05 18:25:29 -04:00
|
|
|
//
|
|
|
|
// assert.Assert(t, cmp.Regexp("^[0-9a-f]{32}$", str))
|
|
|
|
// r := regexp.MustCompile("^[0-9a-f]{32}$")
|
|
|
|
// assert.Assert(t, cmp.Regexp(r, str))
|
2018-11-14 06:28:16 -05:00
|
|
|
func Regexp(re RegexOrPattern, v string) Comparison {
|
|
|
|
match := func(re *regexp.Regexp) Result {
|
|
|
|
return toResult(
|
|
|
|
re.MatchString(v),
|
|
|
|
fmt.Sprintf("value %q does not match regexp %q", v, re.String()))
|
|
|
|
}
|
|
|
|
|
|
|
|
return func() Result {
|
|
|
|
switch regex := re.(type) {
|
|
|
|
case *regexp.Regexp:
|
|
|
|
return match(regex)
|
|
|
|
case string:
|
|
|
|
re, err := regexp.Compile(regex)
|
|
|
|
if err != nil {
|
|
|
|
return ResultFailure(err.Error())
|
|
|
|
}
|
|
|
|
return match(re)
|
|
|
|
default:
|
|
|
|
return ResultFailure(fmt.Sprintf("invalid type %T for regex pattern", regex))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-06-08 12:23:38 -04:00
|
|
|
// Equal succeeds if x == y. See assert.Equal for full documentation.
|
2018-02-28 10:11:02 -05:00
|
|
|
func Equal(x, y interface{}) Comparison {
|
|
|
|
return func() Result {
|
|
|
|
switch {
|
|
|
|
case x == y:
|
|
|
|
return ResultSuccess
|
|
|
|
case isMultiLineStringCompare(x, y):
|
2018-06-08 12:23:38 -04:00
|
|
|
diff := format.UnifiedDiff(format.DiffConfig{A: x.(string), B: y.(string)})
|
2022-09-22 09:38:19 -04:00
|
|
|
return multiLineDiffResult(diff, x, y)
|
2018-02-28 10:11:02 -05:00
|
|
|
}
|
|
|
|
return ResultFailureTemplate(`
|
2020-02-22 12:12:14 -05:00
|
|
|
{{- printf "%v" .Data.x}} (
|
2018-02-28 10:11:02 -05:00
|
|
|
{{- with callArg 0 }}{{ formatNode . }} {{end -}}
|
|
|
|
{{- printf "%T" .Data.x -}}
|
2020-02-22 12:12:14 -05:00
|
|
|
) != {{ printf "%v" .Data.y}} (
|
2018-02-28 10:11:02 -05:00
|
|
|
{{- with callArg 1 }}{{ formatNode . }} {{end -}}
|
|
|
|
{{- printf "%T" .Data.y -}}
|
|
|
|
)`,
|
|
|
|
map[string]interface{}{"x": x, "y": y})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func isMultiLineStringCompare(x, y interface{}) bool {
|
|
|
|
strX, ok := x.(string)
|
|
|
|
if !ok {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
strY, ok := y.(string)
|
|
|
|
if !ok {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
return strings.Contains(strX, "\n") || strings.Contains(strY, "\n")
|
|
|
|
}
|
|
|
|
|
2022-09-22 09:38:19 -04:00
|
|
|
func multiLineDiffResult(diff string, x, y interface{}) Result {
|
2018-02-28 10:11:02 -05:00
|
|
|
return ResultFailureTemplate(`
|
|
|
|
--- {{ with callArg 0 }}{{ formatNode . }}{{else}}←{{end}}
|
|
|
|
+++ {{ with callArg 1 }}{{ formatNode . }}{{else}}→{{end}}
|
|
|
|
{{ .Data.diff }}`,
|
2022-09-22 09:38:19 -04:00
|
|
|
map[string]interface{}{"diff": diff, "x": x, "y": y})
|
2018-02-28 10:11:02 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
// Len succeeds if the sequence has the expected length.
|
|
|
|
func Len(seq interface{}, expected int) Comparison {
|
|
|
|
return func() (result Result) {
|
|
|
|
defer func() {
|
|
|
|
if e := recover(); e != nil {
|
|
|
|
result = ResultFailure(fmt.Sprintf("type %T does not have a length", seq))
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
value := reflect.ValueOf(seq)
|
|
|
|
length := value.Len()
|
|
|
|
if length == expected {
|
|
|
|
return ResultSuccess
|
|
|
|
}
|
|
|
|
msg := fmt.Sprintf("expected %s (length %d) to have length %d", seq, length, expected)
|
|
|
|
return ResultFailure(msg)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Contains succeeds if item is in collection. Collection may be a string, map,
|
|
|
|
// slice, or array.
|
|
|
|
//
|
|
|
|
// If collection is a string, item must also be a string, and is compared using
|
|
|
|
// strings.Contains().
|
|
|
|
// If collection is a Map, contains will succeed if item is a key in the map.
|
|
|
|
// If collection is a slice or array, item is compared to each item in the
|
|
|
|
// sequence using reflect.DeepEqual().
|
|
|
|
func Contains(collection interface{}, item interface{}) Comparison {
|
|
|
|
return func() Result {
|
|
|
|
colValue := reflect.ValueOf(collection)
|
|
|
|
if !colValue.IsValid() {
|
2022-09-22 09:38:19 -04:00
|
|
|
return ResultFailure("nil does not contain items")
|
2018-02-28 10:11:02 -05:00
|
|
|
}
|
|
|
|
msg := fmt.Sprintf("%v does not contain %v", collection, item)
|
|
|
|
|
|
|
|
itemValue := reflect.ValueOf(item)
|
|
|
|
switch colValue.Type().Kind() {
|
|
|
|
case reflect.String:
|
|
|
|
if itemValue.Type().Kind() != reflect.String {
|
|
|
|
return ResultFailure("string may only contain strings")
|
|
|
|
}
|
|
|
|
return toResult(
|
|
|
|
strings.Contains(colValue.String(), itemValue.String()),
|
|
|
|
fmt.Sprintf("string %q does not contain %q", collection, item))
|
|
|
|
|
|
|
|
case reflect.Map:
|
|
|
|
if itemValue.Type() != colValue.Type().Key() {
|
|
|
|
return ResultFailure(fmt.Sprintf(
|
|
|
|
"%v can not contain a %v key", colValue.Type(), itemValue.Type()))
|
|
|
|
}
|
|
|
|
return toResult(colValue.MapIndex(itemValue).IsValid(), msg)
|
|
|
|
|
|
|
|
case reflect.Slice, reflect.Array:
|
|
|
|
for i := 0; i < colValue.Len(); i++ {
|
|
|
|
if reflect.DeepEqual(colValue.Index(i).Interface(), item) {
|
|
|
|
return ResultSuccess
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return ResultFailure(msg)
|
|
|
|
default:
|
|
|
|
return ResultFailure(fmt.Sprintf("type %T does not contain items", collection))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Panics succeeds if f() panics.
|
|
|
|
func Panics(f func()) Comparison {
|
|
|
|
return func() (result Result) {
|
|
|
|
defer func() {
|
|
|
|
if err := recover(); err != nil {
|
|
|
|
result = ResultSuccess
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
f()
|
|
|
|
return ResultFailure("did not panic")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Error succeeds if err is a non-nil error, and the error message equals the
|
|
|
|
// expected message.
|
|
|
|
func Error(err error, message string) Comparison {
|
|
|
|
return func() Result {
|
|
|
|
switch {
|
|
|
|
case err == nil:
|
|
|
|
return ResultFailure("expected an error, got nil")
|
|
|
|
case err.Error() != message:
|
|
|
|
return ResultFailure(fmt.Sprintf(
|
2018-11-14 06:28:16 -05:00
|
|
|
"expected error %q, got %s", message, formatErrorMessage(err)))
|
2018-02-28 10:11:02 -05:00
|
|
|
}
|
|
|
|
return ResultSuccess
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// ErrorContains succeeds if err is a non-nil error, and the error message contains
|
|
|
|
// the expected substring.
|
|
|
|
func ErrorContains(err error, substring string) Comparison {
|
|
|
|
return func() Result {
|
|
|
|
switch {
|
|
|
|
case err == nil:
|
|
|
|
return ResultFailure("expected an error, got nil")
|
|
|
|
case !strings.Contains(err.Error(), substring):
|
|
|
|
return ResultFailure(fmt.Sprintf(
|
2018-11-14 06:28:16 -05:00
|
|
|
"expected error to contain %q, got %s", substring, formatErrorMessage(err)))
|
2018-02-28 10:11:02 -05:00
|
|
|
}
|
|
|
|
return ResultSuccess
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-02-22 12:12:14 -05:00
|
|
|
type causer interface {
|
|
|
|
Cause() error
|
|
|
|
}
|
|
|
|
|
2018-11-14 06:28:16 -05:00
|
|
|
func formatErrorMessage(err error) string {
|
2022-11-05 18:25:29 -04:00
|
|
|
//nolint:errorlint // unwrapping is not appropriate here
|
2020-02-22 12:12:14 -05:00
|
|
|
if _, ok := err.(causer); ok {
|
2018-11-14 06:28:16 -05:00
|
|
|
return fmt.Sprintf("%q\n%+v", err, err)
|
|
|
|
}
|
|
|
|
// This error was not wrapped with github.com/pkg/errors
|
|
|
|
return fmt.Sprintf("%q", err)
|
|
|
|
}
|
|
|
|
|
2018-02-28 10:11:02 -05:00
|
|
|
// Nil succeeds if obj is a nil interface, pointer, or function.
|
|
|
|
//
|
|
|
|
// Use NilError() for comparing errors. Use Len(obj, 0) for comparing slices,
|
|
|
|
// maps, and channels.
|
|
|
|
func Nil(obj interface{}) Comparison {
|
|
|
|
msgFunc := func(value reflect.Value) string {
|
|
|
|
return fmt.Sprintf("%v (type %s) is not nil", reflect.Indirect(value), value.Type())
|
|
|
|
}
|
|
|
|
return isNil(obj, msgFunc)
|
|
|
|
}
|
|
|
|
|
|
|
|
func isNil(obj interface{}, msgFunc func(reflect.Value) string) Comparison {
|
|
|
|
return func() Result {
|
|
|
|
if obj == nil {
|
|
|
|
return ResultSuccess
|
|
|
|
}
|
|
|
|
value := reflect.ValueOf(obj)
|
|
|
|
kind := value.Type().Kind()
|
|
|
|
if kind >= reflect.Chan && kind <= reflect.Slice {
|
|
|
|
if value.IsNil() {
|
|
|
|
return ResultSuccess
|
|
|
|
}
|
|
|
|
return ResultFailure(msgFunc(value))
|
|
|
|
}
|
|
|
|
|
|
|
|
return ResultFailure(fmt.Sprintf("%v (type %s) can not be nil", value, value.Type()))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// ErrorType succeeds if err is not nil and is of the expected type.
|
|
|
|
//
|
|
|
|
// Expected can be one of:
|
2022-11-05 18:25:29 -04:00
|
|
|
//
|
|
|
|
// func(error) bool
|
|
|
|
//
|
2020-02-22 12:12:14 -05:00
|
|
|
// Function should return true if the error is the expected type.
|
2022-11-05 18:25:29 -04:00
|
|
|
//
|
|
|
|
// type struct{}, type &struct{}
|
|
|
|
//
|
2020-02-22 12:12:14 -05:00
|
|
|
// A struct or a pointer to a struct.
|
|
|
|
// Fails if the error is not of the same type as expected.
|
2022-11-05 18:25:29 -04:00
|
|
|
//
|
|
|
|
// type &interface{}
|
|
|
|
//
|
2020-02-22 12:12:14 -05:00
|
|
|
// A pointer to an interface type.
|
|
|
|
// Fails if err does not implement the interface.
|
2022-11-05 18:25:29 -04:00
|
|
|
//
|
|
|
|
// reflect.Type
|
|
|
|
//
|
2020-02-22 12:12:14 -05:00
|
|
|
// Fails if err does not implement the reflect.Type
|
2018-02-28 10:11:02 -05:00
|
|
|
func ErrorType(err error, expected interface{}) Comparison {
|
|
|
|
return func() Result {
|
|
|
|
switch expectedType := expected.(type) {
|
|
|
|
case func(error) bool:
|
|
|
|
return cmpErrorTypeFunc(err, expectedType)
|
|
|
|
case reflect.Type:
|
|
|
|
if expectedType.Kind() == reflect.Interface {
|
|
|
|
return cmpErrorTypeImplementsType(err, expectedType)
|
|
|
|
}
|
|
|
|
return cmpErrorTypeEqualType(err, expectedType)
|
|
|
|
case nil:
|
2022-09-22 09:38:19 -04:00
|
|
|
return ResultFailure("invalid type for expected: nil")
|
2018-02-28 10:11:02 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
expectedType := reflect.TypeOf(expected)
|
|
|
|
switch {
|
2018-06-08 12:23:38 -04:00
|
|
|
case expectedType.Kind() == reflect.Struct, isPtrToStruct(expectedType):
|
2018-02-28 10:11:02 -05:00
|
|
|
return cmpErrorTypeEqualType(err, expectedType)
|
|
|
|
case isPtrToInterface(expectedType):
|
|
|
|
return cmpErrorTypeImplementsType(err, expectedType.Elem())
|
|
|
|
}
|
|
|
|
return ResultFailure(fmt.Sprintf("invalid type for expected: %T", expected))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func cmpErrorTypeFunc(err error, f func(error) bool) Result {
|
|
|
|
if f(err) {
|
|
|
|
return ResultSuccess
|
|
|
|
}
|
|
|
|
actual := "nil"
|
|
|
|
if err != nil {
|
|
|
|
actual = fmt.Sprintf("%s (%T)", err, err)
|
|
|
|
}
|
|
|
|
return ResultFailureTemplate(`error is {{ .Data.actual }}
|
|
|
|
{{- with callArg 1 }}, not {{ formatNode . }}{{end -}}`,
|
|
|
|
map[string]interface{}{"actual": actual})
|
|
|
|
}
|
|
|
|
|
|
|
|
func cmpErrorTypeEqualType(err error, expectedType reflect.Type) Result {
|
|
|
|
if err == nil {
|
|
|
|
return ResultFailure(fmt.Sprintf("error is nil, not %s", expectedType))
|
|
|
|
}
|
|
|
|
errValue := reflect.ValueOf(err)
|
|
|
|
if errValue.Type() == expectedType {
|
|
|
|
return ResultSuccess
|
|
|
|
}
|
|
|
|
return ResultFailure(fmt.Sprintf("error is %s (%T), not %s", err, err, expectedType))
|
|
|
|
}
|
|
|
|
|
|
|
|
func cmpErrorTypeImplementsType(err error, expectedType reflect.Type) Result {
|
|
|
|
if err == nil {
|
|
|
|
return ResultFailure(fmt.Sprintf("error is nil, not %s", expectedType))
|
|
|
|
}
|
|
|
|
errValue := reflect.ValueOf(err)
|
|
|
|
if errValue.Type().Implements(expectedType) {
|
|
|
|
return ResultSuccess
|
|
|
|
}
|
|
|
|
return ResultFailure(fmt.Sprintf("error is %s (%T), not %s", err, err, expectedType))
|
|
|
|
}
|
|
|
|
|
|
|
|
func isPtrToInterface(typ reflect.Type) bool {
|
|
|
|
return typ.Kind() == reflect.Ptr && typ.Elem().Kind() == reflect.Interface
|
|
|
|
}
|
2018-06-08 12:23:38 -04:00
|
|
|
|
|
|
|
func isPtrToStruct(typ reflect.Type) bool {
|
|
|
|
return typ.Kind() == reflect.Ptr && typ.Elem().Kind() == reflect.Struct
|
|
|
|
}
|
2022-09-22 09:38:19 -04:00
|
|
|
|
|
|
|
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})
|
|
|
|
}
|
|
|
|
}
|