diff --git a/examples/db/api_test.go b/examples/db/api_test.go index 76ab7b1..b40b9b7 100644 --- a/examples/db/api_test.go +++ b/examples/db/api_test.go @@ -8,6 +8,7 @@ import ( "net/http/httptest" "strings" + txdb "github.com/DATA-DOG/go-txdb" "github.com/DATA-DOG/godog" "github.com/DATA-DOG/godog/gherkin" ) diff --git a/fmt_pretty.go b/fmt_pretty.go index 9d317b1..153ab5a 100644 --- a/fmt_pretty.go +++ b/fmt_pretty.go @@ -308,7 +308,7 @@ func (f *pretty) printStepKind(res *stepResult) { f.printStep(res.step, res.def, res.typ.clr()) if res.err != nil { - fmt.Fprintln(f.out, s(f.indent*2)+redb(res.err)) + fmt.Fprintln(f.out, s(f.indent*2)+redb(fmt.Sprintf("%+v", res.err))) } if res.typ == pending { fmt.Fprintln(f.out, s(f.indent*3)+yellow("TODO: write pending definition")) diff --git a/fmt_progress.go b/fmt_progress.go index 8a20b1c..ba47b11 100644 --- a/fmt_progress.go +++ b/fmt_progress.go @@ -59,7 +59,7 @@ func (f *progress) Summary() { fmt.Fprintln(f.out, "\n--- "+red("Failed steps:")+"\n") for _, fail := range f.failed { fmt.Fprintln(f.out, s(4)+red(fail.step.Keyword+" "+fail.step.Text)+black(" # "+fail.line())) - fmt.Fprintln(f.out, s(6)+red("Error: ")+redb(fail.err)+"\n") + fmt.Fprintln(f.out, s(6)+red("Error: ")+redb(fmt.Sprintf("%+v", fail.err))+"\n") } } f.basefmt.Summary() diff --git a/stacktrace.go b/stacktrace.go new file mode 100644 index 0000000..03d2835 --- /dev/null +++ b/stacktrace.go @@ -0,0 +1,132 @@ +package godog + +import ( + "fmt" + "io" + "path" + "runtime" + "strings" +) + +// Frame represents a program counter inside a stack frame. +type stackFrame uintptr + +// pc returns the program counter for this frame; +// multiple frames may have the same PC value. +func (f stackFrame) pc() uintptr { return uintptr(f) - 1 } + +// file returns the full path to the file that contains the +// function for this Frame's pc. +func (f stackFrame) file() string { + fn := runtime.FuncForPC(f.pc()) + if fn == nil { + return "unknown" + } + file, _ := fn.FileLine(f.pc()) + return file +} + +// line returns the line number of source code of the +// function for this Frame's pc. +func (f stackFrame) line() int { + fn := runtime.FuncForPC(f.pc()) + if fn == nil { + return 0 + } + _, line := fn.FileLine(f.pc()) + return line +} + +// Format formats the frame according to the fmt.Formatter interface. +// +// %s source file +// %d source line +// %n function name +// %v equivalent to %s:%d +// +// Format accepts flags that alter the printing of some verbs, as follows: +// +// %+s path of source file relative to the compile time GOPATH +// %+v equivalent to %+s:%d +func (f stackFrame) Format(s fmt.State, verb rune) { + funcname := func(name string) string { + i := strings.LastIndex(name, "/") + name = name[i+1:] + i = strings.Index(name, ".") + return name[i+1:] + } + + switch verb { + case 's': + switch { + case s.Flag('+'): + pc := f.pc() + fn := runtime.FuncForPC(pc) + if fn == nil { + io.WriteString(s, "unknown") + } else { + file, _ := fn.FileLine(pc) + fmt.Fprintf(s, "%s\n\t%s", fn.Name(), file) + } + default: + io.WriteString(s, path.Base(f.file())) + } + case 'd': + fmt.Fprintf(s, "%d", f.line()) + case 'n': + name := runtime.FuncForPC(f.pc()).Name() + io.WriteString(s, funcname(name)) + case 'v': + f.Format(s, 's') + io.WriteString(s, ":") + f.Format(s, 'd') + } +} + +// stack represents a stack of program counters. +type stack []uintptr + +func (s *stack) Format(st fmt.State, verb rune) { + switch verb { + case 'v': + switch { + case st.Flag('+'): + for _, pc := range *s { + f := stackFrame(pc) + fmt.Fprintf(st, "\n%+v", f) + } + } + } +} + +func callStack() *stack { + const depth = 32 + var pcs [depth]uintptr + n := runtime.Callers(3, pcs[:]) + var st stack = pcs[0:n] + return &st +} + +// fundamental is an error that has a message and a stack, but no caller. +type traceError struct { + msg string + *stack +} + +func (f *traceError) Error() string { return f.msg } + +func (f *traceError) Format(s fmt.State, verb rune) { + switch verb { + case 'v': + if s.Flag('+') { + io.WriteString(s, f.msg) + f.stack.Format(s, verb) + return + } + fallthrough + case 's': + io.WriteString(s, f.msg) + case 'q': + fmt.Fprintf(s, "%q", f.msg) + } +} diff --git a/suite.go b/suite.go index 7682bc4..55fd057 100644 --- a/suite.go +++ b/suite.go @@ -220,10 +220,9 @@ func (s *Suite) runStep(step *gherkin.Step, prevStepErr error) (err error) { defer func() { if e := recover(); e != nil { - var ok bool - err, ok = e.(error) - if !ok { - err = fmt.Errorf("%v", e) + err = &traceError{ + msg: fmt.Sprintf("%v", e), + stack: callStack(), } } switch err { diff --git a/suite_test.go b/suite_test.go index 4907516..6c28314 100644 --- a/suite_test.go +++ b/suite_test.go @@ -287,6 +287,7 @@ func (s *suiteContext) iRunFeatureSuite() error { s.testedSuite.fmt = testFormatterFunc("godog", &s.out) s.testedSuite.run() s.testedSuite.fmt.Summary() + return nil }