Skip to content

Commit

Permalink
Make .IsError(...) more flexible (#27)
Browse files Browse the repository at this point in the history
Add support to match either no error, any error, specific error, or error with message containing string or matching regex.
  • Loading branch information
maargenton authored Nov 11, 2024
1 parent 1a3bb8b commit 8ff19c8
Show file tree
Hide file tree
Showing 5 changed files with 89 additions and 17 deletions.
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 5 additions & 3 deletions pkg/utils/builder/builder_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
9 changes: 8 additions & 1 deletion pkg/utils/builder/builder_api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package builder_test

import (
"fmt"
"regexp"
"testing"

"github.com/maargenton/go-testpredicate/pkg/subexpr"
Expand Down Expand Up @@ -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
Expand Down
62 changes: 50 additions & 12 deletions pkg/utils/predicate/impl/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
19 changes: 19 additions & 0 deletions pkg/utils/predicate/impl/error_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package impl_test

import (
"fmt"
"regexp"
"testing"

"github.com/maargenton/go-testpredicate/pkg/utils/predicate/impl"
Expand All @@ -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 {
Expand Down

0 comments on commit 8ff19c8

Please sign in to comment.