Skip to content

Commit

Permalink
Add ErrorIs and ErrorAs assertions
Browse files Browse the repository at this point in the history
  • Loading branch information
rliebz committed Apr 27, 2024
1 parent 87709f9 commit 8ddef3a
Show file tree
Hide file tree
Showing 2 changed files with 289 additions and 0 deletions.
103 changes: 103 additions & 0 deletions be/errors.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package be

import (
"errors"
"fmt"
"reflect"
"strings"

"github.com/rliebz/ghost"
Expand Down Expand Up @@ -121,3 +123,104 @@ want: %v`,
),
}
}

// ErrorIs asserts that an error matches another using [errors.Is].
func ErrorIs(err error, target error) ghost.Result {
args := ghostlib.ArgsFromAST(err, target)
argErr, argTarget := args[0], args[1]

if errors.Is(err, target) {
return ghost.Result{
Ok: true,
Message: fmt.Sprintf(`error %v is target %v
error: %v
target: %v`,
argErr,
argTarget,
err,
target,
),
}
}

return ghost.Result{
Ok: false,
Message: fmt.Sprintf(`error %v is not target %v
error: %v
target: %v`,
argErr,
argTarget,
err,
target,
),
}
}

var errorType = reflect.TypeOf((*error)(nil)).Elem()

// ErrorAs asserts that an error matches another using [errors.As].
func ErrorAs(err error, target any) ghost.Result {
args := ghostlib.ArgsFromAST(err, target)
argErr, argTarget := args[0], args[1]

if err == nil {
return ghost.Result{
Ok: false,
Message: fmt.Sprintf("error %v was nil", argErr),
}
}

// These next few checks are for invalid usage, where errors.As will panic if
// a caller hits any of them. As an assertion library, it's probably more
// polite to never panic.

if target == nil {
return ghost.Result{
Ok: false,
Message: fmt.Sprintf("target %v cannot be nil", argTarget),
}
}

val := reflect.ValueOf(target)
typ := val.Type()
if typ.Kind() != reflect.Ptr || val.IsNil() {
return ghost.Result{
Ok: false,
Message: fmt.Sprintf("target %v must be a non-nil pointer", argTarget),
}
}
targetType := typ.Elem()

if targetType.Kind() != reflect.Interface && !targetType.Implements(errorType) {
return ghost.Result{
Ok: false,
Message: fmt.Sprintf("*target %v must be interface or implement error", argTarget),
}
}

if errors.As(err, target) {
return ghost.Result{
Ok: true,
Message: fmt.Sprintf(`error %v set as target %v
error: %v
target: %v`,
argErr,
argTarget,
err,
targetType,
),
}
}

return ghost.Result{
Ok: false,
Message: fmt.Sprintf(`error %v cannot be set as target %v
error: %v
target: %v`,
argErr,
argTarget,
err,
targetType,
),
}
}
186 changes: 186 additions & 0 deletions be/errors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ package be_test

import (
"errors"
"fmt"
"io/fs"
"os"
"testing"

"github.com/rliebz/ghost"
Expand Down Expand Up @@ -177,3 +180,186 @@ got: <nil>
want: boo`))
})
}

func TestErrorIs(t *testing.T) {
t.Run("match", func(t *testing.T) {
g := ghost.New(t)

target := errors.New("foobar")
err := fmt.Errorf("wrapping: %w", target)

result := be.ErrorIs(err, target)
g.Should(be.True(result.Ok))
g.Should(be.Equal(
result.Message,
`error err is target target
error: wrapping: foobar
target: foobar`,
))

result = be.ErrorIs(fmt.Errorf("wrapping: %w", target), target)
g.Should(be.True(result.Ok))
g.Should(be.Equal(
result.Message,
`error fmt.Errorf("wrapping: %w", target) is target target
error: wrapping: foobar
target: foobar`,
))
})

t.Run("no match", func(t *testing.T) {
g := ghost.New(t)

target := errors.New("foobar")
err := fmt.Errorf("wrapping: %v", target) //nolint:errorlint // test case

result := be.ErrorIs(err, target)
g.Should(be.False(result.Ok))
g.Should(be.Equal(
result.Message,
`error err is not target target
error: wrapping: foobar
target: foobar`,
))

result = be.ErrorIs(fmt.Errorf("wrapping: %v", target), target) //nolint:errorlint // test case
g.Should(be.False(result.Ok))
g.Should(be.Equal(
result.Message,
`error fmt.Errorf("wrapping: %v", target) is not target target
error: wrapping: foobar
target: foobar`,
))
})

t.Run("nil", func(t *testing.T) {
g := ghost.New(t)

var target error
var err error

result := be.ErrorIs(err, target)
g.Should(be.True(result.Ok))
g.Should(be.Equal(result.Message, `error err is target target
error: <nil>
target: <nil>`))

result = be.ErrorIs(nil, nil)
g.Should(be.True(result.Ok))
g.Should(be.Equal(result.Message, `error nil is target nil
error: <nil>
target: <nil>`))
})
}

func TestErrorAs(t *testing.T) {
t.Run("match", func(t *testing.T) {
g := ghost.New(t)

var target *fs.PathError
_, err := os.Open("some-non-existing-file")

result := be.ErrorAs(err, &target)
g.Should(be.True(result.Ok))
g.Should(be.Equal(
result.Message,
`error err set as target &target
error: open some-non-existing-file: no such file or directory
target: *fs.PathError`,
))

result = be.ErrorAs(fmt.Errorf("wrapping: %w", err), &target)
g.Should(be.True(result.Ok))
g.Should(be.Equal(
result.Message,
`error fmt.Errorf("wrapping: %w", err) set as target &target
error: wrapping: open some-non-existing-file: no such file or directory
target: *fs.PathError`,
))
})

t.Run("no match", func(t *testing.T) {
g := ghost.New(t)

var target *fs.PathError
err := errors.New("oh no")

result := be.ErrorAs(err, &target)
g.Should(be.False(result.Ok))
g.Should(be.Equal(
result.Message,
`error err cannot be set as target &target
error: oh no
target: *fs.PathError`,
))

result = be.ErrorAs(errors.New("oh no"), &target)
g.Should(be.False(result.Ok))
g.Should(be.Equal(
result.Message,
`error errors.New("oh no") cannot be set as target &target
error: oh no
target: *fs.PathError`,
))
})

t.Run("nil error", func(t *testing.T) {
g := ghost.New(t)

var target error
var err error

result := be.ErrorAs(err, target)
g.Should(be.False(result.Ok))
g.Should(be.Equal(result.Message, `error err was nil`))

result = be.ErrorAs(nil, nil)
g.Should(be.False(result.Ok))
g.Should(be.Equal(result.Message, `error nil was nil`))
})

t.Run("nil target", func(t *testing.T) {
g := ghost.New(t)

var target error
err := errors.New("oh no")

result := be.ErrorAs(err, target)
g.Should(be.False(result.Ok))
g.Should(be.Equal(result.Message, `target target cannot be nil`))

result = be.ErrorAs(errors.New("oh no"), nil)
g.Should(be.False(result.Ok))
g.Should(be.Equal(result.Message, `target nil cannot be nil`))
})

t.Run("non-pointer target", func(t *testing.T) {
g := ghost.New(t)

target := "Hello"
err := errors.New("oh no")

result := be.ErrorAs(err, target)
g.Should(be.False(result.Ok))
g.Should(be.Equal(result.Message, `target target must be a non-nil pointer`))

result = be.ErrorAs(errors.New("oh no"), "Hello")
g.Should(be.False(result.Ok))
g.Should(be.Equal(result.Message, `target "Hello" must be a non-nil pointer`))
})

t.Run("non-error target element", func(t *testing.T) {
g := ghost.New(t)

target := "Hello"
err := errors.New("oh no")

result := be.ErrorAs(err, &target)
g.Should(be.False(result.Ok))
g.Should(be.Equal(result.Message, `*target &target must be interface or implement error`))

result = be.ErrorAs(errors.New("oh no"), new(string))
g.Should(be.False(result.Ok))
g.Should(be.Equal(result.Message, `*target new(string) must be interface or implement error`))
})
}

0 comments on commit 8ddef3a

Please sign in to comment.