Skip to content
Merged
Show file tree
Hide file tree
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
*.vim*
coverage.*

# AI
/.codex/

# Binaries for programs and plugins
*.exe
*.exe~
Expand Down
24 changes: 24 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# AGENTS.md

## Project

This is a mature Go library. Preserve API compatibility unless the task
explicitly requires an API change.

## Verification

Run:

make

before considering a change complete.

## Working principles

- Prefer minimal changes.
- Do not refactor unrelated working code.
- Follow existing code and documentation conventions.
- Treat existing behavior as intentional unless evidence suggests otherwise.
- Check exported Go API documentation when exported behavior changes.
- Keep README and other user documentation consistent with the implementation.
- Do not commit changes unless explicitly requested.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

### Version history

##### 1.2.3
- Bug Fix (issue-30): automatic error annotation fixed, when app functions were
named like, e.g., `func MyHandle() (err error)`
- Refactoring internal parts, will help maintenance and new contributors
- Maintenance updates, like GitHub actions, etc.

##### 1.2.2
- Bug Fix (issue-27): automatic error annotation works now for try.T functions
- Updated documentation
Expand Down
52 changes: 43 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ func CopyFile(src, dst string) (err error) {
- [Assertion](#assertion)
- [Asserters](#asserters)
- [Assertion Package for Runtime Use](#assertion-package-for-runtime-use)
- [Returning Sentinel Error Values](#returning-sentinel-error-values)
- [Assertion Package for Unit Testing](#assertion-package-for-unit-testing)
- [Automatic Flags](#automatic-flags)
- [Support for Cobra Flags](#support-for-cobra-flags)
Expand Down Expand Up @@ -370,6 +371,38 @@ We have now described design-by-contract for development and runtime use. What
makes err2's assertion packages unique, and extremely powerful, is its use for
automatic testing as well.

#### Returning Sentinel Error Values

An assertion can also return a sentinel error through `err2.Handle`. When the
first optional argument is an `error`, a failed assertion uses that error as
the cause. The normal asserters add useful assertion information by wrapping
the error, so callers should check it with `errors.Is`:

```go
var ErrArgumentsNeeded = errors.New("arguments needed")

func run(args []string) (err error) {
defer err2.Handle(&err)

assert.SNotEmpty(args, ErrArgumentsNeeded)
return nil
}

func main() {
if err := run(nil); errors.Is(err, ErrArgumentsNeeded) {
// handle missing arguments
}
}
```

If a function must return the exact sentinel value for direct comparison, use
the `Plain` asserter and disable `err2.Handle`'s automatic annotation:

```go
defer assert.PushAsserter(assert.Plain)()
defer err2.Handle(&err, nil)
```

#### Assertion Package for Unit Testing

The same asserts can be used **and shared** during the unit tests over module
Expand All @@ -391,10 +424,9 @@ func TestWebOfTrustInfo(t *testing.T) {

assert.Equal(wot.CommonInvider, 0)
assert.Equal(wot.Hops, 1)

wot = NewWebOfTrust(bob.Node, carol.Node)
assert.Equal(wot.CommonInvider, hop.NotConnected)
assert.Equal(wot.Hops, hop.NotConnected)
...
err := wrongPath2.Prove() // shouldn't succeed!
assert.ErrorIs(err, ErrCycleOrDuplicate) // testing sentinel error value
...
```

Expand Down Expand Up @@ -623,8 +655,10 @@ Please see the full version history from [CHANGELOG](./CHANGELOG.md).

### Latest Release

##### 1.2.3
- Bug Fix (issue-30): automatic error annotation fixed, when app functions were
named like, e.g., `func MyHandle() (err error)`
- Refactoring internal parts, will help maintenance and new contributors
- Maintenance updates, like GitHub actions, etc.
##### 1.3.0
- Assertion failures can return sentinel error values through `err2.Handle`
- `assert.PushAsserter` now honors goroutine-specific asserters during unit
tests, allowing `Plain` to preserve exact sentinel error values
- New `assert.ErrorIs`, `assert.ErrorIsNot`, and `assert.MKeyNotExists`
functions
- Extended assertion documentation and examples
96 changes: 70 additions & 26 deletions assert/assert.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package assert

import (
"errors"
"flag"
"fmt"
"os"
Expand All @@ -27,7 +28,12 @@ type Asserter = uint32
//
// Note that Plain is only [Asserter] that override auto-generated assertion
// messages with given arguments like 'pool name cannot be empty'. Others add
// given arguments at the end of the auto-generated assert message.
// given arguments at the end of the auto-generated assert message. For that
// reason Plain is useful when you want to return sentinel error values with
// asserts and you don't need any extra information to be wrapped to an error
// value:
//
// assert.NotEmpty(c.PoolName, ErrMissingPoolName)
//
// [Production] (pkg's default) is the best [Asserter] for most cases. The
// assertion violations are treated as Go error values. And only a pragmatic
Expand Down Expand Up @@ -774,13 +780,32 @@ func MKeyExists[M ~map[T]U, T comparable, U any](
val, ok = obj[key]

if !ok {
doMKeyExists(key, a)
doMKeyExists("doesn't", key, a)
}
return val
}

func doMKeyExists(key any, a []any) {
defMsg := fmt.Sprintf(assertionMsg+": key '%v' doesn't exist", key)
// MKeyNotExists asserts that the map key NOT exists. If not it panics/errors
// (current [Asserter]) the auto-generated (args appended) message.
//
// Note that when [Plain] [Asserter] is used ([PushAsserter] or even
// [SetDefault]), optional arguments are used to override the auto-generated
// assert violation message.
func MKeyNotExists[M ~map[T]U, T comparable, U any](
obj M,
key T,
a ...any,
) {
var ok bool
_, ok = obj[key]

if ok {
doMKeyExists("shouldn't", key, a)
}
}

func doMKeyExists(not string, key any, a []any) {
defMsg := fmt.Sprintf(assertionMsg+": key '%v' %s exist", key, not)
current().reportAssertionFault(1, defMsg, a)
}

Expand Down Expand Up @@ -927,6 +952,30 @@ func doError(a []any) {
current().reportAssertionFault(1, defMsg, a)
}

// ErrorIs asserts that the err is the target error. If it isn't it panics and
// builds a violation message.
//
// Note that when [Plain] [Asserter] is used ([PushAsserter] or even
// [SetDefault]), optional arguments are used to override the auto-generated
// assert violation message.
func ErrorIs(err, target error, a ...any) {
if !errors.Is(err, target) {
doShouldBeEqual(assertionNotEqualMsg, err.Error(), target.Error(), a)
}
}

// ErrorIsNot asserts that the err isn't the target error. If it is it panics
// and builds a violation message.
//
// Note that when [Plain] [Asserter] is used ([PushAsserter] or even
// [SetDefault]), optional arguments are used to override the auto-generated
// assert violation message.
func ErrorIsNot(err, target error, a ...any) {
if errors.Is(err, target) {
doShouldBeEqual(assertionEqualMsg, err.Error(), target.Error(), a)
}
}

// Greater asserts that the value is greater than want. If it is not it panics
// and builds a violation message. Thanks to inlining, the performance penalty
// is equal to a single 'if-statement' that is almost nothing.
Expand Down Expand Up @@ -1049,33 +1098,35 @@ func SetDefault(i Asserter) (old Asserter) {
return
}

// PushAsserter set [Asserter] for the current GLS (Gorounine Local Storage).
// PushAsserter set [Asserter] for the current GLS (Goroutine Local Storage).
// That allows us to have multiple different [Asserter] in use in the same
// process.
//
// Let's say that in some function you want to return plain error messages
// instead of the panic asserts, you can use following in the top-level
// function:
// When you want to return plain error messages or sentinel error values, you
// should use following in the top-level function:
//
// defer assert.PushAsserter(assert.Plain)()
//
// The [Asserter] Plain prevents asserts wrapping extra information to an error
// value.
//
// Note that [github.com/lainio/err2.Handle] is capable to annotate errors
// automatically. You can prevent that using nil in the optional arguments:
//
// defer err2.Handle(&err, nil)
func PushAsserter(i Asserter) (retFn function) {
var (
prevFound bool
prevAsserter asserter
currentGID int
)

// get pkg lvl asserter
curAsserter := defAsserter[def]
// .. to check if we are doing unit tests
if !curAsserter.isUnitTesting() {
// .. allow GLS specific asserter. NOTE see current()
currentGID = goid()
asserterMap.Tx(func(m map[int]asserter) {
prevAsserter, prevFound = m[currentGID]
m[currentGID] = defAsserter[i]
})
}
currentGID = goid()
asserterMap.Tx(func(m map[int]asserter) {
prevAsserter, prevFound = m[currentGID]
m[currentGID] = defAsserter[i]
})

if prevFound {
return func() {
asserterMap.Set(currentGID, prevAsserter)
Expand Down Expand Up @@ -1124,13 +1175,6 @@ func newDefInd(v string) Asserter {
return ind
}

func combineArgs(format string, a []any) []any {
args := make([]any, 1, len(a)+1)
args[0] = format
args = append(args, a)
return args
}

func goid() int {
var buf [64]byte
runtime.Stack(buf[:], false)
Expand Down
Loading
Loading