Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add be.AssignedAs and deprecate be.Panic #5

Merged
merged 1 commit into from
Dec 2, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -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
@@ -20,7 +20,6 @@ linters:
- errchkjson
- errname
- errorlint
- exportloopref
- gocognit
- gocritic
- gofumpt
20 changes: 18 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -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))
@@ -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
86 changes: 46 additions & 40 deletions be/assertions.go
Original file line number Diff line number Diff line change
@@ -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)
@@ -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)
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)
@@ -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)
16 changes: 13 additions & 3 deletions example_test.go
Original file line number Diff line number Diff line change
@@ -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))
@@ -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)
2 changes: 1 addition & 1 deletion tusk.yml
Original file line number Diff line number Diff line change
@@ -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