merr: initial implementation, designed to replace merry and work nicely with mlog, while allowing support for multi-errors and other niceties in the future

This commit is contained in:
Brian Picciano 2019-01-13 20:08:38 -05:00
parent 33a10a4ac7
commit 56f3e71acd
6 changed files with 481 additions and 0 deletions

73
merr/kv.go Normal file
View File

@ -0,0 +1,73 @@
package merr
import (
"fmt"
"path/filepath"
)
// not really used for attributes, but w/e
const attrKeyErr attrKey = "err"
const attrKeyErrSrc attrKey = "errSrc"
// KVer implements the mlog.KVer interface. This is defined here to avoid this
// package needing to actually import mlog.
type KVer struct {
kv map[string]interface{}
}
// KV implements the mlog.KVer interface.
func (kv KVer) KV() map[string]interface{} {
return kv.kv
}
// KV returns a KVer which contains all visible values embedded in the error, as
// well as the original error string itself. Keys will be turned into strings
// using the fmt.Sprint function.
//
// If any keys conflict then their type information will be included as part of
// the key.
func KV(e error) KVer {
er := wrap(e, false, 1)
kvm := make(map[string]interface{}, len(er.attr)+1)
keys := map[string]interface{}{} // in this case the value is the raw key
setKey := func(k, v interface{}) {
kStr := fmt.Sprint(k)
oldKey := keys[kStr]
if oldKey == nil {
keys[kStr] = k
kvm[kStr] = v
return
}
// check if oldKey is in kvm, if so it needs to be moved to account for
// its type info
if oldV, ok := kvm[kStr]; ok {
delete(kvm, kStr)
kvm[fmt.Sprintf("%T(%s)", oldKey, kStr)] = oldV
}
kvm[fmt.Sprintf("%T(%s)", k, kStr)] = v
}
setKey(attrKeyErr, er.err.Error())
for k, v := range er.attr {
if !v.visible {
continue
}
stack, ok := v.val.(Stack)
if !ok {
setKey(k, v.val)
continue
}
// compress the stack trace to just be the top-most frame
frame := stack.Frame()
file, dir := filepath.Base(frame.File), filepath.Dir(frame.File)
dir = filepath.Base(dir) // only want the first dirname, ie the pkg name
setKey(attrKeyErrSrc, fmt.Sprintf("%s/%s:%d", dir, file, frame.Line))
}
return KVer{kvm}
}

69
merr/kv_test.go Normal file
View File

@ -0,0 +1,69 @@
package merr
import (
"strings"
. "testing"
"github.com/mediocregopher/mediocre-go-lib/mtest/massert"
)
func TestKV(t *T) {
er := New("foo")
kv := KV(er).KV()
massert.Fatal(t, massert.Comment(
massert.All(
massert.Len(kv, 2),
massert.Equal("foo", kv["err"]),
massert.Equal(true,
strings.HasPrefix(kv["errSrc"].(string), "merr/kv_test.go:")),
),
"kv: %#v", kv,
))
type A string
type B string
type C string
er = WithValue(er, "invisible", "you can't see me", false)
er = WithValue(er, A("k"), "1", true)
kv = KV(er).KV()
massert.Fatal(t, massert.Comment(
massert.All(
massert.Len(kv, 3),
massert.Equal("foo", kv["err"]),
massert.Equal(true,
strings.HasPrefix(kv["errSrc"].(string), "merr/kv_test.go:")),
massert.Equal("1", kv["k"]),
),
"kv: %#v", kv,
))
er = WithValue(er, B("k"), "2", true)
kv = KV(er).KV()
massert.Fatal(t, massert.Comment(
massert.All(
massert.Len(kv, 4),
massert.Equal("foo", kv["err"]),
massert.Equal(true,
strings.HasPrefix(kv["errSrc"].(string), "merr/kv_test.go:")),
massert.Equal("1", kv["merr.A(k)"]),
massert.Equal("2", kv["merr.B(k)"]),
),
"kv: %#v", kv,
))
er = WithValue(er, C("k"), "3", true)
kv = KV(er).KV()
massert.Fatal(t, massert.Comment(
massert.All(
massert.Len(kv, 5),
massert.Equal("foo", kv["err"]),
massert.Equal(true,
strings.HasPrefix(kv["errSrc"].(string), "merr/kv_test.go:")),
massert.Equal("1", kv["merr.A(k)"]),
massert.Equal("2", kv["merr.B(k)"]),
massert.Equal("3", kv["merr.C(k)"]),
),
"kv: %#v", kv,
))
}

166
merr/merr.go Normal file
View File

@ -0,0 +1,166 @@
// Package merr extends the errors package with features like key-value
// attributes for errors, embedded stacktraces, and multi-errors.
package merr
import (
"errors"
"fmt"
"sort"
"strings"
"sync"
)
var strBuilderPool = sync.Pool{
New: func() interface{} { return new(strings.Builder) },
}
func putStrBuilder(sb *strings.Builder) {
sb.Reset()
strBuilderPool.Put(sb)
}
////////////////////////////////////////////////////////////////////////////////
type val struct {
visible bool
val interface{}
}
func (v val) String() string {
return fmt.Sprint(v.val)
}
type err struct {
err error
attr map[interface{}]val
}
// attr keys internal to this package
type attrKey string
func wrap(e error, cp bool, skip int) *err {
er, ok := e.(*err)
if !ok {
er := &err{err: e, attr: map[interface{}]val{}}
if skip >= 0 {
setStack(er, skip+1)
}
return er
} else if !cp {
return er
}
er2 := &err{
err: er.err,
attr: make(map[interface{}]val, len(er.attr)),
}
for k, v := range er.attr {
er2.attr[k] = v
}
if _, ok := er2.attr[attrKeyStack]; !ok && skip >= 0 {
setStack(er, skip+1)
}
return er2
}
// Wrap takes in an error and returns one wrapping it in merr's inner type,
// which embeds information like the stack trace.
func Wrap(e error) error {
return wrap(e, false, 1)
}
// New returns a new error with the given string as its error string. New
// automatically wraps the error in merr's inner type, which embeds information
// like the stack trace.
func New(str string) error {
return wrap(errors.New(str), false, 1)
}
// Errorf is like New, but allows for formatting of the string.
func Errorf(str string, args ...interface{}) error {
return wrap(fmt.Errorf(str, args...), false, 1)
}
func (er *err) visibleAttrs() [][2]string {
out := make([][2]string, 0, len(er.attr))
for k, v := range er.attr {
if !v.visible {
continue
}
out = append(out, [2]string{
strings.Trim(fmt.Sprintf("%q", k), `"`),
fmt.Sprint(v.val),
})
}
sort.Slice(out, func(i, j int) bool {
return out[i][0] < out[j][0]
})
return out
}
func (er *err) Error() string {
visAttrs := er.visibleAttrs()
if len(visAttrs) == 0 {
return er.err.Error()
}
sb := strBuilderPool.Get().(*strings.Builder)
defer putStrBuilder(sb)
sb.WriteString(strings.TrimSpace(er.err.Error()))
for _, attr := range visAttrs {
k, v := strings.TrimSpace(attr[0]), strings.TrimSpace(attr[1])
sb.WriteString("\n\t* ")
sb.WriteString(k)
sb.WriteString(": ")
// if there's no newlines then print v inline with k
if strings.Index(v, "\n") < 0 {
sb.WriteString(v)
continue
}
for _, vLine := range strings.Split(v, "\n") {
sb.WriteString("\n\t\t")
sb.WriteString(strings.TrimSpace(vLine))
}
}
return sb.String()
}
// WithValue returns a copy of the original error, automatically wrapping it if
// the error is not from merr (see Wrap). The returned error has a value set on
// with for the given key.
//
// visible determines whether or not the value is visible in the output of
// Error.
func WithValue(e error, k, v interface{}, visible bool) error {
er := wrap(e, true, 1)
er.attr[k] = val{val: v, visible: visible}
return er
}
// GetValue returns the value embedded in the error for the given key, or nil if
// the error isn't from this package or doesn't have that key embedded.
func GetValue(e error, k interface{}) interface{} {
return wrap(e, false, -1).attr[k].val
}
// Base takes in an error and checks if it is merr's internal error type. If it
// is then the underlying error which is being wrapped is returned. If it's not
// then the passed in error is returned as-is.
func Base(e error) error {
if er, ok := e.(*err); ok {
return er.err
}
return e
}
// Equal is a shortcut for Base(e1) == Base(e2).
func Equal(e1, e2 error) bool {
return Base(e1) == Base(e2)
}

41
merr/merr_test.go Normal file
View File

@ -0,0 +1,41 @@
package merr
import (
"errors"
. "testing"
"github.com/mediocregopher/mediocre-go-lib/mtest/massert"
)
func TestError(t *T) {
er := &err{
err: errors.New("foo"),
attr: map[interface{}]val{
"a": val{val: "aaa aaa\n", visible: true},
"b": val{val: "invisible"},
"c": val{val: "ccc\nccc\n", visible: true},
"d\t": val{val: "weird key but ok", visible: true},
},
}
str := er.Error()
exp := `foo
* a: aaa aaa
* c:
ccc
ccc
* d\t: weird key but ok`
massert.Fatal(t, massert.Equal(exp, str))
}
func TestBase(t *T) {
errFoo, errBar := errors.New("foo"), errors.New("bar")
erFoo := Wrap(errFoo)
massert.Fatal(t, massert.All(
massert.Equal(errFoo, Base(erFoo)),
massert.Equal(errBar, Base(errBar)),
massert.Not(massert.Equal(errFoo, erFoo)),
massert.Not(massert.Equal(errBar, Base(erFoo))),
massert.Equal(true, Equal(errFoo, erFoo)),
massert.Equal(false, Equal(errBar, erFoo)),
))
}

85
merr/stack.go Normal file
View File

@ -0,0 +1,85 @@
package merr
import (
"fmt"
"runtime"
"strings"
"text/tabwriter"
)
// MaxStackSize indicates the maximum number of stack frames which will be
// stored when embedding stack traces in errors.
var MaxStackSize = 50
const attrKeyStack attrKey = "stack"
// Stack represents a stack trace at a particular point in execution.
type Stack []uintptr
// Frame returns the first frame in the stack.
func (s Stack) Frame() runtime.Frame {
if len(s) == 0 {
panic("cannot call Frame on empty stack")
}
frame, _ := runtime.CallersFrames([]uintptr(s)).Next()
return frame
}
// Frames returns all runtime.Frame instances for this stack.
func (s Stack) Frames() []runtime.Frame {
if len(s) == 0 {
return nil
}
out := make([]runtime.Frame, 0, len(s))
frames := runtime.CallersFrames([]uintptr(s))
for {
frame, more := frames.Next()
out = append(out, frame)
if !more {
break
}
}
return out
}
// String returns the full stack trace.
func (s Stack) String() string {
sb := strBuilderPool.Get().(*strings.Builder)
defer putStrBuilder(sb)
tw := tabwriter.NewWriter(sb, 0, 4, 4, ' ', 0)
for _, frame := range s.Frames() {
file := fmt.Sprintf("%s:%d", frame.File, frame.Line)
fmt.Fprintf(tw, "%s\t%s\n", file, frame.Function)
}
if err := tw.Flush(); err != nil {
panic(err)
}
return sb.String()
}
func setStack(er *err, skip int) {
stackSlice := make([]uintptr, MaxStackSize)
// incr skip once for setStack, and once for runtime.Callers
l := runtime.Callers(skip+2, stackSlice)
er.attr[attrKeyStack] = val{val: Stack(stackSlice[:l]), visible: true}
}
// WithStack returns a copy of the original error, automatically wrapping it if
// the error is not from merr (see Wrap). The returned error has the embedded
// stacktrace set to the frame calling this function.
//
// skip can be used to exclude that many frames from the top of the stack.
func WithStack(e error, skip int) error {
er := wrap(e, true, -1)
setStack(er, skip+1)
return er
}
// GetStack returns the Stack which was embedded in the error, if the error is
// from this package. If not then nil is returned.
func GetStack(e error) Stack {
stack, _ := wrap(e, false, -1).attr[attrKeyStack].val.(Stack)
return stack
}

47
merr/stack_test.go Normal file
View File

@ -0,0 +1,47 @@
package merr
import (
"strings"
. "testing"
"github.com/mediocregopher/mediocre-go-lib/mtest/massert"
)
func TestStack(t *T) {
foo := New("foo")
fooStack := GetStack(foo)
// test Frame
frame := fooStack.Frame()
massert.Fatal(t, massert.All(
massert.Equal(true, strings.Contains(frame.File, "stack_test.go")),
massert.Equal(true, strings.Contains(frame.Function, "TestStack")),
))
frames := fooStack.Frames()
massert.Fatal(t, massert.Comment(
massert.All(
massert.Equal(true, len(frames) >= 2),
massert.Equal(true, strings.Contains(frames[0].File, "stack_test.go")),
massert.Equal(true, strings.Contains(frames[0].Function, "TestStack")),
),
"fooStack.String():\n%s", fooStack.String(),
))
// test that WithStack works and can be used to skip frames
inner := func() {
bar := WithStack(foo, 1)
barStack := GetStack(bar)
frames := barStack.Frames()
massert.Fatal(t, massert.Comment(
massert.All(
massert.Equal(true, len(frames) >= 2),
massert.Equal(true, strings.Contains(frames[0].File, "stack_test.go")),
massert.Equal(true, strings.Contains(frames[0].Function, "TestStack")),
),
"barStack.String():\n%s", barStack.String(),
))
}
inner()
}