From 8422e93f02b1a5d1b06a189abc85db7dc437bf92 Mon Sep 17 00:00:00 2001 From: Theta-Dev Date: Sat, 16 Oct 2021 19:55:31 +0200 Subject: [PATCH 1/2] Add extended TryErr --- gotry_generate/main.go | 28 ++++---- try/std_types_test.go | 4 +- try/try.go | 149 ++++++++++++++++++++++++++++++++++------- try/try_test.go | 44 ++++++++---- 4 files changed, 172 insertions(+), 53 deletions(-) diff --git a/gotry_generate/main.go b/gotry_generate/main.go index 805aa1e..50eadd5 100644 --- a/gotry_generate/main.go +++ b/gotry_generate/main.go @@ -2,7 +2,6 @@ package main import ( "bufio" - "errors" "flag" "fmt" "io" @@ -26,7 +25,7 @@ type line struct { Types []string } -func parseDefinitions(defFileName string, outFileName string) (err error) { +func parseDefinitions(defFileName string, outFileName string) (err try.TryErr) { defer try.Annotate("error parsing definitions", &err) defFile := try.File(os.Open(defFileName)) @@ -78,12 +77,12 @@ func parseDefinitions(defFileName string, outFileName string) (err error) { return } -func parseLine(lineStr string, imports map[string]bool) (*line, error) { +func parseLine(lineStr string, imports map[string]bool) (*line, try.TryErr) { lineParts := strings.Split(lineStr, ";") lineData := line{} if len(lineParts) < 2 { - return nil, errors.New("missing type name") + return nil, try.NewErr("missing type name") } // Parse variable name @@ -92,7 +91,7 @@ func parseLine(lineStr string, imports map[string]bool) (*line, error) { lineData.Name = match[1] lineData.WithSlice = match[2] != "" } else { - return nil, errors.New("invalid variable name") + return nil, try.NewErr("invalid variable name") } // Parse type name(s) @@ -103,7 +102,7 @@ func parseLine(lineStr string, imports map[string]bool) (*line, error) { } } if len(lineData.Types) == 0 { - return nil, errors.New("invalid type name") + return nil, try.NewErr("invalid type name") } // Parse imports @@ -115,7 +114,7 @@ func parseLine(lineStr string, imports map[string]bool) (*line, error) { return &lineData, nil } -func copyLibrary(targetDir string, noTests, noStds bool) (caught error) { +func copyLibrary(targetDir string, noTests, noStds bool) (caught try.TryErr) { defer try.Return(&caught) filesToCopy := []string{"try.go"} @@ -145,12 +144,13 @@ func copyLibrary(targetDir string, noTests, noStds bool) (caught error) { } // Add go-generate command - gencmd := "\n//go:generate go run code.thetadev.de/ThetaDev/gotry/gotry_generate " + - "-def types.csv -o .\n" - f := try.File( - os.OpenFile(filepath.Join(targetDir, "try.go"), os.O_APPEND|os.O_WRONLY, 0o644)) - try.Empty(f.WriteString(gencmd)) - try.Check(f.Close()) + /* + gencmd := "\n//go:generate go run code.thetadev.de/ThetaDev/gotry/gotry_generate " + + "-def types.csv -o .\n" + f := try.File( + os.OpenFile(filepath.Join(targetDir, "try.go"), os.O_APPEND|os.O_WRONLY, 0o644)) + try.Empty(f.WriteString(gencmd)) + try.Check(f.Close())*/ // If not present, add definition file template definitionFile := filepath.Join(targetDir, "types.csv") @@ -170,7 +170,7 @@ func goFmt(goFile string) error { } func main() { - defer try.Catch(func(err error) { + defer try.Catch(func(err try.TryErr) { fmt.Println("Error: " + err.Error()) }) diff --git a/try/std_types_test.go b/try/std_types_test.go index 2441cb3..e818ecf 100644 --- a/try/std_types_test.go +++ b/try/std_types_test.go @@ -15,7 +15,7 @@ func TestStrHelper_noThrow(t *testing.T) { } func TestStrHelper_throw(t *testing.T) { - var err error + var err TryErr defer Return(&err) String(throw()) @@ -31,7 +31,7 @@ func TestStrStrHelper(t *testing.T) { } func Example_copyFile() { - copyFile := func(src, dst string) (err error) { + copyFile := func(src, dst string) (err TryErr) { defer Returnf(&err, "copy %s %s", src, dst) // These helpers are as fast as Check() calls diff --git a/try/try.go b/try/try.go index 326a789..6d0ab48 100644 --- a/try/try.go +++ b/try/try.go @@ -9,9 +9,113 @@ https://github.com/lainio/err2 import ( "errors" "fmt" + "regexp" + "runtime" "runtime/debug" + "strings" ) +// TryErr is an interface for an error with extended stack info. +type TryErr interface { + Error() string + Unwrap() error + CallStack() []call + CallStackString() string + PrintCallStack() + GetData() *tryErrData + Annotate(format string, args ...interface{}) +} + +// tryErrObj is an extended error struct. +type tryErrObj struct { + err error + callStack []call +} + +// tryErrData is exported error data. Can be converted to JSON. +type tryErrData struct { + Msg string `json:"msg"` + CallStack []call `json:"call_stack"` +} + +type call struct { + Function string `json:"fn"` + Line int `json:"l"` +} + +func newErr(err error) *tryErrObj { + terr := &tryErrObj{err: err, callStack: []call{}} + + pc, _, _, ok := runtime.Caller(0) + if !ok { + return terr + } + thisFnc := runtime.FuncForPC(pc) + if thisFnc == nil { + return terr + } + + rexp := regexp.MustCompile(`.+\/`) + thisPkg := rexp.FindString(thisFnc.Name()) + + for i := 1; ; i++ { + pc, f, l, ok := runtime.Caller(i) + + if !ok { + break + } + + fnc := runtime.FuncForPC(pc).Name() + + if strings.HasPrefix(fnc, thisPkg) && !strings.HasSuffix(f, "_test.go") { + continue + } + + terr.callStack = append(terr.callStack, call{Line: l, Function: fnc}) + } + + return terr +} + +func NewErr(msg string) *tryErrObj { + return newErr(errors.New(msg)) +} + +func (e *tryErrObj) Error() string { + return e.err.Error() +} + +func (e *tryErrObj) Unwrap() error { + return e.err +} + +func (e *tryErrObj) CallStack() []call { + return e.callStack +} + +func (e *tryErrObj) CallStackString() string { + res := fmt.Sprintf("ERROR: %s\n", e.err.Error()) + for _, c := range e.callStack { + res += fmt.Sprintf("%s:%d\n", c.Function, c.Line) + } + return res +} + +func (e *tryErrObj) PrintCallStack() { + fmt.Println(e.CallStackString()) +} + +func (e *tryErrObj) GetData() *tryErrData { + return &tryErrData{ + Msg: e.err.Error(), + CallStack: e.callStack, + } +} + +func (e *tryErrObj) Annotate(format string, args ...interface{}) { + e.err = fmt.Errorf(format+": %v", append(args, e)...) +} + // Empty is a helper method to handle errors of func() (string, error) functions. func Empty(_ interface{}, err error) { Check(err) @@ -30,7 +134,7 @@ func Any(args ...interface{}) []interface{} { // {return err} on happy path. func Check(err error) { if err != nil { - panic(err) + panic(newErr(err)) } } @@ -45,7 +149,7 @@ func check(args []interface{}) { if !ok { panic("wrong signature") } - panic(err) + panic(newErr(err)) } } @@ -53,7 +157,7 @@ func check(args []interface{}) { // functions returning errors them self. For those functions that doesn't // return errors there is a Catch function. Note! The handler function f is // called only when err != nil. -func Handle(err *error, f func()) { +func Handle(err *TryErr, f func()) { // This and Catch are similar but we need to call recover() here because // how it works with defer. We cannot refactor these to use same function. @@ -69,9 +173,9 @@ func Handle(err *error, f func()) { if *err != nil { f() } - case error: + case TryErr: // We or someone did transport this error thru panic. - *err = r.(error) + *err = r.(TryErr) f() default: panic(r) @@ -80,18 +184,19 @@ func Handle(err *error, f func()) { // Returnf wraps an error. It's similar to fmt.Errorf, but it's called only if // error != nil. -func Returnf(err *error, format string, args ...interface{}) { +func Returnf(err *TryErr, format string, args ...interface{}) { // This and Handle are similar but we need to call recover here because how // it works with defer. We cannot refactor these two to use same function. if r := recover(); r != nil { - e, ok := r.(error) + e, ok := r.(TryErr) if !ok { panic(r) // Not ours, carry on panicking } - *err = fmt.Errorf(format+": %v", append(args, e)...) + *err = e + e.Annotate(format, args...) } else if *err != nil { // if other handlers call recovery() we still.. - *err = fmt.Errorf(format+": %v", append(args, *err)...) + (*err).Annotate(format, args...) } } @@ -99,12 +204,12 @@ func Returnf(err *error, format string, args ...interface{}) { // Go's main function is a good example. Note! There can be only one deferred // Catch function per non error returning function. See Handle for more // information. -func Catch(f func(err error)) { +func Catch(f func(err TryErr)) { // This and Handle are similar but we need to call recover here because how // it works with defer. We cannot refactor these 2 to use same function. if r := recover(); r != nil { - e, ok := r.(error) + e, ok := r.(TryErr) if !ok { panic(r) } @@ -114,12 +219,12 @@ func Catch(f func(err error)) { // CatchAll is a helper function to catch and write handlers for all errors and // all panics thrown in the current go routine. -func CatchAll(errorHandler func(err error), panicHandler func(v interface{})) { +func CatchAll(errorHandler func(err TryErr), panicHandler func(v interface{})) { // This and Handle are similar but we need to call recover here because how // it works with defer. We cannot refactor these 2 to use same function. if r := recover(); r != nil { - e, ok := r.(error) + e, ok := r.(TryErr) if ok { errorHandler(e) } else { @@ -131,12 +236,12 @@ func CatchAll(errorHandler func(err error), panicHandler func(v interface{})) { // CatchTrace is a helper function to catch and handle all errors. It recovers a // panic as well and prints its call stack. This is preferred helper for go // workers on long running servers. -func CatchTrace(errorHandler func(err error)) { +func CatchTrace(errorHandler func(err TryErr)) { // This and Handle are similar but we need to call recover here because how // it works with defer. We cannot refactor these 2 to use same function. if r := recover(); r != nil { - e, ok := r.(error) + e, ok := r.(TryErr) if ok { errorHandler(e) } else { @@ -149,12 +254,12 @@ func CatchTrace(errorHandler func(err error)) { // Return is same as Handle but it's for functions which don't wrap or annotate // their errors. If you want to annotate errors see Annotate for more // information. -func Return(err *error) { +func Return(err *TryErr) { // This and Handle are similar but we need to call recover here because how // it works with defer. We cannot refactor these two to use same function. if r := recover(); r != nil { - e, ok := r.(error) + e, ok := r.(TryErr) if !ok { panic(r) // Not ours, carry on panicking } @@ -165,20 +270,18 @@ func Return(err *error) { // Annotate is for annotating an error. It's similar to Returnf but it takes only // two arguments: a prefix string and a pointer to error. It adds ": " between // the prefix and the error text automatically. -func Annotate(prefix string, err *error) { +func Annotate(prefix string, err *TryErr) { // This and Handle are similar but we need to call recover here because how // it works with defer. We cannot refactor these two to use same function. if r := recover(); r != nil { - e, ok := r.(error) + e, ok := r.(TryErr) if !ok { panic(r) // Not ours, carry on panicking } *err = e - format := prefix + ": " + e.Error() - *err = errors.New(format) + e.Annotate(prefix) } else if *err != nil { // if other handlers call recovery() we still.. - format := prefix + ": " + (*err).Error() - *err = errors.New(format) + (*err).Annotate(prefix) } } diff --git a/try/try_test.go b/try/try_test.go index 19ba93a..7716f49 100644 --- a/try/try_test.go +++ b/try/try_test.go @@ -8,6 +8,7 @@ https://github.com/lainio/err2 import ( "fmt" + "strings" "testing" ) @@ -64,7 +65,7 @@ func TestAny_noError(t *testing.T) { } func TestDefault_Error(t *testing.T) { - var err error + var err TryErr defer Return(&err) Any(throw()) @@ -73,7 +74,7 @@ func TestDefault_Error(t *testing.T) { } func TestAny_Error(t *testing.T) { - var err error + var err TryErr defer Handle(&err, func() {}) Any(throw()) @@ -82,7 +83,7 @@ func TestAny_Error(t *testing.T) { } func panickingHandle() { - var err error + var err TryErr defer Handle(&err, func() {}) Any(wrongSignature()) @@ -98,7 +99,7 @@ func TestPanickingCarryOn_Handle(t *testing.T) { } func panickingCatchAll() { - defer CatchAll(func(err error) {}, func(v interface{}) {}) + defer CatchAll(func(err TryErr) {}, func(v interface{}) {}) Any(wrongSignature()) } @@ -113,7 +114,7 @@ func TestPanickingCatchAll(t *testing.T) { } func panickingCatchTrace() { - defer CatchTrace(func(err error) {}) + defer CatchTrace(func(err TryErr) {}) Any(wrongSignature()) } @@ -128,7 +129,7 @@ func TestPanickingCatchTrace(t *testing.T) { } func panickingReturn() { - var err error + var err TryErr defer Return(&err) Any(wrongSignature()) @@ -144,7 +145,7 @@ func TestPanicking_Return(t *testing.T) { } func panickingCatch() { - defer Catch(func(err error) {}) + defer Catch(func(err TryErr) {}) Any(wrongSignature()) } @@ -159,7 +160,7 @@ func TestPanicking_Catch(t *testing.T) { } func TestCatch_Error(t *testing.T) { - defer Catch(func(err error) { + defer Catch(func(err TryErr) { // fmt.Printf("error and defer handling:%s\n", err) }) @@ -169,14 +170,14 @@ func TestCatch_Error(t *testing.T) { } func ExampleReturn() { - var err error + var err TryErr defer Return(&err) Any(noThrow()) // Output: } func ExampleAnnotate() { - annotated := func() (err error) { + annotated := func() (err TryErr) { defer Annotate("annotated", &err) Any(throw()) return err @@ -187,7 +188,7 @@ func ExampleAnnotate() { } func ExampleReturnf() { - annotated := func() (err error) { + annotated := func() (err TryErr) { defer Returnf(&err, "annotated: %s", "err2") Any(throw()) return err @@ -198,7 +199,7 @@ func ExampleReturnf() { } func ExampleAnnotate_deferStack() { - annotated := func() (err error) { + annotated := func() (err TryErr) { defer Annotate("annotated 2nd", &err) defer Annotate("annotated 1st", &err) Any(throw()) @@ -210,9 +211,9 @@ func ExampleAnnotate_deferStack() { } func ExampleHandle() { - doSomething := func(a, b int) (err error) { + doSomething := func(a, b int) (err TryErr) { defer Handle(&err, func() { - err = fmt.Errorf("error with (%d, %d): %w", a, b, err) + err.Annotate("error with (%d, %d)", a, b) }) Any(throw()) return err @@ -278,3 +279,18 @@ func BenchmarkRecursionWithErrorCheck_NotRelated(b *testing.B) { } } } + +func TestNewTryErr(t *testing.T) { + tryErr := NewErr("I f*cked up") + callStack := tryErr.CallStackString() + + if !strings.HasPrefix(callStack, "ERROR: I f*cked up\n") { + fmt.Println("Call stack does not have prefix.\n" + callStack) + t.Fail() + } + + if strings.Count(callStack, "\n") != 4 { + fmt.Println("Call stack is not 4 lines long.\n" + callStack) + t.Fail() + } +} From 5cc2a3dcbbb9f85b727bd236e471442dcd443276 Mon Sep 17 00:00:00 2001 From: Theta-Dev Date: Sat, 16 Oct 2021 20:37:14 +0200 Subject: [PATCH 2/2] improve annotations --- gotry_generate/main.go | 11 +--------- try/std_types_test.go | 2 +- try/try.go | 49 ++++++++++++++++-------------------------- try/try_test.go | 47 ++++++++++++++++++++++++++-------------- 4 files changed, 51 insertions(+), 58 deletions(-) diff --git a/gotry_generate/main.go b/gotry_generate/main.go index 50eadd5..571aaf5 100644 --- a/gotry_generate/main.go +++ b/gotry_generate/main.go @@ -26,7 +26,7 @@ type line struct { } func parseDefinitions(defFileName string, outFileName string) (err try.TryErr) { - defer try.Annotate("error parsing definitions", &err) + defer try.Annotate(&err, "error parsing definitions") defFile := try.File(os.Open(defFileName)) @@ -143,15 +143,6 @@ func copyLibrary(targetDir string, noTests, noStds bool) (caught try.TryErr) { try.Check(dest.Close()) } - // Add go-generate command - /* - gencmd := "\n//go:generate go run code.thetadev.de/ThetaDev/gotry/gotry_generate " + - "-def types.csv -o .\n" - f := try.File( - os.OpenFile(filepath.Join(targetDir, "try.go"), os.O_APPEND|os.O_WRONLY, 0o644)) - try.Empty(f.WriteString(gencmd)) - try.Check(f.Close())*/ - // If not present, add definition file template definitionFile := filepath.Join(targetDir, "types.csv") diff --git a/try/std_types_test.go b/try/std_types_test.go index e818ecf..fef7111 100644 --- a/try/std_types_test.go +++ b/try/std_types_test.go @@ -32,7 +32,7 @@ func TestStrStrHelper(t *testing.T) { func Example_copyFile() { copyFile := func(src, dst string) (err TryErr) { - defer Returnf(&err, "copy %s %s", src, dst) + defer Annotate(&err, fmt.Sprintf("copy %s %s", src, dst)) // These helpers are as fast as Check() calls r := File(os.Open(src)) diff --git a/try/try.go b/try/try.go index 6d0ab48..5cb12f7 100644 --- a/try/try.go +++ b/try/try.go @@ -23,19 +23,21 @@ type TryErr interface { CallStackString() string PrintCallStack() GetData() *tryErrData - Annotate(format string, args ...interface{}) + Annotate(msg string) } // tryErrObj is an extended error struct. type tryErrObj struct { - err error - callStack []call + err error + annotations []string + callStack []call } // tryErrData is exported error data. Can be converted to JSON. type tryErrData struct { - Msg string `json:"msg"` - CallStack []call `json:"call_stack"` + Msg string `json:"msg"` + Annotations []string `json:"annotations"` + CallStack []call `json:"call_stack"` } type call struct { @@ -82,7 +84,9 @@ func NewErr(msg string) *tryErrObj { } func (e *tryErrObj) Error() string { - return e.err.Error() + msgs := e.annotations + msgs = append(msgs, e.err.Error()) + return strings.Join(msgs, ": ") } func (e *tryErrObj) Unwrap() error { @@ -107,13 +111,14 @@ func (e *tryErrObj) PrintCallStack() { func (e *tryErrObj) GetData() *tryErrData { return &tryErrData{ - Msg: e.err.Error(), - CallStack: e.callStack, + Msg: e.err.Error(), + CallStack: e.callStack, + Annotations: e.annotations, } } -func (e *tryErrObj) Annotate(format string, args ...interface{}) { - e.err = fmt.Errorf(format+": %v", append(args, e)...) +func (e *tryErrObj) Annotate(msg string) { + e.annotations = append(e.annotations, msg) } // Empty is a helper method to handle errors of func() (string, error) functions. @@ -182,24 +187,6 @@ func Handle(err *TryErr, f func()) { } } -// Returnf wraps an error. It's similar to fmt.Errorf, but it's called only if -// error != nil. -func Returnf(err *TryErr, format string, args ...interface{}) { - // This and Handle are similar but we need to call recover here because how - // it works with defer. We cannot refactor these two to use same function. - - if r := recover(); r != nil { - e, ok := r.(TryErr) - if !ok { - panic(r) // Not ours, carry on panicking - } - *err = e - e.Annotate(format, args...) - } else if *err != nil { // if other handlers call recovery() we still.. - (*err).Annotate(format, args...) - } -} - // Catch is a convenient helper to those functions that doesn't return errors. // Go's main function is a good example. Note! There can be only one deferred // Catch function per non error returning function. See Handle for more @@ -270,7 +257,7 @@ func Return(err *TryErr) { // Annotate is for annotating an error. It's similar to Returnf but it takes only // two arguments: a prefix string and a pointer to error. It adds ": " between // the prefix and the error text automatically. -func Annotate(prefix string, err *TryErr) { +func Annotate(err *TryErr, msg string) { // This and Handle are similar but we need to call recover here because how // it works with defer. We cannot refactor these two to use same function. @@ -280,8 +267,8 @@ func Annotate(prefix string, err *TryErr) { panic(r) // Not ours, carry on panicking } *err = e - e.Annotate(prefix) + e.Annotate(msg) } else if *err != nil { // if other handlers call recovery() we still.. - (*err).Annotate(prefix) + (*err).Annotate(msg) } } diff --git a/try/try_test.go b/try/try_test.go index 7716f49..882ad1c 100644 --- a/try/try_test.go +++ b/try/try_test.go @@ -178,7 +178,7 @@ func ExampleReturn() { func ExampleAnnotate() { annotated := func() (err TryErr) { - defer Annotate("annotated", &err) + defer Annotate(&err, "annotated") Any(throw()) return err } @@ -187,33 +187,22 @@ func ExampleAnnotate() { // Output: annotated: this is an ERROR } -func ExampleReturnf() { - annotated := func() (err TryErr) { - defer Returnf(&err, "annotated: %s", "err2") - Any(throw()) - return err - } - err := annotated() - fmt.Printf("%v", err) - // Output: annotated: err2: this is an ERROR -} - func ExampleAnnotate_deferStack() { annotated := func() (err TryErr) { - defer Annotate("annotated 2nd", &err) - defer Annotate("annotated 1st", &err) + defer Annotate(&err, "annotated 2nd") + defer Annotate(&err, "annotated 1st") Any(throw()) return err } err := annotated() fmt.Printf("%v", err) - // Output: annotated 2nd: annotated 1st: this is an ERROR + // Output: annotated 1st: annotated 2nd: this is an ERROR } func ExampleHandle() { doSomething := func(a, b int) (err TryErr) { defer Handle(&err, func() { - err.Annotate("error with (%d, %d)", a, b) + err.Annotate(fmt.Sprintf("error with (%d, %d)", a, b)) }) Any(throw()) return err @@ -294,3 +283,29 @@ func TestNewTryErr(t *testing.T) { t.Fail() } } + +func TestGetData(t *testing.T) { + tryErr := NewErr("I f*cked up") + tryErr.Annotate("test1") + tryErr.Annotate("test2") + data := tryErr.GetData() + + if data.Msg != "I f*cked up" { + fmt.Println("wrong msg") + t.Fail() + } + + if data.Annotations[0] != "test1" { + fmt.Println("wrong annotation#0") + t.Fail() + } + if data.Annotations[1] != "test2" { + fmt.Println("wrong annotation#1") + t.Fail() + } + + if len(data.CallStack) != 3 { + fmt.Println("call stack length != 3, " + fmt.Sprint(len(data.CallStack))) + t.Fail() + } +}