Commit f93101f1 authored by Francesco Banconi's avatar Francesco Banconi

In the error output, replace code context with stack

Also only show values in Cmp/DeepEquals error outputs when "go test -v" is used, in order to reduce noise.
parent fed54e22
...@@ -7,6 +7,7 @@ import ( ...@@ -7,6 +7,7 @@ import (
"fmt" "fmt"
"reflect" "reflect"
"regexp" "regexp"
"testing"
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts" "github.com/google/go-cmp/cmp/cmpopts"
...@@ -78,15 +79,21 @@ func (c *equalsChecker) Check(got interface{}, args []interface{}, note func(key ...@@ -78,15 +79,21 @@ func (c *equalsChecker) Check(got interface{}, args []interface{}, note func(key
// c.Assert(got, qt.CmpEquals(), []int{42, 47}) // Same as qt.DeepEquals. // c.Assert(got, qt.CmpEquals(), []int{42, 47}) // Same as qt.DeepEquals.
// //
func CmpEquals(opts ...cmp.Option) Checker { func CmpEquals(opts ...cmp.Option) Checker {
return cmpEquals(testing.Verbose(), opts...)
}
func cmpEquals(verbose bool, opts ...cmp.Option) Checker {
return &cmpEqualsChecker{ return &cmpEqualsChecker{
argNames: []string{"got", "want"}, argNames: []string{"got", "want"},
opts: opts, opts: opts,
verbose: verbose,
} }
} }
type cmpEqualsChecker struct { type cmpEqualsChecker struct {
argNames argNames
opts cmp.Options opts cmp.Options
verbose bool
} }
// Check implements Checker.Check by checking that got == args[0] according to // Check implements Checker.Check by checking that got == args[0] according to
...@@ -102,8 +109,14 @@ func (c *cmpEqualsChecker) Check(got interface{}, args []interface{}, note func( ...@@ -102,8 +109,14 @@ func (c *cmpEqualsChecker) Check(got interface{}, args []interface{}, note func(
}() }()
want := args[0] want := args[0]
if diff := cmp.Diff(got, want, c.opts...); diff != "" { if diff := cmp.Diff(got, want, c.opts...); diff != "" {
// Only output values when the verbose flag is set.
if c.verbose {
note("diff (-got +want)", Unquoted(diff))
return errors.New("values are not deep equal")
}
note("error", Unquoted("values are not deep equal"))
note("diff (-got +want)", Unquoted(diff)) note("diff (-got +want)", Unquoted(diff))
return errors.New("values are not deep equal") return ErrSilent
} }
return nil return nil
} }
......
...@@ -15,7 +15,15 @@ import ( ...@@ -15,7 +15,15 @@ import (
qt "github.com/frankban/quicktest" qt "github.com/frankban/quicktest"
) )
var goTime = time.Date(2012, 3, 28, 0, 0, 0, 0, time.UTC) var (
goTime = time.Date(2012, 3, 28, 0, 0, 0, 0, time.UTC)
chInt = func() chan int {
ch := make(chan int, 4)
ch <- 42
ch <- 47
return ch
}()
)
var ( var (
sameInts = cmpopts.SortSlices(func(x, y int) bool { sameInts = cmpopts.SortSlices(func(x, y int) bool {
...@@ -189,7 +197,7 @@ want args: ...@@ -189,7 +197,7 @@ want args:
`, `,
}, { }, {
about: "CmpEquals: same values", about: "CmpEquals: same values",
checker: qt.CmpEquals(), checker: qt.InternalCmpEquals(false),
got: cmpEqualsGot, got: cmpEqualsGot,
args: []interface{}{cmpEqualsGot}, args: []interface{}{cmpEqualsGot},
expectedNegateFailure: ` expectedNegateFailure: `
...@@ -208,7 +216,18 @@ want: ...@@ -208,7 +216,18 @@ want:
`, `,
}, { }, {
about: "CmpEquals: different values", about: "CmpEquals: different values",
checker: qt.CmpEquals(), checker: qt.InternalCmpEquals(false),
got: cmpEqualsGot,
args: []interface{}{cmpEqualsWant},
expectedCheckFailure: fmt.Sprintf(`
error:
values are not deep equal
diff (-got +want):
%s
`, diff(cmpEqualsGot, cmpEqualsWant)),
}, {
about: "CmpEquals: different values: verbose",
checker: qt.InternalCmpEquals(true),
got: cmpEqualsGot, got: cmpEqualsGot,
args: []interface{}{cmpEqualsWant}, args: []interface{}{cmpEqualsWant},
expectedCheckFailure: fmt.Sprintf(` expectedCheckFailure: fmt.Sprintf(`
...@@ -235,7 +254,7 @@ want: ...@@ -235,7 +254,7 @@ want:
`, diff(cmpEqualsGot, cmpEqualsWant)), `, diff(cmpEqualsGot, cmpEqualsWant)),
}, { }, {
about: "CmpEquals: same values with options", about: "CmpEquals: same values with options",
checker: qt.CmpEquals(sameInts), checker: qt.InternalCmpEquals(false, sameInts),
got: []int{1, 2, 3}, got: []int{1, 2, 3},
args: []interface{}{ args: []interface{}{
[]int{3, 2, 1}, []int{3, 2, 1},
...@@ -250,7 +269,20 @@ want: ...@@ -250,7 +269,20 @@ want:
`, `,
}, { }, {
about: "CmpEquals: different values with options", about: "CmpEquals: different values with options",
checker: qt.CmpEquals(sameInts), checker: qt.InternalCmpEquals(false, sameInts),
got: []int{1, 2, 4},
args: []interface{}{
[]int{3, 2, 1},
},
expectedCheckFailure: fmt.Sprintf(`
error:
values are not deep equal
diff (-got +want):
%s
`, diff([]int{1, 2, 4}, []int{3, 2, 1}, sameInts)),
}, {
about: "CmpEquals: different values with options: verbose",
checker: qt.InternalCmpEquals(true, sameInts),
got: []int{1, 2, 4}, got: []int{1, 2, 4},
args: []interface{}{ args: []interface{}{
[]int{3, 2, 1}, []int{3, 2, 1},
...@@ -267,7 +299,7 @@ want: ...@@ -267,7 +299,7 @@ want:
`, diff([]int{1, 2, 4}, []int{3, 2, 1}, sameInts)), `, diff([]int{1, 2, 4}, []int{3, 2, 1}, sameInts)),
}, { }, {
about: "CmpEquals: structs with unexported fields not allowed", about: "CmpEquals: structs with unexported fields not allowed",
checker: qt.CmpEquals(), checker: qt.InternalCmpEquals(false),
got: struct{ answer int }{ got: struct{ answer int }{
answer: 42, answer: 42,
}, },
...@@ -287,7 +319,7 @@ want: ...@@ -287,7 +319,7 @@ want:
`, `,
}, { }, {
about: "CmpEquals: structs with unexported fields ignored", about: "CmpEquals: structs with unexported fields ignored",
checker: qt.CmpEquals(cmpopts.IgnoreUnexported(struct{ answer int }{})), checker: qt.InternalCmpEquals(false, cmpopts.IgnoreUnexported(struct{ answer int }{})),
got: struct{ answer int }{ got: struct{ answer int }{
answer: 42, answer: 42,
}, },
...@@ -305,82 +337,8 @@ want: ...@@ -305,82 +337,8 @@ want:
<same as "got"> <same as "got">
`, `,
}, { }, {
about: "CmpEquals: not enough arguments", about: "CmpEquals: same times",
checker: qt.CmpEquals(), checker: qt.InternalCmpEquals(false),
expectedCheckFailure: `
error:
bad check: not enough arguments provided to checker: got 0, want 1
want args:
want
`,
expectedNegateFailure: `
error:
bad check: not enough arguments provided to checker: got 0, want 1
want args:
want
`,
}, {
about: "CmpEquals: too many arguments",
checker: qt.CmpEquals(),
got: []int{42},
args: []interface{}{[]int{42}, "bad wolf"},
expectedCheckFailure: `
error:
bad check: too many arguments provided to checker: got 2, want 1
got args:
[]interface {}{
[]int{42},
"bad wolf",
}
want args:
want
`,
expectedNegateFailure: `
error:
bad check: too many arguments provided to checker: got 2, want 1
got args:
[]interface {}{
[]int{42},
"bad wolf",
}
want args:
want
`,
}, {
about: "DeepEquals: same values",
checker: qt.DeepEquals,
got: []int{1, 2, 3},
args: []interface{}{
[]int{1, 2, 3},
},
expectedNegateFailure: `
error:
unexpected success
got:
[]int{1, 2, 3}
want:
<same as "got">
`,
}, {
about: "DeepEquals: different values",
checker: qt.DeepEquals,
got: []int{1, 2, 3},
args: []interface{}{
[]int{3, 2, 1},
},
expectedCheckFailure: fmt.Sprintf(`
error:
values are not deep equal
diff (-got +want):
%s
got:
[]int{1, 2, 3}
want:
[]int{3, 2, 1}
`, diff([]int{1, 2, 3}, []int{3, 2, 1})),
}, {
about: "DeepEquals: same times",
checker: qt.DeepEquals,
got: goTime, got: goTime,
args: []interface{}{ args: []interface{}{
goTime, goTime,
...@@ -394,8 +352,8 @@ want: ...@@ -394,8 +352,8 @@ want:
<same as "got"> <same as "got">
`, `,
}, { }, {
about: "DeepEquals: different times", about: "CmpEquals: different times: verbose",
checker: qt.DeepEquals, checker: qt.InternalCmpEquals(true),
got: goTime.Add(24 * time.Hour), got: goTime.Add(24 * time.Hour),
args: []interface{}{ args: []interface{}{
goTime, goTime,
...@@ -411,8 +369,8 @@ want: ...@@ -411,8 +369,8 @@ want:
s"2012-03-28 00:00:00 +0000 UTC" s"2012-03-28 00:00:00 +0000 UTC"
`, diff(goTime.Add(24*time.Hour), goTime)), `, diff(goTime.Add(24*time.Hour), goTime)),
}, { }, {
about: "DeepEquals: not enough arguments", about: "CmpEquals: not enough arguments",
checker: qt.DeepEquals, checker: qt.InternalCmpEquals(false),
expectedCheckFailure: ` expectedCheckFailure: `
error: error:
bad check: not enough arguments provided to checker: got 0, want 1 bad check: not enough arguments provided to checker: got 0, want 1
...@@ -426,16 +384,17 @@ want args: ...@@ -426,16 +384,17 @@ want args:
want want
`, `,
}, { }, {
about: "DeepEquals: too many arguments", about: "CmpEquals: too many arguments",
checker: qt.DeepEquals, checker: qt.InternalCmpEquals(false),
args: []interface{}{nil, nil}, got: []int{42},
args: []interface{}{[]int{42}, "bad wolf"},
expectedCheckFailure: ` expectedCheckFailure: `
error: error:
bad check: too many arguments provided to checker: got 2, want 1 bad check: too many arguments provided to checker: got 2, want 1
got args: got args:
[]interface {}{ []interface {}{
nil, []int{42},
nil, "bad wolf",
} }
want args: want args:
want want
...@@ -445,8 +404,8 @@ error: ...@@ -445,8 +404,8 @@ error:
bad check: too many arguments provided to checker: got 2, want 1 bad check: too many arguments provided to checker: got 2, want 1
got args: got args:
[]interface {}{ []interface {}{
nil, []int{42},
nil, "bad wolf",
} }
want args: want args:
want want
...@@ -585,13 +544,6 @@ error: ...@@ -585,13 +544,6 @@ error:
values are not deep equal values are not deep equal
diff (-got +want): diff (-got +want):
%s %s
got:
[]string{"bad", "wolf"}
want:
[]interface {}{
"bad",
"wolf",
}
`, diff([]string{"bad", "wolf"}, []interface{}{"bad", "wolf"})), `, diff([]string{"bad", "wolf"}, []interface{}{"bad", "wolf"})),
}, { }, {
about: "ContentEquals: not enough arguments", about: "ContentEquals: not enough arguments",
...@@ -697,7 +649,8 @@ error: ...@@ -697,7 +649,8 @@ error:
got value: got value:
s"voyages" s"voyages"
regexp: regexp:
"these are the voyages"`, "these are the voyages"
`,
}, { }, {
about: "Matches: empty pattern", about: "Matches: empty pattern",
checker: qt.Matches, checker: qt.Matches,
...@@ -731,10 +684,16 @@ regexp: ...@@ -731,10 +684,16 @@ regexp:
args: []interface{}{"("}, args: []interface{}{"("},
expectedCheckFailure: ` expectedCheckFailure: `
error: error:
bad check: cannot compile regexp: error parsing regexp: missing closing ):`, bad check: cannot compile regexp: error parsing regexp: missing closing ): ` + "`^(()$`" + `
regexp:
"("
`,
expectedNegateFailure: ` expectedNegateFailure: `
error: error:
bad check: cannot compile regexp: error parsing regexp: missing closing ):`, bad check: cannot compile regexp: error parsing regexp: missing closing ): ` + "`^(()$`" + `
regexp:
"("
`,
}, { }, {
about: "Matches: pattern not a string", about: "Matches: pattern not a string",
checker: qt.Matches, checker: qt.Matches,
...@@ -888,10 +847,16 @@ regexp: ...@@ -888,10 +847,16 @@ regexp:
args: []interface{}{"("}, args: []interface{}{"("},
expectedCheckFailure: ` expectedCheckFailure: `
error: error:
bad check: cannot compile regexp: error parsing regexp: missing closing ):`, bad check: cannot compile regexp: error parsing regexp: missing closing ): ` + "`^(()$`" + `
regexp:
"("
`,
expectedNegateFailure: ` expectedNegateFailure: `
error: error:
bad check: cannot compile regexp: error parsing regexp: missing closing ):`, bad check: cannot compile regexp: error parsing regexp: missing closing ): ` + "`^(()$`" + `
regexp:
"("
`,
}, { }, {
about: "ErrorMatches: pattern not a string", about: "ErrorMatches: pattern not a string",
checker: qt.ErrorMatches, checker: qt.ErrorMatches,
...@@ -1067,10 +1032,20 @@ regexp: ...@@ -1067,10 +1032,20 @@ regexp:
args: []interface{}{"("}, args: []interface{}{"("},
expectedCheckFailure: ` expectedCheckFailure: `
error: error:
bad check: cannot compile regexp: error parsing regexp: missing closing ):`, bad check: cannot compile regexp: error parsing regexp: missing closing ): ` + "`^(()$`" + `
panic value:
"error: bad wolf"
regexp:
"("
`,
expectedNegateFailure: ` expectedNegateFailure: `
error: error:
bad check: cannot compile regexp: error parsing regexp: missing closing ):`, bad check: cannot compile regexp: error parsing regexp: missing closing ): ` + "`^(()$`" + `
panic value:
"error: bad wolf"
regexp:
"("
`,
}, { }, {
about: "PanicMatches: pattern not a string", about: "PanicMatches: pattern not a string",
checker: qt.PanicMatches, checker: qt.PanicMatches,
...@@ -1294,20 +1269,18 @@ want length: ...@@ -1294,20 +1269,18 @@ want length:
}, { }, {
about: "HasLen: channels with the same length", about: "HasLen: channels with the same length",
checker: qt.HasLen, checker: qt.HasLen,
got: func() chan int { got: chInt,
ch := make(chan int, 4) args: []interface{}{2},
ch <- 42 expectedNegateFailure: fmt.Sprintf(`
ch <- 47
return ch
}(),
args: []interface{}{2},
expectedNegateFailure: `
error: error:
unexpected success unexpected success
len(got): len(got):
int(2) int(2)
got: got:
(chan int)`, (chan int)(%v)
want length:
<same as "len(got)">
`, chInt),
}, { }, {
about: "HasLen: maps with the same length", about: "HasLen: maps with the same length",
checker: qt.HasLen, checker: qt.HasLen,
...@@ -1371,15 +1344,18 @@ want length: ...@@ -1371,15 +1344,18 @@ want length:
}, { }, {
about: "HasLen: channels with different lengths", about: "HasLen: channels with different lengths",
checker: qt.HasLen, checker: qt.HasLen,
got: make(chan struct{}), got: chInt,
args: []interface{}{2}, args: []interface{}{4},
expectedCheckFailure: ` expectedCheckFailure: fmt.Sprintf(`
error: error:
unexpected length unexpected length
len(got): len(got):
int(0) int(2)
got: got:
(chan struct {})`, (chan int)(%v)
want length:
int(4)
`, chInt),
}, { }, {
about: "HasLen: maps with different lengths", about: "HasLen: maps with different lengths",
checker: qt.HasLen, checker: qt.HasLen,
......
...@@ -2,4 +2,7 @@ ...@@ -2,4 +2,7 @@
package quicktest package quicktest
var Prefixf = prefixf var (
InternalCmpEquals = cmpEquals
Prefixf = prefixf
)
...@@ -513,7 +513,7 @@ func TestCRunDefer(t *testing.T) { ...@@ -513,7 +513,7 @@ func TestCRunDefer(t *testing.T) {
func checkResult(t *testing.T, ok bool, got, want string) { func checkResult(t *testing.T, ok bool, got, want string) {
if want != "" { if want != "" {
assertPrefix(t, got, want) assertPrefix(t, got, want+"stack:\n")
assertBool(t, ok, false) assertBool(t, ok, false)
return return
} }
......
...@@ -3,15 +3,16 @@ ...@@ -3,15 +3,16 @@
package quicktest package quicktest
import ( import (
"bufio"
"bytes" "bytes"
"fmt" "fmt"
"go/ast"
"go/parser"
"go/printer"
"go/token"
"io" "io"
"os" "reflect"
"path/filepath"
"runtime" "runtime"
"strings" "strings"
"text/tabwriter"
) )
// reportParams holds parameters for reporting a test error. // reportParams holds parameters for reporting a test error.
...@@ -43,7 +44,7 @@ func report(err error, p reportParams) string { ...@@ -43,7 +44,7 @@ func report(err error, p reportParams) string {
var buf bytes.Buffer var buf bytes.Buffer
buf.WriteByte('\n') buf.WriteByte('\n')
writeError(&buf, err, p) writeError(&buf, err, p)
writeInvocation(&buf) writeStack(&buf)
return buf.String() return buf.String()
} }
...@@ -93,51 +94,81 @@ func writeError(w io.Writer, err error, p reportParams) { ...@@ -93,51 +94,81 @@ func writeError(w io.Writer, err error, p reportParams) {
} }
} }
// writeInvocation writes the source code context for the current failure into // writeStack writes the traceback information for the current failure into the
// the provided writer. // provided writer.
func writeInvocation(w io.Writer) { func writeStack(w io.Writer) {
fmt.Fprintln(w, "sources:") fmt.Fprintln(w, "stack:")
// TODO: we can do better than 4. pc := make([]uintptr, 8)
_, file, line, ok := runtime.Caller(4) sg := &stmtGetter{
if !ok { fset: token.NewFileSet(),
fmt.Fprint(w, prefixf(prefix, "<invocation not available>")) files: make(map[string]*ast.File, 8),
return config: &printer.Config{
} Mode: printer.UseSpaces,
fmt.Fprint(w, prefixf(prefix, "%s:%d:", filepath.Base(file), line)) Tabwidth: 4,
prefix := prefix + prefix },
f, err := os.Open(file)
if err != nil {
fmt.Fprint(w, prefixf(prefix, "<cannot open source file: %s>", err))
return
} }
defer f.Close() runtime.Callers(5, pc)
var current int frames := runtime.CallersFrames(pc)
var found bool thisPackage := reflect.TypeOf(C{}).PkgPath() + "."
tw := tabwriter.NewWriter(w, 0, 0, 2, ' ', 0) for {
sc := bufio.NewScanner(f) frame, more := frames.Next()
for sc.Scan() { if strings.HasPrefix(frame.Function, "testing.") || strings.HasPrefix(frame.Function, thisPackage) {
current++ // Do not include stdlib test runner and quicktest checker calls.
if current > line+contextLines {
break break
} }
if current < line-contextLines { fmt.Fprint(w, prefixf(prefix, "%s:%d", frame.File, frame.Line))
continue stmt, err := sg.Get(frame.File, frame.Line)
if err != nil {
fmt.Fprint(w, prefixf(prefix+prefix, "<%s>", err))
} else {
fmt.Fprint(w, prefixf(prefix+prefix, "%s", stmt))
} }
linePrefix := fmt.Sprintf("%s%d", prefix, current) if !more {
if current == line { // There are no more callers.
found = true break
linePrefix += "!"
} }
fmt.Fprint(tw, prefixf(linePrefix+"\t", "%s", sc.Text()))
}
tw.Flush()
if err = sc.Err(); err != nil {
fmt.Fprint(w, prefixf(prefix, "<cannot scan source file: %s>", err))
return
} }
if !found { }
fmt.Fprint(w, prefixf(prefix, "<cannot find source lines>"))
type stmtGetter struct {
fset *token.FileSet
files map[string]*ast.File
config *printer.Config
}
// Get returns the lines of code of the statement at the given file and line.
func (sg *stmtGetter) Get(file string, line int) (string, error) {
f := sg.files[file]
if f == nil {
var err error
f, err = parser.ParseFile(sg.fset, file, nil, parser.ParseComments)
if err != nil {
return "", fmt.Errorf("cannot parse source file: %s", err)
}
sg.files[file] = f
} }
var stmt string
ast.Inspect(f, func(n ast.Node) bool {
if n == nil || stmt != "" {
return false
}
pos := sg.fset.Position(n.Pos()).Line
end := sg.fset.Position(n.End()).Line
// Go < v1.9 reports the line where the statements ends, not the line
// where it begins.
if line == pos || line == end {
var buf bytes.Buffer
// TODO: include possible comment after the statement.
sg.config.Fprint(&buf, sg.fset, &printer.CommentedNode{
Node: n,
Comments: f.Comments,
})
stmt = buf.String()
return false
}
return pos < line && line <= end
})
return stmt, nil
} }
// prefixf formats the given string with the given args. It also inserts the // prefixf formats the given string with the given args. It also inserts the
...@@ -159,10 +190,5 @@ type note struct { ...@@ -159,10 +190,5 @@ type note struct {
value interface{} value interface{}
} }
const ( // prefix is the string used to indent blocks of output.
// contextLines holds the number of lines of code to show when showing a const prefix = " "
// failure context.
contextLines = 3
// prefix is the string used to indent blocks of output.
prefix = " "
)
...@@ -3,48 +3,111 @@ ...@@ -3,48 +3,111 @@
package quicktest_test package quicktest_test
import ( import (
"runtime"
"strconv"
"strings" "strings"
"testing" "testing"
qt "github.com/frankban/quicktest" qt "github.com/frankban/quicktest"
) )
// This test lives in its own file as it relies on its own source code lines. // The tests in this file rely on their own source code lines.
func TestCodeOutput(t *testing.T) { func TestReportOutput(t *testing.T) {
tt := &testingT{} tt := &testingT{}