diff options
-rw-r--r-- | builds/roadrunner-1.8.4-darwin-amd64.zip | bin | 0 -> 9304639 bytes | |||
-rw-r--r-- | builds/roadrunner-1.8.4-freebsd-amd64.zip | bin | 0 -> 5079233 bytes | |||
-rw-r--r-- | builds/roadrunner-1.8.4-linux-amd64.tar.gz | bin | 0 -> 5103405 bytes | |||
-rw-r--r-- | builds/roadrunner-1.8.4-unknown-musl-amd64.zip | bin | 0 -> 5127095 bytes | |||
-rw-r--r-- | builds/roadrunner-1.8.4-windows-amd64.zip | bin | 0 -> 5106658 bytes | |||
-rwxr-xr-x | errors/debug_cap.go | 10 | ||||
-rwxr-xr-x | errors/debug_stack.go | 116 | ||||
-rwxr-xr-x | errors/debug_test.go | 74 | ||||
-rwxr-xr-x | errors/errors.go | 219 | ||||
-rwxr-xr-x | errors/errors_test.go | 177 | ||||
-rwxr-xr-x | errors/go.mod | 1 | ||||
-rwxr-xr-x | errors/marshal.go | 103 |
12 files changed, 700 insertions, 0 deletions
diff --git a/builds/roadrunner-1.8.4-darwin-amd64.zip b/builds/roadrunner-1.8.4-darwin-amd64.zip Binary files differnew file mode 100644 index 00000000..64d49772 --- /dev/null +++ b/builds/roadrunner-1.8.4-darwin-amd64.zip diff --git a/builds/roadrunner-1.8.4-freebsd-amd64.zip b/builds/roadrunner-1.8.4-freebsd-amd64.zip Binary files differnew file mode 100644 index 00000000..4f53b56d --- /dev/null +++ b/builds/roadrunner-1.8.4-freebsd-amd64.zip diff --git a/builds/roadrunner-1.8.4-linux-amd64.tar.gz b/builds/roadrunner-1.8.4-linux-amd64.tar.gz Binary files differnew file mode 100644 index 00000000..24ba9249 --- /dev/null +++ b/builds/roadrunner-1.8.4-linux-amd64.tar.gz diff --git a/builds/roadrunner-1.8.4-unknown-musl-amd64.zip b/builds/roadrunner-1.8.4-unknown-musl-amd64.zip Binary files differnew file mode 100644 index 00000000..c203106e --- /dev/null +++ b/builds/roadrunner-1.8.4-unknown-musl-amd64.zip diff --git a/builds/roadrunner-1.8.4-windows-amd64.zip b/builds/roadrunner-1.8.4-windows-amd64.zip Binary files differnew file mode 100644 index 00000000..3996d36b --- /dev/null +++ b/builds/roadrunner-1.8.4-windows-amd64.zip diff --git a/errors/debug_cap.go b/errors/debug_cap.go new file mode 100755 index 00000000..2c8a2f78 --- /dev/null +++ b/errors/debug_cap.go @@ -0,0 +1,10 @@ +// +build !debug + +package errors + +import "bytes" + +type stack struct{} + +func (e *Error) populateStack() {} +func (e *Error) printStack(*bytes.Buffer) {} diff --git a/errors/debug_stack.go b/errors/debug_stack.go new file mode 100755 index 00000000..fa77ddc8 --- /dev/null +++ b/errors/debug_stack.go @@ -0,0 +1,116 @@ +// +build debug + +package errors + +import ( + "bytes" + "fmt" + "runtime" + "strings" +) + +type stack struct { + callers []uintptr + // TODO(adg): add time of creation +} + +func (e *Error) populateStack() { + e.callers = callers() + + e2, ok := e.Err.(*Error) + if !ok { + return + } + + i := 0 + + ok = false + for ; i < len(e.callers) && i < len(e2.callers); i++ { + // check for similar + if e.callers[len(e.callers)-1-i] != e2.callers[len(e2.callers)-1-i] { + break + } + ok = true + } + + if ok { //we have common PCs + e2Head := e2.callers[:len(e2.callers)-i] + eTail := e.callers + + e.callers = make([]uintptr, len(e2Head)+len(eTail)) + + copy(e.callers, e2Head) + copy(e.callers[len(e2Head):], eTail) + + e2.callers = nil + } +} + +// frame returns the nth frame, with the frame at top of stack being 0. +func frame(callers []uintptr, n int) runtime.Frame { + frames := runtime.CallersFrames(callers) + var f runtime.Frame + for i := len(callers) - 1; i >= n; i-- { + var ok bool + f, ok = frames.Next() + if !ok { + break + } + } + return f +} + +func (e *Error) printStack(b *bytes.Buffer) { + c := callers() + + var prev string + var diff bool + for i := 0; i < len(e.callers); i++ { + pc := e.callers[len(e.callers)-i-1] // get current PC + fn := runtime.FuncForPC(pc) // get function by pc + name := fn.Name() + + if !diff && i < len(c) { + ppc := c[len(c)-i-1] + pname := runtime.FuncForPC(ppc).Name() + if name == pname { + continue + } + diff = true + } + + if name == prev { + continue + } + + trim := 0 + for { + j := strings.IndexAny(name[trim:], "./") + if j < 0 { + break + } + if !strings.HasPrefix(prev, name[:j+trim]) { + break + } + trim += j + 1 // skip over the separator + } + + // Do the printing. + appendStrToBuf(b, Separator) + file, line := fn.FileLine(pc) + fmt.Fprintf(b, "%v:%d: ", file, line) + if trim > 0 { + b.WriteString("...") + } + b.WriteString(name[trim:]) + + prev = name + } +} + +func callers() []uintptr { + var stk [64]uintptr + const skip = 4 + n := runtime.Callers(skip, stk[:]) + return stk[:n] +} diff --git a/errors/debug_test.go b/errors/debug_test.go new file mode 100755 index 00000000..bc866bc8 --- /dev/null +++ b/errors/debug_test.go @@ -0,0 +1,74 @@ +// +build debug + +package errors + +import ( + "fmt" + "regexp" + "strings" + "testing" +) + +var errorLines = strings.Split(strings.TrimSpace(` + .*/errors/debug_test.go:\d+: github.com/ValeryPiashchynski/errors.func1: + .*/errors/debug_test.go:\d+: ...T.func2: + .*/errors/debug_test.go:\d+: ...func3: + .*/errors/debug_test.go:\d+: ...func4: func2 invoke func3: Network error: + func4 operation: error in action +`), "\n") + +var errorLineREs = make([]*regexp.Regexp, len(errorLines)) + +func init() { + for i, s := range errorLines { + errorLineREs[i] = regexp.MustCompile(fmt.Sprintf("^%s", s)) + } +} + +func TestsDebug(t *testing.T) { + got := printErr(t, func1()) + lines := strings.Split(got, "\n") + for i, re := range errorLineREs { + if i >= len(lines) { + // Handled by line number check. + break + } + if !re.MatchString(lines[i]) { + t.Errorf("error does not match at line %v, got:\n\t%q\nwant:\n\t%q", i, lines[i], re) + } + } + if got, want := len(lines), len(errorLines); got != want { + t.Errorf("got %v lines of errors, want %v", got, want) + } +} + +type T struct{} + +func printErr(t *testing.T, err error) string { + return err.Error() +} + +func func1() error { + var t T + return t.func2() +} + +func (T) func2() error { + o := Op("func2 invoke func3") + return E(o, func3()) +} + +func func3() error { + return func4() +} + +func func4() error { + o := Op("func4 operation") + return E(o, Network, Str("error in action")) +} + +///Users/0xdev/Projects/repo/errors/debug_test.go:53: github.com/ValeryPiashchynski/errors.func1: +///Users/0xdev/Projects/repo/errors/debug_test.go:58: ...T.func2: +///Users/0xdev/Projects/repo/errors/debug_test.go:62: ...func3: +///Users/0xdev/Projects/repo/errors/debug_test.go:67: ...func4: func2 invoke func3: Network error: +//func4 operation: error in action diff --git a/errors/errors.go b/errors/errors.go new file mode 100755 index 00000000..def408d8 --- /dev/null +++ b/errors/errors.go @@ -0,0 +1,219 @@ +package errors + +import ( + "bytes" + "encoding" + "errors" + "fmt" + "log" + "runtime" +) + +type Error struct { + Op Op + Kind Kind + Err error + + // Stack information + stack +} + +func (e *Error) isZero() bool { + return e.Op == "" && e.Kind == 0 && e.Err == nil +} + +var ( + _ error = (*Error)(nil) + _ encoding.BinaryUnmarshaler = (*Error)(nil) + _ encoding.BinaryMarshaler = (*Error)(nil) +) + +// Op describes an operation +type Op string + +// separator -> new line plus tabulator to intend error if previuos not nil +var Separator = ":\n\t" + +type Kind uint8 + +// Kinds of errors. +const ( + Undefined Kind = iota // Undefined error. + Network + Other + Test +) + +func (k Kind) String() string { + switch k { + case Undefined: + return "UNDEF" + case Network: + return "Network error" + case Other: + return "Other" + case Test: + return "Test" + + } + return "unknown error kind" +} + +// E builds an error value from its arguments. +func E(args ...interface{}) error { + e := &Error{} + if len(args) == 0 { + msg := "errors.E called with 0 args" + _, file, line, ok := runtime.Caller(1) + if ok { + msg = fmt.Sprintf("%v - %v:%v", msg, file, line) + } + e.Err = errors.New(msg) + } + + for _, arg := range args { + switch arg := arg.(type) { + case Op: + e.Op = arg + case string: + e.Err = Str(arg) + case Kind: + e.Kind = arg + case *Error: + // Make a copy + eCopy := *arg + e.Err = &eCopy + case error: + e.Err = arg + // add map map[string]string + default: + _, file, line, _ := runtime.Caller(1) + log.Printf("errors.E: bad call from %s:%d: %v", file, line, args) + return Errorf("unknown type %T, value %v in error call", arg, arg) + } + } + + // Populate stack information + e.populateStack() + + prev, ok := e.Err.(*Error) + if !ok { + return e + } + + if prev.Kind == e.Kind { + prev.Kind = Undefined + } + + if e.Kind == Undefined { + e.Kind = prev.Kind + prev.Kind = Undefined + } + return e +} + +func (e *Error) Error() string { + b := new(bytes.Buffer) + e.printStack(b) + if e.Op != "" { + appendStrToBuf(b, ": ") + b.WriteString(string(e.Op)) + } + + if e.Kind != 0 { + appendStrToBuf(b, ": ") + b.WriteString(e.Kind.String()) + } + if e.Err != nil { + if prevErr, ok := e.Err.(*Error); ok { + if !prevErr.isZero() { + // indent - separator + appendStrToBuf(b, Separator) + b.WriteString(e.Err.Error()) + } + } else { + appendStrToBuf(b, ": ") + b.WriteString(e.Err.Error()) + } + } + if b.Len() == 0 { + return "no error" + } + return b.String() +} + +// errors.New +func Str(text string) error { + return &errorString{text} +} + +type errorString struct { + s string +} + +func (e *errorString) Error() string { + return e.s +} + +func Errorf(format string, args ...interface{}) error { + return &errorString{fmt.Sprintf(format, args...)} +} + +func Match(err1, err2 error) bool { + e1, ok := err1.(*Error) + if !ok { + return false + } + e2, ok := err2.(*Error) + if !ok { + return false + } + if e1.Op != "" && e2.Op != e1.Op { + return false + } + if e1.Kind != Undefined && e2.Kind != e1.Kind { + return false + } + if e1.Err != nil { + if _, ok := e1.Err.(*Error); ok { + return Match(e1.Err, e2.Err) + } + if e2.Err == nil || e2.Err.Error() != e1.Err.Error() { + return false + } + } + return true +} + +// Is reports whether err is an *Error of the given Kind +func Is(kind Kind, err error) bool { + e, ok := err.(*Error) + if !ok { + return false + } + if e.Kind != Undefined { + return e.Kind == kind + } + if e.Err != nil { + return Is(kind, e.Err) + } + return false +} + +// Do smt with no care about result (and panics) +func SafelyDo(work func()) { + defer func() { + if err := recover(); err != nil { + log.Printf("work failed: %s", err) + } + }() + + work() +} + +func appendStrToBuf(b *bytes.Buffer, str string) { + if b.Len() == 0 { + return + } + b.WriteString(str) +} diff --git a/errors/errors_test.go b/errors/errors_test.go new file mode 100755 index 00000000..50d3d422 --- /dev/null +++ b/errors/errors_test.go @@ -0,0 +1,177 @@ +// +build !debug + +package errors + +import ( + "fmt" + "io" + "os" + "os/exec" + "testing" +) + +func TestDebug(t *testing.T) { + // Test with -tags debug to run the tests in debug_test.go + cmd := exec.Command("go", "test", "-tags", "prod") + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + t.Fatalf("external go test failed: %v", err) + } +} + +func TestMarshal(t *testing.T) { + // Single error. No user is set, so we will have a zero-length field inside. + e1 := E(Op("Get"), Network, "caching in progress") + + // Nested error. + e2 := E(Op("Read"), Undefined, e1) + + b := MarshalError(e2) + e3 := UnmarshalError(b) + + in := e2.(*Error) + out := e3.(*Error) + + // Compare elementwise. + if in.Op != out.Op { + t.Errorf("expected Op %q; got %q", in.Op, out.Op) + } + if in.Kind != out.Kind { + t.Errorf("expected kind %d; got %d", in.Kind, out.Kind) + } + // Note that error will have lost type information, so just check its Error string. + if in.Err.Error() != out.Err.Error() { + t.Errorf("expected Err %q; got %q", in.Err, out.Err) + } +} + +func TestSeparator(t *testing.T) { + defer func(prev string) { + Separator = prev + }(Separator) + Separator = ":: " + + // Single error. No user is set, so we will have a zero-length field inside. + e1 := E(Op("Get"), Network, "network error") + + // Nested error. + e2 := E(Op("Get"), Network, e1) + + want := "Get: Network error:: Get: network error" + if errorAsString(e2) != want { + t.Errorf("expected %q; got %q", want, e2) + } +} + +func TestDoesNotChangePreviousError(t *testing.T) { + err := E(Network) + err2 := E(Op("I will NOT modify err"), err) + + expected := "I will NOT modify err: Network error" + if errorAsString(err2) != expected { + t.Fatalf("Expected %q, got %q", expected, err2) + } + kind := err.(*Error).Kind + if kind != Network { + t.Fatalf("Expected kind %v, got %v", Network, kind) + } +} + +//func TestNoArgs(t *testing.T) { +// defer func() { +// err := recover() +// if err == nil { +// t.Fatal("E() did not panic") +// } +// }() +// _ = E() +//} + +type matchTest struct { + err1, err2 error + matched bool +} + +const ( + op = Op("Op") + op1 = Op("Op1") + op2 = Op("Op2") +) + +var matchTests = []matchTest{ + // Errors not of type *Error fail outright. + {nil, nil, false}, + {io.EOF, io.EOF, false}, + {E(io.EOF), io.EOF, false}, + {io.EOF, E(io.EOF), false}, + // Success. We can drop fields from the first argument and still match. + {E(io.EOF), E(io.EOF), true}, + {E(op, Other, io.EOF), E(op, Other, io.EOF), true}, + {E(op, Other, io.EOF, "test"), E(op, Other, io.EOF, "test", "test"), true}, + {E(op, Other), E(op, Other, io.EOF, "test", "test"), true}, + {E(op), E(op, Other, io.EOF, "test", "test"), true}, + // Failure. + {E(io.EOF), E(io.ErrClosedPipe), false}, + {E(op1), E(op2), false}, + {E(Other), E(Network), false}, + {E("test"), E("test1"), false}, + {E(fmt.Errorf("error")), E(fmt.Errorf("error1")), false}, + {E(op, Other, io.EOF, "test", "test1"), E(op, Other, io.EOF, "test", "test"), false}, + {E("test", Str("something")), E("test"), false}, // Test nil error on rhs. + // Nested *Errors. + {E(op1, E("test")), E(op1, "1", E(op2, "2", "test")), true}, + {E(op1, "test"), E(op1, "1", E(op2, "2", "test")), false}, + {E(op1, E("test")), E(op1, "1", Str(E(op2, "2", "test").Error())), false}, +} + +func TestMatch(t *testing.T) { + for _, test := range matchTests { + matched := Match(test.err1, test.err2) + if matched != test.matched { + t.Errorf("Match(%q, %q)=%t; want %t", test.err1, test.err2, matched, test.matched) + } + } +} + +type kindTest struct { + err error + kind Kind + want bool +} + +var kindTests = []kindTest{ + //Non-Error errors. + {nil, Network, false}, + {Str("not an *Error"), Network, false}, + + // Basic comparisons. + {E(Network), Network, true}, + {E(Test), Network, false}, + {E("no kind"), Network, false}, + {E("no kind"), Other, false}, + + // Nested *Error values. + {E("Nesting", E(Network)), Network, true}, + {E("Nesting", E(Test)), Network, false}, + {E("Nesting", E("no kind")), Network, false}, + {E("Nesting", E("no kind")), Other, false}, +} + +func TestKind(t *testing.T) { + for _, test := range kindTests { + got := Is(test.kind, test.err) + if got != test.want { + t.Errorf("Is(%q, %q)=%t; want %t", test.kind, test.err, got, test.want) + } + } +} + +func errorAsString(err error) string { + if e, ok := err.(*Error); ok { + e2 := *e + e2.stack = stack{} + return e2.Error() + } + return err.Error() +} diff --git a/errors/go.mod b/errors/go.mod new file mode 100755 index 00000000..60dac691 --- /dev/null +++ b/errors/go.mod @@ -0,0 +1 @@ +module github.com/48d90782/errors diff --git a/errors/marshal.go b/errors/marshal.go new file mode 100755 index 00000000..a13ec01f --- /dev/null +++ b/errors/marshal.go @@ -0,0 +1,103 @@ +package errors + +import ( + "encoding/binary" + "log" +) + +func (e *Error) MarshalAppend(b []byte) []byte { + if e == nil { + return b + } + + b = appendString(b, string(e.Op)) + + var tmp [16]byte + N := binary.PutVarint(tmp[:], int64(e.Kind)) + b = append(b, tmp[:N]...) + b = MarshalErrorAppend(e.Err, b) + return b +} + +func (e *Error) MarshalBinary() ([]byte, error) { + return e.MarshalAppend(nil), nil +} + +func MarshalErrorAppend(err error, b []byte) []byte { + if err == nil { + return b + } + if e, ok := err.(*Error); ok { + b = append(b, 'E') + return e.MarshalAppend(b) + } + // Ordinary error. + b = append(b, 'e') + b = appendString(b, err.Error()) + return b + +} + +func MarshalError(err error) []byte { + return MarshalErrorAppend(err, nil) +} + +func (e *Error) UnmarshalBinary(b []byte) error { + if len(b) == 0 { + return nil + } + data, b := getBytes(b) + if data != nil { + e.Op = Op(data) + } + k, N := binary.Varint(b) + e.Kind = Kind(k) + b = b[N:] + e.Err = UnmarshalError(b) + return nil +} + +func UnmarshalError(b []byte) error { + if len(b) == 0 { + return nil + } + code := b[0] + b = b[1:] + switch code { + case 'e': + var data []byte + data, b = getBytes(b) + if len(b) != 0 { + log.Printf("Unmarshal error: trailing bytes") + } + return Str(string(data)) + case 'E': + var err Error + err.UnmarshalBinary(b) + return &err + default: + log.Printf("Unmarshal error: corrupt data %q", b) + return Str(string(b)) + } +} + +func appendString(b []byte, str string) []byte { + var tmp [16]byte + N := binary.PutUvarint(tmp[:], uint64(len(str))) + b = append(b, tmp[:N]...) + b = append(b, str...) + return b +} + +func getBytes(b []byte) (data, remaining []byte) { + u, N := binary.Uvarint(b) + if len(b) < N+int(u) { + log.Printf("Unmarshal error: bad encoding") + return nil, nil + } + if N == 0 { + log.Printf("Unmarshal error: bad encoding") + return nil, b + } + return b[N : N+int(u)], b[N+int(u):] +} |