diff --git a/CHANGELOG.md b/CHANGELOG.md index 33b471d..522198f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,42 @@ ### Version history +##### 0.9.41 +- Issue #18: **bug fixed**: noerr-handler had to be the last one of the err2 + handlers + +##### 0.9.40 +- Significant performance boost for: `defer err2.Handle/Catch()` + - **3x faster happy path than the previous version, which is now equal to + simplest `defer` function in the `err`-returning function** . (Please see + the `defer` benchmarks in the `err2_test.go` and run `make bench_reca`) + - the solution caused a change to API, where the core reason is Go's + optimization "bug". (We don't have confirmation yet.) +- Changed API for deferred error handling: `defer err2.Handle/Catch()` + - *Obsolete*: + ```go + defer err2.Handle(&err, func() {}) // <- relaying closure to access err val + ``` + - Current version: + ```go + defer err2.Handle(&err, func(err error) error { return err }) // not a closure + ``` + Because handler function is not relaying closures any more, it opens a new + opportunity to use and build general helper functions: `err2.Noop`, etc. + - Use auto-migration scripts especially for large code-bases. More information + can be found in the `scripts/` directory's [readme file](./scripts/README.md). + - Added a new (*experimental*) API: + ```go + defer err2.Handle(&err, func(noerr bool) { + assert.That(noerr) // noerr is always true!! + doSomething() + }) + ``` + This is experimental because we aren't sure if this is something we want to + have in the `err2` package. +- Bug fixes: `ResultX.Logf()` now works as it should +- More documentation + ##### 0.9.29 - New API for immediate error handling: `try out handle/catch err` `val := try.Out1strconv.Atois.Catch(10)` diff --git a/Makefile b/Makefile index d8e2af0..73da499 100644 --- a/Makefile +++ b/Makefile @@ -12,7 +12,8 @@ PKGS := $(PKG_ERR2) $(PKG_ASSERT) $(PKG_TRY) $(PKG_DEBUG) $(PKG_HANDLER) $(PKG_S SRCDIRS := $(shell go list -f '{{.Dir}}' $(PKGS)) GO ?= go -TEST_ARGS ?= +TEST_ARGS ?= -benchmem +# -"gcflags '-N -l'" both optimization & inlining disabled # GO ?= go1.20rc2 @@ -45,6 +46,9 @@ testv: test: $(GO) test $(TEST_ARGS) $(PKGS) +escape_err2: + $(GO) test -c -gcflags=-m=2 $(PKG_ERR2) 2>&1 | ag 'escape' + inline_err2: $(GO) test -c -gcflags=-m=2 $(PKG_ERR2) 2>&1 | ag 'inlin' @@ -78,6 +82,9 @@ bench_that: bench_copy: $(GO) test $(TEST_ARGS) -bench='Benchmark_CopyBuffer' $(PKG_TRY) +bench_rech: + $(GO) test $(TEST_ARGS) -bench='BenchmarkRecursionWithTryAnd_HeavyPtrPtr_Defer' $(PKG_ERR2) + bench_rece: $(GO) test $(TEST_ARGS) -bench='BenchmarkRecursionWithTryAnd_Empty_Defer' $(PKG_ERR2) diff --git a/README.md b/README.md index 14457c3..6be1103 100644 --- a/README.md +++ b/README.md @@ -44,8 +44,11 @@ func CopyFile(src, dst string) (err error) { - [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) - [Assertion Package for Unit Testing](#assertion-package-for-unit-testing) +- [Automatic Flags](#automatic-flags) + - [Support for Cobra Flags](#support-for-cobra-flags) - [Code Snippets](#code-snippets) - [Background](#background) - [Learnings by so far](#learnings-by-so-far) @@ -160,6 +163,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). + [Read the package documentation for more information](https://pkg.go.dev/github.com/lainio/err2). @@ -266,6 +273,8 @@ 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. +#### Asserters + 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. @@ -287,6 +296,10 @@ 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. +> 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 Following is example of use of the assert package: @@ -346,6 +359,90 @@ can be the same or different modules. execution, we will find it and can even move thru every step in the call stack.** +## Automatic Flags + +When you are using `err2` or `assert` packages, i.e., just importing them, you +have an option to automatically support for err2 configuration flags through +Go's standard `flag` package. See more information about err2 settings from +[Error Stack Tracing](#error-stack-tracing) and [Asserters](#asserters). + +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**. + +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 +run it again with: + +``` +your-app -err2-trace stderr +``` + +Now you get full error trace addition to the error message. Naturally, this +also works with assertions. You can configure their output with the flag +`asserter`: + +``` +your-app -asserter Debug +``` + +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. + +1. Add std flag package to imports in `cmd/root.go`: + + ```go + import ( + goflag "flag" + ... + ) + ``` + +1. Add the following to (usually) `cmd/root.go`'s `init` function's end: + + ```go + func init() { + ... + // NOTE! Very important. Adds support for std flag pkg users: glog, err2 + pflag.CommandLine.AddGoFlagSet(goflag.CommandLine) + } + ``` + +1. And finally modify your `PersistentPreRunE` in `cmd/root.go` to something + like: + + ```go + PersistentPreRunE: func(cmd *cobra.Command, args []string) (err error) { + defer err2.Handle(&err) + + // NOTE! Very important. Adds support for std flag pkg users: glog, err2 + goflag.Parse() + + try.To(goflag.Set("logtostderr", "true")) + handleViperFlags(cmd) // local helper with envs + glog.CopyStandardLogTo("ERROR") // for err2 + return nil + }, + ``` + +As a result you can have bunch of usable flags added to your CLI: + +``` +Flags: + --asserter asserter asserter: Plain, Prod, Dev, Debug (default Prod) + --err2-log stream stream for logging: nil -> log pkg (default nil) + --err2-panic-trace stream stream for panic tracing (default stderr) + --err2-trace stream stream for error tracing: stderr, stdout (default nil) + ... +``` + ## Code Snippets Most of the repetitive code blocks are offered as code snippets. They are in @@ -430,45 +527,13 @@ Please see the full version history from [CHANGELOG](./CHANGELOG.md). ### Latest Release -##### 0.9.41 -- Issue #18: **bug fixed**: noerr-handler had to be the last one of the err2 - handlers - -##### 0.9.40 -- Significant performance boost for: `defer err2.Handle/Catch()` - - **3x faster happy path than the previous version, which is now equal to - simplest `defer` function in the `err`-returning function** . (Please see - the `defer` benchmarks in the `err2_test.go` and run `make bench_reca`) - - the solution caused a change to API, where the core reason is Go's - optimization "bug". (We don't have confirmation yet.) -- Changed API for deferred error handling: `defer err2.Handle/Catch()` - - *Obsolete*: - ```go - defer err2.Handle(&err, func() {}) // <- relaying closure to access err val - ``` - - Current version: - ```go - defer err2.Handle(&err, func(err error) error { return err }) // not a closure - ``` - Because handler function is not relaying closures any more, it opens a new - opportunity to use and build general helper functions: `err2.Noop`, etc. - - Use auto-migration scripts especially for large code-bases. More information - can be found in the `scripts/` directory's [readme file](./scripts/README.md). - - Added a new (*experimental*) API: - ```go - defer err2.Handle(&err, func(noerr bool) { - assert.That(noerr) // noerr is always true!! - doSomething() - }) - ``` - This is experimental because we aren't sure if this is something we want to - have in the `err2` package. -- Bug fixes: `ResultX.Logf()` now works as it should -- More documentation +##### 0.9.5 +- `flag` package support to set `err2` and `assert` package configuration +- `err2.Catch` default mode is to log error +- cleanup and refactoring, new tests and benchmarks ### Upcoming releases -##### 0.9.5 -- Idea: Go's standard lib's flag pkg integration (similar to `glog`) -- Continue removing unused parts from `assert` pkg -- More documentation, repairing for some sort of marketing +##### 0.9.6 +- Continue removing unused parts and repairing for 1.0.0 release. +- Always more and better documentation diff --git a/assert/assert.go b/assert/assert.go index 58772d8..0e90dc8 100644 --- a/assert/assert.go +++ b/assert/assert.go @@ -1,6 +1,7 @@ package assert import ( + "flag" "fmt" "os" "reflect" @@ -16,13 +17,17 @@ import ( type defInd = uint32 const ( + // Plain converts asserts just plain error messages without extra + // information. + Plain defInd = 0 + iota + // Production (pkg default) is the best asserter for most uses. The // assertion violations are treated as Go error values. And only a // pragmatic caller info is automatically included into the error message // like file name, line number, and caller function, all in one line: // // copy file: main.go:37: CopyFile(): assertion violation: string shouldn't be empty - Production defInd = 0 + iota + Production // Development is the best asserter for most development uses. The // assertion violations are treated as Go error values. And a formatted @@ -76,8 +81,12 @@ const ( Debug ) +type flagAsserter struct{} + // Deprecated: use e.g. assert.That(), only default asserter is used. var ( + PL = AsserterToError + // P is a production Asserter that sets panic objects to errors which // allows err2 handlers to catch them. P = AsserterToError | AsserterCallerInfo @@ -97,7 +106,13 @@ var ( // These two are our indexing system for default asserter. Note also the // mutex blew. All of this is done to keep client package race detector // cool. - defAsserter = []Asserter{P, B, T, TF, D} + // Plain + // Production + // Development + // Test + // TestFull + // Debug + defAsserter = []Asserter{PL, P, B, T, TF, D} def defInd // mu is package lvl Mutex that is used to cool down race detector of @@ -107,10 +122,13 @@ var ( // indexing system we are using for default asserter (above) we are pretty // much theard safe. mu sync.Mutex + + asserterFlag flagAsserter ) func init() { SetDefault(Production) + flag.Var(&asserterFlag, "asserter", "`asserter`: Plain, Prod, Dev, Debug") } type ( @@ -125,6 +143,7 @@ var ( const ( assertionMsg = "assertion violation" + gotWantFmt = ": got '%v', want '%v'" ) // PushTester sets the current testing context for default asserter. This must @@ -338,7 +357,7 @@ func NotEqual[T comparable](val, want T, a ...any) { // Asserter) with the given message. func Equal[T comparable](val, want T, a ...any) { if want != val { - defMsg := fmt.Sprintf(assertionMsg+": got '%v', want '%v'", val, want) + defMsg := fmt.Sprintf(assertionMsg+gotWantFmt, val, want) Default().reportAssertionFault(defMsg, a...) } } @@ -347,7 +366,7 @@ func Equal[T comparable](val, want T, a ...any) { // panics/errors (current Asserter) with the given message. func DeepEqual(val, want any, a ...any) { if !reflect.DeepEqual(val, want) { - defMsg := fmt.Sprintf(assertionMsg+": got '%v', want '%v'", val, want) + defMsg := fmt.Sprintf(assertionMsg+gotWantFmt, val, want) Default().reportAssertionFault(defMsg, a...) } } @@ -372,7 +391,7 @@ func Len(obj string, length int, a ...any) { l := len(obj) if l != length { - defMsg := fmt.Sprintf(assertionMsg+": got '%d', want '%d'", l, length) + defMsg := fmt.Sprintf(assertionMsg+gotWantFmt, l, length) Default().reportAssertionFault(defMsg, a...) } } @@ -385,7 +404,7 @@ func SLen[S ~[]T, T any](obj S, length int, a ...any) { l := len(obj) if l != length { - defMsg := fmt.Sprintf(assertionMsg+": got '%d', want '%d'", l, length) + defMsg := fmt.Sprintf(assertionMsg+gotWantFmt, l, length) Default().reportAssertionFault(defMsg, a...) } } @@ -398,7 +417,7 @@ 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+": got '%d', want '%d'", l, length) + defMsg := fmt.Sprintf(assertionMsg+gotWantFmt, l, length) Default().reportAssertionFault(defMsg, a...) } } @@ -536,6 +555,37 @@ func SetDefault(i defInd) Asserter { return defAsserter[i] } +// mapDefInd runtime asserters, that's why test asserts are removed for now. +var mapDefInd = map[string]defInd{ + "Plain": Plain, + "Prod": Production, + "Dev": Development, + //"Test": Test, + //"TestFull": TestFull, + "Debug": Debug, +} + +var mapDefIndToString = map[defInd]string{ + Plain: "Plain", + Production: "Prod", + Development: "Dev", + Test: "Test", + TestFull: "TestFull", + Debug: "Debug", +} + +func AsserterString() string { + return mapDefIndToString[def] +} + +func newDefInd(v string) defInd { + ind, found := mapDefInd[v] + if !found { + return Plain + } + return ind +} + func combineArgs(format string, a []any) []any { args := make([]any, 1, len(a)+1) args[0] = format @@ -546,10 +596,10 @@ func combineArgs(format string, a []any) []any { func goid() int { var buf [64]byte runtime.Stack(buf[:], false) - return myByteToInt(buf[10:]) + return asciiWordToInt(buf[10:]) } -func myByteToInt(b []byte) int { +func asciiWordToInt(b []byte) int { n := 0 for _, ch := range b { if ch == ' ' { @@ -567,3 +617,19 @@ func myByteToInt(b []byte) int { type Number interface { constraints.Float | constraints.Integer } + +// String is part of the flag interfaces +func (f *flagAsserter) String() string { + return AsserterString() +} + +// Get is part of the flag interfaces, getter. +func (f *flagAsserter) Get() any { + return mapDefIndToString[def] +} + +// Set is part of the flag.Value interface. +func (*flagAsserter) Set(value string) error { + SetDefault(newDefInd(value)) + return nil +} diff --git a/assert/assert_test.go b/assert/assert_test.go index 3bc16d4..a1b0624 100644 --- a/assert/assert_test.go +++ b/assert/assert_test.go @@ -152,15 +152,6 @@ func ExampleZero() { // Output: sample: assert_test.go:146: ExampleZero.func1(): assertion violation: got '1', want (== '0') } -// ifPanicZero in needed that we have argument here! It's like a macro for -// benchmarking. The others aren't needed below. TODO: refactor unneeded -// helpers. -func ifPanicZero(i int) { - if i == 0 { - panic("i == 0") - } -} - func assertZero(i int) { assert.Zero(i) } @@ -218,6 +209,12 @@ func BenchmarkEqual(b *testing.B) { } func BenchmarkAsserter_TrueIfVersion(b *testing.B) { + ifPanicZero := func(i int) { + if i == 0 { + panic("i == 0") + } + } + for n := 0; n < b.N; n++ { ifPanicZero(4) } diff --git a/assert/doc.go b/assert/doc.go index 881870e..d25520a 100644 --- a/assert/doc.go +++ b/assert/doc.go @@ -52,6 +52,16 @@ error messages and diagnostic they need. Please see the code examples for more information. +# Flag Package Support + +The assert package supports Go's flags. All you need to do is to call flag.Parse. +And the following flags are supported (="default-value"): + + -asserter="Prod" + A name of the asserter Plain, Prod, Dev, Debug (see constants) + +And assert package's configuration flags are inserted. + Note. assert.That's performance is equal to the if-statement. Most of the generics-based versions are almost as fast. If your algorithm is performance-critical please run `make bench` in the err2 repo and decide case by diff --git a/assert/goid_test.go b/assert/goid_test.go index 2e53415..9aa154f 100644 --- a/assert/goid_test.go +++ b/assert/goid_test.go @@ -1,6 +1,8 @@ package assert import ( + "bytes" + "fmt" "testing" ) @@ -9,7 +11,18 @@ func TestGoid(t *testing.T) { stackBytes := []byte(`goroutine 518 [running]: `) - id := myByteToInt(stackBytes[10:]) + id := asciiWordToInt(stackBytes[10:]) + if id != 518 { + t.Fail() + } +} + +func Test_oldGoid(t *testing.T) { + t.Parallel() + stackBytes := []byte(`goroutine 518 [running]: +`) + + id := oldGoid(stackBytes) if id != 518 { t.Fail() } @@ -29,6 +42,23 @@ func BenchmarkGoid_MyByteToInt(b *testing.B) { `) for n := 0; n < b.N; n++ { - _ = myByteToInt(stackBytes[10:]) + _ = asciiWordToInt(stackBytes[10:]) + } +} + +func oldGoid(buf []byte) (id int) { + _, err := fmt.Fscanf(bytes.NewReader(buf), "goroutine %d", &id) + if err != nil { + panic("cannot get goroutine id: " + err.Error()) + } + return id +} + +func BenchmarkGoid_Old(b *testing.B) { + stackBytes := []byte(`goroutine 518 [running]: +`) + + for n := 0; n < b.N; n++ { + _ = oldGoid(stackBytes) } } diff --git a/doc.go b/doc.go index 8f6851a..98bf61d 100644 --- a/doc.go +++ b/doc.go @@ -89,6 +89,18 @@ tracer API: err2.SetLogTracer(nil) // the default is nil where std log pkg is used. +# Flag Package Support + +The err2 package supports Go's flags. All you need to do is to call flag.Parse. +And the following flags are supported (="default-value"): + + -err2-log="nil" + A name of the stream currently supported stderr, stdout or nil + -err2-panic-trace="stderr" + A name of the stream currently supported stderr, stdout or nil + -err2-trace="nil" + A name of the stream currently supported stderr, stdout or nil + # Error handling Package err2 relies on declarative control structures to achieve error and panic diff --git a/err2.go b/err2.go index c9cf1db..2e9c53e 100644 --- a/err2.go +++ b/err2.go @@ -103,12 +103,13 @@ func Handle(err *error, a ...any) { // // The deferred Catch is very convenient, because it makes your current // goroutine panic and error-safe, one line only! You can fine tune its -// behavior with functions like err2.SetErrorTrace, assert.SetDefault, etc. -// Start with the defaults. +// behavior with functions like err2.SetErrorTrace, assert.SetDefault, and +// logging settings. Start with the defaults and simplest version of Catch: // // defer err2.Catch() // -// Catch support logging as well: +// In default the above writes errors to logs and panic traces to stderr. +// Naturally, you can annotate logging: // // defer err2.Catch("WARNING: caught errors: %s", name) // @@ -147,7 +148,6 @@ func Catch(a ...any) { err = handler.PreProcess(&err, &handler.Info{ CallerName: "Catch", Any: r, - NilHandler: handler.NilNoop, }, a...) doTrace(err) } diff --git a/err2_test.go b/err2_test.go index b1540da..38a5c33 100644 --- a/err2_test.go +++ b/err2_test.go @@ -4,6 +4,7 @@ import ( "fmt" "io" "os" + "runtime" "testing" "github.com/lainio/err2" @@ -898,6 +899,46 @@ func BenchmarkRecursionWithTryAnd_Empty_Defer(b *testing.B) { } } +func doWork(ePtr *error, r any) { + switch v := r.(type) { + case nil: + return + case runtime.Error: + *ePtr = fmt.Errorf("%v: %w", *ePtr, v) + case error: + *ePtr = fmt.Errorf("%v: %w", *ePtr, v) + default: + // panicing + } +} + +func BenchmarkRecursionWithTryAnd_HeavyPtrPtr_Defer(b *testing.B) { + var recursion func(a int) (r int, err error) + recursion = func(a int) (r int, err error) { + defer func(ePtr *error) { + r := recover() + nothingToDo := r == nil && (ePtr == nil || *ePtr == nil) + if nothingToDo { + return + } + doWork(ePtr, r) + }(&err) + + if a == 0 { + return 0, nil + } + s := try.To1(noThrow()) + _ = s + r = try.To1(recursion(a - 1)) + r += a + return r, nil + } + + for n := 0; n < b.N; n++ { + _, _ = recursion(100) + } +} + func BenchmarkRecursionWithTryAndDefer(b *testing.B) { var recursion func(a int) (r int, err error) recursion = func(a int) (r int, err error) { diff --git a/internal/formatter/formatter.go b/internal/formatter/formatter.go index 203754e..35f3897 100644 --- a/internal/formatter/formatter.go +++ b/internal/formatter/formatter.go @@ -16,8 +16,8 @@ func SetFormatter(fmter format.Interface) { } func Formatter() format.Interface { - fmter, ok := formatter.Load().(format.Interface) - if ok { + fmter, isInterface := formatter.Load().(format.Interface) + if isInterface { return fmter } return nil diff --git a/internal/handler/handler.go b/internal/handler/handler.go index a97df33..8a3c295 100644 --- a/internal/handler/handler.go +++ b/internal/handler/handler.go @@ -267,7 +267,7 @@ func PreProcess(errPtr *error, info *Info, a ...any) error { if len(a) > 0 { subProcess(info, a...) } else { - fnName := "Handle" + fnName := "Handle" // default if info.CallerName != "" { fnName = info.CallerName } @@ -277,9 +277,9 @@ func PreProcess(errPtr *error, info *Info, a ...any) error { Level: lvl, }) if ok { - fmter := fmtstore.Formatter() - if fmter != nil { // TODO: check the init order! - info.Format = fmter.Format(funcName) + setFmter := fmtstore.Formatter() + if setFmter != nil { + info.Format = setFmter.Format(funcName) } else { info.Format = str.Decamel(funcName) } @@ -292,8 +292,7 @@ func PreProcess(errPtr *error, info *Info, a ...any) error { Process(info) - logCatchCallMode := defCatchCallMode && firstArgIsString(a...) - if curErr := info.safeErr(); logCatchCallMode && curErr != nil { + if curErr := info.safeErr(); defCatchCallMode && curErr != nil { _, _, frame, ok := debug.FuncName(debug.StackInfo{ PackageName: "", FuncName: "Catch", @@ -308,7 +307,8 @@ func PreProcess(errPtr *error, info *Info, a ...any) error { return err } -func firstArgIsString(a ...any) bool { +// firstArgIsString not used any more. +func _(a ...any) bool { if len(a) > 0 { _, isStr := a[0].(string) return isStr diff --git a/internal/str/str.go b/internal/str/str.go index 6950aaa..183b904 100644 --- a/internal/str/str.go +++ b/internal/str/str.go @@ -11,14 +11,17 @@ import ( ) var ( - re = regexp.MustCompile(`([A-Z]+)`) + uncamel = regexp.MustCompile(`([A-Z]+)`) + clean = regexp.MustCompile(`[^\w]`) ) -// CamelRegexp return the given string as space delimeted. Note! it's slow. Use +// DecamelRegexp return the given string as space delimeted. Note! it's slow. Use // Decamel instead. -func CamelRegexp(str string) string { - str = re.ReplaceAllString(str, ` $1`) +func DecamelRegexp(str string) string { + str = clean.ReplaceAllString(str, " ") + str = uncamel.ReplaceAllString(str, ` $1`) str = strings.Trim(str, " ") + str = strings.ToLower(str) return str } diff --git a/internal/str/str_test.go b/internal/str/str_test.go index dbb13c6..7e16b35 100644 --- a/internal/str/str_test.go +++ b/internal/str/str_test.go @@ -11,9 +11,9 @@ const ( camelStr = "BenchmarkRecursionWithOldErrorIfCheckAnd_Defer" ) -func BenchmarkCamelRegexp(b *testing.B) { +func BenchmarkDecamelRegexp(b *testing.B) { for n := 0; n < b.N; n++ { - _ = str.CamelRegexp(camelStr) + _ = str.DecamelRegexp(camelStr) } } @@ -23,6 +23,31 @@ func BenchmarkDecamel(b *testing.B) { } } +func TestCamel(t *testing.T) { + t.Parallel() + type args struct { + s string + } + tests := []struct { + name string + args args + want string + }{ + {"simple", args{"CamelString"}, "camel string"}, + {"number", args{"CamelString2Testing"}, "camel string2 testing"}, + {"acronym", args{"ARMCamelString"}, "armcamel string"}, + {"acronym at end", args{"archIsARM"}, "arch is arm"}, + } + for _, ttv := range tests { + tt := ttv + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got := str.DecamelRegexp(tt.args.s) + test.RequireEqual(t, got, tt.want) + }) + } +} + func TestDecamel(t *testing.T) { t.Parallel() type args struct { diff --git a/internal/tracer/tracer.go b/internal/tracer/tracer.go index 0bfe1e5..1663c24 100644 --- a/internal/tracer/tracer.go +++ b/internal/tracer/tracer.go @@ -2,9 +2,12 @@ package tracer import ( + "flag" "io" "os" "sync/atomic" + + "github.com/lainio/err2/internal/x" ) type value struct { @@ -28,12 +31,45 @@ func init() { // nil is a good default for try.Out().Logf() because then we use std log. 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(&Panic, "err2-panic-trace", "`stream` for panic tracing") } func (v *value) Tracer() io.Writer { - return v.Load().(writer).w + if w, ok := v.Load().(writer); ok { + return w.w + } + return nil } func (v *value) SetTracer(w io.Writer) { v.Store(writer{w: w}) } + +// String is part of the flag interfaces +func (v *value) String() string { + if v == nil { + return "null" + } + return x.Whom(v.Tracer() != nil, "stderr", "nil") +} + +// Get is part of the flag interfaces, getter. +func (v *value) Get() any { + return v.Tracer() +} + +// Set is part of the flag.Value interface. +func (v *value) Set(value string) error { + switch value { + case "stderr": + v.SetTracer(os.Stderr) + case "stdout": + v.SetTracer(os.Stdout) + case "nil": + v.SetTracer(nil) + } + return nil +} diff --git a/samples/main-db-sample.go b/samples/main-db-sample.go index 07b5abc..ed713e6 100644 --- a/samples/main-db-sample.go +++ b/samples/main-db-sample.go @@ -43,14 +43,14 @@ func doDBMain() { db, from, to := new(Database), new(Account), new(Account) - // --- TODO: play with these lines to simulate different errors: + // --- play with these lines to simulate different errors: db.errRoll = errRollback //db.err = errBegin // tx fails from.balance = 1100 // no enough funds from.errWithdraw = errWithdraw // withdraw error to.errDeposit = errDeposit // deposit error amount := 100 // no enough funds - // --- TODO: simulation variables end + // --- simulation variables end try.To(db.MoneyTransfer(from, to, amount)) diff --git a/scripts/migrate.sh b/scripts/migrate.sh index c71f8cd..15ae2a5 100755 --- a/scripts/migrate.sh +++ b/scripts/migrate.sh @@ -128,7 +128,7 @@ echo "=================================" echo if [[ $bads != "" ]]; then - echo "====== TODO Summary ====" >&2 + echo "====== Summary ====" >&2 echo "Please check the following files before commit:" >&2 echo "" >&2 echo "$bads" | tr " " "\n" >&2 diff --git a/scripts/release-readme.md b/scripts/release-readme.md index 7d9e1a0..837ae04 100644 --- a/scripts/release-readme.md +++ b/scripts/release-readme.md @@ -7,5 +7,5 @@ Run the `release.sh` script: ``` Note! The version string format is 'v0.8.0'. Don't forget the v at the -beginning. TODO: update the script. +beginning. diff --git a/scripts/release.sh b/scripts/release.sh index 446b766..7836f64 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -15,6 +15,22 @@ check_prerequisites() { echo "ERROR: give version number, e.g. v0.8.2" exit 1 fi + + if [[ $1 =~ ^v[0-9]{1,2}\.[0-9]{1,2}\.[0-9]{1,3}$ ]]; then + # echo "version string format is CORRECT" + local last_tag=`git tag -l | sort | tail -1` + if [[ "$last_tag" == "$1" ]]; then + echo "tag $last_tag already exists" + exit 1 + fi + if [[ "$last_tag" > "$1" ]]; then + echo "greater tag $last_tag already exists" + exit 1 + fi + else + echo "version string format ins't correct" + exit 1 + fi } check_prerequisites $1 @@ -32,7 +48,7 @@ if [[ -z "$(git status --porcelain)" ]]; then git tag -a "$version" -m "v. $version" git push origin "$cur_branch" --tags GOPROXY=proxy.golang.org go list -m github.com/lainio/err2@"$version" - goreleaser release + goreleaser release --clean else echo 'ERROR: working dir is not clean' fi