Compare commits

...

3 commits

Author SHA1 Message Date
34a01c6279 Pass through try-compatible errors
All checks were successful
continuous-integration/drone/push Build is passing
2021-10-17 10:59:33 +02:00
b6ff49baee add ReturnStd
All checks were successful
continuous-integration/drone/push Build is passing
2021-10-16 23:32:06 +02:00
1cf7829749 Improved code generator
All checks were successful
continuous-integration/drone/push Build is passing
Better stack trace
Add try.CatchTrace, try.FromErr
2021-10-16 22:56:51 +02:00
4 changed files with 88 additions and 131 deletions

View file

@ -4,11 +4,9 @@ import (
"bufio"
"flag"
"fmt"
"io"
"os"
"os/exec"
"path"
"path/filepath"
"regexp"
"strings"
"text/template"
@ -26,10 +24,11 @@ type line struct {
UseExt bool
}
func parseDefinitions(defFileName, outFileName string, useExt bool) (err try.Err) {
func parseDefinitions(defFileName, outFileName, pkgName string) (err try.Err) {
defer try.Annotate(&err, "error parsing definitions")
defFile := try.File(os.Open(defFileName))
useExt := pkgName != "try"
fileScanner := bufio.NewScanner(defFile)
fileScanner.Split(bufio.ScanLines)
@ -66,10 +65,12 @@ func parseDefinitions(defFileName, outFileName string, useExt bool) (err try.Err
try.Check(tmpl.Execute(outFile, struct {
Definitions []line
Imports map[string]bool
PkgName string
UseExt bool
}{
Definitions: lines,
Imports: imports,
PkgName: pkgName,
UseExt: useExt,
}))
@ -118,64 +119,12 @@ func parseLine(lineStr string, imports map[string]bool) (*line, try.Err) {
return &lineData, nil
}
func copyLibrary(targetDir string, noTests, noStds bool) (caught try.Err) {
defer try.Return(&caught)
filesToCopy := []string{"try.go"}
if !noTests {
filesToCopy = append(filesToCopy, "try_test.go")
}
if !noStds {
filesToCopy = append(filesToCopy, "std_types.go")
if !noTests {
filesToCopy = append(filesToCopy, "std_types_test.go")
}
}
for _, fname := range filesToCopy {
f, e := gotry.TryFiles.Open("try/" + fname)
try.Check(e)
targetPath := filepath.Join(targetDir, fname)
dest := try.File(os.Create(targetPath))
try.Empty(io.Copy(dest, f))
fmt.Printf("Copied %s\n", fname)
try.Check(f.Close())
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")
if _, e := os.Stat(definitionFile); os.IsNotExist(e) {
f := try.File(os.Create(definitionFile))
try.Int(f.WriteString(
"# Add your type definitions here\n# Example:\n# DB;*gorm.DB;gorm.io/gorm\n"))
try.Check(f.Close())
}
return
}
func goFmt(goFile string) error {
return exec.Command("gofmt", "-s", "-w", goFile).Run()
}
func main() {
defer try.Catch(func(err try.Err) {
fmt.Println("Error: " + err.Error())
})
defer try.CatchTrace(1)
// Read flags
var definitionFile string
@ -186,24 +135,16 @@ func main() {
"Definition file")
flag.StringVar(&targetDir, "o", "try", "Target directory")
flag.StringVar(&typeFile, "of", "types", "Name of generated type definition file")
flagInit := flag.Bool("init", false, "Copy gotry package")
flagNoTests := flag.Bool("no-tests", false, "Dont copy tests")
flagNoStds := flag.Bool("no-std", false, "Dont copy std types")
flagUseExt := flag.Bool("ext", false, "Use gotry as an external dependency")
flag.Parse()
try.Check(os.MkdirAll(targetDir, 0o777))
// Copy library
if *flagInit {
try.Check(copyLibrary(targetDir, *flagNoTests, *flagNoStds))
}
// Generate types
if _, e := os.Stat(definitionFile); os.IsNotExist(e) {
fmt.Println("Definition file does not exist")
try.Check(try.NewErr("Definition file does not exist"))
} else {
try.Check(parseDefinitions(definitionFile,
path.Join(targetDir, typeFile+".go"), *flagUseExt))
try.Check(
parseDefinitions(definitionFile, path.Join(targetDir, typeFile+".go"),
path.Base(targetDir)))
}
}

View file

@ -12,7 +12,6 @@ import (
"os"
"regexp"
"runtime"
"runtime/debug"
"strings"
)
@ -42,12 +41,20 @@ type tryErrData struct {
}
type call struct {
File string `json:"file"`
Function string `json:"fn"`
Line int `json:"l"`
}
// newErr wraps a standard error into a try-compatible one with extended stack info.
func newErr(err error) *tryErr {
// FromErr wraps a standard error into a try-compatible one with extended stack info.
func FromErr(err error) Err {
// If the error is already try-compatible, return
//nolint:errorlint
cterr, ok := err.(Err)
if ok {
return cterr
}
terr := &tryErr{err: err, callStack: []call{}}
pc, _, _, ok := runtime.Caller(0)
@ -75,15 +82,15 @@ func newErr(err error) *tryErr {
continue
}
terr.callStack = append(terr.callStack, call{Line: l, Function: fnc})
terr.callStack = append(terr.callStack, call{File: f, Function: fnc, Line: l})
}
return terr
}
// NewErr creates a new try-compatible error with extended stack info.
func NewErr(msg string) *tryErr {
return newErr(errors.New(msg))
func NewErr(msg string) Err {
return FromErr(errors.New(msg))
}
// Error returns the standard go error message (initial message + annotations).
@ -106,9 +113,14 @@ func (e *tryErr) CallStack() []call {
// CallStackString outputs the call stack as a printable multiline string.
func (e *tryErr) CallStackString() string {
res := fmt.Sprintf("ERROR: %s\n", e.Error())
res := fmt.Sprintf("ERROR: %s\n", e.err.Error())
for _, msg := range e.annotations {
res += fmt.Sprintln(msg)
}
res += fmt.Sprintln("\nTraceback (most recent call first):")
for _, c := range e.callStack {
res += fmt.Sprintf("%s:%d\n", c.Function, c.Line)
res += fmt.Sprintf("%s:%d\n", c.File, c.Line)
res += fmt.Sprintf(" %s\n", c.Function)
}
return res
}
@ -150,7 +162,7 @@ func Any(args ...interface{}) []interface{} {
// {return err} on happy path.
func Check(err error) {
if err != nil {
panic(newErr(err))
panic(FromErr(err))
}
}
@ -165,7 +177,7 @@ func check(args []interface{}) {
if !ok {
panic("wrong signature")
}
panic(newErr(err))
panic(FromErr(err))
}
}
@ -174,9 +186,6 @@ func check(args []interface{}) {
// return errors there is a Catch function. Note! The handler function f is
// called only when err != nil.
func Handle(err *Err, 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.
// We put real panic objects back and keep only those which are
// carrying our errors. We must also call all of the handlers in defer
// stack.
@ -203,9 +212,6 @@ func Handle(err *Err, f func()) {
// Catch function per non error returning function. See Handle for more
// information.
func Catch(f func(err Err)) {
// 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.(Err)
if !ok {
@ -218,9 +224,6 @@ func Catch(f func(err Err)) {
// 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 Err), 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.(Err)
if ok {
@ -231,20 +234,18 @@ func CatchAll(errorHandler func(err Err), 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 Err)) {
// 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.
// CatchTrace catches all errors and prints their call stack.
// Setting the exit parameter to a value above 0 will make the program
// exit in case of an error.
func CatchTrace(exit int) {
if r := recover(); r != nil {
e, ok := r.(Err)
if ok {
errorHandler(e)
} else {
println(r)
debug.PrintStack()
if !ok {
panic(r)
}
e.PrintCallStack()
if exit > 0 {
os.Exit(exit)
}
}
}
@ -253,9 +254,6 @@ func CatchTrace(errorHandler func(err Err)) {
// their errors. If you want to annotate errors see Annotate for more
// information.
func Return(err *Err) {
// 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.(Err)
if !ok {
@ -265,13 +263,19 @@ func Return(err *Err) {
}
}
// 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(err *Err, 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.
// ReturnStd is like Return, but it returns the Go standard error type.
func ReturnStd(err *error) {
if r := recover(); r != nil {
e, ok := r.(Err)
if !ok {
panic(r) // Not ours, carry on panicking
}
*err = e.Unwrap()
}
}
// Annotate adds additional messages to the error.
func Annotate(err *Err, msg string) {
if r := recover(); r != nil {
e, ok := r.(Err)
if !ok {

View file

@ -114,21 +114,6 @@ func TestPanickingCatchAll(t *testing.T) {
panickingCatchAll()
}
func panickingCatchTrace() {
defer CatchTrace(func(err Err) {})
Any(wrongSignature())
}
func TestPanickingCatchTrace(t *testing.T) {
defer func() {
if recover() != nil {
t.Error("panics should NOT carry on when tracing")
}
}()
panickingCatchTrace()
}
func panickingReturn() {
var err Err
defer Return(&err)
@ -179,8 +164,9 @@ func TestNewTryErr(t *testing.T) {
t.Fail()
}
if strings.Count(callStack, "\n") != 4 {
fmt.Println("Call stack is not 4 lines long.\n" + callStack)
if strings.Count(callStack, "\n") != 9 {
fmt.Println("Call stack is not 9 lines long.\n" +
fmt.Sprint(strings.Count(callStack, "\n")))
t.Fail()
}
}
@ -213,13 +199,43 @@ func TestGetData(t *testing.T) {
func TestErrCompare(t *testing.T) {
err := errors.New("TestError")
tryErr := newErr(err)
tryErr := FromErr(err)
if !errors.Is(tryErr, err) {
t.Fail()
}
}
func TestReturnStd(t *testing.T) {
tf := func() (err error) {
defer ReturnStd(&err)
String(throw())
return
}
err := tf()
if err.Error() != "this is an ERROR" {
t.Fail()
}
}
func TestCheckTryErr(t *testing.T) {
testErr := NewErr("TestErr")
tf := func() (err Err) {
defer Return(&err)
Check(testErr)
return
}
err := tf()
if err != testErr {
t.Fail()
}
}
func ExampleReturn() {
var err Err
defer Return(&err)

View file

@ -1,7 +1,6 @@
package try
package {{.PkgName}}
// gotry auto-generated type definitions. DO NOT EDIT.
{{- if .Imports }}
import (
{{- if .UseExt }}
@ -11,9 +10,6 @@ import (
"{{$pkg}}"
{{- end}}
)
{{- else if .UseExt }}
import "code.thetadev.de/ThetaDev/gotry/try"
{{- end}}
{{range $d := .Definitions }}
// {{$d.Name}} is a helper method to handle errors of
// func() ({{range $i, $t := $d.Types}}{{$t}}, {{end}}error) functions.