Unverified Commit fed54e22 authored by Francesco Banconi's avatar Francesco Banconi Committed by GitHub

Merge pull request #35 from frankban/formatter

Add ability to provide custom format function for values
parents fff1a5b7 c5512f4c
......@@ -61,6 +61,9 @@ func (c *equalsChecker) Check(got interface{}, args []interface{}, note func(key
}
}()
if want := args[0]; got != want {
if _, ok := got.(error); ok && want == nil {
return errors.New("got non-nil error")
}
return errors.New("values are not equal")
}
return nil
......@@ -142,7 +145,6 @@ func (c *matchesChecker) Check(got interface{}, args []interface{}, note func(ke
case string:
return match(v, pattern, "value does not match regexp", note)
case fmt.Stringer:
note("value.String()", v.String())
return match(v.String(), pattern, "value.String() does not match regexp", note)
}
note("value", got)
......@@ -174,7 +176,6 @@ func (c *errorMatchesChecker) Check(got interface{}, args []interface{}, note fu
if err == nil {
return errors.New("no error found")
}
note("error message", err.Error())
return match(err.Error(), args[0], "error does not match regexp", note)
}
......@@ -326,9 +327,7 @@ func (c *satisfiesChecker) Check(got interface{}, args []interface{}, note func(
note("predicate function", predicate)
return BadCheckf("cannot use value of type %v as type %v in argument to predicate function", v.Type(), t)
}
result := f.Call([]reflect.Value{v})[0].Interface().(bool)
note("result", result)
if result {
if f.Call([]reflect.Value{v})[0].Interface().(bool) {
return nil
}
return fmt.Errorf("value does not satisfy predicate function")
......
......@@ -4,9 +4,10 @@ package quicktest_test
import (
"bytes"
"errors"
"fmt"
"strings"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
......@@ -14,6 +15,8 @@ import (
qt "github.com/frankban/quicktest"
)
var goTime = time.Date(2012, 3, 28, 0, 0, 0, 0, time.UTC)
var (
sameInts = cmpopts.SortSlices(func(x, y int) bool {
return x < y
......@@ -80,6 +83,33 @@ got:
want:
"42"
`,
}, {
about: "Equals: nil and nil",
checker: qt.Equals,
got: nil,
args: []interface{}{nil},
expectedNegateFailure: `
error:
unexpected success
got:
nil
want:
<same as "got">
`,
}, {
about: "Equals: error is not nil",
checker: qt.Equals,
got: errBadWolf,
args: []interface{}{nil},
expectedCheckFailure: `
error:
got non-nil error
got:
bad wolf
file:line
want:
nil
`,
}, {
about: "Equals: nil struct",
checker: qt.Equals,
......@@ -181,11 +211,11 @@ want:
checker: qt.CmpEquals(),
got: cmpEqualsGot,
args: []interface{}{cmpEqualsWant},
expectedCheckFailure: `
expectedCheckFailure: fmt.Sprintf(`
error:
values are not deep equal
diff (-got +want):
` + diff(cmpEqualsGot, cmpEqualsWant) + `
%s
got:
struct { Strings []interface {}; Ints []int }{
Strings: {
......@@ -202,7 +232,7 @@ want:
},
Ints: {42},
}
`,
`, diff(cmpEqualsGot, cmpEqualsWant)),
}, {
about: "CmpEquals: same values with options",
checker: qt.CmpEquals(sameInts),
......@@ -225,16 +255,16 @@ want:
args: []interface{}{
[]int{3, 2, 1},
},
expectedCheckFailure: `
expectedCheckFailure: fmt.Sprintf(`
error:
values are not deep equal
diff (-got +want):
` + diff([]int{1, 2, 4}, []int{3, 2, 1}, sameInts) + `
%s
got:
[]int{1, 2, 4}
want:
[]int{3, 2, 1}
`,
`, diff([]int{1, 2, 4}, []int{3, 2, 1}, sameInts)),
}, {
about: "CmpEquals: structs with unexported fields not allowed",
checker: qt.CmpEquals(),
......@@ -338,16 +368,48 @@ want:
args: []interface{}{
[]int{3, 2, 1},
},
expectedCheckFailure: `
expectedCheckFailure: fmt.Sprintf(`
error:
values are not deep equal
diff (-got +want):
` + diff([]int{1, 2, 3}, []int{3, 2, 1}) + `
%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,
args: []interface{}{
goTime,
},
expectedNegateFailure: `
error:
unexpected success
got:
s"2012-03-28 00:00:00 +0000 UTC"
want:
<same as "got">
`,
}, {
about: "DeepEquals: different times",
checker: qt.DeepEquals,
got: goTime.Add(24 * time.Hour),
args: []interface{}{
goTime,
},
expectedCheckFailure: fmt.Sprintf(`
error:
values are not deep equal
diff (-got +want):
%s
got:
s"2012-03-29 00:00:00 +0000 UTC"
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,
......@@ -518,11 +580,11 @@ want:
args: []interface{}{
[]interface{}{"bad", "wolf"},
},
expectedCheckFailure: `
expectedCheckFailure: fmt.Sprintf(`
error:
values are not deep equal
diff (-got +want):
` + diff([]string{"bad", "wolf"}, []interface{}{"bad", "wolf"}) + `
%s
got:
[]string{"bad", "wolf"}
want:
......@@ -530,7 +592,7 @@ want:
"bad",
"wolf",
}
`,
`, diff([]string{"bad", "wolf"}, []interface{}{"bad", "wolf"})),
}, {
about: "ContentEquals: not enough arguments",
checker: qt.ContentEquals,
......@@ -606,10 +668,11 @@ regexp:
expectedNegateFailure: `
error:
unexpected success
value.String():
"resistance is futile"
got value:
&bytes.Buffer{`,
s"resistance is futile"
regexp:
"resistance is (futile|useful)"
`,
}, {
about: "Matches: mismatch",
checker: qt.Matches,
......@@ -631,10 +694,10 @@ regexp:
expectedCheckFailure: `
error:
value.String() does not match regexp
value.String():
"voyages"
got value:
&bytes.Buffer{`,
s"voyages"
regexp:
"these are the voyages"`,
}, {
about: "Matches: empty pattern",
checker: qt.Matches,
......@@ -751,82 +814,77 @@ want args:
}, {
about: "ErrorMatches: perfect match",
checker: qt.ErrorMatches,
got: errors.New("error: bad wolf"),
args: []interface{}{"error: bad wolf"},
got: errBadWolf,
args: []interface{}{"bad wolf"},
expectedNegateFailure: `
error:
unexpected success
error message:
"error: bad wolf"
got error:
&errors.errorString{s:"error: bad wolf"}
bad wolf
file:line
regexp:
<same as "error message">
"bad wolf"
`,
}, {
about: "ErrorMatches: match",
checker: qt.ErrorMatches,
got: errors.New("error: bad wolf"),
args: []interface{}{"error: .*"},
got: errBadWolf,
args: []interface{}{"bad .*"},
expectedNegateFailure: `
error:
unexpected success
error message:
"error: bad wolf"
got error:
&errors.errorString{s:"error: bad wolf"}
bad wolf
file:line
regexp:
"error: .*"
"bad .*"
`,
}, {
about: "ErrorMatches: mismatch",
checker: qt.ErrorMatches,
got: errors.New("error: bad wolf"),
args: []interface{}{"error: exterminate"},
got: errBadWolf,
args: []interface{}{"exterminate"},
expectedCheckFailure: `
error:
error does not match regexp
error message:
"error: bad wolf"
got error:
&errors.errorString{s:"error: bad wolf"}
bad wolf
file:line
regexp:
"error: exterminate"
"exterminate"
`,
}, {
about: "ErrorMatches: empty pattern",
checker: qt.ErrorMatches,
got: errors.New("error: bad wolf"),
got: errBadWolf,
args: []interface{}{""},
expectedCheckFailure: `
error:
error does not match regexp
error message:
"error: bad wolf"
got error:
&errors.errorString{s:"error: bad wolf"}
bad wolf
file:line
regexp:
""
`,
}, {
about: "ErrorMatches: complex pattern",
checker: qt.ErrorMatches,
got: errors.New("bad wolf"),
got: errBadWolf,
args: []interface{}{"bad wolf|end of the universe"},
expectedNegateFailure: `
error:
unexpected success
error message:
"bad wolf"
got error:
&errors.errorString{s:"bad wolf"}
bad wolf
file:line
regexp:
"bad wolf|end of the universe"
`,
}, {
about: "ErrorMatches: invalid pattern",
checker: qt.ErrorMatches,
got: errors.New("bad wolf"),
got: errBadWolf,
args: []interface{}{"("},
expectedCheckFailure: `
error:
......@@ -837,21 +895,17 @@ error:
}, {
about: "ErrorMatches: pattern not a string",
checker: qt.ErrorMatches,
got: errors.New("bad wolf"),
got: errBadWolf,
args: []interface{}{[]int{42}},
expectedCheckFailure: `
error:
bad check: regexp is not a string
error message:
"bad wolf"
regexp:
[]int{42}
`,
expectedNegateFailure: `
error:
bad check: regexp is not a string
error message:
"bad wolf"
regexp:
[]int{42}
`,
......@@ -907,14 +961,14 @@ want args:
}, {
about: "ErrorMatches: too many arguments",
checker: qt.ErrorMatches,
got: errors.New("error: bad wolf"),
args: []interface{}{"error: bad wolf", []string{"bad", "wolf"}},
got: errBadWolf,
args: []interface{}{"bad wolf", []string{"bad", "wolf"}},
expectedCheckFailure: `
error:
bad check: too many arguments provided to checker: got 2, want 1
got args:
[]interface {}{
"error: bad wolf",
"bad wolf",
[]string{"bad", "wolf"},
}
want args:
......@@ -925,7 +979,7 @@ error:
bad check: too many arguments provided to checker: got 2, want 1
got args:
[]interface {}{
"error: bad wolf",
"bad wolf",
[]string{"bad", "wolf"},
}
want args:
......@@ -1455,10 +1509,8 @@ want args:
expectedNegateFailure: `
error:
unexpected success
result:
bool(true)
arg:
&"bad wolf"
bad check: bad wolf
predicate function:
func(error) bool {...}
`,
......@@ -1472,8 +1524,6 @@ predicate function:
expectedNegateFailure: `
error:
unexpected success
result:
bool(true)
arg:
int(42)
predicate function:
......@@ -1489,8 +1539,6 @@ predicate function:
expectedNegateFailure: `
error:
unexpected success
result:
bool(true)
arg:
nil
predicate function:
......@@ -1504,8 +1552,6 @@ predicate function:
expectedCheckFailure: `
error:
value does not satisfy predicate function
result:
bool(false)
arg:
nil
predicate function:
......@@ -1521,8 +1567,6 @@ predicate function:
expectedCheckFailure: `
error:
value does not satisfy predicate function
result:
bool(false)
arg:
"bad wolf"
predicate function:
......@@ -1804,13 +1848,7 @@ func TestCheckers(t *testing.T) {
}
}
func diff(got, want interface{}, opts ...cmp.Option) string {
// TODO frankban: should we put prefixf in an export_test.go file?
s := ""
for _, line := range strings.Split(cmp.Diff(got, want, opts...), "\n") {
if line != "" {
s += " " + line + "\n"
}
}
return strings.TrimSuffix(s, "\n")
func diff(x, y interface{}, opts ...cmp.Option) string {
d := cmp.Diff(x, y, opts...)
return strings.TrimSuffix(qt.Prefixf(" ", "%s", d), "\n")
}
......@@ -4,6 +4,7 @@ package quicktest_test
import (
"errors"
"fmt"
"testing"
qt "github.com/frankban/quicktest"
......@@ -23,3 +24,22 @@ func TestIsBadCheck(t *testing.T) {
err = errors.New("bad wolf")
assertBool(t, qt.IsBadCheck(err), false)
}
var errBadWolf = &errTest{}
// errTest is an error type used in tests.
type errTest struct{}
// Error implements error.
func (*errTest) Error() string {
return "bad wolf"
}
// Format implements fmt.Formatter.
func (err *errTest) Format(f fmt.State, c rune) {
if !f.Flag('+') || c != 'v' {
fmt.Fprint(f, "unexpected verb for formatting the error")
}
fmt.Fprint(f, err.Error())
fmt.Fprint(f, "\n file:line")
}
// Licensed under the MIT license, see LICENCE file for details.
package quicktest
var Prefixf = prefixf
// Licensed under the MIT license, see LICENCE file for details.
package quicktest
import (
"fmt"
"github.com/kr/pretty"
)
// Format formats the given value as a string. It is used to print values in
// test failures unless that's changed by calling C.SetFormat.
func Format(v interface{}) string {
switch v := v.(type) {
case error:
return fmt.Sprintf("%+v", v)
case fmt.Stringer:
return fmt.Sprintf("s%q", v)
}
// The pretty.Sprint equivalent does not quote string values.
return fmt.Sprintf("%# v", pretty.Formatter(v))
}
type formatFunc func(interface{}) string
// Licensed under the MIT license, see LICENCE file for details.
package quicktest_test
import (
"bytes"
"testing"
qt "github.com/frankban/quicktest"
)
var formatTests = []struct {
about string
value interface{}
want string
}{{
about: "error value",
value: errBadWolf,
want: "bad wolf\n file:line",
}, {
about: "stringer",
value: bytes.NewBufferString("I am a stringer"),
want: `s"I am a stringer"`,
}, {
about: "string",
value: "these are the voyages",
want: `"these are the voyages"`,
}, {
about: "slice",
value: []int{1, 2, 3},
want: "[]int{1, 2, 3}",
}, {
about: "time",
value: goTime,
want: `s"2012-03-28 00:00:00 +0000 UTC"`,
}}
func TestFormat(t *testing.T) {
for _, test := range formatTests {
t.Run(test.about, func(t *testing.T) {
got := qt.Format(test.value)
if got != test.want {
t.Fatalf("format:\ngot %q\nwant %q", got, test.want)
}
})
}
}
......@@ -36,6 +36,7 @@ func New(t testing.TB) *C {
return &C{
TB: t,
deferred: func() {},
format: Format,
}
}
......@@ -44,7 +45,9 @@ func New(t testing.TB) *C {
// uses the wrapped TB value to fail the test appropriately.
type C struct {
testing.TB
deferred func()
format formatFunc
}
// Defer registers a function to be called when c.Done is
......@@ -85,6 +88,14 @@ func (c *C) Cleanup() {
c.Done()
}
// SetFormat sets the function used to print values in test failures.
// By default Format is used.
// Any subsequent subtests invoked with c.Run will also use this function by
// default.
func (c *C) SetFormat(format func(interface{}) string) {
c.format = format
}
// Check runs the given check and continues execution in case of failure.
// For instance:
//
......@@ -94,7 +105,7 @@ func (c *C) Cleanup() {
// Additional args (not consumed by the checker), when provided, are included
// as comments in the failure output when the check fails.
func (c *C) Check(got interface{}, checker Checker, args ...interface{}) bool {
return check(c.TB.Error, checker, got, args)
return c.check(c.TB.Error, checker, got, args)
}
// Assert runs the given check and stops execution in case of failure.
......@@ -106,7 +117,7 @@ func (c *C) Check(got interface{}, checker Checker, args ...interface{}) bool {
// Additional args (not consumed by the checker), when provided, are included
// as comments in the failure output when the check fails.
func (c *C) Assert(got interface{}, checker Checker, args ...interface{}) bool {
return check(c.TB.Fatal, checker, got, args)
return c.check(c.TB.Fatal, checker, got, args)
}
// Run runs f as a subtest of t called name. It's a wrapper around
......@@ -134,9 +145,10 @@ func (c *C) Run(name string, f func(c *C)) bool {
panic(fmt.Sprintf("cannot execute Run with underlying concrete type %T", c.TB))
}
return r.Run(name, func(t *testing.T) {
c := New(t)
defer c.Done()
f(c)
c2 := New(t)
defer c2.Done()
c2.SetFormat(c.format)
f(c2)
})
}
......@@ -157,37 +169,40 @@ func (c *C) Parallel() {
// check performs the actual check and calls the provided fail function in case
// of failure.
func check(fail func(...interface{}), checker Checker, got interface{}, args []interface{}) bool {
func (c *C) check(fail func(...interface{}), checker Checker, got interface{}, args []interface{}) bool {
// Allow checkers to annotate messages.
var ns []note
rp := reportParams{
got: got,
args: args,
format: c.format,
}
note := func(key string, value interface{}) {
ns = append(ns, note{
rp.notes = append(rp.notes, note{
key: key,
value: value,
})
}
// Ensure that we have a checker.
if checker == nil {
fail(report(nil, got, args, Comment{}, ns, BadCheckf("nil checker provided")))
fail(report(BadCheckf("nil checker provided"), rp))
return false
}
// Extract a comment if it has been provided.
argNames := checker.ArgNames()
wantNumArgs := len(argNames) - 1
var c Comment
rp.argNames = checker.ArgNames()
wantNumArgs := len(rp.argNames) - 1
if len(args) > 0 {
if comment, ok := args[len(args)-1].(Comment); ok {
c = comment
args = args[:len(args)-1]
rp.comment = comment
rp.args = args[:len(args)-1]
}
}
// Validate that we have the correct number of arguments.
if gotNumArgs := len(args); gotNumArgs != wantNumArgs {
if gotNumArgs := len(rp.args); gotNumArgs != wantNumArgs {
if gotNumArgs > 0 {
note("got args", args)
note("got args", rp.args)
}
if wantNumArgs > 0 {
note("want args", Unquoted(strings.Join(argNames[1:], ", ")))
note("want args", Unquoted(strings.Join(rp.argNames[1:], ", ")))
}
var prefix string
if gotNumArgs > wantNumArgs {
......@@ -195,13 +210,13 @@ func check(fail func(...interface{}), checker Checker, got interface{}, args []i
} else {
prefix = "not enough arguments provided to checker"
}
fail(report(argNames, got, args, c, ns, BadCheckf("%s: got %d, want %d", prefix, gotNumArgs, wantNumArgs)))
fail(report(BadCheckf("%s: got %d, want %d", prefix, gotNumArgs, wantNumArgs), rp))
return false
}
// Execute the check and report the failure if necessary.
if err := checker.Check(got, args, note); err != nil {
fail(report(argNames, got, args, c, ns, err))
fail(report(err, rp))
return false
}
return true
......
......@@ -18,6 +18,7 @@ var cTests = []struct {
checker qt.Checker
got interface{}
args []interface{}
format func(interface{}) string
expectedFailure string
}{{
about: "success",
......@@ -250,6 +251,44 @@ arg3:
arg4:
<same as "note4">
`,
}, {
about: "many arguments and notes with custom format function",
checker: &testingChecker{
argNames: []string{"arg1", "arg2", "arg3"},
addNotes: func(note func(key string, value interface{})) {
note("note1", "these")
note("note2", qt.Unquoted("are"))
note("note3", "the")
note("note4", "voyages")
note("note5", true)
},
err: errors.New("bad wolf"),
},
got: 42,
args: []interface{}{"val2", "val3"},
format: func(v interface{}) string {
return fmt.Sprintf("bad wolf %v", v)
},
expectedFailure: `
error:
bad wolf
note1:
bad wolf these
note2:
are
note3:
bad wolf the
note4:
bad wolf voyages
note5:
bad wolf true
arg1:
bad wolf 42
arg2:
bad wolf val2
arg3:
bad wolf val3
`,
}, {
about: "bad check with notes",
checker: &testingChecker{
......@@ -292,6 +331,9 @@ func TestCAssertCheck(t *testing.T) {
t.Run("Check: "+test.about, func(t *testing.T) {
tt := &testingT{}
c := qt.New(tt)
if test.format != nil {
c.SetFormat(test.format)
}
ok := c.Check(test.got, test.checker, test.args...)
checkResult(t, ok, tt.errorString(), test.expectedFailure)
if tt.fatalString() != "" {
......@@ -301,6 +343,9 @@ func TestCAssertCheck(t *testing.T) {
t.Run("Assert: "+test.about, func(t *testing.T) {
tt := &testingT{}
c := qt.New(tt)
if test.format != nil {
c.SetFormat(test.format)
}