diff --git a/CHANGELOG.md b/CHANGELOG.md index 55b93f6..90bdfa4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ### Version history +##### 0.9.51 +- `flag` package support to set `err2` and `assert` package configuration +- `err2.Catch` default mode is to log error +- cleanup and refactoring, new tests and benchmarks + ##### 0.9.5 **mistake in build number: 5 < 41** - `flag` package support to set `err2` and `assert` package configuration - `err2.Catch` default mode is to log error diff --git a/README.md b/README.md index 5838f8a..e30a13e 100644 --- a/README.md +++ b/README.md @@ -87,8 +87,8 @@ little error handling. But most importantly, it doesn't help developers with > Automation is not just about efficiency but primarily about repeatability and > resilience. -- Gregor Hohpe -Automatic error propagation is crucial because it makes your code tolerant -of the change. And, of course, it helps to make your code error-safe: +Automatic error propagation is crucial because it makes your code change +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) @@ -242,7 +242,7 @@ notExist := try.Is(r2.err, plugin.ErrNotExist) **Note.** Any other error than `plugin.ErrNotExist` is treated as an real error: 1. `try.Is` function first checks `if err == nil`, and if yes, it returns `false`. -2. Then it checks if `errors.Is` == `plugin.ErrNotExist` and if yes, it returns +2. Then it checks if `errors.Is(err, plugin.ErrNotExist)` and if yes, it returns `true`. 3. Finally, it calls `try.To` for the non nil error, and we already know what then happens: nearest `err2.Handle` gets it first. @@ -527,10 +527,11 @@ Please see the full version history from [CHANGELOG](./CHANGELOG.md). ### Latest Release -##### 0.9.51 -- `flag` package support to set `err2` and `assert` package configuration -- `err2.Catch` default mode is to log error -- cleanup and refactoring, new tests and benchmarks +##### 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 diff --git a/assert/assert.go b/assert/assert.go index 82512c3..d5d094d 100644 --- a/assert/assert.go +++ b/assert/assert.go @@ -146,8 +146,10 @@ var ( ) const ( - assertionMsg = "assertion violation" - gotWantFmt = ": got '%v', want '%v'" + assertionMsg = "assertion violation" + gotWantFmt = ": got '%v', want '%v'" + gotWantLongerFmt = ": got '%v', should be longer than '%v'" + gotWantShorterFmt = ": got '%v', should be shorter than '%v'" ) // PushTester sets the current testing context for default asserter. This must @@ -400,6 +402,32 @@ func Len(obj string, length int, a ...any) { } } +// 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) + + if l > length { + defMsg := fmt.Sprintf(assertionMsg+gotWantLongerFmt, l, length) + Default().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) + + if l <= length { + defMsg := fmt.Sprintf(assertionMsg+gotWantShorterFmt, l, length) + Default().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 @@ -413,6 +441,32 @@ func SLen[S ~[]T, T any](obj S, length int, a ...any) { } } +// 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. +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...) + } +} + +// 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. +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...) + } +} + // 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 @@ -426,6 +480,71 @@ func MLen[M ~map[T]U, T comparable, U any](obj M, length int, a ...any) { } } +// 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. +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...) + } +} + +// 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. +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...) + } +} + +// 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. +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...) + } +} + +// 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. +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...) + } +} + +// 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. +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...) + } +} + // MKeyExists asserts that the map key exists. If not it panics/errors (current // Asserter) with the given message. func MKeyExists[M ~map[T]U, T comparable, U any](obj M, key T, a ...any) (val U) { @@ -485,7 +604,9 @@ func MNotEmpty[M ~map[T]U, T comparable, U any](obj M, a ...any) { // 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. +// 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. func NoError(err error, a ...any) { if err != nil { defMsg := "NoError:" + assertionMsg + ": " + err.Error() diff --git a/assert/assert_test.go b/assert/assert_test.go index a1b0624..06ec08d 100644 --- a/assert/assert_test.go +++ b/assert/assert_test.go @@ -152,6 +152,32 @@ func ExampleZero() { // Output: sample: assert_test.go:146: ExampleZero.func1(): assertion violation: got '1', want (== '0') } +func ExampleSLonger() { + sample := func(b []byte) (err error) { + defer err2.Handle(&err, "sample") + + assert.SLonger(b, 0) // ok + assert.SLonger(b, 1) // not ok + return err + } + err := sample([]byte{01}) // len = 1 + fmt.Printf("%v", err) + // Output: sample: assert_test.go:160: ExampleSLonger.func1(): assertion violation: got '1', should be longer than '1' +} + +func ExampleMShorter() { + sample := func(b map[byte]byte) (err error) { + defer err2.Handle(&err, "sample") + + assert.MShorter(b, 1) // ok + assert.MShorter(b, 0) // not ok + return err + } + err := sample(map[byte]byte{01: 01}) // len = 1 + fmt.Printf("%v", err) + // Output: sample: assert_test.go:172: ExampleMShorter.func1(): assertion violation: got '1', should be shorter than '1' +} + func assertZero(i int) { assert.Zero(i) } diff --git a/assert/asserter.go b/assert/asserter.go index 5974e0f..c085b49 100644 --- a/assert/asserter.go +++ b/assert/asserter.go @@ -47,109 +47,6 @@ const ( // every test log or result output has 4 spaces in them const officialTestOutputPrefix = " " -// NoImplementation always fails with no implementation. -// Deprecated: use e.g. assert.NotImplemented(), only default asserter is used. -func (asserter Asserter) NoImplementation(a ...any) { - asserter.reportAssertionFault("not implemented", a...) -} - -// True asserts that term is true. If not it panics with the given formatting -// string. Note! This and Truef are the most performant of all the assertion -// functions. -// Deprecated: use e.g. assert.That(), only default asserter is used. -func (asserter Asserter) True(term bool, a ...any) { - if !term { - asserter.reportAssertionFault("assertion fault", a...) - } -} - -// Truef asserts that term is true. If not it panics with the given formatting -// string. -// Deprecated: use e.g. assert.That(), only default asserter is used. -func (asserter Asserter) Truef(term bool, format string, a ...any) { - if !term { - if asserter.hasStackTrace() { - debug.PrintStack(1) - } - asserter.reportPanic(fmt.Sprintf(format, a...)) - } -} - -// Len asserts that length of the object is equal to given. If not it -// panics/errors (current Asserter) with the given msg. Note! This is very slow -// (before we have generics). If you need performance use EqualInt. It's not so -// convenient, though. -// Deprecated: use e.g. assert.Len(), only default asserter is used. -func (asserter Asserter) Len(obj any, length int, a ...any) { - ok, l := getLen(obj) - if !ok { - panic("cannot get length") - } - - if l != length { - defMsg := fmt.Sprintf("got %d, want %d", l, length) - asserter.reportAssertionFault(defMsg, a...) - } -} - -// EqualInt asserts that integers are equal. If not it panics/errors (current -// Asserter) with the given msg. -// Deprecated: use e.g. assert.Equal(), only default asserter is used. -func (asserter Asserter) EqualInt(val, want int, a ...any) { - if want != val { - defMsg := fmt.Sprintf("got %d, want %d", val, want) - asserter.reportAssertionFault(defMsg, a...) - } -} - -// Lenf asserts that length of the object is equal to given. If not it -// panics/errors (current Asserter) with the given msg. Note! This is very slow -// (before we have generics). If you need performance use EqualInt. It's not so -// convenient, though. -// Deprecated: use e.g. assert.Len(), only default asserter is used. -func (asserter Asserter) Lenf(obj any, length int, format string, a ...any) { - args := combineArgs(format, a) - asserter.Len(obj, length, args...) -} - -// Empty asserts that length of the object is zero. If not it panics with the -// given formatting string. Note! This is slow. -// Deprecated: use e.g. assert.Empty(), only default asserter is used. -func (asserter Asserter) Empty(obj any, msg ...any) { - ok, l := getLen(obj) - if !ok { - panic("cannot get length") - } - - if l != 0 { - defMsg := fmt.Sprintf("got %d, want == 0", l) - asserter.reportAssertionFault(defMsg, msg...) - } -} - -// NotEmptyf asserts that length of the object greater than zero. If not it -// panics with the given formatting string. Note! This is slow. -// Deprecated: use e.g. assert.NotEmpty(), only default asserter is used. -func (asserter Asserter) NotEmptyf(obj any, format string, msg ...any) { - args := combineArgs(format, msg) - asserter.Empty(obj, args...) -} - -// NotEmpty asserts that length of the object greater than zero. If not it -// panics with the given formatting string. Note! This is slow. -// Deprecated: use e.g. assert.NotEmpty(), only default asserter is used. -func (asserter Asserter) NotEmpty(obj any, msg ...any) { - ok, l := getLen(obj) - if !ok { - panic("cannot get length") - } - - if l == 0 { - defMsg := fmt.Sprintf("got %d, want > 0", l) - asserter.reportAssertionFault(defMsg, msg...) - } -} - func (asserter Asserter) reportAssertionFault(defaultMsg string, a ...any) { if asserter.hasStackTrace() { if asserter.isUnitTesting() { diff --git a/err2.go b/err2.go index 2e9c53e..02491a4 100644 --- a/err2.go +++ b/err2.go @@ -3,6 +3,7 @@ package err2 import ( "errors" "fmt" + "os" "github.com/lainio/err2/internal/handler" ) @@ -32,6 +33,11 @@ var ( // same error. These error are mainly for that purpose. 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{} ) // Handle is the general purpose error handling function. What makes it so @@ -115,7 +121,11 @@ func Handle(err *error, a ...any) { // // The preceding line catches the errors and panics and prints an annotated // error message about the error source (from where the error was thrown) to the -// currently set log. +// 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: +// +// 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 @@ -174,6 +184,32 @@ 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: // @@ -201,6 +237,10 @@ func Err(f func(err error)) func(error) error { } } +type nullDev struct{} + +func (nullDev) Write([]byte) (int, error) { return 0, nil } + func doTrace(err error) { if err == nil || err.Error() == "" { return diff --git a/samples/main-db-sample.go b/samples/main-db-sample.go index ed713e6..9d663a7 100644 --- a/samples/main-db-sample.go +++ b/samples/main-db-sample.go @@ -3,7 +3,6 @@ package main import ( "errors" "fmt" - "os" "github.com/lainio/err2" "github.com/lainio/err2/try" @@ -36,9 +35,6 @@ func (db *Database) MoneyTransfer(from, to *Account, amount int) (err error) { } func doDBMain() { - err2.SetErrorTracer(os.Stderr) - err2.SetErrorTracer(nil) // <- out-comment/rm to get automatic error traces - defer err2.Catch("CATCH Warning: %s", "test-name") db, from, to := new(Database), new(Account), new(Account) diff --git a/samples/main-play.go b/samples/main-play.go index ddc9487..746ed98 100644 --- a/samples/main-play.go +++ b/samples/main-play.go @@ -37,6 +37,31 @@ func CopyFile(src, dst string) (err error) { return nil } +func ClassicCopyFile(src, dst string) error { + r, err := os.Open(src) + if err != nil { + return fmt.Errorf("copy %s %s: %v", src, dst, err) + } + defer r.Close() + + w, err := os.Create(dst) + if err != nil { + return fmt.Errorf("copy %s %s: %v", src, dst, err) + } + + if _, err := io.Copy(w, r); err != nil { + w.Close() + os.Remove(dst) + return fmt.Errorf("copy %s %s: %v", src, dst, err) + } + + if err := w.Close(); err != nil { + os.Remove(dst) + return fmt.Errorf("copy %s %s: %v", src, dst, err) + } + return nil +} + // OrgCopyFile copies the source file to the given destination. If any error occurs it // returns an error value describing the reason. func OrgCopyFile(src, dst string) (err error) { @@ -104,7 +129,7 @@ func doPlayMain() { //err2.SetFormatter(formatter.Noop) // default is formatter.Decamel // errors are caught without specific handlers. - defer err2.Catch("CATCH") + defer err2.Catch(err2.Stderr) // If you don't want to use tracers or you just need a proper error handler // here. diff --git a/samples/main.go b/samples/main.go index 39bf731..6961bb8 100644 --- a/samples/main.go +++ b/samples/main.go @@ -13,7 +13,7 @@ var ( ) func main() { - defer err2.Catch() + defer err2.Catch(err2.Stderr) log.SetFlags(log.Lshortfile | log.LstdFlags) flag.Parse() diff --git a/snippets/go.json b/snippets/go.json index 8ed17bb..36b268b 100644 --- a/snippets/go.json +++ b/snippets/go.json @@ -1,5 +1,15 @@ { ".source.go": { + "defer err2.Catch to stderr": { + "prefix": "ecte", + "body": "defer err2.Catch(err2.Stderr)\n", + "description": "Snippet for err2.Catch(Stderr)" + }, + "defer err2.Catch to stdout": { + "prefix": "ecto", + "body": "defer err2.Catch(err2.Stdout)\n", + "description": "Snippet for err2.Catch(Stdout)" + }, "defer err2.Catch": { "prefix": "eca", "body": "defer err2.Catch()\n",