diff --git a/README.md b/README.md index 54cfdb7..e089c16 100644 --- a/README.md +++ b/README.md @@ -154,7 +154,13 @@ func TestCompareAPI(t *testing.T) { func TestErrorAPI(t *testing.T) { var sentinel = fmt.Errorf("sentinel") var err = fmt.Errorf("error: %w", sentinel) - verify.That(t, err).IsError(sentinel) + var re = regexp.MustCompile("^error: sentinel$") + + verify.That(t, nil).IsError(nil) // No error + verify.That(t, err).IsError("") // Any error + verify.That(t, err).IsError(sentinel) // Specific error or nested error + verify.That(t, err).IsError("sentinel") // Message contains string + verify.That(t, err).IsError(re) // Message matches regexp var err2 = fmt.Errorf("error: %w", &MyError{Code: 123}) var myError *MyError diff --git a/pkg/utils/builder/builder_api.go b/pkg/utils/builder/builder_api.go index 2128f67..35d980f 100644 --- a/pkg/utils/builder/builder_api.go +++ b/pkg/utils/builder/builder_api.go @@ -125,9 +125,11 @@ func (b *Builder) Ne(rhs interface{}) *predicate.Predicate { // --------------------------------------------------------------------------- // From pkg/utils/predicate/impl/error.go -// IsError tests if a value is an error matching or wrapping the expected error -// (according to go 1.13 error.Is()). -func (b *Builder) IsError(expected error) *predicate.Predicate { +// IsError tests an error value to be either nil, a specific error according to +// `errors.Is()`, or an error whose message contains a specified string or +// matches a regexp. `.IsError("")` matches any error whose message contains an +// empty string, which is any non-nil error. +func (b *Builder) IsError(expected any) *predicate.Predicate { b.p.RegisterPredicate(impl.IsError(expected)) if b.t != nil { b.t.Helper() diff --git a/pkg/utils/builder/builder_api_test.go b/pkg/utils/builder/builder_api_test.go index 0a0c255..defe1e7 100644 --- a/pkg/utils/builder/builder_api_test.go +++ b/pkg/utils/builder/builder_api_test.go @@ -2,6 +2,7 @@ package builder_test import ( "fmt" + "regexp" "testing" "github.com/maargenton/go-testpredicate/pkg/subexpr" @@ -45,7 +46,13 @@ func (err *MyError) Error() string { func TestErrorAPI(t *testing.T) { var sentinel = fmt.Errorf("sentinel") var err = fmt.Errorf("error: %w", sentinel) - verify.That(t, err).IsError(sentinel) + var re = regexp.MustCompile("^error: sentinel$") + + verify.That(t, nil).IsError(nil) // No error + verify.That(t, err).IsError("") // Any error + verify.That(t, err).IsError(sentinel) // Specific error or nested error + verify.That(t, err).IsError("sentinel") // Message contains string + verify.That(t, err).IsError(re) // Message matches regexp var err2 = fmt.Errorf("error: %w", &MyError{Code: 123}) var myError *MyError diff --git a/pkg/utils/predicate/impl/error.go b/pkg/utils/predicate/impl/error.go index cd96fab..4823820 100644 --- a/pkg/utils/predicate/impl/error.go +++ b/pkg/utils/predicate/impl/error.go @@ -4,29 +4,67 @@ import ( "errors" "fmt" "reflect" + "regexp" + "strings" "github.com/maargenton/go-testpredicate/pkg/utils/predicate" ) -// IsError tests if a value is an error matching or wrapping the expected error -// (according to go 1.13 error.Is()). -func IsError(expected error) (desc string, f predicate.PredicateFunc) { - if expected != nil { - desc = fmt.Sprintf("{} is error '%v'", expected) - } else { +// IsError tests an error value to be either nil, a specific error according to +// `errors.Is()`, or an error whose message contains a specified string or +// matches a regexp. `.IsError("")` matches any error whose message contains an +// empty string, which is any non-nil error. +func IsError(expected any) (desc string, f predicate.PredicateFunc) { + var predicateError error + + if expected == nil { desc = "{} is no error" + } else { + if _, ok := expected.(error); ok { + desc = fmt.Sprintf("{} is error '%v'", expected) + } else if s, ok := expected.(string); ok { + if len(s) == 0 { + desc = "{} is an error" + } else { + desc = fmt.Sprintf("{} is error containing '%v'", s) + } + } else if re, ok := expected.(*regexp.Regexp); ok { + desc = fmt.Sprintf("{} is error matching /%v/", re) + } else { + predicateError = fmt.Errorf( + "invalid argument of type '%T' for 'IsError()' predicate", + expected) + } } + + if predicateError != nil { + f = func(v interface{}) (r bool, ctx []predicate.ContextValue, err error) { + err = predicateError + return + } + return + } + f = func(v interface{}) (r bool, ctx []predicate.ContextValue, err error) { - if v == nil { - r = expected == nil - } else if errValue, ok := v.(error); ok { - r = errors.Is(errValue, expected) + var errValue, isError = v.(error) + if !isError && v != nil { + err = fmt.Errorf("value of type '%T' is not an error", v) + return + } + if isError { ctx = []predicate.ContextValue{ {Name: "message", Value: errValue.Error()}, } + } - } else { - err = fmt.Errorf("value of type '%T' is not an error", v) + if expected == nil { + r = errValue == nil + } else if expectedErr, ok := expected.(error); ok { + r = errors.Is(errValue, expectedErr) + } else if expectedString, ok := expected.(string); ok && errValue != nil { + r = strings.Contains(errValue.Error(), expectedString) + } else if expectedRegexp, ok := expected.(*regexp.Regexp); ok && errValue != nil { + r = expectedRegexp.MatchString(errValue.Error()) } return } diff --git a/pkg/utils/predicate/impl/error_test.go b/pkg/utils/predicate/impl/error_test.go index 92924f9..ba1fc2b 100644 --- a/pkg/utils/predicate/impl/error_test.go +++ b/pkg/utils/predicate/impl/error_test.go @@ -2,6 +2,7 @@ package impl_test import ( "fmt" + "regexp" "testing" "github.com/maargenton/go-testpredicate/pkg/utils/predicate/impl" @@ -26,6 +27,24 @@ func TestIsError(t *testing.T) { value: 123, errorMsg: "value of type 'int' is not an error", }) + + verifyPredicate(t, pr(impl.IsError("")), expectation{value: nil, pass: false}) + verifyPredicate(t, pr(impl.IsError("")), expectation{value: other, pass: true}) + verifyPredicate(t, pr(impl.IsError("")), expectation{ + value: 123, + errorMsg: "value of type 'int' is not an error", + }) + + verifyPredicate(t, pr(impl.IsError("not a part of the error")), expectation{value: err, pass: false}) + verifyPredicate(t, pr(impl.IsError("wrapper")), expectation{value: err, pass: true}) + verifyPredicate(t, pr(impl.IsError("sentinel")), expectation{value: err, pass: true}) + + var re = regexp.MustCompile(`^wrapper: sentinel$`) + verifyPredicate(t, pr(impl.IsError(re)), expectation{value: err, pass: true}) + verifyPredicate(t, pr(impl.IsError(re)), expectation{value: sentinel, pass: false}) + + verifyPredicate(t, pr(impl.IsError(123)), expectation{value: err, pass: false, + errorMsg: "invalid argument of type 'int' for 'IsError()' predicate"}) } type MyError struct {