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 (
"fmt"
"reflect"
"regexp"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
......@@ -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.
//
func CmpEquals(opts ...cmp.Option) Checker {
return cmpEquals(testing.Verbose(), opts...)
}
func cmpEquals(verbose bool, opts ...cmp.Option) Checker {
return &cmpEqualsChecker{
argNames: []string{"got", "want"},
opts: opts,
verbose: verbose,
}
}
type cmpEqualsChecker struct {
argNames
opts cmp.Options
verbose bool
}
// Check implements Checker.Check by checking that got == args[0] according to
......@@ -102,9 +109,15 @@ func (c *cmpEqualsChecker) Check(got interface{}, args []interface{}, note func(
}()
want := args[0]
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))
return ErrSilent
}
return nil
}
......
......@@ -15,7 +15,15 @@ import (
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 (
sameInts = cmpopts.SortSlices(func(x, y int) bool {
......@@ -189,7 +197,7 @@ want args:
`,
}, {
about: "CmpEquals: same values",
checker: qt.CmpEquals(),
checker: qt.InternalCmpEquals(false),
got: cmpEqualsGot,
args: []interface{}{cmpEqualsGot},
expectedNegateFailure: `
......@@ -208,7 +216,18 @@ want:
`,
}, {
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,
args: []interface{}{cmpEqualsWant},
expectedCheckFailure: fmt.Sprintf(`
......@@ -235,7 +254,7 @@ want:
`, diff(cmpEqualsGot, cmpEqualsWant)),
}, {
about: "CmpEquals: same values with options",
checker: qt.CmpEquals(sameInts),
checker: qt.InternalCmpEquals(false, sameInts),
got: []int{1, 2, 3},
args: []interface{}{
[]int{3, 2, 1},
......@@ -250,7 +269,20 @@ want:
`,
}, {
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},
args: []interface{}{
[]int{3, 2, 1},
......@@ -267,7 +299,7 @@ want:
`, diff([]int{1, 2, 4}, []int{3, 2, 1}, sameInts)),
}, {
about: "CmpEquals: structs with unexported fields not allowed",
checker: qt.CmpEquals(),
checker: qt.InternalCmpEquals(false),
got: struct{ answer int }{
answer: 42,
},
......@@ -287,7 +319,7 @@ want:
`,
}, {
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 }{
answer: 42,
},
......@@ -305,82 +337,8 @@ want:
<same as "got">
`,
}, {
about: "CmpEquals: not enough arguments",
checker: qt.CmpEquals(),
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,
about: "CmpEquals: same times",
checker: qt.InternalCmpEquals(false),
got: goTime,
args: []interface{}{
goTime,
......@@ -394,8 +352,8 @@ want:
<same as "got">
`,
}, {
about: "DeepEquals: different times",
checker: qt.DeepEquals,
about: "CmpEquals: different times: verbose",
checker: qt.InternalCmpEquals(true),
got: goTime.Add(24 * time.Hour),
args: []interface{}{
goTime,
......@@ -411,8 +369,8 @@ want:
s"2012-03-28 00:00:00 +0000 UTC"
`, diff(goTime.Add(24*time.Hour), goTime)),
}, {
about: "DeepEquals: not enough arguments",
checker: qt.DeepEquals,
about: "CmpEquals: not enough arguments",
checker: qt.InternalCmpEquals(false),
expectedCheckFailure: `
error:
bad check: not enough arguments provided to checker: got 0, want 1
......@@ -426,16 +384,17 @@ want args:
want
`,
}, {
about: "DeepEquals: too many arguments",
checker: qt.DeepEquals,
args: []interface{}{nil, nil},
about: "CmpEquals: too many arguments",
checker: qt.InternalCmpEquals(false),
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 {}{
nil,
nil,
[]int{42},
"bad wolf",
}
want args:
want
......@@ -445,8 +404,8 @@ error:
bad check: too many arguments provided to checker: got 2, want 1
got args:
[]interface {}{
nil,
nil,
[]int{42},
"bad wolf",
}
want args:
want
......@@ -585,13 +544,6 @@ error:
values are not deep equal
diff (-got +want):
%s
got:
[]string{"bad", "wolf"}
want:
[]interface {}{
"bad",
"wolf",
}
`, diff([]string{"bad", "wolf"}, []interface{}{"bad", "wolf"})),
}, {
about: "ContentEquals: not enough arguments",
......@@ -697,7 +649,8 @@ error:
got value:
s"voyages"
regexp:
"these are the voyages"`,
"these are the voyages"
`,
}, {
about: "Matches: empty pattern",
checker: qt.Matches,
......@@ -731,10 +684,16 @@ regexp:
args: []interface{}{"("},
expectedCheckFailure: `
error:
bad check: cannot compile regexp: error parsing regexp: missing closing ):`,
bad check: cannot compile regexp: error parsing regexp: missing closing ): ` + "`^(()$`" + `
regexp:
"("
`,
expectedNegateFailure: `
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",
checker: qt.Matches,
......@@ -888,10 +847,16 @@ regexp:
args: []interface{}{"("},
expectedCheckFailure: `
error:
bad check: cannot compile regexp: error parsing regexp: missing closing ):`,
bad check: cannot compile regexp: error parsing regexp: missing closing ): ` + "`^(()$`" + `
regexp:
"("
`,
expectedNegateFailure: `
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",
checker: qt.ErrorMatches,
......@@ -1067,10 +1032,20 @@ regexp:
args: []interface{}{"("},
expectedCheckFailure: `
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: `
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",
checker: qt.PanicMatches,
......@@ -1294,20 +1269,18 @@ want length:
}, {
about: "HasLen: channels with the same length",
checker: qt.HasLen,
got: func() chan int {
ch := make(chan int, 4)
ch <- 42
ch <- 47
return ch
}(),
got: chInt,
args: []interface{}{2},
expectedNegateFailure: `
expectedNegateFailure: fmt.Sprintf(`
error:
unexpected success
len(got):
int(2)
got:
(chan int)`,
(chan int)(%v)
want length:
<same as "len(got)">
`, chInt),
}, {
about: "HasLen: maps with the same length",
checker: qt.HasLen,
......@@ -1371,15 +1344,18 @@ want length:
}, {
about: "HasLen: channels with different lengths",
checker: qt.HasLen,
got: make(chan struct{}),
args: []interface{}{2},
expectedCheckFailure: `
got: chInt,
args: []interface{}{4},
expectedCheckFailure: fmt.Sprintf(`
error:
unexpected length
len(got):
int(0)
int(2)
got:
(chan struct {})`,
(chan int)(%v)
want length:
int(4)
`, chInt),
}, {
about: "HasLen: maps with different lengths",
checker: qt.HasLen,
......
......@@ -2,4 +2,7 @@
package quicktest
var Prefixf = prefixf
var (
InternalCmpEquals = cmpEquals
Prefixf = prefixf
)
......@@ -513,7 +513,7 @@ func TestCRunDefer(t *testing.T) {
func checkResult(t *testing.T, ok bool, got, want string) {
if want != "" {
assertPrefix(t, got, want)
assertPrefix(t, got, want+"stack:\n")
assertBool(t, ok, false)
return
}
......
......@@ -3,15 +3,16 @@
package quicktest
import (
"bufio"
"bytes"
"fmt"
"go/ast"
"go/parser"
"go/printer"
"go/token"
"io"
"os"
"path/filepath"
"reflect"
"runtime"
"strings"
"text/tabwriter"
)
// reportParams holds parameters for reporting a test error.
......@@ -43,7 +44,7 @@ func report(err error, p reportParams) string {
var buf bytes.Buffer
buf.WriteByte('\n')
writeError(&buf, err, p)
writeInvocation(&buf)
writeStack(&buf)
return buf.String()
}
......@@ -93,51 +94,81 @@ func writeError(w io.Writer, err error, p reportParams) {
}
}
// writeInvocation writes the source code context for the current failure into
// the provided writer.
func writeInvocation(w io.Writer) {
fmt.Fprintln(w, "sources:")
// TODO: we can do better than 4.
_, file, line, ok := runtime.Caller(4)
if !ok {
fmt.Fprint(w, prefixf(prefix, "<invocation not available>"))
return
// writeStack writes the traceback information for the current failure into the
// provided writer.
func writeStack(w io.Writer) {
fmt.Fprintln(w, "stack:")
pc := make([]uintptr, 8)
sg := &stmtGetter{
fset: token.NewFileSet(),
files: make(map[string]*ast.File, 8),
config: &printer.Config{
Mode: printer.UseSpaces,
Tabwidth: 4,
},
}
runtime.Callers(5, pc)
frames := runtime.CallersFrames(pc)
thisPackage := reflect.TypeOf(C{}).PkgPath() + "."
for {
frame, more := frames.Next()
if strings.HasPrefix(frame.Function, "testing.") || strings.HasPrefix(frame.Function, thisPackage) {
// Do not include stdlib test runner and quicktest checker calls.
break
}
fmt.Fprint(w, prefixf(prefix, "%s:%d:", filepath.Base(file), line))
prefix := prefix + prefix
f, err := os.Open(file)
fmt.Fprint(w, prefixf(prefix, "%s:%d", frame.File, frame.Line))
stmt, err := sg.Get(frame.File, frame.Line)
if err != nil {
fmt.Fprint(w, prefixf(prefix, "<cannot open source file: %s>", err))
return
fmt.Fprint(w, prefixf(prefix+prefix, "<%s>", err))
} else {
fmt.Fprint(w, prefixf(prefix+prefix, "%s", stmt))
}
defer f.Close()
var current int
var found bool
tw := tabwriter.NewWriter(w, 0, 0, 2, ' ', 0)
sc := bufio.NewScanner(f)
for sc.Scan() {
current++
if current > line+contextLines {
if !more {
// There are no more callers.
break
}
if current < line-contextLines {
continue
}
linePrefix := fmt.Sprintf("%s%d", prefix, current)
if current == line {
found = true
linePrefix += "!"
}
fmt.Fprint(tw, prefixf(linePrefix+"\t", "%s", sc.Text()))
}
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)
}
tw.Flush()
if err = sc.Err(); err != nil {
fmt.Fprint(w, prefixf(prefix, "<cannot scan source file: %s>", err))
return
sg.files[file] = f
}
if !found {
fmt.Fprint(w, prefixf(prefix, "<cannot find source lines>"))
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
......@@ -159,10 +190,5 @@ type note struct {
value interface{}
}
const (
// contextLines holds the number of lines of code to show when showing a
// failure context.
contextLines = 3
// prefix is the string used to indent blocks of output.
prefix = " "
)
// prefix is the string used to indent blocks of output.
const prefix = " "
......@@ -3,48 +3,111 @@
package quicktest_test
import (
"runtime"
"strconv"
"strings"
"testing"
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{}
c := qt.New(tt)
// Context line #1.
// Context line #2.
// Context line #3.
c.Assert(42, qt.Equals, 47)
// Context line #4.
// Context line #5.
// Context line #6.
codeOutput := strings.Replace(tt.fatalString(), "\t", " ", -1)
if codeOutput != expectedCodeOutput {
t.Fatalf(`failure:
------------------------------ got ------------------------------
%s------------------------------ want -----------------------------
%s-----------------------------------------------------------------`,
codeOutput, expectedCodeOutput)
}
}
var expectedCodeOutput = `
want := `
error:
values are not equal
got:
int(42)
want:
int(47)
sources:
report_test.go:20:
17 // Context line #1.
18 // Context line #2.
19 // Context line #3.
20! c.Assert(42, qt.Equals, 47)
21 // Context line #4.
22 // Context line #5.
23 // Context line #6.
stack:
$file:19
c.Assert(42, qt.Equals, 47)
`
assertReport(t, tt, want)
}
func f1(c *qt.C) {
f2(c)
}
func f2(c *qt.C) {
c.Assert(42, qt.IsNil) // Real assertion here!
}
func TestIndirectReportOutput(t *testing.T) {
tt := &testingT{}
c := qt.New(tt)
f1(c)
want := `
error:
42 is not nil
got:
int(42)
stack:
$file:39
c.Assert(42, qt.IsNil)
$file:35
f2(c)
$file:45
f1(c)
`
assertReport(t, tt, want)
}
func TestMultilineReportOutput(t *testing.T) {
tt := &testingT{}
c := qt.New(tt)
c.Assert(
"this string", // Comment 1.
qt.Equals,
"another string",
qt.Commentf("a comment"), // Comment 2.
) // Comment 3.
want := `
error:
values are not equal
comment:
a comment
got:
"this string"
want:
"another string"
stack:
$file:$line
c.Assert(
"this string", // Comment 1.
qt.Equals,
"another string",
qt.Commentf("a comment"), // Comment 2.
)
`
assertReport(t, tt, want)
}
func assertReport(t *testing.T, tt *testingT, want string) {
got := strings.Replace(tt.fatalString(), "\t", " ", -1)
// Adjust for file names in different systems.
_, file, _, ok := runtime.Caller(0)
assertBool(t, ok, true)
want = strings.Replace(want, "$file", file, -1)
// Adjust for line number based on Go < v1.9 reporting the line where the
// statement ends.
line := 65
vers := runtime.Version()
if vers == "go1.7" || vers == "go1.8" {
line = 70
}
want = strings.Replace(want, "$line", strconv.Itoa(line), 1)
if got != want {
t.Fatalf(`failure:
------------------------------ got ------------------------------
%s------------------------------ want -----------------------------
%s-----------------------------------------------------------------`,
got, want)
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment