diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 18adcd3..b3a373b 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -11,10 +11,10 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Go - uses: actions/setup-go@v3 + uses: actions/setup-go@v5 with: go-version: 1.19 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index edb0dd3..c606485 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,39 +5,39 @@ jobs: name: lint runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: golangci-lint - uses: golangci/golangci-lint-action@v2 + uses: golangci/golangci-lint-action@v5 with: version: latest args: --timeout=5m test: strategy: matrix: - go-version: [1.18.x, 1.19.x, 1.20.x, 1.21.x] + go-version: [1.18.x, 1.19.x, 1.20.x, 1.21.x, 1.22.x] os: [windows-latest, ubuntu-latest, macos-latest] runs-on: ${{ matrix.os }} steps: - name: setup go - uses: actions/setup-go@v2 + uses: actions/setup-go@v5 with: go-version: ${{ matrix.go-version }} - name: checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: test run: make test test-cov: runs-on: ubuntu-latest steps: - name: setup - uses: actions/setup-go@v2 + uses: actions/setup-go@v5 with: go-version: 1.19.x - name: checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: test run: make test_cov_out - name: upload - uses: codecov/codecov-action@v2 + uses: codecov/codecov-action@v4 with: files: ./coverage.txt diff --git a/.golangci.yml b/.golangci.yml index bc511f1..5bebe3a 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -33,12 +33,7 @@ linters-settings: # Default: 5 min-complexity: 8 gomnd: - settings: - mnd: - # don't include the "operation" and "assign" - checks: argument,case,condition,return govet: - check-shadowing: true lll: line-length: 140 maligned: diff --git a/CHANGELOG.md b/CHANGELOG.md index 662568b..9d8507a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ ### Version history +##### 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 + ##### 0.9.52 - `err2.Stderr` helpers for `Catch/Handle` to direct auto-logging + snippets - `assert` package `Shorter` `Longer` helpers for automatic messages diff --git a/Makefile b/Makefile index 73da499..496e8b7 100644 --- a/Makefile +++ b/Makefile @@ -11,6 +11,7 @@ PKGS := $(PKG_ERR2) $(PKG_ASSERT) $(PKG_TRY) $(PKG_DEBUG) $(PKG_HANDLER) $(PKG_S SRCDIRS := $(shell go list -f '{{.Dir}}' $(PKGS)) +MAX_LINE ?= 80 GO ?= go TEST_ARGS ?= -benchmem # -"gcflags '-N -l'" both optimization & inlining disabled @@ -61,9 +62,33 @@ inline_handler: tinline_handler: $(GO) test -c -gcflags=-m=2 $(PKG_HANDLER) 2>&1 | ag 'inlin' +inline_assert: + $(GO) test -c -gcflags=-m=2 $(PKG_ASSERT) 2>&1 | ag 'inlin' + bench: $(GO) test $(TEST_ARGS) -bench=. $(PKGS) +bench_T: + $(GO) test $(TEST_ARGS) -bench='BenchmarkT_.*' $(PKG_ERR2) + +bench_S: + $(GO) test $(TEST_ARGS) -bench='BenchmarkS.*' $(PKG_ASSERT) + +bench_M: + $(GO) test $(TEST_ARGS) -bench='BenchmarkM.*' $(PKG_ASSERT) + +bench_C: + $(GO) test $(TEST_ARGS) -bench='BenchmarkC.*' $(PKG_ASSERT) + +bench_nil: + $(GO) test $(TEST_ARGS) -bench='Benchmark.*Nil' $(PKG_ASSERT) + +bench_empty: + $(GO) test $(TEST_ARGS) -bench='Benchmark.*Empty' $(PKG_ASSERT) + +bench_zero: + $(GO) test $(TEST_ARGS) -bench='BenchmarkZero.*' $(PKG_ASSERT) + bench_goid: $(GO) test $(TEST_ARGS) -bench='BenchmarkGoid' $(PKG_ASSERT) @@ -77,7 +102,7 @@ bench_go: $(GO) test $(TEST_ARGS) -bench='BenchmarkTry_StringGenerics' $(PKG_ERR2) bench_that: - $(GO) test $(TEST_ARGS) -bench='BenchmarkThat.*' $(PKG_ASSERT) + $(GO) test $(TEST_ARGS) -bench='Benchmark.*That.*' $(PKG_ASSERT) bench_copy: $(GO) test $(TEST_ARGS) -bench='Benchmark_CopyBuffer' $(PKG_TRY) @@ -106,6 +131,14 @@ bench_x: vet: | test $(GO) vet $(PKGS) +fmt: + @echo "Pretty formatting with golines" + @golines -t 5 -w -m $(MAX_LINE) --ignore-generated . + +dry-fmt: + @echo "--dry-run: Pretty formatting with golines" + @golines -t 5 --dry-run -m $(MAX_LINE) --ignore-generated . + gofmt: @echo Checking code is gofmted @test -z "$(shell gofmt -s -l -d -e $(SRCDIRS) | tee /dev/stderr)" @@ -124,8 +157,17 @@ test_cov: test_cov_out go tool cover -html=coverage.txt -o=coverage.html firefox ./coverage.html 1>&- 2>&- & -lint: - @golangci-lint run +test_cov_pc_assert: + go tool cover -func=coverage.txt | ag assert -.PHONY: check +test_cov_zero: test_cov_out + go tool cover -func=coverage.txt | ag '\:\s*[A-Z]+.*\s+0\.0%' +test_cov_assert_zero: test_cov_out + go tool cover -func=coverage.txt | ag 'assert\/.*\:\s*[A-Z]+.*\s+0\.0%' + +test_cov_pc: + go tool cover -func=coverage.txt + +lint: + @golangci-lint run diff --git a/README.md b/README.md index 9473df2..0565455 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,13 @@ +# err2 + [![test](https://github.com/lainio/err2/actions/workflows/test.yml/badge.svg?branch=master)](https://github.com/lainio/err2/actions/workflows/test.yml) ![Go Version](https://img.shields.io/badge/go%20version-%3E=1.18-61CFDD.svg?style=flat-square) [![PkgGoDev](https://pkg.go.dev/badge/mod/github.com/lainio/err2)](https://pkg.go.dev/mod/github.com/lainio/err2) [![Go Report Card](https://goreportcard.com/badge/github.com/lainio/err2?style=flat-square)](https://goreportcard.com/report/github.com/lainio/err2) -# err2 + + +---- The package extends Go's error handling with **fully automatic error checking and propagation** like other modern programming languages: **Zig**, Rust, Swift, @@ -28,6 +32,8 @@ func CopyFile(src, dst string) (err error) { } ``` +---- + `go get github.com/lainio/err2` - [Structure](#structure) @@ -37,7 +43,6 @@ func CopyFile(src, dst string) (err error) { - [Error Stack Tracing](#error-stack-tracing) - [Error Checks](#error-checks) - [Filters for non-errors like io.EOF](#filters-for-non-errors-like-ioeof) -- [Backwards Compatibility Promise for the API](#backwards-compatibility-promise-for-the-api) - [Assertion](#assertion) - [Asserters](#asserters) - [Assertion Package for Runtime Use](#assertion-package-for-runtime-use) @@ -48,7 +53,7 @@ func CopyFile(src, dst string) (err error) { - [Background](#background) - [Learnings by so far](#learnings-by-so-far) - [Support And Contributions](#support-and-contributions) -- [Roadmap](#roadmap) +- [History](#history) ## Structure @@ -57,37 +62,38 @@ func CopyFile(src, dst string) (err error) { - The `err2` (main) package includes declarative error handling functions. - The `try` package offers error checking functions. - The `assert` package implements assertion helpers for **both** unit-testing - and *design-by-contract*. + and *design-by-contract* with the *same API and cross-usage*. ## Performance All of the listed above **without any performance penalty**! You are welcome to run `benchmarks` in the project repo and see yourself. -Please note that many benchmarks run 'too fast' according to the common Go -benchmarking rules, i.e., compiler optimizations -([inlining](https://en.wikipedia.org/wiki/Inline_expansion)) are working so well -that there are no meaningful results. But for this type of package, where **we -compete with if-statements, that's precisely what we hope to achieve.** The -whole package is written toward that goal. Especially with parametric -polymorphism, it's been quite the effort. +
+It's too fast! +
-## Automatic Error Propagation +> Most of the benchmarks run 'too fast' according to the common Go +> benchmarking rules, i.e., compiler optimizations +> ([inlining](https://en.wikipedia.org/wiki/Inline_expansion)) are working so +> well that there are no meaningful results. But for this type of package, where +> **we compete with if-statements, that's precisely what we hope to achieve.** +> The whole package is written toward that goal. Especially with parametric +> polymorphism, it's been quite the effort. -The current version of Go tends to produce too much error checking and too -little error handling. But most importantly, it doesn't help developers with -**automatic** error propagation, which would have the same benefits as, e.g., -**automated** garbage collection or automatic testing: +
-> Automation is not just about efficiency but primarily about repeatability and -> resilience. -- Gregor Hohpe +## Automatic Error Propagation -Automatic error propagation is crucial because it makes your code change -tolerant. 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) -The err2 package is your automation buddy: + +
+The err2 package is your automation buddy: +
1. It helps to declare error handlers with `defer`. If you're familiar with [Zig language](https://ziglang.org/), you can think `defer err2.Handle(&err,...)` @@ -103,40 +109,62 @@ You can use all of them or just the other. However, if you use `try` for error checks, you must remember to use Go's `recover()` by yourself, or your error isn't transformed to an `error` return value at any point. -## Error handling +
-The `err2` relies on Go's declarative programming structure `defer`. The -`err2` helps to set deferred functions (error handlers) which are only called if -`err != nil`. +## Error Handling -Every function which uses err2 for error-checking should have at least one error -handler. The current function panics if there are no error handlers and an error -occurs. However, if *any* function above in the call stack has an err2 error -handler, it will catch the error. +The err2 relies on Go's declarative programming structure `defer`. The +err2 helps to set deferred error handlers which are only called if an error +occurs. -This is the simplest form of `err2` automatic error handler: +This is the simplest form of an 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) ``` +
+The explanation of the above code and its error handler: +
+ +Simplest rule for err2 error handlers are: +1. Use named error return value: `(..., err error)` +1. Add at least one error handler at the beginning of your function (see the + above code block). *Handlers are called only if error ≠ nil.* +1. Use `err2.handle` functions different calling schemes to achieve needed + behaviour. For example, without no extra arguments `err2.Handle` + automatically annotates your errors by building annotations string from the + function's current name: `doSomething → "do something:"`. Default is decamel + and add spaces. See `err2.SetFormatter` for more information. +1. Every function which uses err2 for error-checking should have at least one + error handler. The current function panics if there are no error handlers and + an error occurs. However, if *any* function above in the call stack has an + err2 error handler, it will catch the error. + 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, as well as you can chain error handling functions per +per function as you need. You can also chain error handling functions per `err2.Handle` that allows you to build new error handling middleware for your own purposes. +
+ #### Error Stack Tracing -The err2 offers optional stack tracing. It's automatic and optimized. Optimized -means that the call stack is processed before output. That means that stack -trace starts from where the actual error/panic is occurred, not where the error -is caught. You don't need to search for the line where the pointer was nil or -received an error. That line is in the first one you are seeing: +The err2 offers optional stack tracing. It's *automatic* and *optimized*. + +
+The example of the optimized call stack: +
+ +Optimized means that the call stack is processed before output. That means that +stack trace *starts from where the actual error/panic is occurred*, not where +the error or panic is caught. You don't need to search for the line where the +pointer was nil or received an error. That line is in the first one you are +seeing: -```console +``` --- runtime error: index out of range [0] with length 0 --- @@ -147,6 +175,8 @@ main.main() /home/.../go/src/github.com/lainio/ic/main.go:77 +0x248 ``` +
+ Just set the `err2.SetErrorTracer` or `err2.SetPanicTracer` to the stream you want traces to be written: @@ -160,9 +190,10 @@ If no `Tracer` is set no stack tracing is done. This is the default because in the most cases proper error messages are enough and panics are handled immediately by a programmer. -> Note. Since v0.9.5 you can set these tracers through Go's standard flag -> package just by adding `flag.Parse()` to your program. See more information -> from [Automatic Flags](#automatic-flags). +> [!NOTE] +> Since v0.9.5 you can set *tracers* through Go's standard flag package just by +> adding `flag.Parse()` call to your source code. See more information from +> [Automatic Flags](#automatic-flags). [Read the package documentation for more information](https://pkg.go.dev/github.com/lainio/err2). @@ -191,38 +222,55 @@ but not without an error handler (`err2.Handle`). However, you can put your error handlers where ever you want in your call stack. That can be handy in the internal packages and certain types of algorithms. +
+Immediate Error Handling Options +
+ In cases where you want to handle the error immediately after the function call -return you can use Go's default `if` statement. However, we encourage you to use -the `errdefer` concept, `defer err2.Handle(&err)` for all of your error -handling. +you can use Go's default `if` statement. However, we recommend you to use +`defer err2.Handle(&err)` for all of your error handling, because it keeps your +code modifiable, refactorable, and skimmable. Nevertheless, there might be cases where you might want to: -1. Suppress the error and use some default value. -1. Just write a logline and continue without a break. +1. Suppress the error and use some default value. In next, use 100 if `Atoi` + fails: + ```go + b := try.Out1(strconv.Atoi(s)).Catch(100) + ``` +1. Just write logging output and continue without breaking the execution. In + next, add log if `Atoi` fails. + ```go + b := try.Out1(strconv.Atoi(s)).Logf("%s => 100", s).Catch(100) + ``` 1. Annotate the specific error value even when you have a general error handler. -1. You want to handle the specific error value, let's say, at the same line - or statement. - -The `err2/try` package offers other helpers based on the DSL concept -where the DSL's domain is error-handling. It's based on functions `try.Out`, -`try.Out1`, and `try.Out2`, which return instances of types `Result`, `Result1`, -and `Result2`. The `try.Result` is similar to other programming languages, -i.e., discriminated union. Please see more from its documentation. - -Now we could have the following: - -```go -b := try.Out1(strconv.Atoi(s)).Logf("%s => 100", s).Catch(100) -``` + You are already familiar with `try.To` functions. There's *fast* annotation + versions `try.T` which can be used as shown below: + ```go + b := try.T1(io.ReadAll(r))("cfg file read") + // where original were, for example: + b := try.To1(io.ReadAll(r)) + ``` +1. You want to handle the specific error value at the same line or statement. In + below, the function `doSomething` returns an error value. If it returns + `ErrNotSoBad`, we just suppress it. All the other errors are send to the + current error handler and will be handled there, but are also annotated with + 'fatal' prefix before that here. + ```go + try.Out(doSomething()).Handle(ErrNotSoBad, err2.Reset).Handle("fatal") + ``` -The previous statement tries to convert incoming string value `s`, but if it -doesn't succeed, it writes a warning to logs and uses the default value (100). -The logging result includes the original error message as well. +The `err2/try` package offers other helpers based on the error-handling +language/API. It's based on functions `try.Out`, `try.Out1`, and `try.Out2`, +which return instances of types `Result`, `Result1`, and `Result2`. The +`try.Result` is similar to other programming languages, i.e., discriminated +union. Please see more from its documentation. It's easy to see that panicking about the errors at the start of the development is far better than not checking errors at all. But most importantly, `err2/try` **keeps the code readable.** +
+ #### Filters for non-errors like io.EOF When error values are used to transport some other information instead of @@ -230,72 +278,74 @@ actual errors we have functions like `try.Is` and even `try.IsEOF` for convenience. With these you can write code where error is translated to boolean value: + ```go notExist := try.Is(r2.err, plugin.ErrNotExist) // real errors are cought and the returned boolean tells if value // dosen't exist returned as `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(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. -These `try.Is` functions help cleanup mesh idiomatic Go, i.e. mixing happy and -error path, leads to. +> [!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(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. For more information see the examples in the documentation of both functions. -## Backwards Compatibility Promise for the API - -The `err2` package's API will be **backward compatible**. Before version -1.0.0 is released, the API changes occasionally, but **we promise to offer -automatic conversion scripts for your repos to update them for the latest API.** -We also mark functions deprecated before they become obsolete. Usually, one -released version before. We have tested this with a large code base in our -systems, and it works wonderfully. - -More information can be found in the `scripts/` directory [readme -file](./scripts/README.md). - ## Assertion The `assert` package is meant to be used for *design-by-contract-* type of -development where you set pre- and post-conditions for your functions. It's not -meant to replace the normal error checking but speed up the incremental hacking -cycle. The default mode is to return an `error` value that includes a formatted -and detailed assertion violation message. A developer gets immediate and proper -feedback, allowing cleanup of the code and APIs before the release. +development where you set pre- and post-conditions for *all* of your functions, +*including test functions*. These asserts are as fast as if-statements when not +triggered. + +> [!IMPORTANT] +> It works *both runtime and for tests.* And even better, same asserts work in +> both running modes. #### Asserters +
+Fast Clean Code with Asserters +
+ +Asserts are not meant to replace the normal error checking but speed up the +incremental hacking cycle like TDD. The default mode is to return an `error` +value that includes a formatted and detailed assertion violation message. A +developer gets immediate and proper feedback independently of the running mode, +allowing very fast feedback cycles. + The assert package offers a few pre-build *asserters*, which are used to -configure how the assert package deals with assert violations. The line below -exemplifies how the default asserter is set in the package. +configure *how the assert package deals with assert violations*. The line below +exemplifies how the default asserter is set in the package. (See the +documentation for more information about asserters.) ```go assert.SetDefault(assert.Production) ``` If you want to suppress the caller info (source file name, line number, etc.) -and get just the plain panics from the asserts, you should set the -default asserter with the following line: +from certain asserts, you can do that per a goroutine or a function. You should +set the asserter with the following line for the current function: ```go -assert.SetDefault(assert.Debug) +defer assert.PushAsserter(assert.Plain)() ``` -For certain type of programs this is the best way. It allows us to keep all the -error messages as simple as possible. And by offering option to turn additional -information on, which allows super users and developers get more technical -information when needed. +This is especially good if you want to use assert functions for CLI's flag +validation or you want your app behave like legacy Go programs. -> Note. Since v0.9.5 you can set these asserters through Go's standard flag -> package just by adding `flag.Parse()` to your program. See more information -> from [Automatic Flags](#automatic-flags). +
+ +> [!NOTE] +> Since v0.9.5 you can set these asserters through Go's standard flag package +> just by adding `flag.Parse()` to your program. See more information from +> [Automatic Flags](#automatic-flags). #### Assertion Package for Runtime Use @@ -303,10 +353,10 @@ Following is example of use of the assert package: ```go func marshalAttestedCredentialData(json []byte, data *protocol.AuthenticatorData) []byte { - assert.SLen(data.AttData.AAGUID, 16, "wrong AAGUID length") - assert.NotEmpty(data.AttData.CredentialID, "empty credential id") - assert.SNotEmpty(data.AttData.CredentialPublicKey, "empty credential public key") - ... + assert.SLen(data.AttData.AAGUID, 16, "wrong AAGUID length") + assert.NotEmpty(data.AttData.CredentialID, "empty credential id") + assert.SNotEmpty(data.AttData.CredentialPublicKey, "empty credential public key") + ... ``` We have now described design-by-contract for development and runtime use. What @@ -315,7 +365,11 @@ automatic testing as well. #### Assertion Package for Unit Testing -The same asserts can be used **and shared** during the unit tests: +The same asserts can be used **and shared** during the unit tests over module +boundaries. + +
+The unit test code example: ```go func TestWebOfTrustInfo(t *testing.T) { @@ -343,6 +397,8 @@ of the actual Test function, **it's reported as a standard test failure.** That means we don't need to open our internal pre- and post-conditions just for testing. +
+ **We can share the same assertions between runtime and test execution.** The err2 `assert` package integration to the Go `testing` package is completed at @@ -363,9 +419,12 @@ 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). -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 -on when ever you need**. +You can deploy your applications and services with the simple *end-user friendly +error messages and no stack traces.* + +
+You can switch them on whenever you need them again. +
Let's say you have build CLI (`your-app`) tool with the support for Go's flag package, and the app returns an error. Let's assume you're a developer. You can @@ -387,12 +446,17 @@ That adds more information to the assertion statement, which in default is in production (`Prod`) mode, i.e., outputs a single-line assertion message. All you need to do is to add `flag.Parse` to your `main` function. +
#### Support for Cobra Flags If you are using [cobra](https://github.com/spf13/cobra) you can still easily support packages like `err2` and `glog` and their flags. +
+Add cobra support: +
+ 1. Add std flag package to imports in `cmd/root.go`: ```go @@ -440,17 +504,29 @@ Flags: ... ``` +
+ ## Code Snippets -Most of the repetitive code blocks are offered as code snippets. They are in -`./snippets` in VC code format, which is well supported e.g. neovim, etc. +
+Code snippets as learning helpers. +
+ +The snippets are in `./snippets` and in VC code format, which is well supported +e.g. neovim, etc. They are proven to be useful tool especially when you are +starting to use the err2 and its sub-packages. The snippets must be installed manually to your preferred IDE/editor. During the installation you can modify the according your style or add new ones. We would prefer if you could contribute some of the back to the err2 package. +
## Background +
+Why this repo exists? +
+ `err2` implements similar error handling mechanism as drafted in the original [check/handle proposal](https://go.googlesource.com/proposal/+/master/design/go2draft-error-handling-overview.md). @@ -490,17 +566,21 @@ help: background) to projects, - and most importantly, **it keeps your code more refactorable** because you don't have to repeat yourself. +
## Learnings by so far -We have used the `err2` and `assert` packages in several projects. The results -have been so far very encouraging: +
+We have used the err2 and assert packages in several projects. +
+ +The results have been so far very encouraging: - If you forget to use handler, but you use checks from the package, you will -get panics (and optionally stack traces) if an error occurs. That is much better -than getting unrelated panic somewhere else in the code later. There have also -been cases when code reports error correctly because the 'upper' handler catches -it. +get panics on errors (and optimized stack traces that can be suppressed). That +is much better than getting unrelated panic somewhere else in the code later. +There have also been cases when code reports error correctly because the 'upper' +handler catches it. - Because the use of `err2.Handle` is so easy, error messages are much better and informative. When using `err2.Handle`'s automatic annotation your error @@ -512,26 +592,29 @@ been much easier.** There is an excellent [blog post](https://jesseduffield.com/ about the issues you are facing with Go's error handling without the help of the err2 package. +
+ ## Support And Contributions -The package has been in experimental mode quite long time. Since the Go generics -we are transiting towards more official mode. Currently we offer support by -GitHub Discussions. Naturally, any issues and contributions are welcome as well! +The package was in experimental mode quite long time. Since the Go generics we +did transit to official mode. Currently we offer support by GitHub Issues and +Discussions. Naturally, we appreciate all feedback and contributions are +very welcome! -## Roadmap +## History Please see the full version history from [CHANGELOG](./CHANGELOG.md). ### Latest Release -##### 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 +##### 1.1.0 +- `assert` package: + - bug fix: call stack traversal during unit testing in some situations + - **all generics-based functions are inline expansed** + - *performance* is now *same as if-statements for all functions* + - new assert functions: `MNil`, `CNil`, `Less`, `Greater`, etc. + - all assert messages follow Go idiom: `got, want` + - `Asserter` can be set per goroutine: `PushAsserter` +- `try` package: + - new check functions: `T`, `T1`, `T2`, `T3`, for quick refactoring from `To` functions to annotate an error locally + - **all functions are inline expansed**: if-statement equal performance diff --git a/assert/assert.go b/assert/assert.go index 28a1600..b33c653 100644 --- a/assert/assert.go +++ b/assert/assert.go @@ -14,7 +14,7 @@ import ( "golang.org/x/exp/constraints" ) -type defInd = uint32 +type Asserter = uint32 // Asserters are the way to set what kind of messages assert package outputs if // assertion is violated. @@ -25,18 +25,18 @@ type defInd = uint32 // // assert.NotEmpty(c.PoolName, "pool name cannot be empty") // -// Note that Plain is only asserter that override auto-generated assertion +// 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 +// [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 +// [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: @@ -47,14 +47,14 @@ type defInd = uint32 // assertion violation: string shouldn't be empty // -------------------------------- // -// [Test] minimalistic asserter for unit test use. More pragmatic is the -// [TestFull] asserter (test default). +// [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 +// 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 +// [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. // @@ -70,7 +70,7 @@ type defInd = uint32 // 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 +// [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. // @@ -86,7 +86,7 @@ type defInd = uint32 // // [nvim-go]: https://github.com/lainio/nvim-go const ( - Plain defInd = 0 + iota + Plain Asserter = 0 + iota Production Development Test @@ -118,7 +118,7 @@ var ( // TestFull // Debug defAsserter = []asserter{plain, prod, dev, test, testFull, dbg} - def defInd + def Asserter // mu is package lvl Mutex that is used to cool down race detector of // client pkgs, i.e. packages that use us can use -race flag in their test @@ -137,6 +137,8 @@ func init() { } type ( + mapAsserter = map[int]asserter + testersMap = map[int]testing.TB function = func() ) @@ -144,10 +146,18 @@ type ( var ( // testers must be set if assertion package is used for the unit testing. testers = x.NewRWMap[testersMap]() + + asserterMap = x.NewRWMap[mapAsserter]() ) const ( - assertionMsg = "assertion violation" + assertionNot = "not" + + assertionMsg = "assertion failure" + assertionEqualMsg = "assertion failure: equal" + assertionNotEqualMsg = "assertion failure: not equal" + assertionLenMsg = "assertion failure: length" + gotWantFmt = ": got '%v', want '%v'" gotWantLongerFmt = ": got '%v', should be longer than '%v'" gotWantShorterFmt = ": got '%v', should be shorter than '%v'" @@ -155,7 +165,7 @@ const ( conCatErrStr = ": " ) -// PushTester sets the current testing context for default asserter. This must +// 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 { @@ -176,7 +186,7 @@ const ( // 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 +// PushTester allows you to change the current default [Asserter] by accepting it // as a second argument. // // Note that you MUST call PushTester for sub-goroutines: @@ -184,13 +194,13 @@ const ( // defer assert.PushTester(t)() // does the cleanup // ... // go func() { -// assert.PushTester(t) // left cleanup out! Leave it for the test, see ^ +// assert.PushTester(t)() // ... // -// Note that the second argument, if given, changes the default asserter for +// 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 { +func PushTester(t testing.TB, a ...Asserter) function { if len(a) > 0 { SetDefault(a[0]) } else if current()&asserterUnitTesting == 0 { @@ -213,7 +223,7 @@ func PushTester(t testing.TB, a ...defInd) function { // assert.PushTester(t) // <- important! // defer assert.PopTester() // <- for good girls and not so bad boys // ... -// assert.That(something, "test won't work") +// assert.That(something, "won't work") // }) // } // @@ -228,29 +238,32 @@ func PopTester() { return } + var stackLvl = 5 // amount of functions before we're here + var framesToSkip = 3 // how many fn calls there is before FuncName call + var msg string switch t := r.(type) { case string: msg = t case runtime.Error: + stackLvl-- // move stack trace cursor + framesToSkip++ // see fatal(), skip 1 more when runtime panics msg = t.Error() case error: msg = t.Error() default: - msg = "test panic catch" + msg = fmt.Sprintf("panic: %v", t) } // 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 traces if it would be possible. - const stackLvl = 6 // amount of functions before we're here debug.PrintStackForTest(os.Stderr, stackLvl) // Now that call stack errors are printed, if any. Let's print the actual // line that caused the error, i.e., was throwing the error. Note that we // are here in the 'catch-function'. - const framesToSkip = 4 // how many fn calls there is before FuncName call fatal("assertion catching: "+msg, framesToSkip) } @@ -260,22 +273,23 @@ 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. +// Note that when [Plain] [Asserter] is used ([PushAsserter] or even +// [SetDefault]), optional arguments are used to override the auto-generated +// assert violation message. func NotImplemented(a ...any) { - current().reportAssertionFault("not implemented", a) + current().reportAssertionFault(0, assertionMsg+": 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. +// Note that when [Plain] [Asserter] is used ([PushAsserter] or even +// [SetDefault]), optional arguments are used to override the auto-generated +// assert violation message. func ThatNot(term bool, a ...any) { if term { - defMsg := assertionMsg - current().reportAssertionFault(defMsg, a) + doThat(a) } } @@ -283,44 +297,50 @@ func ThatNot(term bool, a ...any) { // 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. +// Note that when [Plain] [Asserter] is used ([PushAsserter] or even +// [SetDefault]), optional arguments are used to override the auto-generated +// assert violation message. func That(term bool, a ...any) { if !term { - defMsg := assertionMsg - current().reportAssertionFault(defMsg, a) + doThat(a) } } +func doThat(a []any) { + defMsg := assertionMsg + current().reportAssertionFault(1, defMsg, a) +} + // NotNil asserts that the pointer IS NOT nil. If it is it panics/errors (default -// Asserter) the auto-generated (args appended) 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. +// Note that when [Plain] [Asserter] is used ([PushAsserter] or even +// [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" - current().reportAssertionFault(defMsg, a) + doNamed("not", "pointer", "nil", a) } } // Nil asserts that the pointer IS nil. If it is not it panics/errors (default -// Asserter) the auto-generated (args appended) 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. +// Note that when [Plain] [Asserter] is used ([PushAsserter] or even +// [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" - current().reportAssertionFault(defMsg, a) + doNamed("", "pointer", "nil", a) } } // INil asserts that the interface value IS nil. If it is it panics/errors -// (default Asserter) the auto-generated (args appended) 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 that when [Plain] [Asserter] is used ([PushAsserter] or even +// [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! @@ -329,16 +349,16 @@ func Nil[T any](p *T, a ...any) { // [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" - current().reportAssertionFault(defMsg, a) + doNamed("", "interface", "nil", a) } } // INotNil asserts that the interface value is NOT nil. If it is it -// panics/errors (default Asserter) the auto-generated (args appended) 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 that when [Plain] [Asserter] is used ([PushAsserter] or even +// [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! @@ -347,109 +367,144 @@ func INil(i any, a ...any) { // [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" - current().reportAssertionFault(defMsg, a) + doNamed("not", "interface", "nil", a) } } // SNil asserts that the slice IS nil. If it is it panics/errors (default -// Asserter) the auto-generated (args appended) 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. +// Note that when [Plain] [Asserter] is used ([PushAsserter] or even +// [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" - current().reportAssertionFault(defMsg, a) + doNamed("", "slice", "nil", a) + } +} + +// CNil asserts that the channel is nil. If it is not it panics/errors +// (default [Asserter]) the auto-generated (args appended) message. +// +// Note that when [Plain] [Asserter] is used ([PushAsserter] or even +// [SetDefault]), optional arguments are used to override the auto-generated +// assert violation message. +func CNil[C ~chan T, T any](c C, a ...any) { + if c != nil { + doNamed("", "channel", "nil", a) + } +} + +// MNil asserts that the map is nil. If it is not it panics/errors (default +// [Asserter]) the auto-generated (args appended) message. +// +// Note that when [Plain] [Asserter] is used ([PushAsserter] or even +// [SetDefault]), optional arguments are used to override the auto-generated +// assert violation message. +func MNil[M ~map[T]U, T comparable, U any](m M, a ...any) { + if m != nil { + doNamed("", "map", "nil", a) } } // SNotNil asserts that the slice is not nil. If it is it panics/errors (default -// Asserter) the auto-generated (args appended) 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. +// Note that when [Plain] [Asserter] is used ([PushAsserter] or even +// [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" - current().reportAssertionFault(defMsg, a) + doNamed("not", "slice", "nil", a) } } // CNotNil asserts that the channel is not nil. If it is it panics/errors -// (default Asserter) the auto-generated (args appended) 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 that when [Plain] [Asserter] is used ([PushAsserter] or even +// [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" - current().reportAssertionFault(defMsg, a) + doNamed("not", "channel", "nil", a) } } // MNotNil asserts that the map is not nil. If it is it panics/errors (default -// Asserter) the auto-generated (args appended) 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. +// Note that when [Plain] [Asserter] is used ([PushAsserter] or even +// [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" - current().reportAssertionFault(defMsg, a) + doNamed("not", "map", "nil", a) } } // NotEqual asserts that the values aren't equal. If they are it panics/errors -// (according the current Asserter) with the auto-generated message. You can +// (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 that when [Plain] [Asserter] is used ([PushAsserter] or even +// [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 +// 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) - current().reportAssertionFault(defMsg, a) + doShouldNotBeEqual(assertionNotEqualMsg, val, want, a) } } // 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 +// 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 that when [Plain] [Asserter] is used ([PushAsserter] or even +// [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) - current().reportAssertionFault(defMsg, a) + doShouldBeEqual(assertionEqualMsg, val, want, a) } } +func doShouldBeEqual[T comparable](aname string, val, want T, a []any) { + defMsg := fmt.Sprintf(aname+gotWantFmt, val, want) + current().reportAssertionFault(1, defMsg, a) +} + +func doShouldNotBeEqual[T comparable](aname string, val, want T, a []any) { + defMsg := fmt.Sprintf(aname+": got '%v' want (!= '%v')", val, want) + current().reportAssertionFault(1, defMsg, a) +} + // DeepEqual asserts that the (whatever) values are equal. If not it -// panics/errors (according the current Asserter) with the auto-generated +// 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 that when [Plain] [Asserter] is used ([PushAsserter] or even +// [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) - current().reportAssertionFault(defMsg, a) + current().reportAssertionFault(0, defMsg, a) } } // NotDeepEqual asserts that the (whatever) values are equal. If not it -// panics/errors (according the current Asserter) with the auto-generated +// 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 that when [Plain] [Asserter] is used ([PushAsserter] or even +// [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: @@ -457,18 +512,23 @@ func DeepEqual(val, want any, a ...any) { // 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) - current().reportAssertionFault(defMsg, a) + defMsg := fmt.Sprintf( + assertionMsg+": got '%v', want (!= '%v')", + val, + want, + ) + current().reportAssertionFault(0, defMsg, a) } } // Len asserts that the length of the string is equal to the given. If not it -// panics/errors (according the current Asserter) with the auto-generated +// 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 that when [Plain] [Asserter] is used ([PushAsserter] or even +// [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. @@ -476,18 +536,18 @@ func Len(obj string, length int, a ...any) { l := len(obj) if l != length { - defMsg := fmt.Sprintf(assertionMsg+gotWantFmt, l, length) - current().reportAssertionFault(defMsg, a) + doShouldBeEqual(assertionLenMsg, l, length, a) } } // Longer asserts that the length of the string is longer to the given. If not -// it panics/errors (according the current Asserter) with the auto-generated +// 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 that when [Plain] [Asserter] is used ([PushAsserter] or even +// [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. @@ -495,18 +555,23 @@ func Longer(s string, length int, a ...any) { l := len(s) if l <= length { - defMsg := fmt.Sprintf(assertionMsg+gotWantLongerFmt, l, length) - current().reportAssertionFault(defMsg, a) + doLonger(l, length, a) } } +func doLonger(l int, length int, a []any) { + defMsg := fmt.Sprintf(assertionMsg+gotWantLongerFmt, l, length) + current().reportAssertionFault(1, defMsg, a) +} + // Shorter asserts that the length of the string is shorter to the given. If not -// it panics/errors (according the current Asserter) with the auto-generated +// 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 that when [Plain] [Asserter] is used ([PushAsserter] or even +// [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. @@ -514,18 +579,23 @@ func Shorter(str string, length int, a ...any) { l := len(str) if l >= length { - defMsg := fmt.Sprintf(assertionMsg+gotWantShorterFmt, l, length) - current().reportAssertionFault(defMsg, a) + doShorter(l, length, a) } } +func doShorter(l int, length int, a []any) { + defMsg := fmt.Sprintf(assertionMsg+gotWantShorterFmt, l, length) + current().reportAssertionFault(1, defMsg, a) +} + // SLen 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 +// 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 that when [Plain] [Asserter] is used ([PushAsserter] or even +// [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. @@ -533,18 +603,18 @@ 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) - current().reportAssertionFault(defMsg, a) + doShouldBeEqual(assertionLenMsg, l, length, a) } } // SLonger 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 +// 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 that when [Plain] [Asserter] is used ([PushAsserter] or even +// [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. @@ -552,18 +622,18 @@ 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) - current().reportAssertionFault(defMsg, a) + doLonger(l, length, a) } } // 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 +// 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 that when [Plain] [Asserter] is used ([PushAsserter] or even +// [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. @@ -571,18 +641,18 @@ 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) - current().reportAssertionFault(defMsg, a) + doShorter(l, length, a) } } // MLen asserts that the length of the map is equal to the given. If not it -// panics/errors (according the current Asserter) with the auto-generated +// 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 that when [Plain] [Asserter] is used ([PushAsserter] or even +// [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. @@ -590,18 +660,18 @@ 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) - current().reportAssertionFault(defMsg, a) + doShouldBeEqual(assertionLenMsg, l, length, a) } } // MLonger asserts that the length of the map is longer to the given. If not it -// panics/errors (according the current Asserter) with the auto-generated +// 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 that when [Plain] [Asserter] is used ([PushAsserter] or even +// [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. @@ -609,18 +679,18 @@ 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) - current().reportAssertionFault(defMsg, a) + doLonger(l, length, a) } } // MShorter asserts that the length of the map is shorter to the given. If not -// it panics/errors (according the current Asserter) with the auto-generated +// 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 that when [Plain] [Asserter] is used ([PushAsserter] or even +// [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. @@ -628,18 +698,18 @@ 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) - current().reportAssertionFault(defMsg, a) + doShorter(l, length, a) } } // CLen asserts that the length of the chan is equal to the given. If not it -// panics/errors (according the current Asserter) with the auto-generated +// 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 that when [Plain] [Asserter] is used ([PushAsserter] or even +// [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. @@ -647,18 +717,18 @@ 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) - current().reportAssertionFault(defMsg, a) + doShouldBeEqual(assertionLenMsg, l, length, a) } } // CLonger asserts that the length of the chan is longer to the given. If not it -// panics/errors (according the current Asserter) with the auto-generated +// 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 that when [Plain] [Asserter] is used ([PushAsserter] or even +// [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. @@ -666,18 +736,18 @@ 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) - current().reportAssertionFault(defMsg, a) + doLonger(l, length, a) } } // CShorter asserts that the length of the chan is shorter to the given. If not -// it panics/errors (according the current Asserter) with the auto-generated +// 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 that when [Plain] [Asserter] is used ([PushAsserter] or even +// [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. @@ -685,59 +755,86 @@ 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) - current().reportAssertionFault(defMsg, a) + doShorter(l, length, a) } } // MKeyExists asserts that the map key exists. If not it panics/errors (current -// Asserter) the auto-generated (args appended) 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) { +// Note that when [Plain] [Asserter] is used ([PushAsserter] or even +// [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) - current().reportAssertionFault(defMsg, a) + doMKeyExists(key, a) } return val } +func doMKeyExists(key any, a []any) { + defMsg := fmt.Sprintf(assertionMsg+": key '%v' doesn't exist", key) + current().reportAssertionFault(1, defMsg, a) +} + // NotEmpty asserts that the string is not empty. If it is, it panics/errors -// (according the current Asserter) with the auto-generated message. You can +// (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 that when [Plain] [Asserter] is used ([PushAsserter] or even +// [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" - current().reportAssertionFault(defMsg, a) + doNamed("not", "string", "empty", a) } } // Empty asserts that the string is empty. If it is NOT, it panics/errors -// (according the current Asserter) with the auto-generated message. You can +// (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 that when [Plain] [Asserter] is used ([PushAsserter] or even +// [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" - current().reportAssertionFault(defMsg, a) + doNamed("", "string", "empty", a) + } +} + +// SEmpty asserts that the slice is empty. If it is 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 ([PushAsserter] or even +// [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 SEmpty[S ~[]T, T any](obj S, a ...any) { + l := len(obj) + + if l != 0 { + doNamed("", "slice", "empty", a) } } // SNotEmpty asserts that the slice is not empty. If it is, it panics/errors -// (according the current Asserter) with the auto-generated message. You can +// (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 that when [Plain] [Asserter] is used ([PushAsserter] or even +// [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. @@ -745,19 +842,39 @@ func SNotEmpty[S ~[]T, T any](obj S, a ...any) { l := len(obj) if l == 0 { - defMsg := assertionMsg + ": slice shouldn't be empty" - current().reportAssertionFault(defMsg, a) + doNamed("not", "slice", "empty", a) + } +} + +// MEmpty asserts that the map is empty. If it is 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. +// You can append the generated got-want message by using optional message +// arguments. +// +// Note that when [Plain] [Asserter] is used ([PushAsserter] or even +// [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 MEmpty[M ~map[T]U, T comparable, U any](obj M, a ...any) { + l := len(obj) + + if l != 0 { + doNamed("", "map", "empty", a) } } // MNotEmpty asserts that the map is not empty. If it is, it panics/errors -// (according the current Asserter) with the auto-generated message. You can +// (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 that when [Plain] [Asserter] is used ([PushAsserter] or even +// [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. @@ -765,11 +882,16 @@ 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" - current().reportAssertionFault(defMsg, a) + doNamed("not", "map", "empty", a) } } +func doNamed(not, tname, got string, a []any) { + not = x.Whom(not == assertionNot, " not ", " ") + defMsg := assertionMsg + ": " + tname + " should" + not + "be " + got + current().reportAssertionFault(1, 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. @@ -783,7 +905,7 @@ func MNotEmpty[M ~map[T]U, T comparable, U any](obj M, a ...any) { func NoError(err error, a ...any) { if err != nil { defMsg := assertionMsg + conCatErrStr + err.Error() - current().reportAssertionFault(defMsg, a) + current().reportAssertionFault(0, defMsg, a) } } @@ -791,53 +913,116 @@ func NoError(err error, a ...any) { // 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. +// Note that when [Plain] [Asserter] is used ([PushAsserter] or even +// [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" - current().reportAssertionFault(defMsg, a) + doError(a) + } +} + +func doError(a []any) { + defMsg := "Error:" + assertionMsg + ": missing error" + current().reportAssertionFault(1, defMsg, a) +} + +// Greater asserts that the value is greater than want. If it is not it panics +// and builds a violation message. Thanks to inlining, the performance penalty +// is equal to a single 'if-statement' that is almost nothing. +// +// Note that when [Plain] [Asserter] is used ([PushAsserter] or even +// [SetDefault]), optional arguments are used to override the auto-generated +// assert violation message. +func Greater[T Number](val, want T, a ...any) { + if val <= want { + doGreater(val, want, a) } } +func doGreater[T Number](val, want T, a []any) { + defMsg := fmt.Sprintf(assertionMsg+": got '%v', want <= '%v'", val, want) + current().reportAssertionFault(1, defMsg, a) +} + +// Less asserts that the value is less than want. If it is not it panics and +// builds a violation message. Thanks to inlining, the performance penalty is +// equal to a single 'if-statement' that is almost nothing. +// +// Note that when [Plain] [Asserter] is used ([PushAsserter] or even +// [SetDefault]), optional arguments are used to override the auto-generated +// assert violation message. +func Less[T Number](val, want T, a ...any) { + if val >= want { + doLess(val, want, a) + } +} + +func doLess[T Number](val, want T, a []any) { + defMsg := fmt.Sprintf(assertionMsg+": got '%v', want >= '%v'", val, want) + current().reportAssertionFault(1, defMsg, 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. +// Note that when [Plain] [Asserter] is used ([PushAsserter] or even +// [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) - current().reportAssertionFault(defMsg, a) + doZero(val, a) } } +func doZero[T Number](val T, a []any) { + defMsg := fmt.Sprintf(assertionMsg+": got '%v', want (== '0')", val) + current().reportAssertionFault(1, defMsg, 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. +// Note that when [Plain] [Asserter] is used ([PushAsserter] or even +// [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) - current().reportAssertionFault(defMsg, a) + doNotZero(val, a) } } -// current returns a current default asserter used for package-level -// functions like assert.That(). +func doNotZero[T Number](val T, a []any) { + defMsg := fmt.Sprintf(assertionMsg+": got '%v', want (!= 0)", val) + current().reportAssertionFault(1, defMsg, a) +} + +// current returns a current default [Asserter] used for assert functions like +// assert.That() in this gorounine. // -// Note, this indexing stuff is done because of race detection to work on client +// 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] +// +// NOTE that since our GLS [asserterMap] we still continue to use indexing. +func current() (curAsserter asserter) { + glsID := goid() + asserterMap.Rx(func(m map[int]asserter) { + aster, found := m[glsID] + if found { + curAsserter = aster + } else { + // use pkg lvl asserter if asserter is not set for gorounine. + curAsserter = defAsserter[def] + } + }) + return curAsserter } -// SetDefault sets the current default asserter for assert pkg. It also returns -// the previous asserter. +// 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 @@ -849,12 +1034,12 @@ func current() asserter { // 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. +// testing and race detection, please try to use same [Asserter] for all of them +// and set [Asserter] only one in TestMain, or in init. // // func TestMain(m *testing.M) { // SetDefault(assert.TestFull) -func SetDefault(i defInd) (old defInd) { +func SetDefault(i Asserter) (old Asserter) { // pkg lvl lock to allow only one pkg client call this at one of the time // together with the indexing, i.e we don't need to switch asserter // variable or pointer to it but just index to array they are stored. @@ -871,8 +1056,56 @@ func SetDefault(i defInd) (old defInd) { return } +// PushAsserter set [Asserter] for the current GLS (Gorounine Local Storage). +// That allows us to have multiple different [Asserter] in use in the same +// process. +// +// Let's say that in some function you want to return plain error messages +// instead of the panic asserts, you can use following in the top-level +// function: +// +// defer assert.PushAsserter(assert.Plain)() +func PushAsserter(i Asserter) (retFn function) { + var ( + prevFound bool + prevAsserter asserter + ) + + // get pkg lvl asserter + curAsserter := defAsserter[def] + // .. to check if we are doing unit tests + if !curAsserter.isUnitTesting() { + // .. allow GLS specific asserter. NOTE see current() + curGoRID := goid() + //asserterMap.Set(curGoRID, defAsserter[i]) + asserterMap.Tx(func(m map[int]asserter) { + cur, found := m[curGoRID] + if found { + prevAsserter = cur + prevFound = found + } + m[curGoRID] = defAsserter[i] + }) + } + if prevFound { + return func() { + asserterMap.Set(goid(), prevAsserter) + } + } + return PopAsserter +} + +// PopAsserter pops current gorounine specific [Asserter] from packages memory. +// Gorounine specific [Asserter] can be set with [PushAsserter]. +// +// When gorounine [Asserter] isn't set package's default [Asserter] is used. See +// [SetDefault] for more information. +func PopAsserter() { + asserterMap.Del(goid()) +} + // mapDefInd runtime asserters, that's why test asserts are removed for now. -var mapDefInd = map[string]defInd{ +var mapDefInd = map[string]Asserter{ "Plain": Plain, "Prod": Production, "Dev": Development, @@ -881,7 +1114,7 @@ var mapDefInd = map[string]defInd{ "Debug": Debug, } -var mapDefIndToString = map[defInd]string{ +var mapDefIndToString = map[Asserter]string{ Plain: "Plain", Production: "Prod", Development: "Dev", @@ -894,7 +1127,7 @@ func defaultAsserterString() string { return mapDefIndToString[def] } -func newDefInd(v string) defInd { +func newDefInd(v string) Asserter { ind, found := mapDefInd[v] if !found { return Plain diff --git a/assert/assert_test.go b/assert/assert_test.go index 2db8b34..f3cd074 100644 --- a/assert/assert_test.go +++ b/assert/assert_test.go @@ -18,71 +18,78 @@ func ExampleThat() { } err := sample() fmt.Printf("%v", err) - // Output: testing: run example: assert_test.go:16: ExampleThat.func1(): assertion violation: optional message + // Output: testing: run example: assert_test.go:16: ExampleThat.func1(): assertion failure: optional message } func ExampleNotNil() { sample := func(b *byte) (err error) { defer err2.Handle(&err, "sample") - assert.NotNil(b) + assert.Nil(b) // OK + assert.NotNil(b) // Not OK return err } var b *byte err := sample(b) fmt.Printf("%v", err) - // Output: sample: assert_test.go:28: ExampleNotNil.func1(): assertion violation: pointer shouldn't be nil + // Output: sample: assert_test.go:29: ExampleNotNil.func1(): assertion failure: pointer should not be nil } func ExampleMNotNil() { sample := func(b map[string]byte) (err error) { defer err2.Handle(&err, "sample") - assert.MNotNil(b) + assert.MEmpty(b) // OK + assert.MNil(b) // OK + assert.MNotNil(b) // Not OK return err } var b map[string]byte err := sample(b) fmt.Printf("%v", err) - // Output: sample: assert_test.go:41: ExampleMNotNil.func1(): assertion violation: map shouldn't be nil + // Output: sample: assert_test.go:44: ExampleMNotNil.func1(): assertion failure: map should not be nil } func ExampleCNotNil() { sample := func(c chan byte) (err error) { defer err2.Handle(&err, "sample") - assert.CNotNil(c) + assert.CNil(c) // OK + assert.CNotNil(c) // Not OK return err } var c chan byte err := sample(c) fmt.Printf("%v", err) - // Output: sample: assert_test.go:54: ExampleCNotNil.func1(): assertion violation: channel shouldn't be nil + // Output: sample: assert_test.go:58: ExampleCNotNil.func1(): assertion failure: channel should not be nil } func ExampleSNotNil() { sample := func(b []byte) (err error) { defer err2.Handle(&err, "sample") - assert.SNotNil(b) + assert.SEmpty(b) // OK + assert.SNil(b) // OK + assert.SNotNil(b) // Not OK return err } var b []byte err := sample(b) fmt.Printf("%v", err) - // Output: sample: assert_test.go:67: ExampleSNotNil.func1(): assertion violation: slice shouldn't be nil + // Output: sample: assert_test.go:73: ExampleSNotNil.func1(): assertion failure: slice should not be nil } func ExampleEqual() { sample := func(b []byte) (err error) { defer err2.Handle(&err, "sample") - assert.Equal(len(b), 3) + assert.NotEqual(b[0], 3) // OK, b[0] != 3; (b[0] == 1) + assert.Equal(b[1], 1) // Not OK, b[1] == 2 return err } err := sample([]byte{1, 2}) fmt.Printf("%v", err) - // Output: sample: assert_test.go:80: ExampleEqual.func1(): assertion violation: got '2', want '3' + // Output: sample: assert_test.go:87: ExampleEqual.func1(): assertion failure: equal: got '2', want '1' } func ExampleSLen() { @@ -94,7 +101,7 @@ func ExampleSLen() { } err := sample([]byte{1, 2}) fmt.Printf("%v", err) - // Output: sample: assert_test.go:92: ExampleSLen.func1(): assertion violation: got '2', want '3' + // Output: sample: assert_test.go:99: ExampleSLen.func1(): assertion failure: length: got '2', want '3' } func ExampleSNotEmpty() { @@ -106,20 +113,20 @@ func ExampleSNotEmpty() { } err := sample([]byte{}) fmt.Printf("%v", err) - // Output: sample: assert_test.go:104: ExampleSNotEmpty.func1(): assertion violation: slice shouldn't be empty + // Output: sample: assert_test.go:111: ExampleSNotEmpty.func1(): assertion failure: slice should not be empty } func ExampleNotEmpty() { sample := func(b string) (err error) { defer err2.Handle(&err, "sample") - assert.Empty(b) - assert.NotEmpty(b) + assert.Empty(b) // OK + assert.NotEmpty(b) // not OK return err } err := sample("") fmt.Printf("%v", err) - // Output: sample: assert_test.go:117: ExampleNotEmpty.func1(): assertion violation: string shouldn't be empty + // Output: sample: assert_test.go:124: ExampleNotEmpty.func1(): assertion failure: string should not be empty } func ExampleMKeyExists() { @@ -129,14 +136,14 @@ func ExampleMKeyExists() { m := map[string]string{ "1": "one", } - v := assert.MKeyExists(m, "1") - assert.Equal(v, "one") - _ = assert.MKeyExists(m, b) + v := assert.MKeyExists(m, "1") // OK, 1 --> one + assert.Equal(v, "one") // OK + _ = assert.MKeyExists(m, b) // fails with b = 2 return err } err := sample("2") fmt.Printf("%v", err) - // Output: sample: assert_test.go:134: ExampleMKeyExists.func1(): assertion violation: key '2' doesn't exist + // Output: sample: assert_test.go:141: ExampleMKeyExists.func1(): assertion failure: key '2' doesn't exist } func ExampleZero() { @@ -149,33 +156,34 @@ func ExampleZero() { var b int8 = 1 // we want sample to assert the violation. err := sample(b) fmt.Printf("%v", err) - // Output: sample: assert_test.go:146: ExampleZero.func1(): assertion violation: got '1', want (== '0') + // Output: sample: assert_test.go:153: ExampleZero.func1(): assertion failure: 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 + 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' + // Output: sample: assert_test.go:167: ExampleSLonger.func1(): assertion failure: 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 + assert.MNotEmpty(b) // OK + 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' + // Output: sample: assert_test.go:180: ExampleMShorter.func1(): assertion failure: got '1', should be shorter than '1' } func ExampleSShorter() { @@ -188,23 +196,241 @@ func ExampleSShorter() { } 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) + // Output: sample: assert_test.go:194: ExampleSShorter.func1(): assertion failure: got '1', should be shorter than '0': optional message (test_str) } -func assertZero(i int) { - assert.Zero(i) +func ExampleLess() { + sample := func(b int8) (err error) { + defer err2.Handle(&err, "sample") + + assert.Equal(b, 1) // ok + assert.Less(b, 2) // ok + assert.Less(b, 1) // not ok + return err + } + var b int8 = 1 + err := sample(b) + fmt.Printf("%v", err) + // Output: sample: assert_test.go:208: ExampleLess.func1(): assertion failure: got '1', want >= '1' +} + +func ExampleGreater() { + sample := func(b int8) (err error) { + defer err2.Handle(&err, "sample") + + assert.Equal(b, 2) // ok + assert.Greater(b, 1) // ok + assert.Greater(b, 2) // not ok + return err + } + var b int8 = 2 + err := sample(b) + fmt.Printf("%v", err) + // Output: sample: assert_test.go:223: ExampleGreater.func1(): assertion failure: got '2', want <= '2' +} + +func ExampleNotZero() { + sample := func(b int8) (err error) { + defer err2.Handle(&err, "sample") + + assert.NotZero(b) + return err + } + var b int8 + err := sample(b) + fmt.Printf("%v", err) + // Output: sample: assert_test.go:236: ExampleNotZero.func1(): assertion failure: got '0', want (!= 0) +} + +func ExampleMLen() { + sample := func(b map[int]byte) (err error) { + defer err2.Handle(&err, "sample") + + assert.MLonger(b, 1) // OK + assert.MShorter(b, 3) // OK + assert.MLen(b, 3) // Not OK + return err + } + err := sample(map[int]byte{1: 1, 2: 2}) + fmt.Printf("%v", err) + // Output: sample: assert_test.go:251: ExampleMLen.func1(): assertion failure: length: got '2', want '3' +} + +func ExampleCLen() { + sample := func(b chan int) (err error) { + defer err2.Handle(&err, "sample") + + assert.CLonger(b, 1) // OK + assert.CShorter(b, 3) // OK + assert.CLen(b, 3) // Not OK + return err + } + d := make(chan int, 2) + d <- int(1) + d <- int(1) + err := sample(d) + fmt.Printf("%v", err) + // Output: sample: assert_test.go:265: ExampleCLen.func1(): assertion failure: length: got '2', want '3' +} + +func ExampleThatNot() { + sample := func() (err error) { + defer err2.Handle(&err) + + assert.ThatNot(true, "overrides if Plain asserter") + return err + } + + // Set a specific asserter for this goroutine only, we want plain errors + defer assert.PushAsserter(assert.Plain)() + + err := sample() + fmt.Printf("%v", err) + // Output: testing: run example: overrides if Plain asserter +} + +func ExampleINotNil() { + sample := func(b error) (err error) { + defer err2.Handle(&err, "sample") + + assert.INotNil(b) // OK + assert.INil(b) // Not OK + return err + } + var b = fmt.Errorf("test") + err := sample(b) + fmt.Printf("%v", err) + // Output: sample: assert_test.go:297: ExampleINotNil.func1(): assertion failure: interface should be nil +} + +func ExampleLen() { + sample := func(b string) (err error) { + defer err2.Handle(&err, "sample") + + assert.Shorter(b, 3) // OK + assert.Longer(b, 1) // OK + assert.Len(b, 3) // Not OK + return err + } + err := sample("12") + fmt.Printf("%v", err) + // Output: sample: assert_test.go:312: ExampleLen.func1(): assertion failure: length: got '2', want '3' +} + +func ExampleDeepEqual() { + sample := func(b []byte) (err error) { + defer err2.Handle(&err, "sample") + + assert.NoError(err) + assert.NotDeepEqual(len(b), 3) // OK, correct size is 2 + assert.DeepEqual(len(b), 3) // Not OK, size is still 2 + return err + } + err := sample([]byte{1, 2}) + fmt.Printf("%v", err) + // Output: sample: assert_test.go:326: ExampleDeepEqual.func1(): assertion failure: got '2', want '3' +} + +func ExampleError() { + sample := func(b error) (err error) { + defer err2.Handle(&err, "sample") + + assert.Error(b) // OK + assert.NoError(b) // Not OK + return err + } + var b = fmt.Errorf("test") + err := sample(b) + fmt.Printf("%v", err) + // Output: sample: assert_test.go:339: ExampleError.func1(): assertion failure: test +} + +func ExampleNotImplemented() { + sample := func(_ error) (err error) { + defer err2.Handle(&err, "sample") + + assert.NotImplemented() // Not OK + return err + } + var b = fmt.Errorf("test") + err := sample(b) + fmt.Printf("%v", err) + // Output: sample: assert_test.go:352: ExampleNotImplemented.func1(): assertion failure: not implemented +} + +func BenchmarkMKeyExists(b *testing.B) { + bs := map[int]int{0: 0, 1: 1} + for n := 0; n < b.N; n++ { + assert.MKeyExists(bs, 1) + } +} + +func BenchmarkMKeyExistsOKIdiom(b *testing.B) { + bs := map[int]int{0: 0, 1: 1} + found := false + for n := 0; n < b.N; n++ { + _, ok := bs[1] + if ok { + found = ok + } + } + _ = found +} + +func BenchmarkMNotEmpty(b *testing.B) { + bs := map[int]int{0: 0, 1: 1} + for n := 0; n < b.N; n++ { + assert.MNotEmpty(bs) + } } -func assertZeroGen(i int) { - assert.Equal(i, 0) +func BenchmarkMEmpty(b *testing.B) { + bs := map[int]int{} + for n := 0; n < b.N; n++ { + assert.MEmpty(bs) + } +} + +func BenchmarkNotEmpty(b *testing.B) { + bs := "not empty" + for n := 0; n < b.N; n++ { + assert.NotEmpty(bs) + } +} + +func BenchmarkEmpty(b *testing.B) { + bs := "" + for n := 0; n < b.N; n++ { + assert.Empty(bs) + } +} + +func BenchmarkLonger(b *testing.B) { + bs := "tst" + for n := 0; n < b.N; n++ { + assert.Longer(bs, 2) + } } -func assertMLen(b map[byte]byte, l int) { - assert.MLen(b, l) +func BenchmarkShorter(b *testing.B) { + bs := "1" + for n := 0; n < b.N; n++ { + assert.Shorter(bs, 2) + } } -func assertEqualInt2(b int) { - assert.Equal(b, 2) +func BenchmarkSEmpty(b *testing.B) { + bs := []int{} + for n := 0; n < b.N; n++ { + assert.SEmpty(bs) + } +} + +func BenchmarkSNotEmpty(b *testing.B) { + bs := []byte{0} + for n := 0; n < b.N; n++ { + assert.SNotEmpty(bs) + } } func BenchmarkSNotNil(b *testing.B) { @@ -214,6 +440,41 @@ func BenchmarkSNotNil(b *testing.B) { } } +func BenchmarkMNotNil(b *testing.B) { + var bs = map[int]int{0: 0} + for n := 0; n < b.N; n++ { + assert.MNotNil(bs) + } +} + +func BenchmarkCNotNil(b *testing.B) { + var bs = make(chan int) + for n := 0; n < b.N; n++ { + assert.CNotNil(bs) + } +} + +func BenchmarkINotNil(b *testing.B) { + var bs any = err2.ErrNotAccess + for n := 0; n < b.N; n++ { + assert.INotNil(bs) + } +} + +func BenchmarkINil(b *testing.B) { + var bs any + for n := 0; n < b.N; n++ { + assert.INil(bs) + } +} + +func BenchmarkNil(b *testing.B) { + var bs *int + for n := 0; n < b.N; n++ { + assert.Nil(bs) + } +} + func BenchmarkNotNil(b *testing.B) { bs := new(int) for n := 0; n < b.N; n++ { @@ -221,6 +482,27 @@ func BenchmarkNotNil(b *testing.B) { } } +func BenchmarkSNil(b *testing.B) { + var bs []int + for n := 0; n < b.N; n++ { + assert.SNil(bs) + } +} + +func BenchmarkMNil(b *testing.B) { + var bs map[int]int + for n := 0; n < b.N; n++ { + assert.MNil(bs) + } +} + +func BenchmarkCNil(b *testing.B) { + var bs chan int + for n := 0; n < b.N; n++ { + assert.CNil(bs) + } +} + func BenchmarkThat(b *testing.B) { const four = 4 for n := 0; n < b.N; n++ { @@ -228,22 +510,34 @@ func BenchmarkThat(b *testing.B) { } } -func BenchmarkNotEmpty(b *testing.B) { - str := "test" +func BenchmarkZero(b *testing.B) { + const zero = 0 for n := 0; n < b.N; n++ { - assert.NotEmpty(str) + assert.Zero(zero) } } -func BenchmarkZero(b *testing.B) { +func BenchmarkGreater(b *testing.B) { + for n := 0; n < b.N; n++ { + assert.Greater(1, 0) + } +} + +func BenchmarkLess(b *testing.B) { + for n := 0; n < b.N; n++ { + assert.Less(0, 1) + } +} + +func BenchmarkError(b *testing.B) { for n := 0; n < b.N; n++ { - assertZero(0) + assert.Error(err2.ErrNotAccess) } } func BenchmarkEqual(b *testing.B) { for n := 0; n < b.N; n++ { - assertZeroGen(0) + assert.Equal(n, n) } } @@ -262,7 +556,21 @@ func BenchmarkAsserter_TrueIfVersion(b *testing.B) { func BenchmarkMLen(b *testing.B) { d := map[byte]byte{1: 1, 2: 2} for n := 0; n < b.N; n++ { - assertMLen(d, 2) + assert.MLen(d, 2) + } +} + +func BenchmarkMShorter(b *testing.B) { + d := map[byte]byte{1: 1, 2: 2} + for n := 0; n < b.N; n++ { + assert.MShorter(d, 4) + } +} + +func BenchmarkMLonger(b *testing.B) { + d := map[byte]byte{1: 1, 2: 2} + for n := 0; n < b.N; n++ { + assert.MLonger(d, 1) } } @@ -273,6 +581,47 @@ func BenchmarkSLen(b *testing.B) { } } +func BenchmarkSShorter(b *testing.B) { + d := []byte{1, 2} + for n := 0; n < b.N; n++ { + assert.SShorter(d, 3) + } +} + +func BenchmarkSLonger(b *testing.B) { + d := []byte{1, 2} + for n := 0; n < b.N; n++ { + assert.SLonger(d, 1) + } +} + +func BenchmarkCLen(b *testing.B) { + d := make(chan int, 2) + d <- int(1) + d <- int(1) + for n := 0; n < b.N; n++ { + assert.CLen(d, 2) + } +} + +func BenchmarkCShorter(b *testing.B) { + d := make(chan int, 2) + d <- int(1) + d <- int(1) + for n := 0; n < b.N; n++ { + assert.CShorter(d, 3) + } +} + +func BenchmarkCLonger(b *testing.B) { + d := make(chan int, 2) + d <- int(1) + d <- int(1) + for n := 0; n < b.N; n++ { + assert.CLonger(d, 1) + } +} + func BenchmarkLen(b *testing.B) { s := "len" for n := 0; n < b.N; n++ { @@ -287,10 +636,17 @@ func BenchmarkSLen_thatVersion(b *testing.B) { } } +func BenchmarkNotEqualInt(b *testing.B) { + const d = 2 + for n := 0; n < b.N; n++ { + assert.NotEqual(d, 3) + } +} + func BenchmarkEqualInt(b *testing.B) { const d = 2 for n := 0; n < b.N; n++ { - assertEqualInt2(d) + assert.Equal(d, 2) } } diff --git a/assert/asserter.go b/assert/asserter.go index 803213d..7edccda 100644 --- a/assert/asserter.go +++ b/assert/asserter.go @@ -56,22 +56,28 @@ const officialTestOutputPrefix = " " // 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) { +func (asserter asserter) reportAssertionFault( + extraInd int, + defaultMsg string, + a []any, +) { if asserter.hasStackTrace() { if asserter.isUnitTesting() { // Note. that the assert in the test function is printed in // reportPanic below - const stackLvl = 5 // amount of functions before we're here + const StackLvl = 5 // amount of functions before we're here + stackLvl := StackLvl + extraInd debug.PrintStackForTest(os.Stderr, stackLvl) } else { // amount of functions before we're here, which is different // between runtime (this) and test-run (above) - const stackLvl = 2 + const StackLvl = 2 + stackLvl := StackLvl + extraInd debug.PrintStack(stackLvl) } } if asserter.hasCallerInfo() { - defaultMsg = asserter.callerInfo(defaultMsg) + defaultMsg = asserter.callerInfo(defaultMsg, extraInd) } if len(a) > 0 { if format, ok := a[0].(string); ok { @@ -126,13 +132,14 @@ Assertion Fault at: var shortFmtStr = `%s:%d: %s(): %s` -func (asserter asserter) callerInfo(msg string) (info string) { +func (asserter asserter) callerInfo(msg string, extraInd int) (info string) { ourFmtStr := shortFmtStr if asserter.hasFormattedCallerInfo() { ourFmtStr = longFmtStr } - const framesToSkip = 3 // how many fn calls there is before FuncName call + const ToSkip = 3 + framesToSkip := ToSkip + extraInd // how many fn calls there is before FuncName call includePath := asserter.isUnitTesting() funcName, filename, line, ok := str.FuncName(framesToSkip, includePath) if ok { @@ -157,7 +164,8 @@ func (asserter asserter) hasStackTrace() bool { } func (asserter asserter) hasCallerInfo() bool { - return asserter&asserterCallerInfo != 0 || asserter.hasFormattedCallerInfo() + return asserter&asserterCallerInfo != 0 || + asserter.hasFormattedCallerInfo() } func (asserter asserter) hasFormattedCallerInfo() bool { diff --git a/assert/doc.go b/assert/doc.go index d6a246e..b026f2f 100644 --- a/assert/doc.go +++ b/assert/doc.go @@ -13,7 +13,7 @@ sub-gouroutines: assert.Equal(alice.Len(), 1) // assert anything normally ... go func() { - assert.PushTester(t) // <-- Needs to do again for a new goroutine + assert.PushTester(t)() // <-- Needs to do again for a new goroutine # Merge Runtime And Unit Test Assertions @@ -25,7 +25,7 @@ all assert violations in the unit tests as well: assert.That(c.isLeaf(invitersKey), "only leaf can invite") 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 +reported as test failures. 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 @@ -35,12 +35,14 @@ chapter. # Call Stack Traversal During Tests 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) +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 in Neovim/Vim. For example, you can find +a compatible test result parser for Neovim from this plugin [nvim-go] (fork). -With a sizeable multi-repo environment, this has proven to be valuable. +The call stack traversal has proven to be very valuable for package and module +development in Go, especially when following TDD and fast development feedback +cycles. # Why Runtime Asserts Are So Important? @@ -52,9 +54,12 @@ raise up quality of our software. 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 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. +set a goroutine specific [Asserter] with [PushAsserter] function. + +The assert package's default [Asserter] you can set with [SetDefault] or +-asserter flag if Go's flag package (or similar) 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. // Production asserter adds formatted caller info to normal errors. // Information is transported thru error values when err2.Handle is in use. @@ -62,6 +67,11 @@ binary but get the error messages and diagnostic they need. Please see the code examples for more information. +Note that if an [Asserter] is set for a goroutine level, it cannot be changed +with the -asserter flag or [SetDefault]. The GLS [Asserter] is used for a +reason, so it's good that even a unit test asserter won't override it in those +cases. + # Flag Package Support The assert package supports Go's flags. All you need to do is to call @@ -75,15 +85,12 @@ 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. 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. +The performance of the assert functions are equal to the if-statement thanks for +inlining. All of the generics-based versions are the equally fast! -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. +We should prefer specialized versions like [assert.Equal] that we get precise +and readable error messages automatically. Error messagas follow Go idiom of +'got xx, want yy'. And we still can annotate error message if we want. # Naming diff --git a/doc.go b/doc.go index af37f26..7d7b007 100644 --- a/doc.go +++ b/doc.go @@ -75,7 +75,7 @@ programmatically (before [flag.Parse] if you are using that): or err2.SetPanicTracer(log.Writer()) // panic stack trace to std logger -Note. Since [Catch]'s default mode is to recover from panics, it's a good +Note that 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. diff --git a/err2_test.go b/err2_test.go index ce2cda9..61c91fa 100644 --- a/err2_test.go +++ b/err2_test.go @@ -1,6 +1,7 @@ package err2_test import ( + "errors" "fmt" "io" "os" @@ -8,24 +9,24 @@ import ( "testing" "github.com/lainio/err2" - "github.com/lainio/err2/internal/test" + "github.com/lainio/err2/internal/require" "github.com/lainio/err2/try" ) const errStringInThrow = "this is an ERROR" -func throw() (string, error) { - return "", fmt.Errorf(errStringInThrow) -} +var ( + errToTest = errors.New(errStringInThrow) +) -func twoStrNoThrow() (string, string, error) { return "test", "test", nil } -func intStrNoThrow() (int, string, error) { return 1, "test", nil } -func boolIntStrNoThrow() (bool, int, string, error) { return true, 1, "test", nil } -func noThrow() (string, error) { return "test", nil } +func throw() (string, error) { return "", errToTest } +func noThrow() (string, error) { return "test", nil } +func noErr() error { return nil } -func noErr() error { - return nil -} +func twoStrNoThrow() (string, string, error) { return "test", "test", nil } +func intStrNoThrow() (int, string, error) { return 1, "test", nil } + +func boolIntStrNoThrow() (bool, int, string, error) { return true, 1, "test", nil } func TestTry_noError(t *testing.T) { t.Parallel() @@ -33,6 +34,11 @@ func TestTry_noError(t *testing.T) { try.To2(twoStrNoThrow()) try.To2(intStrNoThrow()) try.To3(boolIntStrNoThrow()) + + _ = try.T1(noThrow())("test") + _, _ = try.T2(twoStrNoThrow())("test") + _, _ = try.T2(intStrNoThrow())("test") + try.T3(boolIntStrNoThrow())("test") // linter says: _, _, _, } func TestDefault_Error(t *testing.T) { @@ -57,115 +63,136 @@ func TestTry_Error(t *testing.T) { func TestHandle_noerrHandler(t *testing.T) { t.Parallel() - t.Run("noerr handler in ONLY one and NO error happens", func(t *testing.T) { - t.Parallel() - var err error - var handlerCalled bool - defer func() { - test.Require(t, handlerCalled) - }() - // This is the handler we are thesting! - defer err2.Handle(&err, func(noerr bool) { - handlerCalled = noerr - }) + t.Run( + "noerr handler in ONLY one and NO error happens", + func(t *testing.T) { + t.Parallel() + var err error + var handlerCalled bool + defer func() { + require.That(t, handlerCalled) + }() + // This is the handler we are testing! + defer err2.Handle(&err, func(noerr bool) { + handlerCalled = noerr + }) - try.To(noErr()) - }) + try.To(noErr()) + }, + ) - t.Run("noerr handler is the last and NO error happens", func(t *testing.T) { - t.Parallel() - var err error - var handlerCalled bool - defer func() { - test.Require(t, handlerCalled) - }() - defer err2.Handle(&err, func(err error) error { - // this should not be called, so lets try to fuckup things... - handlerCalled = false - test.Require(t, false) - return err - }) + t.Run( + "noerr handler is the last and NO error happens", + func(t *testing.T) { + t.Parallel() + var err error + var handlerCalled bool + defer func() { + require.That(t, handlerCalled) + }() + defer err2.Handle(&err, func(err error) error { + // this should not be called, so lets try to fuckup things... + handlerCalled = false + require.That(t, false) + return err + }) - // This is the handler we are thesting! - defer err2.Handle(&err, func(noerr bool) { - handlerCalled = noerr - }) + // This is the handler we are testing! + defer err2.Handle(&err, func(noerr bool) { + handlerCalled = noerr + }) - try.To(noErr()) - }) + try.To(noErr()) + }, + ) t.Run("noerr handler is the last and error happens", func(t *testing.T) { t.Parallel() var err error var handlerCalled bool defer func() { - test.Require(t, !handlerCalled) + require.ThatNot(t, handlerCalled) }() + + // This is the handler we are testing! defer err2.Handle(&err, func(err error) error { + require.ThatNot(t, handlerCalled) handlerCalled = false - test.Require(t, true, "error should be handled") + require.That(t, true, "error should be handled") return err }) - // This is the handler we are thesting! - defer err2.Handle(&err, func(noerr bool) { - test.Require(t, noerr) - handlerCalled = noerr + // This is the handler we are testing! AND it's not called in error. + defer err2.Handle(&err, func(bool) { + require.That(t, false, "when error this is not called") }) try.To1(throw()) }) - t.Run("noerr is first and error happens with many handlers", func(t *testing.T) { - t.Parallel() - var ( - err error - finalAnnotatedErr = fmt.Errorf("err: %v", errStringInThrow) - handlerCalled bool - ) - defer func() { - test.Require(t, !handlerCalled) - test.RequireEqual(t, err.Error(), finalAnnotatedErr.Error()) - }() + t.Run( + "noerr is first and error happens with many handlers", + func(t *testing.T) { + t.Parallel() + var ( + err error + finalAnnotatedErr = fmt.Errorf("err: %v", errStringInThrow) + handlerCalled bool + callCount int + ) + defer func() { + require.ThatNot(t, handlerCalled) + require.Equal(t, callCount, 2) + require.Equal(t, err.Error(), finalAnnotatedErr.Error()) + }() - // This is the handler we are thesting! - defer err2.Handle(&err, func(noerr bool) { - test.Require(t, false, "if error occurs/reset, this cannot happen") - handlerCalled = noerr - }) + // This is the handler we are testing! AND it's not called in error. + defer err2.Handle(&err, func(noerr bool) { + require.That( + t, + false, + "if error occurs/reset, this cannot happen", + ) + handlerCalled = noerr + }) - // important! test that our handler doesn't change the current error - // and it's not nil - defer err2.Handle(&err, func(er error) error { - test.Require(t, er != nil, "er val: ", er, err) - return er - }) + // important! test that our handler doesn't change the current error + // and it's not nil + defer err2.Handle(&err, func(er error) error { + require.That(t, er != nil, "er val: ", er, err) + require.Equal(t, callCount, 1, "this is called in sencond") + callCount++ + return er + }) - defer err2.Handle(&err, func(err error) error { - // this should not be called, so lets try to fuckup things... - handlerCalled = false - test.Require(t, err != nil) - return finalAnnotatedErr - }) - try.To1(throw()) - }) + defer err2.Handle(&err, func(err error) error { + // this should not be called, so lets try to fuckup things... + require.Equal(t, callCount, 0, "this is called in first") + callCount++ + handlerCalled = false + require.That(t, err != nil) + return finalAnnotatedErr + }) + try.To1(throw()) + }, + ) t.Run("noerr handler is first and NO error happens", func(t *testing.T) { t.Parallel() var err error var handlerCalled bool defer func() { - test.Require(t, handlerCalled) + require.That(t, handlerCalled) }() - // This is the handler we are thesting! + // This is the handler we are testing! defer err2.Handle(&err, func(noerr bool) { - test.Require(t, noerr) + require.That(t, noerr) handlerCalled = noerr }) defer err2.Handle(&err, func(err error) error { - test.Require(t, false, "no error to handle!") + require.That(t, false, "no error to handle!") // this should not be called, so lets try to fuckup things... handlerCalled = false // see first deferred function return err @@ -173,108 +200,119 @@ func TestHandle_noerrHandler(t *testing.T) { try.To(noErr()) }) - t.Run("noerr handler is first of MANY and NO error happens", func(t *testing.T) { - t.Parallel() - var err error - var handlerCalled bool - defer func() { - test.Require(t, handlerCalled) - }() + t.Run( + "noerr handler is first of MANY and NO error happens", + func(t *testing.T) { + t.Parallel() + var err error + var handlerCalled bool + defer func() { + require.That(t, handlerCalled) + }() - // This is the handler we are thesting! - defer err2.Handle(&err, func(noerr bool) { - test.Require(t, noerr) - handlerCalled = noerr - }) + // This is the handler we are testing! + defer err2.Handle(&err, func(noerr bool) { + require.That(t, true) + require.That(t, noerr) + handlerCalled = noerr + }) - defer err2.Handle(&err) + defer err2.Handle(&err) - defer err2.Handle(&err, func(err error) error { - test.Require(t, false, "no error to handle!") - // this should not be called, so lets try to fuckup things... - handlerCalled = false // see first deferred function - return err - }) + defer err2.Handle(&err, func(err error) error { + require.That(t, false, "no error to handle!") + // this should not be called, so lets try to fuckup things... + handlerCalled = false // see first deferred function + return err + }) - defer err2.Handle(&err, func(err error) error { - test.Require(t, false, "no error to handle!") - // this should not be called, so lets try to fuckup things... - handlerCalled = false // see first deferred function - return err - }) - try.To(noErr()) - }) + defer err2.Handle(&err, func(err error) error { + require.That(t, false, "no error to handle!") + // this should not be called, so lets try to fuckup things... + handlerCalled = false // see first deferred function + return err + }) + try.To(noErr()) + }, + ) - t.Run("noerr handler is first of MANY and error happens UNTIL RESET", func(t *testing.T) { - t.Parallel() - var err error - var noerrHandlerCalled, errHandlerCalled bool - defer func() { - test.Require(t, noerrHandlerCalled) - test.Require(t, errHandlerCalled) - }() + t.Run( + "noerr handler is first of MANY and error happens UNTIL RESET", + func(t *testing.T) { + t.Parallel() + var err error + var noerrHandlerCalled, errHandlerCalled bool + defer func() { + require.That(t, noerrHandlerCalled) + require.That(t, errHandlerCalled) + }() - // This is the handler we are thesting! - defer err2.Handle(&err, func(noerr bool) { - test.Require(t, true) // we are here, for debugging - test.Require(t, noerr) - noerrHandlerCalled = noerr - }) + // This is the handler we are testing! + defer err2.Handle(&err, func(noerr bool) { + require.That(t, true) // we are here, for debugging + require.That(t, noerr) + noerrHandlerCalled = noerr + }) - // this is the err handler that -- RESETS -- the error to nil - defer err2.Handle(&err, func(err error) error { - test.Require(t, err != nil) // helps fast debugging + // this is the err handler that -- RESETS -- the error to nil + defer err2.Handle(&err, func(err error) error { + require.That(t, err != nil) // helps fast debugging - // this should not be called, so lets try to fuckup things... - noerrHandlerCalled = false // see first deferred function - // keep the track that we have been here - errHandlerCalled = true // see first deferred function - return nil - }) + // this should not be called, so lets try to fuckup things... + noerrHandlerCalled = false // see first deferred function + // keep the track that we have been here + errHandlerCalled = true // see first deferred function + return nil + }) - defer err2.Handle(&err, func(err error) error { - test.Require(t, err != nil) // helps fast debugging - // this should not be called, so lets try to fuckup things... - noerrHandlerCalled = false // see first deferred function + defer err2.Handle(&err, func(err error) error { + require.That(t, err != nil) // helps fast debugging + // this should not be called, so lets try to fuckup things... + noerrHandlerCalled = false // see first deferred function - errHandlerCalled = true // see first deferred function - return err - }) - try.To1(throw()) - }) + errHandlerCalled = true // see first deferred function + return err + }) + try.To1(throw()) + }, + ) - t.Run("noerr handler is middle of MANY and NO error happens", func(t *testing.T) { - t.Parallel() - var err error - var handlerCalled bool - defer func() { - test.Require(t, handlerCalled) - }() + t.Run( + "noerr handler is middle of MANY and NO error happens", + func(t *testing.T) { + t.Parallel() + var err error + var handlerCalled bool + defer func() { + require.That(t, handlerCalled) + }() - defer err2.Handle(&err) - defer err2.Handle(&err) + defer err2.Handle(&err) + defer err2.Handle(&err) - defer err2.Handle(&err, func(err error) error { - test.Require(t, false, "no error to handle!") - // this should not be called, so lets try to fuckup things... - handlerCalled = false // see first deferred function - return err - }) + defer err2.Handle(&err, func(err error) error { + require.That(t, false, "no error to handle!") + // this should not be called, so lets try to fuckup things... + handlerCalled = false // see first deferred function + return err + }) - // This is the handler we are thesting! - defer err2.Handle(&err, func(noerr bool) { - test.Require(t, noerr) - handlerCalled = noerr - }) + // This is the handler we are testing! + defer err2.Handle(&err, func(noerr bool) { + require.That(t, true, "this must be called") + require.That(t, noerr) + handlerCalled = noerr + }) - defer err2.Handle(&err, func(err error) error { - test.Require(t, false, "no error to handle!") - // this should not be called, so lets try to fuckup things... - handlerCalled = false // see first deferred function - return err - }) - try.To(noErr()) - }) + defer err2.Handle(&err, func(err error) error { + require.That(t, false, "no error to handle!") + // this should not be called, so lets try to fuckup things... + handlerCalled = false // see first deferred function + return err + }) + try.To(noErr()) + }, + ) } func TestPanickingCatchAll(t *testing.T) { @@ -338,7 +376,11 @@ func TestPanickingCatchAll(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() defer func() { - test.Require(t, recover() == nil, "panics should NOT carry on") + require.That( + t, + recover() == nil, + "panics should NOT carry on", + ) }() tt.args.f() }) @@ -382,7 +424,11 @@ func TestPanickingCarryOn_Handle(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() defer func() { - test.Require(t, recover() != nil, "panics should went thru when not our errors") + require.That( + t, + recover() != nil, + "panics should went thru when not our errors", + ) }() tt.args.f() }) @@ -508,12 +554,12 @@ func TestPanicking_Handle(t *testing.T) { defer func() { r := recover() if tt.wants == nil { - test.Require(t, r != nil, "wants err, then panic") + require.That(t, r != nil, "wants err, then panic") } }() err := tt.args.f() if err != nil { - test.RequireEqual(t, err.Error(), tt.wants.Error()) + require.Equal(t, err.Error(), tt.wants.Error()) } }) } @@ -554,7 +600,11 @@ func TestPanicking_Catch(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() defer func() { - test.Require(t, recover() == nil, "panics should NOT carry on") + require.That( + t, + recover() == nil, + "panics should NOT carry on", + ) }() tt.args.f() }) @@ -573,7 +623,7 @@ func TestCatch_Error(t *testing.T) { func Test_TryOutError(t *testing.T) { t.Parallel() defer err2.Catch(func(err error) error { - test.RequireEqual(t, err.Error(), "fails: test: this is an ERROR", + require.Equal(t, err.Error(), "fails: test: this is an ERROR", "=> we should catch right error str here") return err }) @@ -583,7 +633,7 @@ func Test_TryOutError(t *testing.T) { // let's test try.Out1() and it's throw capabilities here, even try.To1() // is the preferred way. retVal = try.Out1(noThrow()).Handle().Val1 - test.Require(t, retVal == "test", "if no error happens, we get value") + require.Equal(t, retVal, "test", "if no error happens, we get value") _ = try.Out1(throw()).Handle("fails: %v", retVal).Val1 t.Fail() // If everything works in Handle we are never here. @@ -615,11 +665,11 @@ func TestCatch_Panic(t *testing.T) { func TestSetErrorTracer(t *testing.T) { t.Parallel() w := err2.ErrorTracer() - test.Require(t, w == nil, "error tracer should be nil") + require.That(t, w == nil, "error tracer should be nil") var w1 io.Writer err2.SetErrorTracer(w1) w = err2.ErrorTracer() - test.Require(t, w == nil, "error tracer should be nil") + require.That(t, w == nil, "error tracer should be nil") } func ExampleCatch_withFmt() { @@ -804,6 +854,37 @@ func BenchmarkTry_ErrVar(b *testing.B) { } } +func BenchmarkTry_StringGenerics(b *testing.B) { + for n := 0; n < b.N; n++ { + _ = try.To1(noThrow()) + } +} + +func BenchmarkTry_StrStrGenerics(b *testing.B) { + for n := 0; n < b.N; n++ { + _, _ = try.To2(twoStrNoThrow()) + } +} + +func BenchmarkTryInsideCall(b *testing.B) { + for n := 0; n < b.N; n++ { + try.To(noErr()) + } +} + +func BenchmarkTryVarCall(b *testing.B) { + for n := 0; n < b.N; n++ { + err := noErr() + try.To(err) + } +} + +func BenchmarkTryOut_ErrVarNoRetval(b *testing.B) { + for n := 0; n < b.N; n++ { + try.Out(noErr()).Handle() + } +} + func BenchmarkTryOut_ErrVar(b *testing.B) { for n := 0; n < b.N; n++ { _, err := noThrow() @@ -811,34 +892,42 @@ func BenchmarkTryOut_ErrVar(b *testing.B) { } } -func BenchmarkTry_StringGenerics(b *testing.B) { +func BenchmarkTryOut_LogStringGenerics(b *testing.B) { for n := 0; n < b.N; n++ { - _ = try.To1(noThrow()) + _ = try.Out1(noThrow()).Logf("test").Val1 } } func BenchmarkTryOut_StringGenerics(b *testing.B) { for n := 0; n < b.N; n++ { - _ = try.Out1(noThrow()).Handle() + _ = try.Out1(noThrow()).Handle().Val1 } } -func BenchmarkTry_StrStrGenerics(b *testing.B) { +func BenchmarkTryOut_StringGenericsNoVal(b *testing.B) { for n := 0; n < b.N; n++ { - _, _ = try.To2(twoStrNoThrow()) + _ = try.Out1(noThrow()).Logf("test").Val1 + r := try.Out1(noThrow()).Handle() + _ = r.Val1 } } -func BenchmarkTryInsideCall(b *testing.B) { +func BenchmarkT_ErrVar(b *testing.B) { for n := 0; n < b.N; n++ { - try.To(noErr()) + _, err := noThrow() + try.T(err)("test") } } -func BenchmarkTryVarCall(b *testing.B) { +func BenchmarkT_StringGenerics(b *testing.B) { for n := 0; n < b.N; n++ { - err := noErr() - try.To(err) + _ = try.T1(noThrow())("test") + } +} + +func BenchmarkT_IntStringGenerics(b *testing.B) { + for n := 0; n < b.N; n++ { + _, _ = try.T2(intStrNoThrow())("test") } } diff --git a/internal/debug/debug.go b/internal/debug/debug.go index eb94254..8185dbe 100644 --- a/internal/debug/debug.go +++ b/internal/debug/debug.go @@ -38,7 +38,9 @@ var ( PackageRegexp = regexp.MustCompile(`lainio/err2[a-zA-Z0-9_/.\[\]]*\(`) // we want to check that this is not our package - packageRegexp = regexp.MustCompile(`^github\.com/lainio/err2[a-zA-Z0-9_/.\[\]]*\(`) + packageRegexp = regexp.MustCompile( + `^github\.com/lainio/err2[a-zA-Z0-9_/.\[\]]*\(`, + ) // testing package exluding regexps: testingPkgRegexp = regexp.MustCompile(`^testing\.`) @@ -77,7 +79,8 @@ func (si StackInfo) needToCalcFnNameAnchor() bool { // isLvlOnly return true if all fields are nil and Level != 0 that should be // used then. func (si StackInfo) isLvlOnly() bool { - return si.Level != 0 && si.Regexp == nil && si.PackageName == "" && si.FuncName == "" + return si.Level != 0 && si.Regexp == nil && si.PackageName == "" && + si.FuncName == "" } func (si StackInfo) canPrint(s string, anchorLine, i int) (ok bool) { @@ -105,7 +108,9 @@ func (si StackInfo) canPrint(s string, anchorLine, i int) (ok bool) { // runtime.Stack and processed to proper format to be shown in test output by // starting from stackLevel. func PrintStackForTest(w io.Writer, stackLevel int) { - stackBuf := bytes.NewBuffer(debug.Stack()) + stack := debug.Stack() + //println(string(stack)) + stackBuf := bytes.NewBuffer(stack) printStackForTest(stackBuf, w, stackLevel) } diff --git a/internal/debug/debug_test.go b/internal/debug/debug_test.go index cef1010..a6b1748 100644 --- a/internal/debug/debug_test.go +++ b/internal/debug/debug_test.go @@ -6,7 +6,7 @@ import ( "strings" "testing" - "github.com/lainio/err2/internal/test" + "github.com/lainio/err2/internal/require" ) func TestFullName(t *testing.T) { @@ -21,17 +21,28 @@ func TestFullName(t *testing.T) { } tests := []ttest{ {"all empty", args{StackInfo{"", "", 0, nil, nil}}, ""}, - {"namespaces", args{StackInfo{"lainio/err2", "", 0, nil, nil}}, "lainio/err2"}, - {"both", args{StackInfo{"lainio/err2", "try", 0, nil, nil}}, "lainio/err2.try"}, - {"short both", args{StackInfo{"err2", "Handle", 0, nil, nil}}, "err2.Handle"}, + { + "namespaces", + args{StackInfo{"lainio/err2", "", 0, nil, nil}}, + "lainio/err2", + }, + { + "both", + args{StackInfo{"lainio/err2", "try", 0, nil, nil}}, + "lainio/err2.try", + }, + { + "short both", + args{StackInfo{"err2", "Handle", 0, nil, nil}}, + "err2.Handle", + }, {"func", args{StackInfo{"", "try", 0, nil, nil}}, "try"}, } for _, ttv := range tests { tt := ttv t.Run(tt.name, func(t *testing.T) { t.Parallel() - test.Requiref(t, tt.retval == tt.fullName(), "must be equal: %s", - tt.retval) + require.Equal(t, tt.retval, tt.fullName()) }) } } @@ -83,7 +94,7 @@ func TestIsAnchor(t *testing.T) { tt := ttv t.Run(tt.name, func(t *testing.T) { t.Parallel() - test.Require(t, tt.retval == tt.isAnchor(tt.input), "equal") + require.Equal(t, tt.retval, tt.isAnchor(tt.input)) }) } } @@ -129,7 +140,7 @@ func TestIsFuncAnchor(t *testing.T) { tt := ttv t.Run(tt.name, func(t *testing.T) { t.Parallel() - test.Require(t, tt.retval == tt.isFuncAnchor(tt.input), "equal") + require.Equal(t, tt.retval, tt.isFuncAnchor(tt.input)) }) } } @@ -150,7 +161,7 @@ func TestFnLNro(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() output := fnLNro(tt.input) - test.Require(t, output == tt.output, output) + require.Equal(t, output, tt.output) }) } } @@ -165,25 +176,43 @@ func TestFnName(t *testing.T) { tests := []ttest{ {"panic", "panic({0x102ed30c0, 0x1035910f0})", "panic"}, - {"our namespace", "github.com/lainio/err2/internal/debug.FprintStack({0x102ff7e88, 0x14000010020}, {{0x0, 0x0}, {0x102c012b8, 0x6}, 0x1, 0x140000bcb40})", - "debug.FprintStack"}, - {"our namespace and func1", "github.com/lainio/err2/internal/debug.FprintStack.func1({0x102ff7e88, 0x14000010020}, {{0x0, 0x0}, {0x102c012b8, 0x6}, 0x1, 0x140000bcb40})", - "debug.FprintStack"}, - {"our double namespace", "github.com/lainio/err2/internal/handler.Info.callPanicHandler({{0x102ed30c0, 0x1035910f0}, {0x102ff7e88, 0x14000010020}, 0x0, 0x140018643e0, 0x0})", - "handler.Info.callPanicHandler"}, - {"our handler process", "github.com/lainio/err2/internal/handler.Process.func1({{0x102ed30c0, 0x1035910f0}, {0x102ff7e88, 0x14000010020}, 0x0, 0x140018643e0, 0x0})", - "handler.Process"}, - {"our handler process and more anonymous funcs", "github.com/lainio/err2/internal/handler.Process.func1.2({{0x102ed30c0, 0x1035910f0}, {0x102ff7e88, 0x14000010020}, 0x0, 0x140018643e0, 0x0})", - "handler.Process"}, - {"method and package name", "github.com/findy-network/findy-agent/agent/ssi.(*DIDAgent).AssertWallet(...)", - "ssi.(*DIDAgent).AssertWallet"}, + { + "our namespace", + "github.com/lainio/err2/internal/debug.FprintStack({0x102ff7e88, 0x14000010020}, {{0x0, 0x0}, {0x102c012b8, 0x6}, 0x1, 0x140000bcb40})", + "debug.FprintStack", + }, + { + "our namespace and func1", + "github.com/lainio/err2/internal/debug.FprintStack.func1({0x102ff7e88, 0x14000010020}, {{0x0, 0x0}, {0x102c012b8, 0x6}, 0x1, 0x140000bcb40})", + "debug.FprintStack", + }, + { + "our double namespace", + "github.com/lainio/err2/internal/handler.Info.callPanicHandler({{0x102ed30c0, 0x1035910f0}, {0x102ff7e88, 0x14000010020}, 0x0, 0x140018643e0, 0x0})", + "handler.Info.callPanicHandler", + }, + { + "our handler process", + "github.com/lainio/err2/internal/handler.Process.func1({{0x102ed30c0, 0x1035910f0}, {0x102ff7e88, 0x14000010020}, 0x0, 0x140018643e0, 0x0})", + "handler.Process", + }, + { + "our handler process and more anonymous funcs", + "github.com/lainio/err2/internal/handler.Process.func1.2({{0x102ed30c0, 0x1035910f0}, {0x102ff7e88, 0x14000010020}, 0x0, 0x140018643e0, 0x0})", + "handler.Process", + }, + { + "method and package name", + "github.com/findy-network/findy-agent/agent/ssi.(*DIDAgent).AssertWallet(...)", + "ssi.(*DIDAgent).AssertWallet", + }, } for _, ttv := range tests { tt := ttv t.Run(tt.name, func(t *testing.T) { t.Parallel() output := fnName(tt.input) - test.Require(t, output == tt.output, output) + require.Equal(t, output, tt.output) }) } } @@ -210,7 +239,7 @@ func TestStackPrint_noLimits(t *testing.T) { FuncName: "", Level: 0, }) - test.Require(t, tt.input == w.String(), "") + require.Equal(t, tt.input, w.String()) }) } } @@ -238,8 +267,8 @@ func TestStackPrintForTest(t *testing.T) { // print(tt.output) // println("------") // print(w.String()) - test.Requiref(t, a == b, "%d %d", a, b) - test.Require(t, tt.output == w.String(), w.String()) + require.Equal(t, a, b) + require.Equal(t, tt.output, w.String()) }) } } @@ -256,15 +285,52 @@ func TestCalcAnchor(t *testing.T) { anchor int } tests := []ttest{ - {"macOS from test using regexp", args{inputFromMac, StackInfo{"", "panic(", 1, PackageRegexp, nil}}, 12}, + { + "macOS from test using regexp", + args{ + inputFromMac, + StackInfo{"", "panic(", 1, PackageRegexp, nil}, + }, + 12, + }, {"short", args{input, StackInfo{"", "panic(", 0, nil, nil}}, 6}, - {"short error stack", args{inputByError, StackInfo{"", "panic(", 0, PackageRegexp, nil}}, 4}, - {"short and nolimit", args{input, StackInfo{"", "", 0, nil, nil}}, nilAnchor}, - {"short and only LVL is 2", args{input, StackInfo{"", "", 2, nil, nil}}, 2}, + { + "short error stack", + args{ + inputByError, + StackInfo{"", "panic(", 0, PackageRegexp, nil}, + }, + 4, + }, + { + "short and nolimit", + args{input, StackInfo{"", "", 0, nil, nil}}, + nilAnchor, + }, + { + "short and only LVL is 2", + args{input, StackInfo{"", "", 2, nil, nil}}, + 2, + }, {"medium", args{input1, StackInfo{"", "panic(", 0, nil, nil}}, 10}, - {"from test using panic", args{inputFromTest, StackInfo{"", "panic(", 0, nil, nil}}, 8}, - {"from test", args{inputFromTest, StackInfo{"", "panic(", 0, PackageRegexp, nil}}, 14}, - {"macOS from test using panic", args{inputFromMac, StackInfo{"", "panic(", 0, nil, nil}}, 12}, + { + "from test using panic", + args{inputFromTest, StackInfo{"", "panic(", 0, nil, nil}}, + 8, + }, + { + "from test", + args{ + inputFromTest, + StackInfo{"", "panic(", 0, PackageRegexp, nil}, + }, + 14, + }, + { + "macOS from test using panic", + args{inputFromMac, StackInfo{"", "panic(", 0, nil, nil}}, + 12, + }, } for _, ttv := range tests { tt := ttv @@ -272,8 +338,7 @@ func TestCalcAnchor(t *testing.T) { t.Parallel() r := strings.NewReader(tt.input) anchor := calcAnchor(r, tt.StackInfo) - test.Requiref(t, tt.anchor == anchor, "not equal: %d != %d, got", - tt.anchor, anchor) + require.Equal(t, tt.anchor, anchor) }) } } @@ -290,15 +355,51 @@ func TestStackPrint_limit(t *testing.T) { output string } tests := []ttest{ - {"real test trace", args{inputFromTest, StackInfo{"", "", 8, nil, exludeRegexps}}, outputFromTest}, - {"only level 4", args{input1, StackInfo{"", "", 4, nil, nil}}, output1}, - {"short", args{input, StackInfo{"err2", "Returnw(", 0, nil, nil}}, output}, - {"medium", args{input1, StackInfo{"err2", "Returnw(", 0, nil, nil}}, output1}, - {"medium level 2", args{input1, StackInfo{"err2", "Returnw(", 2, nil, nil}}, output12}, - {"medium level 0", args{input1, StackInfo{"err2", "Returnw(", 0, nil, nil}}, output1}, - {"medium panic", args{input1, StackInfo{"", "panic(", 0, nil, nil}}, output1panic}, - {"long", args{input2, StackInfo{"err2", "Handle(", 0, nil, nil}}, output2}, - {"long lvl 2", args{input2, StackInfo{"err2", "Handle(", 3, nil, nil}}, output23}, + { + "real test trace", + args{inputFromTest, StackInfo{"", "", 8, nil, exludeRegexps}}, + outputFromTest, + }, + { + "only level 4", + args{input1, StackInfo{"", "", 4, nil, nil}}, + output1, + }, + { + "short", + args{input, StackInfo{"err2", "Returnw(", 0, nil, nil}}, + output, + }, + { + "medium", + args{input1, StackInfo{"err2", "Returnw(", 0, nil, nil}}, + output1, + }, + { + "medium level 2", + args{input1, StackInfo{"err2", "Returnw(", 2, nil, nil}}, + output12, + }, + { + "medium level 0", + args{input1, StackInfo{"err2", "Returnw(", 0, nil, nil}}, + output1, + }, + { + "medium panic", + args{input1, StackInfo{"", "panic(", 0, nil, nil}}, + output1panic, + }, + { + "long", + args{input2, StackInfo{"err2", "Handle(", 0, nil, nil}}, + output2, + }, + { + "long lvl 2", + args{input2, StackInfo{"err2", "Handle(", 3, nil, nil}}, + output23, + }, } for _, ttv := range tests { tt := ttv @@ -309,10 +410,10 @@ func TestStackPrint_limit(t *testing.T) { stackPrint(r, w, tt.StackInfo) ins := strings.Split(tt.input, "\n") outs := strings.Split(w.String(), "\n") - test.Requiref(t, len(ins) > len(outs), + require.Thatf(t, len(ins) > len(outs), "input length:%d should be greater:%d", len(ins), len(outs)) - a, b := tt.output, w.String() - test.Requiref(t, a == b, "a: %v != b: %v", a, b) + b, a := tt.output, w.String() + require.Equal(t, a, b) }) } } @@ -331,26 +432,56 @@ func TestFuncName(t *testing.T) { outFrame int } tests := []ttest{ - {"basic", args{input2, StackInfo{"", "Handle", 1, nil, nil}}, "err2.ReturnW", 214, 6}, - {"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}, + { + "basic", + args{input2, StackInfo{"", "Handle", 1, nil, nil}}, + "err2.ReturnW", + 214, + 6, + }, + { + "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 t.Run(tt.name, func(t *testing.T) { t.Parallel() r := strings.NewReader(tt.input) - name, ln, fr, ok := funcName(r, StackInfo{ + name, ln, fr, found := funcName(r, StackInfo{ PackageName: tt.PackageName, FuncName: tt.FuncName, Level: tt.Level, }) - test.Require(t, ok, "not found") - test.Requiref(t, tt.output == name, "not equal %v", name) - test.Requiref(t, ln == tt.outln, "ln must be equal %d == %d", ln, tt.outln) - test.RequireEqual(t, fr, tt.outFrame) + require.That(t, found) + require.Equal(t, tt.output, name) + require.Equal(t, ln, tt.outln) + require.Equal(t, fr, tt.outFrame) }) } } diff --git a/internal/handler/handler.go b/internal/handler/handler.go index 19dd9f1..235dc23 100644 --- a/internal/handler/handler.go +++ b/internal/handler/handler.go @@ -60,7 +60,13 @@ type Info struct { } const ( - wrapError = ": %w" + // Wrapping is best default because we can be in the situation where we + // have received meaningful sentinel error which we need to wrap, even + // automatically. If we'd have used %v (no wrapping) we would lose the + // sentinel error info and we couldn't use annotation for this error. + // However, we should think about this more later from performance point of + // view. + WrapError = ": %w" ) func PanicNoop(any) {} @@ -220,7 +226,7 @@ func (i *Info) safeErr() error { // wrapStr returns always wrap string that means we are using "%w" to chain // errors to be able to use errors.Is and errors.As functions form Go stl. func (i *Info) wrapStr() string { - return wrapError + return WrapError } // WorkToDo returns if there is something to process. This is offered for diff --git a/internal/handler/handler_test.go b/internal/handler/handler_test.go index 1214f2f..dd72499 100644 --- a/internal/handler/handler_test.go +++ b/internal/handler/handler_test.go @@ -7,7 +7,7 @@ import ( "testing" "github.com/lainio/err2/internal/handler" - "github.com/lainio/err2/internal/test" + "github.com/lainio/err2/internal/require" "github.com/lainio/err2/internal/x" ) @@ -121,11 +121,11 @@ func TestProcess(t *testing.T) { if handler.WorkToDo(tt.args.Any, tt.args.Err) { handler.Process(&tt.args.Info) - test.RequireEqual(t, panicHandlerCalled, tt.want.panicCalled) - test.RequireEqual(t, errorHandlerCalled, tt.want.errorCalled) - test.RequireEqual(t, nilHandlerCalled, tt.want.nilCalled) + require.Equal(t, panicHandlerCalled, tt.want.panicCalled) + require.Equal(t, errorHandlerCalled, tt.want.errorCalled) + require.Equal(t, nilHandlerCalled, tt.want.nilCalled) - test.RequireEqual(t, myErrVal.Error(), tt.want.errStr) + require.Equal(t, myErrVal.Error(), tt.want.errStr) } resetCalled() }) @@ -152,13 +152,13 @@ func TestPreProcess_debug(t *testing.T) { // and that's what error stack tracing is all about Handle() - test.Require(t, !panicHandlerCalled) - test.Require(t, !errorHandlerCalled) - test.Require(t, !nilHandlerCalled) + require.ThatNot(t, panicHandlerCalled) + require.ThatNot(t, errorHandlerCalled) + require.ThatNot(t, nilHandlerCalled) // See the name of this test function. Decamel it + error const want = "testing: t runner: error" - test.RequireEqual(t, myErrVal.Error(), want) + require.Equal(t, myErrVal.Error(), want) resetCalled() } @@ -243,11 +243,11 @@ func TestPreProcess(t *testing.T) { err = handler.PreProcess(&err, &tt.args.Info, tt.args.a) - test.RequireEqual(t, panicHandlerCalled, tt.want.panicCalled) - test.RequireEqual(t, errorHandlerCalled, tt.want.errorCalled) - test.RequireEqual(t, nilHandlerCalled, tt.want.nilCalled) + require.Equal(t, panicHandlerCalled, tt.want.panicCalled) + require.Equal(t, errorHandlerCalled, tt.want.errorCalled) + require.Equal(t, nilHandlerCalled, tt.want.nilCalled) - test.RequireEqual(t, err.Error(), tt.want.errStr) + require.Equal(t, err.Error(), tt.want.errStr) } resetCalled() }) diff --git a/internal/handler/handlers_test.go b/internal/handler/handlers_test.go index 2e5b700..cd76762 100644 --- a/internal/handler/handlers_test.go +++ b/internal/handler/handlers_test.go @@ -5,7 +5,7 @@ import ( "github.com/lainio/err2" "github.com/lainio/err2/internal/handler" - "github.com/lainio/err2/internal/test" + "github.com/lainio/err2/internal/require" ) func TestHandlers(t *testing.T) { @@ -20,16 +20,66 @@ func TestHandlers(t *testing.T) { 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}, + { + "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, @@ -43,17 +93,17 @@ func TestHandlers(t *testing.T) { t.Parallel() anys := tt.args.f - test.Require(t, anys != nil, "cannot be nil") + require.That(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") + require.That(t, fns != nil, "cannot be nil") + require.Equal(t, dis, tt.dis, "disabled wanted") errHandler := handler.Pipeline(fns) err := errHandler(err2.ErrNotFound) if err == nil { - test.Require(t, tt.want == nil) + require.That(t, tt.want == nil) } else { - test.RequireEqual(t, err.Error(), tt.want.Error()) + require.Equal(t, err.Error(), tt.want.Error()) } }) } diff --git a/internal/require/test.go b/internal/require/test.go new file mode 100644 index 0000000..c1a87fa --- /dev/null +++ b/internal/require/test.go @@ -0,0 +1,60 @@ +package require + +import ( + "fmt" + "testing" +) + +// ThatNot fails the test if the condition is true. +func ThatNot(tb testing.TB, condition bool, v ...interface{}) { + tb.Helper() + if condition { + tb.Fatal(v...) + } +} + +// That fails the test if the condition is false. +func That(tb testing.TB, condition bool, v ...interface{}) { + tb.Helper() + if !condition { + tb.Fatal(v...) + } +} + +// Thatf fails the test if the condition is false. +func Thatf(tb testing.TB, condition bool, format string, v ...interface{}) { + tb.Helper() + if !condition { + tb.Fatalf(format, v...) + } +} + +// Equal fails the test if the values aren't equal. +func Equal[T comparable](tb testing.TB, val, want T, a ...any) { + tb.Helper() + if want != val { + defMsg := fmt.Sprintf("got '%v', want '%v' ", val, want) + if len(a) == 0 { + tb.Fatal(defMsg) + } + format, ok := a[0].(string) + if ok { + tb.Fatalf(defMsg+format, a[1:]...) + } + } +} + +// NotEqual fails the test if the values aren't equal. +func NotEqual[T comparable](tb testing.TB, val, want T, a ...any) { + tb.Helper() + if want == val { + defMsg := fmt.Sprintf("got '%v', want != '%v' ", val, want) + if len(a) == 0 { + tb.Fatal(defMsg) + } + format, ok := a[0].(string) + if ok { + tb.Fatalf(defMsg+format, a[1:]...) + } + } +} diff --git a/internal/str/str_test.go b/internal/str/str_test.go index 45d02b1..9bf8160 100644 --- a/internal/str/str_test.go +++ b/internal/str/str_test.go @@ -3,8 +3,8 @@ package str_test import ( "testing" + "github.com/lainio/err2/internal/require" "github.com/lainio/err2/internal/str" - "github.com/lainio/err2/internal/test" ) const ( @@ -43,7 +43,7 @@ func TestCamel(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() got := str.DecamelRegexp(tt.args.s) - test.RequireEqual(t, got, tt.want) + require.Equal(t, got, tt.want) }) } } @@ -60,15 +60,39 @@ func TestDecamel(t *testing.T) { }{ {"simple", args{"CamelString"}, "camel string"}, {"underscore", args{"CamelString_error"}, "camel string error"}, - {"our contant", args{camelStr}, "benchmark recursion with old error if check and defer"}, + { + "our contant", + args{camelStr}, + "benchmark recursion with old error if check and defer", + }, {"number", args{"CamelString2Testing"}, "camel string2 testing"}, {"acronym", args{"ARMCamelString"}, "armcamel string"}, {"acronym at end", args{"archIsARM"}, "arch is arm"}, - {"simple method", args{"(*DIDAgent).AssertWallet"}, "didagent assert wallet"}, - {"package name and simple method", args{"ssi.(*DIDAgent).CreateWallet"}, "ssi: didagent create wallet"}, - {"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"}, + { + "simple method", + args{"(*DIDAgent).AssertWallet"}, + "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", + }, {"from spf13 cobra", args{"bot.glob..func5"}, "bot: glob: func5"}, } for _, ttv := range tests { @@ -76,7 +100,7 @@ func TestDecamel(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() got := str.Decamel(tt.args.s) - test.RequireEqual(t, got, tt.want) + require.Equal(t, got, tt.want) }) } } diff --git a/internal/test/test.go b/internal/test/test.go deleted file mode 100644 index b6a7894..0000000 --- a/internal/test/test.go +++ /dev/null @@ -1,37 +0,0 @@ -package test - -import ( - "fmt" - "testing" -) - -// Require fails the test if the condition is false. -func Require(tb testing.TB, condition bool, v ...interface{}) { - tb.Helper() - if !condition { - tb.Fatal(v...) - } -} - -// Requiref fails the test if the condition is false. -func Requiref(tb testing.TB, condition bool, format string, v ...interface{}) { - tb.Helper() - if !condition { - tb.Fatalf(format, v...) - } -} - -// RequireEqual fails the test if the values aren't equal -func RequireEqual[T comparable](tb testing.TB, val, want T, a ...any) { - tb.Helper() - if want != val { - defMsg := fmt.Sprintf("got '%v', want '%v' ", val, want) - if len(a) == 0 { - tb.Fatal(defMsg) - } - format, ok := a[0].(string) - if ok { - tb.Fatalf(defMsg+format, a[1:]...) - } - } -} diff --git a/internal/tracer/tracer.go b/internal/tracer/tracer.go index 1663c24..aa10296 100644 --- a/internal/tracer/tracer.go +++ b/internal/tracer/tracer.go @@ -33,7 +33,11 @@ func init() { Log.SetTracer(nil) flag.Var(&Log, "err2-log", "`stream` for logging: nil -> log pkg") - flag.Var(&Error, "err2-trace", "`stream` for error tracing: stderr, stdout") + flag.Var( + &Error, + "err2-trace", + "`stream` for error tracing: stderr, stdout", + ) flag.Var(&Panic, "err2-panic-trace", "`stream` for panic tracing") } diff --git a/internal/x/x_test.go b/internal/x/x_test.go index d79feaf..1140013 100644 --- a/internal/x/x_test.go +++ b/internal/x/x_test.go @@ -4,13 +4,46 @@ import ( "reflect" "testing" - "github.com/lainio/err2/internal/test" + "github.com/lainio/err2/internal/require" ) var ( - original = []int{2, 16, 128, 1024, 8192, 65536, 524288, 4194304, 16777216, 134217728} - lengths = []int{2, 16, 128, 1024, 8192, 65536, 524288, 4194304, 16777216, 134217728} - reverseLengths = []int{134217728, 16777216, 4194304, 524288, 65536, 8192, 1024, 128, 16, 2} + original = []int{ + 2, + 16, + 128, + 1024, + 8192, + 65536, + 524288, + 4194304, + 16777216, + 134217728, + } + lengths = []int{ + 2, + 16, + 128, + 1024, + 8192, + 65536, + 524288, + 4194304, + 16777216, + 134217728, + } + reverseLengths = []int{ + 134217728, + 16777216, + 4194304, + 524288, + 65536, + 8192, + 1024, + 128, + 16, + 2, + } ) func TestSwap(t *testing.T) { @@ -19,30 +52,30 @@ func TestSwap(t *testing.T) { var ( lhs, rhs = 1, 2 // these are ints as default ) - test.Require(t, lhs == 1) - test.Require(t, rhs == 2) + require.Equal(t, lhs, 1) + require.Equal(t, rhs, 2) Swap(&lhs, &rhs) - test.Require(t, lhs == 2) - test.Require(t, rhs == 1) + require.Equal(t, lhs, 2) + require.Equal(t, rhs, 1) } { var ( lhs, rhs float64 = 1, 2 ) - test.Require(t, lhs == 1) - test.Require(t, rhs == 2) + require.Equal(t, lhs, 1) + require.Equal(t, rhs, 2) Swap(&lhs, &rhs) - test.Require(t, lhs == 2) - test.Require(t, rhs == 1) + require.Equal(t, lhs, 2) + require.Equal(t, rhs, 1) } } func TestSReverse(t *testing.T) { t.Parallel() SReverse(lengths) - test.Require(t, reflect.DeepEqual(lengths, reverseLengths)) + require.That(t, reflect.DeepEqual(lengths, reverseLengths)) SReverse(lengths) // it's reverse now turn it to original - test.Require(t, reflect.DeepEqual(lengths, original)) + require.That(t, reflect.DeepEqual(lengths, original)) } func BenchmarkSSReverse(b *testing.B) { diff --git a/logo/logo.png b/logo/logo.png new file mode 100644 index 0000000..bca3efa Binary files /dev/null and b/logo/logo.png differ diff --git a/samples/README.md b/samples/README.md index b239a58..9241d5f 100644 --- a/samples/README.md +++ b/samples/README.md @@ -1,23 +1,34 @@ # Samples Please play with the samples by editing them and running the main files: -- `main.go` just a starter for different playgrounds -- `main-play.go` general playground based on CopyFile and recursion -- `main-db-sample.go` simulates DB transaction and money transfer +- `main.go` just a starter for different playgrounds (includes `asserter` tester) +- `main-play.go` general playground based on `CopyFile` and recursion +- `main-db-sample.go` simulates DB transaction with money transfer - `main-nil.go` samples and tests for logger and using `err2.Handle` for success Run a default playground `play` mode: ```go -go run ./... +go run . ``` -Or run the DB based version to maybe better understand how powerful the -automatic error string building is: +> [!TIP] +> Set a proper alias to play with samples: +> ```sh +> alias sa='go run .' +> ``` + +Run the DB based version to maybe better understand how powerful the automatic +error string building is: + ```go -go run ./... -mode db +sa -mode db +# or +go run . -mode db ``` You can print usage: ```go -go run ./... -h +sa -h +# or +go run . -h ``` diff --git a/samples/main-db-sample.go b/samples/main-db-sample.go index 9d663a7..4b0e2a3 100644 --- a/samples/main-db-sample.go +++ b/samples/main-db-sample.go @@ -14,19 +14,21 @@ func (db *Database) MoneyTransfer(from, to *Account, amount int) (err error) { tx := try.To1(db.BeginTransaction()) defer err2.Handle(&err, func(err error) error { if errRoll := tx.Rollback(); errRoll != nil { - // with go 1.20: err = fmt.Errorf("%w: ROLLBACK ERROR: %w", err, errRoll) - err = fmt.Errorf("%v: ROLLBACK ERROR: %w", err, errRoll) - } - return err - }) + // with go 1.20< we can wrap two errors as below: + // err = fmt.Errorf("%w: ROLLBACK ERROR: %w", err, errRoll) - try.To(from.ReserveBalance(tx, amount)) + // with go 1.18 (err2 minimum need) we cannot wrap two errors + // same time. + err = fmt.Errorf("%v\nROLLBACK ERROR: %w", err, errRoll) - defer err2.Handle(&err, func(err error) error { // optional, following sample's wording - err = fmt.Errorf("cannot %w", err) + // NOTE: that this is a good sample how difficult error handling + // can be. Now we select to wrap rollback error and use original + // as a main error message, no wrapping for it. + } return err }) + try.To(from.ReserveBalance(tx, amount)) try.To(from.Withdraw(tx, amount)) try.To(to.Deposit(tx, amount)) try.To(tx.Commit()) diff --git a/samples/main-nil.go b/samples/main-nil.go index c89a069..cc41b27 100644 --- a/samples/main-nil.go +++ b/samples/main-nil.go @@ -35,18 +35,26 @@ func doMainAll() { logger.Info("=== 1. preferred successful status output ===") doMain1() - logger.Info("=== 2. err2.Handle(NilThenerr, func(noerr)) and try.To successful status ===") + logger.Info( + "=== 2. err2.Handle(NilThenerr, func(noerr)) and try.To successful status ===", + ) doMain2() - logger.Info("=== 3. err2.Handle(NilThenerr, func(noerr)) and try.Out successful status ===") + logger.Info( + "=== 3. err2.Handle(NilThenerr, func(noerr)) and try.Out successful status ===", + ) doMain3() logger.Info("=== ERROR status versions ===") myErr = errAddNode logger.Info("=== 1. preferred successful status output ===") doMain1() - logger.Info("=== 2. err2.Handle(NilThenerr, func(noerr)) and try.To successful status ===") + logger.Info( + "=== 2. err2.Handle(NilThenerr, func(noerr)) and try.To successful status ===", + ) doMain2() - logger.Info("=== 3. err2.Handle(NilThenerr, func(noerr)) and try.Out successful status ===") + logger.Info( + "=== 3. err2.Handle(NilThenerr, func(noerr)) and try.Out successful status ===", + ) doMain3() } diff --git a/samples/main-play.go b/samples/main-play.go index 035ee3e..dd12042 100644 --- a/samples/main-play.go +++ b/samples/main-play.go @@ -9,9 +9,11 @@ package main import ( + "flag" "fmt" "io" "os" + "strconv" "github.com/lainio/err2" "github.com/lainio/err2/assert" @@ -90,22 +92,23 @@ func OrgCopyFile(src, dst string) (err error) { return nil } -func CallRecur(d int) (err error) { +func CallRecur(d int) (ret int, err error) { defer err2.Handle(&err) return doRecur(d) } -func doRecur(d int) (err error) { +func doRecur(d int) (ret int, err error) { d-- if d >= 0 { // Keep below to show how asserts work - assert.NotZero(d) + //assert.NotZero(d) // Comment out the above assert statement to simulate runtime-error - fmt.Println(10 / d) - return doRecur(d) + ret = 10 / d + fmt.Println(ret) + //return doRecur(d) } - return fmt.Errorf("root error") + return ret, fmt.Errorf("root error") } func doPlayMain() { @@ -136,7 +139,7 @@ func doPlayMain() { doDoMain() //try.To(doMain()) - println("___ happy ending ===") + fmt.Println("___ happy ending ===") } func doDoMain() { @@ -163,13 +166,20 @@ func doMain() (err error) { // Both source and destination don't exist //try.To(OrgCopyFile("/notfound/path/file.go", "/notfound/path/file.bak")) - // 2nd argument is empty - 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)) + // to play with real args: + try.To(CopyFile(flag.Arg(0), flag.Arg(1))) + + if len(flag.Args()) > 0 { + // Next fn demonstrates how error and panic traces work, comment out all + // above CopyFile calls to play with: + argument := try.To1(strconv.Atoi(flag.Arg(0))) + ret := try.To1(CallRecur(argument)) + fmt.Println("ret val:", ret) + } else { + // 2nd argument is empty to assert + try.To(OrgCopyFile("main.go", "")) + } - println("=== you cannot see this ===") + fmt.Println("=== you cannot see this ===") return nil } diff --git a/samples/main.go b/samples/main.go index afd933d..bb0ab8b 100644 --- a/samples/main.go +++ b/samples/main.go @@ -6,10 +6,16 @@ import ( "os" "github.com/lainio/err2" + "github.com/lainio/err2/assert" ) var ( - mode = flag.String("mode", "play", "runs the wanted playground: db, play, nil") + mode = flag.String( + "mode", + "play", + "runs the wanted playground: db, play, nil, assert,"+ + "\nassert-keep (= uses assert.Debug in GLS)", + ) isErr = flag.Bool("err", false, "tells if we want to have an error") ) @@ -36,7 +42,28 @@ func main() { doMain2() case "play": doPlayMain() + case "assert": + doAssertMainKeepGLSAsserter(false) + case "assert-keep": + doAssertMainKeepGLSAsserter(true) default: err2.Throwf("unknown (%v) playground given", *mode) } } + +func doAssertMainKeepGLSAsserter(keep bool) { + asserterPusher(keep) + asserterTester() +} + +func asserterTester() { + //defer assert.PushAsserter(assert.Development)() + assert.That(false) +} + +func asserterPusher(keep bool) { + pop := assert.PushAsserter(assert.Debug) + if !keep { // if not keep we free + pop() + } +} diff --git a/snippets/go.json b/snippets/go.json index 36b268b..b85c415 100644 --- a/snippets/go.json +++ b/snippets/go.json @@ -35,6 +35,11 @@ "body": "defer err2.Handle(&err, func(err error) error {\n\t$0\n})\n", "description": "Snippet for err2.Handle(&err, func(err error) error {})" }, + "defer assert.PushAsserter(assert.Plain)()": { + "prefix": "apu", + "body": "defer assert.PushAsserter(${1:assert.Plain})()\n", + "description": "Snippet for 1-liner pushing AND poping assert.Plain" + }, "defer assert.PushTester(t)()": { "prefix": "aspa", "body": "defer assert.PushTester(${1:t})()\n", diff --git a/try/copy_test.go b/try/copy_test.go index c11389c..7e50099 100644 --- a/try/copy_test.go +++ b/try/copy_test.go @@ -8,7 +8,7 @@ import ( "os" "testing" - "github.com/lainio/err2/internal/test" + "github.com/lainio/err2/internal/require" "github.com/lainio/err2/try" ) @@ -16,8 +16,8 @@ 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) + require.Thatf(b, err == nil, "error: %v", err) + require.That(b, all != nil) buf := make([]byte, 4) dst := bufio.NewWriter(bytes.NewBuffer(make([]byte, 0, len(all)))) @@ -29,8 +29,8 @@ func Benchmark_CopyBufferMy(b *testing.B) { func Benchmark_CopyBufferStd(b *testing.B) { all, err := os.ReadFile(dataFile) - test.Requiref(b, err == nil, "error: %v", err) - test.Require(b, all != nil) + require.Thatf(b, err == nil, "error: %v", err) + require.That(b, all != nil) buf := make([]byte, 4) dst := bufio.NewWriter(bytes.NewBuffer(make([]byte, 0, len(all)))) @@ -42,8 +42,8 @@ func Benchmark_CopyBufferStd(b *testing.B) { func Benchmark_CopyBufferOur(b *testing.B) { all, err := os.ReadFile(dataFile) - test.Requiref(b, err == nil, "error: %v", err) - test.Require(b, all != nil) + require.Thatf(b, err == nil, "error: %v", err) + require.That(b, all != nil) tmp := make([]byte, 4) dst := bufio.NewWriter(bytes.NewBuffer(make([]byte, 0, len(all)))) @@ -57,7 +57,11 @@ 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) { +func myCopyBuffer( + dst io.Writer, + src io.Reader, + buf []byte, +) (written int64, err error) { for { nr, er := src.Read(buf) if nr > 0 { diff --git a/try/out.go b/try/out.go index d73472c..d7677d3 100644 --- a/try/out.go +++ b/try/out.go @@ -43,6 +43,9 @@ type ( // // error sending response: UDP not listening func (o *Result) Logf(a ...any) *Result { + if o.Err == nil { + return o + } return o.logf(logfFrameLvl, a) } @@ -57,6 +60,9 @@ func (o *Result) Logf(a ...any) *Result { // // error sending response: UDP not listening func (o *Result1[T]) Logf(a ...any) *Result1[T] { + if o.Err == nil { + return o + } o.Result.logf(logfFrameLvl, a) return o } @@ -72,6 +78,9 @@ 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] { + if o.Err == nil { + return o + } o.Result.logf(logfFrameLvl, a) return o } @@ -97,6 +106,11 @@ func (o *Result) Handle(a ...any) *Result { if o.Err == nil { return o } + o.transportErr(a) + return o +} + +func (o *Result) transportErr(a []any) { noArguments := len(a) == 0 if noArguments { panic(o.Err) @@ -117,11 +131,11 @@ func (o *Result) Handle(a ...any) *Result { } } } - // someone of the handler functions might reset the error value. + + // some of the handler functions might reset the error value. if o.Err != nil { panic(o.Err) } - return o } // Handle allows you to add an error handler to [try.Out] handler chain. Handle @@ -264,17 +278,17 @@ func Out1[T any](v T, err error) *Result1[T] { // 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}}} + return &Result2[T, U]{ + Val2: v2, + Result1: Result1[T]{Val1: v1, Result: Result{Err: err}}, + } } func wrapStr() string { - return ": %w" + return handler.WrapError } func (o *Result) logf(lvl int, a []any) *Result { - if o.Err == nil { - return o - } s := o.Err.Error() if len(a) != 0 { f, isFormat := a[0].(string) diff --git a/try/out_test.go b/try/out_test.go index 454bfdf..60ea173 100644 --- a/try/out_test.go +++ b/try/out_test.go @@ -11,7 +11,7 @@ import ( "testing" "github.com/lainio/err2" - "github.com/lainio/err2/internal/test" + "github.com/lainio/err2/internal/require" "github.com/lainio/err2/try" ) @@ -102,8 +102,8 @@ func TestResult2_Logf(t *testing.T) { return v1 + v2, v2 } num1, num2 := countSomething("1", "bad") - test.RequireEqual(t, num2, 2) - test.RequireEqual(t, num1, 3) + require.Equal(t, num2, 2) + require.Equal(t, num1, 3) } func TestResult_Handle(t *testing.T) { @@ -121,10 +121,10 @@ func TestResult_Handle(t *testing.T) { return nil } err := callFn(1) - test.Requiref(t, err == nil, "no error when Out.Handle sets it nil") + require.That(t, err == nil, "no error when Out.Handle sets it nil") err = callFn(0) - test.Requiref(t, err != nil, "want error when Out.Handle sets it the same") + require.That(t, err != nil, "want error when Out.Handle sets it the same") } func ExampleResult1_Handle() { diff --git a/try/try.go b/try/try.go index 641853f..7ed1310 100644 --- a/try/try.go +++ b/try/try.go @@ -60,14 +60,31 @@ Or you might just want to change it later to error return: Please see the documentation and examples of [Result], [Result1], and [Result2] types and their methods. + +# try.T — Checking and Annotation + +The try package offers functions [T], [T1], [T2], and [T3] to allow fast +incremental code refactoring. For example, if you want to add an error check +specific annotation to the error check already done with [To1]: + + try.To1(io.Copy(w, r)) + +you can easily change it to [T1] and give extra message added to the error: + + try.T1(io.Copy(w, r))("error during stream copy") + +The T functions are offered mainly to allow faste feedback loop to play with the +error messages and see what works the best. */ package try import ( "errors" + "fmt" "io" "github.com/lainio/err2" + "github.com/lainio/err2/internal/handler" ) // To is a helper function to call functions which returns an error value and @@ -239,3 +256,52 @@ func IsNotRecoverable(err error) bool { func IsNotEnabled(err error) bool { return Is(err, err2.ErrNotEnabled) } + +// T is similar as [To] but it let's you to annotate a possible error at place. +// +// try.T(f.Close)("annotations") +func T(err error) func(fs string) { + return func(fs string) { + if err == nil { + return + } + panic(annotateErr(err, fs)) + } +} + +// T1 is similar as [To1] but it let's you to annotate a possible error at place. +// +// f := try.T1(os.Open("filename")("cannot open cfg file") +func T1[T any](v T, err error) func(fs string) T { + return func(fs string) T { + if err == nil { + return v + } + panic(annotateErr(err, fs)) + } +} + +// T2 is similar as [To2] but it let's you to annotate a possible error at place. +func T2[T, U any](v T, u U, err error) func(fs string) (T, U) { + return func(fs string) (T, U) { + if err == nil { + return v, u + } + panic(annotateErr(err, fs)) + } + +} + +func annotateErr(err error, fs string) error { + return fmt.Errorf(fs+handler.WrapError, err) +} + +// T3 is similar as [To3] but it let's you to annotate a possible error at place. +func T3[T, U, V any](v1 T, v2 U, v3 V, err error) func(fs string) (T, U, V) { + return func(fs string) (T, U, V) { + if err == nil { + return v1, v2, v3 + } + panic(annotateErr(err, fs)) + } +}