diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d8507a..92e71e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ ### Version history +##### 1.1.0 +- `assert` package: + - bug fix: call stack traversal during unit testing in some situations + - **all generics-based functions are inline expansed** + - *performance* is now *same as if-statements for all functions* + - new assert functions: `MNil`, `CNil`, `Less`, `Greater`, etc. + - all assert messages follow Go idiom: `got, want` + - `Asserter` can be set per goroutine: `PushAsserter` +- `try` package: + - new check functions: `T`, `T1`, `T2`, `T3`, for quick refactoring from `To` functions to annotate an error locally + - **all functions are inline expansed**: if-statement equal performance + ##### 1.0.0 - **Finally! We are very happy, and thanks to all who have helped!** - Lots of documentation updates and cleanups for version 1.0.0 diff --git a/README.md b/README.md index 0565455..628b141 100644 --- a/README.md +++ b/README.md @@ -103,7 +103,8 @@ tolerant*. And, of course, it helps to make your code error-safe. handler. 3. It helps us use design-by-contract type preconditions. 4. It offers automatic stack tracing for every error, runtime error, or panic. - If you are familiar with Zig, the `err2` error traces are same as Zig's. + If you are familiar with Zig, the `err2` error return traces are same as + Zig's. You can use all of them or just the other. However, if you use `try` for error checks, you must remember to use Go's `recover()` by yourself, or your error @@ -152,7 +153,11 @@ own purposes. #### Error Stack Tracing -The err2 offers optional stack tracing. It's *automatic* and *optimized*. +The err2 offers optional stack tracing in two different formats: +1. Optimized call stacks (`-err2-trace`) +1. Error return traces similar to Zig (`-err2-ret-trace`) + +Both are *automatic* and fully *optimized*.
The example of the optimized call stack: @@ -177,12 +182,14 @@ main.main()
-Just set the `err2.SetErrorTracer` or `err2.SetPanicTracer` to the stream you -want traces to be written: +Just set the `err2.SetErrorTracer`, `err2.SetErrRetTracer` or +`err2.SetPanicTracer` to the stream you want traces to be written: ```go err2.SetErrorTracer(os.Stderr) // write error stack trace to stderr // or, for example: +err2.SetErrRetTracer(os.Stderr) // write error return trace (like Zig) +// or, for example: err2.SetPanicTracer(log.Writer()) // stack panic trace to std logger ``` @@ -382,12 +389,12 @@ func TestWebOfTrustInfo(t *testing.T) { // And if there's violations during the test run they are reported as // test failures for this TestWebOfTrustInfo -test. - assert.Equal(0, wot.CommonInvider) - assert.Equal(1, wot.Hops) + assert.Equal(wot.CommonInvider, 0) + assert.Equal(wot.Hops, 1) wot = NewWebOfTrust(bob.Node, carol.Node) - assert.Equal(-1, wot.CommonInvider) - assert.Equal(-1, wot.Hops) + assert.Equal(wot.CommonInvider, hop.NotConnected) + assert.Equal(wot.Hops, hop.NotConnected) ... ``` @@ -592,6 +599,15 @@ been much easier.** There is an excellent [blog post](https://jesseduffield.com/ about the issues you are facing with Go's error handling without the help of the err2 package. +- If you don't want to bubble up error from every function, we have learned that +`Try` prefix convention is pretty cool way to solve limitations of Go +programming language help to make your code more skimmable. If your internal +functions normally would be something like `func CopyFile(s, t string) (err +error)`, you can replace them with `func TryCopyFile(s, t string)`, where `Try` +prefix remind you that the function throws errors. You can decide at what level +of the call stack you will catch them with `err2.Handle` or `err2.Catch`, +depending your case and API. + ## Support And Contributions @@ -607,14 +623,10 @@ Please see the full version history from [CHANGELOG](./CHANGELOG.md). ### Latest Release -##### 1.1.0 -- `assert` package: - - bug fix: call stack traversal during unit testing in some situations - - **all generics-based functions are inline expansed** - - *performance* is now *same as if-statements for all functions* - - new assert functions: `MNil`, `CNil`, `Less`, `Greater`, etc. - - all assert messages follow Go idiom: `got, want` - - `Asserter` can be set per goroutine: `PushAsserter` -- `try` package: - - new check functions: `T`, `T1`, `T2`, `T3`, for quick refactoring from `To` functions to annotate an error locally - - **all functions are inline expansed**: if-statement equal performance +##### 1.2.0 +- Now `-err2-ret-trace` and `err2.SetErrRetTracer` gives us *error return traces* + which are even more readable than `-err2-trace`, `err2.SetErrorTracer` with + long error return traces +- A new automatic error formatter/generator added for `TryCopyFile` convention +- New features for `sample/` to demonstrate latest features +- Extended documentation diff --git a/assert/assert.go b/assert/assert.go index b33c653..13b78f1 100644 --- a/assert/assert.go +++ b/assert/assert.go @@ -1021,24 +1021,17 @@ func current() (curAsserter asserter) { return curAsserter } -// SetDefault sets the current default [Asserter] for assert pkg. It also -// returns the previous [Asserter]. +// SetDefault sets the new default [Asserter] for the assert pkg instance you're +// currently using. It also returns the previous [Asserter]. The default +// asserter is [Production] that's best for most use cases and packages. // -// Note that you should use this in TestMain function, and use [flag] package to -// set it for the app. For the tests you can set it to panic about every -// assertion fault, or to throw an error, or/and print the call stack -// immediately when assert occurs. The err2 package helps you to catch and -// report all types of the asserts. +// Note that for most cases [PushAsserter] is most suitable. [PushAsserter] +// allows you to set [Asserter] per goroutine or function, i.e., until you pop +// the asserter out, and the assert package uses the current default [Asserter] +// set by [SetDefault]. // -// Note that if you are using tracers you might get two call stacks, so test -// what's best for your case. -// -// Tip. If our own packages (client packages for assert) have lots of parallel -// testing and race detection, please try to use same [Asserter] for all of them -// and set [Asserter] only one in TestMain, or in init. -// -// func TestMain(m *testing.M) { -// SetDefault(assert.TestFull) +// Note that if you are using tracers you might get overlapping call stacks, so +// test what's best for your case. func SetDefault(i Asserter) (old Asserter) { // pkg lvl lock to allow only one pkg client call this at one of the time // together with the indexing, i.e we don't need to switch asserter diff --git a/doc.go b/doc.go index 7d7b007..9119fdd 100644 --- a/doc.go +++ b/doc.go @@ -1,6 +1,6 @@ /* -Package err2 provides three main functionality: - 1. err2 package includes helper functions for error handling & automatic error +Package err2 is error handling solution including three main functionality: + 1. err2 package offers helper functions for error handling & automatic error stack tracing 2. [github.com/lainio/err2/try] sub-package is for error checking 3. [github.com/lainio/err2/assert] sub-package is for design-by-contract and diff --git a/err2_test.go b/err2_test.go index 61c91fa..09dc69a 100644 --- a/err2_test.go +++ b/err2_test.go @@ -9,7 +9,7 @@ import ( "testing" "github.com/lainio/err2" - "github.com/lainio/err2/internal/require" + "github.com/lainio/err2/internal/except" "github.com/lainio/err2/try" ) @@ -70,7 +70,7 @@ func TestHandle_noerrHandler(t *testing.T) { var err error var handlerCalled bool defer func() { - require.That(t, handlerCalled) + except.That(t, handlerCalled) }() // This is the handler we are testing! defer err2.Handle(&err, func(noerr bool) { @@ -88,12 +88,12 @@ func TestHandle_noerrHandler(t *testing.T) { var err error var handlerCalled bool defer func() { - require.That(t, handlerCalled) + except.That(t, handlerCalled) }() defer err2.Handle(&err, func(err error) error { // this should not be called, so lets try to fuckup things... handlerCalled = false - require.That(t, false) + except.That(t, false) return err }) @@ -111,20 +111,20 @@ func TestHandle_noerrHandler(t *testing.T) { var err error var handlerCalled bool defer func() { - require.ThatNot(t, handlerCalled) + except.ThatNot(t, handlerCalled) }() // This is the handler we are testing! defer err2.Handle(&err, func(err error) error { - require.ThatNot(t, handlerCalled) + except.ThatNot(t, handlerCalled) handlerCalled = false - require.That(t, true, "error should be handled") + except.That(t, true, "error should be handled") return err }) // This is the handler we are testing! AND it's not called in error. defer err2.Handle(&err, func(bool) { - require.That(t, false, "when error this is not called") + except.That(t, false, "when error this is not called") }) try.To1(throw()) @@ -141,14 +141,14 @@ func TestHandle_noerrHandler(t *testing.T) { callCount int ) defer func() { - require.ThatNot(t, handlerCalled) - require.Equal(t, callCount, 2) - require.Equal(t, err.Error(), finalAnnotatedErr.Error()) + except.ThatNot(t, handlerCalled) + except.Equal(t, callCount, 2) + except.Equal(t, err.Error(), finalAnnotatedErr.Error()) }() // This is the handler we are testing! AND it's not called in error. defer err2.Handle(&err, func(noerr bool) { - require.That( + except.That( t, false, "if error occurs/reset, this cannot happen", @@ -159,18 +159,18 @@ func TestHandle_noerrHandler(t *testing.T) { // important! test that our handler doesn't change the current error // and it's not nil defer err2.Handle(&err, func(er error) error { - require.That(t, er != nil, "er val: ", er, err) - require.Equal(t, callCount, 1, "this is called in sencond") + except.That(t, er != nil, "er val: ", er, err) + except.Equal(t, callCount, 1, "this is called in sencond") callCount++ return er }) defer err2.Handle(&err, func(err error) error { // this should not be called, so lets try to fuckup things... - require.Equal(t, callCount, 0, "this is called in first") + except.Equal(t, callCount, 0, "this is called in first") callCount++ handlerCalled = false - require.That(t, err != nil) + except.That(t, err != nil) return finalAnnotatedErr }) try.To1(throw()) @@ -182,17 +182,17 @@ func TestHandle_noerrHandler(t *testing.T) { var err error var handlerCalled bool defer func() { - require.That(t, handlerCalled) + except.That(t, handlerCalled) }() // This is the handler we are testing! defer err2.Handle(&err, func(noerr bool) { - require.That(t, noerr) + except.That(t, noerr) handlerCalled = noerr }) defer err2.Handle(&err, func(err error) error { - require.That(t, false, "no error to handle!") + except.That(t, false, "no error to handle!") // this should not be called, so lets try to fuckup things... handlerCalled = false // see first deferred function return err @@ -207,27 +207,27 @@ func TestHandle_noerrHandler(t *testing.T) { var err error var handlerCalled bool defer func() { - require.That(t, handlerCalled) + except.That(t, handlerCalled) }() // This is the handler we are testing! defer err2.Handle(&err, func(noerr bool) { - require.That(t, true) - require.That(t, noerr) + except.That(t, true) + except.That(t, noerr) handlerCalled = noerr }) defer err2.Handle(&err) defer err2.Handle(&err, func(err error) error { - require.That(t, false, "no error to handle!") + except.That(t, false, "no error to handle!") // this should not be called, so lets try to fuckup things... handlerCalled = false // see first deferred function return err }) defer err2.Handle(&err, func(err error) error { - require.That(t, false, "no error to handle!") + except.That(t, false, "no error to handle!") // this should not be called, so lets try to fuckup things... handlerCalled = false // see first deferred function return err @@ -243,20 +243,20 @@ func TestHandle_noerrHandler(t *testing.T) { var err error var noerrHandlerCalled, errHandlerCalled bool defer func() { - require.That(t, noerrHandlerCalled) - require.That(t, errHandlerCalled) + except.That(t, noerrHandlerCalled) + except.That(t, errHandlerCalled) }() // This is the handler we are testing! defer err2.Handle(&err, func(noerr bool) { - require.That(t, true) // we are here, for debugging - require.That(t, noerr) + except.That(t, true) // we are here, for debugging + except.That(t, noerr) noerrHandlerCalled = noerr }) // this is the err handler that -- RESETS -- the error to nil defer err2.Handle(&err, func(err error) error { - require.That(t, err != nil) // helps fast debugging + except.That(t, err != nil) // helps fast debugging // this should not be called, so lets try to fuckup things... noerrHandlerCalled = false // see first deferred function @@ -266,7 +266,7 @@ func TestHandle_noerrHandler(t *testing.T) { }) defer err2.Handle(&err, func(err error) error { - require.That(t, err != nil) // helps fast debugging + except.That(t, err != nil) // helps fast debugging // this should not be called, so lets try to fuckup things... noerrHandlerCalled = false // see first deferred function @@ -284,14 +284,14 @@ func TestHandle_noerrHandler(t *testing.T) { var err error var handlerCalled bool defer func() { - require.That(t, handlerCalled) + except.That(t, handlerCalled) }() defer err2.Handle(&err) defer err2.Handle(&err) defer err2.Handle(&err, func(err error) error { - require.That(t, false, "no error to handle!") + except.That(t, false, "no error to handle!") // this should not be called, so lets try to fuckup things... handlerCalled = false // see first deferred function return err @@ -299,13 +299,13 @@ func TestHandle_noerrHandler(t *testing.T) { // This is the handler we are testing! defer err2.Handle(&err, func(noerr bool) { - require.That(t, true, "this must be called") - require.That(t, noerr) + except.That(t, true, "this must be called") + except.That(t, noerr) handlerCalled = noerr }) defer err2.Handle(&err, func(err error) error { - require.That(t, false, "no error to handle!") + except.That(t, false, "no error to handle!") // this should not be called, so lets try to fuckup things... handlerCalled = false // see first deferred function return err @@ -376,7 +376,7 @@ func TestPanickingCatchAll(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() defer func() { - require.That( + except.That( t, recover() == nil, "panics should NOT carry on", @@ -424,7 +424,7 @@ func TestPanickingCarryOn_Handle(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() defer func() { - require.That( + except.That( t, recover() != nil, "panics should went thru when not our errors", @@ -554,12 +554,12 @@ func TestPanicking_Handle(t *testing.T) { defer func() { r := recover() if tt.wants == nil { - require.That(t, r != nil, "wants err, then panic") + except.That(t, r != nil, "wants err, then panic") } }() err := tt.args.f() if err != nil { - require.Equal(t, err.Error(), tt.wants.Error()) + except.Equal(t, err.Error(), tt.wants.Error()) } }) } @@ -600,7 +600,7 @@ func TestPanicking_Catch(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() defer func() { - require.That( + except.That( t, recover() == nil, "panics should NOT carry on", @@ -623,7 +623,7 @@ func TestCatch_Error(t *testing.T) { func Test_TryOutError(t *testing.T) { t.Parallel() defer err2.Catch(func(err error) error { - require.Equal(t, err.Error(), "fails: test: this is an ERROR", + except.Equal(t, err.Error(), "fails: test: this is an ERROR", "=> we should catch right error str here") return err }) @@ -633,7 +633,7 @@ func Test_TryOutError(t *testing.T) { // let's test try.Out1() and it's throw capabilities here, even try.To1() // is the preferred way. retVal = try.Out1(noThrow()).Handle().Val1 - require.Equal(t, retVal, "test", "if no error happens, we get value") + except.Equal(t, retVal, "test", "if no error happens, we get value") _ = try.Out1(throw()).Handle("fails: %v", retVal).Val1 t.Fail() // If everything works in Handle we are never here. @@ -665,11 +665,21 @@ func TestCatch_Panic(t *testing.T) { func TestSetErrorTracer(t *testing.T) { t.Parallel() w := err2.ErrorTracer() - require.That(t, w == nil, "error tracer should be nil") + except.That(t, w == nil, "error tracer should be nil") var w1 io.Writer err2.SetErrorTracer(w1) w = err2.ErrorTracer() - require.That(t, w == nil, "error tracer should be nil") + except.That(t, w == nil, "error tracer should be nil") +} + +func TestSetErrRetTracer(t *testing.T) { + t.Parallel() + w := err2.ErrRetTracer() + except.That(t, w == nil, "error return tracer should be nil") + var w1 io.Writer + err2.SetErrRetTracer(w1) + w = err2.ErrRetTracer() + except.That(t, w == nil, "error return tracer should be nil") } func ExampleCatch_withFmt() { diff --git a/formatter/formatter.go b/formatter/formatter.go index 73ae476..db27913 100644 --- a/formatter/formatter.go +++ b/formatter/formatter.go @@ -24,8 +24,24 @@ type Formatter struct { DoFmt } -// Decamel is preimplemented and default formatter to produce human -// readable error strings from function names. +// DecamelAndRmTryPrefix is pre-implemented formatter to produce human readable +// error strings from function names. It's similar to [Decamel] but also removes +// try-prefixes from function names: +// +// func TryCopyFile(..) -> "copy file: file not exists" +// ^-------^ -> generated from 'func TryCopyFile' +// +// It's convenient helper for those who wants to write compact functions by +// following convention to always add 'Try' prefix to those functions that can +// throw errors thru panics. Fox example, if you're using helpers like +// [github.com/lainio/err2/assert.That] and [github.com/lainio/err2/try.To] but +// you don't want to handle errors in your current function, it's still good +// practice to use convention to mark that function to throw errors. However, we +// suggest that you don't do that in your packages public API functions. +var DecamelAndRmTryPrefix = &Formatter{DoFmt: str.DecamelRmTryPrefix} + +// Decamel is pre-implemented and default formatter to produce human readable +// error strings from function names. // // func CopyFile(..) -> "copy file: file not exists" // ^-------^ -> generated from 'func CopyFile' diff --git a/internal/debug/debug.go b/internal/debug/debug.go index 8185dbe..f3197d5 100644 --- a/internal/debug/debug.go +++ b/internal/debug/debug.go @@ -26,6 +26,8 @@ type StackInfo struct { // these are used to filter out specific lines from output ExlRegexp []*regexp.Regexp + + PrintFirstOnly bool } var ( @@ -39,14 +41,19 @@ var ( // we want to check that this is not our package packageRegexp = regexp.MustCompile( - `^github\.com/lainio/err2[a-zA-Z0-9_/.\[\]]*\(`, + `^github\.com/lainio/err2[a-zA-Z0-9_/\.\[\]\@]*\(`, ) // testing package exluding regexps: testingPkgRegexp = regexp.MustCompile(`^testing\.`) testingFileRegexp = regexp.MustCompile(`^.*\/src\/testing\/testing\.go`) - exludeRegexps = []*regexp.Regexp{testingPkgRegexp, testingFileRegexp} + exludeRegexps = []*regexp.Regexp{testingPkgRegexp, testingFileRegexp} + exludeRegexpsAll = []*regexp.Regexp{ + testingPkgRegexp, + testingFileRegexp, + packageRegexp, + } ) func (si StackInfo) fullName() string { @@ -89,7 +96,11 @@ func (si StackInfo) canPrint(s string, anchorLine, i int) (ok bool) { // printed from call stack. anchorLine = 0 } - ok = i >= 2*si.Level+anchorLine + if si.PrintFirstOnly { + ok = i >= 2*si.Level+anchorLine && i < 2*si.Level+anchorLine+2 + } else { + ok = i >= 2*si.Level+anchorLine + } if si.ExlRegexp == nil { return ok @@ -271,7 +282,7 @@ func stackPrint(r io.Reader, w io.Writer, si StackInfo) { line := scanner.Text() // we can print a line if we didn't find anything, i.e. anchorLine is - // nilAnchor, which means that our start is not limited by then anchor + // nilAnchor, which means that our start is not limited by the anchor canPrint := anchorLine == nilAnchor // if it's not nilAnchor we need to check it more carefully if !canPrint { diff --git a/internal/debug/debug_test.go b/internal/debug/debug_test.go index a6b1748..501458e 100644 --- a/internal/debug/debug_test.go +++ b/internal/debug/debug_test.go @@ -6,7 +6,7 @@ import ( "strings" "testing" - "github.com/lainio/err2/internal/require" + "github.com/lainio/err2/internal/except" ) func TestFullName(t *testing.T) { @@ -20,29 +20,29 @@ func TestFullName(t *testing.T) { retval string } tests := []ttest{ - {"all empty", args{StackInfo{"", "", 0, nil, nil}}, ""}, + {"all empty", args{StackInfo{"", "", 0, nil, nil, false}}, ""}, { "namespaces", - args{StackInfo{"lainio/err2", "", 0, nil, nil}}, + args{StackInfo{"lainio/err2", "", 0, nil, nil, false}}, "lainio/err2", }, { "both", - args{StackInfo{"lainio/err2", "try", 0, nil, nil}}, + args{StackInfo{"lainio/err2", "try", 0, nil, nil, false}}, "lainio/err2.try", }, { "short both", - args{StackInfo{"err2", "Handle", 0, nil, nil}}, + args{StackInfo{"err2", "Handle", 0, nil, nil, false}}, "err2.Handle", }, - {"func", args{StackInfo{"", "try", 0, nil, nil}}, "try"}, + {"func", args{StackInfo{"", "try", 0, nil, nil, false}}, "try"}, } for _, ttv := range tests { tt := ttv t.Run(tt.name, func(t *testing.T) { t.Parallel() - require.Equal(t, tt.retval, tt.fullName()) + except.Equal(t, tt.retval, tt.fullName()) }) } } @@ -61,40 +61,40 @@ func TestIsAnchor(t *testing.T) { tests := []ttest{ {"panic func and short regexp", args{ "github.com/lainio/err2.Return(0x14001c1ee20)", - StackInfo{"", "panic(", 0, PackageRegexp, nil}}, true}, + StackInfo{"", "panic(", 0, PackageRegexp, nil, false}}, true}, {"func hit and regexp on", args{ "github.com/lainioxx/err2_printStackIf({0x1545d2, 0x6}, 0x0, {0x12e3e0?, 0x188f50?})", - StackInfo{"", "printStackIf(", 0, noHitRegexp, nil}}, false}, + StackInfo{"", "printStackIf(", 0, noHitRegexp, nil, false}}, false}, {"short regexp no match", args{ "github.com/lainioxx/err2_printStackIf({0x1545d2, 0x6}, 0x0, {0x12e3e0?, 0x188f50?})", - StackInfo{"", "", 0, noHitRegexp, nil}}, false}, + StackInfo{"", "", 0, noHitRegexp, nil, false}}, false}, {"short regexp", args{ "github.com/lainio/err2/assert.That({0x1545d2, 0x6}, 0x0, {0x12e3e0?, 0x188f50?})", - StackInfo{"", "", 0, PackageRegexp, nil}}, true}, + StackInfo{"", "", 0, PackageRegexp, nil, false}}, true}, {"short", args{ "github.com/lainio/err2.printStackIf({0x1545d2, 0x6}, 0x0, {0x12e3e0?, 0x188f50?})", - StackInfo{"", "", 0, nil, nil}}, true}, + StackInfo{"", "", 0, nil, nil, false}}, true}, {"short-but-false", args{ "github.com/lainio/err2.printStackIf({0x1545d2, 0x6}, 0x0, {0x12e3e0?, 0x188f50?})", - StackInfo{"err2", "Handle", 0, nil, nil}}, false}, + StackInfo{"err2", "Handle", 0, nil, nil, false}}, false}, {"medium", args{ "github.com/lainio/err2.Returnw(0x40000b3e60, {0x0, 0x0}, {0x0, 0x0, 0x0})", - StackInfo{"err2", "Returnw", 0, nil, nil}}, true}, + StackInfo{"err2", "Returnw", 0, nil, nil, false}}, true}, {"medium-but-false", args{ "github.com/lainio/err2.Returnw(0x40000b3e60, {0x0, 0x0}, {0x0, 0x0, 0x0})", - StackInfo{"err2", "Return(", 0, nil, nil}}, false}, + StackInfo{"err2", "Return(", 0, nil, nil, false}}, false}, {"long", args{ "github.com/lainio/err2.Handle(0x40000b3ed8, 0x40000b3ef8)", - StackInfo{"err2", "Handle", 0, nil, nil}}, true}, + StackInfo{"err2", "Handle", 0, nil, nil, false}}, true}, {"package name only", args{ "github.com/lainio/err2/try.To1[...](...)", - StackInfo{"lainio/err2", "", 0, nil, nil}}, true}, + StackInfo{"lainio/err2", "", 0, nil, nil, false}}, true}, } for _, ttv := range tests { tt := ttv t.Run(tt.name, func(t *testing.T) { t.Parallel() - require.Equal(t, tt.retval, tt.isAnchor(tt.input)) + except.Equal(t, tt.retval, tt.isAnchor(tt.input)) }) } } @@ -113,34 +113,34 @@ func TestIsFuncAnchor(t *testing.T) { tests := []ttest{ {"func hit and regexp on", args{ "github.com/lainioxx/err2_printStackIf({0x1545d2, 0x6}, 0x0, {0x12e3e0?, 0x188f50?})", - StackInfo{"", "printStackIf(", 0, noHitRegexp, nil}}, true}, + StackInfo{"", "printStackIf(", 0, noHitRegexp, nil, false}}, true}, {"short regexp", args{ "github.com/lainio/err2/assert.That({0x1545d2, 0x6}, 0x0, {0x12e3e0?, 0x188f50?})", - StackInfo{"", "", 0, PackageRegexp, nil}}, true}, + StackInfo{"", "", 0, PackageRegexp, nil, false}}, true}, {"short", args{ "github.com/lainio/err2.printStackIf({0x1545d2, 0x6}, 0x0, {0x12e3e0?, 0x188f50?})", - StackInfo{"", "", 0, nil, nil}}, true}, + StackInfo{"", "", 0, nil, nil, false}}, true}, {"short-but-false", args{ "github.com/lainio/err2.printStackIf({0x1545d2, 0x6}, 0x0, {0x12e3e0?, 0x188f50?})", - StackInfo{"err2", "Handle", 0, nil, nil}}, false}, + StackInfo{"err2", "Handle", 0, nil, nil, false}}, false}, {"medium", args{ "github.com/lainio/err2.Returnw(0x40000b3e60, {0x0, 0x0}, {0x0, 0x0, 0x0})", - StackInfo{"err2", "Returnw", 0, nil, nil}}, true}, + StackInfo{"err2", "Returnw", 0, nil, nil, false}}, true}, {"medium-but-false", args{ "github.com/lainio/err2.Returnw(0x40000b3e60, {0x0, 0x0}, {0x0, 0x0, 0x0})", - StackInfo{"err2", "Return(", 0, nil, nil}}, false}, + StackInfo{"err2", "Return(", 0, nil, nil, false}}, false}, {"long", args{ "github.com/lainio/err2.Handle(0x40000b3ed8, 0x40000b3ef8)", - StackInfo{"err2", "Handle", 0, nil, nil}}, true}, + StackInfo{"err2", "Handle", 0, nil, nil, false}}, true}, {"package name only", args{ "github.com/lainio/err2/try.To1[...](...)", - StackInfo{"lainio/err2", "", 0, nil, nil}}, true}, + StackInfo{"lainio/err2", "", 0, nil, nil, false}}, true}, } for _, ttv := range tests { tt := ttv t.Run(tt.name, func(t *testing.T) { t.Parallel() - require.Equal(t, tt.retval, tt.isFuncAnchor(tt.input)) + except.Equal(t, tt.retval, tt.isFuncAnchor(tt.input)) }) } } @@ -161,7 +161,7 @@ func TestFnLNro(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() output := fnLNro(tt.input) - require.Equal(t, output, tt.output) + except.Equal(t, output, tt.output) }) } } @@ -212,7 +212,7 @@ func TestFnName(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() output := fnName(tt.input) - require.Equal(t, output, tt.output) + except.Equal(t, output, tt.output) }) } } @@ -239,7 +239,7 @@ func TestStackPrint_noLimits(t *testing.T) { FuncName: "", Level: 0, }) - require.Equal(t, tt.input, w.String()) + except.Equal(t, tt.input, w.String()) }) } } @@ -267,8 +267,8 @@ func TestStackPrintForTest(t *testing.T) { // print(tt.output) // println("------") // print(w.String()) - require.Equal(t, a, b) - require.Equal(t, tt.output, w.String()) + except.Equal(t, a, b) + except.Equal(t, tt.output, w.String()) }) } } @@ -285,50 +285,58 @@ func TestCalcAnchor(t *testing.T) { anchor int } tests := []ttest{ + { + "macOS from test using ALL regexp", + args{ + inputFromMac, + StackInfo{"", "StartPSM(", 1, nil, exludeRegexpsAll, false}, + }, + 16, + }, { "macOS from test using regexp", args{ inputFromMac, - StackInfo{"", "panic(", 1, PackageRegexp, nil}, + StackInfo{"", "panic(", 1, PackageRegexp, nil, false}, }, 12, }, - {"short", args{input, StackInfo{"", "panic(", 0, nil, nil}}, 6}, + {"short", args{input, StackInfo{"", "panic(", 0, nil, nil, false}}, 6}, { "short error stack", args{ inputByError, - StackInfo{"", "panic(", 0, PackageRegexp, nil}, + StackInfo{"", "panic(", 0, PackageRegexp, nil, false}, }, 4, }, { "short and nolimit", - args{input, StackInfo{"", "", 0, nil, nil}}, + args{input, StackInfo{"", "", 0, nil, nil, false}}, nilAnchor, }, { "short and only LVL is 2", - args{input, StackInfo{"", "", 2, nil, nil}}, + args{input, StackInfo{"", "", 2, nil, nil, false}}, 2, }, - {"medium", args{input1, StackInfo{"", "panic(", 0, nil, nil}}, 10}, + {"medium", args{input1, StackInfo{"", "panic(", 0, nil, nil, false}}, 10}, { "from test using panic", - args{inputFromTest, StackInfo{"", "panic(", 0, nil, nil}}, + args{inputFromTest, StackInfo{"", "panic(", 0, nil, nil, false}}, 8, }, { "from test", args{ inputFromTest, - StackInfo{"", "panic(", 0, PackageRegexp, nil}, + StackInfo{"", "panic(", 0, PackageRegexp, nil, false}, }, 14, }, { "macOS from test using panic", - args{inputFromMac, StackInfo{"", "panic(", 0, nil, nil}}, + args{inputFromMac, StackInfo{"", "panic(", 0, nil, nil, false}}, 12, }, } @@ -338,12 +346,54 @@ func TestCalcAnchor(t *testing.T) { t.Parallel() r := strings.NewReader(tt.input) anchor := calcAnchor(r, tt.StackInfo) - require.Equal(t, tt.anchor, anchor) + except.Equal(t, tt.anchor, anchor) }) } } func TestStackPrint_limit(t *testing.T) { + t.Parallel() + type args struct { + input string + StackInfo + } + type ttest struct { + name string + args + output string + } + tests := []ttest{ + { + "find function with FRAME from test stack", + args{inputFromTest, + StackInfo{"", "", 8, nil, exludeRegexpsAll, true}}, + outputFromTestOnlyFunction, + }, + { + "find function with FRAME from mac stack", + args{inputFromMac, + StackInfo{"", "", 7, nil, exludeRegexpsAll, true}}, + outputFromMacOneFunction, + }, + } + for _, ttv := range tests { + tt := ttv + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + readStack := strings.NewReader(tt.input) + writeStack := new(bytes.Buffer) + stackPrint(readStack, writeStack, tt.StackInfo) + ins := strings.Split(tt.input, "\n") + outs := strings.Split(writeStack.String(), "\n") + except.Thatf(t, len(ins) > len(outs), + "input length:%d should be greater:%d", len(ins), len(outs)) + wantResult, gotResult := tt.output, writeStack.String() + except.Equal(t, gotResult, wantResult) + }) + } +} + +func TestStackPrint_OneFunction(t *testing.T) { t.Parallel() type args struct { input string @@ -357,47 +407,47 @@ func TestStackPrint_limit(t *testing.T) { tests := []ttest{ { "real test trace", - args{inputFromTest, StackInfo{"", "", 8, nil, exludeRegexps}}, + args{inputFromTest, StackInfo{"", "", 8, nil, exludeRegexps, false}}, outputFromTest, }, { "only level 4", - args{input1, StackInfo{"", "", 4, nil, nil}}, + args{input1, StackInfo{"", "", 4, nil, nil, false}}, output1, }, { "short", - args{input, StackInfo{"err2", "Returnw(", 0, nil, nil}}, + args{input, StackInfo{"err2", "Returnw(", 0, nil, nil, false}}, output, }, { "medium", - args{input1, StackInfo{"err2", "Returnw(", 0, nil, nil}}, + args{input1, StackInfo{"err2", "Returnw(", 0, nil, nil, false}}, output1, }, { "medium level 2", - args{input1, StackInfo{"err2", "Returnw(", 2, nil, nil}}, + args{input1, StackInfo{"err2", "Returnw(", 2, nil, nil, false}}, output12, }, { "medium level 0", - args{input1, StackInfo{"err2", "Returnw(", 0, nil, nil}}, + args{input1, StackInfo{"err2", "Returnw(", 0, nil, nil, false}}, output1, }, { "medium panic", - args{input1, StackInfo{"", "panic(", 0, nil, nil}}, + args{input1, StackInfo{"", "panic(", 0, nil, nil, false}}, output1panic, }, { "long", - args{input2, StackInfo{"err2", "Handle(", 0, nil, nil}}, + args{input2, StackInfo{"err2", "Handle(", 0, nil, nil, false}}, output2, }, { "long lvl 2", - args{input2, StackInfo{"err2", "Handle(", 3, nil, nil}}, + args{input2, StackInfo{"err2", "Handle(", 3, nil, nil, false}}, output23, }, } @@ -410,10 +460,10 @@ func TestStackPrint_limit(t *testing.T) { stackPrint(r, w, tt.StackInfo) ins := strings.Split(tt.input, "\n") outs := strings.Split(w.String(), "\n") - require.Thatf(t, len(ins) > len(outs), + except.Thatf(t, len(ins) > len(outs), "input length:%d should be greater:%d", len(ins), len(outs)) b, a := tt.output, w.String() - require.Equal(t, a, b) + except.Equal(t, a, b) }) } } @@ -434,35 +484,35 @@ func TestFuncName(t *testing.T) { tests := []ttest{ { "basic", - args{input2, StackInfo{"", "Handle", 1, nil, nil}}, + args{input2, StackInfo{"", "Handle", 1, nil, nil, false}}, "err2.ReturnW", 214, 6, }, { "basic lvl 3", - args{input2, StackInfo{"", "Handle", 3, nil, nil}}, + args{input2, StackInfo{"", "Handle", 3, nil, nil, false}}, "err2.ReturnW", 214, 6, }, { "basic lvl 2", - args{input2, StackInfo{"lainio/err2", "Handle", 1, nil, nil}}, + args{input2, StackInfo{"lainio/err2", "Handle", 1, nil, nil, false}}, "err2.ReturnW", 214, 6, }, { "method", - args{inputFromTest, StackInfo{"", "Handle", 1, nil, nil}}, + args{inputFromTest, StackInfo{"", "Handle", 1, nil, nil, false}}, "ssi.(*DIDAgent).AssertWallet", 146, 8, }, { "pipeline", - args{inputPipelineStack, StackInfo{"", "Handle", -1, nil, nil}}, + args{inputPipelineStack, StackInfo{"", "Handle", -1, nil, nil, false}}, "CopyFile", 29, 9, @@ -478,10 +528,10 @@ func TestFuncName(t *testing.T) { FuncName: tt.FuncName, Level: tt.Level, }) - require.That(t, found) - require.Equal(t, tt.output, name) - require.Equal(t, ln, tt.outln) - require.Equal(t, fr, tt.outFrame) + except.That(t, found) + except.Equal(t, tt.output, name) + except.Equal(t, ln, tt.outln) + except.Equal(t, fr, tt.outFrame) }) } } @@ -517,6 +567,11 @@ created by github.com/findy-network/findy-agent/agent/prot.FindAndStartTask /Users/harrilainio/go/src/github.com/findy-network/findy-agent/agent/prot/processor.go:337 +0x21c ` + outputFromMacOneFunction = `goroutine 518 [running]: +github.com/findy-network/findy-agent/agent/cloud.(*Agent).PwPipe(0x1400024e2d0, {0x140017ad770, 0x24}) + /Users/harrilainio/go/src/github.com/findy-network/findy-agent/agent/cloud/agent.go:202 +0x324 +` + inputFromTest = `goroutine 31 [running]: testing.tRunner.func1.2({0xa8e0e0, 0x40001937d0}) /usr/local/go/src/testing/testing.go:1389 +0x1c8 @@ -557,6 +612,11 @@ github.com/findy-network/findy-agent/agent/sec_test.TestPipe_packPeer(0x4000106d /home/god/go/src/github.com/findy-network/findy-agent/agent/sec/pipe_test.go:355 +0x1b8 ` + outputFromTestOnlyFunction = `goroutine 31 [running]: +github.com/findy-network/findy-agent/agent/ssi.(*DIDAgent).AssertWallet(...) + /home/god/go/src/github.com/findy-network/findy-agent/agent/ssi/agent.go:146 +` + inputByError = `goroutine 1 [running]: panic({0x137b20, 0x400007ac60}) /usr/local/go/src/runtime/panic.go:838 +0x20c diff --git a/internal/require/test.go b/internal/except/except.go similarity index 98% rename from internal/require/test.go rename to internal/except/except.go index c1a87fa..932099f 100644 --- a/internal/require/test.go +++ b/internal/except/except.go @@ -1,4 +1,4 @@ -package require +package except import ( "fmt" diff --git a/internal/handler/handler.go b/internal/handler/handler.go index 235dc23..e80300d 100644 --- a/internal/handler/handler.go +++ b/internal/handler/handler.go @@ -94,11 +94,20 @@ func (i *Info) callNilHandler() { } func (i *Info) checkErrorTracer() { + errRet := false + if i.ErrorTracer == nil { + i.ErrorTracer = tracer.ErrRet.Tracer() + errRet = true + } if i.ErrorTracer == nil { i.ErrorTracer = tracer.Error.Tracer() + errRet = false } if i.ErrorTracer != nil { si := stackPrologueError + if errRet { + si = stackPrologueErrRet + } if i.Any == nil { i.Any = i.safeErr() } @@ -405,18 +414,29 @@ see 'err2/scripts/README.md' and run auto-migration scripts for your repo func printStack(w io.Writer, si debug.StackInfo, msg any) { fmt.Fprintf(w, "---\n%v\n---\n", msg) debug.FprintStack(w, si) + if si.PrintFirstOnly { + fmt.Fprintln(w, "") + } } var ( - // stackPrologueRuntime = newSI("", "panic(", 1) - stackPrologueError = newErrSI() - stackProloguePanic = newSI("", "panic(", 1) + stackPrologueError = newErrSIOld() + stackPrologueErrRet = newErrSI() + stackProloguePanic = newSI("", "panic(", 1) ) -func newErrSI() debug.StackInfo { +func newErrSIOld() debug.StackInfo { return debug.StackInfo{Regexp: debug.PackageRegexp, Level: 1} } +func newErrSI() debug.StackInfo { + return debug.StackInfo{ + Level: 1, + Regexp: debug.PackageRegexp, + PrintFirstOnly: true, + } +} + func newSI(pn, fn string, lvl int) debug.StackInfo { return debug.StackInfo{ PackageName: pn, diff --git a/internal/handler/handler_test.go b/internal/handler/handler_test.go index dd72499..0e8476b 100644 --- a/internal/handler/handler_test.go +++ b/internal/handler/handler_test.go @@ -6,8 +6,8 @@ import ( "fmt" "testing" + "github.com/lainio/err2/internal/except" "github.com/lainio/err2/internal/handler" - "github.com/lainio/err2/internal/require" "github.com/lainio/err2/internal/x" ) @@ -121,11 +121,11 @@ func TestProcess(t *testing.T) { if handler.WorkToDo(tt.args.Any, tt.args.Err) { handler.Process(&tt.args.Info) - require.Equal(t, panicHandlerCalled, tt.want.panicCalled) - require.Equal(t, errorHandlerCalled, tt.want.errorCalled) - require.Equal(t, nilHandlerCalled, tt.want.nilCalled) + except.Equal(t, panicHandlerCalled, tt.want.panicCalled) + except.Equal(t, errorHandlerCalled, tt.want.errorCalled) + except.Equal(t, nilHandlerCalled, tt.want.nilCalled) - require.Equal(t, myErrVal.Error(), tt.want.errStr) + except.Equal(t, myErrVal.Error(), tt.want.errStr) } resetCalled() }) @@ -152,13 +152,13 @@ func TestPreProcess_debug(t *testing.T) { // and that's what error stack tracing is all about Handle() - require.ThatNot(t, panicHandlerCalled) - require.ThatNot(t, errorHandlerCalled) - require.ThatNot(t, nilHandlerCalled) + except.ThatNot(t, panicHandlerCalled) + except.ThatNot(t, errorHandlerCalled) + except.ThatNot(t, nilHandlerCalled) // See the name of this test function. Decamel it + error const want = "testing: t runner: error" - require.Equal(t, myErrVal.Error(), want) + except.Equal(t, myErrVal.Error(), want) resetCalled() } @@ -243,11 +243,11 @@ func TestPreProcess(t *testing.T) { err = handler.PreProcess(&err, &tt.args.Info, tt.args.a) - require.Equal(t, panicHandlerCalled, tt.want.panicCalled) - require.Equal(t, errorHandlerCalled, tt.want.errorCalled) - require.Equal(t, nilHandlerCalled, tt.want.nilCalled) + except.Equal(t, panicHandlerCalled, tt.want.panicCalled) + except.Equal(t, errorHandlerCalled, tt.want.errorCalled) + except.Equal(t, nilHandlerCalled, tt.want.nilCalled) - require.Equal(t, err.Error(), tt.want.errStr) + except.Equal(t, err.Error(), tt.want.errStr) } resetCalled() }) diff --git a/internal/handler/handlers_test.go b/internal/handler/handlers_test.go index cd76762..a98586c 100644 --- a/internal/handler/handlers_test.go +++ b/internal/handler/handlers_test.go @@ -4,8 +4,8 @@ import ( "testing" "github.com/lainio/err2" + "github.com/lainio/err2/internal/except" "github.com/lainio/err2/internal/handler" - "github.com/lainio/err2/internal/require" ) func TestHandlers(t *testing.T) { @@ -93,17 +93,17 @@ func TestHandlers(t *testing.T) { t.Parallel() anys := tt.args.f - require.That(t, anys != nil, "cannot be nil") + except.That(t, anys != nil, "cannot be nil") fns, dis := handler.ToErrorFns(anys) - require.That(t, fns != nil, "cannot be nil") - require.Equal(t, dis, tt.dis, "disabled wanted") + except.That(t, fns != nil, "cannot be nil") + except.Equal(t, dis, tt.dis, "disabled wanted") errHandler := handler.Pipeline(fns) err := errHandler(err2.ErrNotFound) if err == nil { - require.That(t, tt.want == nil) + except.That(t, tt.want == nil) } else { - require.Equal(t, err.Error(), tt.want.Error()) + except.Equal(t, err.Error(), tt.want.Error()) } }) } diff --git a/internal/str/str.go b/internal/str/str.go index c9d9d13..5259c81 100644 --- a/internal/str/str.go +++ b/internal/str/str.go @@ -25,6 +25,10 @@ func DecamelRegexp(str string) string { return str } +func DecamelRmTryPrefix(s string) string { + return strings.ReplaceAll(Decamel(s), "try ", "") +} + // Decamel return the given string as space delimeted. It's optimized to split // and decamel function names returned from Go call stacks. For more information // see its test cases. diff --git a/internal/str/str_test.go b/internal/str/str_test.go index 9bf8160..c013d0b 100644 --- a/internal/str/str_test.go +++ b/internal/str/str_test.go @@ -3,7 +3,7 @@ package str_test import ( "testing" - "github.com/lainio/err2/internal/require" + "github.com/lainio/err2/internal/except" "github.com/lainio/err2/internal/str" ) @@ -23,6 +23,12 @@ func BenchmarkDecamel(b *testing.B) { } } +func BenchmarkDecamelRmTryPrefix(b *testing.B) { + for n := 0; n < b.N; n++ { + _ = str.DecamelRmTryPrefix(camelStr) + } +} + func TestCamel(t *testing.T) { t.Parallel() type args struct { @@ -43,7 +49,7 @@ func TestCamel(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() got := str.DecamelRegexp(tt.args.s) - require.Equal(t, got, tt.want) + except.Equal(t, got, tt.want) }) } } @@ -100,7 +106,83 @@ func TestDecamel(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() got := str.Decamel(tt.args.s) - require.Equal(t, got, tt.want) + except.Equal(t, got, tt.want) + }) + } +} + +func TestDecamelRmTryPrefix(t *testing.T) { + t.Parallel() + type args struct { + s string + } + tests := []struct { + name string + args args + want string + }{ + {"simple", args{"CamelString"}, "camel string"}, + {"simple try", args{"TryCamelString"}, "camel string"}, + {"underscore", args{"CamelString_error"}, "camel string error"}, + {"underscore and try", args{"TryCamelString_error"}, "camel string error"}, + { + "our contant", + args{camelStr}, + "benchmark recursion with old error if check and defer", + }, + {"number", args{"CamelString2Testing"}, "camel string2 testing"}, + {"acronym", args{"ARMCamelString"}, "armcamel string"}, + {"acronym and try at END so it left", args{"ARMCamelStringTry"}, "armcamel string try"}, + {"acronym and try", args{"TryARMCamelString"}, "armcamel string"}, + {"acronym at end", args{"archIsARM"}, "arch is arm"}, + { + "simple method", + args{"(*DIDAgent).AssertWallet"}, + "didagent assert wallet", + }, + { + "package name and simple method", + args{"ssi.(*DIDAgent).CreateWallet"}, + "ssi: didagent create wallet", + }, + { + "package name and simple method and Function start try", + args{"ssi.(*DIDAgent).TryCreateWallet"}, + "ssi: didagent create wallet", + }, + { + "simple method and anonym", + args{"(*DIDAgent).AssertWallet.Func1"}, + "didagent assert wallet: func1", + }, + { + "complex method and anonym", + args{"(**DIDAgent).AssertWallet.Func1"}, + "didagent assert wallet: func1", + }, + { + "complex method and anonym AND try", + args{"(**DIDAgent).TryAssertWallet.Func1"}, + "didagent assert wallet: func1", + }, + { + "unnatural method and anonym", + args{"(**DIDAgent)...AssertWallet...Func1"}, + "didagent assert wallet: func1", + }, + { + "unnatural method and anonym AND try", + args{"(**DIDAgent)...TryAssertWallet...TryFunc1"}, + "didagent assert wallet: func1", + }, + {"from spf13 cobra", args{"bot.glob..func5"}, "bot: glob: func5"}, + } + for _, ttv := range tests { + tt := ttv + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got := str.DecamelRmTryPrefix(tt.args.s) + except.Equal(t, got, tt.want) }) } } diff --git a/internal/tracer/tracer.go b/internal/tracer/tracer.go index aa10296..53f8fbe 100644 --- a/internal/tracer/tracer.go +++ b/internal/tracer/tracer.go @@ -19,12 +19,14 @@ type writer struct { } var ( - Error value - Panic value - Log value + Error value + Panic value + Log value + ErrRet value ) func init() { + ErrRet.SetTracer(nil) Error.SetTracer(nil) // Because we stop panics as default, we need to output as default Panic.SetTracer(os.Stderr) @@ -39,6 +41,11 @@ func init() { "`stream` for error tracing: stderr, stdout", ) flag.Var(&Panic, "err2-panic-trace", "`stream` for panic tracing") + flag.Var( + &ErrRet, + "err2-ret-trace", + "`stream` for error return tracing: stderr, stdout", + ) } func (v *value) Tracer() io.Writer { diff --git a/internal/x/x_test.go b/internal/x/x_test.go index 1140013..b18fc78 100644 --- a/internal/x/x_test.go +++ b/internal/x/x_test.go @@ -4,7 +4,7 @@ import ( "reflect" "testing" - "github.com/lainio/err2/internal/require" + "github.com/lainio/err2/internal/except" ) var ( @@ -52,30 +52,30 @@ func TestSwap(t *testing.T) { var ( lhs, rhs = 1, 2 // these are ints as default ) - require.Equal(t, lhs, 1) - require.Equal(t, rhs, 2) + except.Equal(t, lhs, 1) + except.Equal(t, rhs, 2) Swap(&lhs, &rhs) - require.Equal(t, lhs, 2) - require.Equal(t, rhs, 1) + except.Equal(t, lhs, 2) + except.Equal(t, rhs, 1) } { var ( lhs, rhs float64 = 1, 2 ) - require.Equal(t, lhs, 1) - require.Equal(t, rhs, 2) + except.Equal(t, lhs, 1) + except.Equal(t, rhs, 2) Swap(&lhs, &rhs) - require.Equal(t, lhs, 2) - require.Equal(t, rhs, 1) + except.Equal(t, lhs, 2) + except.Equal(t, rhs, 1) } } func TestSReverse(t *testing.T) { t.Parallel() SReverse(lengths) - require.That(t, reflect.DeepEqual(lengths, reverseLengths)) + except.That(t, reflect.DeepEqual(lengths, reverseLengths)) SReverse(lengths) // it's reverse now turn it to original - require.That(t, reflect.DeepEqual(lengths, original)) + except.That(t, reflect.DeepEqual(lengths, original)) } func BenchmarkSSReverse(b *testing.B) { diff --git a/samples/main-play.go b/samples/main-play.go index dd12042..fabe80f 100644 --- a/samples/main-play.go +++ b/samples/main-play.go @@ -12,6 +12,7 @@ import ( "flag" "fmt" "io" + "log" "os" "strconv" @@ -64,10 +65,9 @@ func ClassicCopyFile(src, dst string) error { return nil } -// OrgCopyFile copies the source file to the given destination. If any error occurs it +// TryCopyFile copies the source file to the given destination. If any error occurs it // returns an error value describing the reason. -func OrgCopyFile(src, dst string) (err error) { - defer err2.Handle(&err) // automatic error message: see err2.Formatter +func TryCopyFile(src, dst string) { // You can out-comment above handler line(s) to see what happens. // You'll learn that call stacks are for every function level 'catch' @@ -79,17 +79,13 @@ func OrgCopyFile(src, dst string) (err error) { r := try.To1(os.Open(src)) defer r.Close() - w, err := os.Create(dst) - if err != nil { - return fmt.Errorf("mixing traditional error checking: %w", err) - } + w := try.To1(os.Create(dst)) defer err2.Handle(&err, func(err error) error { try.Out(os.Remove(dst)).Logf("cleaning error") return err }) defer w.Close() try.To1(io.Copy(w, r)) - return nil } func CallRecur(d int) (ret int, err error) { @@ -99,18 +95,37 @@ func CallRecur(d int) (ret int, err error) { } func doRecur(d int) (ret int, err error) { - d-- if d >= 0 { // Keep below to show how asserts work //assert.NotZero(d) // Comment out the above assert statement to simulate runtime-error ret = 10 / d - fmt.Println(ret) + log.Println("ret:", ret) //return doRecur(d) } return ret, fmt.Errorf("root error") } +type runMode int + +const ( + runModePlay runMode = iota + runModePlayRec +) + +func (rm runMode) String() string { + return []string{"play", "play-recursion"}[rm] +} + +var rMode runMode + +func setRunMode() { + playRec := runModePlayRec + if *mode == playRec.String() { + rMode = runModePlayRec + } +} + func doPlayMain() { // Keep here that you can play without changing imports assert.That(true) @@ -127,11 +142,7 @@ func doPlayMain() { // errors are caught without specific handlers. defer err2.Catch(err2.Stderr) - // If you don't want to use tracers or you just need a proper error handler - // here. - // defer err2.Catch(func(err error) { - // fmt.Println("ERROR:", err) - // }) + setRunMode() // by calling one of these you can test how automatic logging in above // catch works correctly: the last source of error check is shown in line @@ -161,23 +172,27 @@ func doMain() (err error) { // how err2 works. Especially interesting is automatic stack tracing. // // source file exists, but the destination is not in high probability - //try.To(OrgCopyFile("main.go", "/notfound/path/file.bak")) + //TryCopyFile("main.go", "/notfound/path/file.bak") // Both source and destination don't exist - //try.To(OrgCopyFile("/notfound/path/file.go", "/notfound/path/file.bak")) - - // to play with real args: - try.To(CopyFile(flag.Arg(0), flag.Arg(1))) + //TryCopyFile("/notfound/path/file.go", "/notfound/path/file.bak") if len(flag.Args()) > 0 { - // Next fn demonstrates how error and panic traces work, comment out all - // above CopyFile calls to play with: - argument := try.To1(strconv.Atoi(flag.Arg(0))) - ret := try.To1(CallRecur(argument)) - fmt.Println("ret val:", ret) + if rMode == runModePlayRec { + // Next fn demonstrates how error and panic traces work, comment + // out all above CopyFile calls to play with: + argument := try.To1(strconv.Atoi(flag.Arg(0))) + ret := try.To1(CallRecur(argument)) + fmt.Println("ret val:", ret) + } else { + // to play with real args: + //TryCopyFile(flag.Arg(0), flag.Arg(1)) + try.To(CopyFile(flag.Arg(0), flag.Arg(1))) + } } else { // 2nd argument is empty to assert - try.To(OrgCopyFile("main.go", "")) + TryCopyFile("main.go", "") + //try.To(CopyFile("main.go", "")) } fmt.Println("=== you cannot see this ===") diff --git a/samples/main.go b/samples/main.go index bb0ab8b..db79f4b 100644 --- a/samples/main.go +++ b/samples/main.go @@ -7,6 +7,7 @@ import ( "github.com/lainio/err2" "github.com/lainio/err2/assert" + "github.com/lainio/err2/formatter" ) var ( @@ -14,7 +15,8 @@ var ( "mode", "play", "runs the wanted playground: db, play, nil, assert,"+ - "\nassert-keep (= uses assert.Debug in GLS)", + "\nassert-keep (= uses assert.Debug in GLS),"+ + "\nplay-recursion (= runs recursion example)", ) isErr = flag.Bool("err", false, "tells if we want to have an error") ) @@ -23,6 +25,10 @@ func init() { // highlight that this is before flag.Parse to allow it to work properly. err2.SetLogTracer(os.Stderr) // for import err2.SetLogTracer(nil) + + // select which one you want to play with + err2.SetFormatter(formatter.DecamelAndRmTryPrefix) + // err2.SetFormatter(formatter.Decamel) } func main() { @@ -40,7 +46,7 @@ func main() { doMain1() case "nil2": doMain2() - case "play": + case "play", "play-recursion": doPlayMain() case "assert": doAssertMainKeepGLSAsserter(false) diff --git a/tracer.go b/tracer.go index 954768f..f5a96f3 100644 --- a/tracer.go +++ b/tracer.go @@ -8,10 +8,22 @@ import ( // ErrorTracer returns current [io.Writer] for automatic error stack tracing. // The default value is nil. +// +// See [SetErrRetTracer] and [SetErrorTracer] for more information of the two +// different error tracing systems. func ErrorTracer() io.Writer { return tracer.Error.Tracer() } +// ErrRetTracer returns current [io.Writer] for automatic error return stack +// tracing. The default value is nil. +// +// See [SetErrRetTracer] and [SetErrorTracer] for more information of the two +// different error tracing systems. +func ErrRetTracer() io.Writer { + return tracer.Error.Tracer() +} + // PanicTracer returns current [io.Writer] for automatic panic stack tracing. Note // that [runtime.Error] types which are transported by panics are controlled by // this. The default value is [os.Stderr]. @@ -36,18 +48,36 @@ func LogTracer() io.Writer { // Error trace is almost the same format as Go's standard call stack but it may // have multiple sections because every [Handle] and [Catch] prints it. If an // error happens in a deep call stack, the error trace includes various parts. -// The principle is similar to [Zig Error Return Traces], where you see how -// error bubbles up. However, our error trace is a combination of error return -// traces and stack traces because we get all the needed information at once. +// If you prefer similar to [Zig Error Return Traces], where you see how +// error bubbles up, you should use [SetErrRetTrace]. // // Remember that you can reset these with [flag] package support. See // documentation of err2 package's flag section. -// -// [Zig Error Return Traces]: https://ziglang.org/documentation/master/#Error-Return-Traces func SetErrorTracer(w io.Writer) { tracer.Error.SetTracer(w) } +// SetErrRetTracer sets a [io.Writer] for automatic error return stack tracing. +// The err2 default is nil. Note that any function that has deferred [Handle] or +// [Catch] is capable to print error return stack trace: +// +// func CopyFile(src, dst string) (err error) { +// defer err2.Handle(&err) // <- makes error trace printing decision +// +// Error return trace is almost the same format as Go's standard call stack but +// it may have multiple sections because every [Handle] and [Catch] prints it. +// If an error happens in a deep call stack, the error return trace includes +// various parts. The principle is similar to [Zig Error Return Traces], where +// you see how error bubbles up. +// +// Remember that you can reset these with [flag] package support. See +// documentation of err2 package's flag section. +// +// [Zig Error Return Traces]: https://ziglang.org/documentation/master/#Error-Return-Traces +func SetErrRetTracer(w io.Writer) { + tracer.ErrRet.SetTracer(w) +} + // SetPanicTracer sets a [io.Writer] for automatic panic stack tracing. The err2 // default is [os.Stderr]. Note that [runtime.Error] types which are transported by // panics are controlled by this. Note also that the current function is capable diff --git a/try/copy_test.go b/try/copy_test.go index 7e50099..1498538 100644 --- a/try/copy_test.go +++ b/try/copy_test.go @@ -8,7 +8,7 @@ import ( "os" "testing" - "github.com/lainio/err2/internal/require" + "github.com/lainio/err2/internal/except" "github.com/lainio/err2/try" ) @@ -16,8 +16,8 @@ const dataFile = "./try.go" func Benchmark_CopyBufferMy(b *testing.B) { all, err := os.ReadFile(dataFile) - require.Thatf(b, err == nil, "error: %v", err) - require.That(b, all != nil) + except.Thatf(b, err == nil, "error: %v", err) + except.That(b, all != nil) buf := make([]byte, 4) dst := bufio.NewWriter(bytes.NewBuffer(make([]byte, 0, len(all)))) @@ -29,8 +29,8 @@ func Benchmark_CopyBufferMy(b *testing.B) { func Benchmark_CopyBufferStd(b *testing.B) { all, err := os.ReadFile(dataFile) - require.Thatf(b, err == nil, "error: %v", err) - require.That(b, all != nil) + except.Thatf(b, err == nil, "error: %v", err) + except.That(b, all != nil) buf := make([]byte, 4) dst := bufio.NewWriter(bytes.NewBuffer(make([]byte, 0, len(all)))) @@ -42,8 +42,8 @@ func Benchmark_CopyBufferStd(b *testing.B) { func Benchmark_CopyBufferOur(b *testing.B) { all, err := os.ReadFile(dataFile) - require.Thatf(b, err == nil, "error: %v", err) - require.That(b, all != nil) + except.Thatf(b, err == nil, "error: %v", err) + except.That(b, all != nil) tmp := make([]byte, 4) dst := bufio.NewWriter(bytes.NewBuffer(make([]byte, 0, len(all)))) diff --git a/try/out_test.go b/try/out_test.go index 60ea173..1ce0946 100644 --- a/try/out_test.go +++ b/try/out_test.go @@ -11,7 +11,7 @@ import ( "testing" "github.com/lainio/err2" - "github.com/lainio/err2/internal/require" + "github.com/lainio/err2/internal/except" "github.com/lainio/err2/try" ) @@ -102,8 +102,8 @@ func TestResult2_Logf(t *testing.T) { return v1 + v2, v2 } num1, num2 := countSomething("1", "bad") - require.Equal(t, num2, 2) - require.Equal(t, num1, 3) + except.Equal(t, num2, 2) + except.Equal(t, num1, 3) } func TestResult_Handle(t *testing.T) { @@ -121,10 +121,10 @@ func TestResult_Handle(t *testing.T) { return nil } err := callFn(1) - require.That(t, err == nil, "no error when Out.Handle sets it nil") + except.That(t, err == nil, "no error when Out.Handle sets it nil") err = callFn(0) - require.That(t, err != nil, "want error when Out.Handle sets it the same") + except.That(t, err != nil, "want error when Out.Handle sets it the same") } func ExampleResult1_Handle() { diff --git a/try/try.go b/try/try.go index 7ed1310..c8eaff1 100644 --- a/try/try.go +++ b/try/try.go @@ -1,8 +1,9 @@ /* -Package try is a package for [To], [To1], and [To2] functions that implement the error -checking. [To] functions check 'if err != nil' and if it throws the err to the -error handlers, which are implemented by the err2 package. More information -about err2 and try packager roles can be seen in the FileCopy example: +Package try is a package for error checking with the functions: [To], [To1], and +[To2]. These functions check the last given error argument isn't nil, i.e., 'if +err != nil' and if it is, it throws the err to the error handlers. More +information about err2 and try packages roles can be seen in the FileCopy +example: ... r := try.To1(os.Open(src))