Skip to content

Commit

Permalink
Add be.AssignedAs and remove be.Panic (#5)
Browse files Browse the repository at this point in the history
I don't think the current panic assertion is very good, because
asserting _that_ something panics is a lot less effective than asserting
the _value_ of a panic. So this refactor makes it so you have to
orchestrate the panic recovery yourself, but also makes it easier to
assert something of type `any` with a more specific assertion.

And generally I think `be.AssignedAs` is a smart assertion to have. It
roughly resembles the shape of `be.ErrorAs`, so it shouldn't be very
surprising.
  • Loading branch information
rliebz authored Dec 2, 2024
1 parent 0adc681 commit 2090947
Show file tree
Hide file tree
Showing 7 changed files with 177 additions and 85 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,4 @@ jobs:
- name: Lint
uses: golangci/golangci-lint-action@v3
with:
version: v1.57.2
version: v1.62.2
1 change: 0 additions & 1 deletion .golangci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ linters:
- errchkjson
- errname
- errorlint
- exportloopref
- gocognit
- gocritic
- gofumpt
Expand Down
20 changes: 18 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,6 @@ g.Should(be.DeepEqual([]string{"a", "b"}, []string{"a", "b"}))
g.Should(be.SliceContaining([]int{1, 2, 3}, 2))
g.Should(be.StringContaining("foobar", "foo"))

g.Should(be.Panic(func() { panic("oh no") }))

var err error
g.NoError(err)
g.Must(be.Nil(err))
Expand Down Expand Up @@ -203,6 +201,24 @@ g.Should(BeThirteen(myInt)) // "myInt is 0"
g.Should(BeThirteen(5 + 6)) // "5 + 6 is 11"
```

#### Handling Panics

If you expect your code to panic, it is better to assert that the value passed
to `panic` has the properties you expect, rather than to make an assumption
that the panic you encountered is the panic you were expecting. Ghost can be
combined with `defer`/`recover` to access the full expressiveness of test
assertions:

```go
defer func() {
var err error
g.Must(be.AssignedAs(recover(), &err))
g.Should(be.ErrorEqual(err, "a specific error occurred"))
}()

doStuff()
```

## Philosophy

### Ghost Does Assertions
Expand Down
86 changes: 46 additions & 40 deletions be/assertions.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,52 @@ import (
"github.com/rliebz/ghost/internal/constraints"
)

// AssignedAs assigns a value to a target of an arbitrary type.
//
// The target must be a non-nil pointer.
func AssignedAs[T any](value any, target *T) ghost.Result {
args := ghostlib.ArgsFromAST(value, target)
argValue, argTarget := args[0], args[1]

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

typedValue, ok := value.(T)
if !ok {
return ghost.Result{
Ok: false,
Message: fmt.Sprintf(
`%s (%T) could not be assigned to %s (%T)
value: %v`,
argValue,
value,
argTarget,
target,
value,
),
}
}

*target = typedValue

return ghost.Result{
Ok: true,
Message: fmt.Sprintf(
`%s (%T) was assigned to %s (%T)
value: %v`,
argValue,
value,
argTarget,
target,
value,
),
}
}

// Close asserts that a value is within a delta of another.
func Close[T constraints.Integer | constraints.Float](got, want, delta T) ghost.Result {
args := ghostlib.ArgsFromAST(got, want, delta)
Expand Down Expand Up @@ -286,46 +332,6 @@ func isNil(v any) bool {
return false
}

// Panic asserts that the given function panics when invoked.
func Panic(f func()) (result ghost.Result) {
args := ghostlib.ArgsFromAST(f)
argF := args[0]

defer func() {
if r := recover(); r != nil {
if strings.Contains(argF, "\n") {
result = ghost.Result{
Ok: true,
Message: fmt.Sprintf(`function panicked with value: %v
%v
`, r, argF),
}
} else {
result = ghost.Result{
Ok: true,
Message: fmt.Sprintf(`function %v panicked with value: %v`, argF, r),
}
}
}
}()

f()

if strings.Contains(argF, "\n") {
return ghost.Result{
Ok: false,
Message: fmt.Sprintf(`function did not panic
%v
`, argF),
}
}

return ghost.Result{
Ok: false,
Message: fmt.Sprintf("function %v did not panic", argF),
}
}

// SliceContaining asserts that an element exists in a given slice.
func SliceContaining[T comparable](slice []T, element T) ghost.Result {
args := ghostlib.ArgsFromAST(slice, element)
Expand Down
135 changes: 98 additions & 37 deletions be/assertions_test.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,112 @@
package be_test

import (
"bytes"
"errors"
"io"
"strings"
"testing"

"github.com/rliebz/ghost"
"github.com/rliebz/ghost/be"
)

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

var got any = "some-value"
var want string

result := be.AssignedAs(got, &want)
g.Should(be.True(result.Ok))
g.Should(be.Equal(result.Message, `got (string) was assigned to &want (*string)
value: some-value`))

result = be.AssignedAs("some-value", new(string))
g.Should(be.True(result.Ok))
g.Should(be.Equal(result.Message, `"some-value" (string) was assigned to new(string) (*string)
value: some-value`))
})

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

var got any = 15
var want string

result := be.AssignedAs(got, &want)
g.Should(be.False(result.Ok))
g.Should(be.Equal(result.Message, `got (int) could not be assigned to &want (*string)
value: 15`))

result = be.AssignedAs(15, new(string))
g.Should(be.False(result.Ok))
g.Should(be.Equal(result.Message, `15 (int) could not be assigned to new(string) (*string)
value: 15`,
))
})

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

var got any = new(bytes.Buffer)
var want io.Reader

result := be.AssignedAs(got, &want)
g.Should(be.True(result.Ok))
g.Should(be.Equal(result.Message, `got (*bytes.Buffer) was assigned to &want (*io.Reader)
value: `))

result = be.AssignedAs(new(bytes.Buffer), new(io.Reader))
g.Should(be.True(result.Ok))
g.Should(be.Equal(
result.Message,
`new(bytes.Buffer) (*bytes.Buffer) was assigned to new(io.Reader) (*io.Reader)
value: `))
})

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

var got any = 15
var want io.Reader

result := be.AssignedAs(got, &want)
g.Should(be.False(result.Ok))
g.Should(be.Equal(result.Message, `got (int) could not be assigned to &want (*io.Reader)
value: 15`))

result = be.AssignedAs(15, new(io.Reader))
g.Should(be.False(result.Ok))
g.Should(be.Equal(result.Message, `15 (int) could not be assigned to new(io.Reader) (*io.Reader)
value: 15`))
})

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

var got any = 15
var want *int

result := be.AssignedAs(got, want)
g.Should(be.False(result.Ok))
g.Should(be.Equal(result.Message, "target want cannot be nil"))
})

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

defer func() {
var err error
g.Must(be.AssignedAs(recover(), &err))
g.Should(be.ErrorEqual(err, "oops"))
}()

panic(errors.New("oops"))
})
}

func TestClose(t *testing.T) {
t.Run("in delta", func(t *testing.T) {
g := ghost.New(t)
Expand Down Expand Up @@ -557,43 +655,6 @@ func TestNil(t *testing.T) {
})
}

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

f := func() { panic(errors.New("oh no")) }

result := be.Panic(f)
g.Should(be.True(result.Ok))
g.Should(be.Equal(result.Message, "function f panicked with value: oh no"))

result = be.Panic(func() { panic(errors.New("oh no")) })
g.Should(be.True(result.Ok))
g.Should(be.Equal(result.Message, `function panicked with value: oh no
func() {
panic(errors.New("oh no"))
}
`))
})

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

f := func() {}

result := be.Panic(f)
g.Should(be.False(result.Ok))
g.Should(be.Equal(result.Message, "function f did not panic"))

result = be.Panic(func() {})
g.Should(be.False(result.Ok))
g.Should(be.Equal(result.Message, `function did not panic
func() {
}
`))
})
}

func TestSliceContaining(t *testing.T) {
t.Run("contains <= 3", func(t *testing.T) {
g := ghost.New(t)
Expand Down
16 changes: 13 additions & 3 deletions example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,6 @@ func TestExample(t *testing.T) {

g.Should(be.StringContaining("foobar", "foo"))

g.Should(be.Panic(func() { panic("oh no") }))
g.ShouldNot(be.Panic(func() {}))

var err error
g.NoError(err)
g.Must(be.Nil(err))
Expand All @@ -44,6 +41,19 @@ func TestExample(t *testing.T) {
g.ShouldNot(be.JSONEqual(`{"a":1}`, `{"a":2}`))
}

func ExampleAssignedAs() {
t := new(testing.T) // from the test
g := ghost.New(t)

defer func() {
var err error
g.Must(be.AssignedAs(recover(), &err))
g.Should(be.ErrorEqual(err, "oops"))
}()

panic(errors.New("oops"))
}

func ExampleEventually() {
t := new(testing.T) // from the test
g := ghost.New(t)
Expand Down
2 changes: 1 addition & 1 deletion tusk.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ tasks:
usage: Run static analysis
description: |
Run golangci-lint using the project configuration.
run: go run github.com/golangci/golangci-lint/cmd/golangci-lint@v1.57.1 run ./...
run: go run github.com/golangci/golangci-lint/cmd/golangci-lint@v1.62.2 run ./...

test:
usage: Run unit tests
Expand Down

0 comments on commit 2090947

Please sign in to comment.