From dfafd27c57049228db3dd78fe01d3839e4544d32 Mon Sep 17 00:00:00 2001 From: sum2000 Date: Tue, 24 Dec 2019 20:16:36 -0500 Subject: [PATCH] feat: json and str formats support with custom error type --- eris.go | 31 ++--- eris_test.go | 9 +- format.go | 196 +++++++++++++++++++++++++++++ format_test.go | 328 ++++++++++++++++++++++++++++++++++++++++++++++++ printer.go | 179 -------------------------- printer_test.go | 1 - stack.go | 61 ++++++--- stack_test.go | 1 - 8 files changed, 586 insertions(+), 220 deletions(-) create mode 100644 format.go create mode 100644 format_test.go delete mode 100644 printer.go delete mode 100644 printer_test.go delete mode 100644 stack_test.go diff --git a/eris.go b/eris.go index 1d64cca..b9d1261 100644 --- a/eris.go +++ b/eris.go @@ -113,19 +113,6 @@ func Cause(err error) error { } } -func formatError(err error, s fmt.State, verb rune) { - var withTrace bool - switch verb { - case 'v': - if s.Flag('+') { - withTrace = true - } - } - f := NewDefaultFormat(withTrace) - p := NewDefaultPrinter(f) - io.WriteString(s, p.Sprint(err)) -} - type rootError struct { msg string stack *stack @@ -136,7 +123,7 @@ func (e *rootError) Error() string { } func (e *rootError) Format(s fmt.State, verb rune) { - formatError(e, s, verb) + printError(e, s, verb) } func (e *rootError) Is(target error) bool { @@ -157,7 +144,7 @@ func (e *wrapError) Error() string { } func (e *wrapError) Format(s fmt.State, verb rune) { - formatError(e, s, verb) + printError(e, s, verb) } func (e *wrapError) Is(target error) bool { @@ -170,3 +157,17 @@ func (e *wrapError) Is(target error) bool { func (e *wrapError) Unwrap() error { return e.err } + +func printError(err error, s fmt.State, verb rune) { + var withTrace bool + switch verb { + case 'v': + if s.Flag('+') { + withTrace = true + } + } + format := NewDefaultFormat(withTrace) + uErr := Unpack(err) + str := uErr.ToString(format) + io.WriteString(s, str) +} diff --git a/eris_test.go b/eris_test.go index 249e4c0..a188aef 100644 --- a/eris_test.go +++ b/eris_test.go @@ -230,9 +230,10 @@ func TestErrorFormatting(t *testing.T) { t.Errorf("%v: expected { %v } got { %v }", desc, tc.output, err) } - // todo: not sure how to automate stack trace verification - fmt.Printf("error formatting results (%v):\n", desc) - fmt.Printf("%v\n", err) - fmt.Printf("%+v", err) + // todo: automate stack trace verification + fmt.Sprintf("error formatting results (%v):\n", desc) + fmt.Sprintf("%v\n", err) + fmt.Sprintf("%+v", err) + } } diff --git a/format.go b/format.go new file mode 100644 index 0000000..4121cb9 --- /dev/null +++ b/format.go @@ -0,0 +1,196 @@ +package eris + +import ( + "fmt" +) + +// Format defines an error output format to be used with the default formatter. +type Format struct { + WithTrace bool // Flag that enables stack trace output. + Msg string // Separator between error messages and stack frame data. + TBeg string // Separator at the beginning of each stack frame. + TSep string // Separator between elements of each stack frame. + Sep string // Separator between each error in the chain. +} + +// NewDefaultFormat conveniently returns a basic format for the default string formatter. +func NewDefaultFormat(withTrace bool) Format { + stringFmt := Format{ + WithTrace: withTrace, + Sep: ": ", + } + if withTrace { + stringFmt.Msg = "\n" + stringFmt.TBeg = "\t" + stringFmt.TSep = ": " + stringFmt.Sep = "\n" + } + return stringFmt +} + +// UnpackedError represents complete information about an error. +// +// This type can be used for custom error logging and parsing. Use `eris.Unpack` to build an UnpackedError +// from any error type. The ErrChain and ErrRoot fields correspond to `wrapError` and `rootError` types, +// respectively. If any other error type is unpacked, it will appear in the ExternalErr field. +type UnpackedError struct { + ErrChain *[]ErrLink + ErrRoot *ErrRoot + ExternalErr string +} + +// Unpack returns UnpackedError type for a given golang error type. +func Unpack(err error) UnpackedError { + e := UnpackedError{} + switch err.(type) { + case nil: + return UnpackedError{} + case *rootError: + e = unpackRootErr(err.(*rootError)) + case *wrapError: + chain := []ErrLink{} + e = unpackWrapErr(&chain, err.(*wrapError)) + default: + e.ExternalErr = err.Error() + } + return e +} + +// ToString returns a default formatted string for a given eris error. +func (upErr *UnpackedError) ToString(format Format) string { + var str string + if upErr.ErrChain != nil { + for _, eLink := range *upErr.ErrChain { + str += eLink.formatStr(format) + } + } + str += upErr.ErrRoot.formatStr(format) + if upErr.ExternalErr != "" { + str += fmt.Sprint(upErr.ExternalErr) + } + return str +} + +// ToJSON returns a JSON formatted map for a given eris error. +func (upErr *UnpackedError) ToJSON(format Format) map[string]interface{} { + if upErr == nil { + return nil + } + jsonMap := make(map[string]interface{}) + if fmtRootErr := upErr.ErrRoot.formatJSON(format); fmtRootErr != nil { + jsonMap["error root"] = fmtRootErr + } + if upErr.ErrChain != nil { + var wrapArr []map[string]interface{} + for _, eLink := range *upErr.ErrChain { + wrapMap := eLink.formatJSON(format) + wrapArr = append(wrapArr, wrapMap) + } + jsonMap["error chain"] = wrapArr + } + if upErr.ExternalErr != "" { + jsonMap["external error"] = fmt.Sprint(upErr.ExternalErr) + } + return jsonMap +} + +func unpackRootErr(err *rootError) UnpackedError { + return UnpackedError{ + ErrRoot: &ErrRoot{ + Msg: err.msg, + Stack: err.stack.get(), + }, + } +} + +func unpackWrapErr(chain *[]ErrLink, err *wrapError) UnpackedError { + link := ErrLink{} + link.Frame = *err.frame.get() + link.Msg = err.msg + *chain = append(*chain, link) + + e := UnpackedError{} + e.ErrChain = chain + + nextErr := err.Unwrap() + switch nextErr.(type) { + case nil: + return e + case *rootError: + uErr := unpackRootErr(nextErr.(*rootError)) + e.ErrRoot = uErr.ErrRoot + case *wrapError: + e = unpackWrapErr(chain, nextErr.(*wrapError)) + default: + e.ExternalErr = err.Error() + } + return e +} + +type ErrRoot struct { + Msg string + Stack []StackFrame +} + +func (err *ErrRoot) formatStr(format Format) string { + if err == nil { + return "" + } + str := err.Msg + str += format.Msg + if format.WithTrace { + stackArr := formatStackFrames(err.Stack, format.TSep) + for _, frame := range stackArr { + str += format.TBeg + str += frame + str += format.Sep + } + } + return str +} + +func (err *ErrRoot) formatJSON(format Format) map[string]interface{} { + if err == nil { + return nil + } + rootMap := make(map[string]interface{}) + rootMap["message"] = fmt.Sprint(err.Msg) + if format.WithTrace { + rootMap["stack"] = formatStackFrames(err.Stack, format.TSep) + } + return rootMap +} + +type ErrLink struct { + Msg string + Frame StackFrame +} + +func (eLink *ErrLink) formatStr(format Format) string { + var str string + str += eLink.Msg + str += format.Msg + if format.WithTrace { + str += format.TBeg + str += eLink.Frame.formatFrame(format.TSep) + } + str += format.Sep + return str +} + +func (eLink *ErrLink) formatJSON(format Format) map[string]interface{} { + wrapMap := make(map[string]interface{}) + wrapMap["message"] = fmt.Sprint(eLink.Msg) + if format.WithTrace { + wrapMap["stack"] = eLink.Frame.formatFrame(format.TSep) + } + return wrapMap +} + +func formatStackFrames(s []StackFrame, sep string) []string { + var str []string + for _, f := range s { + str = append(str, f.formatFrame(sep)) + } + return str +} diff --git a/format_test.go b/format_test.go new file mode 100644 index 0000000..755e590 --- /dev/null +++ b/format_test.go @@ -0,0 +1,328 @@ +package eris_test + +import ( + "encoding/json" + "errors" + "reflect" + "testing" + + "github.com/morningvera/eris" +) + +func errChainsEqual(a []eris.ErrLink, b []eris.ErrLink) bool { + // If one is nil, the other must also be nil. + if (a == nil) != (b == nil) { + return false + } + + if len(a) != len(b) { + return false + } + + for i := range a { + if a[i].Msg != b[i].Msg { + return false + } + } + + return true +} + +func TestUnpack(t *testing.T) { + tests := map[string]struct { + cause error + input []string + output eris.UnpackedError + }{ + "nil error": { + cause: nil, + input: nil, + output: eris.UnpackedError{}, + }, + "nil root error": { + cause: nil, + input: []string{"additional context"}, + output: eris.UnpackedError{}, + }, + "standard error wrapping with internal root cause (eris.New)": { + cause: eris.New("root error"), + input: []string{"additional context", "even more context"}, + output: eris.UnpackedError{ + ErrRoot: &eris.ErrRoot{ + Msg: "root error", + }, + ErrChain: &[]eris.ErrLink{ + { + Msg: "even more context", + }, + { + Msg: "additional context", + }, + }, + }, + }, + "standard error wrapping with external root cause (errors.New)": { + cause: errors.New("external error"), + input: []string{"additional context", "even more context"}, + output: eris.UnpackedError{ + ErrRoot: &eris.ErrRoot{ + Msg: "external error", + }, + ErrChain: &[]eris.ErrLink{ + { + Msg: "even more context", + }, + { + Msg: "additional context", + }, + }, + }, + }, + "no error wrapping with internal root cause (eris.Errorf)": { + cause: eris.Errorf("%v", "root error"), + output: eris.UnpackedError{ + ErrRoot: &eris.ErrRoot{ + Msg: "root error", + }, + }, + }, + "no error wrapping with external root cause (errors.New)": { + cause: errors.New("external error"), + output: eris.UnpackedError{ + ExternalErr: "external error", + }, + }, + } + for desc, tt := range tests { + t.Run(desc, func(t *testing.T) { + err := setupTestCase(false, tt.cause, tt.input) + if got := eris.Unpack(err); got.ErrChain != nil && tt.output.ErrChain != nil && !errChainsEqual(*got.ErrChain, *tt.output.ErrChain) { + t.Errorf("Unpack() ErrorChain = %v, want %v", *got.ErrChain, *tt.output.ErrChain) + } + if got := eris.Unpack(err); got.ErrRoot != nil && tt.output.ErrRoot != nil && !reflect.DeepEqual(got.ErrRoot.Msg, tt.output.ErrRoot.Msg) { + t.Errorf("Unpack() ErrorRoot = %v, want %v", got.ErrRoot.Msg, tt.output.ErrRoot.Msg) + } + }) + } +} + +func TestFormatStr(t *testing.T) { + tests := map[string]struct { + basicInput eris.UnpackedError + formattedInput eris.UnpackedError + basicOutput string + formattedOutput string + }{ + "basic root error": { + basicInput: eris.UnpackedError{ + ErrRoot: &eris.ErrRoot{ + Msg: "root error", + }, + }, + formattedInput: eris.UnpackedError{}, + basicOutput: "root error", + }, + "basic root error (formatted)": { + basicInput: eris.UnpackedError{}, + formattedInput: eris.UnpackedError{ + ErrRoot: &eris.ErrRoot{ + Msg: "root error", + Stack: []eris.StackFrame{ + { + Name: "eris.TestFormatStr", + File: "format_test.go", + Line: 99, + }, + { + Name: "golang.Runtime", + File: "runtime.go", + Line: 100, + }, + }, + }, + }, + formattedOutput: "root error\n\teris.TestFormatStr: format_test.go: 99\n\tgolang.Runtime: runtime.go: 100\n", + }, + "basic wrapped error": { + basicInput: eris.UnpackedError{ + ErrRoot: &eris.ErrRoot{ + Msg: "root error", + }, + ErrChain: &[]eris.ErrLink{ + { + Msg: "even more context", + }, + { + Msg: "additional context", + }, + }, + }, + formattedInput: eris.UnpackedError{}, + basicOutput: "even more context: additional context: root error", + }, + "basic wrapped error (formatted)": { + basicInput: eris.UnpackedError{}, + formattedInput: eris.UnpackedError{ + ErrRoot: &eris.ErrRoot{ + Msg: "root error", + Stack: []eris.StackFrame{ + { + Name: "eris.TestFormatStr", + File: "format_test.go", + Line: 99, + }, + { + Name: "golang.Runtime", + File: "runtime.go", + Line: 100, + }, + }, + }, + ErrChain: &[]eris.ErrLink{ + { + Msg: "additional context", + Frame: eris.StackFrame{ + Name: "eris.TestFormatStr", + File: "format_test.go", + Line: 300, + }, + }, + }, + }, + formattedOutput: "additional context\n\teris.TestFormatStr: format_test.go: 300\nroot error\n\teris.TestFormatStr: format_test.go: 99\n\tgolang.Runtime: runtime.go: 100\n", + }, + "basic external error": { + basicInput: eris.UnpackedError{ + ExternalErr: "external error", + }, + formattedInput: eris.UnpackedError{}, + basicOutput: "external error", + }, + } + for desc, tt := range tests { + t.Run(desc, func(t *testing.T) { + if got := tt.basicInput.ToString(eris.NewDefaultFormat(false)); !reflect.DeepEqual(got, tt.basicOutput) { + t.Errorf("ToString() = %v, want %v", got, tt.basicOutput) + } + }) + t.Run(desc, func(t *testing.T) { + if got := tt.formattedInput.ToString(eris.NewDefaultFormat(true)); !reflect.DeepEqual(got, tt.formattedOutput) { + t.Errorf("ToString() = %v, want %v", got, tt.formattedOutput) + } + }) + } +} + +func TestFormatJSON(t *testing.T) { + tests := map[string]struct { + basicInput eris.UnpackedError + formattedInput eris.UnpackedError + basicOutput string + formattedOutput string + }{ + "basic root error": { + basicInput: eris.UnpackedError{ + ErrRoot: &eris.ErrRoot{ + Msg: "root error", + }, + }, + formattedInput: eris.UnpackedError{}, + basicOutput: `{"error root":{"message":"root error"}}`, + formattedOutput: `{}`, + }, + "basic root error (formatted)": { + basicInput: eris.UnpackedError{}, + formattedInput: eris.UnpackedError{ + ErrRoot: &eris.ErrRoot{ + Msg: "root error", + Stack: []eris.StackFrame{ + { + Name: "eris.TestFormatStr", + File: "format_test.go", + Line: 99, + }, + { + Name: "golang.Runtime", + File: "runtime.go", + Line: 100, + }, + }, + }, + }, + formattedOutput: `{"error root":{"message":"root error","stack":["eris.TestFormatStr: format_test.go: 99","golang.Runtime: runtime.go: 100"]}}`, + basicOutput: `{}`, + }, + "basic wrapped error": { + basicInput: eris.UnpackedError{ + ErrRoot: &eris.ErrRoot{ + Msg: "root error", + }, + ErrChain: &[]eris.ErrLink{ + { + Msg: "even more context", + }, + { + Msg: "additional context", + }, + }, + }, + formattedInput: eris.UnpackedError{}, + basicOutput: `{"error chain":[{"message":"even more context"},{"message":"additional context"}],"error root":{"message":"root error"}}`, + formattedOutput: `{}`, + }, + "basic wrapped error (formatted)": { + basicInput: eris.UnpackedError{}, + formattedInput: eris.UnpackedError{ + ErrRoot: &eris.ErrRoot{ + Msg: "root error", + Stack: []eris.StackFrame{ + { + Name: "eris.TestFormatStr", + File: "format_test.go", + Line: 99, + }, + { + Name: "golang.Runtime", + File: "runtime.go", + Line: 100, + }, + }, + }, + ErrChain: &[]eris.ErrLink{ + { + Msg: "additional context", + Frame: eris.StackFrame{ + Name: "eris.TestFormatStr", + File: "format_test.go", + Line: 300, + }, + }, + }, + }, + basicOutput: `{}`, + formattedOutput: `{"error chain":[{"message":"additional context","stack":"eris.TestFormatStr: format_test.go: 300"}],"error root":{"message":"root error","stack":["eris.TestFormatStr: format_test.go: 99","golang.Runtime: runtime.go: 100"]}}`, + }, + "basic external error": { + basicInput: eris.UnpackedError{ + ExternalErr: "external error", + }, + formattedInput: eris.UnpackedError{}, + basicOutput: `{"external error":"external error"}`, + formattedOutput: `{}`, + }, + } + for desc, tt := range tests { + t.Run(desc, func(t *testing.T) { + result, _ := json.Marshal(tt.basicInput.ToJSON(eris.NewDefaultFormat(false))) + if got := string(result); !reflect.DeepEqual(got, tt.basicOutput) { + t.Errorf("ToJSON() = %v, want %v", got, tt.basicOutput) + } + }) + t.Run(desc, func(t *testing.T) { + result, _ := json.Marshal(tt.formattedInput.ToJSON(eris.NewDefaultFormat(true))) + if got := string(result); !reflect.DeepEqual(got, tt.formattedOutput) { + t.Errorf("ToJSON() = %v, want %v", got, tt.formattedOutput) + } + }) + } +} diff --git a/printer.go b/printer.go deleted file mode 100644 index 9768c8e..0000000 --- a/printer.go +++ /dev/null @@ -1,179 +0,0 @@ -package eris - -import ( - "encoding/json" - "fmt" -) - -// Format defines an error output format to be used with the default printer. -type Format struct { - WithTrace bool // Flag that enables stack trace output. - Msg string // Separator between error messages and stack frame data. - TBeg string // Separator at the beginning of each stack frame. - TSep string // Separator between elements of each stack frame. - Sep string // Separator between each error in the chain. -} - -// NewDefaultFormat conveniently returns a basic format for the default string printer. -func NewDefaultFormat(withTrace bool) Format { - stringFmt := Format{ - WithTrace: withTrace, - Sep: ": ", - } - if withTrace { - stringFmt.Msg = "\n" - stringFmt.TBeg = "\t" - stringFmt.TSep = ":" - stringFmt.Sep = "\n" - } - return stringFmt -} - -// Printer defines a basic printer interface. -type Printer interface { - // Sprint returns a formatted string for a given error. - Sprint(err error) string -} - -type defaultPrinter struct { - format Format -} - -// NewDefaultPrinter returns a basic printer that converts errors into strings. -func NewDefaultPrinter(format Format) Printer { - return &defaultPrinter{ - format: format, - } -} - -// Sprint returns a default formatted string for a given error. -func (p *defaultPrinter) Sprint(err error) string { - var str string - switch err.(type) { - case nil: - return "" - case *rootError: - str = p.printRootError(err.(*rootError)) - case *wrapError: - str = p.printWrapError(err.(*wrapError)) - default: - str = fmt.Sprint(err) + p.format.Sep - } - return str -} - -func (p *defaultPrinter) printRootError(err *rootError) string { - str := err.msg - str += p.format.Msg - if p.format.WithTrace { - stackArr := printStack(err.stack, p.format.TSep) - for _, frame := range stackArr { - str += p.format.TBeg - str += frame - str += p.format.Sep - } - } - return str -} - -func (p *defaultPrinter) printWrapError(err *wrapError) string { - str := err.msg - str += p.format.Msg - if p.format.WithTrace { - str += p.format.TBeg - str += printFrame(err.frame, p.format.TSep) - } - str += p.format.Sep - - nextErr := err.Unwrap() - switch nextErr.(type) { - case nil: - return "" - case *rootError: - str += p.printRootError(nextErr.(*rootError)) - case *wrapError: - str += p.printWrapError(nextErr.(*wrapError)) - default: - str += fmt.Sprint(nextErr) + p.format.Sep - } - - return str -} - -type jsonPrinter struct { - format Format -} - -// NewJSONPrinter returns a basic printer that converts errors into JSON formatted strings. -func NewJSONPrinter(format Format) Printer { - return &jsonPrinter{ - format: format, - } -} - -// Sprint returns a JSON formatted string for a given error. -func (p *jsonPrinter) Sprint(err error) string { - jsonMap := make(map[string]interface{}) - switch err.(type) { - case nil: - return "{}" - case *rootError: - jsonMap["error root"] = p.printRootError(err.(*rootError)) - case *wrapError: - jsonMap = p.printWrapError(err.(*wrapError)) - default: - jsonMap["external error"] = fmt.Sprint(err) - } - str, _ := json.Marshal(jsonMap) - return string(str) -} - -func (p *jsonPrinter) printRootError(err *rootError) map[string]interface{} { - rootMap := make(map[string]interface{}) - rootMap["message"] = fmt.Sprint(err.msg) - if p.format.WithTrace { - rootMap["stack"] = printStack(err.stack, p.format.TSep) - } - return rootMap -} - -func (p *jsonPrinter) printWrapError(err *wrapError) map[string]interface{} { - jsonMap := make(map[string]interface{}) - - nextErr := error(err) - var wrapArr []map[string]interface{} - for { - if nextErr == nil { - break - } else if e, ok := nextErr.(*rootError); ok { - jsonMap["error root"] = p.printRootError(e) - } else if e, ok := nextErr.(*wrapError); ok { - wrapMap := make(map[string]interface{}) - wrapMap["message"] = fmt.Sprint(e.msg) - if p.format.WithTrace { - wrapMap["stack"] = printFrame(e.frame, p.format.TSep) - } - wrapArr = append(wrapArr, wrapMap) - } else { - jsonMap["external error"] = fmt.Sprint(nextErr) - } - nextErr = Unwrap(nextErr) - } - jsonMap["error chain"] = wrapArr - - return jsonMap -} - -func printFrame(f *frame, sep string) string { - fData := f.get() - return fmt.Sprintf("%v%v%v%v%v", fData.name, sep, fData.file, sep, fData.line) -} - -func printStack(s *stack, sep string) []string { - var str []string - for _, f := range *s { - frame := frame(f) - str = append(str, printFrame(&frame, sep)) - } - return str -} diff --git a/printer_test.go b/printer_test.go deleted file mode 100644 index 1d056ca..0000000 --- a/printer_test.go +++ /dev/null @@ -1 +0,0 @@ -package eris_test diff --git a/stack.go b/stack.go index 12d2a17..8ef275c 100644 --- a/stack.go +++ b/stack.go @@ -1,35 +1,54 @@ package eris import ( + "fmt" "runtime" "strings" ) -// frame is a single program counter of a stack frame. -type frame uintptr +// StackFrame stores a frame's runtime information in a human readable format. +type StackFrame struct { + Name string + File string + Line int +} -type stackFrame struct { - name string - file string - line int +func (f *StackFrame) formatFrame(sep string) string { + return fmt.Sprintf("%v%v%v%v%v", f.Name, sep, f.File, sep, f.Line) } +// caller returns a single stack frame. the argument skip is the number of stack frames +// to ascend, with 0 identifying the caller of Caller. func caller(skip int) *frame { pc, _, _, _ := runtime.Caller(skip) var f frame = frame(pc) return &f } +// callers returns a stack trace. the argument skip is the number of stack frames to skip +// before recording in pc, with 0 identifying the frame for Callers itself and 1 identifying +// the caller of Callers. +func callers(skip int) *stack { + const depth = 64 + var pcs [depth]uintptr + n := runtime.Callers(skip, pcs[:]) + var st stack = pcs[0:n] + return &st +} + +// frame is a single program counter of a stack frame. +type frame uintptr + func (f frame) pc() uintptr { return uintptr(f) - 1 } -func (f frame) get() *stackFrame { +func (f frame) get() *StackFrame { fn := runtime.FuncForPC(f.pc()) if fn == nil { - return &stackFrame{ - name: "unknown", - file: "unknown", + return &StackFrame{ + Name: "unknown", + File: "unknown", } } @@ -38,20 +57,22 @@ func (f frame) get() *stackFrame { name = name[i+1:] file, line := fn.FileLine(f.pc()) - return &stackFrame{ - name: name, - file: file, - line: line, + return &StackFrame{ + Name: name, + File: file, + Line: line, } } // stack is an array of program counters. type stack []uintptr -func callers(skip int) *stack { - const depth = 64 - var pcs [depth]uintptr - n := runtime.Callers(skip, pcs[:]) - var st stack = pcs[0:n] - return &st +func (s *stack) get() []StackFrame { + var sFrames []StackFrame + for _, f := range *s { + frame := frame(f) + sFrame := frame.get() + sFrames = append(sFrames, *sFrame) + } + return sFrames } diff --git a/stack_test.go b/stack_test.go deleted file mode 100644 index 1d056ca..0000000 --- a/stack_test.go +++ /dev/null @@ -1 +0,0 @@ -package eris_test