diff --git a/.gitignore b/.gitignore index 912fe33..35c9adf 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,9 @@ *.vim* coverage.* +# AI +/.codex/ + # Binaries for programs and plugins *.exe *.exe~ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..f1a1919 --- /dev/null +++ b/AGENTS.md @@ -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. diff --git a/CHANGELOG.md b/CHANGELOG.md index 33e55e6..4aad705 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 82b7b02..7a8e0ef 100644 --- a/README.md +++ b/README.md @@ -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) @@ -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 @@ -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 ... ``` @@ -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 diff --git a/assert/assert.go b/assert/assert.go index 1df956b..e7fbf39 100644 --- a/assert/assert.go +++ b/assert/assert.go @@ -1,6 +1,7 @@ package assert import ( + "errors" "flag" "fmt" "os" @@ -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 @@ -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) } @@ -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. @@ -1049,15 +1098,22 @@ 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 @@ -1065,17 +1121,12 @@ func PushAsserter(i Asserter) (retFn function) { 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) @@ -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) diff --git a/assert/assert_test.go b/assert/assert_test.go index f3cd074..c164a7f 100644 --- a/assert/assert_test.go +++ b/assert/assert_test.go @@ -1,6 +1,7 @@ package assert_test // Note!! Some tests here are related to line # of the file import ( + "errors" "fmt" "os" "testing" @@ -18,7 +19,7 @@ func ExampleThat() { } err := sample() fmt.Printf("%v", err) - // Output: testing: run example: assert_test.go:16: ExampleThat.func1(): assertion failure: optional message + // Output: testing: run example: assert_test.go:17: ExampleThat.func1(): assertion failure: optional message } func ExampleNotNil() { @@ -32,7 +33,7 @@ func ExampleNotNil() { var b *byte err := sample(b) fmt.Printf("%v", err) - // Output: sample: assert_test.go:29: ExampleNotNil.func1(): assertion failure: pointer should not be nil + // Output: sample: assert_test.go:30: ExampleNotNil.func1(): assertion failure: pointer should not be nil } func ExampleMNotNil() { @@ -47,7 +48,7 @@ func ExampleMNotNil() { var b map[string]byte err := sample(b) fmt.Printf("%v", err) - // Output: sample: assert_test.go:44: ExampleMNotNil.func1(): assertion failure: map should not be nil + // Output: sample: assert_test.go:45: ExampleMNotNil.func1(): assertion failure: map should not be nil } func ExampleCNotNil() { @@ -61,7 +62,7 @@ func ExampleCNotNil() { var c chan byte err := sample(c) fmt.Printf("%v", err) - // Output: sample: assert_test.go:58: ExampleCNotNil.func1(): assertion failure: channel should not be nil + // Output: sample: assert_test.go:59: ExampleCNotNil.func1(): assertion failure: channel should not be nil } func ExampleSNotNil() { @@ -76,7 +77,7 @@ func ExampleSNotNil() { var b []byte err := sample(b) fmt.Printf("%v", err) - // Output: sample: assert_test.go:73: ExampleSNotNil.func1(): assertion failure: slice should not be nil + // Output: sample: assert_test.go:74: ExampleSNotNil.func1(): assertion failure: slice should not be nil } func ExampleEqual() { @@ -89,7 +90,7 @@ func ExampleEqual() { } err := sample([]byte{1, 2}) fmt.Printf("%v", err) - // Output: sample: assert_test.go:87: ExampleEqual.func1(): assertion failure: equal: got '2', want '1' + // Output: sample: assert_test.go:88: ExampleEqual.func1(): assertion failure: equal: got '2', want '1' } func ExampleSLen() { @@ -101,7 +102,7 @@ func ExampleSLen() { } err := sample([]byte{1, 2}) fmt.Printf("%v", err) - // Output: sample: assert_test.go:99: ExampleSLen.func1(): assertion failure: length: got '2', want '3' + // Output: sample: assert_test.go:100: ExampleSLen.func1(): assertion failure: length: got '2', want '3' } func ExampleSNotEmpty() { @@ -113,7 +114,7 @@ func ExampleSNotEmpty() { } err := sample([]byte{}) fmt.Printf("%v", err) - // Output: sample: assert_test.go:111: ExampleSNotEmpty.func1(): assertion failure: slice should not be empty + // Output: sample: assert_test.go:112: ExampleSNotEmpty.func1(): assertion failure: slice should not be empty } func ExampleNotEmpty() { @@ -126,7 +127,7 @@ func ExampleNotEmpty() { } err := sample("") fmt.Printf("%v", err) - // Output: sample: assert_test.go:124: ExampleNotEmpty.func1(): assertion failure: string should not be empty + // Output: sample: assert_test.go:125: ExampleNotEmpty.func1(): assertion failure: string should not be empty } func ExampleMKeyExists() { @@ -143,7 +144,7 @@ func ExampleMKeyExists() { } err := sample("2") fmt.Printf("%v", err) - // Output: sample: assert_test.go:141: ExampleMKeyExists.func1(): assertion failure: key '2' doesn't exist + // Output: sample: assert_test.go:142: ExampleMKeyExists.func1(): assertion failure: key '2' doesn't exist } func ExampleZero() { @@ -156,7 +157,7 @@ func ExampleZero() { var b int8 = 1 // we want sample to assert the violation. err := sample(b) fmt.Printf("%v", err) - // Output: sample: assert_test.go:153: ExampleZero.func1(): assertion failure: got '1', want (== '0') + // Output: sample: assert_test.go:154: ExampleZero.func1(): assertion failure: got '1', want (== '0') } func ExampleSLonger() { @@ -169,7 +170,7 @@ func ExampleSLonger() { } err := sample([]byte{01}) // len = 1 fmt.Printf("%v", err) - // Output: sample: assert_test.go:167: ExampleSLonger.func1(): assertion failure: got '1', should be longer than '1' + // Output: sample: assert_test.go:168: ExampleSLonger.func1(): assertion failure: got '1', should be longer than '1' } func ExampleMShorter() { @@ -183,7 +184,7 @@ func ExampleMShorter() { } err := sample(map[byte]byte{01: 01}) // len = 1 fmt.Printf("%v", err) - // Output: sample: assert_test.go:180: ExampleMShorter.func1(): assertion failure: got '1', should be shorter than '1' + // Output: sample: assert_test.go:181: ExampleMShorter.func1(): assertion failure: got '1', should be shorter than '1' } func ExampleSShorter() { @@ -196,7 +197,7 @@ func ExampleSShorter() { } err := sample([]byte{01}) // len = 1 fmt.Printf("%v", err) - // Output: sample: assert_test.go:194: ExampleSShorter.func1(): assertion failure: got '1', should be shorter than '0': optional message (test_str) + // Output: sample: assert_test.go:195: ExampleSShorter.func1(): assertion failure: got '1', should be shorter than '0': optional message (test_str) } func ExampleLess() { @@ -211,7 +212,7 @@ func ExampleLess() { var b int8 = 1 err := sample(b) fmt.Printf("%v", err) - // Output: sample: assert_test.go:208: ExampleLess.func1(): assertion failure: got '1', want >= '1' + // Output: sample: assert_test.go:209: ExampleLess.func1(): assertion failure: got '1', want >= '1' } func ExampleGreater() { @@ -226,7 +227,7 @@ func ExampleGreater() { var b int8 = 2 err := sample(b) fmt.Printf("%v", err) - // Output: sample: assert_test.go:223: ExampleGreater.func1(): assertion failure: got '2', want <= '2' + // Output: sample: assert_test.go:224: ExampleGreater.func1(): assertion failure: got '2', want <= '2' } func ExampleNotZero() { @@ -239,7 +240,7 @@ func ExampleNotZero() { var b int8 err := sample(b) fmt.Printf("%v", err) - // Output: sample: assert_test.go:236: ExampleNotZero.func1(): assertion failure: got '0', want (!= 0) + // Output: sample: assert_test.go:237: ExampleNotZero.func1(): assertion failure: got '0', want (!= 0) } func ExampleMLen() { @@ -253,7 +254,7 @@ func ExampleMLen() { } err := sample(map[int]byte{1: 1, 2: 2}) fmt.Printf("%v", err) - // Output: sample: assert_test.go:251: ExampleMLen.func1(): assertion failure: length: got '2', want '3' + // Output: sample: assert_test.go:252: ExampleMLen.func1(): assertion failure: length: got '2', want '3' } func ExampleCLen() { @@ -270,7 +271,7 @@ func ExampleCLen() { d <- int(1) err := sample(d) fmt.Printf("%v", err) - // Output: sample: assert_test.go:265: ExampleCLen.func1(): assertion failure: length: got '2', want '3' + // Output: sample: assert_test.go:266: ExampleCLen.func1(): assertion failure: length: got '2', want '3' } func ExampleThatNot() { @@ -300,7 +301,7 @@ func ExampleINotNil() { var b = fmt.Errorf("test") err := sample(b) fmt.Printf("%v", err) - // Output: sample: assert_test.go:297: ExampleINotNil.func1(): assertion failure: interface should be nil + // Output: sample: assert_test.go:298: ExampleINotNil.func1(): assertion failure: interface should be nil } func ExampleLen() { @@ -314,7 +315,7 @@ func ExampleLen() { } err := sample("12") fmt.Printf("%v", err) - // Output: sample: assert_test.go:312: ExampleLen.func1(): assertion failure: length: got '2', want '3' + // Output: sample: assert_test.go:313: ExampleLen.func1(): assertion failure: length: got '2', want '3' } func ExampleDeepEqual() { @@ -328,7 +329,7 @@ func ExampleDeepEqual() { } err := sample([]byte{1, 2}) fmt.Printf("%v", err) - // Output: sample: assert_test.go:326: ExampleDeepEqual.func1(): assertion failure: got '2', want '3' + // Output: sample: assert_test.go:327: ExampleDeepEqual.func1(): assertion failure: got '2', want '3' } func ExampleError() { @@ -342,7 +343,7 @@ func ExampleError() { var b = fmt.Errorf("test") err := sample(b) fmt.Printf("%v", err) - // Output: sample: assert_test.go:339: ExampleError.func1(): assertion failure: test + // Output: sample: assert_test.go:340: ExampleError.func1(): assertion failure: test } func ExampleNotImplemented() { @@ -355,7 +356,91 @@ func ExampleNotImplemented() { var b = fmt.Errorf("test") err := sample(b) fmt.Printf("%v", err) - // Output: sample: assert_test.go:352: ExampleNotImplemented.func1(): assertion failure: not implemented + // Output: sample: assert_test.go:353: ExampleNotImplemented.func1(): assertion failure: not implemented +} + +func ExampleMKeyNotExists() { + sample := func(b string) (err error) { + defer err2.Handle(&err, "sample") + + m := map[string]string{ + "1": "one", + } + assert.MKeyNotExists(m, b) // Doesn't fail: ∄ b ∈ m | b=2 + assert.MKeyNotExists(m, "1") // Fails: ∃ 1 ∈ m + return err + } + err := sample("2") + fmt.Printf("%v", err) + // Output: sample: assert_test.go:370: ExampleMKeyNotExists.func1(): assertion failure: key '1' shouldn't exist +} + +func ExampleThat_sentinelErrorWithIs() { + var ErrArgumentsNeeded = errors.New("arguments missing") + + sample := func(a ...int) (err error) { + // [err2.Handle] default is automatic error annotation, caller needs + // to use [errors.Is] to check error values returned. + defer err2.Handle(&err) + + // we can use asserts to return actual error values + assert.That(len(a) != 0, ErrArgumentsNeeded) + return err + } + err := sample() + // because sample function annotates error values we must use [errors.Is] + if errors.Is(err, ErrArgumentsNeeded) { + // err is wrapped (annotated) by [err2.Handle] + fmt.Printf("ERR: %v", err) + } else { + fmt.Print("never here!", err) + } + // Output: ERR: testing: run example: assert_test.go:387: ExampleThat_sentinelErrorWithIs.func1(): assertion failure: arguments missing +} + +func ExampleThat_sentinelErrorWithValueComparison() { + var ErrArgumentsNeeded = errors.New("arguments missing") + + sample := func(a ...int) (err error) { + // prevent assert pkg's to add extra info to err values + defer assert.PushAsserter(assert.Plain)() + // also remove automatic error annotation with nil argument + defer err2.Handle(&err, nil) + + // we can use asserts to return actual error values + assert.That(len(a) != 0, ErrArgumentsNeeded) + return err + } + + // sample function is documented to return just plain error values + err := sample() + + if err == ErrArgumentsNeeded { + fmt.Printf("ERR: %v", err) + } else { + fmt.Print("never here!") + } + // Output: ERR: arguments missing +} + +func ExampleEqual_sentinel() { + var ErrNotEqual = errors.New("different values") + + sample := func(b []byte) (err error) { + defer assert.PushAsserter(assert.Plain)() + + defer err2.Handle(&err, "sample") + + assert.NotEqual(b[0], 3) // OK, b[0] != 3; (b[0] == 1) + + // Note that only ErrNotEqual value is used, rest of the args are + // ignored. + assert.Equal(b[1], 1, ErrNotEqual, "%d", 1) // Not OK, b[1] == 2 + return err + } + err := sample([]byte{1, 2}) + fmt.Printf("%v", err) + // Output: sample: different values } func BenchmarkMKeyExists(b *testing.B) { @@ -535,6 +620,12 @@ func BenchmarkError(b *testing.B) { } } +func BenchmarkErrorIs(b *testing.B) { + err := err2.ErrNotAccess + for n := 0; n < b.N; n++ { + assert.ErrorIs(err, err2.ErrNotAccess) + } +} func BenchmarkEqual(b *testing.B) { for n := 0; n < b.N; n++ { assert.Equal(n, n) @@ -662,3 +753,20 @@ func setUp() { } func tearDown() {} + +func TestPushAsserter(t *testing.T) { + sentinel := errors.New("sentinel") + + functionReturningSentinel := func() (err error) { + defer err2.Handle(&err, nil /* <- no error annotations */) + + assert.That(false, sentinel) // i.e., `sentinel` is returned + return nil + } + + defer assert.PushTester(t, assert.Production)() // Examples need Production + defer assert.PushAsserter(assert.Plain)() // We want sentinel + + err := functionReturningSentinel() + assert.That(err == sentinel) // we can use ==; aren't forced to errors.Is +} diff --git a/assert/asserter.go b/assert/asserter.go index 7edccda..228c8d5 100644 --- a/assert/asserter.go +++ b/assert/asserter.go @@ -48,22 +48,30 @@ const ( const officialTestOutputPrefix = " " // reportAssertionFault reports assertion fault according the current asserter -// and its config. If extra argumnets are given (a ...any) and the first is +// and its config. If extra arguments are given (a ...any) and the first is // string, it's treated as format string and following args as its parameters. // -// Note. We use the pattern where we build defaultMsg argument reaady in cases +// Note. We use the pattern where we build defaultMsg argument ready in cases // like 'got: X, want: Y'. This hits two birds with one stone: we have automatic // and correct assert messages, and we can add information to it if we want to. // If asserter is Plain (isErrorOnly()) user wants to override automatic assert -// messgages with our given, usually simple message. +// messages with our given, usually simple message. func (asserter asserter) reportAssertionFault( extraInd int, defaultMsg string, a []any, ) { - if asserter.hasStackTrace() { + var ( + optionalArgsNotEmpty = len(a) > 0 + err error + firstIsError bool + ) + if optionalArgsNotEmpty { + err, firstIsError = a[0].(error) + } + if !firstIsError && asserter.hasStackTrace() { if asserter.isUnitTesting() { - // Note. that the assert in the test function is printed in + // Note. That the assert in the test function is printed in // reportPanic below const StackLvl = 5 // amount of functions before we're here stackLvl := StackLvl + extraInd @@ -79,8 +87,23 @@ func (asserter asserter) reportAssertionFault( if asserter.hasCallerInfo() { defaultMsg = asserter.callerInfo(defaultMsg, extraInd) } - if len(a) > 0 { - if format, ok := a[0].(string); ok { + if optionalArgsNotEmpty { + if firstIsError { + // The first given argument's type is error. We treat it as a + // sentinel error value and let our catching part (err2.Handle) + // to report it as-it-is. + // ==> Asserts can be used for error reporting. + // NOTE! If our package is allowed to annotate error value we do + // it with wrapping, which means that [errors.Is] must be used! + newErr := err + allowDefMsg := !asserter.isErrorOnly() && defaultMsg != "" + if allowDefMsg { + newErr = fmt.Errorf("%s: %w", defaultMsg, err) + } + // we don't want to use unit test's error reporting, and that's + // why cannot use asserter.reportPanic call here. + panic(newErr) + } else if format, ok := a[0].(string); ok { allowDefMsg := !asserter.isErrorOnly() && defaultMsg != "" f := x.Whom(allowDefMsg, defaultMsg+conCatErrStr+format, format) asserter.reportPanic(fmt.Sprintf(f, a[1:]...)) diff --git a/assert/doc.go b/assert/doc.go index b026f2f..83f1b50 100644 --- a/assert/doc.go +++ b/assert/doc.go @@ -32,6 +32,33 @@ deep the recursion is or if parallel test runs are performed. The failure report includes all the locations of the meaningful call stack steps. See the next chapter. +# Use Asserts To Return Sentinel Error Values + +We saw how same assertions can be used for app running and for automatic tests. +There is a third way. If your function needs to report error values and you want +to make your code easier to skim, i.e, less if statements, you can use asserts. + +The following code block illustrates the behavior: + + func (p Path) ProveWithGetBKID(getBKID getBackupKey) (err error) { + defer err2.Handle(&err) + + assert.Equal(p.FirstEdge().Body.Version, EdgeVersion, ErrUnsupportedVersion) + assert.Equal(p.FirstEdge().Body.Prev, AnchorDigest(), ErrWrongAnchor) + +Now the caller can check the actual error values and decide what to do with a +specific error. + +Note that if you want to support different asserter types you should guide your +function callers to use [errors.Is] for error value checking. In cases where if +statements are preferred it's good to prefix your functions like this: + + func (p Path) ProveWithGetBKID(getBKID getBackupKey) (err error) { + defer assert.PushAsserter(assert.Plain)() // no assert annotations + defer err2.Handle(&err, nil) // no error annotations + +More information can be found from [assert.That] examples. + # Call Stack Traversal During Tests The Assert package allows us to track assertion violations over the package and