Skip to content

Commit

Permalink
util/must: add Handle for convenient failure handling
Browse files Browse the repository at this point in the history
This patch adds `Handle()`, a convenience function that avoids returning
assertion errors when running many assertions. Instead, non-fatal
assertion failures throw panics that are caught by `Handle()` and
converted to errors. Example usage:

```go
// nolint:errcheck
err := must.Handle(ctx, func(ctx context.Context) {
  must.True(ctx, true, "not true")
  must.Equal(ctx, 1, 1, "not equal")
})
```

Inside a `Handle` closure, must assertions never return errors, so they
can be ignored. However, the errcheck linter will complain about this
and must be disabled.

Initially, an attempt was made to pass in a struct with infallible
assertion methods, for a safer API. Unfortunately, methods can't use
generics, nor can function types, so this appears to be one of the only
ways to achieve this.

Epic: none
Release note: None
  • Loading branch information
erikgrinaker committed Jul 28, 2023
1 parent 0a1699d commit 40a3971
Show file tree
Hide file tree
Showing 2 changed files with 54 additions and 0 deletions.
38 changes: 38 additions & 0 deletions pkg/util/must/must.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,10 @@ func failDepth(ctx context.Context, depth int, format string, args ...interface{
log.ErrorfDepth(ctx, depth, "%+v", err)
MaybeSendReport(ctx, err)
}
// If we're running under Handle(), propagate the error as a panic.
if ctx.Value(handleKey{}) != nil {
panic(handlePanic{err: err})
}
return err
}

Expand Down Expand Up @@ -236,6 +240,40 @@ func Expensive(f func() error) error {
return nil
}

// Handle runs the given closure and automatically converts non-fatal assertion
// failures to a returned error, without needing to return them explicitly.
// Fatal assertions fatal at the call site as usual.
//
// Non-fatal assertions failures are propagated as panics that are recovered by
// Handle() and returned. Panics not thrown by must are propagated to the
// caller.
//
// Within a Handle() closure, assertion failures never return errors, but the
// errcheck linter will complain about unhandled errors. Either explicitly
// ignore them with e.g. "_ = must.True()", or add a nolint:errcheck comment
// above the call to Handle().
//
// gcassert:inline
func Handle(ctx context.Context, f func(context.Context)) (err error) {
defer func() {
if r := recover(); r != nil {
if f, ok := r.(handlePanic); ok {
err = f.err
} else {
panic(r)
}
}
}()
f(context.WithValue(ctx, handleKey{}, struct{}{}))
return
}

// withKey is a context key for Handle().
type handleKey struct{}

// handlePanic is thrown and recovered via Handle().
type handlePanic struct{ err error }

// True requires v to be true. Otherwise, fatals in dev builds or errors
// in release builds (by default).
//
Expand Down
16 changes: 16 additions & 0 deletions pkg/util/must/must_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -424,3 +424,19 @@ func TestFatalOn(t *testing.T) {

// We don't test failures, because it fatals.
}

func TestWith(t *testing.T) {
defer leaktest.AfterTest(t)()
defer noopFail()()

ctx := context.Background()

require.NoError(t, must.With(ctx, func(ctx context.Context) {
must.Equal(ctx, 1, 1, "equal")
}))

err := must.With(ctx, func(ctx context.Context) {
must.Equal(ctx, 1, 2, "equal")
})
require.Error(t, err)
}

0 comments on commit 40a3971

Please sign in to comment.