diff --git a/.gitignore b/.gitignore index c3ce8c0..912fe33 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ # IDEs, etc. .idea -Session.vim* +*.vim* coverage.* # Binaries for programs and plugins diff --git a/CHANGELOG.md b/CHANGELOG.md index 90bdfa4..662568b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ ### Version history +##### 0.9.52 +- `err2.Stderr` helpers for `Catch/Handle` to direct auto-logging + snippets +- `assert` package `Shorter` `Longer` helpers for automatic messages +- `asserter` package remove deprecated slow reflection based funcs +- cleanup and refactoring for sample apps + ##### 0.9.51 - `flag` package support to set `err2` and `assert` package configuration - `err2.Catch` default mode is to log error @@ -17,7 +23,7 @@ handlers ##### 0.9.40 -- Significant performance boost for: `defer err2.Handle/Catch()` +- Significant performance boost for: `defer err2.Handle/Catch()` - **3x faster happy path than the previous version, which is now equal to simplest `defer` function in the `err`-returning function** . (Please see the `defer` benchmarks in the `err2_test.go` and run `make bench_reca`) @@ -67,7 +73,7 @@ especially performance ##### 0.9.0 -- **Clean and simple API** +- **Clean and simple API** - Removing deprecated functions: - Only `err2.Handle` for error returning functions - Only `err2.Catch` for function that doesn't return error diff --git a/README.md b/README.md index e30a13e..9473df2 100644 --- a/README.md +++ b/README.md @@ -10,24 +10,19 @@ and propagation** like other modern programming languages: **Zig**, Rust, Swift, etc. `err2` isn't an exception handling library, but an entirely orthogonal package with Go's existing error handling mechanism. -```go +```go func CopyFile(src, dst string) (err error) { defer err2.Handle(&err) - assert.NotEmpty(src) - assert.NotEmpty(dst) - 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, err2.Err(func(error) { - try.Out1(os.Remove(dst)).Logf("cleaning error") + try.Out(os.Remove(dst)).Logf("cleaning error") })) defer w.Close() + try.To1(io.Copy(w, r)) return nil } @@ -88,7 +83,7 @@ little error handling. But most importantly, it doesn't help developers with > resilience. -- Gregor Hohpe Automatic error propagation is crucial because it makes your code change -tolerant. And, of course, it helps to make your code error-safe: +tolerant. And, of course, it helps to make your code error-safe: ![Never send a human to do a machine's job](https://www.magicalquote.com/wp-content/uploads/2013/10/Never-send-a-human-to-do-a-machines-job.jpg) @@ -99,7 +94,7 @@ The err2 package is your automation buddy: line exactly similar as [Zig's `errdefer`](https://ziglang.org/documentation/master/#errdefer). 2. It helps to check and transport errors to the nearest (the defer-stack) error - handler. + 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. @@ -124,12 +119,14 @@ This is the simplest form of `err2` automatic error handler: ```go func doSomething() (err error) { // below: if err != nil { return ftm.Errorf("%s: %w", CUR_FUNC_NAME, err) } - defer err2.Handle(&err) + defer err2.Handle(&err) ``` See more information from `err2.Handle`'s documentation. It supports several error-handling scenarios. And remember that you can have as many error handlers -per function as you need. +per function as you need, as well as you can chain error handling functions per +`err2.Handle` that allows you to build new error handling middleware for your +own purposes. #### Error Stack Tracing @@ -248,7 +245,7 @@ notExist := try.Is(r2.err, plugin.ErrNotExist) happens: nearest `err2.Handle` gets it first. These `try.Is` functions help cleanup mesh idiomatic Go, i.e. mixing happy and -error path, leads to. +error path, leads to. For more information see the examples in the documentation of both functions. @@ -328,7 +325,7 @@ func TestWebOfTrustInfo(t *testing.T) { assert.SLen(common, 2) wot := dave.WebOfTrustInfo(eve.Node) //<- this includes asserts as well!! - // And if there's violations during the test run they are reported as + // And if there's violations during the test run they are reported as // test failures for this TestWebOfTrustInfo -test. assert.Equal(0, wot.CommonInvider) @@ -364,7 +361,7 @@ stack.** When you are using `err2` or `assert` packages, i.e., just importing them, you have an option to automatically support for err2 configuration flags through Go's standard `flag` package. See more information about err2 settings from -[Error Stack Tracing](#error-stack-tracing) and [Asserters](#asserters). +[Error Stack Tracing](#error-stack-tracing) and [Asserters](#asserters). Now you can always deploy your applications and services with the simple end-user friendly error messages and no stack traces, **but you can switch them @@ -421,10 +418,10 @@ support packages like `err2` and `glog` and their flags. ```go PersistentPreRunE: func(cmd *cobra.Command, args []string) (err error) { defer err2.Handle(&err) - + // NOTE! Very important. Adds support for std flag pkg users: glog, err2 goflag.Parse() - + try.To(goflag.Set("logtostderr", "true")) handleViperFlags(cmd) // local helper with envs glog.CopyStandardLogTo("ERROR") // for err2 @@ -470,7 +467,7 @@ part of the algorithm itself.** **there are no performance penalty at all**. However, the mandatory use of the `defer` might prevent some code optimisations like function inlining. And still, we have cases where using the `err2` and `try` package simplify the algorithm so -that it's faster than the return value if err != nil version. (**See the +that it's faster than the return value if err != nil version. (**See the benchmarks for `io.Copy` in the repo.**) If you have a performance-critical use case, we always recommend you to write @@ -527,14 +524,14 @@ Please see the full version history from [CHANGELOG](./CHANGELOG.md). ### Latest Release -##### 0.9.52 -- `err2.Stderr` helpers for `Catch/Handle` to direct auto-logging + snippets -- `assert` package `Shorter` `Longer` helpers for automatic messages -- `asserter` package remove deprecated slow reflection based funcs -- cleanup and refactoring for sample apps - -### Upcoming releases - -##### 0.9.6 -- Continue removing unused parts and repairing for 1.0.0 release. -- Always more and better documentation +##### 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 +- `Catch/Handle` take unlimited amount error handler functions + - allows building e.g. error handling middlewares + - this is major feature because it allows building helpers/add-ons +- automatic outputs aren't overwritten by given args, only with `assert.Plain` +- Minor API fixes to still simplify it: + - remove exported vars, obsolete types and funcs from `assert` pkg + - `Result2.Def2()` sets only `Val2` +- technical refactorings: variadic function calls only in API level diff --git a/assert/assert.go b/assert/assert.go index d5d094d..28a1600 100644 --- a/assert/assert.go +++ b/assert/assert.go @@ -16,107 +16,108 @@ import ( type defInd = uint32 +// Asserters are the way to set what kind of messages assert package outputs if +// assertion is violated. +// +// [Plain] converts asserts just plain K&D error messages without extra +// information. That's useful for apps that want to use assert package to +// validate e.g. command fields: +// +// assert.NotEmpty(c.PoolName, "pool name cannot be empty") +// +// 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. +// +// [Production] (pkg's default) is the best asserter for most cases. The +// assertion violations are treated as Go error values. And only a pragmatic +// caller info is included into the error values like source filename, line +// number, and caller function, all in one line: +// +// copy file: main.go:37: CopyFile(): assertion violation: string shouldn't be empty +// +// [Development] is the best asserter for development use. The assertion +// violations are treated as Go error values. And a formatted caller info is +// included to the error message like source filename , line number, and caller +// function. Everything in a noticeable multi-line message: +// +// -------------------------------- +// Assertion Fault at: +// main.go:37 CopyFile(): +// assertion violation: string shouldn't be empty +// -------------------------------- +// +// [Test] minimalistic asserter for unit test use. More pragmatic is the +// [TestFull] asserter (test default). +// +// Use this asserter if your IDE/editor doesn't support full file names and it +// relies a relative path (Go standard). You can use this also if you need +// temporary problem solving for your programming environment. +// +// [TestFull] asserter (test default). The TestFull asserter includes the caller +// info and the call stack for unit testing, similarly like err2's error traces. +// Note that [PushTester] set's TestFull if it's not yet set. +// +// The call stack produced by the test asserts can be used over Go module +// boundaries. For example, if your app and it's sub packages both use +// err2/assert for unit testing and runtime checks, the runtime assertions will +// be automatically converted to test asserts. If any of the runtime asserts of +// the sub packages fails during the app tests, the app test fails as well. +// +// Note that the cross-module assertions produce long file names (path +// included), and some of the Go test result parsers cannot handle that. A +// proper test result parser like [nvim-go] (fork) works very well. Also most of +// the make result parsers can process the output properly and allow traverse of +// locations of the error trace. +// +// [Debug] asserter transforms assertion violations to panic calls where panic +// object's type is string, i.e., err2 package treats it as a normal panic, not +// an error. +// +// For example, the pattern that e.g. Go's standard library uses: +// +// if p == nil { +// panic("pkg: ptr cannot be nil") +// } +// +// is equal to: +// +// assert.NotNil(p) +// +// [nvim-go]: https://github.com/lainio/nvim-go const ( - // Plain converts asserts just plain K&D error messages without extra - // information. Plain defInd = 0 + iota - - // Production (pkg default) is the best asserter for most uses. The - // assertion violations are treated as Go error values. And only a - // pragmatic caller info is automatically included into the error message - // like file name, line number, and caller function, all in one line: - // - // copy file: main.go:37: CopyFile(): assertion violation: string shouldn't be empty Production - - // Development is the best asserter for most development uses. The - // assertion violations are treated as Go error values. And a formatted - // caller info is automatically included to the error message like file - // name, line number, and caller function. Everything in a beautiful - // multi-line message: - // - // -------------------------------- - // Assertion Fault at: - // main.go:37 CopyFile(): - // assertion violation: string shouldn't be empty - // -------------------------------- Development - - // Test minimalistic asserter for unit test use. More pragmatic is the - // TestFull asserter (test default). - // - // Use this asserter if your IDE/editor doesn't support full file names and - // it relies a relative path (Go standard). You can use this also if you - // need temporary problem solving for your programming environment. Test - - // TestFull asserter (test default). The TestFull asserter includes the - // caller info and the call stack for unit testing, similarly like err2's - // error traces. - // - // The call stack produced by the test asserts can be used over Go module - // boundaries. For example, if your app and it's sub packages both use - // err2/assert for unit testing and runtime checks, the runtime assertions - // will be automatically converted to test asserts. If any of the runtime - // asserts of the sub packages fails during the app tests, the app test - // fails as well. - // - // Note, that the cross-module assertions produce long file names (path - // included), and some of the Go test result parsers cannot handle that. - // A proper test result parser like 'github.com/lainio/nvim-go' (fork) - // works very well. Also most of the make result parsers can process the - // output properly and allow traverse of locations of the error trace. TestFull - - // Debug asserter transforms assertion violations to panic calls where - // panic object's type is string, i.e., err2 package treats it as a normal - // panic, not an error. - // - // The pattern that e.g. Go's standard - // library uses: - // - // if p == nil { - // panic("pkg: ptr cannot be nil") - // } - // - // is equal to: - // - // assert.NotNil(p) Debug ) type flagAsserter struct{} -// Deprecated: use e.g. assert.That(), only default asserter is used. +// Asserters var ( - PL = AsserterToError - - // P is a production Asserter that sets panic objects to errors which - // allows err2 handlers to catch them. - P = AsserterToError | AsserterCallerInfo - - B = AsserterToError | AsserterFormattedCallerInfo - - T = AsserterUnitTesting - TF = AsserterUnitTesting | AsserterStackTrace | AsserterCallerInfo - - // D is a development Asserter that sets panic objects to strings that - // doesn't by caught by err2 handlers. - // Deprecated: use e.g. assert.That(), only default asserter is used. - D = AsserterDebug + plain = asserterToError + prod = asserterToError | asserterCallerInfo + dev = asserterToError | asserterFormattedCallerInfo + test = asserterUnitTesting + testFull = asserterUnitTesting | asserterStackTrace | asserterCallerInfo + dbg = asserterDebug ) var ( - // These two are our indexing system for default asserter. Note also the - // mutex blew. All of this is done to keep client package race detector + // These two are our indexing system for default asserter. Note, also the + // mutex below. All of this is done to keep client package race detector // cool. + // // Plain // Production // Development // Test // TestFull // Debug - defAsserter = []Asserter{PL, P, B, T, TF, D} + defAsserter = []asserter{plain, prod, dev, test, testFull, dbg} def defInd // mu is package lvl Mutex that is used to cool down race detector of @@ -150,47 +151,54 @@ const ( gotWantFmt = ": got '%v', want '%v'" gotWantLongerFmt = ": got '%v', should be longer than '%v'" gotWantShorterFmt = ": got '%v', should be shorter than '%v'" + + conCatErrStr = ": " ) // PushTester sets the current testing context for default asserter. This must // be called at the beginning of every test. There is two way of doing it: // // for _, tt := range tests { -// t.Run(tt.name, func(t *testing.T) { // Shorter way, litle magic -// defer assert.PushTester(t)() // <- IMPORTANT! NOTE! (t)() -// ... -// assert.That(something, "test won't work") -// }) -// t.Run(tt.name, func(t *testing.T) { // Longer, explicit way, 2 lines -// assert.PushTester(t) // <- IMPORTANT! -// defer assert.PopTester() -// ... -// assert.That(something, "test won't work") -// }) +// t.Run(tt.name, func(t *testing.T) { // Shorter way, litle magic +// defer assert.PushTester(t)() // <- IMPORTANT! NOTE! (t)() +// ... +// assert.That(something, "test won't work") +// }) +// t.Run(tt.name, func(t *testing.T) { // Longer, explicit way, 2 lines +// assert.PushTester(t) // <- IMPORTANT! +// defer assert.PopTester() +// ... +// assert.That(something, "test won't work") +// }) // } // -// Because PushTester returns PopTester it allows us to merge these two calls to -// one line. See the first t.Run call. NOTE. More information in PopTester. +// Because PushTester returns [PopTester] it allows us to merge these two calls +// to one line. See the first t.Run call above. See more information in +// [PopTester]. // // PushTester allows you to change the current default asserter by accepting it -// as a second argument. NOTE. That it change the default asserter for whole -// package. The argument is mainly for temporary development use and isn't not -// preferrred API usage. +// as a second argument. +// +// Note that you MUST call PushTester for sub-goroutines: +// +// defer assert.PushTester(t)() // does the cleanup +// ... +// go func() { +// assert.PushTester(t) // left cleanup out! Leave it for the test, see ^ +// ... +// +// Note that the second argument, if given, changes the default asserter for +// whole package. The argument is mainly for temporary development use and isn't +// not preferred API usage. func PushTester(t testing.TB, a ...defInd) function { if len(a) > 0 { SetDefault(a[0]) - } else if Default()&AsserterUnitTesting == 0 { + } else if current()&asserterUnitTesting == 0 { // if this is forgotten or tests don't have proper place to set it // it's good to keep the API as simple as possible SetDefault(TestFull) } - testers.Tx(func(m testersMap) { - rid := goid() - if _, ok := m[rid]; ok { - panic("PushTester is already called") - } - m[rid] = t - }) + testers.Set(goid(), t) return PopTester } @@ -198,25 +206,22 @@ func PushTester(t testing.TB, a ...defInd) function { // memory cleanup and adding similar to err2.Catch error/panic safety for tests. // By using PopTester you get error logs tuned for unit testing. // -// You have two ways to call PopTester. With defer right after PushTester: +// You have two ways to call [PopTester]. With defer right after [PushTester]: // // for _, tt := range tests { -// t.Run(tt.name, func(t *testing.T) { -// assert.PushTester(t) // <- important! -// defer assert.PopTester() // <- for good girls and not so bad boys -// ... -// assert.That(something, "test won't work") -// }) +// t.Run(tt.name, func(t *testing.T) { +// assert.PushTester(t) // <- important! +// defer assert.PopTester() // <- for good girls and not so bad boys +// ... +// assert.That(something, "test won't work") +// }) // } // -// If you want to have one liner to combine Push/PopTester: +// If you want, you can combine [PushTester] and PopTester to one-liner: // // defer assert.PushTester(t)() func PopTester() { - defer testers.Tx(func(m testersMap) { - goid := goid() - delete(m, goid) - }) + defer testers.Del(goid()) r := recover() if r == nil { @@ -235,10 +240,10 @@ func PopTester() { msg = "test panic catch" } - // First, print the call stack. Note. that we aren't support full error + // First, print the call stack. Note that we aren't support full error // tracing with unit test logging. However, using it has proved the top // level error stack as more enough. Even so that we could consider using - // it for normal error stack straces if it would be possible. + // it for normal error stack traces if it would be possible. const stackLvl = 6 // amount of functions before we're here debug.PrintStackForTest(os.Stderr, stackLvl) @@ -254,430 +259,616 @@ func tester() (t testing.TB) { } // NotImplemented always panics with 'not implemented' assertion message. +// +// Note that when [Plain] asserter is used ([SetDefault]), optional arguments +// are used to override the auto-generated assert violation message. func NotImplemented(a ...any) { - Default().reportAssertionFault("not implemented", a...) + current().reportAssertionFault("not implemented", a) } // ThatNot asserts that the term is NOT true. If is it panics with the given // formatting string. Thanks to inlining, the performance penalty is equal to a // single 'if-statement' that is almost nothing. +// +// Note that when [Plain] asserter is used ([SetDefault]), optional arguments +// are used to override the auto-generated assert violation message. func ThatNot(term bool, a ...any) { if term { - defMsg := "ThatNot: " + assertionMsg - Default().reportAssertionFault(defMsg, a...) + defMsg := assertionMsg + current().reportAssertionFault(defMsg, a) } } // That asserts that the term is true. If not it panics with the given // formatting string. Thanks to inlining, the performance penalty is equal to a // single 'if-statement' that is almost nothing. +// +// Note that when [Plain] asserter is used ([SetDefault]), optional arguments +// are used to override the auto-generated assert violation message. func That(term bool, a ...any) { if !term { - defMsg := "That: " + assertionMsg - Default().reportAssertionFault(defMsg, a...) + defMsg := assertionMsg + current().reportAssertionFault(defMsg, a) } } // NotNil asserts that the pointer IS NOT nil. If it is it panics/errors (default -// Asserter) with the given message. +// Asserter) the auto-generated (args appended) message. +// +// Note that when [Plain] asserter is used ([SetDefault]), optional arguments +// are used to override the auto-generated assert violation message. func NotNil[P ~*T, T any](p P, a ...any) { if p == nil { defMsg := assertionMsg + ": pointer shouldn't be nil" - Default().reportAssertionFault(defMsg, a...) + current().reportAssertionFault(defMsg, a) } } // Nil asserts that the pointer IS nil. If it is not it panics/errors (default -// Asserter) with the given message. +// Asserter) the auto-generated (args appended) message. +// +// Note that when [Plain] asserter is used ([SetDefault]), optional arguments +// are used to override the auto-generated assert violation message. func Nil[T any](p *T, a ...any) { if p != nil { defMsg := assertionMsg + ": pointer should be nil" - Default().reportAssertionFault(defMsg, a...) + current().reportAssertionFault(defMsg, a) } } // INil asserts that the interface value IS nil. If it is it panics/errors -// (default Asserter) with the given message. +// (default Asserter) the auto-generated (args appended) message. +// +// Note that when [Plain] asserter is used ([SetDefault]), optional arguments +// are used to override the auto-generated assert violation message. +// +// Note, use this only for real interface types. Go's interface's has two values +// so this won't work e.g. slices! +// Read more information about [the interface type]. +// +// [the interface type]: https://go.dev/doc/faq#nil_error func INil(i any, a ...any) { if i != nil { defMsg := assertionMsg + ": interface should be nil" - Default().reportAssertionFault(defMsg, a...) + current().reportAssertionFault(defMsg, a) } } // INotNil asserts that the interface value is NOT nil. If it is it -// panics/errors (default Asserter) with the given message. +// panics/errors (default Asserter) the auto-generated (args appended) message. +// +// Note that when [Plain] asserter is used ([SetDefault]), optional arguments +// are used to override the auto-generated assert violation message. +// +// Note, use this only for real interface types. Go's interface's has two values +// so this won't work e.g. slices! +// Read more information about [the interface type]. +// +// [the interface type]: https://go.dev/doc/faq#nil_error func INotNil(i any, a ...any) { if i == nil { defMsg := assertionMsg + ": interface shouldn't be nil" - Default().reportAssertionFault(defMsg, a...) + current().reportAssertionFault(defMsg, a) } } // SNil asserts that the slice IS nil. If it is it panics/errors (default -// Asserter) with the given message. +// Asserter) the auto-generated (args appended) message. +// +// Note that when [Plain] asserter is used ([SetDefault]), optional arguments +// are used to override the auto-generated assert violation message. func SNil[S ~[]T, T any](s S, a ...any) { if s != nil { defMsg := assertionMsg + ": slice should be nil" - Default().reportAssertionFault(defMsg, a...) + current().reportAssertionFault(defMsg, a) } } // SNotNil asserts that the slice is not nil. If it is it panics/errors (default -// Asserter) with the given message. +// Asserter) the auto-generated (args appended) message. +// +// Note that when [Plain] asserter is used ([SetDefault]), optional arguments +// are used to override the auto-generated assert violation message. func SNotNil[S ~[]T, T any](s S, a ...any) { if s == nil { defMsg := assertionMsg + ": slice shouldn't be nil" - Default().reportAssertionFault(defMsg, a...) + current().reportAssertionFault(defMsg, a) } } // CNotNil asserts that the channel is not nil. If it is it panics/errors -// (default Asserter) with the given message. +// (default Asserter) the auto-generated (args appended) message. +// +// Note that when [Plain] asserter is used ([SetDefault]), optional arguments +// are used to override the auto-generated assert violation message. func CNotNil[C ~chan T, T any](c C, a ...any) { if c == nil { defMsg := assertionMsg + ": channel shouldn't be nil" - Default().reportAssertionFault(defMsg, a...) + current().reportAssertionFault(defMsg, a) } } // MNotNil asserts that the map is not nil. If it is it panics/errors (default -// Asserter) with the given message. +// Asserter) the auto-generated (args appended) message. +// +// Note that when [Plain] asserter is used ([SetDefault]), optional arguments +// are used to override the auto-generated assert violation message. func MNotNil[M ~map[T]U, T comparable, U any](m M, a ...any) { if m == nil { defMsg := assertionMsg + ": map shouldn't be nil" - Default().reportAssertionFault(defMsg, a...) + current().reportAssertionFault(defMsg, a) } } // NotEqual asserts that the values aren't equal. If they are it panics/errors -// (current Asserter) with the given message. +// (according the current Asserter) with the auto-generated message. You can +// append the generated got-want message by using optional message arguments. +// +// Note that when [Plain] asserter is used ([SetDefault]), optional arguments +// are used to override the auto-generated assert violation message. +// +// Note, when asserter is [Plain], optional arguments are used to build a new +// assert violation message. func NotEqual[T comparable](val, want T, a ...any) { if want == val { defMsg := fmt.Sprintf(assertionMsg+": got '%v' want (!= '%v')", val, want) - Default().reportAssertionFault(defMsg, a...) + current().reportAssertionFault(defMsg, a) } } -// Equal asserts that the values are equal. If not it panics/errors (current -// Asserter) with the given message. +// Equal asserts that the values are equal. If not it panics/errors (according +// the current Asserter) with the auto-generated message. You can append the +// generated got-want message by using optional message arguments. +// +// Note that when [Plain] asserter is used ([SetDefault]), optional arguments +// are used to override the auto-generated assert violation message. func Equal[T comparable](val, want T, a ...any) { if want != val { defMsg := fmt.Sprintf(assertionMsg+gotWantFmt, val, want) - Default().reportAssertionFault(defMsg, a...) + current().reportAssertionFault(defMsg, a) } } // DeepEqual asserts that the (whatever) values are equal. If not it -// panics/errors (current Asserter) with the given message. +// panics/errors (according the current Asserter) with the auto-generated +// message. You can append the generated got-want message by using optional +// message arguments. +// +// Note that when [Plain] asserter is used ([SetDefault]), optional arguments +// are used to override the auto-generated assert violation message. func DeepEqual(val, want any, a ...any) { if !reflect.DeepEqual(val, want) { defMsg := fmt.Sprintf(assertionMsg+gotWantFmt, val, want) - Default().reportAssertionFault(defMsg, a...) + current().reportAssertionFault(defMsg, a) } } // NotDeepEqual asserts that the (whatever) values are equal. If not it -// panics/errors (current Asserter) with the given message. NOTE, it uses -// reflect.DeepEqual which means that also the types must be the same: +// panics/errors (according the current Asserter) with the auto-generated +// message. You can append the generated got-want message by using optional +// message arguments. +// +// Note that when [Plain] asserter is used ([SetDefault]), optional arguments +// are used to override the auto-generated assert violation message. +// +// Note, it uses reflect.DeepEqual which means that also the types must be the +// same: // // assert.DeepEqual(pubKey, ed25519.PublicKey(pubKeyBytes)) func NotDeepEqual(val, want any, a ...any) { if reflect.DeepEqual(val, want) { defMsg := fmt.Sprintf(assertionMsg+": got '%v', want (!= '%v')", val, want) - Default().reportAssertionFault(defMsg, a...) + current().reportAssertionFault(defMsg, a) } } // Len asserts that the length of the string is equal to the given. If not it -// panics/errors (current Asserter) with the given message. Note! This is -// reasonably fast but not as fast as 'That' because of lacking inlining for the -// current implementation of Go's type parametric functions. +// panics/errors (according the current Asserter) with the auto-generated +// message. You can append the generated got-want message by using optional +// message arguments. +// +// Note that when [Plain] asserter is used ([SetDefault]), optional arguments +// are used to override the auto-generated assert violation message. +// +// Note! This is reasonably fast but not as fast as [That] because of lacking +// inlining for the current implementation of Go's type parametric functions. func Len(obj string, length int, a ...any) { l := len(obj) if l != length { defMsg := fmt.Sprintf(assertionMsg+gotWantFmt, l, length) - Default().reportAssertionFault(defMsg, a...) + current().reportAssertionFault(defMsg, a) } } // Longer asserts that the length of the string is longer to the given. If not -// it panics/errors (current Asserter) with the given message. Note! This is -// reasonably fast but not as fast as 'That' because of lacking inlining for the -// current implementation of Go's type parametric functions. -func Longer(obj string, length int, a ...any) { - l := len(obj) +// it panics/errors (according the current Asserter) with the auto-generated +// message. You can append the generated got-want message by using optional +// message arguments. +// +// Note that when [Plain] asserter is used ([SetDefault]), optional arguments +// are used to override the auto-generated assert violation message. +// +// Note! This is reasonably fast but not as fast as [That] because of lacking +// inlining for the current implementation of Go's type parametric functions. +func Longer(s string, length int, a ...any) { + l := len(s) - if l > length { + if l <= length { defMsg := fmt.Sprintf(assertionMsg+gotWantLongerFmt, l, length) - Default().reportAssertionFault(defMsg, a...) + current().reportAssertionFault(defMsg, a) } } // Shorter asserts that the length of the string is shorter to the given. If not -// it panics/errors (current Asserter) with the given message. Note! This is -// reasonably fast but not as fast as 'That' because of lacking inlining for the -// current implementation of Go's type parametric functions. -func Shorter(obj string, length int, a ...any) { - l := len(obj) +// it panics/errors (according the current Asserter) with the auto-generated +// message. You can append the generated got-want message by using optional +// message arguments. +// +// Note that when [Plain] asserter is used ([SetDefault]), optional arguments +// are used to override the auto-generated assert violation message. +// +// Note! This is reasonably fast but not as fast as [That] because of lacking +// inlining for the current implementation of Go's type parametric functions. +func Shorter(str string, length int, a ...any) { + l := len(str) - if l <= length { + if l >= length { defMsg := fmt.Sprintf(assertionMsg+gotWantShorterFmt, l, length) - Default().reportAssertionFault(defMsg, a...) + current().reportAssertionFault(defMsg, a) } } // SLen asserts that the length of the slice is equal to the given. If not it -// panics/errors (current Asserter) with the given message. Note! This is -// reasonably fast but not as fast as 'That' because of lacking inlining for the -// current implementation of Go's type parametric functions. +// panics/errors (according the current Asserter) with the auto-generated +// message. You can append the generated got-want message by using optional +// message arguments. +// +// Note that when [Plain] asserter is used ([SetDefault]), optional arguments +// are used to override the auto-generated assert violation message. +// +// Note! This is reasonably fast but not as fast as [That] because of lacking +// inlining for the current implementation of Go's type parametric functions. func SLen[S ~[]T, T any](obj S, length int, a ...any) { l := len(obj) if l != length { defMsg := fmt.Sprintf(assertionMsg+gotWantFmt, l, length) - Default().reportAssertionFault(defMsg, a...) + current().reportAssertionFault(defMsg, a) } } // SLonger asserts that the length of the slice is equal to the given. If not it -// panics/errors (current Asserter) with the given message. Note! This is -// reasonably fast but not as fast as 'That' because of lacking inlining for the -// current implementation of Go's type parametric functions. +// panics/errors (according the current Asserter) with the auto-generated +// message. You can append the generated got-want message by using optional +// message arguments. +// +// Note that when [Plain] asserter is used ([SetDefault]), optional arguments +// are used to override the auto-generated assert violation message. +// +// Note! This is reasonably fast but not as fast as [That] because of lacking +// inlining for the current implementation of Go's type parametric functions. func SLonger[S ~[]T, T any](obj S, length int, a ...any) { l := len(obj) if l <= length { defMsg := fmt.Sprintf(assertionMsg+gotWantLongerFmt, l, length) - Default().reportAssertionFault(defMsg, a...) + current().reportAssertionFault(defMsg, a) } } -// SShorter asserts that the length of the slice is equal to the given. If not it -// panics/errors (current Asserter) with the given message. Note! This is -// reasonably fast but not as fast as 'That' because of lacking inlining for the -// current implementation of Go's type parametric functions. +// SShorter asserts that the length of the slice is equal to the given. If not +// it panics/errors (according the current Asserter) with the auto-generated +// message. You can append the generated got-want message by using optional +// message arguments. +// +// Note that when [Plain] asserter is used ([SetDefault]), optional arguments +// are used to override the auto-generated assert violation message. +// +// Note! This is reasonably fast but not as fast as [That] because of lacking +// inlining for the current implementation of Go's type parametric functions. func SShorter[S ~[]T, T any](obj S, length int, a ...any) { l := len(obj) if l >= length { defMsg := fmt.Sprintf(assertionMsg+gotWantShorterFmt, l, length) - Default().reportAssertionFault(defMsg, a...) + current().reportAssertionFault(defMsg, a) } } // MLen asserts that the length of the map is equal to the given. If not it -// panics/errors (current Asserter) with the given message. Note! This is -// reasonably fast but not as fast as 'That' because of lacking inlining for the -// current implementation of Go's type parametric functions. +// panics/errors (according the current Asserter) with the auto-generated +// message. You can append the generated got-want message by using optional +// message arguments. +// +// Note that when [Plain] asserter is used ([SetDefault]), optional arguments +// are used to override the auto-generated assert violation message. +// +// Note! This is reasonably fast but not as fast as [That] because of lacking +// inlining for the current implementation of Go's type parametric functions. func MLen[M ~map[T]U, T comparable, U any](obj M, length int, a ...any) { l := len(obj) if l != length { defMsg := fmt.Sprintf(assertionMsg+gotWantFmt, l, length) - Default().reportAssertionFault(defMsg, a...) + current().reportAssertionFault(defMsg, a) } } // MLonger asserts that the length of the map is longer to the given. If not it -// panics/errors (current Asserter) with the given message. Note! This is -// reasonably fast but not as fast as 'That' because of lacking inlining for the -// current implementation of Go's type parametric functions. +// panics/errors (according the current Asserter) with the auto-generated +// message. You can append the generated got-want message by using optional +// message arguments. +// +// Note that when [Plain] asserter is used ([SetDefault]), optional arguments +// are used to override the auto-generated assert violation message. +// +// Note! This is reasonably fast but not as fast as [That] because of lacking +// inlining for the current implementation of Go's type parametric functions. func MLonger[M ~map[T]U, T comparable, U any](obj M, length int, a ...any) { l := len(obj) if l <= length { defMsg := fmt.Sprintf(assertionMsg+gotWantLongerFmt, l, length) - Default().reportAssertionFault(defMsg, a...) + current().reportAssertionFault(defMsg, a) } } // MShorter asserts that the length of the map is shorter to the given. If not -// it panics/errors (current Asserter) with the given message. Note! This is -// reasonably fast but not as fast as 'That' because of lacking inlining for the -// current implementation of Go's type parametric functions. +// it panics/errors (according the current Asserter) with the auto-generated +// message. You can append the generated got-want message by using optional +// message arguments. +// +// Note that when [Plain] asserter is used ([SetDefault]), optional arguments +// are used to override the auto-generated assert violation message. +// +// Note! This is reasonably fast but not as fast as [That] because of lacking +// inlining for the current implementation of Go's type parametric functions. func MShorter[M ~map[T]U, T comparable, U any](obj M, length int, a ...any) { l := len(obj) if l >= length { defMsg := fmt.Sprintf(assertionMsg+gotWantShorterFmt, l, length) - Default().reportAssertionFault(defMsg, a...) + current().reportAssertionFault(defMsg, a) } } // CLen asserts that the length of the chan is equal to the given. If not it -// panics/errors (current Asserter) with the given message. Note! This is -// reasonably fast but not as fast as 'That' because of lacking inlining for the -// current implementation of Go's type parametric functions. +// panics/errors (according the current Asserter) with the auto-generated +// message. You can append the generated got-want message by using optional +// message arguments. +// +// Note that when [Plain] asserter is used ([SetDefault]), optional arguments +// are used to override the auto-generated assert violation message. +// +// Note! This is reasonably fast but not as fast as [That] because of lacking +// inlining for the current implementation of Go's type parametric functions. func CLen[C ~chan T, T any](obj C, length int, a ...any) { l := len(obj) if l != length { defMsg := fmt.Sprintf(assertionMsg+gotWantFmt, l, length) - Default().reportAssertionFault(defMsg, a...) + current().reportAssertionFault(defMsg, a) } } // CLonger asserts that the length of the chan is longer to the given. If not it -// panics/errors (current Asserter) with the given message. Note! This is -// reasonably fast but not as fast as 'That' because of lacking inlining for the -// current implementation of Go's type parametric functions. +// panics/errors (according the current Asserter) with the auto-generated +// message. You can append the generated got-want message by using optional +// message arguments. +// +// Note that when [Plain] asserter is used ([SetDefault]), optional arguments +// are used to override the auto-generated assert violation message. +// +// Note! This is reasonably fast but not as fast as [That] because of lacking +// inlining for the current implementation of Go's type parametric functions. func CLonger[C ~chan T, T any](obj C, length int, a ...any) { l := len(obj) if l <= length { defMsg := fmt.Sprintf(assertionMsg+gotWantLongerFmt, l, length) - Default().reportAssertionFault(defMsg, a...) + current().reportAssertionFault(defMsg, a) } } // CShorter asserts that the length of the chan is shorter to the given. If not -// it panics/errors (current Asserter) with the given message. Note! This is -// reasonably fast but not as fast as 'That' because of lacking inlining for the -// current implementation of Go's type parametric functions. +// it panics/errors (according the current Asserter) with the auto-generated +// message. You can append the generated got-want message by using optional +// message arguments. +// +// Note that when [Plain] asserter is used ([SetDefault]), optional arguments +// are used to override the auto-generated assert violation message. +// +// Note! This is reasonably fast but not as fast as [That] because of lacking +// inlining for the current implementation of Go's type parametric functions. func CShorter[C ~chan T, T any](obj C, length int, a ...any) { l := len(obj) if l >= length { defMsg := fmt.Sprintf(assertionMsg+gotWantShorterFmt, l, length) - Default().reportAssertionFault(defMsg, a...) + current().reportAssertionFault(defMsg, a) } } // MKeyExists asserts that the map key exists. If not it panics/errors (current -// Asserter) with the given message. +// Asserter) the auto-generated (args appended) message. +// +// Note that when [Plain] asserter is used ([SetDefault]), optional arguments +// are used to override the auto-generated assert violation message. func MKeyExists[M ~map[T]U, T comparable, U any](obj M, key T, a ...any) (val U) { var ok bool val, ok = obj[key] if !ok { defMsg := fmt.Sprintf(assertionMsg+": key '%v' doesn't exist", key) - Default().reportAssertionFault(defMsg, a...) + current().reportAssertionFault(defMsg, a) } return val } // NotEmpty asserts that the string is not empty. If it is, it panics/errors -// (current Asserter) with the given message. +// (according the current Asserter) with the auto-generated message. You can +// append the generated got-want message by using optional message arguments. +// +// Note that when [Plain] asserter is used ([SetDefault]), optional arguments +// are used to override the auto-generated assert violation message. func NotEmpty(obj string, a ...any) { if obj == "" { defMsg := assertionMsg + ": string shouldn't be empty" - Default().reportAssertionFault(defMsg, a...) + current().reportAssertionFault(defMsg, a) } } // Empty asserts that the string is empty. If it is NOT, it panics/errors -// (current Asserter) with the given message. +// (according the current Asserter) with the auto-generated message. You can +// append the generated got-want message by using optional message arguments. +// +// Note that when [Plain] asserter is used ([SetDefault]), optional arguments +// are used to override the auto-generated assert violation message. func Empty(obj string, a ...any) { if obj != "" { defMsg := assertionMsg + ": string should be empty" - Default().reportAssertionFault(defMsg, a...) + current().reportAssertionFault(defMsg, a) } } // SNotEmpty asserts that the slice is not empty. If it is, it panics/errors -// (current Asserter) with the given message. Note! This is reasonably fast but -// not as fast as 'That' because of lacking inlining for the current -// implementation of Go's type parametric functions. +// (according the current Asserter) with the auto-generated message. You can +// append the generated got-want message by using optional message arguments. +// +// Note that when [Plain] asserter is used ([SetDefault]), optional arguments +// are used to override the auto-generated assert violation message. +// +// Note! This is reasonably fast but not as fast as [That] because of lacking +// inlining for the current implementation of Go's type parametric functions. func SNotEmpty[S ~[]T, T any](obj S, a ...any) { l := len(obj) if l == 0 { defMsg := assertionMsg + ": slice shouldn't be empty" - Default().reportAssertionFault(defMsg, a...) + current().reportAssertionFault(defMsg, a) } } // MNotEmpty asserts that the map is not empty. If it is, it panics/errors -// (current Asserter) with the given message. Note! This is reasonably fast but -// not as fast as 'That' because of lacking inlining for the current -// implementation of Go's type parametric functions. +// (according the current Asserter) with the auto-generated message. You can +// append the generated got-want message by using optional message arguments. +// You can append the generated got-want message by using optional message +// arguments. +// +// Note that when [Plain] asserter is used ([SetDefault]), optional arguments +// are used to override the auto-generated assert violation message. +// +// Note! This is reasonably fast but not as fast as [That] because of lacking +// inlining for the current implementation of Go's type parametric functions. func MNotEmpty[M ~map[T]U, T comparable, U any](obj M, a ...any) { l := len(obj) if l == 0 { defMsg := assertionMsg + ": map shouldn't be empty" - Default().reportAssertionFault(defMsg, a...) + current().reportAssertionFault(defMsg, a) } } // NoError asserts that the error is nil. If is not it panics with the given // formatting string. Thanks to inlining, the performance penalty is equal to a -// single 'if-statement' that is almost nothing. Note. We recommend that you -// prefer try.To every case even in tests because they work exactly the same -// during the test runs and you can use same code for both: runtime and tests. +// single 'if-statement' that is almost nothing. +// +// Note. We recommend that you prefer [github.com/lainio/err2/try.To]. They work +// exactly the same during the test runs and you can use the same code for both: +// runtime and tests. However, there are cases that you want assert that there +// is no error in cases where fast fail and immediate stop of execution is +// wanted at runtime. With asserts ([Production], [Development], [Debug]) you +// get the file location as well. func NoError(err error, a ...any) { if err != nil { - defMsg := "NoError:" + assertionMsg + ": " + err.Error() - Default().reportAssertionFault(defMsg, a...) + defMsg := assertionMsg + conCatErrStr + err.Error() + current().reportAssertionFault(defMsg, a) } } -// Error asserts that the err is not nil. If it is it panics with the given -// formatting string. Thanks to inlining, the performance penalty is equal to a +// Error asserts that the err is not nil. If it is it panics and builds a +// violation message. Thanks to inlining, the performance penalty is equal to a // single 'if-statement' that is almost nothing. +// +// Note that when [Plain] asserter is used ([SetDefault]), optional arguments +// are used to override the auto-generated assert violation message. func Error(err error, a ...any) { if err == nil { defMsg := "Error:" + assertionMsg + ": missing error" - Default().reportAssertionFault(defMsg, a...) + current().reportAssertionFault(defMsg, a) } } -// Zero asserts that the value is 0. If it is not it panics with the given -// formatting string. Thanks to inlining, the performance penalty is equal to a +// Zero asserts that the value is 0. 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. +// +// Note that when [Plain] asserter is used ([SetDefault]), optional arguments +// are used to override the auto-generated assert violation message. func Zero[T Number](val T, a ...any) { if val != 0 { defMsg := fmt.Sprintf(assertionMsg+": got '%v', want (== '0')", val) - Default().reportAssertionFault(defMsg, a...) + current().reportAssertionFault(defMsg, a) } } -// NotZero asserts that the value != 0. If it is not it panics with the given -// formatting string. Thanks to inlining, the performance penalty is equal to a +// NotZero asserts that the value != 0. 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. +// +// Note that when [Plain] asserter is used ([SetDefault]), optional arguments +// are used to override the auto-generated assert violation message. func NotZero[T Number](val T, a ...any) { if val == 0 { defMsg := fmt.Sprintf(assertionMsg+": got '%v', want (!= 0)", val) - Default().reportAssertionFault(defMsg, a...) + current().reportAssertionFault(defMsg, a) } } -// Default returns a current default asserter used for package-level -// functions like assert.That(). The package sets the default asserter as -// follows: +// current returns a current default asserter used for package-level +// functions like assert.That(). // -// SetDefaultAsserter(AsserterToError | AsserterFormattedCallerInfo) -// -// Which means that it is treats assert failures as Go errors, but in addition -// to that, it formats the assertion message properly. Naturally, only if err2 -// handlers are found in the call stack, these errors are caught. - -// You are free to set it according to your current preferences with the -// SetDefault function. -func Default() Asserter { +// Note, this indexing stuff is done because of race detection to work on client +// packages. And, yes, we have tested it. This is fastest way to make it without +// locks HERE. Only the setting the index is secured with the mutex. +func current() asserter { return defAsserter[def] } -// SetDefault set the current default asserter for the package. For example, you -// might set it to panic about every assertion fault, and in other cases, throw -// an error, and print the call stack immediately when assert occurs. 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 allo foot hem and do it only one in TestMain, or in init. +// SetDefault sets the current default asserter for assert pkg. It also returns +// the previous asserter. +// +// 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 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. // -// SetDefault(assert.TestFull) -func SetDefault(i defInd) Asserter { - // pkg lvl lock to allow only one pkg client call this at the time +// func TestMain(m *testing.M) { +// SetDefault(assert.TestFull) +func SetDefault(i defInd) (old defInd) { + // 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 + // variable or pointer to it but just index to array they are stored. + // All of this make race detector happy at the client pkgs. mu.Lock() defer mu.Unlock() - // to make this fully thread safe the def var should be atomic, BUT it - // would be owerkill. We need only defaults to be set at once. + old = def + // theoretically, to make this fully thread safe the def var should be + // atomic, BUT it would be overkill. We need only defaults to be set at + // once. AND because we use indexing to actual asserter the thread-safety + // and performance are guaranteed, def = i - return defAsserter[i] + return } // mapDefInd runtime asserters, that's why test asserts are removed for now. @@ -699,7 +890,7 @@ var mapDefIndToString = map[defInd]string{ Debug: "Debug", } -func AsserterString() string { +func defaultAsserterString() string { return mapDefIndToString[def] } @@ -714,7 +905,7 @@ func newDefInd(v string) defInd { func combineArgs(format string, a []any) []any { args := make([]any, 1, len(a)+1) args[0] = format - args = append(args, a...) + args = append(args, a) return args } @@ -745,7 +936,7 @@ type Number interface { // String is part of the flag interfaces func (f *flagAsserter) String() string { - return AsserterString() + return defaultAsserterString() } // Get is part of the flag interfaces, getter. diff --git a/assert/assert_test.go b/assert/assert_test.go index 06ec08d..2db8b34 100644 --- a/assert/assert_test.go +++ b/assert/assert_test.go @@ -13,12 +13,12 @@ func ExampleThat() { sample := func() (err error) { defer err2.Handle(&err) - assert.That(false, "assertion test") + assert.That(false, "optional message") return err } err := sample() fmt.Printf("%v", err) - // Output: testing: run example: assertion test + // Output: testing: run example: assert_test.go:16: ExampleThat.func1(): assertion violation: optional message } func ExampleNotNil() { @@ -178,6 +178,19 @@ func ExampleMShorter() { // Output: sample: assert_test.go:172: ExampleMShorter.func1(): assertion violation: got '1', should be shorter than '1' } +func ExampleSShorter() { + sample := func(b []byte) (err error) { + defer err2.Handle(&err, "sample") + + assert.SShorter(b, 2) // ok + assert.SShorter(b, 0, "optional message (%s)", "test_str") // not ok + return err + } + err := sample([]byte{01}) // len = 1 + fmt.Printf("%v", err) + // Output: sample: assert_test.go:186: ExampleSShorter.func1(): assertion violation: got '1', should be shorter than '0': optional message (test_str) +} + func assertZero(i int) { assert.Zero(i) } @@ -260,6 +273,13 @@ func BenchmarkSLen(b *testing.B) { } } +func BenchmarkLen(b *testing.B) { + s := "len" + for n := 0; n < b.N; n++ { + assert.Len(s, 3) + } +} + func BenchmarkSLen_thatVersion(b *testing.B) { d := []byte{1, 2} for n := 0; n < b.N; n++ { diff --git a/assert/asserter.go b/assert/asserter.go index c085b49..803213d 100644 --- a/assert/asserter.go +++ b/assert/asserter.go @@ -4,50 +4,59 @@ import ( "errors" "fmt" "os" - "reflect" "github.com/lainio/err2/internal/debug" "github.com/lainio/err2/internal/str" + "github.com/lainio/err2/internal/x" ) -// Asserter is type for asserter object guided by its flags. -type Asserter uint32 +// asserter is type for asserter object guided by its flags. +type asserter uint32 const ( - // AsserterDebug is the default mode where all asserts are treaded as + // asserterDebug is the default mode where all asserts are treaded as // panics - AsserterDebug Asserter = 0 + asserterDebug asserter = 0 - // AsserterToError is Asserter flag to guide asserter to use Go's error + // asserterToError is Asserter flag to guide asserter to use Go's error // type for panics. - AsserterToError Asserter = 1 << iota + asserterToError asserter = 1 << iota - // AsserterStackTrace is Asserter flag to print call stack to stdout OR if + // asserterStackTrace is Asserter flag to print call stack to stdout OR if // in AsserterUnitTesting mode the call stack is printed to test result // output if there is any assertion failures. - AsserterStackTrace + asserterStackTrace - // AsserterCallerInfo is an asserter flag to add info of the function + // asserterCallerInfo is an asserter flag to add info of the function // asserting. It includes filename, line number and function name. // This is especially powerful with AsserterUnitTesting where it allows get // information where the assertion violation happens even over modules! - AsserterCallerInfo + asserterCallerInfo - // AsserterFormattedCallerInfo is an asserter flag to add info of the function + // asserterFormattedCallerInfo is an asserter flag to add info of the function // asserting. It includes filename, line number and function name in // multi-line formatted string output. - AsserterFormattedCallerInfo + asserterFormattedCallerInfo - // AsserterUnitTesting is an asserter only for unit testing. It can be + // asserterUnitTesting is an asserter only for unit testing. It can be // compined with AsserterCallerInfo and/or AsserterStackTrace. There is // variable T which have all of these three asserters. - AsserterUnitTesting + asserterUnitTesting ) // every test log or result output has 4 spaces in them const officialTestOutputPrefix = " " -func (asserter Asserter) reportAssertionFault(defaultMsg string, a ...any) { +// reportAssertionFault reports assertion fault according the current asserter +// and its config. If extra argumnets 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 +// 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. +func (asserter asserter) reportAssertionFault(defaultMsg string, a []any) { if asserter.hasStackTrace() { if asserter.isUnitTesting() { // Note. that the assert in the test function is printed in @@ -66,26 +75,18 @@ func (asserter Asserter) reportAssertionFault(defaultMsg string, a ...any) { } if len(a) > 0 { if format, ok := a[0].(string); ok { - asserter.reportPanic(fmt.Sprintf(format, a[1:]...)) + allowDefMsg := !asserter.isErrorOnly() && defaultMsg != "" + f := x.Whom(allowDefMsg, defaultMsg+conCatErrStr+format, format) + asserter.reportPanic(fmt.Sprintf(f, a[1:]...)) } else { - asserter.reportPanic(fmt.Sprintln(a...)) + asserter.reportPanic(fmt.Sprintln(append([]any{defaultMsg}, a...))) } } else { asserter.reportPanic(defaultMsg) } } -func getLen(x any) (ok bool, length int) { - v := reflect.ValueOf(x) - defer func() { - if e := recover(); e != nil { - ok = false - } - }() - return true, v.Len() -} - -func (asserter Asserter) reportPanic(s string) { +func (asserter asserter) reportPanic(s string) { if asserter.isUnitTesting() && asserter.hasCallerInfo() { fmt.Fprintln(os.Stderr, officialTestOutputPrefix+s) tester().FailNow() @@ -125,7 +126,7 @@ Assertion Fault at: var shortFmtStr = `%s:%d: %s(): %s` -func (asserter Asserter) callerInfo(msg string) (info string) { +func (asserter asserter) callerInfo(msg string) (info string) { ourFmtStr := shortFmtStr if asserter.hasFormattedCallerInfo() { ourFmtStr = longFmtStr @@ -143,24 +144,28 @@ func (asserter Asserter) callerInfo(msg string) (info string) { return } -func (asserter Asserter) hasToError() bool { - return asserter&AsserterToError != 0 +func (asserter asserter) isErrorOnly() bool { + return asserter == asserterToError +} + +func (asserter asserter) hasToError() bool { + return asserter&asserterToError != 0 } -func (asserter Asserter) hasStackTrace() bool { - return asserter&AsserterStackTrace != 0 +func (asserter asserter) hasStackTrace() bool { + return asserter&asserterStackTrace != 0 } -func (asserter Asserter) hasCallerInfo() bool { - return asserter&AsserterCallerInfo != 0 || asserter.hasFormattedCallerInfo() +func (asserter asserter) hasCallerInfo() bool { + return asserter&asserterCallerInfo != 0 || asserter.hasFormattedCallerInfo() } -func (asserter Asserter) hasFormattedCallerInfo() bool { - return asserter&AsserterFormattedCallerInfo != 0 +func (asserter asserter) hasFormattedCallerInfo() bool { + return asserter&asserterFormattedCallerInfo != 0 } // isUnitTesting is expensive because it calls tester(). think carefully where // to use it -func (asserter Asserter) isUnitTesting() bool { - return asserter&AsserterUnitTesting != 0 && tester() != nil +func (asserter asserter) isUnitTesting() bool { + return asserter&asserterUnitTesting != 0 && tester() != nil } diff --git a/assert/doc.go b/assert/doc.go index 766635b..d6a246e 100644 --- a/assert/doc.go +++ b/assert/doc.go @@ -1,37 +1,46 @@ /* Package assert includes runtime assertion helpers both for normal execution as well as a assertion package for Go's testing. What makes solution unique is its -capable to support both modes with same API. Only thing you need to do is to -add following two lines at the beginning of your unit tests: +capable to support both modes with the same API. Only thing you need to do is to +add a [PushTester] line at the beginning of your unit tests and its +sub-gouroutines: func TestInvite(t *testing.T) { defer assert.PushTester(t)() // push testing variable t beginning of any test + // v-----v Invite's control flow includes assertions alice.Node = root1.Invite(alice.Node, root1.Key, alice.PubKey, 1) assert.Equal(alice.Len(), 1) // assert anything normally + ... + go func() { + assert.PushTester(t) // <-- Needs to do again for a new goroutine -# Merge Runtime And Test Assertions +# Merge Runtime And Unit Test Assertions -Especially powerful feature is that even if some assertion violation happens -during the execution of called functions not the test it self. See the above -example. If assertion failure happens inside of the Invite() function instead of -the actual Test function, TestInvite, it's still reported correctly as normal -test failure when TestInvite is executed. It doesn't matter how deep the -recursion is, or if parallel test runs are performed. It works just as you -hoped. +The next block is the actual Invite function's first two lines. Even if the +assertion line is written more from a runtime detection point of view, it catches +all assert violations in the unit tests as well: -# Call Stack Traversal During tests + func (c Chain) Invite(...) { + assert.That(c.isLeaf(invitersKey), "only leaf can invite") -The asserter package has super powerful feature. It allows us track assertion -violations over package and even module boundaries. When using err2 assert -package for runtime Asserts and assert violation happens in what ever package -and module, the whole call stack is brougth to unit test logs. Naturally this is -optinal. Only thing you need to do is set proper asserter and call PushTester. +If some assertion violation occurs in the deep call stack, they are still +reported as a test failure. See the above code blocks. If assertion failure +happens somewhere inside the Invite function's call stack, it's still reported +correctly as a test failure of the TestInvite unit test. It doesn't matter how +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 unit testing asserter - assert.SetDefault(assert.TestFull) +# Call Stack Traversal During Tests -With large multi repo environment this has proven to be valuable. +The Assert package allows us to track assertion violations over the package and +even module boundaries. When an assertion fails during the unit testing, the +whole call stack is brought to unit test logs. And some help with your IDE, such +as transferring output to a location list, for example, in Neovim/Vim. For +example, you can find a proper test result parser like [nvim-go] (fork) + +With a sizeable multi-repo environment, this has proven to be valuable. # Why Runtime Asserts Are So Important? @@ -40,22 +49,23 @@ raise up quality of our software. "Assertions are active comments" -The package offers a convenient way to set preconditions to code which allow us -detect programming errors and API violations faster. Still allowing +The assert package offers a convenient way to set preconditions to code which +allow us detect programming errors and API violations faster. Still allowing production-time error handling if needed. And everything is automatic. You can -set proper asserter according to flag or environment variable. This allows -developer, operator and every-day user share the exact same binary but get the -error messages and diagnostic they need. +set asserter with [SetDefault] function or --asserter flag if Go's flag package is +in use. This allows developer, operator and every-day user share the exact same +binary but get the error messages and diagnostic they need. - // add formatted caller info for normal errors coming from assertions + // Production asserter adds formatted caller info to normal errors. + // Information is transported thru error values when err2.Handle is in use. assert.SetDefault(assert.Production) Please see the code examples for more information. # Flag Package Support -The assert package supports Go's flags. All you need to do is to call flag.Parse. -And the following flags are supported (="default-value"): +The assert package supports Go's flags. All you need to do is to call +[flag.Parse]. And the following flags are supported (="default-value"): -asserter="Prod" A name of the asserter Plain, Prod, Dev, Debug @@ -65,16 +75,22 @@ And assert package's configuration flags are inserted. # Performance -assert.That's performance is equal to the if-statement thanks for inlining. And -the most of the generics-based versions are about the equally fast. +[assert.That]'s performance is equal to the if-statement thanks for inlining. And +the most of the generics-based versions are about the equally fast. Practice has +thought that we should prefer other than [assert.That] because by using detailed +version like [assert.Shorter] we get precise error messages automatically. Some +also prefer readability of specific asserters. If your algorithm is performance-critical please run `make bench` in the err2 repo and decide case by case. Also you can make an issue or even PR if you would -like to have something similar like glog.V() function. +like to have something similar like [glog.V] function. + +# Naming -# Technical Notes +Because performance has been number one requirement and Go's generics cannot +discrete slices, maps and channels we have used naming prefixes accordingly: S = +slice, M = map, C = channel. No prefix is (currently) for the string type. -Format string functions need to be own instances because of Go's vet and test -tool integration. +[nvim-go]: https://github.com/lainio/nvim-go */ package assert diff --git a/doc.go b/doc.go index 98bf61d..af37f26 100644 --- a/doc.go +++ b/doc.go @@ -2,15 +2,9 @@ Package err2 provides three main functionality: 1. err2 package includes helper functions for error handling & automatic error stack tracing - 2. try package is for error checking - 3. assert package is for design-by-contract and preconditions both for normal - runtime and for testing - -The traditional error handling idiom in Go is roughly akin to - - if err != nil { return err } - -which applied recursively. + 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 + preconditions both for normal runtime and for unit testing The err2 package drives programmers to focus on error handling rather than checking errors. We think that checks should be so easy that we never forget @@ -19,7 +13,7 @@ them. The CopyFile example shows how it works: // CopyFile copies source file to the given destination. If any error occurs it // returns error value describing the reason. func CopyFile(src, dst string) (err error) { - // Add first error handler just to annotate the error properly. + // Add first error handler is to catch and annotate the error properly. defer err2.Handle(&err) // Try to open the file. If error occurs now, err will be @@ -32,16 +26,18 @@ them. The CopyFile example shows how it works: // Try to create a file. If error occurs now, err will be annotated and // returned properly. w := try.To1(os.Create(dst)) - // Add error handler to clean up the destination file. Place it here that - // the next deferred close is called before our Remove call. + // Add error handler to clean up the destination file in case of + // error. Handler fn is called only if there has been an error at the + // following try.To check. We place it here that the next deferred + // close is called before our Remove a file call. defer err2.Handle(&err, err2.Err(func(error) { - os.Remove(dst) + try.Out(os.Remove(dst)).Logf("cleanup failed") })) defer w.Close() // Try to copy the file. If error occurs now, all previous error handlers - // will be called in the reversed order. And final return error is - // properly annotated in all the cases. + // will be called in the reversed order. And a final error value is + // properly annotated and returned in all the cases. try.To1(io.Copy(w, r)) // All OK, just return nil. @@ -50,48 +46,63 @@ them. The CopyFile example shows how it works: # Error checks and Automatic Error Propagation -The try package provides convenient helpers to check the errors. For example, +The [github.com/lainio/err2/try] package provides convenient helpers to check the errors. For example, instead of - b, err := ioutil.ReadAll(r) + b, err := io.ReadAll(r) if err != nil { return err } we can write - b := try.To1(ioutil.ReadAll(r)) + b := try.To1(io.ReadAll(r)) -Note that try.ToX functions are as fast as if err != nil statements. Please see -the try package documentation for more information about the error checks. +Note that try.To functions are as fast as if err != nil statements. Please see +the [github.com/lainio/err2/try] package documentation for more information +about the error checks. # Automatic Stack Tracing -err2 offers optional stack tracing. And yes, it's fully automatic. Just set the -tracers at the beginning your app, e.g. main function, to the stream you want -traces to be written: +err2 offers optional stack tracing. And yes, it's fully automatic. Just call + + flag.Parse() # this is enough for err2 pkg to add its flags + +at the beginning your app, e.g. main function, or set the tracers +programmatically (before [flag.Parse] if you are using that): err2.SetErrorTracer(os.Stderr) // write error stack trace to stderr or err2.SetPanicTracer(log.Writer()) // panic stack trace to std logger -Note. Since err2.Catch's default mode is to catch panics, the panic tracer's -default values is os.Stderr. The default error tracer is nil. +Note. Since [Catch]'s default mode is to recover from panics, it's a good +practice still print their stack trace. The panic tracer's default values is +[os.Stderr]. The default error tracer is nil. err2.SetPanicTracer(os.Stderr) // panic stack tracer's default is stderr err2.SetErrorTracer(nil) // error stack tracer's default is nil +Note that both panic and error traces are optimized by err2 package. That means +that the head of the stack trace isn't the panic function, but an actual line +that caused it. It works for all three categories: + - normal error values + - [runtime.Error] values + - any types of the panics + +The last two types are handled as panics in the error handling functions given +to [Handle] and [Catch]. + # Automatic Logging -Same err2 capablities support automatic logging like the err2.Catch and -try.Result.Logf functions. To be able to tune up how logging behaves we offer a +Same err2 capablities support automatic logging like the [Catch] and +[try.Result.Logf] functions. To be able to tune up how logging behaves we offer a tracer API: err2.SetLogTracer(nil) // the default is nil where std log pkg is used. # Flag Package Support -The err2 package supports Go's flags. All you need to do is to call flag.Parse. +The err2 package supports Go's flags. All you need to do is to call [flag.Parse]. And the following flags are supported (="default-value"): -err2-log="nil" @@ -101,6 +112,10 @@ And the following flags are supported (="default-value"): -err2-trace="nil" A name of the stream currently supported stderr, stdout or nil +Note that you have called [SetErrorTracer] and others, before you call +[flag.Parse]. This allows you set the defaults according your app's need and allow +end-user change them during the runtime. + # Error handling Package err2 relies on declarative control structures to achieve error and panic @@ -108,7 +123,7 @@ safety. In every function which uses err2 or try package for error-checking has to have at least one declarative error handler if it returns error value. If there are no error handlers and error occurs it panics. We think that panicking for the errors is much better than not checking errors at all. Nevertheless, if -the call stack includes any err2 error handlers like err2.Handle the error is +the call stack includes any err2 error handlers like [Handle] the error is handled where the handler is saved to defer-stack. (defer is not lexically scoped) diff --git a/err2.go b/err2.go index 02491a4..f26f6da 100644 --- a/err2.go +++ b/err2.go @@ -3,51 +3,54 @@ package err2 import ( "errors" "fmt" - "os" "github.com/lainio/err2/internal/handler" ) type ( - // Handler is a function type used to process error values in Handle and - // Catch. We currently have a few build-ins of the Handler: err2.Noop, - // err2.Reset, etc. - Handler func(error) error + // Handler is a function type used to process error values in [Handle] and + // [Catch]. We currently have a few build-ins of the Handler: [Noop], + // [Reset], etc. + Handler = handler.ErrorFn ) +// Sentinel error value helpers. They are convenient thanks to +// [github.com/lainio/err2/try.IsNotFound] and similar functions. +// +// [ErrNotFound] ... [ErrNotEnabled] are similar no-error like [io.EOF] for +// those who really want to use error return values to transport non errors. +// It's far better to have discriminated unions as errors for function calls. +// But if you insist the related helpers are in they +// [github.com/lainio/err2/try] package: +// [github.com/lainio/err2/try.IsNotFound], ... +// +// [ErrRecoverable] and [ErrNotRecoverable] since Go 1.20 wraps multiple errors +// same time, i.e. wrapped errors aren't list anymore but tree. This allows mark +// multiple semantics to same error. These error are mainly for that purpose. var ( - // ErrNotFound is similar *no-error* like io.EOF for those who really want to - // use error return values to transport non errors. It's far better to have - // discriminated unions as errors for function calls. But if you insist the - // related helpers are in they try package: try.IsNotFound(), ... These - // 'global' errors and their helper functions in try package are for - // experimenting now. - ErrNotFound = errors.New("not found") - ErrNotExist = errors.New("not exist") - ErrAlreadyExist = errors.New("already exist") - ErrNotAccess = errors.New("permission denied") - ErrNotEnabled = errors.New("not enabled") - - // Since Go 1.20 wraps multiple errors same time, i.e. wrapped errors - // aren't list anymore but tree. This allows mark multiple semantics to - // same error. These error are mainly for that purpose. + ErrNotFound = errors.New("not found") + ErrNotExist = errors.New("not exist") + ErrAlreadyExist = errors.New("already exist") + ErrNotAccess = errors.New("permission denied") + ErrNotEnabled = errors.New("not enabled") ErrNotRecoverable = errors.New("cannot recover") ErrRecoverable = errors.New("recoverable") - - // Stdnull is helper variable for io.Writer need e.g. err2.SetLogTracer in - // cases you don't want to use automatic log writer, i.e. LogTracer == nil. - // It's usually used to change how the Catch works, e.g., in CLI apps. - Stdnull = &nullDev{} ) +// Stdnull implements [io.Writer] that writes nothing, e.g., +// [SetLogTracer] in cases you don't want to use automatic log writer (=nil), +// i.e., [LogTracer] == /dev/null. It can be used to change how the [Catch] +// works, e.g., in CLI apps. +var Stdnull = &nullDev{} + // Handle is the general purpose error handling function. What makes it so // convenient is its ability to handle all error handling cases: // - just return the error value to caller // - annotate the error value // - execute real error handling like cleanup and releasing resources. // -// There is no performance penalty. The handler is called only when err != nil. -// There is no limit how many Handle functions can be added to defer stack. They +// There's no performance penalty. The handler is called only when err != nil. +// There's no limit how many Handle functions can be added to defer stack. They // all are called if an error has occurred. // // The function has an automatic mode where errors are annotated by function @@ -58,36 +61,46 @@ var ( // // Note. If you are still using sentinel errors you must be careful with the // automatic error annotation because it uses wrapping. If you must keep the -// error value got from error checks: 'try.To(..)', you must disable automatic -// error annotation (%w), or set the returned error values in the handler -// function. Disabling can be done by setting second argument nil: +// error value got from error checks: [github.com/lainio/err2/try.To], you must +// disable automatic error annotation (%w), or set the returned error values in +// the handler function. Disabling can be done by setting second argument nil: // // func SaveData(...) (err error) { // defer err2.Handle(&err, nil) // nil arg disable automatic annotation. // // In case of the actual error handling, the handler function should be given as -// an second argument: +// a second argument: // // defer err2.Handle(&err, func(err error) error { -// if rmErr := os.Remove(dst); rmErr != nil { -// return fmt.Errorf("%w: cleanup error: %w", err, rmErr) -// } -// return err +// if rmErr := os.Remove(dst); rmErr != nil { +// return fmt.Errorf("%w: cleanup error: %w", err, rmErr) +// } +// return err // }) // -// If you need to stop general panics in handler, you can do that by giving a -// panic handler function: +// You can have unlimited amount of error handlers. They are called if error +// happens and they are called in the same order as they are given or until one +// of them resets the error like [Reset] (notice the other predefined error +// handlers) in the next samples: +// +// defer err2.Handle(&err, err2.Reset, err2.Log) // Log not called +// defer err2.Handle(&err, err2.Noop, err2.Log) // handlers > 1: err annotated +// defer err2.Handle(&err, nil, err2.Log) // nil disables auto-annotation +// +// If you need to stop general panics in a handler, you can do that by declaring +// a panic handler. See the second handler below: // // defer err2.Handle(&err, -// err2.Err( func(error) { os.Remove(dst) }), // err2.Err() keeps it short -// func(p any) {} // <- handler stops panics, re-throw or not +// err2.Err( func(error) { os.Remove(dst) }), // err2.Err() keeps it short +// // below handler catches panics, but you can re-throw if needed +// func(p any) {} // ) func Handle(err *error, a ...any) { // This and others are similar but we need to call `recover` here because // how how it works with defer. r := recover() - if !handler.WorkToDo(r, err) && !handler.NoerrCallToDo(a...) { + if !handler.WorkToDo(r, err) && !handler.NoerrCallToDo(a) { return } @@ -97,20 +110,21 @@ func Handle(err *error, a ...any) { *err = handler.PreProcess(err, &handler.Info{ CallerName: "Handle", Any: r, - }, a...) + }, a) } // Catch is a convenient helper to those functions that doesn't return errors. -// Note, that Catch always catch the panics. If you don't want to stop them -// (recover) you should add panic handler and continue panicking there. There -// can be only one deferred Catch function per non error returning function like -// main(). There is several ways to use the Catch function. And always remember -// the defer. +// Note that Catch always catch the panics. If you don't want to stop them +// (i.e., use of [recover]) you should add panic handler and continue panicking +// there. There can be only one deferred Catch function per non error returning +// functions, i.e. goroutine functions like main(). There is several ways to use +// the Catch function. And always remember the [defer]. // // The deferred Catch is very convenient, because it makes your current -// goroutine panic and error-safe, one line only! You can fine tune its -// behavior with functions like err2.SetErrorTrace, assert.SetDefault, and -// logging settings. Start with the defaults and simplest version of Catch: +// goroutine panic and error-safe. You can fine tune its 'global' behavior with +// functions like [SetErrorTracer], [SetPanicTracer], and [SetLogTracer]. Its +// 'local' behavior depends the arguments you give it. Let's start with the +// defaults and simplest version of Catch: // // defer err2.Catch() // @@ -123,16 +137,17 @@ func Handle(err *error, a ...any) { // error message about the error source (from where the error was thrown) to the // currently set log. Note, when log stream isn't set, the standard log is used. // It can be bound to, e.g., glog. And if you want to suppress automatic logging -// use the following setup: +// entirely use the following setup: // // err2.SetLogTracer(err2.Stdnull) // // The next one stops errors and panics, but allows you handle errors, like // cleanups, etc. The error handler function has same signature as Handle's -// error handling function, i.e., err2.Handler. By returning nil resets the +// error handling function [Handler]. By returning nil resets the // error, which allows e.g. prevent automatic error logs to happening. -// Otherwise, the output results depends on the current Tracer and assert -// settings. Default setting print call stacks for panics but not for errors: +// Otherwise, the output results depends on the current trace and assert +// settings. The default trace setting prints call stacks for panics but not for +// errors: // // defer err2.Catch(func(err error) error { return err} ) // @@ -140,9 +155,15 @@ func Handle(err *error, a ...any) { // // defer err2.Catch(err2.Noop) // -// The last one calls your error handler, and you have an explicit panic -// handler too, where you can e.g. continue panicking to propagate it for above -// callers or stop it like below: +// You can give unlimited amount of error handlers. They are called if error +// happens and they are called in the same order as they are given or until one +// of them resets the error like [Reset] in the next sample: +// +// defer err2.Catch(err2.Noop, err2.Reset, err2.Log) // err2.Log not called! +// +// The next sample calls your error handler, and you have an explicit panic +// handler as well, where you can e.g. continue panicking to propagate it for +// above callers or stop it like below: // // defer err2.Catch(func(err error) error { return err }, func(p any) {}) func Catch(a ...any) { @@ -158,12 +179,12 @@ func Catch(a ...any) { err = handler.PreProcess(&err, &handler.Info{ CallerName: "Catch", Any: r, - }, a...) + }, a) doTrace(err) } -// Throwf builds and throws (panics) an error. For creation it's similar to -// fmt.Errorf. Because panic is used to transport the error instead of error +// Throwf builds and throws an error (panic). For creation it's similar to +// [fmt.Errorf]. Because panic is used to transport the error instead of error // return value, it's called only if you want to non-local control structure for // error handling, i.e. your current function doesn't have error return value. // @@ -184,59 +205,6 @@ func Throwf(format string, args ...any) { panic(err) } -// Stderr is a built-in helper to use with Handle and Catch. It prints the -// error to stderr and it resets the current error value. It's a handy Catch -// handler in main function. -// -// You can use it like this: -// -// func main() { -// defer err2.Catch(err2.Stderr) -func Stderr(err error) error { - fmt.Fprintln(os.Stderr, err.Error()) - return nil -} - -// Stdout is a built-in helper to use with Handle and Catch. It prints the -// error to stdout and it resets the current error value. It's a handy Catch -// handler in main function. -// -// You can use it like this: -// -// func main() { -// defer err2.Catch(err2.Stdout) -func Stdout(err error) error { - fmt.Fprintln(os.Stdout, err.Error()) - return nil -} - -// Noop is a built-in helper to use with Handle and Catch. It keeps the current -// error value the same. You can use it like this: -// -// defer err2.Handle(&err, err2.Noop) -func Noop(err error) error { return err } - -// Reset is a built-in helper to use with Handle and Catch. It sets the current -// error value to nil. You can use it like this to reset the error: -// -// defer err2.Handle(&err, err2.Reset) -func Reset(error) error { return nil } - -// Err is a built-in helper to use with Handle and Catch. It offers simplifier -// for error handling function for cases where you don't need to change the -// current error value. For instance, if you want to just write error to stdout, -// and don't want to use SetLogTracer and keep it to write to your logs. -// -// defer err2.Catch(err2.Err(func(err error) { -// fmt.Println("ERROR:", err) -// })) -func Err(f func(err error)) func(error) error { - return func(err error) error { - f(err) - return err - } -} - type nullDev struct{} func (nullDev) Write([]byte) (int, error) { return 0, nil } diff --git a/err2_test.go b/err2_test.go index 2d2c36b..ce2cda9 100644 --- a/err2_test.go +++ b/err2_test.go @@ -292,7 +292,7 @@ func TestPanickingCatchAll(t *testing.T) { func() { defer err2.Catch( err2.Noop, - func(v any) {}, + func(any) {}, ) panic("panic") }, @@ -304,7 +304,7 @@ func TestPanickingCatchAll(t *testing.T) { func() { defer err2.Catch( err2.Err(func(error) {}), // Using simplifier - func(v any) {}, + func(any) {}, ) var b []byte b[0] = 0 @@ -453,7 +453,7 @@ func TestPanicking_Handle(t *testing.T) { func() (err error) { defer err2.Handle(&err, func(err error) error { return err }, - func(p any) {}, + func(any) {}, ) panic("panic") }, @@ -463,7 +463,7 @@ func TestPanicking_Handle(t *testing.T) { {"general panic stoped with handler", args{ func() (err error) { - defer err2.Handle(&err, func(p any) {}) + defer err2.Handle(&err, func(any) {}) panic("panic") }, }, @@ -472,7 +472,7 @@ func TestPanicking_Handle(t *testing.T) { {"general panic stoped with handler plus fmt string", args{ func() (err error) { - defer err2.Handle(&err, func(p any) {}, "string") + defer err2.Handle(&err, func(any) {}, "string") panic("panic") }, }, @@ -492,7 +492,7 @@ func TestPanicking_Handle(t *testing.T) { {"runtime.error panic stopped with handler", args{ func() (err error) { - defer err2.Handle(&err, func(p any) {}) + defer err2.Handle(&err, func(any) {}) var b []byte b[0] = 0 return nil @@ -600,12 +600,12 @@ func TestCatch_Panic(t *testing.T) { }() defer err2.Catch( - func(err error) error { + func(error) error { t.Log("it was panic, not an error") t.Fail() // we should not be here return nil }, - func(v any) { + func(any) { panicHandled = true }) @@ -655,9 +655,22 @@ func ExampleHandle_errThrow() { // Output: testing: run example: our error } +func ExampleHandle_annotatedErrReturn() { + normalReturn := func() (err error) { + defer err2.Handle(&err) // automatic annotation + return fmt.Errorf("our error") + } + err := normalReturn() + fmt.Printf("%v", err) + + // ------- func name comes from Go example/test harness + // ------- v ------------------ v -------- + // Output: testing: run example: our error +} + func ExampleHandle_errReturn() { normalReturn := func() (err error) { - defer err2.Handle(&err, "") + defer err2.Handle(&err, nil) // nil disables automatic annotation return fmt.Errorf("our error") } err := normalReturn() @@ -729,7 +742,9 @@ func ExampleHandle_handlerFn() { doSomething := func(a, b int) (err error) { defer err2.Handle(&err, func(err error) error { // Example for just annotating current err. Normally Handle is - // used for cleanup. See CopyFile example for more information. + // used for e.g. cleanup, not annotation that can be left for + // err2 automatic annotation. See CopyFile example for more + // information. return fmt.Errorf("error with (%d, %d): %v", a, b, err) }) try.To1(throw()) @@ -740,6 +755,26 @@ func ExampleHandle_handlerFn() { // Output: error with (1, 2): this is an ERROR } +func ExampleHandle_multipleHandlerFns() { + doSomething := func(a, b int) (err error) { + defer err2.Handle(&err, + // cause automatic annotation <== 2 error handlers do the trick + err2.Noop, + func(err error) error { + // Example for just annotating current err. Normally Handle + // is used for e.g. cleanup, not annotation that can be left + // for err2 automatic annotation. See CopyFile example for + // more information. + return fmt.Errorf("%w error with (%d, %d)", err, a, b) + }) + try.To1(throw()) + return err + } + err := doSomething(1, 2) + fmt.Printf("%v", err) + // Output: testing: run example: this is an ERROR error with (1, 2) +} + func ExampleHandle_noThrow() { doSomething := func(a, b int) (err error) { defer err2.Handle(&err, func(err error) error { diff --git a/formatter.go b/formatter.go index f198f45..994a2b5 100644 --- a/formatter.go +++ b/formatter.go @@ -10,8 +10,12 @@ func init() { } // SetFormatter sets the current formatter for the err2 package. The default -// formatter.Decamel tries to process function names to human readable and the -// idiomatic Go format, i.e. all lowercase, space delimiter, etc. +// [formatter.Decamel] processes function names to human readable and the +// idiomatic Go format, i.e. all lowercase, space delimiter, package names colon +// separated. The example how a quite complex method name gives a proper error +// message prefix: +// +// "ssi.(*DIDAgent).CreateWallet" -> "ssi: didagent create wallet" // // Following line sets a noop formatter where errors are taken as function names // are in the call stack. @@ -24,8 +28,8 @@ func SetFormatter(f formatter.Interface) { fmtstore.SetFormatter(f) } -// Returns the current formatter. See more information from SetFormatter and -// formatter package. +// Returns the current formatter. See more information from [SetFormatter] and +// [formatter] package. func Formatter() formatter.Interface { return fmtstore.Formatter() } diff --git a/formatter/formatter.go b/formatter/formatter.go index 0946ea2..73ae476 100644 --- a/formatter/formatter.go +++ b/formatter/formatter.go @@ -24,18 +24,18 @@ type Formatter struct { DoFmt } -var ( - // Decamel is preimplemented and default formatter to produce human - // readable error strings from function names. - // func CopyFile(..) -> "copy file: file not exists" - // ^-------^ -> generated from CopyFile - Decamel = &Formatter{DoFmt: str.Decamel} +// Decamel is preimplemented and default formatter to produce human +// readable error strings from function names. +// +// func CopyFile(..) -> "copy file: file not exists" +// ^-------^ -> generated from 'func CopyFile' +var Decamel = &Formatter{DoFmt: str.Decamel} - // Noop is preimplemented formatter that does nothing to function name. - // func CopyFile(..) -> "CopyFile: file not exists" - // ^------^ -> function name as it is: CopyFile - Noop = &Formatter{DoFmt: func(i string) string { return i }} -) +// Noop is preimplemented formatter that does nothing to function name. +// +// func CopyFile(..) -> "CopyFile: file not exists" +// ^------^ -> function name as it is: CopyFile +var Noop = &Formatter{DoFmt: func(i string) string { return i }} // Format just calls function set in the DoFmt field. func (f *Formatter) Format(input string) string { diff --git a/handlers.go b/handlers.go new file mode 100644 index 0000000..af3e9c6 --- /dev/null +++ b/handlers.go @@ -0,0 +1,114 @@ +package err2 + +import ( + "fmt" + "os" + + "github.com/lainio/err2/internal/handler" +) + +// Stderr is a built-in helper to use with [Handle] and [Catch]. It prints the +// error to stderr and it resets the current error value. It's a handy [Catch] +// handler in main function. +// +// You can use it like this: +// +// func main() { +// defer err2.Catch(err2.Stderr) +func Stderr(err error) error { + if err == nil { + return nil + } + fmt.Fprintln(os.Stderr, err.Error()) + return nil +} + +// Stdout is a built-in helper to use with [Handle] and [Catch]. It prints the +// error to stdout and it resets the current error value. It's a handy [Catch] +// handler in main function. +// +// You can use it like this: +// +// func main() { +// defer err2.Catch(err2.Stdout) +func Stdout(err error) error { + if err == nil { + return nil + } + fmt.Fprintln(os.Stdout, err.Error()) + return nil +} + +// Noop is a built-in helper to use with [Handle] and [Catch]. It keeps the current +// error value the same. You can use it like this: +// +// defer err2.Handle(&err, err2.Noop) +func Noop(err error) error { return err } + +// Reset is a built-in helper to use with [Handle] and [Catch]. It sets the current +// error value to nil. You can use it like this to reset the error: +// +// defer err2.Handle(&err, err2.Reset) +func Reset(error) error { return nil } + +// Err is a built-in helper to use with [Handle] and [Catch]. It offers simplifier +// for error handling function for cases where you don't need to change the +// current error value. For instance, if you want to just write error to stdout, +// and don't want to use [SetLogTracer] and keep it to write to your logs. +// +// defer err2.Catch(err2.Err(func(err error) { +// fmt.Println("ERROR:", err) +// })) +// +// Note that since Err helper we have other helpers like [Stdout] that allows +// previous block be written as simple as: +// +// defer err2.Catch(err2.Stdout) +func Err(f func(err error)) Handler { + return func(err error) error { + f(err) + return err + } +} + +const lvl = 10 + +// Log is a built-in helper to use with [Handle] and [Catch]. Log prints error +// string to the current log that is set by [SetLogTracer]. +func Log(err error) error { + if err == nil { + return nil + } + _ = handler.LogOutput(lvl, err.Error()) + return err +} + +// StderrNoReset is a built-in helper to use with [Handle] and [Catch]. It prints +// the error to stderr. If you need to reset err value use Stderr instead. +// +// You can use it like this: +// +// func myFunction() { +// defer err2.Handle(err2.Noop, err2.StderrNoReset) +func StderrNoReset(err error) error { + if err == nil { + return nil + } + fmt.Fprintln(os.Stderr, err.Error()) + return err +} + +// StdoutNoReset is a built-in helper to use with [Handle] and [Catch]. It prints +// the error to stdout. +// +// You can use it like this: +// +// func main() { +// defer err2.Catch(err2.StdoutNoReset) +func StdoutNoReset(err error) error { + if err == nil { + return nil + } + fmt.Fprintln(os.Stdout, err.Error()) + return err +} diff --git a/internal/debug/debug.go b/internal/debug/debug.go index 56bfad7..eb94254 100644 --- a/internal/debug/debug.go +++ b/internal/debug/debug.go @@ -159,7 +159,9 @@ func FprintStack(w io.Writer, si StackInfo) { // frames we should go back (Level), and other fields tell how to find the // actual line where calculation should be started. func FuncName(si StackInfo) (n string, ln int, frame int, ok bool) { - stackBuf := bytes.NewBuffer(debug.Stack()) + stack := debug.Stack() + //println(string(stack)) + stackBuf := bytes.NewBuffer(stack) return funcName(stackBuf, si) } diff --git a/internal/debug/debug_test.go b/internal/debug/debug_test.go index 638486a..cef1010 100644 --- a/internal/debug/debug_test.go +++ b/internal/debug/debug_test.go @@ -335,6 +335,7 @@ func TestFuncName(t *testing.T) { {"basic lvl 3", args{input2, StackInfo{"", "Handle", 3, nil, nil}}, "err2.ReturnW", 214, 6}, {"basic lvl 2", args{input2, StackInfo{"lainio/err2", "Handle", 1, nil, nil}}, "err2.ReturnW", 214, 6}, {"method", args{inputFromTest, StackInfo{"", "Handle", 1, nil, nil}}, "ssi.(*DIDAgent).AssertWallet", 146, 8}, + {"pipeline", args{inputPipelineStack, StackInfo{"", "Handle", -1, nil, nil}}, "CopyFile", 29, 9}, } for _, ttv := range tests { tt := ttv @@ -571,4 +572,35 @@ main.test0() main.main() /home/god/go/src/github.com/lainio/ic/main.go:74 +0x1d0 ` + + inputPipelineStack = `goroutine 1 [running]: +runtime/debug.Stack() + /usr/local/go/src/runtime/debug/stack.go:24 +0x64 +github.com/lainio/err2/internal/debug.FuncName({{0x0, 0x0}, {0x12f04a, 0x6}, 0xffffffffffffffff, 0x0, {0x0, 0x0, 0x0}}) + /home/parallels/go/src/github.com/lainio/err2/internal/debug/debug.go:162 +0x44 +github.com/lainio/err2/internal/handler.doBuildFormatStr(0x4000121b58?, 0x9bc5c?) + /home/parallels/go/src/github.com/lainio/err2/internal/handler/handler.go:317 +0x7c +github.com/lainio/err2/internal/handler.buildFormatStr(...) + /home/parallels/go/src/github.com/lainio/err2/internal/handler/handler.go:305 +github.com/lainio/err2/internal/handler.PreProcess(0x4000121d88, 0x4000121ba0, {0x0, 0x0, 0x0}) + /home/parallels/go/src/github.com/lainio/err2/internal/handler/handler.go:280 +0xf8 +github.com/lainio/err2.Handle(0x4000121d88, {0x0, 0x0, 0x0}) + /home/parallels/go/src/github.com/lainio/err2/err2.go:103 +0xd4 +panic({0x115f20?, 0x4000036660?}) + /usr/local/go/src/runtime/panic.go:770 +0x124 +github.com/lainio/err2/try.To(...) + /home/parallels/go/src/github.com/lainio/err2/try/try.go:82 +github.com/lainio/err2/try.To1[...](...) + /home/parallels/go/src/github.com/lainio/err2/try/try.go:97 +main.CopyFile({0x12f23c?, 0x1609c?}, {0x132cef, 0x17}) + /home/parallels/go/src/github.com/lainio/err2/samples/main-play.go:29 +0x254 +main.doMain() + /home/parallels/go/src/github.com/lainio/err2/samples/main-play.go:159 +0x68 +main.doDoMain(...) + /home/parallels/go/src/github.com/lainio/err2/samples/main-play.go:143 +main.doPlayMain() + /home/parallels/go/src/github.com/lainio/err2/samples/main-play.go:136 +0x68 +main.main() + /home/parallels/go/src/github.com/lainio/err2/samples/main.go:38 +0x15c +` ) diff --git a/internal/handler/handler.go b/internal/handler/handler.go index 8a3c295..19dd9f1 100644 --- a/internal/handler/handler.go +++ b/internal/handler/handler.go @@ -18,9 +18,9 @@ import ( type ( // we want these to be type aliases, so they are much nicer to use - PanicHandler = func(p any) - ErrorHandler = func(err error) error // this is only proper type that work - NilHandler = func(err error) error // these two are the same + PanicFn = func(p any) + ErrorFn = func(err error) error // this is only proper type that work + NilFn = func(err error) error // these two are the same //CheckHandler = func(noerr bool, err error) error CheckHandler = func(noerr bool) @@ -41,13 +41,13 @@ type Info struct { // These are called if handler.Process caller sets it. If they aren't set // default implementations are used. NOTE. We have to use both which means // that we get nilHandler call if recovery() is called by any other - // handler then we call still ErrorHandler and get the error from Any. It + // handler then we call still ErrorFn and get the error from Any. It // goes for other way around: we get error but nilHandler is only one to // set, we use that for the error (which is accessed from the closure). - ErrorHandler // If nil default implementation is used. - NilHandler // If nil default (pre-defined here) implementation is used. + ErrorFn // If nil default implementation is used. + NilFn // If nil default (pre-defined here) implementation is used. - PanicHandler // If nil panic() is called. + PanicFn // If nil panic() is called. CheckHandler // this would be for cases where there isn't any error, but // this should be the last defer. @@ -55,6 +55,8 @@ type Info struct { CallerName string werr error + + needErrorAnnotation bool } const ( @@ -77,8 +79,8 @@ func (i *Info) callNilHandler() { if i.safeErr() != nil { i.checkErrorTracer() } - if i.NilHandler != nil { - *i.Err = i.NilHandler(i.werr) + if i.NilFn != nil { + *i.Err = i.NilFn(i.werr) i.werr = *i.Err // remember change both our errors! } else { i.defaultNilHandler() @@ -100,8 +102,16 @@ func (i *Info) checkErrorTracer() { func (i *Info) callErrorHandler() { i.checkErrorTracer() - if i.ErrorHandler != nil { - *i.Err = i.ErrorHandler(i.Any.(error)) + if i.ErrorFn != nil { + // we want to auto-annotate error first and exec ErrorFn then + i.werr = i.workError() + if i.needErrorAnnotation && i.werr != nil { + i.buildFmtErr() + *i.Err = i.ErrorFn(*i.Err) + } else { + *i.Err = i.ErrorFn(i.Any.(error)) + } + i.werr = *i.Err // remember change both our errors! } else { i.defaultErrorHandler() @@ -120,8 +130,8 @@ func (i *Info) checkPanicTracer() { func (i *Info) callPanicHandler() { i.checkPanicTracer() - if i.PanicHandler != nil { - i.PanicHandler(i.Any) + if i.PanicFn != nil { + i.PanicFn(i.Any) } else { panic(i.Any) } @@ -153,8 +163,8 @@ func (i *Info) buildFmtErr() { } func (i *Info) safeCallErrorHandler() { - if i.ErrorHandler != nil { - *i.Err = i.ErrorHandler(i.werr) + if i.ErrorFn != nil { + *i.Err = i.ErrorFn(i.werr) } } @@ -172,8 +182,8 @@ func (i *Info) defaultNilHandler() { } func (i *Info) safeCallNilHandler() { - if i.NilHandler != nil { - *i.Err = i.NilHandler(i.werr) + if i.NilFn != nil { + *i.Err = i.NilFn(i.werr) } } @@ -221,8 +231,10 @@ func WorkToDo(r any, err *error) bool { return (err != nil && *err != nil) || r != nil } -func NoerrCallToDo(a ...any) (yes bool) { - //var yes bool +// NoerrCallToDo returns if we have the _exception case_, aka, func (noerr bool) +// where these handlers are called even normally only error handlers are called, +// i.e. those which have error to handle. +func NoerrCallToDo(a []any) (yes bool) { if len(a) != 0 { _, yes = a[0].(CheckHandler) } @@ -249,7 +261,7 @@ func Process(info *Info) { // PreProcess is currently used for err2 API like err2.Handle and .Catch. // - replaces the Process -func PreProcess(errPtr *error, info *Info, a ...any) error { +func PreProcess(errPtr *error, info *Info, a []any) error { // Bug in Go? // start to use local error ptr only for optimization reasons. // We get 3x faster defer handlers without unsing ptr to original err @@ -265,29 +277,13 @@ func PreProcess(errPtr *error, info *Info, a ...any) error { const lvl = -1 if len(a) > 0 { - subProcess(info, a...) + subProcess(info, a) } else { - fnName := "Handle" // default - if info.CallerName != "" { - fnName = info.CallerName - } - funcName, _, _, ok := debug.FuncName(debug.StackInfo{ - PackageName: "", - FuncName: fnName, - Level: lvl, - }) - if ok { - setFmter := fmtstore.Formatter() - if setFmter != nil { - info.Format = setFmter.Format(funcName) - } else { - info.Format = str.Decamel(funcName) - } - } + buildFormatStr(info, lvl) } - defCatchCallMode := info.PanicHandler == nil && info.CallerName == "Catch" + defCatchCallMode := info.PanicFn == nil && info.CallerName == "Catch" if defCatchCallMode { - info.PanicHandler = PanicNoop + info.PanicFn = PanicNoop } Process(info) @@ -307,45 +303,91 @@ func PreProcess(errPtr *error, info *Info, a ...any) error { return err } -// firstArgIsString not used any more. -func _(a ...any) bool { - if len(a) > 0 { - _, isStr := a[0].(string) - return isStr +func buildFormatStr(info *Info, lvl int) { + if fs, ok := doBuildFormatStr(info, lvl); ok { + info.Format = fs } - return false } -func subProcess(info *Info, a ...any) { +func doBuildFormatStr(info *Info, lvl int) (fs string, ok bool) { + fnName := "Handle" + if info.CallerName != "" { + fnName = info.CallerName + } + funcName, _, _, ok := debug.FuncName(debug.StackInfo{ + PackageName: "", + FuncName: fnName, + Level: lvl, + }) + if ok { + setFmter := fmtstore.Formatter() + if setFmter != nil { + return setFmter.Format(funcName), true + } + return str.Decamel(funcName), true + } + return +} + +func subProcess(info *Info, a []any) { + // not that switch cannot be 0: see call side switch len(a) { - case 2: // currently we support only this order of 2 handlers in Catch - processArg(info, 0, a...) - if _, ok := a[1].(PanicHandler); ok { - processArg(info, 1, a...) + case 0: + msg := `--- +programming error: subProcess: case 0: +---` + fmt.Fprintln(os.Stderr, color.Red()+msg+color.Reset()) + case 1: + processArg(info, 0, a) + default: // case 2, 3, ... + processArg(info, 0, a) + if _, ok := a[1].(PanicFn); ok { + processArg(info, 1, a) + } else if _, ok := a[1].(ErrorFn); ok { + // check second ^ and then change the rest by combining them to + // one that we set to proper places: ErrorFn and NilFn + errorFns, dis := ToErrorFns(a) + autoOn := !dis + hfn := Pipeline(errorFns) + info.ErrorFn = hfn + info.NilFn = hfn + + if fs, ok := doBuildFormatStr(info, -1); autoOn && ok { + //println("fmt:", fs) + info.Format = fs + info.needErrorAnnotation = true + } } - default: - processArg(info, 0, a...) } } -func processArg(info *Info, i int, a ...any) { +func isAutoAnnotationFn(errorFns []ErrorFn) bool { + for _, f := range errorFns { + if f == nil { + return false + } + } + return true +} + +func processArg(info *Info, i int, a []any) { switch first := a[i].(type) { case string: info.Format = first info.Args = a[i+1:] - case ErrorHandler: // err2.Catch uses this - info.ErrorHandler = first - info.NilHandler = first - case PanicHandler: // err2.Catch uses this - info.PanicHandler = first + case ErrorFn: // err2.Catch uses this + info.ErrorFn = first + info.NilFn = first + case PanicFn: // err2.Catch uses this + info.PanicFn = first case CheckHandler: info.CheckHandler = first case nil: - info.NilHandler = NilNoop + info.NilFn = NilNoop default: // we don't panic here because we can already be in recovery, but lets // try to show an RED error message at least. - const msg = `err2 fatal error: + const msg = `err2 fatal error: --- unsupported handler function type: err2.Handle/Catch: see 'err2/scripts/README.md' and run auto-migration scripts for your repo @@ -386,3 +428,37 @@ func LogOutput(lvl int, s string) (err error) { fmt.Fprintln(w, s) return nil } + +// Pipeline is a helper to call several error handlers in a sequence. +// +// defer err2.Handle(&err, err2.Pipeline(err2.Log, MapToHTTPErr)) +func Pipeline(f []ErrorFn) ErrorFn { + return func(err error) error { + for _, handler := range f { + err = handler(err) + } + return err + } +} + +func ToErrorFns(handlerFns []any) (hs []ErrorFn, dis bool) { + count := len(handlerFns) + hs = make([]ErrorFn, 0, count) + for _, a := range handlerFns { + autoAnnotationDisabling := a == nil + if !autoAnnotationDisabling { + if fn, ok := a.(ErrorFn); ok { + hs = append(hs, fn) + } else { + msg := `--- +assertion violation: your handlers should be 'func(error) error' type +---` + fmt.Fprintln(os.Stderr, color.Red()+msg+color.Reset()) + return nil, true + } + } else { + dis = autoAnnotationDisabling + } + } + return hs, dis +} diff --git a/internal/handler/handler_test.go b/internal/handler/handler_test.go index 027e9d7..1214f2f 100644 --- a/internal/handler/handler_test.go +++ b/internal/handler/handler_test.go @@ -30,21 +30,21 @@ func TestProcess(t *testing.T) { }{ {"all nil and our handlers", args{Info: handler.Info{ - Any: nil, - Err: &nilError, - NilHandler: nilHandler, - ErrorHandler: errorHandler, - PanicHandler: panicHandler, + Any: nil, + Err: &nilError, + NilFn: nilHandler, + ErrorFn: errorHandler, + PanicFn: panicHandler, }}, want{ errStr: "error", }}, {"error is transported in panic", args{Info: handler.Info{ - Any: errors.New("error"), - Err: &nilError, - ErrorHandler: errorHandler, - PanicHandler: panicHandler, + Any: errors.New("error"), + Err: &nilError, + ErrorFn: errorHandler, + PanicFn: panicHandler, }}, want{ panicCalled: false, @@ -53,11 +53,11 @@ func TestProcess(t *testing.T) { }}, {"runtime.Error is transported in panic", args{Info: handler.Info{ - Any: myErrRT, - Err: &nilError, - NilHandler: nilHandler, - ErrorHandler: errorHandler, - PanicHandler: panicHandler, + Any: myErrRT, + Err: &nilError, + NilFn: nilHandler, + ErrorFn: errorHandler, + PanicFn: panicHandler, }}, want{ panicCalled: true, @@ -65,11 +65,11 @@ func TestProcess(t *testing.T) { }}, {"panic is transported in panic", args{Info: handler.Info{ - Any: "panic", - Err: &nilError, - NilHandler: nilHandler, - ErrorHandler: errorHandler, - PanicHandler: panicHandler, + Any: "panic", + Err: &nilError, + NilFn: nilHandler, + ErrorFn: errorHandler, + PanicFn: panicHandler, }}, want{ panicCalled: true, @@ -77,11 +77,11 @@ func TestProcess(t *testing.T) { }}, {"error in panic and default format print", args{Info: handler.Info{ - Any: errors.New("error"), - Err: &nilError, - Format: "format %v", - Args: []any{"test"}, - PanicHandler: panicHandler, + Any: errors.New("error"), + Err: &nilError, + Format: "format %v", + Args: []any{"test"}, + PanicFn: panicHandler, }}, want{ panicCalled: false, @@ -89,12 +89,12 @@ func TestProcess(t *testing.T) { }}, {"error transported in panic and our OWN handler", args{Info: handler.Info{ - Any: errors.New("error"), - Err: &nilError, - Format: "format %v", - Args: []any{"test"}, - ErrorHandler: errorHandlerForAnnotate, - PanicHandler: panicHandler, + Any: errors.New("error"), + Err: &nilError, + Format: "format %v", + Args: []any{"test"}, + ErrorFn: errorHandlerForAnnotate, + PanicFn: panicHandler, }}, want{ panicCalled: false, @@ -103,10 +103,10 @@ func TestProcess(t *testing.T) { }}, {"error is transported in error val", args{Info: handler.Info{ - Any: nil, - Err: &myErrVal, - ErrorHandler: errorHandler, - PanicHandler: panicHandler, + Any: nil, + Err: &myErrVal, + ErrorFn: errorHandler, + PanicFn: panicHandler, }}, want{ panicCalled: false, @@ -141,7 +141,7 @@ var Info = handler.Info{ func Handle() { a := []any{} Info.Err = &myErrVal - myErrVal = handler.PreProcess(&myErrVal, &Info, a...) + myErrVal = handler.PreProcess(&myErrVal, &Info, a) } func TestPreProcess_debug(t *testing.T) { @@ -196,11 +196,11 @@ func TestPreProcess(t *testing.T) { {"all nil and our handlers", args{ Info: handler.Info{ - Any: nil, - Err: &nilError, - NilHandler: nilHandler, - ErrorHandler: errorHandlerForAnnotate, - PanicHandler: panicHandler, + Any: nil, + Err: &nilError, + NilFn: nilHandler, + ErrorFn: errorHandlerForAnnotate, + PanicFn: panicHandler, }, a: []any{"test"}}, // no affec because want{ @@ -241,7 +241,7 @@ func TestPreProcess(t *testing.T) { var err = x.Whom(tt.args.Info.Err != nil, *tt.args.Info.Err, nil) - err = handler.PreProcess(&err, &tt.args.Info, tt.args.a...) + err = handler.PreProcess(&err, &tt.args.Info, tt.args.a) test.RequireEqual(t, panicHandlerCalled, tt.want.panicCalled) test.RequireEqual(t, errorHandlerCalled, tt.want.errorCalled) diff --git a/internal/handler/handlers_test.go b/internal/handler/handlers_test.go new file mode 100644 index 0000000..2e5b700 --- /dev/null +++ b/internal/handler/handlers_test.go @@ -0,0 +1,60 @@ +package handler_test + +import ( + "testing" + + "github.com/lainio/err2" + "github.com/lainio/err2/internal/handler" + "github.com/lainio/err2/internal/test" +) + +func TestHandlers(t *testing.T) { + t.Parallel() + type args struct { + f []any // we use any because it's same as real-world case at start + } + tests := []struct { + name string + args args + want error + dis bool + }{ + {"one", args{f: []any{err2.Noop}}, err2.ErrNotFound, false}, + {"one disabled NOT real case", args{f: []any{nil}}, err2.ErrNotFound, true}, + {"two", args{f: []any{err2.Noop, err2.Noop}}, err2.ErrNotFound, false}, + {"three", args{f: []any{err2.Noop, err2.Noop, err2.Noop}}, err2.ErrNotFound, false}, + {"three last disabled", args{f: []any{err2.Noop, err2.Noop, nil}}, err2.ErrNotFound, true}, + {"three 2nd disabled", args{f: []any{err2.Noop, nil, err2.Noop}}, err2.ErrNotFound, true}, + {"three all disabled", args{f: []any{nil, nil, nil}}, err2.ErrNotFound, true}, + {"reset", args{f: []any{err2.Noop, err2.Noop, err2.Reset}}, nil, false}, + {"reset and disabled", args{f: []any{nil, err2.Noop, err2.Reset}}, nil, true}, + {"reset first", args{f: []any{err2.Reset, err2.Noop, err2.Noop}}, nil, false}, + {"reset second", args{f: []any{err2.Noop, err2.Reset, err2.Noop}}, nil, false}, + {"set new first", args{f: []any{ + func(error) error { return err2.ErrAlreadyExist }, err2.Noop}}, err2.ErrAlreadyExist, false}, + {"set new second", args{f: []any{err2.Noop, + func(error) error { return err2.ErrAlreadyExist }, err2.Noop}}, err2.ErrAlreadyExist, false}, + {"set new first and reset", args{f: []any{ + func(error) error { return err2.ErrAlreadyExist }, err2.Reset}}, nil, false}, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + anys := tt.args.f + + test.Require(t, anys != nil, "cannot be nil") + fns, dis := handler.ToErrorFns(anys) + test.Require(t, fns != nil, "cannot be nil") + test.Require(t, dis == tt.dis, "disabled wanted") + + errHandler := handler.Pipeline(fns) + err := errHandler(err2.ErrNotFound) + if err == nil { + test.Require(t, tt.want == nil) + } else { + test.RequireEqual(t, err.Error(), tt.want.Error()) + } + }) + } +} diff --git a/internal/str/str.go b/internal/str/str.go index 183b904..c9d9d13 100644 --- a/internal/str/str.go +++ b/internal/str/str.go @@ -15,8 +15,8 @@ var ( clean = regexp.MustCompile(`[^\w]`) ) -// DecamelRegexp return the given string as space delimeted. Note! it's slow. Use -// Decamel instead. +// DecamelRegexp return the given string as space delimeted. Note! It's slow. +// Use [Decamel] instead. It's left here for learning purposes. func DecamelRegexp(str string) string { str = clean.ReplaceAllString(str, " ") str = uncamel.ReplaceAllString(str, ` $1`) @@ -78,7 +78,7 @@ func Decamel(s string) string { // separated filename, and line number. If frame cannot be found ok is false. // // See more information from runtime.Caller. The skip tells how many stack -// frames are skipped. Note, that FuncName calculates itself to skip frames. +// frames are skipped. Note that FuncName calculates itself the skip frames. func FuncName(skip int, long bool) (n, fname string, ln int, ok bool) { pc, file, ln, yes := runtime.Caller(skip + 1) // +1 skip ourself if yes { diff --git a/internal/str/str_test.go b/internal/str/str_test.go index 7e16b35..45d02b1 100644 --- a/internal/str/str_test.go +++ b/internal/str/str_test.go @@ -65,7 +65,7 @@ func TestDecamel(t *testing.T) { {"acronym", args{"ARMCamelString"}, "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).AssertWallet"}, "ssi: didagent assert wallet"}, + {"package name and simple method", args{"ssi.(*DIDAgent).CreateWallet"}, "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"}, {"unnatural method and anonym", args{"(**DIDAgent)...AssertWallet...Func1"}, "didagent assert wallet: func1"}, diff --git a/samples/main-play.go b/samples/main-play.go index 746ed98..035ee3e 100644 --- a/samples/main-play.go +++ b/samples/main-play.go @@ -3,7 +3,7 @@ // favorite editor and start to play with the main.go file. The comments on it // guide you. // -// We have only a few examples built over the CopyFile and callRecur functions, +// We have only a few examples built over the [CopyFile] and [CallRecur] functions, // but with them you can try all the important APIs from err2, try, and assert. // Just follow the comments and try suggested things :-) package main @@ -90,7 +90,7 @@ func OrgCopyFile(src, dst string) (err error) { return nil } -func callRecur(d int) (err error) { +func CallRecur(d int) (err error) { defer err2.Handle(&err) return doRecur(d) @@ -112,13 +112,6 @@ func doPlayMain() { // Keep here that you can play without changing imports assert.That(true) - // If asserts are treated as panics instead of errors, you get the stack trace. - // you can try that by taking the next line out of the comment: - assert.SetDefault(assert.Development) - - // same thing but one line assert msg - //assert.SetDefault(assert.Production) - // To see how automatic stack tracing works. //err2.SetErrorTracer(os.Stderr) @@ -143,7 +136,7 @@ func doPlayMain() { doDoMain() //try.To(doMain()) - println("______===") + println("___ happy ending ===") } func doDoMain() { @@ -151,23 +144,31 @@ func doDoMain() { } func doMain() (err error) { + // Example of Handle/Catch API where we can have multiple handlers. + // Note that this is a silly sample where logging is done trice and noops + // are used without a purpose. All of this is that you get an idea how you + // could use the error handlers and chain them together. + + //defer err2.Handle(&err, err2.Noop, err2.Log, err2.Log) + //defer err2.Handle(&err, nil, err2.Noop, err2.Log) + //defer err2.Handle(&err, nil, err2.Log) defer err2.Handle(&err) // You can select any one of the try.To(CopyFile lines to play with and see // how err2 works. Especially interesting is automatic stack tracing. // // source file exists, but the destination is not in high probability - try.To(CopyFile("main.go", "/notfound/path/file.bak")) + //try.To(OrgCopyFile("main.go", "/notfound/path/file.bak")) // Both source and destination don't exist - //try.To(CopyFile("/notfound/path/file.go", "/notfound/path/file.bak")) + //try.To(OrgCopyFile("/notfound/path/file.go", "/notfound/path/file.bak")) // 2nd argument is empty - //try.To(CopyFile("main.go", "")) + try.To(OrgCopyFile("main.go", "")) // Next fn demonstrates how error and panic traces work, comment out all // above CopyFile calls to play with: - try.To(callRecur(1)) + try.To(CallRecur(1)) println("=== you cannot see this ===") return nil diff --git a/samples/main.go b/samples/main.go index 6961bb8..afd933d 100644 --- a/samples/main.go +++ b/samples/main.go @@ -3,6 +3,7 @@ package main import ( "flag" "log" + "os" "github.com/lainio/err2" ) @@ -12,6 +13,12 @@ var ( isErr = flag.Bool("err", false, "tells if we want to have an error") ) +func init() { + // highlight that this is before flag.Parse to allow it to work properly. + err2.SetLogTracer(os.Stderr) // for import + err2.SetLogTracer(nil) +} + func main() { defer err2.Catch(err2.Stderr) log.SetFlags(log.Lshortfile | log.LstdFlags) diff --git a/tracer.go b/tracer.go index e13fdaa..954768f 100644 --- a/tracer.go +++ b/tracer.go @@ -6,60 +6,85 @@ import ( "github.com/lainio/err2/internal/tracer" ) -// ErrorTracer returns current io.Writer for automatic error stack tracing. +// ErrorTracer returns current [io.Writer] for automatic error stack tracing. // The default value is nil. func ErrorTracer() 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. +// 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]. func PanicTracer() io.Writer { return tracer.Panic.Tracer() } -// LogTracer returns current io.Writer for try.Out().Logf(). -// The default value is nil. +// LogTracer returns a current [io.Writer] for the explicit [try.Result.Logf] +// function and automatic logging used in [Handle] and [Catch]. The +// default value is nil. func LogTracer() io.Writer { return tracer.Log.Tracer() } -// SetErrorTracer sets a io.Writer for automatic error stack tracing. The err2 -// default is nil. Note that the current function is capable to print error -// stack trace when the function has at least one deferred error handler, e.g: +// SetErrorTracer sets a [io.Writer] for automatic error stack tracing. The err2 +// default is nil. Note that any function that has deferred [Handle] or [Catch] +// is capable to print error stack trace: // // func CopyFile(src, dst string) (err error) { -// defer err2.Handle(&err) // <- error trace print decision is done here +// defer err2.Handle(&err) // <- makes error trace printing decision +// +// 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. +// +// 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) } -// 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 +// 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 // to print panic stack trace when the function has at least one deferred error // handler, e.g: // // func CopyFile(src, dst string) (err error) { -// defer err2.Handle(&err) // <- error trace print decision is done here +// defer err2.Handle(&err) // <- panic trace print decision is done here +// +// Remember that you can reset these with [flag] package support. See +// documentation of err2 package's flag section. func SetPanicTracer(w io.Writer) { tracer.Panic.SetTracer(w) } -// SetLogTracer sets a io.Writer for try.Out().Logf() function. The default is -// nil and then err2 uses std log package for logging. +// SetLogTracer sets a current [io.Writer] for the explicit [try.Result.Logf] +// function and automatic logging used in [Handle] and [Catch]. The +// default is nil and then err2 uses std log package for logging. // -// You can use that to redirect packages like glog and have proper logging. For -// glog, add this line at the beginning of your app: +// You can use the std log package to redirect other logging packages like [glog] +// to automatically work with the err2 package. For the [glog], add this line at +// the beginning of your app: // // glog.CopyStandardLogTo("INFO") +// +// Remember that you can reset these with [flag] package support. See +// documentation of err2 package's flag section. func SetLogTracer(w io.Writer) { tracer.Log.SetTracer(w) } -// SetTracers a convenient helper to set a io.Writer for error and panic stack -// tracing. More information see SetErrorTracer and SetPanicTracer functions. +// SetTracers a helper to set a [io.Writer] for error and panic stack tracing, the +// log tracer is set as well. More information see [SetErrorTracer], +// [SetPanicTracer], and [SetLogTracer] functions. +// +// Remember that you can reset these with [flag] package support. See +// documentation of err2 package's flag section. func SetTracers(w io.Writer) { tracer.Error.SetTracer(w) tracer.Panic.SetTracer(w) diff --git a/try/copy_test.go b/try/copy_test.go index 3978c7d..c11389c 100644 --- a/try/copy_test.go +++ b/try/copy_test.go @@ -3,6 +3,7 @@ package try_test import ( "bufio" "bytes" + "errors" "io" "os" "testing" @@ -13,6 +14,19 @@ import ( const dataFile = "./try.go" +func Benchmark_CopyBufferMy(b *testing.B) { + all, err := os.ReadFile(dataFile) + test.Requiref(b, err == nil, "error: %v", err) + test.Require(b, all != nil) + + buf := make([]byte, 4) + dst := bufio.NewWriter(bytes.NewBuffer(make([]byte, 0, len(all)))) + src := bytes.NewReader(all) + for n := 0; n < b.N; n++ { + try.To1(myCopyBuffer(dst, src, buf)) + } +} + func Benchmark_CopyBufferStd(b *testing.B) { all, err := os.ReadFile(dataFile) test.Requiref(b, err == nil, "error: %v", err) @@ -40,3 +54,36 @@ func Benchmark_CopyBufferOur(b *testing.B) { } } } + +// myCopyBuffer is copy/paste from Go std lib to remove noice and measure only a +// loop +func myCopyBuffer(dst io.Writer, src io.Reader, buf []byte) (written int64, err error) { + for { + nr, er := src.Read(buf) + if nr > 0 { + nw, ew := dst.Write(buf[0:nr]) + if nw < 0 || nr < nw { + nw = 0 + if ew == nil { + ew = errors.New("invalid write result") + } + } + written += int64(nw) + if ew != nil { + err = ew + break + } + if nr != nw { + err = io.ErrShortWrite + break + } + } + if er != nil { + if er != io.EOF { + err = er + } + break + } + } + return written, err +} diff --git a/try/out.go b/try/out.go index d51bf3b..d73472c 100644 --- a/try/out.go +++ b/try/out.go @@ -8,23 +8,23 @@ import ( ) type ( - // ErrFn is function type for try.OutX handlers. - ErrFn = func(err error) error + // ErrFn is function type for [try.Out] handlers. + ErrFn = handler.ErrorFn - // Result is the base of our error handling language for try.Out functions. + // Result is the base of our error handling language for [try.Out] functions. Result struct { // Err holds the error value returned from try.Out function result. Err error } - // Result1 is the base of our error handling DSL for try.Out1 functions. + // Result1 is the base of our error handling DSL for [try.Out1] functions. Result1[T any] struct { // Val1 holds the first value returned from try.Out1 function result. Val1 T Result } - // Result2 is the base of our error handling DSL for try.Out2 functions. + // Result2 is the base of our error handling DSL for [try.Out2] functions. Result2[T any, U any] struct { // Val2 holds the first value returned from try.Out2 function result. Val2 U @@ -32,8 +32,8 @@ type ( } ) -// Logf prints a log line to pre-set logging stream (err2.SetLogWriter) -// if the current Result.Err != nil. Logf follows Printf formatting logic. The +// Logf prints a log line to pre-set logging stream [err2.SetLogWriter] +// if the current [Result.Err] != nil. Logf follows Printf formatting logic. The // current error value will be added at the end of the logline with ": %v\n", // err. For example, the line: // @@ -43,11 +43,11 @@ type ( // // error sending response: UDP not listening func (o *Result) Logf(a ...any) *Result { - return o.logf(logfFrameLvl, a...) + return o.logf(logfFrameLvl, a) } -// Logf prints a log line to pre-set logging stream (err2.SetLogWriter) -// if the current Result.Err != nil. Logf follows Printf formatting logic. The +// Logf prints a log line to pre-set logging stream [err2.SetLogWriter] +// if the current [Result.Err] != nil. Logf follows Printf formatting logic. The // current error value will be added at the end of the logline with ": %v\n", // err. For example, the line: // @@ -57,12 +57,12 @@ func (o *Result) Logf(a ...any) *Result { // // error sending response: UDP not listening func (o *Result1[T]) Logf(a ...any) *Result1[T] { - o.Result.logf(logfFrameLvl, a...) + o.Result.logf(logfFrameLvl, a) return o } -// Logf prints a log line to pre-set logging stream (err2.SetLogWriter) -// if the current Result.Err != nil. Logf follows Printf formatting logic. The +// Logf prints a log line to pre-set logging stream [err2.SetLogWriter] +// if the current [Result.Err] != nil. Logf follows Printf formatting logic. The // current error value will be added at the end of the logline with ": %v\n", // err. For example, the line: // @@ -72,25 +72,25 @@ func (o *Result1[T]) Logf(a ...any) *Result1[T] { // // error sending response: UDP not listening func (o *Result2[T, U]) Logf(a ...any) *Result2[T, U] { - o.Result.logf(logfFrameLvl, a...) + o.Result.logf(logfFrameLvl, a) return o } -// Handle allows you to add an error handler to try.Out handler chain. Handle +// Handle allows you to add an error handler to [try.Out] handler chain. Handle // is a general purpose error handling function. It can handle several error // handling cases: // - if no argument is given and .Err != nil, it throws an error value immediately -// - if two arguments (errTarget, ErrFn) and Is(.Err, errTarget) ErrFn is called +// - if two arguments (errTarget, ErrFn) and Is(.Err, errTarget) [ErrFn] is called // - if first argument is (string) and .Err != nil the error value is annotated and thrown -// - if first argument is (ErrFn) and .Err != nil, it calls ErrFn +// - if first argument is (ErrFn) and .Err != nil, it calls [ErrFn] // -// The handler function (ErrFn) can process and annotate the incoming error how +// The handler function [ErrFn] can process and annotate the incoming error how // it wants and returning error value decides if error is thrown. Handle // annotates and throws an error immediately i.e. terminates error handling DSL -// chain if Result.Err != nil. Handle supports error annotation similarly as -// fmt.Errorf. +// chain if [Result.Err] != nil. Handle supports error annotation similarly as +// [fmt.Errorf]. // -// For instance, to implement same as try.To(), you could do the following: +// For instance, to implement same as [try.To], you could do the following: // // d := try.Out(json.Unmarshal(b, &v)).Handle() func (o *Result) Handle(a ...any) *Result { @@ -124,21 +124,21 @@ func (o *Result) Handle(a ...any) *Result { return o } -// Handle allows you to add an error handler to try.Out handler chain. Handle +// Handle allows you to add an error handler to [try.Out] handler chain. Handle // is a general purpose error handling function. It can handle several error // handling cases: // - if no argument is given and .Err != nil, it throws an error value immediately -// - if two arguments (errTarget, ErrFn) and Is(.Err, errTarget) ErrFn is called +// - if two arguments (errTarget, [ErrFn]) and Is(.Err, errTarget) [ErrFn] is called // - if first argument is (string) and .Err != nil the error value is annotated and thrown -// - if first argument is (ErrFn) and .Err != nil, it calls ErrFn +// - if first argument is [ErrFn] and .Err != nil, it calls [ErrFn] // -// The handler function (ErrFn) can process and annotate the incoming error how +// The handler function [ErrFn] can process and annotate the incoming error how // it wants and returning error value decides if error is thrown. Handle // annotates and throws an error immediately i.e. terminates error handling DSL -// chain if Result.Err != nil. Handle supports error annotation similarly as -// fmt.Errorf. +// chain if [Result.Err] != nil. Handle supports error annotation similarly as +// [fmt.Errorf]. // -// For instance, to implement same as try.To(), you could do the following: +// For instance, to implement same as [try.To], you could do the following: // // d := try.Out(json.Unmarshal(b, &v)).Handle() func (o *Result1[T]) Handle(a ...any) *Result1[T] { @@ -146,21 +146,21 @@ func (o *Result1[T]) Handle(a ...any) *Result1[T] { return o } -// Handle allows you to add an error handler to try.Out handler chain. Handle +// Handle allows you to add an error handler to [try.Out] handler chain. Handle // is a general purpose error handling function. It can handle several error // handling cases: // - if no argument is given and .Err != nil, it throws an error value immediately -// - if two arguments (errTarget, ErrFn) and Is(.Err, errTarget) ErrFn is called +// - if two arguments (errTarget, [ErrFn]) and Is(.Err, errTarget) ErrFn is called // - if first argument is (string) and .Err != nil the error value is annotated and thrown -// - if first argument is (ErrFn) and .Err != nil, it calls ErrFn +// - if first argument is [ErrFn] and .Err != nil, it calls [ErrFn] // -// The handler function (ErrFn) can process and annotate the incoming error how +// The handler function [ErrFn] can process and annotate the incoming error how // it wants and returning error value decides if error is thrown. Handle // annotates and throws an error immediately i.e. terminates error handling DSL -// chain if Result.Err != nil. Handle supports error annotation similarly as -// fmt.Errorf. +// chain if [Result.Err] != nil. Handle supports error annotation similarly as +// [fmt.Errorf]. // -// For instance, to implement same as try.To(), you could do the following: +// For instance, to implement same as [try.To], you could do the following: // // d := try.Out(json.Unmarshal(b, &v)).Handle() func (o *Result2[T, U]) Handle(a ...any) *Result2[T, U] { @@ -168,8 +168,8 @@ func (o *Result2[T, U]) Handle(a ...any) *Result2[T, U] { return o } -// Catch catches the error and sets Result.Val1 if given. The value is used -// only in the case if Result.Err != nil. Catch returns the Val1 in all cases. +// Catch catches the error and sets [Result1.Val1] if given. The value is used +// only in the case if [Result1.Err] != nil. Catch returns the Val1 in all cases. func (o *Result1[T]) Catch(v ...T) T { if o.Err != nil && len(v) == 1 { o.Val1 = v[0] @@ -177,10 +177,10 @@ func (o *Result1[T]) Catch(v ...T) T { return o.Val1 } -// Catch catches the error and sets Result.Val1/Val2 if given. The value(s) is -// used in the case of Result.Err != nil. Catch returns the Val1 and Val2 in all -// cases. In case you want to set only Val2's default value, use Def2 before -// Catch call. +// Catch catches the error and sets [Result2.Val1] [Result2.Val2] if given. The +// value(s) is used in the case of [Result2.Err] != nil. Catch returns the [Val1] and +// [Val2] in all cases. In case you want to set only [Val2]'s default value, use +// [Def2] before Catch call. func (o *Result2[T, U]) Catch(a ...any) (T, U) { if o.Err != nil { switch len(a) { @@ -194,8 +194,8 @@ func (o *Result2[T, U]) Catch(a ...any) (T, U) { return o.Val1, o.Val2 } -// Def1 sets default value for Result.Val1. The value is returned in case of -// Result.Err != nil. +// Def1 sets default value for [Result.Val1.] The value is returned in case of +// [Result.Err] != nil. func (o *Result1[T]) Def1(v T) *Result1[T] { if o.Err == nil { return o @@ -204,19 +204,28 @@ func (o *Result1[T]) Def1(v T) *Result1[T] { return o } -// Def2 sets default value for Result.Val2. The value is returned in case of -// Result.Err != nil. -func (o *Result2[T, U]) Def2(v T, v2 U) *Result2[T, U] { +// Def1 sets default value for [Result.Val1]. The value is returned in case of +// [Result.Err] != nil. +func (o *Result2[T, U]) Def1(v T) *Result2[T, U] { if o.Err == nil { return o } o.Val1 = v + return o +} + +// Def2 sets default value for [Result.Val2]. The value is returned in case of +// [Result.Err] != nil. +func (o *Result2[T, U]) Def2(v2 U) *Result2[T, U] { + if o.Err == nil { + return o + } o.Val2 = v2 return o } // Out is a helper function to call functions which returns (error) and start -// error handling with DSL. For instance, to implement same as try.To(), you +// error handling with DSL. For instance, to implement same as [try.To], you // could do the following: // // d := try.Out(json.Unmarshal(b, &v)).Handle() @@ -229,8 +238,8 @@ func Out(err error) *Result { } // Out1 is a helper function to call functions which returns (T, error). That -// allows you to use Result1, which makes possible to -// start error handling with DSL. For instance, instead of try.To1() you could +// allows you to use [Result1], which makes possible to +// start error handling with DSL. For instance, instead of [try.To1] you could // do the following: // // d := try.Out1(os.ReadFile(filename).Handle().Val1 @@ -244,15 +253,15 @@ func Out1[T any](v T, err error) *Result1[T] { } // Out2 is a helper function to call functions which returns (T, error). That -// allows you to use Result2, which makes possible to -// start error handling with DSL. For instance, instead of try.To2() you could +// allows you to use [Result2], which makes possible to +// start error handling with DSL. For instance, instead of [try.To2] you could // do the following: // // token := try.Out2(p.ParseUnverified(tokenStr, &customClaims{})).Handle().Val1 // // or in some other cases, some of these would be desired action: // -// x, y := try.Out2(convTwoStr(s1, s2)).Logf("wrong number").Catch(1, 2) +// x, y := try.Out2(convTwoStr(s1, s2)).Logf("bad number").Catch(1, 2) // y := try.Out2(convTwoStr(s1, s2)).Handle().Val2 func Out2[T any, U any](v1 T, v2 U, err error) *Result2[T, U] { return &Result2[T, U]{Val2: v2, Result1: Result1[T]{Val1: v1, Result: Result{Err: err}}} @@ -262,7 +271,7 @@ func wrapStr() string { return ": %w" } -func (o *Result) logf(lvl int, a ...any) *Result { +func (o *Result) logf(lvl int, a []any) *Result { if o.Err == nil { return o } diff --git a/try/out_test.go b/try/out_test.go index d032771..454bfdf 100644 --- a/try/out_test.go +++ b/try/out_test.go @@ -26,7 +26,7 @@ func ExampleOut1_copyFile() { // If you prefer immediate error handling for some reason. _ = try.Out1(io.Copy(w, r)). - Handle(io.EOF, func(err error) error { + Handle(io.EOF, func(error) error { fmt.Println("err == io.EOF") return nil // by returning nil we can reset the error // return err // fallthru to next check if err != nil @@ -81,19 +81,16 @@ func ExampleResult1_Logf() { return try.Out1(strconv.Atoi(s)).Logf("not number").Catch(100) } num1 := countSomething("1") - num2 := countSomething("WRONG") + num2 := countSomething("BAD") fmt.Printf("results: %d, %d", num1, num2) err2.SetLogTracer(nil) - // Output: not number: strconv.Atoi: parsing "WRONG": invalid syntax + // Output: not number: strconv.Atoi: parsing "BAD": invalid syntax // results: 1, 100 } func TestResult2_Logf(t *testing.T) { t.Parallel() - // Set log tracing to stdout that we can see it in Example output. In - // normal cases that would be a Logging stream or stderr. - err2.SetLogTracer(os.Stdout) convTwoStr := func(s1, s2 string) (_ int, _ int, err error) { defer err2.Handle(&err, nil) @@ -101,11 +98,10 @@ func TestResult2_Logf(t *testing.T) { return try.To1(strconv.Atoi(s1)), try.To1(strconv.Atoi(s2)), nil } countSomething := func(s1, s2 string) (int, int) { - v1, v2 := try.Out2(convTwoStr(s1, s2)).Logf("wrong number").Catch(1, 2) + v1, v2 := try.Out2(convTwoStr(s1, s2)).Logf("bad number").Catch(1, 2) return v1 + v2, v2 } - num1, num2 := countSomething("1", "err") - fmt.Printf("results: %d, %d\n", num1, num2) + num1, num2 := countSomething("1", "bad") test.RequireEqual(t, num2, 2) test.RequireEqual(t, num1, 3) } @@ -136,7 +132,7 @@ func ExampleResult1_Handle() { callRead := func(in io.Reader, b []byte) (eof bool, n int) { // we should use try.To1, but this is sample of try.Out.Handle n = try.Out1(in.Read(b)). - Handle(io.EOF, func(err error) error { + Handle(io.EOF, func(error) error { eof = true return nil }). // our errors.Is == true, handler to get eof status diff --git a/try/result2_test.go b/try/result2_test.go index 15a4954..a05b536 100644 --- a/try/result2_test.go +++ b/try/result2_test.go @@ -21,14 +21,14 @@ func ExampleResult2_Logf() { err2.SetLogTracer(os.Stdout) countSomething := func(s1, s2 string) (int, int) { - r := try.Out2(convTwoStr(s1, s2)).Logf().Def2(10, 10) + r := try.Out2(convTwoStr(s1, s2)).Logf().Def1(10).Def2(10) v1, v2 := r.Val1, r.Val2 return v1 + v2, v2 } _, _ = countSomething("1", "2") - num1, num2 := countSomething("WRONG", "2") + num1, num2 := countSomething("BAD", "2") fmt.Printf("results: %d, %d", num1, num2) err2.SetLogTracer(nil) - // Output: testing: run example: strconv.Atoi: parsing "WRONG": invalid syntax + // Output: testing: run example: strconv.Atoi: parsing "BAD": invalid syntax // results: 20, 10 } diff --git a/try/try.go b/try/try.go index 07e5137..641853f 100644 --- a/try/try.go +++ b/try/try.go @@ -1,6 +1,6 @@ /* -Package try is a package for try.ToX functions that implement the error -checking. try.ToX functions check 'if err != nil' and if it throws the err to the +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: @@ -10,7 +10,7 @@ about err2 and try packager roles can be seen in the FileCopy example: w := try.To1(os.Create(dst)) defer err2.Handle(&err, func(error) error { - try.To(os.Remove(dst)).Logf() + try.Out(os.Remove(dst)).Logf() return nil }) defer w.Close() @@ -20,31 +20,33 @@ about err2 and try packager roles can be seen in the FileCopy example: # try.To — Fast Checking -All of the try.To functions are as fast as the simple 'if err != nil {' +All of the [To] functions are as fast as the simple 'if err != nil {' statement, thanks to the compiler inlining and optimization. -Note that try.ToX function names end to a number (x) because: +We have three error check functions: [To], [To1], and [To2] because: "No variadic type parameters. There is no support for variadic type parameters, which would permit writing a single generic function that takes different numbers of both type parameters and regular parameters." - Go Generics -The leading number at the end of the To2 tells that To2 takes two different -non-error arguments, and the third one must be an error value. +For example, the leading number at the end of the [To2] tells that [To2] takes +two different non-error arguments, and the third one must be an error value. -Looking at the FileCopy example again, you see that all the functions -are directed to try.To1 are returning (type1, error) tuples. All of these -tuples are the correct input to try.To1. However, if you have a function that -returns (type1, type2, error), you must use try.To2 function to check the error. -Currently the try.To3 takes (3 + 1) return values which is the greatest amount. +Looking at the [CopyFile] example again, you see that all the functions +are directed to [To1] are returning (type1, error) tuples. All of these +tuples are the correct input to [To1]. However, if you have a function that +returns (type1, type2, error), you must use [To2] function to check the error. +Currently the [To3] takes (3 + 1) return values which is the greatest amount. If more is needed, let us know. # try.Out — Error Handling Language -The try package offers an error handling DSL. It's for cases where you want to -do something specific after error returing function call. For example, you might -want to ignore the specific error and use a default value. That's possible with -the following code: +The try package offers an error handling DSL that's based on [Out], [Out1], and +[Out2] functions and their corresponding return values [Result], [Result1], and +[Result2]. DSL is for the cases where you want to do something specific after +error returning function call. Those cases are rare. But you might want, for +example, to ignore the specific error and use a default value without any +special error handling. That's possible with the following code: number := try.Out1(strconv.Atoi(str)).Catch(100) @@ -56,7 +58,8 @@ Or you might just want to change it later to error return: try.Out(os.Remove(dst)).Handle("file cleanup fail") -Please see the documentation and examples of ResultX types and their methods. +Please see the documentation and examples of [Result], [Result1], and [Result2] +types and their methods. */ package try @@ -71,7 +74,7 @@ import ( // check the value. If an error occurs, it panics the error so that err2 // handlers can catch it if needed. Note! If no err2.Handle or err2.Catch exist // in the call stack and To panics an error, the error is not handled, and the -// app will crash. When using try.To functions you should always have proper +// app will crash. When using To function you should always have proper // err2.Handle or err2.Catch statements in the call stack. // // defer err2.Handle(&err) @@ -87,7 +90,7 @@ func To(err error) { // and check the error value. If an error occurs, it panics the error so that // err2 handlers can catch it if needed. Note! If no err2.Handle or err2.Catch // exist in the call stack and To1 panics an error, the error is not handled, -// and the app will crash. When using try.To1 functions you should always have +// and the app will crash. When using To1 function you should always have // proper err2.Handle or err2.Catch statements in the call stack. // // defer err2.Handle(&err) @@ -102,7 +105,7 @@ func To1[T any](v T, err error) T { // and check the error value. If an error occurs, it panics the error so that // err2 handlers can catch it if needed. Note! If no err2.Handle or err2.Catch // exist in the call stack and To2 panics an error, the error is not handled, -// and the app will crash. When using try.To2 functions you should always have +// and the app will crash. When using To2 function you should always have // proper err2.Handle or err2.Catch statements in the call stack. // // defer err2.Handle(&err) @@ -117,7 +120,7 @@ func To2[T, U any](v1 T, v2 U, err error) (T, U) { // error) and check the error value. If an error occurs, it panics the error so // that err2 handlers can catch it if needed. Note! If no err2.Handle or // err2.Catch exist in the call stack and To3 panics an error, the error is -// not handled, and the app will crash. When using try.To3 functions you should +// not handled, and the app will crash. When using To3 function you should // always have proper err2.Handle or err2.Catch statements in the call stack. func To3[T, U, V any](v1 T, v2 U, v3 V, err error) (T, U, V) { To(err) @@ -125,7 +128,7 @@ func To3[T, U, V any](v1 T, v2 U, v3 V, err error) (T, U, V) { } // Is function performs a filtered error check for the given argument. It's the -// same as To function, but it checks if the error matches the filter before +// same as [To] function, but it checks if the error matches the filter before // throwing an error. The false return value tells that there are no errors and // the true value that the error is the filter. func Is(err, filter error) bool { @@ -138,100 +141,101 @@ func Is(err, filter error) bool { return false } -// IsEOF1 function performs a filtered error check for the given argument. It's the -// same as To function, but it checks if the error matches the 'io.EOF' before -// throwing an error. The false return value tells that there are no errors and -// the true value that the error is the 'io.EOF'. +// IsEOF1 function performs a filtered error check for the given argument. It's +// the same as [To] function, but it checks if the error matches the [io.EOF] +// before throwing an error. The false return value tells that there are no +// errors and the true value that the error is the [io.EOF]. func IsEOF1[T any](v T, err error) (bool, T) { isFilter := Is(err, io.EOF) return isFilter, v } // IsEOF2 function performs a filtered error check for the given argument. It's the -// same as To function, but it checks if the error matches the 'io.EOF' before +// same as [To] function, but it checks if the error matches the [io.EOF] before // throwing an error. The false return value tells that there are no errors and -// the true value that the error is the 'io.EOF'. +// the true value that the error is the [io.EOF]. func IsEOF2[T, U any](v1 T, v2 U, err error) (bool, T, U) { isFilter := Is(err, io.EOF) return isFilter, v1, v2 } // IsEOF function performs a filtered error check for the given argument. It's the -// same as To function, but it checks if the error matches the 'io.EOF' before +// same as [To] function, but it checks if the error matches the [io.EOF] before // throwing an error. The false return value tells that there are no errors. -// The true tells that the err's chain includes 'io.EOF'. +// The true tells that the err's chain includes [io.EOF]. func IsEOF(err error) bool { return Is(err, io.EOF) } // IsNotFound function performs a filtered error check for the given argument. -// It's the same as To function, but it checks if the error matches the -// 'err2.NotFound' before throwing an error. The false return value tells that +// It's the same as [To] function, but it checks if the error matches the +// [err2.NotFound] before throwing an error. The false return value tells that // there are no errors. The true tells that the err's chain includes -// 'err2.NotFound'. +// [err2.NotFound]. func IsNotFound(err error) bool { return Is(err, err2.ErrNotFound) } // IsNotFound1 function performs a filtered error check for the given argument. -// It's the same as To function, but it checks if the error matches the -// 'err2.NotFound' before throwing an error. The false return value tells that +// It's the same as [To] function, but it checks if the error matches the +// [err2.NotFound] before throwing an error. The false return value tells that // there are no errors. The true tells that the err's chain includes -// 'err2.NotFound'. +// [err2.NotFound]. func IsNotFound1[T any](v T, err error) (bool, T) { isFilter := Is(err, err2.ErrNotFound) return isFilter, v } // IsNotExist function performs a filtered error check for the given argument. -// It's the same as To function, but it checks if the error matches the -// 'err2.NotExist' before throwing an error. The false return value tells that +// It's the same as [To] function, but it checks if the error matches the +// [err2.NotExist] before throwing an error. The false return value tells that // there are no errors. The true tells that the err's chain includes -// 'err2.NotExist'. +// [err2.NotExist]. func IsNotExist(err error) bool { return Is(err, err2.ErrNotExist) } // IsExist function performs a filtered error check for the given argument. It's -// the same as To function, but it checks if the error matches the 'err2.Exist' -// before throwing an error. The false return value tells that there are no -// errors. The true tells that the err's chain includes 'err2.Exist'. +// the same as [To] function, but it checks if the error matches the +// [err2.AlreadyExist] before throwing an error. The false return value tells +// that there are no errors. The true tells that the err's chain includes +// [err2.AlreadyExist]. func IsAlreadyExist(err error) bool { return Is(err, err2.ErrAlreadyExist) } // IsNotAccess function performs a filtered error check for the given argument. -// It's the same as To function, but it checks if the error matches the -// 'err2.NotAccess' before throwing an error. The false return value tells that +// It's the same as [To] function, but it checks if the error matches the +// [err2.NotAccess] before throwing an error. The false return value tells that // there are no errors. The true tells that the err's chain includes -// 'err2.NotAccess'. +// [err2.NotAccess]. func IsNotAccess(err error) bool { return Is(err, err2.ErrNotAccess) } // IsRecoverable function performs a filtered error check for the given -// argument. It's the same as To function, but it checks if the error matches -// the 'err2.ErrRecoverable' before throwing an error. The false return value +// argument. It's the same as [To] function, but it checks if the error matches +// the [err2.ErrRecoverable] before throwing an error. The false return value // tells that there are no errors. The true tells that the err's chain includes -// 'err2.ErrRecoverable'. +// [err2.ErrRecoverable]. func IsRecoverable(err error) bool { return Is(err, err2.ErrRecoverable) } // IsNotRecoverable function performs a filtered error check for the given -// argument. It's the same as To function, but it checks if the error matches -// the 'err2.ErrNotRecoverable' before throwing an error. The false return value +// argument. It's the same as [To] function, but it checks if the error matches +// the [err2.ErrNotRecoverable] before throwing an error. The false return value // tells that there are no errors. The true tells that the err's chain includes -// 'err2.ErrNotRecoverable'. +// [err2.ErrNotRecoverable]. func IsNotRecoverable(err error) bool { return Is(err, err2.ErrNotRecoverable) } // IsNotEnabled function performs a filtered error check for the given argument. -// It's the same as To function, but it checks if the error matches the -// 'err2.ErrNotEnabled' before throwing an error. The false return value tells +// It's the same as [To] function, but it checks if the error matches the +// [err2.ErrNotEnabled] before throwing an error. The false return value tells // that there are no errors. The true tells that the err's chain includes -// 'err2.ErrNotEnabled'. +// [err2.ErrNotEnabled]. func IsNotEnabled(err error) bool { return Is(err, err2.ErrNotEnabled) } diff --git a/try/try_test.go b/try/try_test.go index 4240ad1..6aaba1e 100644 --- a/try/try_test.go +++ b/try/try_test.go @@ -105,7 +105,7 @@ func ExampleIsEOF1() { func Example_copyFile() { copyFile := func(src, dst string) (err error) { - defer err2.Handle(&err, "copy %s %s", src, dst) + defer err2.Handle(&err, "copy file %s %s", src, dst) // These try package helpers are as fast as Check() calls which is as // fast as `if err != nil {}` @@ -126,5 +126,5 @@ func Example_copyFile() { if err != nil { fmt.Println(err) } - // Output: copy /notfound/path/file.go /notfound/path/file.bak: open /notfound/path/file.go: no such file or directory + // Output: copy file /notfound/path/file.go /notfound/path/file.bak: open /notfound/path/file.go: no such file or directory }