├── .gitignore ├── export_test.go ├── .github ├── dependabot.yaml └── workflows │ └── ci.yaml ├── go.mod ├── patch.go ├── comment_test.go ├── comment.go ├── go.sum ├── TODO ├── LICENSE ├── error.go ├── error_test.go ├── iter.go ├── quicktest.go ├── README.md ├── qtsuite ├── suite_test.go └── suite.go ├── format.go ├── format_test.go ├── report_test.go ├── report.go ├── example_test.go ├── quicktest_test.go ├── checker.go └── checker_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | -------------------------------------------------------------------------------- /export_test.go: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license, see LICENSE file for details. 2 | 3 | package qt 4 | 5 | var ( 6 | Prefixf = prefixf 7 | TestingVerbose = &testingVerbose 8 | ) 9 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | # Check for updates to GitHub Actions every weekday. 8 | interval: "daily" 9 | 10 | - package-ecosystem: "gomod" 11 | directory: "/" 12 | schedule: 13 | # Check for updates to go modules every weekday. 14 | interval: "daily" 15 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-quicktest/qt 2 | 3 | require ( 4 | github.com/google/go-cmp v0.6.0 5 | github.com/kr/pretty v0.3.1 6 | ) 7 | 8 | require ( 9 | github.com/kr/text v0.2.0 // indirect 10 | github.com/rogpeppe/go-internal v1.12.0 // indirect 11 | ) 12 | 13 | retract ( 14 | v1.14.3 // Contains retractions only. 15 | v1.14.2 // Published accidentally. 16 | v1.14.1 // Published accidentally. 17 | v1.9.0 // Published accidentally. 18 | v1.7.0 // Published accidentally. 19 | v1.3.0 // Published accidentally. 20 | v0.0.3 // First retract attempt, that didn't work. 21 | ) 22 | 23 | go 1.18 24 | -------------------------------------------------------------------------------- /patch.go: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license, see LICENSE file for details. 2 | 3 | package qt 4 | 5 | import "testing" 6 | 7 | // Patch sets a variable to a temporary value for the duration of the test. 8 | // 9 | // It sets the value pointed to by the given destination to the given 10 | // value, which must be assignable to the element type of the destination. 11 | // 12 | // At the end of the test (see "Deferred execution" in the package docs), the 13 | // destination is set back to its original value. 14 | func Patch[T any](tb testing.TB, dest *T, value T) { 15 | old := *dest 16 | *dest = value 17 | tb.Cleanup(func() { 18 | *dest = old 19 | }) 20 | } 21 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | build_test: 6 | name: Build and Test 7 | strategy: 8 | matrix: 9 | go: ['1.18', '1.19', '1.20', '1.21'] 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: actions/setup-go@v4 14 | with: 15 | go-version: ${{ matrix.go }} 16 | - uses: actions/cache@v3 17 | with: 18 | path: ~/go/pkg/mod 19 | key: ubuntu-go-${{ hashFiles('**/go.sum') }} 20 | restore-keys: | 21 | ubuntu-go- 22 | - name: Test 23 | run: go test -mod readonly -race ./... 24 | - name: Test Verbose 25 | run: go test -mod readonly -race -v ./... 26 | -------------------------------------------------------------------------------- /comment_test.go: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license, see LICENSE file for details. 2 | 3 | package qt_test 4 | 5 | import ( 6 | "testing" 7 | 8 | "github.com/go-quicktest/qt" 9 | ) 10 | 11 | func TestCommentf(t *testing.T) { 12 | c := qt.Commentf("the answer is %d", 42) 13 | comment := c.String() 14 | expectedComment := "the answer is 42" 15 | if comment != expectedComment { 16 | t.Fatalf("comment error:\ngot %q\nwant %q", comment, expectedComment) 17 | } 18 | } 19 | 20 | func TestConstantCommentf(t *testing.T) { 21 | const expectedComment = "bad wolf" 22 | c := qt.Commentf(expectedComment) 23 | comment := c.String() 24 | if comment != expectedComment { 25 | t.Fatalf("constant comment error:\ngot %q\nwant %q", comment, expectedComment) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /comment.go: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license, see LICENSE file for details. 2 | 3 | package qt 4 | 5 | import "fmt" 6 | 7 | // Commentf returns a test comment whose output is formatted according to 8 | // the given format specifier and args. It may be provided as the last argument 9 | // to any check or assertion and will be displayed if the check or assertion 10 | // fails. 11 | func Commentf(format string, args ...any) Comment { 12 | return Comment{ 13 | format: format, 14 | args: args, 15 | } 16 | } 17 | 18 | // Comment represents additional information on a check or an assertion which is 19 | // displayed when the check or assertion fails. 20 | type Comment struct { 21 | format string 22 | args []any 23 | } 24 | 25 | // String outputs a string formatted according to the stored format specifier 26 | // and args. 27 | func (c Comment) String() string { 28 | return fmt.Sprintf(c.format, c.args...) 29 | } 30 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 2 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 3 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 4 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 5 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 6 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 7 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 8 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 9 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 10 | github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= 11 | github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= 12 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | ## Possible things to do 2 | 3 | var defaultRetryStrategy = &retry.Strategy{ 4 | Delay: time.Millisecond, 5 | MaxDelay: 200 * time.Millisecond, 6 | MaxDuration: 5 * time.Second 7 | } 8 | 9 | var defaultStableStrategy = &retry.Strategy{ 10 | Delay: time.Millisecond, 11 | MaxDuration: 50 * time.Millisecond, 12 | } 13 | 14 | func Eventually[T any](f func() T, checker func(T) Checker, retry *retry.Strategy) Checker 15 | func EventuallyStable[T any](f func() T, checker func(T) Checker, retry, stableRetry *retry.Strategy) Checker 16 | 17 | // QT provides a version of the Assert and Check primitives 18 | // that use a customizable Format function. 19 | // 20 | // For example: 21 | // 22 | // package mypackage 23 | // 24 | // import _qt "github.com/go-quicktest/qt" 25 | // 26 | // var qt = _qt.QT{ 27 | // Format: myFormat, 28 | // } 29 | type QT struct { 30 | Format func(interface{}) string 31 | } 32 | 33 | func (qt QT) Assert(t testing.TB, checker Checker, comment ...Comment) 34 | func (qt QT) Check(t testing.TB, checker Checker, comment ...Comment) bool 35 | 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Canonical Ltd. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /error.go: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license, see LICENCE file for details. 2 | 3 | package qt 4 | 5 | import ( 6 | "fmt" 7 | ) 8 | 9 | // BadCheckf returns an error used to report a problem with the checker 10 | // invocation or testing execution itself (like wrong number or type of 11 | // arguments) rather than a real Check or Assert failure. 12 | // This helper can be used when implementing checkers. 13 | func BadCheckf(format string, a ...any) error { 14 | e := badCheck(fmt.Sprintf(format, a...)) 15 | return &e 16 | } 17 | 18 | // IsBadCheck reports whether the given error has been created by BadCheckf. 19 | // This helper can be used when implementing checkers. 20 | func IsBadCheck(err error) bool { 21 | _, ok := err.(*badCheck) 22 | return ok 23 | } 24 | 25 | type badCheck string 26 | 27 | // Error implements the error interface. 28 | func (e *badCheck) Error() string { 29 | return "bad check: " + string(*e) 30 | } 31 | 32 | // ErrSilent is the error used when there is no need to include in the failure 33 | // output the "error" and "check" keys and all the keys automatically 34 | // added for args. This helper can be used when implementing checkers. 35 | var ErrSilent = fmt.Errorf("silent failure") 36 | -------------------------------------------------------------------------------- /error_test.go: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license, see LICENSE file for details. 2 | 3 | package qt_test 4 | 5 | import ( 6 | "errors" 7 | "fmt" 8 | "testing" 9 | 10 | "github.com/go-quicktest/qt" 11 | ) 12 | 13 | func TestBadCheckf(t *testing.T) { 14 | err := qt.BadCheckf("bad %s", "wolf") 15 | expectedMessage := "bad check: bad wolf" 16 | if err.Error() != expectedMessage { 17 | t.Fatalf("error:\ngot %q\nwant %q", err, expectedMessage) 18 | } 19 | } 20 | 21 | func TestIsBadCheck(t *testing.T) { 22 | err := qt.BadCheckf("bad wolf") 23 | assertBool(t, qt.IsBadCheck(err), true) 24 | err = errors.New("bad wolf") 25 | assertBool(t, qt.IsBadCheck(err), false) 26 | } 27 | 28 | var errBadWolf = &errTest{ 29 | msg: "bad wolf", 30 | formatted: true, 31 | } 32 | 33 | var errBadWolfMultiLine = &errTest{ 34 | msg: "bad wolf\nfaulty logic", 35 | formatted: true, 36 | } 37 | 38 | // errTest is an error type used in tests. 39 | type errTest struct { 40 | msg string 41 | formatted bool 42 | } 43 | 44 | // Error implements error. 45 | func (err *errTest) Error() string { 46 | return err.msg 47 | } 48 | 49 | // Format implements fmt.Formatter. 50 | func (err *errTest) Format(f fmt.State, c rune) { 51 | if !f.Flag('+') || c != 'v' { 52 | fmt.Fprint(f, "unexpected verb for formatting the error") 53 | } 54 | fmt.Fprint(f, err.Error()) 55 | if err.formatted { 56 | fmt.Fprint(f, "\n file:line") 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /iter.go: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license, see LICENSE file for details. 2 | 3 | package qt 4 | 5 | import ( 6 | "fmt" 7 | "reflect" 8 | ) 9 | 10 | // containerIter provides an interface for iterating over a container 11 | // (map, slice or array). 12 | type containerIter[T any] interface { 13 | // next advances to the next item in the container. 14 | next() bool 15 | // key returns the current key as a string. 16 | key() string 17 | // value returns the current value. 18 | value() T 19 | } 20 | 21 | func newSliceIter[T any](slice []T) *sliceIter[T] { 22 | return &sliceIter[T]{ 23 | slice: slice, 24 | index: -1, 25 | } 26 | } 27 | 28 | // sliceIter implements containerIter for slices and arrays. 29 | type sliceIter[T any] struct { 30 | slice []T 31 | index int 32 | } 33 | 34 | func (i *sliceIter[T]) next() bool { 35 | i.index++ 36 | return i.index < len(i.slice) 37 | } 38 | 39 | func (i *sliceIter[T]) value() T { 40 | return i.slice[i.index] 41 | } 42 | 43 | func (i *sliceIter[T]) key() string { 44 | return fmt.Sprintf("index %d", i.index) 45 | } 46 | 47 | func newMapIter[K comparable, V any](m map[K]V) containerIter[V] { 48 | return mapValueIter[V]{ 49 | iter: reflect.ValueOf(m).MapRange(), 50 | } 51 | } 52 | 53 | // mapValueIter implements containerIter for maps. 54 | type mapValueIter[T any] struct { 55 | iter *reflect.MapIter 56 | v T 57 | } 58 | 59 | func (i mapValueIter[T]) next() bool { 60 | return i.iter.Next() 61 | } 62 | 63 | func (i mapValueIter[T]) key() string { 64 | return fmt.Sprintf("key %#v", i.iter.Key()) 65 | } 66 | 67 | func (i mapValueIter[T]) value() T { 68 | return valueAs[T](i.iter.Value()) 69 | } 70 | -------------------------------------------------------------------------------- /quicktest.go: -------------------------------------------------------------------------------- 1 | // Package qt implements assertions and other helpers wrapped around the 2 | // standard library's testing types. 3 | package qt 4 | 5 | import ( 6 | "testing" 7 | ) 8 | 9 | // Assert checks that the provided argument passes the given check and calls 10 | // tb.Fatal otherwise, including any Comment arguments in the failure. 11 | func Assert(t testing.TB, checker Checker, comments ...Comment) bool { 12 | t.Helper() 13 | return check(t, checkParams{ 14 | fail: t.Fatal, 15 | checker: checker, 16 | comments: comments, 17 | }) 18 | } 19 | 20 | // Check checks that the provided argument passes the given check and calls 21 | // tb.Error otherwise, including any Comment arguments in the failure. 22 | func Check(t testing.TB, checker Checker, comments ...Comment) bool { 23 | t.Helper() 24 | return check(t, checkParams{ 25 | fail: t.Error, 26 | checker: checker, 27 | comments: comments, 28 | }) 29 | } 30 | 31 | func check(t testing.TB, p checkParams) bool { 32 | t.Helper() 33 | rp := reportParams{ 34 | comments: p.comments, 35 | } 36 | 37 | // Allow checkers to annotate messages. 38 | note := func(key string, value any) { 39 | rp.notes = append(rp.notes, note{ 40 | key: key, 41 | value: value, 42 | }) 43 | } 44 | 45 | // Ensure that we have a checker. 46 | if p.checker == nil { 47 | p.fail(report(BadCheckf("nil checker provided"), rp)) 48 | return false 49 | } 50 | rp.args = p.checker.Args() 51 | 52 | // Run the check. 53 | if err := p.checker.Check(note); err != nil { 54 | p.fail(report(err, rp)) 55 | return false 56 | } 57 | return true 58 | } 59 | 60 | type checkParams struct { 61 | fail func(...any) 62 | checker Checker 63 | comments []Comment 64 | } 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # qt: quicker Go tests 2 | 3 | `go get github.com/go-quicktest/qt` 4 | 5 | Package qt provides a collection of Go helpers for writing tests. It uses generics, 6 | so requires Go 1.18 at least. 7 | 8 | For a complete API reference, see the [package documentation](https://pkg.go.dev/github.com/go-quicktest/qt). 9 | 10 | Quicktest helpers can be easily integrated inside regular Go tests, for 11 | instance: 12 | ```go 13 | import "github.com/go-quicktest/qt" 14 | 15 | func TestFoo(t *testing.T) { 16 | t.Run("numbers", func(t *testing.T) { 17 | numbers, err := somepackage.Numbers() 18 | qt.Assert(t, qt.DeepEquals(numbers, []int{42, 47}) 19 | qt.Assert(t, qt.ErrorMatches(err, "bad wolf")) 20 | }) 21 | t.Run("nil", func(t *testing.T) { 22 | got := somepackage.MaybeNil() 23 | qt.Assert(t, qt.IsNil(got), qt.Commentf("value: %v", somepackage.Value)) 24 | }) 25 | } 26 | ``` 27 | 28 | ### Assertions 29 | 30 | An assertion looks like this, where `qt.Equals` could be replaced by any available 31 | checker. If the assertion fails, the underlying `t.Fatal` method is called to 32 | describe the error and abort the test. 33 | 34 | qt.Assert(t, qt.Equals(someValue, wantValue)) 35 | 36 | If you don’t want to abort on failure, use `Check` instead, which calls `Error` 37 | instead of `Fatal`: 38 | 39 | qt.Check(t, qt.Equals(someValue, wantValue)) 40 | 41 | The library provides some base checkers like `Equals`, `DeepEquals`, `Matches`, 42 | `ErrorMatches`, `IsNil` and others. More can be added by implementing the Checker 43 | interface. 44 | 45 | ### Other helpers 46 | 47 | The `Patch` helper makes it a little more convenient to change a global 48 | or other variable for the duration of a test. 49 | -------------------------------------------------------------------------------- /qtsuite/suite_test.go: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license, see LICENSE file for details. 2 | 3 | package qtsuite_test 4 | 5 | import ( 6 | "testing" 7 | 8 | "github.com/go-quicktest/qt" 9 | "github.com/go-quicktest/qt/qtsuite" 10 | ) 11 | 12 | func TestRunSuite(t *testing.T) { 13 | var calls []call 14 | qtsuite.Run(t, testSuite{calls: &calls}) 15 | qt.Assert(t, qt.DeepEquals(calls, []call{ 16 | {"Test1", 0}, 17 | {"Test4", 0}, 18 | })) 19 | } 20 | 21 | func TestRunSuiteEmbedded(t *testing.T) { 22 | var calls []call 23 | suite := struct { 24 | testSuite 25 | }{testSuite: testSuite{calls: &calls}} 26 | qtsuite.Run(t, suite) 27 | qt.Assert(t, qt.DeepEquals(calls, []call{ 28 | {"Test1", 0}, 29 | {"Test4", 0}, 30 | })) 31 | } 32 | 33 | func TestRunSuitePtr(t *testing.T) { 34 | var calls []call 35 | qtsuite.Run(t, &testSuite{calls: &calls}) 36 | qt.Assert(t, qt.DeepEquals(calls, []call{ 37 | {"Init", 0}, 38 | {"Test1", 1}, 39 | {"Init", 0}, 40 | {"Test4", 1}, 41 | })) 42 | } 43 | 44 | type testSuite struct { 45 | init int 46 | calls *[]call 47 | } 48 | 49 | func (s testSuite) addCall(name string) { 50 | *s.calls = append(*s.calls, call{Name: name, Init: s.init}) 51 | } 52 | 53 | func (s *testSuite) Init(*testing.T) { 54 | s.addCall("Init") 55 | s.init++ 56 | } 57 | 58 | func (s testSuite) Test1(*testing.T) { 59 | s.addCall("Test1") 60 | } 61 | 62 | func (s testSuite) Test4(*testing.T) { 63 | s.addCall("Test4") 64 | } 65 | 66 | func (s testSuite) Testa(*testing.T) { 67 | s.addCall("Testa") 68 | } 69 | 70 | type call struct { 71 | Name string 72 | Init int 73 | } 74 | 75 | // It's not clear how to test this. 76 | // 77 | //func TestInvalidInit(t *testing.T) { 78 | // c := qt.New(t) 79 | // tt := &testingT{} 80 | // tc := qt.New(tt) 81 | // qtsuite.Run(tc, invalidTestSuite{}) 82 | // qt.Assert(t, qt.Equals(tt.fatalString(), "wrong signature for Init, must be Init(*testing.T)")) 83 | //} 84 | // 85 | //type invalidTestSuite struct{} 86 | // 87 | //func (invalidTestSuite) Init() {} 88 | //} 89 | -------------------------------------------------------------------------------- /format.go: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license, see LICENSE file for details. 2 | 3 | package qt 4 | 5 | import ( 6 | "fmt" 7 | "reflect" 8 | "strconv" 9 | "strings" 10 | "unicode/utf8" 11 | 12 | "github.com/kr/pretty" 13 | ) 14 | 15 | // Format formats the given value as a string. It is used to print values in 16 | // test failures. 17 | func Format(v any) string { 18 | switch v := v.(type) { 19 | case error: 20 | s, ok := checkStringCall(v, v.Error) 21 | if !ok { 22 | return "e" 23 | } 24 | if msg := fmt.Sprintf("%+v", v); msg != s { 25 | // The error has formatted itself with additional information. 26 | // Leave that as is. 27 | return msg 28 | } 29 | return "e" + quoteString(s) 30 | case fmt.Stringer: 31 | s, ok := checkStringCall(v, v.String) 32 | if !ok { 33 | return "s" 34 | } 35 | return "s" + quoteString(s) 36 | case string: 37 | return quoteString(v) 38 | case uintptr, uint, uint8, uint16, uint32, uint64: 39 | // Use decimal base (rather than hexadecimal) for representing uint types. 40 | return fmt.Sprintf("%T(%d)", v, v) 41 | } 42 | if bytes, ok := byteSlice(v); ok && bytes != nil && utf8.Valid(bytes) { 43 | // It's a top level slice of bytes that's also valid UTF-8. 44 | // Ideally, this would happen at deeper levels too, 45 | // but this is sufficient for some significant cases 46 | // (json.RawMessage for example). 47 | return fmt.Sprintf("%T(%s)", v, quoteString(string(bytes))) 48 | } 49 | // The pretty.Sprint equivalent does not quote string values. 50 | return fmt.Sprintf("%# v", pretty.Formatter(v)) 51 | } 52 | 53 | func byteSlice(x any) ([]byte, bool) { 54 | v := reflect.ValueOf(x) 55 | if !v.IsValid() { 56 | return nil, false 57 | } 58 | t := v.Type() 59 | if t.Kind() == reflect.Slice && t.Elem().Kind() == reflect.Uint8 { 60 | return v.Bytes(), true 61 | } 62 | return nil, false 63 | } 64 | 65 | func quoteString(s string) string { 66 | // TODO think more about what to do about multi-line strings. 67 | if strings.Contains(s, `"`) && !strings.Contains(s, "\n") && strconv.CanBackquote(s) { 68 | return "`" + s + "`" 69 | } 70 | return strconv.Quote(s) 71 | } 72 | 73 | // checkStringCall calls f and returns its result, and reports if the call 74 | // succeeded without panicking due to a nil pointer. 75 | // If f panics and v is a nil pointer, it returns false. 76 | func checkStringCall(v any, f func() string) (s string, ok bool) { 77 | defer func() { 78 | err := recover() 79 | if err == nil { 80 | return 81 | } 82 | if val := reflect.ValueOf(v); val.Kind() == reflect.Pointer && val.IsNil() { 83 | ok = false 84 | return 85 | } 86 | panic(err) 87 | }() 88 | return f(), true 89 | } 90 | -------------------------------------------------------------------------------- /qtsuite/suite.go: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license, see LICENSE file for details. 2 | 3 | /* 4 | Package qtsuite allows quicktest to run test suites. 5 | 6 | A test suite is a value with one or more test methods. 7 | For example, the following code defines a suite of test functions that starts 8 | an HTTP server before running each test, and tears it down afterwards: 9 | 10 | type suite struct { 11 | url string 12 | } 13 | 14 | func (s *suite) Init(t *testing.T) { 15 | hnd := func(w http.ResponseWriter, req *http.Request) { 16 | fmt.Fprintf(w, "%s %s", req.Method, req.URL.Path) 17 | } 18 | srv := httptest.NewServer(http.HandlerFunc(hnd)) 19 | t.Cleanup(srv.Close) 20 | s.url = srv.URL 21 | } 22 | 23 | func (s *suite) TestGet(t *testing.T) { 24 | t.Parallel() 25 | resp, err := http.Get(s.url) 26 | qt.Assert(t, qt.IsNil(err)) 27 | defer resp.Body.Close() 28 | b, err := ioutil.ReadAll(resp.Body) 29 | qt.Assert(t, qt.IsNil(err)) 30 | qt.Assert(t, qt.Equals(string(b), "GET /")) 31 | } 32 | 33 | func (s *suite) TestHead(t *testing.T) { 34 | t.Parallel() 35 | resp, err := http.Head(s.url + "/path") 36 | qt.Assert(t, qt.IsNil(err)) 37 | defer resp.Body.Close() 38 | b, err := ioutil.ReadAll(resp.Body) 39 | qt.Assert(t, qt.IsNil(err)) 40 | qt.Assert(t, qt.Equals(string(b), "")) 41 | qt.Assert(t, qt.Equals(resp.ContentLength, 10)) 42 | } 43 | 44 | The above code could be invoked from a test function like this: 45 | 46 | func TestHTTPMethods(t *testing.T) { 47 | qtsuite.Run(t, &suite{}) 48 | } 49 | */ 50 | package qtsuite 51 | 52 | import ( 53 | "reflect" 54 | "strings" 55 | "testing" 56 | "unicode" 57 | "unicode/utf8" 58 | ) 59 | 60 | // Run runs each test method defined on the given value as a separate 61 | // subtest. A test is a method of the form 62 | // 63 | // func (T) TestXxx(*testing.T) 64 | // 65 | // where Xxx does not start with a lowercase letter. 66 | // 67 | // If suite is a pointer, the value pointed to is copied before any 68 | // methods are invoked on it: a new copy is made for each test. This 69 | // means that it is OK for tests to modify fields in suite concurrently 70 | // if desired - it's OK to call t.Parallel(). 71 | // 72 | // If suite has a method of the form 73 | // 74 | // func (T) Init(*testing.T) 75 | // 76 | // this method will be invoked before each test run. 77 | func Run(t *testing.T, suite any) { 78 | sv := reflect.ValueOf(suite) 79 | st := sv.Type() 80 | init, hasInit := st.MethodByName("Init") 81 | if hasInit && !isValidMethod(init) { 82 | t.Fatal("wrong signature for Init, must be Init(*testing.T)") 83 | } 84 | for i := 0; i < st.NumMethod(); i++ { 85 | m := st.Method(i) 86 | if !isTestMethod(m) { 87 | continue 88 | } 89 | t.Run(m.Name, func(t *testing.T) { 90 | if !isValidMethod(m) { 91 | t.Fatalf("wrong signature for %s, must be %s(*testing.T)", m.Name, m.Name) 92 | } 93 | 94 | sv := sv 95 | if st.Kind() == reflect.Pointer { 96 | sv1 := reflect.New(st.Elem()) 97 | sv1.Elem().Set(sv.Elem()) 98 | sv = sv1 99 | } 100 | args := []reflect.Value{sv, reflect.ValueOf(t)} 101 | if hasInit { 102 | init.Func.Call(args) 103 | } 104 | m.Func.Call(args) 105 | }) 106 | } 107 | } 108 | 109 | var tType = reflect.TypeOf((*testing.T)(nil)) 110 | 111 | func isTestMethod(m reflect.Method) bool { 112 | if !strings.HasPrefix(m.Name, "Test") { 113 | return false 114 | } 115 | r, n := utf8.DecodeRuneInString(m.Name[4:]) 116 | return n == 0 || !unicode.IsLower(r) 117 | } 118 | 119 | func isValidMethod(m reflect.Method) bool { 120 | return m.Type.NumIn() == 2 && m.Type.NumOut() == 0 && m.Type.In(1) == tType 121 | } 122 | -------------------------------------------------------------------------------- /format_test.go: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license, see LICENSE file for details. 2 | 3 | package qt_test 4 | 5 | import ( 6 | "bytes" 7 | "testing" 8 | 9 | "github.com/go-quicktest/qt" 10 | ) 11 | 12 | var formatTests = []struct { 13 | about string 14 | value any 15 | want string 16 | }{{ 17 | about: "error value", 18 | value: errBadWolf, 19 | want: "bad wolf\n file:line", 20 | }, { 21 | about: "error value: not formatted", 22 | value: &errTest{ 23 | msg: "exterminate!", 24 | }, 25 | want: `e"exterminate!"`, 26 | }, { 27 | about: "error value: with quotes", 28 | value: &errTest{ 29 | msg: `cannot open "/no/such/file"`, 30 | }, 31 | want: "e`cannot open \"/no/such/file\"`", 32 | }, { 33 | about: "error value: multi-line", 34 | value: &errTest{ 35 | msg: `err: 36 | "these are the voyages"`, 37 | }, 38 | want: `e"err:\n\"these are the voyages\""`, 39 | }, { 40 | about: "error value: with backquotes", 41 | value: &errTest{ 42 | msg: "cannot `open` \"file\"", 43 | }, 44 | want: `e"cannot ` + "`open`" + ` \"file\""`, 45 | }, { 46 | about: "error value: not guarding against nil", 47 | value: (*errTest)(nil), 48 | want: `e`, 49 | }, { 50 | about: "stringer", 51 | value: bytes.NewBufferString("I am a stringer"), 52 | want: `s"I am a stringer"`, 53 | }, { 54 | about: "stringer: with quotes", 55 | value: bytes.NewBufferString(`I say "hello"`), 56 | want: "s`I say \"hello\"`", 57 | }, { 58 | about: "stringer: not guarding against nil", 59 | value: (*nilStringer)(nil), 60 | want: "s", 61 | }, { 62 | about: "string", 63 | value: "these are the voyages", 64 | want: `"these are the voyages"`, 65 | }, { 66 | about: "string: with quotes", 67 | value: `here is a quote: "`, 68 | want: "`here is a quote: \"`", 69 | }, { 70 | about: "string: multi-line", 71 | value: `foo 72 | "bar" 73 | `, 74 | want: `"foo\n\"bar\"\n"`, 75 | }, { 76 | about: "string: with backquotes", 77 | value: `"` + "`", 78 | want: `"\"` + "`\"", 79 | }, { 80 | about: "slice", 81 | value: []int{1, 2, 3}, 82 | want: "[]int{1, 2, 3}", 83 | }, { 84 | about: "bytes", 85 | value: []byte("hello"), 86 | want: `[]uint8("hello")`, 87 | }, { 88 | about: "custom bytes type", 89 | value: myBytes("hello"), 90 | want: `qt_test.myBytes("hello")`, 91 | }, { 92 | about: "bytes with backquote", 93 | value: []byte(`a "b" c`), 94 | want: "[]uint8(`a \"b\" c`)", 95 | }, { 96 | about: "bytes with invalid utf-8", 97 | value: []byte("\xff"), 98 | want: "[]uint8{0xff}", 99 | }, { 100 | about: "nil byte slice", 101 | value: []byte(nil), 102 | want: "[]uint8(nil)", 103 | }, { 104 | about: "time", 105 | value: goTime, 106 | want: `s"2012-03-28 00:00:00 +0000 UTC"`, 107 | }, { 108 | about: "struct with byte slice", 109 | value: struct{ X []byte }{[]byte("x")}, 110 | want: "struct { X []uint8 }{\n X: {0x78},\n}", 111 | }, { 112 | about: "uint64", 113 | value: uint64(17), 114 | want: "uint64(17)", 115 | }, { 116 | about: "uint32", 117 | value: uint32(17898), 118 | want: "uint32(17898)", 119 | }, { 120 | about: "uintptr", 121 | value: uintptr(13), 122 | want: "uintptr(13)", 123 | }, 124 | } 125 | 126 | func TestFormat(t *testing.T) { 127 | for _, test := range formatTests { 128 | t.Run(test.about, func(t *testing.T) { 129 | got := qt.Format(test.value) 130 | if got != test.want { 131 | t.Fatalf("format:\ngot %q\nwant %q", got, test.want) 132 | } 133 | }) 134 | } 135 | } 136 | 137 | type myBytes []byte 138 | 139 | // nilStringer is a stringer not guarding against nil. 140 | type nilStringer struct { 141 | msg string 142 | } 143 | 144 | func (s *nilStringer) String() string { 145 | return s.msg 146 | } 147 | -------------------------------------------------------------------------------- /report_test.go: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license, see LICENSE file for details. 2 | 3 | package qt_test 4 | 5 | import ( 6 | "runtime" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/go-quicktest/qt" 11 | ) 12 | 13 | // The tests in this file rely on their own source code lines. 14 | 15 | func TestReportOutput(t *testing.T) { 16 | tt := &testingT{} 17 | qt.Assert(tt, qt.Equals(42, 47)) 18 | want := ` 19 | error: 20 | values are not equal 21 | got: 22 | int(42) 23 | want: 24 | int(47) 25 | stack: 26 | $file:17 27 | qt.Assert(tt, qt.Equals(42, 47)) 28 | ` 29 | assertReport(t, tt, want) 30 | } 31 | 32 | func f1(t testing.TB) { 33 | f2(t) 34 | } 35 | 36 | func f2(t testing.TB) { 37 | qt.Assert(t, qt.IsNil([]int{})) // Real assertion here! 38 | } 39 | 40 | func TestIndirectReportOutput(t *testing.T) { 41 | tt := &testingT{} 42 | f1(tt) 43 | want := ` 44 | error: 45 | got non-nil value 46 | got: 47 | []int{} 48 | stack: 49 | $file:37 50 | qt.Assert(t, qt.IsNil([]int{})) 51 | $file:33 52 | f2(t) 53 | $file:42 54 | f1(tt) 55 | ` 56 | assertReport(t, tt, want) 57 | } 58 | 59 | func TestMultilineReportOutput(t *testing.T) { 60 | tt := &testingT{} 61 | qt.Assert(tt, 62 | qt.Equals( 63 | "this string", // Comment 1. 64 | "another string", 65 | ), 66 | qt.Commentf("a comment"), // Comment 2. 67 | ) // Comment 3. 68 | want := ` 69 | error: 70 | values are not equal 71 | comment: 72 | a comment 73 | got: 74 | "this string" 75 | want: 76 | "another string" 77 | stack: 78 | $file:61 79 | qt.Assert(tt, 80 | qt.Equals( 81 | "this string", // Comment 1. 82 | "another string", 83 | ), 84 | qt.Commentf("a comment"), // Comment 2. 85 | ) 86 | ` 87 | assertReport(t, tt, want) 88 | } 89 | 90 | func TestCmpReportOutput(t *testing.T) { 91 | tt := &testingT{} 92 | gotExamples := []*reportExample{{ 93 | AnInt: 42, 94 | }, { 95 | AnInt: 47, 96 | }, { 97 | AnInt: 2, 98 | }, { 99 | AnInt: 1, 100 | }, {}} 101 | wantExamples := []*reportExample{{ 102 | AnInt: 42, 103 | }, { 104 | AnInt: 47, 105 | }, { 106 | AnInt: 1, 107 | }, { 108 | AnInt: 2, 109 | }} 110 | qt.Assert(tt, qt.DeepEquals(gotExamples, wantExamples)) 111 | want := ` 112 | error: 113 | values are not deep equal 114 | diff (-want +got): 115 | []*qt_test.reportExample{ 116 | &{AnInt: 42}, 117 | &{AnInt: 47}, 118 | + &{AnInt: 2}, 119 | &{AnInt: 1}, 120 | - &{AnInt: 2}, 121 | + &{}, 122 | } 123 | got: 124 | []*qt_test.reportExample{ 125 | &qt_test.reportExample{AnInt:42}, 126 | &qt_test.reportExample{AnInt:47}, 127 | &qt_test.reportExample{AnInt:2}, 128 | &qt_test.reportExample{AnInt:1}, 129 | &qt_test.reportExample{}, 130 | } 131 | want: 132 | []*qt_test.reportExample{ 133 | &qt_test.reportExample{AnInt:42}, 134 | &qt_test.reportExample{AnInt:47}, 135 | &qt_test.reportExample{AnInt:1}, 136 | &qt_test.reportExample{AnInt:2}, 137 | } 138 | stack: 139 | $file:110 140 | qt.Assert(tt, qt.DeepEquals(gotExamples, wantExamples)) 141 | ` 142 | assertReport(t, tt, want) 143 | } 144 | 145 | func TestTopLevelAssertReportOutput(t *testing.T) { 146 | tt := &testingT{} 147 | qt.Assert(tt, qt.Equals(42, 47)) 148 | want := ` 149 | error: 150 | values are not equal 151 | got: 152 | int(42) 153 | want: 154 | int(47) 155 | stack: 156 | $file:147 157 | qt.Assert(tt, qt.Equals(42, 47)) 158 | ` 159 | assertReport(t, tt, want) 160 | } 161 | 162 | func assertReport(t *testing.T, tt *testingT, want string) { 163 | t.Helper() 164 | got := strings.Replace(tt.fatalString(), "\t", " ", -1) 165 | // go-cmp can include non-breaking spaces in its output. 166 | got = strings.Replace(got, "\u00a0", " ", -1) 167 | // Adjust for file names in different systems. 168 | _, file, _, ok := runtime.Caller(0) 169 | assertBool(t, ok, true) 170 | want = strings.Replace(want, "$file", file, -1) 171 | if got != want { 172 | t.Fatalf(`failure: 173 | %q 174 | %q 175 | ------------------------------ got ------------------------------ 176 | %s------------------------------ want ----------------------------- 177 | %s-----------------------------------------------------------------`, 178 | got, want, got, want) 179 | } 180 | } 181 | 182 | type reportExample struct { 183 | AnInt int 184 | } 185 | -------------------------------------------------------------------------------- /report.go: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license, see LICENSE file for details. 2 | 3 | package qt 4 | 5 | import ( 6 | "bytes" 7 | "fmt" 8 | "go/ast" 9 | "go/parser" 10 | "go/printer" 11 | "go/token" 12 | "io" 13 | "reflect" 14 | "runtime" 15 | "strings" 16 | "testing" 17 | ) 18 | 19 | // reportParams holds parameters for reporting a test error. 20 | type reportParams struct { 21 | // args holds all the arguments passed to the checker. 22 | args []Arg 23 | // comments optionally holds comments passed when performing the check. 24 | comments []Comment 25 | // notes holds notes added while doing the check. 26 | notes []note 27 | } 28 | 29 | // Unquoted indicates that the string must not be pretty printed in the failure 30 | // output. This is useful when a checker calls note and does not want the 31 | // provided value to be quoted. 32 | type Unquoted string 33 | 34 | // SuppressedIfLong indicates that the value must be suppressed if verbose 35 | // testing is off and the pretty printed version of the value is long. This is 36 | // useful when a checker calls note and does not want the provided value to be 37 | // printed in non-verbose test runs if the value is too long. 38 | type SuppressedIfLong struct { 39 | // Value holds the original annotated value. 40 | Value any 41 | } 42 | 43 | // longValueLines holds the number of lines after which a value is long. 44 | const longValueLines = 10 45 | 46 | // report generates a failure report for the given error, optionally including 47 | // in the output the checker arguments, comment and notes included in the 48 | // provided report parameters. 49 | func report(err error, p reportParams) string { 50 | var buf bytes.Buffer 51 | buf.WriteByte('\n') 52 | writeError(&buf, err, p) 53 | writeStack(&buf) 54 | return buf.String() 55 | } 56 | 57 | // writeError writes a pretty formatted output of the given error using the 58 | // provided report parameters. 59 | func writeError(w io.Writer, err error, p reportParams) { 60 | ptrs := make(map[string]any) 61 | values := make(map[string]string) 62 | 63 | printPair := func(key string, value any) { 64 | fmt.Fprintln(w, key+":") 65 | var v string 66 | 67 | if u, ok := value.(Unquoted); ok { 68 | // Output the raw string without quotes. 69 | v = string(u) 70 | } else if s, ok := value.(SuppressedIfLong); ok { 71 | // Check whether the output is too long and must be suppressed. 72 | v = Format(s.Value) 73 | if !testingVerbose() { 74 | if n := strings.Count(v, "\n"); n > longValueLines { 75 | fmt.Fprint(w, prefixf(prefix, "", n)) 76 | return 77 | } 78 | } 79 | } else { 80 | // Check whether the output has been already seen. 81 | v = Format(value) 82 | isPtr := reflect.ValueOf(value).Kind() == reflect.Pointer 83 | if k := values[v]; k != "" { 84 | if previousValue, ok := ptrs[k]; ok && isPtr && previousValue != value { 85 | fmt.Fprint(w, prefixf(prefix, "", k)) 86 | return 87 | } 88 | fmt.Fprint(w, prefixf(prefix, "", k)) 89 | return 90 | } 91 | if isPtr { 92 | ptrs[key] = value 93 | } 94 | } 95 | 96 | values[v] = key 97 | fmt.Fprint(w, prefixf(prefix, "%s", v)) 98 | } 99 | 100 | // Write the checker error. 101 | if err != ErrSilent { 102 | printPair("error", Unquoted(err.Error())) 103 | } 104 | 105 | // Write comments if provided. 106 | for _, c := range p.comments { 107 | if comment := c.String(); comment != "" { 108 | printPair("comment", Unquoted(comment)) 109 | } 110 | } 111 | 112 | // Write notes if present. 113 | for _, n := range p.notes { 114 | printPair(n.key, n.value) 115 | } 116 | if IsBadCheck(err) || err == ErrSilent { 117 | // For errors in the checker invocation or for silent errors, do not 118 | // show output from args. 119 | return 120 | } 121 | // Write provided args. 122 | for _, arg := range p.args { 123 | printPair(arg.Name, arg.Value) 124 | } 125 | } 126 | 127 | // testingVerbose is defined as a variable for testing. 128 | var testingVerbose = func() bool { 129 | return testing.Verbose() 130 | } 131 | 132 | // writeStack writes the traceback information for the current failure into the 133 | // provided writer. 134 | func writeStack(w io.Writer) { 135 | fmt.Fprintln(w, "stack:") 136 | pc := make([]uintptr, 8) 137 | sg := &stmtGetter{ 138 | fset: token.NewFileSet(), 139 | files: make(map[string]*ast.File, 8), 140 | config: &printer.Config{ 141 | Mode: printer.UseSpaces, 142 | Tabwidth: 4, 143 | }, 144 | } 145 | runtime.Callers(5, pc) 146 | frames := runtime.CallersFrames(pc) 147 | thisPackage := reflect.TypeOf(Unquoted("")).PkgPath() + "." 148 | for { 149 | frame, more := frames.Next() 150 | if strings.HasPrefix(frame.Function, "testing.") { 151 | // Stop before getting back to stdlib test runner calls. 152 | break 153 | } 154 | if fname := strings.TrimPrefix(frame.Function, thisPackage); fname != frame.Function { 155 | if ast.IsExported(fname) { 156 | // Continue without printing frames for quicktest exported API. 157 | continue 158 | } 159 | // Stop when entering quicktest internal calls. 160 | // This is useful for instance when using qtsuite. 161 | break 162 | } 163 | fmt.Fprint(w, prefixf(prefix, "%s:%d", frame.File, frame.Line)) 164 | if strings.HasSuffix(frame.File, ".go") { 165 | stmt, err := sg.Get(frame.File, frame.Line) 166 | if err != nil { 167 | fmt.Fprint(w, prefixf(prefix+prefix, "<%s>", err)) 168 | } else { 169 | fmt.Fprint(w, prefixf(prefix+prefix, "%s", stmt)) 170 | } 171 | } 172 | if !more { 173 | // There are no more callers. 174 | break 175 | } 176 | } 177 | } 178 | 179 | type stmtGetter struct { 180 | fset *token.FileSet 181 | files map[string]*ast.File 182 | config *printer.Config 183 | } 184 | 185 | // Get returns the lines of code of the statement at the given file and line. 186 | func (sg *stmtGetter) Get(file string, line int) (string, error) { 187 | f := sg.files[file] 188 | if f == nil { 189 | var err error 190 | f, err = parser.ParseFile(sg.fset, file, nil, parser.ParseComments) 191 | if err != nil { 192 | return "", fmt.Errorf("cannot parse source file: %s", err) 193 | } 194 | sg.files[file] = f 195 | } 196 | var stmt string 197 | ast.Inspect(f, func(n ast.Node) bool { 198 | if n == nil || stmt != "" { 199 | return false 200 | } 201 | pos := sg.fset.Position(n.Pos()).Line 202 | end := sg.fset.Position(n.End()).Line 203 | // Go < v1.9 reports the line where the statements ends, not the line 204 | // where it begins. 205 | if line == pos || line == end { 206 | var buf bytes.Buffer 207 | // TODO: include possible comment after the statement. 208 | sg.config.Fprint(&buf, sg.fset, &printer.CommentedNode{ 209 | Node: n, 210 | Comments: f.Comments, 211 | }) 212 | stmt = buf.String() 213 | return false 214 | } 215 | return pos < line && line <= end 216 | }) 217 | return stmt, nil 218 | } 219 | 220 | // prefixf formats the given string with the given args. It also inserts the 221 | // final newline if needed and indentation with the given prefix. 222 | func prefixf(prefix, format string, args ...any) string { 223 | var buf []byte 224 | s := strings.TrimSuffix(fmt.Sprintf(format, args...), "\n") 225 | for _, line := range strings.Split(s, "\n") { 226 | buf = append(buf, prefix...) 227 | buf = append(buf, line...) 228 | buf = append(buf, '\n') 229 | } 230 | return string(buf) 231 | } 232 | 233 | // note holds a key/value annotation. 234 | type note struct { 235 | key string 236 | value any 237 | } 238 | 239 | // prefix is the string used to indent blocks of output. 240 | const prefix = " " 241 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package qt_test 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "math" 8 | "net" 9 | "os" 10 | "testing" 11 | 12 | "github.com/go-quicktest/qt" 13 | "github.com/google/go-cmp/cmp" 14 | "github.com/google/go-cmp/cmp/cmpopts" 15 | ) 16 | 17 | func ExampleComment() { 18 | runExampleTest(func(t testing.TB) { 19 | a := 42 20 | qt.Assert(t, qt.Equals(a, 42), qt.Commentf("no answer to life, the universe, and everything")) 21 | }) 22 | // Output: PASS 23 | } 24 | 25 | func ExampleEquals() { 26 | runExampleTest(func(t testing.TB) { 27 | answer := int64(42) 28 | qt.Assert(t, qt.Equals(answer, 42)) 29 | }) 30 | // Output: PASS 31 | } 32 | 33 | func ExampleDeepEquals() { 34 | runExampleTest(func(t testing.TB) { 35 | list := []int{42, 47} 36 | qt.Assert(t, qt.DeepEquals(list, []int{42, 47})) 37 | }) 38 | // Output: PASS 39 | } 40 | 41 | func ExampleCmpEquals() { 42 | runExampleTest(func(t testing.TB) { 43 | list := []int{42, 47} 44 | qt.Assert(t, qt.CmpEquals(list, []int{47, 42}, cmpopts.SortSlices(func(i, j int) bool { 45 | return i < j 46 | }))) 47 | }) 48 | // Output: PASS 49 | } 50 | 51 | type myStruct struct { 52 | a int 53 | } 54 | 55 | func customDeepEquals[T any](got, want T) qt.Checker { 56 | return qt.CmpEquals(got, want, cmp.AllowUnexported(myStruct{})) 57 | } 58 | 59 | func ExampleCmpEquals_customfunc() { 60 | runExampleTest(func(t testing.TB) { 61 | got := &myStruct{ 62 | a: 1234, 63 | } 64 | qt.Assert(t, customDeepEquals(got, &myStruct{ 65 | a: 1234, 66 | })) 67 | }) 68 | // Output: PASS 69 | } 70 | 71 | func ExampleContentEquals() { 72 | runExampleTest(func(t testing.TB) { 73 | got := []int{1, 23, 4, 5} 74 | qt.Assert(t, qt.ContentEquals(got, []int{1, 4, 5, 23})) 75 | }) 76 | // Output: PASS 77 | } 78 | 79 | func ExampleMatches() { 80 | runExampleTest(func(t testing.TB) { 81 | qt.Assert(t, qt.Matches("these are the voyages", "these are .*")) 82 | qt.Assert(t, qt.Matches(net.ParseIP("1.2.3.4").String(), "1.*")) 83 | }) 84 | // Output: PASS 85 | } 86 | 87 | func ExampleErrorMatches() { 88 | runExampleTest(func(t testing.TB) { 89 | err := errors.New("bad wolf at the door") 90 | qt.Assert(t, qt.ErrorMatches(err, "bad wolf .*")) 91 | }) 92 | // Output: PASS 93 | } 94 | 95 | func ExamplePanicMatches() { 96 | runExampleTest(func(t testing.TB) { 97 | divide := func(a, b int) int { 98 | return a / b 99 | } 100 | qt.Assert(t, qt.PanicMatches(func() { 101 | divide(5, 0) 102 | }, "runtime error: .*")) 103 | }) 104 | // Output: PASS 105 | } 106 | 107 | func ExampleIsNil() { 108 | runExampleTest(func(t testing.TB) { 109 | got := (*int)(nil) 110 | qt.Assert(t, qt.IsNil(got)) 111 | }) 112 | // Output: PASS 113 | } 114 | 115 | func ExampleIsNotNil() { 116 | runExampleTest(func(t testing.TB) { 117 | got := new(int) 118 | 119 | qt.Assert(t, qt.IsNotNil(got)) 120 | 121 | // Note that unlike reflection-based APIs, a nil 122 | // value inside an interface still counts as non-nil, 123 | // just as if we were comparing the actual interface 124 | // value against nil. 125 | nilValueInInterface := any((*int)(nil)) 126 | qt.Assert(t, qt.IsNotNil(nilValueInInterface)) 127 | }) 128 | // Output: PASS 129 | } 130 | 131 | func ExampleHasLen() { 132 | runExampleTest(func(t testing.TB) { 133 | qt.Assert(t, qt.HasLen([]int{42, 47}, 2)) 134 | 135 | myMap := map[string]int{ 136 | "a": 13, 137 | "b": 4, 138 | "c": 10, 139 | } 140 | qt.Assert(t, qt.HasLen(myMap, 3)) 141 | }) 142 | // Output: PASS 143 | } 144 | 145 | func ExampleImplements() { 146 | runExampleTest(func(t testing.TB) { 147 | var myReader struct { 148 | io.ReadCloser 149 | } 150 | qt.Assert(t, qt.Implements[io.ReadCloser](myReader)) 151 | }) 152 | // Output: PASS 153 | } 154 | 155 | func ExampleSatisfies() { 156 | runExampleTest(func(t testing.TB) { 157 | // Check that an error from os.Open satisfies os.IsNotExist. 158 | _, err := os.Open("/non-existent-file") 159 | qt.Assert(t, qt.Satisfies(err, os.IsNotExist)) 160 | 161 | // Check that a floating point number is a not-a-number. 162 | f := math.NaN() 163 | qt.Assert(t, qt.Satisfies(f, math.IsNaN)) 164 | 165 | }) 166 | // Output: PASS 167 | } 168 | 169 | func ExampleIsTrue() { 170 | runExampleTest(func(t testing.TB) { 171 | isValid := func() bool { 172 | return true 173 | } 174 | qt.Assert(t, qt.IsTrue(1 == 1)) 175 | qt.Assert(t, qt.IsTrue(isValid())) 176 | }) 177 | // Output: PASS 178 | 179 | } 180 | 181 | func ExampleIsFalse() { 182 | runExampleTest(func(t testing.TB) { 183 | isValid := func() bool { 184 | return false 185 | } 186 | qt.Assert(t, qt.IsFalse(1 == 0)) 187 | qt.Assert(t, qt.IsFalse(isValid())) 188 | }) 189 | // Output: PASS 190 | 191 | } 192 | 193 | func ExampleNot() { 194 | runExampleTest(func(t testing.TB) { 195 | 196 | got := []int{1, 2} 197 | qt.Assert(t, qt.Not(qt.IsNil(got))) 198 | 199 | answer := 13 200 | qt.Assert(t, qt.Not(qt.Equals(answer, 42))) 201 | }) 202 | // Output: PASS 203 | } 204 | 205 | func ExampleStringContains() { 206 | runExampleTest(func(t testing.TB) { 207 | qt.Assert(t, qt.StringContains("hello world", "hello")) 208 | }) 209 | // Output: PASS 210 | } 211 | 212 | func ExampleSliceContains() { 213 | runExampleTest(func(t testing.TB) { 214 | qt.Assert(t, qt.SliceContains([]int{3, 5, 7, 99}, 99)) 215 | qt.Assert(t, qt.SliceContains([]string{"a", "cd", "e"}, "cd")) 216 | }) 217 | // Output: PASS 218 | } 219 | 220 | func ExampleMapContains() { 221 | runExampleTest(func(t testing.TB) { 222 | qt.Assert(t, qt.MapContains(map[string]int{ 223 | "hello": 1234, 224 | }, 1234)) 225 | }) 226 | // Output: PASS 227 | } 228 | 229 | func ExampleSliceAny() { 230 | runExampleTest(func(t testing.TB) { 231 | qt.Assert(t, qt.SliceAny([]int{3, 5, 7, 99}, qt.F2(qt.Equals[int], 7))) 232 | qt.Assert(t, qt.SliceAny([][]string{{"a", "b"}, {"c", "d"}}, qt.F2(qt.DeepEquals[[]string], []string{"c", "d"}))) 233 | }) 234 | // Output: PASS 235 | } 236 | 237 | func ExampleMapAny() { 238 | runExampleTest(func(t testing.TB) { 239 | qt.Assert(t, qt.MapAny(map[string]int{"x": 2, "y": 3}, qt.F2(qt.Equals[int], 3))) 240 | }) 241 | // Output: PASS 242 | } 243 | 244 | func ExampleSliceAll() { 245 | runExampleTest(func(t testing.TB) { 246 | qt.Assert(t, qt.SliceAll([]int{3, 5, 8}, func(e int) qt.Checker { 247 | return qt.Not(qt.Equals(e, 0)) 248 | })) 249 | qt.Assert(t, qt.SliceAll([][]string{{"a", "b"}, {"a", "b"}}, qt.F2(qt.DeepEquals[[]string], []string{"a", "b"}))) 250 | }) 251 | // Output: PASS 252 | } 253 | 254 | func ExampleMapAll() { 255 | runExampleTest(func(t testing.TB) { 256 | qt.Assert(t, qt.MapAll(map[string]int{ 257 | "x": 2, 258 | "y": 2, 259 | }, qt.F2(qt.Equals[int], 2))) 260 | }) 261 | // Output: PASS 262 | } 263 | 264 | func ExampleJSONEquals() { 265 | runExampleTest(func(t testing.TB) { 266 | data := `[1, 2, 3]` 267 | qt.Assert(t, qt.JSONEquals(data, []uint{1, 2, 3})) 268 | }) 269 | // Output: PASS 270 | } 271 | 272 | func ExampleErrorAs() { 273 | runExampleTest(func(t testing.TB) { 274 | _, err := os.Open("/non-existent-file") 275 | 276 | // Checking for a specific error type. 277 | qt.Assert(t, qt.ErrorAs(err, new(*os.PathError))) 278 | qt.Assert(t, qt.ErrorAs[*os.PathError](err, nil)) 279 | 280 | // Checking fields on a specific error type. 281 | var pathError *os.PathError 282 | if qt.Check(t, qt.ErrorAs(err, &pathError)) { 283 | qt.Assert(t, qt.Equals(pathError.Path, "/non-existent-file")) 284 | } 285 | }) 286 | // Output: PASS 287 | } 288 | 289 | func ExampleErrorIs() { 290 | runExampleTest(func(t testing.TB) { 291 | _, err := os.Open("/non-existent-file") 292 | 293 | qt.Assert(t, qt.ErrorIs(err, os.ErrNotExist)) 294 | }) 295 | // Output: PASS 296 | } 297 | 298 | func runExampleTest(f func(t testing.TB)) { 299 | defer func() { 300 | if err := recover(); err != nil && err != exampleTestFatal { 301 | panic(err) 302 | } 303 | }() 304 | var t exampleTestingT 305 | f(&t) 306 | if t.failed { 307 | fmt.Println("FAIL") 308 | } else { 309 | fmt.Println("PASS") 310 | } 311 | } 312 | 313 | type exampleTestingT struct { 314 | testing.TB 315 | failed bool 316 | } 317 | 318 | var exampleTestFatal = errors.New("example test fatal error") 319 | 320 | func (t *exampleTestingT) Helper() {} 321 | 322 | func (t *exampleTestingT) Error(args ...any) { 323 | fmt.Printf("ERROR: %s\n", fmt.Sprint(args...)) 324 | t.failed = true 325 | } 326 | 327 | func (t *exampleTestingT) Fatal(args ...any) { 328 | fmt.Printf("FATAL: %s\n", fmt.Sprint(args...)) 329 | t.failed = true 330 | panic(exampleTestFatal) 331 | } 332 | -------------------------------------------------------------------------------- /quicktest_test.go: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license, see LICENCE file for details. 2 | 3 | package qt_test 4 | 5 | import ( 6 | "bytes" 7 | "errors" 8 | "fmt" 9 | "strings" 10 | "testing" 11 | 12 | "github.com/go-quicktest/qt" 13 | ) 14 | 15 | var qtTests = []struct { 16 | about string 17 | checker qt.Checker 18 | comments []qt.Comment 19 | expectedFailure string 20 | }{{ 21 | about: "success", 22 | checker: qt.Equals(42, 42), 23 | }, { 24 | about: "failure", 25 | checker: qt.Equals("42", "47"), 26 | expectedFailure: ` 27 | error: 28 | values are not equal 29 | got: 30 | "42" 31 | want: 32 | "47" 33 | `, 34 | }, { 35 | about: "failure with % signs", 36 | checker: qt.Equals("42%x", "47%y"), 37 | expectedFailure: ` 38 | error: 39 | values are not equal 40 | got: 41 | "42%x" 42 | want: 43 | "47%y" 44 | `, 45 | }, { 46 | about: "failure with comment", 47 | checker: qt.Equals(true, false), 48 | comments: []qt.Comment{qt.Commentf("apparently %v != %v", true, false)}, 49 | expectedFailure: ` 50 | error: 51 | values are not equal 52 | comment: 53 | apparently true != false 54 | got: 55 | bool(true) 56 | want: 57 | bool(false) 58 | `, 59 | }, { 60 | about: "another failure with comment", 61 | checker: qt.IsNil(any(42)), 62 | comments: []qt.Comment{qt.Commentf("bad wolf: %d", 42)}, 63 | expectedFailure: ` 64 | error: 65 | got non-nil value 66 | comment: 67 | bad wolf: 42 68 | got: 69 | int(42) 70 | `, 71 | }, { 72 | about: "failure with constant comment", 73 | checker: qt.IsNil(any("something")), 74 | comments: []qt.Comment{qt.Commentf("these are the voyages")}, 75 | expectedFailure: ` 76 | error: 77 | got non-nil value 78 | comment: 79 | these are the voyages 80 | got: 81 | "something" 82 | `, 83 | }, { 84 | about: "failure with empty comment", 85 | checker: qt.IsNil(any(47)), 86 | comments: []qt.Comment{qt.Commentf("")}, 87 | expectedFailure: ` 88 | error: 89 | got non-nil value 90 | got: 91 | int(47) 92 | `, 93 | }, { 94 | about: "failure with multiple comments", 95 | checker: qt.IsNil(any(42)), 96 | comments: []qt.Comment{ 97 | qt.Commentf("bad wolf: %d", 42), 98 | qt.Commentf("second comment"), 99 | }, 100 | expectedFailure: ` 101 | error: 102 | got non-nil value 103 | comment: 104 | bad wolf: 42 105 | comment: 106 | second comment 107 | got: 108 | int(42) 109 | `, 110 | }, { 111 | about: "nil checker", 112 | expectedFailure: ` 113 | error: 114 | bad check: nil checker provided 115 | `, 116 | }, { 117 | about: "many arguments and notes", 118 | checker: &testingChecker{ 119 | args: []qt.Arg{{ 120 | Name: "arg1", 121 | Value: 42, 122 | }, { 123 | Name: "arg2", 124 | Value: "val2", 125 | }, { 126 | Name: "arg3", 127 | Value: "val3", 128 | }}, 129 | addNotes: func(note func(key string, value any)) { 130 | note("note1", "these") 131 | note("note2", qt.Unquoted("are")) 132 | note("note3", "the") 133 | note("note4", "voyages") 134 | note("note5", true) 135 | }, 136 | err: errors.New("bad wolf"), 137 | }, 138 | expectedFailure: ` 139 | error: 140 | bad wolf 141 | note1: 142 | "these" 143 | note2: 144 | are 145 | note3: 146 | "the" 147 | note4: 148 | "voyages" 149 | note5: 150 | bool(true) 151 | arg1: 152 | int(42) 153 | arg2: 154 | "val2" 155 | arg3: 156 | "val3" 157 | `, 158 | }, { 159 | about: "many arguments and notes with the same value", 160 | checker: &testingChecker{ 161 | args: []qt.Arg{{ 162 | Name: "arg1", 163 | Value: "value1", 164 | }, { 165 | Name: "arg2", 166 | Value: "value1", 167 | }, { 168 | Name: "arg3", 169 | Value: []int{42}, 170 | }, { 171 | Name: "arg4", 172 | Value: nil, 173 | }}, 174 | addNotes: func(note func(key string, value any)) { 175 | note("note1", "value1") 176 | note("note2", []int{42}) 177 | note("note3", "value1") 178 | note("note4", nil) 179 | }, 180 | err: errors.New("bad wolf"), 181 | }, 182 | expectedFailure: ` 183 | error: 184 | bad wolf 185 | note1: 186 | "value1" 187 | note2: 188 | []int{42} 189 | note3: 190 | 191 | note4: 192 | nil 193 | arg1: 194 | 195 | arg2: 196 | 197 | arg3: 198 | 199 | arg4: 200 | 201 | `, 202 | }, { 203 | about: "bad check with notes", 204 | checker: &testingChecker{ 205 | args: []qt.Arg{{ 206 | Name: "got", 207 | Value: 42, 208 | }, { 209 | Name: "want", 210 | Value: "want", 211 | }}, 212 | addNotes: func(note func(key string, value any)) { 213 | note("note", 42) 214 | }, 215 | err: qt.BadCheckf("bad wolf"), 216 | }, 217 | expectedFailure: ` 218 | error: 219 | bad check: bad wolf 220 | note: 221 | int(42) 222 | `, 223 | }, { 224 | about: "silent failure with notes", 225 | checker: &testingChecker{ 226 | args: []qt.Arg{{ 227 | Name: "got", 228 | Value: 42, 229 | }, { 230 | Name: "want", 231 | Value: "want", 232 | }}, 233 | addNotes: func(note func(key string, value any)) { 234 | note("note1", "first note") 235 | note("note2", qt.Unquoted("second note")) 236 | }, 237 | err: qt.ErrSilent, 238 | }, 239 | expectedFailure: ` 240 | note1: 241 | "first note" 242 | note2: 243 | second note 244 | `, 245 | }} 246 | 247 | func TestCAssertCheck(t *testing.T) { 248 | for _, test := range qtTests { 249 | t.Run("Assert: "+test.about, func(t *testing.T) { 250 | tt := &testingT{} 251 | ok := qt.Assert(tt, test.checker, test.comments...) 252 | checkResult(t, ok, tt.fatalString(), test.expectedFailure) 253 | if tt.errorString() != "" { 254 | t.Fatalf("no error messages expected, but got %q", tt.errorString()) 255 | } 256 | }) 257 | t.Run("Check: "+test.about, func(t *testing.T) { 258 | tt := &testingT{} 259 | ok := qt.Check(tt, test.checker, test.comments...) 260 | checkResult(t, ok, tt.errorString(), test.expectedFailure) 261 | if tt.fatalString() != "" { 262 | t.Fatalf("no fatal messages expected, but got %q", tt.fatalString()) 263 | } 264 | }) 265 | 266 | } 267 | } 268 | 269 | func TestHelperCalls(t *testing.T) { 270 | tt := &testingT{} 271 | qt.Assert(tt, qt.IsTrue(false)) 272 | if tt.helperCalls < 2 { 273 | t.Error("Assert doesn't invoke TB.Helper()") 274 | } 275 | 276 | tt = &testingT{} 277 | qt.Check(tt, qt.IsTrue(false)) 278 | if tt.helperCalls < 2 { 279 | t.Error("Check doesn't invoke TB.Helper()") 280 | } 281 | } 282 | 283 | func checkResult(t *testing.T, ok bool, got, want string) { 284 | t.Helper() 285 | if want != "" { 286 | assertPrefix(t, got, want+"stack:\n") 287 | assertBool(t, ok, false) 288 | return 289 | } 290 | if got != "" { 291 | t.Fatalf("output:\ngot %q\nwant empty", got) 292 | } 293 | assertBool(t, ok, true) 294 | } 295 | 296 | // testingT implements testing.TB for testing purposes. 297 | type testingT struct { 298 | testing.TB 299 | 300 | errorBuf bytes.Buffer 301 | fatalBuf bytes.Buffer 302 | 303 | subTestResult bool 304 | subTestName string 305 | subTestT *testing.T 306 | 307 | helperCalls int 308 | parallel bool 309 | } 310 | 311 | // Error overrides testing.TB.Error so that messages are collected. 312 | func (t *testingT) Error(a ...any) { 313 | fmt.Fprint(&t.errorBuf, a...) 314 | } 315 | 316 | // Fatal overrides testing.TB.Fatal so that messages are collected and the 317 | // goroutine is not killed. 318 | func (t *testingT) Fatal(a ...any) { 319 | fmt.Fprint(&t.fatalBuf, a...) 320 | } 321 | 322 | // Parallel overrides testing.TB.Parallel in order to record the call. 323 | func (t *testingT) Parallel() { 324 | t.parallel = true 325 | } 326 | 327 | // Helper overrides testing.TB.Helper in order to count calls. 328 | func (t *testingT) Helper() { 329 | t.helperCalls += 1 330 | } 331 | 332 | // Fatal overrides *testing.T.Fatal so that messages are collected and the 333 | // goroutine is not killed. 334 | func (t *testingT) Run(name string, f func(t *testing.T)) bool { 335 | t.subTestName, t.subTestT = name, &testing.T{} 336 | f(t.subTestT) 337 | return t.subTestResult 338 | } 339 | 340 | // errorString returns the error message. 341 | func (t *testingT) errorString() string { 342 | return t.errorBuf.String() 343 | } 344 | 345 | // fatalString returns the fatal error message. 346 | func (t *testingT) fatalString() string { 347 | return t.fatalBuf.String() 348 | } 349 | 350 | // assertPrefix fails if the got value does not have the given prefix. 351 | func assertPrefix(t testing.TB, got, prefix string) { 352 | t.Helper() 353 | if prefix == "" { 354 | t.Fatal("prefix: empty value provided") 355 | } 356 | if !strings.HasPrefix(got, prefix) { 357 | t.Fatalf(`prefix: 358 | got %q 359 | want %q 360 | -------------------- got -------------------- 361 | %s 362 | -------------------- want ------------------- 363 | %s 364 | ---------------------------------------------`, got, prefix, got, prefix) 365 | } 366 | } 367 | 368 | // assertBool fails if the given boolean values don't match. 369 | func assertBool(t testing.TB, got, want bool) { 370 | t.Helper() 371 | if got != want { 372 | t.Fatalf("bool:\ngot %v\nwant %v", got, want) 373 | } 374 | } 375 | 376 | // testingChecker is a quicktest.Checker used in tests. It receives the 377 | // provided argNames, adds notes via the provided addNotes function, and when 378 | // the check is run the provided error is returned. 379 | type testingChecker struct { 380 | args []qt.Arg 381 | addNotes func(note func(key string, value any)) 382 | err error 383 | } 384 | 385 | // Check implements quicktest.Checker by returning the stored error. 386 | func (c *testingChecker) Check(note func(key string, value any)) error { 387 | if c.addNotes != nil { 388 | c.addNotes(note) 389 | } 390 | return c.err 391 | } 392 | 393 | // Args implements quicktest.Checker by returning the stored args. 394 | func (c *testingChecker) Args() []qt.Arg { 395 | return c.args 396 | } 397 | -------------------------------------------------------------------------------- /checker.go: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license, see LICENSE file for details. 2 | 3 | package qt 4 | 5 | import ( 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "reflect" 10 | "regexp" 11 | "strings" 12 | 13 | "github.com/google/go-cmp/cmp" 14 | "github.com/google/go-cmp/cmp/cmpopts" 15 | "github.com/kr/pretty" 16 | ) 17 | 18 | // Checker is implemented by types used as part of Check/Assert invocations. 19 | type Checker interface { 20 | // Check runs the check for this checker. 21 | // On failure, the returned error is printed along with 22 | // the checker arguments (obtained by calling Args) 23 | // and key-value pairs added by calling the note function. 24 | // 25 | // If Check returns ErrSilent, neither the checker arguments nor 26 | // the error are printed; values with note are still printed. 27 | Check(note func(key string, value any)) error 28 | 29 | // Args returns a slice of all the arguments passed 30 | // to the checker. The first argument should always be 31 | // the "got" value being checked. 32 | Args() []Arg 33 | } 34 | 35 | // Arg holds a single argument to a checker. 36 | type Arg struct { 37 | Name string 38 | Value any 39 | } 40 | 41 | // negatedError is implemented on checkers that want to customize the error that 42 | // is returned when they have succeeded but that success has been negated. 43 | type negatedError interface { 44 | negatedError() error 45 | } 46 | 47 | // Equals returns a Checker checking equality of two comparable values. 48 | // 49 | // Note that T is not constrained to be comparable because 50 | // we also allow comparing interface values which currently 51 | // do not satisfy that constraint. 52 | func Equals[T any](got, want T) Checker { 53 | return &equalsChecker[T]{argPairOf(got, want)} 54 | } 55 | 56 | type equalsChecker[T any] struct { 57 | argPair[T, T] 58 | } 59 | 60 | func (c *equalsChecker[T]) Check(note func(key string, value any)) (err error) { 61 | defer func() { 62 | // A panic is raised when the provided values are interfaces containing 63 | // non-comparable values. 64 | if r := recover(); r != nil { 65 | err = fmt.Errorf("%s", r) 66 | } 67 | }() 68 | 69 | if any(c.got) == any(c.want) { 70 | return nil 71 | } 72 | 73 | // Customize error message for non-nil errors. 74 | if typeOf[T]() == typeOf[error]() { 75 | if any(c.want) == nil { 76 | return errors.New("got non-nil error") 77 | } 78 | if any(c.got) == nil { 79 | return errors.New("got nil error") 80 | } 81 | // Show error types when comparing errors with different types. 82 | gotType := reflect.TypeOf(c.got) 83 | wantType := reflect.TypeOf(c.want) 84 | if gotType != wantType { 85 | note("got type", Unquoted(gotType.String())) 86 | note("want type", Unquoted(wantType.String())) 87 | } 88 | return errors.New("values are not equal") 89 | } 90 | 91 | // Show line diff when comparing different multi-line strings. 92 | if c, ok := any(c).(*equalsChecker[string]); ok { 93 | isMultiLine := func(s string) bool { 94 | i := strings.Index(s, "\n") 95 | return i != -1 && i < len(s)-1 96 | } 97 | if isMultiLine(c.got) || isMultiLine(c.want) { 98 | diff := cmp.Diff(strings.SplitAfter(c.want, "\n"), strings.SplitAfter(c.got, "\n")) 99 | note("line diff (-want +got)", Unquoted(diff)) 100 | } 101 | } 102 | 103 | return errors.New("values are not equal") 104 | } 105 | 106 | // DeepEquals returns a Checker checking equality of two values 107 | // using cmp.DeepEqual. 108 | func DeepEquals[T any](got, want T) Checker { 109 | return CmpEquals(got, want) 110 | } 111 | 112 | // CmpEquals is like DeepEquals but allows custom compare options 113 | // to be passed too, to allow unexported fields to be compared. 114 | // 115 | // It can be useful to define your own version that uses a custom 116 | // set of compare options. See example for details. 117 | func CmpEquals[T any](got, want T, opts ...cmp.Option) Checker { 118 | return &cmpEqualsChecker[T]{ 119 | argPair: argPairOf(got, want), 120 | opts: opts, 121 | } 122 | } 123 | 124 | type cmpEqualsChecker[T any] struct { 125 | argPair[T, T] 126 | opts []cmp.Option 127 | } 128 | 129 | func (c *cmpEqualsChecker[T]) Check(note func(key string, value any)) (err error) { 130 | defer func() { 131 | // A panic is raised in some cases, for instance when trying to compare 132 | // structs with unexported fields and neither AllowUnexported nor 133 | // cmpopts.IgnoreUnexported are provided. 134 | if r := recover(); r != nil { 135 | err = BadCheckf("%s", r) 136 | } 137 | }() 138 | if diff := cmp.Diff(c.want, c.got, c.opts...); diff != "" { 139 | // Only output values when the verbose flag is set. 140 | note("error", Unquoted("values are not deep equal")) 141 | note("diff (-want +got)", Unquoted(diff)) 142 | note("got", SuppressedIfLong{c.got}) 143 | note("want", SuppressedIfLong{c.want}) 144 | return ErrSilent 145 | } 146 | return nil 147 | } 148 | 149 | // ContentEquals is like DeepEquals but any slices in the compared values will 150 | // be sorted before being compared. 151 | func ContentEquals[T any](got, want T) Checker { 152 | return CmpEquals(got, want, cmpopts.SortSlices(func(x, y any) bool { 153 | // TODO frankban: implement a proper sort function. 154 | return pretty.Sprint(x) < pretty.Sprint(y) 155 | })) 156 | } 157 | 158 | // Matches returns a Checker checking that the provided string matches the 159 | // provided regular expression pattern. If want is a string, the pattern will be 160 | // anchored; that is: 161 | // 162 | // Matches(got, "foo") 163 | // 164 | // is equivalent to: 165 | // 166 | // Matches(got, "^foo$") 167 | // 168 | // If want is of type *regexp.Regexp, it will be matched as is. 169 | func Matches[StringOrRegexp string | *regexp.Regexp](got string, want StringOrRegexp) Checker { 170 | return &matchesChecker{ 171 | got: got, 172 | want: want, 173 | match: newMatcher(want), 174 | } 175 | } 176 | 177 | type matchesChecker struct { 178 | got string 179 | want any 180 | match matcher 181 | } 182 | 183 | func (c *matchesChecker) Check(note func(key string, value any)) error { 184 | return c.match(c.got, "value does not match regexp", note) 185 | } 186 | 187 | func (c *matchesChecker) Args() []Arg { 188 | return []Arg{{Name: "got value", Value: c.got}, {Name: "regexp", Value: c.want}} 189 | } 190 | 191 | // ErrorMatches returns a Checker checking that the provided value is an error 192 | // whose message matches the provided regular expression pattern 193 | // (see [Matches] for more details on how the pattern is matched). 194 | func ErrorMatches[StringOrRegexp string | *regexp.Regexp](got error, want StringOrRegexp) Checker { 195 | return &errorMatchesChecker{ 196 | got: got, 197 | want: want, 198 | match: newMatcher(want), 199 | } 200 | } 201 | 202 | type errorMatchesChecker struct { 203 | got error 204 | want any 205 | match matcher 206 | } 207 | 208 | func (c *errorMatchesChecker) Check(note func(key string, value any)) error { 209 | if c.got == nil { 210 | return errors.New("got nil error but want non-nil") 211 | } 212 | return c.match(c.got.Error(), "error does not match regexp", note) 213 | } 214 | 215 | func (c *errorMatchesChecker) Args() []Arg { 216 | return []Arg{{Name: "got error", Value: c.got}, {Name: "regexp", Value: c.want}} 217 | } 218 | 219 | // PanicMatches returns a Checker checking that the provided function panics 220 | // with a message matching the provided regular expression pattern. 221 | // (see [Matches] for more details on how the pattern is matched). 222 | func PanicMatches[StringOrRegexp string | *regexp.Regexp](f func(), want StringOrRegexp) Checker { 223 | return &panicMatchesChecker{ 224 | got: f, 225 | want: want, 226 | match: newMatcher(want), 227 | } 228 | } 229 | 230 | type panicMatchesChecker struct { 231 | got func() 232 | want any 233 | match matcher 234 | } 235 | 236 | func (c *panicMatchesChecker) Check(note func(key string, value any)) (err error) { 237 | defer func() { 238 | r := recover() 239 | if r == nil { 240 | err = errors.New("function did not panic") 241 | return 242 | } 243 | msg := fmt.Sprint(r) 244 | note("panic value", msg) 245 | err = c.match(msg, "panic value does not match regexp", note) 246 | }() 247 | c.got() 248 | return nil 249 | } 250 | 251 | func (c *panicMatchesChecker) Args() []Arg { 252 | return []Arg{{Name: "function", Value: c.got}, {Name: "regexp", Value: c.want}} 253 | } 254 | 255 | // IsNil returns a Checker checking that the provided value is equal to nil. 256 | // 257 | // Note that an interface value containing a nil concrete 258 | // type is not considered to be nil. 259 | func IsNil[T any](got T) Checker { 260 | return isNilChecker[T]{ 261 | got: got, 262 | } 263 | } 264 | 265 | type isNilChecker[T any] struct { 266 | got T 267 | } 268 | 269 | func (c isNilChecker[T]) Check(note func(key string, value any)) error { 270 | v := reflect.ValueOf(&c.got).Elem() 271 | if !canBeNil(v.Kind()) { 272 | return BadCheckf("type %v can never be nil", v.Type()) 273 | } 274 | if v.IsNil() { 275 | return nil 276 | } 277 | return errors.New("got non-nil value") 278 | } 279 | 280 | func (c isNilChecker[T]) Args() []Arg { 281 | return []Arg{{Name: "got", Value: c.got}} 282 | } 283 | 284 | func (c isNilChecker[T]) negatedError() error { 285 | v := reflect.ValueOf(c.got) 286 | if v.IsValid() { 287 | return fmt.Errorf("got nil %s but want non-nil", v.Kind()) 288 | } 289 | return errors.New("got but want non-nil") 290 | } 291 | 292 | // IsNotNil returns a Checker checking that the provided value is not nil. 293 | // IsNotNil(v) is the equivalent of qt.Not(qt.IsNil(v)). 294 | func IsNotNil[T any](got T) Checker { 295 | return Not(IsNil(got)) 296 | } 297 | 298 | // HasLen returns a Checker checking that the provided value has the given 299 | // length. The value may be a slice, array, channel, map or string. 300 | func HasLen[T any](got T, n int) Checker { 301 | return &hasLenChecker[T]{ 302 | got: got, 303 | wantLen: n, 304 | } 305 | } 306 | 307 | type hasLenChecker[T any] struct { 308 | got T 309 | wantLen int 310 | } 311 | 312 | func (c *hasLenChecker[T]) Check(note func(key string, value any)) (err error) { 313 | // TODO we're deliberately not allowing HasLen(interfaceValue) here. 314 | // Perhaps we should? 315 | v := reflect.ValueOf(&c.got).Elem() 316 | switch v.Kind() { 317 | case reflect.Array, reflect.Chan, reflect.Map, reflect.Slice, reflect.String: 318 | case reflect.Pointer: 319 | // Allow a pointer to array. 320 | if reflect.Indirect(reflect.ValueOf(c.got)).Kind() == reflect.Array { 321 | break 322 | } 323 | 324 | // Error on all other pointers. 325 | note("got", c.got) 326 | return BadCheckf("first argument of type %v has no length", v.Type()) 327 | default: 328 | note("got", c.got) 329 | return BadCheckf("first argument of type %v has no length", v.Type()) 330 | } 331 | length := v.Len() 332 | note("len(got)", length) 333 | if length != c.wantLen { 334 | return fmt.Errorf("unexpected length") 335 | } 336 | return nil 337 | } 338 | 339 | func (c *hasLenChecker[T]) Args() []Arg { 340 | return []Arg{{Name: "got", Value: c.got}, {Name: "want length", Value: c.wantLen}} 341 | } 342 | 343 | // Implements returns a Checker checking that the provided value implements the 344 | // interface specified by the type parameter. 345 | func Implements[I any](got any) Checker { 346 | return &implementsChecker{ 347 | got: got, 348 | want: typeOf[I](), 349 | } 350 | } 351 | 352 | type implementsChecker struct { 353 | got any 354 | want reflect.Type 355 | } 356 | 357 | func (c *implementsChecker) Check(note func(key string, value any)) (err error) { 358 | if c.got == nil { 359 | note("error", Unquoted("got nil value but want non-nil")) 360 | note("got", c.got) 361 | return ErrSilent 362 | } 363 | if c.want.Kind() != reflect.Interface { 364 | note("want interface", Unquoted(c.want.String())) 365 | return BadCheckf("want an interface type but a concrete type was provided") 366 | } 367 | 368 | gotType := reflect.TypeOf(c.got) 369 | if !gotType.Implements(c.want) { 370 | return fmt.Errorf("got value does not implement wanted interface") 371 | } 372 | 373 | return nil 374 | } 375 | 376 | func (c *implementsChecker) Args() []Arg { 377 | return []Arg{{Name: "got", Value: c.got}, {Name: "want interface", Value: Unquoted(c.want.String())}} 378 | 379 | } 380 | 381 | // Satisfies returns a Checker checking that the provided value, when used as 382 | // argument of the provided predicate function, causes the function to return 383 | // true. 384 | func Satisfies[T any](got T, f func(T) bool) Checker { 385 | return &satisfiesChecker[T]{ 386 | got: got, 387 | predicate: f, 388 | } 389 | } 390 | 391 | type satisfiesChecker[T any] struct { 392 | got T 393 | predicate func(T) bool 394 | } 395 | 396 | // Check implements Checker.Check by checking that args[0](got) == true. 397 | func (c *satisfiesChecker[T]) Check(note func(key string, value any)) error { 398 | if c.predicate(c.got) { 399 | return nil 400 | } 401 | return fmt.Errorf("value does not satisfy predicate function") 402 | } 403 | 404 | func (c *satisfiesChecker[T]) Args() []Arg { 405 | return []Arg{{ 406 | Name: "got", 407 | Value: c.got, 408 | }, { 409 | Name: "predicate", 410 | Value: c.predicate, 411 | }} 412 | } 413 | 414 | // IsTrue returns a Checker checking that the provided value is true. 415 | func IsTrue[T ~bool](got T) Checker { 416 | return Equals(got, true) 417 | } 418 | 419 | // IsFalse returns a Checker checking that the provided value is false. 420 | func IsFalse[T ~bool](got T) Checker { 421 | return Equals(got, false) 422 | } 423 | 424 | // Not returns a Checker negating the given Checker. 425 | func Not(c Checker) Checker { 426 | // Not(Not(c)) becomes c. 427 | if c, ok := c.(notChecker); ok { 428 | return c.Checker 429 | } 430 | return notChecker{ 431 | Checker: c, 432 | } 433 | } 434 | 435 | type notChecker struct { 436 | Checker 437 | } 438 | 439 | func (c notChecker) Check(note func(key string, value any)) error { 440 | err := c.Checker.Check(note) 441 | if IsBadCheck(err) { 442 | return err 443 | } 444 | if err != nil { 445 | return nil 446 | } 447 | if c, ok := c.Checker.(negatedError); ok { 448 | return c.negatedError() 449 | } 450 | return errors.New("unexpected success") 451 | } 452 | 453 | // StringContains returns a Checker checking that the given string contains the 454 | // given substring. 455 | func StringContains[T ~string](got, substr T) Checker { 456 | return &stringContainsChecker[T]{ 457 | got: got, 458 | substr: substr, 459 | } 460 | } 461 | 462 | type stringContainsChecker[T ~string] struct { 463 | got, substr T 464 | } 465 | 466 | func (c *stringContainsChecker[T]) Check(note func(key string, value any)) error { 467 | if strings.Contains(string(c.got), string(c.substr)) { 468 | return nil 469 | } 470 | return errors.New("no substring match found") 471 | } 472 | 473 | func (c *stringContainsChecker[T]) Args() []Arg { 474 | return []Arg{{ 475 | Name: "got", 476 | Value: c.got, 477 | }, { 478 | Name: "substr", 479 | Value: c.substr, 480 | }} 481 | } 482 | 483 | // SliceContains returns a Checker that succeeds if the given 484 | // slice contains the given element, by comparing for equality. 485 | func SliceContains[T any](container []T, elem T) Checker { 486 | return SliceAny(container, F2(Equals[T], elem)) 487 | } 488 | 489 | // MapContains returns a Checker that succeeds if the given value is 490 | // contained in the values of the given map, by comparing for equality. 491 | func MapContains[K comparable, V any](container map[K]V, elem V) Checker { 492 | return MapAny(container, F2(Equals[V], elem)) 493 | } 494 | 495 | // SliceAny returns a Checker that uses the given checker to check elements 496 | // of a slice. It succeeds if f(v) passes the check for any v in the slice. 497 | // 498 | // See the F2 function for a way to adapt a regular checker function 499 | // to the type expected for the f argument here. 500 | // 501 | // See also SliceAll and SliceContains. 502 | func SliceAny[T any](container []T, f func(elem T) Checker) Checker { 503 | return &anyChecker[T]{ 504 | newIter: func() containerIter[T] { 505 | return newSliceIter(container) 506 | }, 507 | container: container, 508 | elemChecker: f, 509 | } 510 | } 511 | 512 | // MapAny returns a Checker that uses checkers returned by f to check values 513 | // of a map. It succeeds if f(v) passes the check for any value v in the map. 514 | // 515 | // See the F2 function for a way to adapt a regular checker function 516 | // to the type expected for the f argument here. 517 | // 518 | // See also MapAll and MapContains. 519 | func MapAny[K comparable, V any](container map[K]V, f func(elem V) Checker) Checker { 520 | return &anyChecker[V]{ 521 | newIter: func() containerIter[V] { 522 | return newMapIter(container) 523 | }, 524 | container: container, 525 | elemChecker: f, 526 | } 527 | } 528 | 529 | type anyChecker[T any] struct { 530 | newIter func() containerIter[T] 531 | container any 532 | elemChecker func(T) Checker 533 | } 534 | 535 | func (c *anyChecker[T]) Check(note func(key string, value any)) error { 536 | for iter := c.newIter(); iter.next(); { 537 | // For the time being, discard the notes added by the sub-checker, 538 | // because it's not clear what a good behavior would be. 539 | // Should we print all the failed check for all elements? If there's only 540 | // one element in the container, the answer is probably yes, 541 | // but let's leave it for now. 542 | checker := c.elemChecker(iter.value()) 543 | err := checker.Check( 544 | func(key string, value any) {}, 545 | ) 546 | if err == nil { 547 | return nil 548 | } 549 | if IsBadCheck(err) { 550 | return BadCheckf("at %s: %v", iter.key(), err) 551 | } 552 | } 553 | return errors.New("no matching element found") 554 | } 555 | 556 | func (c *anyChecker[T]) Args() []Arg { 557 | // We haven't got an instance of the underlying checker, 558 | // so just make one by passing the zero value. In general 559 | // no checker should panic when being created regardless 560 | // of the actual arguments, so that should be OK. 561 | args := []Arg{{ 562 | Name: "container", 563 | Value: c.container, 564 | }} 565 | if eargs := c.elemChecker(*new(T)).Args(); len(eargs) > 0 { 566 | args = append(args, eargs[1:]...) 567 | } 568 | return args 569 | } 570 | 571 | // SliceAll returns a Checker that uses checkers returned by f 572 | // to check elements of a slice. It succeeds if all elements 573 | // of the slice pass the check. 574 | // On failure it prints the error from the first index that failed. 575 | func SliceAll[T any](container []T, f func(elem T) Checker) Checker { 576 | return &allChecker[T]{ 577 | newIter: func() containerIter[T] { 578 | return newSliceIter(container) 579 | }, 580 | container: container, 581 | elemChecker: f, 582 | } 583 | } 584 | 585 | // MapAll returns a Checker that uses checkers returned by f to check values 586 | // of a map. It succeeds if f(v) passes the check for all values v in the map. 587 | func MapAll[K comparable, V any](container map[K]V, f func(elem V) Checker) Checker { 588 | return &allChecker[V]{ 589 | newIter: func() containerIter[V] { 590 | return newMapIter(container) 591 | }, 592 | container: container, 593 | elemChecker: f, 594 | } 595 | } 596 | 597 | type allChecker[T any] struct { 598 | newIter func() containerIter[T] 599 | container any 600 | elemChecker func(T) Checker 601 | } 602 | 603 | func (c *allChecker[T]) Check(notef func(key string, value any)) error { 604 | for iter := c.newIter(); iter.next(); { 605 | // Store any notes added by the checker so 606 | // we can add our own note at the start 607 | // to say which element failed. 608 | var notes []note 609 | checker := c.elemChecker(iter.value()) 610 | err := checker.Check( 611 | func(key string, val any) { 612 | notes = append(notes, note{key, val}) 613 | }, 614 | ) 615 | if err == nil { 616 | continue 617 | } 618 | if IsBadCheck(err) { 619 | return BadCheckf("at %s: %v", iter.key(), err) 620 | } 621 | notef("error", Unquoted("mismatch at "+iter.key())) 622 | // TODO should we print the whole container value in 623 | // verbose mode? 624 | if err != ErrSilent { 625 | // If the error's not silent, the checker is expecting 626 | // the caller to print the error and the value that failed. 627 | notef("error", Unquoted(err.Error())) 628 | notef("first mismatched element", iter.value()) 629 | } 630 | for _, n := range notes { 631 | notef(n.key, n.value) 632 | } 633 | return ErrSilent 634 | } 635 | return nil 636 | } 637 | 638 | func (c *allChecker[T]) Args() []Arg { 639 | // We haven't got an instance of the underlying checker, 640 | // so just make one by passing the zero value. In general 641 | // no checker should panic when being created regardless 642 | // of the actual arguments, so that should be OK. 643 | args := []Arg{{ 644 | Name: "container", 645 | Value: c.container, 646 | }} 647 | if eargs := c.elemChecker(*new(T)).Args(); len(eargs) > 0 { 648 | args = append(args, eargs[1:]...) 649 | } 650 | return args 651 | } 652 | 653 | // JSONEquals returns a Checker that checks whether a string or byte slice is 654 | // JSON-equivalent to a Go value. See CodecEquals for more information. 655 | // 656 | // It uses DeepEquals to do the comparison. If a more sophisticated comparison 657 | // is required, use CodecEquals directly. 658 | func JSONEquals[T []byte | string](got T, want any) Checker { 659 | return CodecEquals(got, want, json.Marshal, json.Unmarshal) 660 | } 661 | 662 | // CodecEquals returns a Checker that checks for codec value equivalence. 663 | // 664 | // It expects two arguments: a byte slice or a string containing some 665 | // codec-marshaled data, and a Go value. 666 | // 667 | // It uses unmarshal to unmarshal the data into an interface{} value. 668 | // It marshals the Go value using marshal, then unmarshals the result into 669 | // an any value. 670 | // 671 | // It then checks that the two interface{} values are deep-equal to one 672 | // another, using CmpEquals(opts) to perform the check. 673 | // 674 | // See JSONEquals for an example of this in use. 675 | func CodecEquals[T []byte | string]( 676 | got T, 677 | want any, 678 | marshal func(any) ([]byte, error), 679 | unmarshal func([]byte, any) error, 680 | opts ...cmp.Option, 681 | ) Checker { 682 | return &codecEqualChecker[T]{ 683 | argPair: argPairOf(got, want), 684 | marshal: marshal, 685 | unmarshal: unmarshal, 686 | opts: opts, 687 | } 688 | } 689 | 690 | type codecEqualChecker[T []byte | string] struct { 691 | argPair[T, any] 692 | marshal func(any) ([]byte, error) 693 | unmarshal func([]byte, any) error 694 | opts []cmp.Option 695 | } 696 | 697 | func (c *codecEqualChecker[T]) Check(note func(key string, value any)) error { 698 | wantContentBytes, err := c.marshal(c.want) 699 | if err != nil { 700 | return BadCheckf("cannot marshal expected contents: %v", err) 701 | } 702 | var wantContentVal any 703 | if err := c.unmarshal(wantContentBytes, &wantContentVal); err != nil { 704 | return BadCheckf("cannot unmarshal expected contents: %v", err) 705 | } 706 | var gotContentVal any 707 | if err := c.unmarshal([]byte(c.got), &gotContentVal); err != nil { 708 | return fmt.Errorf("cannot unmarshal obtained contents: %v; %q", err, c.got) 709 | } 710 | cmpEq := CmpEquals(gotContentVal, wantContentVal, c.opts...).(*cmpEqualsChecker[any]) 711 | return cmpEq.Check(note) 712 | } 713 | 714 | // ErrorAs retruns a Checker checking that the error is or wraps a specific 715 | // error type. If so, it assigns it to the provided pointer. This is analogous 716 | // to calling errors.As. 717 | func ErrorAs[T any](got error, want *T) Checker { 718 | return &errorAsChecker[T]{ 719 | got: got, 720 | want: want, 721 | } 722 | } 723 | 724 | type errorAsChecker[T any] struct { 725 | got error 726 | want *T 727 | } 728 | 729 | func (c *errorAsChecker[T]) Check(note func(key string, value any)) (err error) { 730 | if c.got == nil { 731 | return errors.New("got nil error but want non-nil") 732 | } 733 | defer func() { 734 | // A panic is raised when the target is not a pointer to an interface 735 | // or error. 736 | if r := recover(); r != nil { 737 | err = BadCheckf("%s", r) 738 | } 739 | }() 740 | want := c.want 741 | if want == nil { 742 | want = new(T) 743 | } 744 | if !errors.As(c.got, want) { 745 | return errors.New("wanted type is not found in error chain") 746 | } 747 | return nil 748 | } 749 | 750 | func (c *errorAsChecker[T]) Args() []Arg { 751 | return []Arg{{ 752 | Name: "got", 753 | Value: c.got, 754 | }, { 755 | Name: "as type", 756 | Value: Unquoted(typeOf[T]().String()), 757 | }} 758 | } 759 | 760 | // ErrorIs returns a Checker that checks that the error is or wraps a specific 761 | // error value. This is analogous to calling errors.Is. 762 | func ErrorIs(got, want error) Checker { 763 | return &errorIsChecker{ 764 | argPair: argPairOf(got, want), 765 | } 766 | } 767 | 768 | type errorIsChecker struct { 769 | argPair[error, error] 770 | } 771 | 772 | func (c *errorIsChecker) Check(note func(key string, value any)) error { 773 | if c.got == nil && c.want != nil { 774 | return errors.New("got nil error but want non-nil") 775 | } 776 | if !errors.Is(c.got, c.want) { 777 | return errors.New("wanted error is not found in error chain") 778 | } 779 | return nil 780 | } 781 | 782 | type matcher = func(got string, msg string, note func(key string, value any)) error 783 | 784 | // newMatcher returns a matcher function that can be used by checkers when 785 | // checking that a string or an error matches the provided StringOrRegexp. 786 | func newMatcher[StringOrRegexp string | *regexp.Regexp](regex StringOrRegexp) matcher { 787 | var re *regexp.Regexp 788 | switch r := any(regex).(type) { 789 | case string: 790 | re0, err := regexp.Compile("^(" + r + ")$") 791 | if err != nil { 792 | return func(got string, msg string, note func(key string, value any)) error { 793 | note("regexp", r) 794 | return BadCheckf("cannot compile regexp: %s", err) 795 | } 796 | } 797 | re = re0 798 | case *regexp.Regexp: 799 | re = r 800 | } 801 | return func(got string, msg string, note func(key string, value any)) error { 802 | if re.MatchString(got) { 803 | return nil 804 | } 805 | return errors.New(msg) 806 | } 807 | } 808 | 809 | func argPairOf[A, B any](a A, b B) argPair[A, B] { 810 | return argPair[A, B]{a, b} 811 | } 812 | 813 | type argPair[A, B any] struct { 814 | got A 815 | want B 816 | } 817 | 818 | func (p argPair[A, B]) Args() []Arg { 819 | return []Arg{{ 820 | Name: "got", 821 | Value: p.got, 822 | }, { 823 | Name: "want", 824 | Value: p.want, 825 | }} 826 | } 827 | 828 | // canBeNil reports whether a value or type of the given kind can be nil. 829 | func canBeNil(k reflect.Kind) bool { 830 | switch k { 831 | case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice: 832 | return true 833 | } 834 | return false 835 | } 836 | 837 | func typeOf[T any]() reflect.Type { 838 | return reflect.TypeOf((*T)(nil)).Elem() 839 | } 840 | 841 | func valueAs[T any](v reflect.Value) (r T) { 842 | reflect.ValueOf(&r).Elem().Set(v) 843 | return 844 | } 845 | 846 | // F2 factors a 2-argument checker function into a single argument function suitable 847 | // for passing to an *Any or *All checker. Whenever the returned function is called, 848 | // cf is called with arguments (got, want). 849 | func F2[Got, Want any](cf func(got Got, want Want) Checker, want Want) func(got Got) Checker { 850 | return func(got Got) Checker { 851 | return cf(got, want) 852 | } 853 | } 854 | -------------------------------------------------------------------------------- /checker_test.go: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license, see LICENSE file for details. 2 | 3 | package qt_test 4 | 5 | import ( 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "regexp" 10 | "strings" 11 | "testing" 12 | "time" 13 | 14 | "github.com/google/go-cmp/cmp" 15 | "github.com/google/go-cmp/cmp/cmpopts" 16 | 17 | "github.com/go-quicktest/qt" 18 | ) 19 | 20 | // errTarget is an error implemented as a pointer. 21 | type errTarget struct { 22 | msg string 23 | } 24 | 25 | func (e *errTarget) Error() string { 26 | return "ptr: " + e.msg 27 | } 28 | 29 | // errTargetNonPtr is an error implemented as a non-pointer. 30 | type errTargetNonPtr struct { 31 | msg string 32 | } 33 | 34 | func (e errTargetNonPtr) Error() string { 35 | return "non ptr: " + e.msg 36 | } 37 | 38 | // Fooer is an interface for testing. 39 | type Fooer interface { 40 | Foo() 41 | } 42 | 43 | type cmpType struct { 44 | Strings []any 45 | Ints []int 46 | } 47 | 48 | type InnerJSON struct { 49 | First string 50 | Second int `json:",omitempty" yaml:",omitempty"` 51 | Third map[string]bool `json:",omitempty" yaml:",omitempty"` 52 | } 53 | 54 | type OuterJSON struct { 55 | First float64 56 | Second []*InnerJSON `json:"Last,omitempty" yaml:"last,omitempty"` 57 | } 58 | 59 | type boolean bool 60 | 61 | var ( 62 | targetErr = &errTarget{msg: "target"} 63 | 64 | goTime = time.Date(2012, 3, 28, 0, 0, 0, 0, time.UTC) 65 | chInt = func() chan int { 66 | ch := make(chan int, 4) 67 | ch <- 42 68 | ch <- 47 69 | return ch 70 | }() 71 | sameInts = cmpopts.SortSlices(func(x, y int) bool { 72 | return x < y 73 | }) 74 | 75 | cmpEqualsGot = cmpType{ 76 | Strings: []any{"who", "dalek"}, 77 | Ints: []int{42, 47}, 78 | } 79 | cmpEqualsWant = cmpType{ 80 | Strings: []any{"who", "dalek"}, 81 | Ints: []int{42}, 82 | } 83 | ) 84 | 85 | var checkerTests = []struct { 86 | about string 87 | checker qt.Checker 88 | verbose bool 89 | expectedCheckFailure string 90 | expectedNegateFailure string 91 | }{{ 92 | about: "Equals: same values", 93 | checker: qt.Equals(42, 42), 94 | expectedNegateFailure: ` 95 | error: 96 | unexpected success 97 | got: 98 | int(42) 99 | want: 100 | 101 | `, 102 | }, { 103 | about: "Equals: different values", 104 | checker: qt.Equals("42", "47"), 105 | expectedCheckFailure: ` 106 | error: 107 | values are not equal 108 | got: 109 | "42" 110 | want: 111 | "47" 112 | `, 113 | }, { 114 | about: "Equals: different strings with quotes", 115 | checker: qt.Equals(`string "foo"`, `string "bar"`), 116 | expectedCheckFailure: tilde2bq(` 117 | error: 118 | values are not equal 119 | got: 120 | ~string "foo"~ 121 | want: 122 | ~string "bar"~ 123 | `), 124 | }, { 125 | about: "Equals: same multiline strings", 126 | checker: qt.Equals("a\nmultiline\nstring", "a\nmultiline\nstring"), 127 | expectedNegateFailure: ` 128 | error: 129 | unexpected success 130 | got: 131 | "a\nmultiline\nstring" 132 | want: 133 | 134 | `}, { 135 | about: "Equals: different multi-line strings", 136 | checker: qt.Equals("a\nlong\nmultiline\nstring", "just\na\nlong\nmulti-line\nstring\n"), 137 | expectedCheckFailure: fmt.Sprintf(` 138 | error: 139 | values are not equal 140 | line diff (-want +got): 141 | %s 142 | got: 143 | "a\nlong\nmultiline\nstring" 144 | want: 145 | "just\na\nlong\nmulti-line\nstring\n" 146 | `, diff([]string{"a\n", "long\n", "multiline\n", "string"}, []string{"just\n", "a\n", "long\n", "multi-line\n", "string\n", ""})), 147 | }, { 148 | about: "Equals: different single-line strings ending with newline", 149 | checker: qt.Equals("foo\n", "bar\n"), 150 | expectedCheckFailure: ` 151 | error: 152 | values are not equal 153 | got: 154 | "foo\n" 155 | want: 156 | "bar\n" 157 | `, 158 | }, { 159 | about: "Equals: different strings starting with newline", 160 | checker: qt.Equals("\nfoo", "\nbar"), 161 | expectedCheckFailure: fmt.Sprintf(` 162 | error: 163 | values are not equal 164 | line diff (-want +got): 165 | %s 166 | got: 167 | "\nfoo" 168 | want: 169 | "\nbar" 170 | `, diff([]string{"\n", "foo"}, []string{"\n", "bar"})), 171 | }, { 172 | about: "Equals: different types", 173 | checker: qt.Equals(42, any("42")), 174 | expectedCheckFailure: ` 175 | error: 176 | values are not equal 177 | got: 178 | int(42) 179 | want: 180 | "42" 181 | `}, { 182 | about: "Equals: nil and nil", 183 | checker: qt.Equals(nil, any(nil)), 184 | expectedNegateFailure: ` 185 | error: 186 | unexpected success 187 | got: 188 | nil 189 | want: 190 | 191 | `, 192 | }, { 193 | about: "Equals: error is not nil", 194 | checker: qt.Equals(error(errBadWolf), error(nil)), 195 | expectedCheckFailure: ` 196 | error: 197 | got non-nil error 198 | got: 199 | bad wolf 200 | file:line 201 | want: 202 | nil 203 | `}, { 204 | about: "Equals: error is not nil: not formatted", 205 | checker: qt.Equals[error](&errTest{ 206 | msg: "bad wolf", 207 | }, nil), 208 | expectedCheckFailure: ` 209 | error: 210 | got non-nil error 211 | got: 212 | e"bad wolf" 213 | want: 214 | nil 215 | `, 216 | }, { 217 | about: "Equals: error does not guard against nil", 218 | checker: qt.Equals[error]((*errTest)(nil), nil), 219 | expectedCheckFailure: ` 220 | error: 221 | got non-nil error 222 | got: 223 | e 224 | want: 225 | nil 226 | `, 227 | }, { 228 | about: "Equals: error is not nil: not formatted and with quotes", 229 | checker: qt.Equals[error](&errTest{ 230 | msg: `failure: "bad wolf"`, 231 | }, nil), 232 | expectedCheckFailure: tilde2bq(` 233 | error: 234 | got non-nil error 235 | got: 236 | e~failure: "bad wolf"~ 237 | want: 238 | nil 239 | `), 240 | }, { 241 | about: "Equals: different errors with same message", 242 | checker: qt.Equals[error](&errTest{ 243 | msg: "bad wolf", 244 | }, errors.New("bad wolf")), 245 | expectedCheckFailure: ` 246 | error: 247 | values are not equal 248 | got type: 249 | *qt_test.errTest 250 | want type: 251 | *errors.errorString 252 | got: 253 | e"bad wolf" 254 | want: 255 | 256 | `, 257 | }, { 258 | about: "Equals: different pointer errors with the same message", 259 | checker: qt.Equals(targetErr, &errTarget{msg: "target"}), 260 | expectedCheckFailure: ` 261 | error: 262 | values are not equal 263 | got: 264 | e"ptr: target" 265 | want: 266 | 267 | `, 268 | }, { 269 | about: "Equals: different pointers with the same formatted output", 270 | checker: qt.Equals(new(int), new(int)), 271 | expectedCheckFailure: ` 272 | error: 273 | values are not equal 274 | got: 275 | &int(0) 276 | want: 277 | 278 | `, 279 | }, { 280 | about: "Equals: nil struct", 281 | checker: qt.Equals[any]((*struct{})(nil), nil), 282 | expectedCheckFailure: ` 283 | error: 284 | values are not equal 285 | got: 286 | (*struct {})(nil) 287 | want: 288 | nil 289 | `, 290 | }, { 291 | about: "Equals: different booleans", 292 | checker: qt.Equals(true, false), 293 | expectedCheckFailure: ` 294 | error: 295 | values are not equal 296 | got: 297 | bool(true) 298 | want: 299 | bool(false) 300 | `, 301 | }, { 302 | about: "Equals: uncomparable types", 303 | checker: qt.Equals[any](struct { 304 | Ints []int 305 | }{ 306 | Ints: []int{42, 47}, 307 | }, struct { 308 | Ints []int 309 | }{ 310 | Ints: []int{42, 47}, 311 | }), 312 | expectedCheckFailure: ` 313 | error: 314 | runtime error: comparing uncomparable type struct { Ints []int } 315 | got: 316 | struct { Ints []int }{ 317 | Ints: {42, 47}, 318 | } 319 | want: 320 | 321 | `}, { 322 | about: "DeepEquals: same values", 323 | checker: qt.DeepEquals(cmpEqualsGot, cmpEqualsGot), 324 | expectedNegateFailure: ` 325 | error: 326 | unexpected success 327 | got: 328 | qt_test.cmpType{ 329 | Strings: { 330 | "who", 331 | "dalek", 332 | }, 333 | Ints: {42, 47}, 334 | } 335 | want: 336 | 337 | `, 338 | }, { 339 | about: "DeepEquals: different values", 340 | checker: qt.DeepEquals(cmpEqualsGot, cmpEqualsWant), 341 | expectedCheckFailure: fmt.Sprintf(` 342 | error: 343 | values are not deep equal 344 | diff (-want +got): 345 | %s 346 | got: 347 | qt_test.cmpType{ 348 | Strings: { 349 | "who", 350 | "dalek", 351 | }, 352 | Ints: {42, 47}, 353 | } 354 | want: 355 | qt_test.cmpType{ 356 | Strings: { 357 | "who", 358 | "dalek", 359 | }, 360 | Ints: {42}, 361 | } 362 | `, diff(cmpEqualsGot, cmpEqualsWant)), 363 | }, { 364 | about: "DeepEquals: different values: long output", 365 | checker: qt.DeepEquals([]any{cmpEqualsWant, cmpEqualsWant}, []any{cmpEqualsWant, cmpEqualsWant, 42}), 366 | expectedCheckFailure: fmt.Sprintf(` 367 | error: 368 | values are not deep equal 369 | diff (-want +got): 370 | %s 371 | got: 372 | 373 | want: 374 | 375 | `, diff([]any{cmpEqualsWant, cmpEqualsWant}, []any{cmpEqualsWant, cmpEqualsWant, 42})), 376 | }, { 377 | about: "DeepEquals: different values: long output and verbose", 378 | checker: qt.DeepEquals([]any{cmpEqualsWant, cmpEqualsWant}, []any{cmpEqualsWant, cmpEqualsWant, 42}), 379 | verbose: true, 380 | expectedCheckFailure: fmt.Sprintf(` 381 | error: 382 | values are not deep equal 383 | diff (-want +got): 384 | %s 385 | got: 386 | []interface {}{ 387 | qt_test.cmpType{ 388 | Strings: { 389 | "who", 390 | "dalek", 391 | }, 392 | Ints: {42}, 393 | }, 394 | qt_test.cmpType{ 395 | Strings: { 396 | "who", 397 | "dalek", 398 | }, 399 | Ints: {42}, 400 | }, 401 | } 402 | want: 403 | []interface {}{ 404 | qt_test.cmpType{ 405 | Strings: { 406 | "who", 407 | "dalek", 408 | }, 409 | Ints: {42}, 410 | }, 411 | qt_test.cmpType{ 412 | Strings: { 413 | "who", 414 | "dalek", 415 | }, 416 | Ints: {42}, 417 | }, 418 | int(42), 419 | } 420 | `, diff([]any{cmpEqualsWant, cmpEqualsWant}, []any{cmpEqualsWant, cmpEqualsWant, 42})), 421 | }, { 422 | about: "CmpEquals: different values, long output", 423 | checker: qt.CmpEquals([]any{cmpEqualsWant, "extra line 1", "extra line 2", "extra line 3"}, []any{cmpEqualsWant, "extra line 1"}), 424 | expectedCheckFailure: fmt.Sprintf(` 425 | error: 426 | values are not deep equal 427 | diff (-want +got): 428 | %s 429 | got: 430 | 431 | want: 432 | []interface {}{ 433 | qt_test.cmpType{ 434 | Strings: { 435 | "who", 436 | "dalek", 437 | }, 438 | Ints: {42}, 439 | }, 440 | "extra line 1", 441 | } 442 | `, diff([]any{cmpEqualsWant, "extra line 1", "extra line 2", "extra line 3"}, []any{cmpEqualsWant, "extra line 1"})), 443 | }, { 444 | about: "CmpEquals: different values: long output and verbose", 445 | checker: qt.CmpEquals([]any{cmpEqualsWant, "extra line 1", "extra line 2"}, []any{cmpEqualsWant, "extra line 1"}), 446 | verbose: true, 447 | expectedCheckFailure: fmt.Sprintf(` 448 | error: 449 | values are not deep equal 450 | diff (-want +got): 451 | %s 452 | got: 453 | []interface {}{ 454 | qt_test.cmpType{ 455 | Strings: { 456 | "who", 457 | "dalek", 458 | }, 459 | Ints: {42}, 460 | }, 461 | "extra line 1", 462 | "extra line 2", 463 | } 464 | want: 465 | []interface {}{ 466 | qt_test.cmpType{ 467 | Strings: { 468 | "who", 469 | "dalek", 470 | }, 471 | Ints: {42}, 472 | }, 473 | "extra line 1", 474 | } 475 | `, diff([]any{cmpEqualsWant, "extra line 1", "extra line 2"}, []any{cmpEqualsWant, "extra line 1"})), 476 | }, { 477 | about: "CmpEquals: different values, long output, same number of lines", 478 | checker: qt.CmpEquals([]any{cmpEqualsWant, "extra line 1", "extra line 2", "extra line 3"}, []any{cmpEqualsWant, "extra line 1", "extra line 2", "extra line three"}), 479 | expectedCheckFailure: fmt.Sprintf(` 480 | error: 481 | values are not deep equal 482 | diff (-want +got): 483 | %s 484 | got: 485 | 486 | want: 487 | 488 | `, diff([]any{cmpEqualsWant, "extra line 1", "extra line 2", "extra line 3"}, []any{cmpEqualsWant, "extra line 1", "extra line 2", "extra line three"})), 489 | }, { 490 | about: "CmpEquals: same values with options", 491 | checker: qt.CmpEquals([]int{1, 2, 3}, []int{3, 2, 1}, sameInts), 492 | expectedNegateFailure: ` 493 | error: 494 | unexpected success 495 | got: 496 | []int{1, 2, 3} 497 | want: 498 | []int{3, 2, 1} 499 | `, 500 | }, { 501 | about: "CmpEquals: different values with options", 502 | checker: qt.CmpEquals([]int{1, 2, 4}, []int{3, 2, 1}, sameInts), 503 | expectedCheckFailure: fmt.Sprintf(` 504 | error: 505 | values are not deep equal 506 | diff (-want +got): 507 | %s 508 | got: 509 | []int{1, 2, 4} 510 | want: 511 | []int{3, 2, 1} 512 | `, diff([]int{1, 2, 4}, []int{3, 2, 1}, sameInts)), 513 | }, { 514 | about: "DeepEquals: structs with unexported fields not allowed", 515 | checker: qt.DeepEquals( 516 | struct{ answer int }{ 517 | answer: 42, 518 | }, 519 | struct{ answer int }{ 520 | answer: 42, 521 | }, 522 | ), 523 | expectedCheckFailure: ` 524 | error: 525 | bad check: cannot handle unexported field at root.answer: 526 | "github.com/go-quicktest/qt_test".(struct { answer int }) 527 | consider using a custom Comparer; if you control the implementation of type, you can also consider using an Exporter, AllowUnexported, or cmpopts.IgnoreUnexported 528 | `, 529 | expectedNegateFailure: ` 530 | error: 531 | bad check: cannot handle unexported field at root.answer: 532 | "github.com/go-quicktest/qt_test".(struct { answer int }) 533 | consider using a custom Comparer; if you control the implementation of type, you can also consider using an Exporter, AllowUnexported, or cmpopts.IgnoreUnexported 534 | `, 535 | }, { 536 | about: "CmpEquals: structs with unexported fields ignored", 537 | checker: qt.CmpEquals( 538 | struct{ answer int }{ 539 | answer: 42, 540 | }, 541 | struct{ answer int }{ 542 | answer: 42, 543 | }, cmpopts.IgnoreUnexported(struct{ answer int }{})), 544 | expectedNegateFailure: ` 545 | error: 546 | unexpected success 547 | got: 548 | struct { answer int }{answer:42} 549 | want: 550 | 551 | `, 552 | }, { 553 | about: "DeepEquals: same times", 554 | checker: qt.DeepEquals(goTime, goTime), 555 | expectedNegateFailure: ` 556 | error: 557 | unexpected success 558 | got: 559 | s"2012-03-28 00:00:00 +0000 UTC" 560 | want: 561 | 562 | `, 563 | }, { 564 | about: "DeepEquals: different times: verbose", 565 | checker: qt.DeepEquals(goTime.Add(24*time.Hour), goTime), 566 | verbose: true, 567 | expectedCheckFailure: fmt.Sprintf(` 568 | error: 569 | values are not deep equal 570 | diff (-want +got): 571 | %s 572 | got: 573 | s"2012-03-29 00:00:00 +0000 UTC" 574 | want: 575 | s"2012-03-28 00:00:00 +0000 UTC" 576 | `, diff(goTime.Add(24*time.Hour), goTime)), 577 | }, { 578 | about: "ContentEquals: same values", 579 | checker: qt.ContentEquals([]string{"these", "are", "the", "voyages"}, []string{"these", "are", "the", "voyages"}), 580 | expectedNegateFailure: ` 581 | error: 582 | unexpected success 583 | got: 584 | []string{"these", "are", "the", "voyages"} 585 | want: 586 | 587 | `, 588 | }, { 589 | about: "ContentEquals: same contents", 590 | checker: qt.ContentEquals([]int{1, 2, 3}, []int{3, 2, 1}), 591 | expectedNegateFailure: ` 592 | error: 593 | unexpected success 594 | got: 595 | []int{1, 2, 3} 596 | want: 597 | []int{3, 2, 1} 598 | `, 599 | }, { 600 | about: "ContentEquals: same contents on complex slice", 601 | checker: qt.ContentEquals( 602 | []struct { 603 | Strings []any 604 | Ints []int 605 | }{cmpEqualsGot, cmpEqualsGot, cmpEqualsWant}, 606 | []struct { 607 | Strings []any 608 | Ints []int 609 | }{cmpEqualsWant, cmpEqualsGot, cmpEqualsGot}, 610 | ), 611 | expectedNegateFailure: ` 612 | error: 613 | unexpected success 614 | got: 615 | []struct { Strings []interface {}; Ints []int }{ 616 | { 617 | Strings: { 618 | "who", 619 | "dalek", 620 | }, 621 | Ints: {42, 47}, 622 | }, 623 | { 624 | Strings: { 625 | "who", 626 | "dalek", 627 | }, 628 | Ints: {42, 47}, 629 | }, 630 | { 631 | Strings: { 632 | "who", 633 | "dalek", 634 | }, 635 | Ints: {42}, 636 | }, 637 | } 638 | want: 639 | []struct { Strings []interface {}; Ints []int }{ 640 | { 641 | Strings: { 642 | "who", 643 | "dalek", 644 | }, 645 | Ints: {42}, 646 | }, 647 | { 648 | Strings: { 649 | "who", 650 | "dalek", 651 | }, 652 | Ints: {42, 47}, 653 | }, 654 | { 655 | Strings: { 656 | "who", 657 | "dalek", 658 | }, 659 | Ints: {42, 47}, 660 | }, 661 | } 662 | `}, { 663 | about: "ContentEquals: same contents on a nested slice", 664 | checker: qt.ContentEquals( 665 | struct { 666 | Nums []int 667 | }{ 668 | Nums: []int{1, 2, 3, 4}, 669 | }, 670 | struct { 671 | Nums []int 672 | }{ 673 | Nums: []int{4, 3, 2, 1}, 674 | }, 675 | ), 676 | expectedNegateFailure: ` 677 | error: 678 | unexpected success 679 | got: 680 | struct { Nums []int }{ 681 | Nums: {1, 2, 3, 4}, 682 | } 683 | want: 684 | struct { Nums []int }{ 685 | Nums: {4, 3, 2, 1}, 686 | } 687 | `, 688 | }, { 689 | about: "ContentEquals: slices of different type", 690 | checker: qt.ContentEquals[any]([]string{"bad", "wolf"}, []any{"bad", "wolf"}), 691 | expectedCheckFailure: fmt.Sprintf(` 692 | error: 693 | values are not deep equal 694 | diff (-want +got): 695 | %s 696 | got: 697 | []string{"bad", "wolf"} 698 | want: 699 | []interface {}{ 700 | "bad", 701 | "wolf", 702 | } 703 | `, diff([]string{"bad", "wolf"}, []any{"bad", "wolf"})), 704 | }, { 705 | about: "Matches: perfect match", 706 | checker: qt.Matches("exterminate", "exterminate"), 707 | expectedNegateFailure: ` 708 | error: 709 | unexpected success 710 | got value: 711 | "exterminate" 712 | regexp: 713 | 714 | `, 715 | }, { 716 | about: "Matches: match", 717 | checker: qt.Matches("these are the voyages", "these are the .*"), 718 | expectedNegateFailure: ` 719 | error: 720 | unexpected success 721 | got value: 722 | "these are the voyages" 723 | regexp: 724 | "these are the .*" 725 | `, 726 | }, { 727 | about: "Matches: mismatch", 728 | checker: qt.Matches("voyages", "these are the voyages"), 729 | expectedCheckFailure: ` 730 | error: 731 | value does not match regexp 732 | got value: 733 | "voyages" 734 | regexp: 735 | "these are the voyages" 736 | `, 737 | }, { 738 | about: "Matches: empty pattern", 739 | checker: qt.Matches("these are the voyages", ""), 740 | expectedCheckFailure: ` 741 | error: 742 | value does not match regexp 743 | got value: 744 | "these are the voyages" 745 | regexp: 746 | "" 747 | `, 748 | }, { 749 | about: "Matches: complex pattern", 750 | checker: qt.Matches("end of the universe", "bad wolf|end of the .*"), 751 | expectedNegateFailure: ` 752 | error: 753 | unexpected success 754 | got value: 755 | "end of the universe" 756 | regexp: 757 | "bad wolf|end of the .*" 758 | `, 759 | }, { 760 | about: "Matches: invalid pattern", 761 | checker: qt.Matches("voyages", "("), 762 | expectedCheckFailure: ` 763 | error: 764 | bad check: cannot compile regexp: error parsing regexp: missing closing ): ` + "`^(()$`" + ` 765 | regexp: 766 | "(" 767 | `, 768 | expectedNegateFailure: ` 769 | error: 770 | bad check: cannot compile regexp: error parsing regexp: missing closing ): ` + "`^(()$`" + ` 771 | regexp: 772 | "(" 773 | `, 774 | }, { 775 | about: "Matches: match with pre-compiled regexp", 776 | checker: qt.Matches("resistance is futile", regexp.MustCompile("resistance is (futile|useful)")), 777 | expectedNegateFailure: ` 778 | error: 779 | unexpected success 780 | got value: 781 | "resistance is futile" 782 | regexp: 783 | s"resistance is (futile|useful)" 784 | `, 785 | }, { 786 | about: "Matches: mismatch with pre-compiled regexp", 787 | checker: qt.Matches("resistance is cool", regexp.MustCompile("resistance is (futile|useful)")), 788 | expectedCheckFailure: ` 789 | error: 790 | value does not match regexp 791 | got value: 792 | "resistance is cool" 793 | regexp: 794 | s"resistance is (futile|useful)" 795 | `, 796 | }, { 797 | about: "Matches: match with pre-compiled multi-line regexp", 798 | checker: qt.Matches("line 1\nline 2", regexp.MustCompile(`line \d\nline \d`)), 799 | expectedNegateFailure: ` 800 | error: 801 | unexpected success 802 | got value: 803 | "line 1\nline 2" 804 | regexp: 805 | s"line \\d\\nline \\d" 806 | `, 807 | }, { 808 | about: "ErrorMatches: perfect match", 809 | checker: qt.ErrorMatches(errBadWolf, "bad wolf"), 810 | expectedNegateFailure: ` 811 | error: 812 | unexpected success 813 | got error: 814 | bad wolf 815 | file:line 816 | regexp: 817 | "bad wolf" 818 | `, 819 | }, { 820 | about: "ErrorMatches: match", 821 | checker: qt.ErrorMatches(errBadWolf, "bad .*"), 822 | expectedNegateFailure: ` 823 | error: 824 | unexpected success 825 | got error: 826 | bad wolf 827 | file:line 828 | regexp: 829 | "bad .*" 830 | `, 831 | }, { 832 | about: "ErrorMatches: mismatch", 833 | checker: qt.ErrorMatches(errBadWolf, "exterminate"), 834 | expectedCheckFailure: ` 835 | error: 836 | error does not match regexp 837 | got error: 838 | bad wolf 839 | file:line 840 | regexp: 841 | "exterminate" 842 | `, 843 | }, { 844 | about: "ErrorMatches: empty pattern", 845 | checker: qt.ErrorMatches(errBadWolf, ""), 846 | expectedCheckFailure: ` 847 | error: 848 | error does not match regexp 849 | got error: 850 | bad wolf 851 | file:line 852 | regexp: 853 | "" 854 | `, 855 | }, { 856 | about: "ErrorMatches: complex pattern", 857 | checker: qt.ErrorMatches(errBadWolf, "bad wolf|end of the universe"), 858 | expectedNegateFailure: ` 859 | error: 860 | unexpected success 861 | got error: 862 | bad wolf 863 | file:line 864 | regexp: 865 | "bad wolf|end of the universe" 866 | `, 867 | }, { 868 | about: "ErrorMatches: invalid pattern", 869 | checker: qt.ErrorMatches(errBadWolf, "("), 870 | expectedCheckFailure: ` 871 | error: 872 | bad check: cannot compile regexp: error parsing regexp: missing closing ): ` + "`^(()$`" + ` 873 | regexp: 874 | "(" 875 | `, 876 | expectedNegateFailure: ` 877 | error: 878 | bad check: cannot compile regexp: error parsing regexp: missing closing ): ` + "`^(()$`" + ` 879 | regexp: 880 | "(" 881 | `, 882 | }, { 883 | about: "ErrorMatches: nil error", 884 | checker: qt.ErrorMatches(nil, "some pattern"), 885 | expectedCheckFailure: ` 886 | error: 887 | got nil error but want non-nil 888 | got error: 889 | nil 890 | regexp: 891 | "some pattern" 892 | `, 893 | }, { 894 | about: "ErrorMatches: match with pre-compiled regexp", 895 | checker: qt.ErrorMatches(errBadWolf, regexp.MustCompile("bad (wolf|dog)")), 896 | expectedNegateFailure: ` 897 | error: 898 | unexpected success 899 | got error: 900 | bad wolf 901 | file:line 902 | regexp: 903 | s"bad (wolf|dog)" 904 | `, 905 | }, { 906 | about: "ErrorMatches: match with pre-compiled multi-line regexp", 907 | checker: qt.ErrorMatches(errBadWolfMultiLine, regexp.MustCompile(`bad (wolf|dog)\nfaulty (logic|statement)`)), 908 | expectedNegateFailure: ` 909 | error: 910 | unexpected success 911 | got error: 912 | bad wolf 913 | faulty logic 914 | file:line 915 | regexp: 916 | s"bad (wolf|dog)\\nfaulty (logic|statement)" 917 | `, 918 | }, { 919 | about: "ErrorMatches: mismatch with pre-compiled regexp", 920 | checker: qt.ErrorMatches(errBadWolf, regexp.MustCompile("good (wolf|dog)")), 921 | expectedCheckFailure: ` 922 | error: 923 | error does not match regexp 924 | got error: 925 | bad wolf 926 | file:line 927 | regexp: 928 | s"good (wolf|dog)" 929 | `, 930 | }, { 931 | about: "PanicMatches: perfect match", 932 | checker: qt.PanicMatches(func() { panic("error: bad wolf") }, "error: bad wolf"), 933 | expectedNegateFailure: ` 934 | error: 935 | unexpected success 936 | panic value: 937 | "error: bad wolf" 938 | function: 939 | func() {...} 940 | regexp: 941 | 942 | `, 943 | }, { 944 | about: "PanicMatches: match", 945 | checker: qt.PanicMatches(func() { panic("error: bad wolf") }, "error: .*"), 946 | expectedNegateFailure: ` 947 | error: 948 | unexpected success 949 | panic value: 950 | "error: bad wolf" 951 | function: 952 | func() {...} 953 | regexp: 954 | "error: .*" 955 | `, 956 | }, { 957 | about: "PanicMatches: mismatch", 958 | checker: qt.PanicMatches(func() { panic("error: bad wolf") }, "error: exterminate"), 959 | expectedCheckFailure: ` 960 | error: 961 | panic value does not match regexp 962 | panic value: 963 | "error: bad wolf" 964 | function: 965 | func() {...} 966 | regexp: 967 | "error: exterminate" 968 | `, 969 | }, { 970 | about: "PanicMatches: empty pattern", 971 | checker: qt.PanicMatches(func() { panic("error: bad wolf") }, ""), 972 | expectedCheckFailure: ` 973 | error: 974 | panic value does not match regexp 975 | panic value: 976 | "error: bad wolf" 977 | function: 978 | func() {...} 979 | regexp: 980 | "" 981 | `, 982 | }, { 983 | about: "PanicMatches: complex pattern", 984 | checker: qt.PanicMatches(func() { panic("bad wolf") }, "bad wolf|end of the universe"), 985 | expectedNegateFailure: ` 986 | error: 987 | unexpected success 988 | panic value: 989 | "bad wolf" 990 | function: 991 | func() {...} 992 | regexp: 993 | "bad wolf|end of the universe" 994 | `, 995 | }, { 996 | about: "PanicMatches: invalid pattern", 997 | checker: qt.PanicMatches(func() { panic("error: bad wolf") }, "("), 998 | expectedCheckFailure: ` 999 | error: 1000 | bad check: cannot compile regexp: error parsing regexp: missing closing ): ` + "`^(()$`" + ` 1001 | panic value: 1002 | "error: bad wolf" 1003 | regexp: 1004 | "(" 1005 | `, 1006 | expectedNegateFailure: ` 1007 | error: 1008 | bad check: cannot compile regexp: error parsing regexp: missing closing ): ` + "`^(()$`" + ` 1009 | panic value: 1010 | "error: bad wolf" 1011 | regexp: 1012 | "(" 1013 | `, 1014 | }, { 1015 | about: "PanicMatches: no panic", 1016 | checker: qt.PanicMatches(func() {}, ".*"), 1017 | expectedCheckFailure: ` 1018 | error: 1019 | function did not panic 1020 | function: 1021 | func() {...} 1022 | regexp: 1023 | ".*" 1024 | `, 1025 | }, { 1026 | about: "PanicMatches: match with pre-compiled regexp", 1027 | checker: qt.PanicMatches(func() { panic("error: bad wolf") }, regexp.MustCompile("error: bad (wolf|dog)")), 1028 | expectedNegateFailure: ` 1029 | error: 1030 | unexpected success 1031 | panic value: 1032 | "error: bad wolf" 1033 | function: 1034 | func() {...} 1035 | regexp: 1036 | s"error: bad (wolf|dog)" 1037 | `, 1038 | }, { 1039 | about: "PanicMatches: match with pre-compiled multi-line regexp", 1040 | checker: qt.PanicMatches(func() { panic("error: bad wolf\nfaulty logic") }, regexp.MustCompile(`error: bad (wolf|dog)\nfaulty (logic|statement)`)), 1041 | expectedNegateFailure: ` 1042 | error: 1043 | unexpected success 1044 | panic value: 1045 | "error: bad wolf\nfaulty logic" 1046 | function: 1047 | func() {...} 1048 | regexp: 1049 | s"error: bad (wolf|dog)\\nfaulty (logic|statement)" 1050 | `, 1051 | }, { 1052 | about: "PanicMatches: mismatch with pre-compiled regexp", 1053 | checker: qt.PanicMatches(func() { panic("error: bad wolf") }, regexp.MustCompile("good (wolf|dog)")), 1054 | expectedCheckFailure: ` 1055 | error: 1056 | panic value does not match regexp 1057 | panic value: 1058 | "error: bad wolf" 1059 | function: 1060 | func() {...} 1061 | regexp: 1062 | s"good (wolf|dog)" 1063 | `, 1064 | }, { 1065 | about: "IsNil: nil", 1066 | checker: qt.IsNil(any(nil)), 1067 | expectedNegateFailure: ` 1068 | error: 1069 | got but want non-nil 1070 | got: 1071 | nil 1072 | `, 1073 | }, { 1074 | about: "IsNil: nil pointer to struct", 1075 | checker: qt.IsNil((*struct{})(nil)), 1076 | expectedNegateFailure: ` 1077 | error: 1078 | got nil ptr but want non-nil 1079 | got: 1080 | (*struct {})(nil) 1081 | `, 1082 | }, { 1083 | about: "IsNil: nil func", 1084 | checker: qt.IsNil((func())(nil)), 1085 | expectedNegateFailure: ` 1086 | error: 1087 | got nil func but want non-nil 1088 | got: 1089 | func() {...} 1090 | `, 1091 | }, { 1092 | about: "IsNil: nil map", 1093 | checker: qt.IsNil((map[string]string)(nil)), 1094 | expectedNegateFailure: ` 1095 | error: 1096 | got nil map but want non-nil 1097 | got: 1098 | map[string]string{} 1099 | `, 1100 | }, { 1101 | about: "IsNil: nil slice", 1102 | checker: qt.IsNil(([]int)(nil)), 1103 | expectedNegateFailure: ` 1104 | error: 1105 | got nil slice but want non-nil 1106 | got: 1107 | []int(nil) 1108 | `, 1109 | }, { 1110 | about: "IsNil: nil error-implementing type", 1111 | checker: qt.IsNil(error((*errTest)(nil))), 1112 | // TODO e isn't great here - perhaps we should 1113 | // mention the type too. 1114 | expectedCheckFailure: ` 1115 | error: 1116 | got non-nil value 1117 | got: 1118 | e 1119 | `, 1120 | }, { 1121 | about: "IsNil: not nil", 1122 | checker: qt.IsNil([]int{}), 1123 | expectedCheckFailure: ` 1124 | error: 1125 | got non-nil value 1126 | got: 1127 | []int{} 1128 | `, 1129 | }, { 1130 | about: "IsNil: error is not nil", 1131 | checker: qt.IsNil(error(errBadWolf)), 1132 | expectedCheckFailure: ` 1133 | error: 1134 | got non-nil value 1135 | got: 1136 | bad wolf 1137 | file:line 1138 | `, 1139 | }, { 1140 | about: "IsNotNil: success", 1141 | checker: qt.IsNotNil(any(42)), 1142 | expectedNegateFailure: ` 1143 | error: 1144 | got non-nil value 1145 | got: 1146 | int(42) 1147 | `, 1148 | }, { 1149 | about: "IsNotNil: failure", 1150 | checker: qt.IsNotNil[any](nil), 1151 | expectedCheckFailure: ` 1152 | error: 1153 | got but want non-nil 1154 | got: 1155 | nil 1156 | `, 1157 | }, { 1158 | about: "HasLen: arrays with the same length", 1159 | checker: qt.HasLen([4]string{"these", "are", "the", "voyages"}, 4), 1160 | expectedNegateFailure: ` 1161 | error: 1162 | unexpected success 1163 | len(got): 1164 | int(4) 1165 | got: 1166 | [4]string{"these", "are", "the", "voyages"} 1167 | want length: 1168 | 1169 | `, 1170 | }, { 1171 | about: "HasLen: pointers to array with the same length", 1172 | checker: qt.HasLen(&[4]string{"these", "are", "the", "voyages"}, 4), 1173 | expectedNegateFailure: ` 1174 | error: 1175 | unexpected success 1176 | len(got): 1177 | int(4) 1178 | got: 1179 | &[4]string{"these", "are", "the", "voyages"} 1180 | want length: 1181 | 1182 | `, 1183 | }, { 1184 | about: "HasLen: channels with the same length", 1185 | checker: qt.HasLen(chInt, 2), 1186 | expectedNegateFailure: fmt.Sprintf(` 1187 | error: 1188 | unexpected success 1189 | len(got): 1190 | int(2) 1191 | got: 1192 | (chan int)(%v) 1193 | want length: 1194 | 1195 | `, chInt), 1196 | }, { 1197 | about: "HasLen: maps with the same length", 1198 | checker: qt.HasLen(map[string]bool{"true": true}, 1), 1199 | expectedNegateFailure: ` 1200 | error: 1201 | unexpected success 1202 | len(got): 1203 | int(1) 1204 | got: 1205 | map[string]bool{"true":true} 1206 | want length: 1207 | 1208 | `, 1209 | }, { 1210 | about: "HasLen: slices with the same length", 1211 | checker: qt.HasLen([]int{}, 0), 1212 | expectedNegateFailure: ` 1213 | error: 1214 | unexpected success 1215 | len(got): 1216 | int(0) 1217 | got: 1218 | []int{} 1219 | want length: 1220 | 1221 | `, 1222 | }, { 1223 | about: "HasLen: strings with the same length", 1224 | checker: qt.HasLen("these are the voyages", 21), 1225 | expectedNegateFailure: ` 1226 | error: 1227 | unexpected success 1228 | len(got): 1229 | int(21) 1230 | got: 1231 | "these are the voyages" 1232 | want length: 1233 | 1234 | `, 1235 | }, { 1236 | about: "HasLen: arrays with different lengths", 1237 | checker: qt.HasLen([4]string{"these", "are", "the", "voyages"}, 0), 1238 | expectedCheckFailure: ` 1239 | error: 1240 | unexpected length 1241 | len(got): 1242 | int(4) 1243 | got: 1244 | [4]string{"these", "are", "the", "voyages"} 1245 | want length: 1246 | int(0) 1247 | `, 1248 | }, { 1249 | about: "HasLen: channels with different lengths", 1250 | checker: qt.HasLen(chInt, 4), 1251 | expectedCheckFailure: fmt.Sprintf(` 1252 | error: 1253 | unexpected length 1254 | len(got): 1255 | int(2) 1256 | got: 1257 | (chan int)(%v) 1258 | want length: 1259 | int(4) 1260 | `, chInt), 1261 | }, { 1262 | about: "HasLen: maps with different lengths", 1263 | checker: qt.HasLen(map[string]bool{"true": true}, 42), 1264 | expectedCheckFailure: ` 1265 | error: 1266 | unexpected length 1267 | len(got): 1268 | int(1) 1269 | got: 1270 | map[string]bool{"true":true} 1271 | want length: 1272 | int(42) 1273 | `, 1274 | }, { 1275 | about: "HasLen: slices with different lengths", 1276 | checker: qt.HasLen([]int{42, 47}, 1), 1277 | expectedCheckFailure: ` 1278 | error: 1279 | unexpected length 1280 | len(got): 1281 | int(2) 1282 | got: 1283 | []int{42, 47} 1284 | want length: 1285 | int(1) 1286 | `, 1287 | }, { 1288 | about: "HasLen: strings with different lengths", 1289 | checker: qt.HasLen("these are the voyages", 42), 1290 | expectedCheckFailure: ` 1291 | error: 1292 | unexpected length 1293 | len(got): 1294 | int(21) 1295 | got: 1296 | "these are the voyages" 1297 | want length: 1298 | int(42) 1299 | `, 1300 | }, { 1301 | about: "HasLen: value without a length", 1302 | checker: qt.HasLen(42, 42), 1303 | expectedCheckFailure: ` 1304 | error: 1305 | bad check: first argument of type int has no length 1306 | got: 1307 | int(42) 1308 | `, 1309 | expectedNegateFailure: ` 1310 | error: 1311 | bad check: first argument of type int has no length 1312 | got: 1313 | int(42) 1314 | `, 1315 | }, { 1316 | about: "HasLen: pointer value without a length", 1317 | checker: qt.HasLen(&[]string{"arrays", "are", "fine", "but", "not", "slices"}, 1), 1318 | expectedCheckFailure: ` 1319 | error: 1320 | bad check: first argument of type *[]string has no length 1321 | got: 1322 | &[]string{"arrays", "are", "fine", "but", "not", "slices"} 1323 | `, 1324 | expectedNegateFailure: ` 1325 | error: 1326 | bad check: first argument of type *[]string has no length 1327 | got: 1328 | &[]string{"arrays", "are", "fine", "but", "not", "slices"} 1329 | `, 1330 | }, { 1331 | about: "Implements: implements interface", 1332 | checker: qt.Implements[error](errBadWolf), 1333 | expectedNegateFailure: ` 1334 | error: 1335 | unexpected success 1336 | got: 1337 | bad wolf 1338 | file:line 1339 | want interface: 1340 | error 1341 | `, 1342 | }, { 1343 | about: "Implements: does not implement interface", 1344 | checker: qt.Implements[Fooer](errBadWolf), 1345 | expectedCheckFailure: ` 1346 | error: 1347 | got value does not implement wanted interface 1348 | got: 1349 | bad wolf 1350 | file:line 1351 | want interface: 1352 | qt_test.Fooer 1353 | `, 1354 | }, { 1355 | about: "Implements: fails if got nil", 1356 | checker: qt.Implements[Fooer](nil), 1357 | expectedCheckFailure: ` 1358 | error: 1359 | got nil value but want non-nil 1360 | got: 1361 | nil 1362 | `, 1363 | }, { 1364 | about: "Satisfies: success with an error", 1365 | checker: qt.Satisfies(qt.BadCheckf("bad wolf"), qt.IsBadCheck), 1366 | expectedNegateFailure: ` 1367 | error: 1368 | unexpected success 1369 | got: 1370 | e"bad check: bad wolf" 1371 | predicate: 1372 | func(error) bool {...} 1373 | `, 1374 | }, { 1375 | about: "Satisfies: success with an int", 1376 | checker: qt.Satisfies(42, func(v int) bool { return v == 42 }), 1377 | expectedNegateFailure: ` 1378 | error: 1379 | unexpected success 1380 | got: 1381 | int(42) 1382 | predicate: 1383 | func(int) bool {...} 1384 | `, 1385 | }, { 1386 | about: "Satisfies: success with nil", 1387 | checker: qt.Satisfies([]int(nil), func(v []int) bool { return true }), 1388 | expectedNegateFailure: ` 1389 | error: 1390 | unexpected success 1391 | got: 1392 | []int(nil) 1393 | predicate: 1394 | func([]int) bool {...} 1395 | `, 1396 | }, { 1397 | about: "Satisfies: failure with an error", 1398 | checker: qt.Satisfies(nil, qt.IsBadCheck), 1399 | expectedCheckFailure: ` 1400 | error: 1401 | value does not satisfy predicate function 1402 | got: 1403 | nil 1404 | predicate: 1405 | func(error) bool {...} 1406 | `, 1407 | }, { 1408 | about: "Satisfies: failure with a string", 1409 | checker: qt.Satisfies("bad wolf", func(string) bool { return false }), 1410 | expectedCheckFailure: ` 1411 | error: 1412 | value does not satisfy predicate function 1413 | got: 1414 | "bad wolf" 1415 | predicate: 1416 | func(string) bool {...} 1417 | `, 1418 | }, { 1419 | about: "IsTrue: success", 1420 | checker: qt.IsTrue(true), 1421 | expectedNegateFailure: ` 1422 | error: 1423 | unexpected success 1424 | got: 1425 | bool(true) 1426 | want: 1427 | 1428 | `, 1429 | }, { 1430 | about: "IsTrue: failure", 1431 | checker: qt.IsTrue(false), 1432 | expectedCheckFailure: ` 1433 | error: 1434 | values are not equal 1435 | got: 1436 | bool(false) 1437 | want: 1438 | bool(true) 1439 | `, 1440 | }, { 1441 | about: "IsTrue: success with subtype", 1442 | checker: qt.IsTrue(boolean(true)), 1443 | expectedNegateFailure: ` 1444 | error: 1445 | unexpected success 1446 | got: 1447 | qt_test.boolean(true) 1448 | want: 1449 | 1450 | `, 1451 | }, { 1452 | about: "IsTrue: failure with subtype", 1453 | checker: qt.IsTrue(boolean(false)), 1454 | expectedCheckFailure: ` 1455 | error: 1456 | values are not equal 1457 | got: 1458 | qt_test.boolean(false) 1459 | want: 1460 | qt_test.boolean(true) 1461 | `, 1462 | }, { 1463 | about: "IsFalse: success", 1464 | checker: qt.IsFalse(false), 1465 | expectedNegateFailure: ` 1466 | error: 1467 | unexpected success 1468 | got: 1469 | bool(false) 1470 | want: 1471 | 1472 | `, 1473 | }, { 1474 | about: "IsFalse: failure", 1475 | checker: qt.IsFalse(true), 1476 | expectedCheckFailure: ` 1477 | error: 1478 | values are not equal 1479 | got: 1480 | bool(true) 1481 | want: 1482 | bool(false) 1483 | `, 1484 | }, { 1485 | about: "StringContains match", 1486 | checker: qt.StringContains("hello, world", "world"), 1487 | expectedNegateFailure: ` 1488 | error: 1489 | unexpected success 1490 | got: 1491 | "hello, world" 1492 | substr: 1493 | "world" 1494 | `, 1495 | }, { 1496 | about: "StringContains no match", 1497 | checker: qt.StringContains("hello, world", "worlds"), 1498 | expectedCheckFailure: ` 1499 | error: 1500 | no substring match found 1501 | got: 1502 | "hello, world" 1503 | substr: 1504 | "worlds" 1505 | `}, { 1506 | about: "SliceContains match", 1507 | checker: qt.SliceContains([]string{"a", "b", "c"}, "a"), 1508 | expectedNegateFailure: ` 1509 | error: 1510 | unexpected success 1511 | container: 1512 | []string{"a", "b", "c"} 1513 | want: 1514 | "a" 1515 | `, 1516 | }, { 1517 | about: "SliceContains mismatch", 1518 | checker: qt.SliceContains([]string{"a", "b", "c"}, "d"), 1519 | expectedCheckFailure: ` 1520 | error: 1521 | no matching element found 1522 | container: 1523 | []string{"a", "b", "c"} 1524 | want: 1525 | "d" 1526 | `, 1527 | }, { 1528 | about: "Contains with map", 1529 | checker: qt.MapContains(map[string]string{ 1530 | "a": "d", 1531 | "b": "a", 1532 | }, "d"), 1533 | expectedNegateFailure: ` 1534 | error: 1535 | unexpected success 1536 | container: 1537 | map[string]string{"a":"d", "b":"a"} 1538 | want: 1539 | "d" 1540 | `, 1541 | }, { 1542 | about: "Contains with map and interface value", 1543 | checker: qt.MapContains(map[string]any{ 1544 | "a": "d", 1545 | "b": "a", 1546 | }, "d"), 1547 | expectedNegateFailure: ` 1548 | error: 1549 | unexpected success 1550 | container: 1551 | map[string]interface {}{ 1552 | "a": "d", 1553 | "b": "a", 1554 | } 1555 | want: 1556 | "d" 1557 | `, 1558 | }, { 1559 | about: "All slice equals", 1560 | checker: qt.SliceAll([]string{"a", "a"}, qt.F2(qt.Equals[string], "a")), 1561 | expectedNegateFailure: ` 1562 | error: 1563 | unexpected success 1564 | container: 1565 | []string{"a", "a"} 1566 | want: 1567 | "a" 1568 | `, 1569 | }, { 1570 | about: "All slice match", 1571 | checker: qt.SliceAll([]string{"red", "blue", "green"}, qt.F2(qt.Matches[string], ".*e.*")), 1572 | expectedNegateFailure: ` 1573 | error: 1574 | unexpected success 1575 | container: 1576 | []string{"red", "blue", "green"} 1577 | regexp: 1578 | ".*e.*" 1579 | `, 1580 | }, { 1581 | about: "All nested match", 1582 | // TODO this is a bit awkward. Is there something we could do to improve it? 1583 | checker: qt.SliceAll([][]string{{"hello", "goodbye"}, {"red", "blue"}, {}}, func(elem []string) qt.Checker { 1584 | return qt.SliceAll(elem, qt.F2(qt.Matches[string], ".*e.*")) 1585 | }), 1586 | expectedNegateFailure: ` 1587 | error: 1588 | unexpected success 1589 | container: 1590 | [][]string{ 1591 | {"hello", "goodbye"}, 1592 | {"red", "blue"}, 1593 | {}, 1594 | } 1595 | regexp: 1596 | ".*e.*" 1597 | `, 1598 | }, { 1599 | about: "All nested mismatch", 1600 | checker: qt.SliceAll([][]string{{"hello", "goodbye"}, {"black", "blue"}, {}}, func(elem []string) qt.Checker { 1601 | return qt.SliceAll(elem, qt.F2(qt.Matches[string], ".*e.*")) 1602 | }), 1603 | expectedCheckFailure: ` 1604 | error: 1605 | mismatch at index 1 1606 | error: 1607 | mismatch at index 0 1608 | error: 1609 | value does not match regexp 1610 | first mismatched element: 1611 | "black" 1612 | `, 1613 | }, { 1614 | about: "All slice mismatch", 1615 | checker: qt.SliceAll([]string{"red", "black"}, qt.F2(qt.Matches[string], ".*e.*")), 1616 | expectedCheckFailure: ` 1617 | error: 1618 | mismatch at index 1 1619 | error: 1620 | value does not match regexp 1621 | first mismatched element: 1622 | "black" 1623 | `, 1624 | }, { 1625 | about: "All slice mismatch with DeepEqual", 1626 | checker: qt.SliceAll([][]string{{"a", "b"}, {"a", "c"}}, qt.F2(qt.DeepEquals[[]string], []string{"a", "b"})), 1627 | expectedCheckFailure: fmt.Sprintf(` 1628 | error: 1629 | mismatch at index 1 1630 | error: 1631 | values are not deep equal 1632 | diff (-want +got): 1633 | %s 1634 | got: 1635 | []string{"a", "c"} 1636 | want: 1637 | []string{"a", "b"} 1638 | `, diff([]string{"a", "c"}, []string{"a", "b"})), 1639 | }, { 1640 | about: "All mismatch with map", 1641 | checker: qt.MapAll(map[string]string{"a": "red", "b": "black"}, qt.F2(qt.Matches[string], ".*e.*")), 1642 | expectedCheckFailure: ` 1643 | error: 1644 | mismatch at key "b" 1645 | error: 1646 | value does not match regexp 1647 | first mismatched element: 1648 | "black" 1649 | `}, { 1650 | about: "Any no match", 1651 | checker: qt.SliceAny([]int{}, qt.F2(qt.Equals[int], 5)), 1652 | expectedCheckFailure: ` 1653 | error: 1654 | no matching element found 1655 | container: 1656 | []int{} 1657 | want: 1658 | int(5) 1659 | `, 1660 | }, { 1661 | about: "JSONEquals simple", 1662 | checker: qt.JSONEquals( 1663 | []byte(`{"First": 47.11}`), 1664 | &OuterJSON{ 1665 | First: 47.11, 1666 | }, 1667 | ), 1668 | expectedNegateFailure: tilde2bq(` 1669 | error: 1670 | unexpected success 1671 | got: 1672 | []uint8(~{"First": 47.11}~) 1673 | want: 1674 | &qt_test.OuterJSON{ 1675 | First: 47.11, 1676 | Second: nil, 1677 | } 1678 | `), 1679 | }, { 1680 | about: "JSONEquals nested", 1681 | checker: qt.JSONEquals( 1682 | `{"First": 47.11, "Last": [{"First": "Hello", "Second": 42}]}`, 1683 | &OuterJSON{ 1684 | First: 47.11, 1685 | Second: []*InnerJSON{ 1686 | {First: "Hello", Second: 42}, 1687 | }, 1688 | }, 1689 | ), 1690 | expectedNegateFailure: tilde2bq(` 1691 | error: 1692 | unexpected success 1693 | got: 1694 | ~{"First": 47.11, "Last": [{"First": "Hello", "Second": 42}]}~ 1695 | want: 1696 | &qt_test.OuterJSON{ 1697 | First: 47.11, 1698 | Second: { 1699 | &qt_test.InnerJSON{ 1700 | First: "Hello", 1701 | Second: 42, 1702 | Third: {}, 1703 | }, 1704 | }, 1705 | } 1706 | `), 1707 | }, { 1708 | about: "JSONEquals nested with newline", 1709 | checker: qt.JSONEquals( 1710 | `{"First": 47.11, "Last": [{"First": "Hello", "Second": 42}, 1711 | {"First": "World", "Third": {"F": false}}]}`, 1712 | &OuterJSON{ 1713 | First: 47.11, 1714 | Second: []*InnerJSON{ 1715 | {First: "Hello", Second: 42}, 1716 | {First: "World", Third: map[string]bool{ 1717 | "F": false, 1718 | }}, 1719 | }, 1720 | }, 1721 | ), 1722 | expectedNegateFailure: ` 1723 | error: 1724 | unexpected success 1725 | got: 1726 | "{\"First\": 47.11, \"Last\": [{\"First\": \"Hello\", \"Second\": 42},\n\t\t\t{\"First\": \"World\", \"Third\": {\"F\": false}}]}" 1727 | want: 1728 | &qt_test.OuterJSON{ 1729 | First: 47.11, 1730 | Second: { 1731 | &qt_test.InnerJSON{ 1732 | First: "Hello", 1733 | Second: 42, 1734 | Third: {}, 1735 | }, 1736 | &qt_test.InnerJSON{ 1737 | First: "World", 1738 | Second: 0, 1739 | Third: {"F":false}, 1740 | }, 1741 | }, 1742 | } 1743 | `, 1744 | }, { 1745 | about: "JSONEquals extra field", 1746 | checker: qt.JSONEquals( 1747 | `{"NotThere": 1}`, 1748 | &OuterJSON{ 1749 | First: 2, 1750 | }, 1751 | ), 1752 | expectedCheckFailure: fmt.Sprintf(` 1753 | error: 1754 | values are not deep equal 1755 | diff (-want +got): 1756 | %s 1757 | got: 1758 | map[string]interface {}{ 1759 | "NotThere": float64(1), 1760 | } 1761 | want: 1762 | map[string]interface {}{ 1763 | "First": float64(2), 1764 | } 1765 | `, diff(map[string]any{"NotThere": 1.0}, map[string]any{"First": 2.0})), 1766 | }, { 1767 | about: "JSONEquals cannot unmarshal obtained value", 1768 | checker: qt.JSONEquals([]byte(`{"NotThere": `), nil), 1769 | expectedCheckFailure: fmt.Sprintf(tilde2bq(` 1770 | error: 1771 | cannot unmarshal obtained contents: %s; "{\"NotThere\": " 1772 | got: 1773 | []uint8(~{"NotThere": ~) 1774 | want: 1775 | nil 1776 | `), mustJSONUnmarshalErr(`{"NotThere": `)), 1777 | }, { 1778 | about: "JSONEquals cannot marshal expected value", 1779 | checker: qt.JSONEquals([]byte(`null`), jsonErrorMarshaler{}), 1780 | expectedCheckFailure: ` 1781 | error: 1782 | bad check: cannot marshal expected contents: json: error calling MarshalJSON for type qt_test.jsonErrorMarshaler: qt json marshal error 1783 | `, 1784 | expectedNegateFailure: ` 1785 | error: 1786 | bad check: cannot marshal expected contents: json: error calling MarshalJSON for type qt_test.jsonErrorMarshaler: qt json marshal error 1787 | `, 1788 | }, { 1789 | about: "JSONEquals with []byte", 1790 | checker: qt.JSONEquals([]byte("null"), nil), 1791 | expectedNegateFailure: ` 1792 | error: 1793 | unexpected success 1794 | got: 1795 | []uint8("null") 1796 | want: 1797 | nil 1798 | `, 1799 | }, { 1800 | about: "JSONEquals with RawMessage", 1801 | checker: qt.JSONEquals([]byte("null"), json.RawMessage("null")), 1802 | expectedNegateFailure: ` 1803 | error: 1804 | unexpected success 1805 | got: 1806 | []uint8("null") 1807 | want: 1808 | json.RawMessage("null") 1809 | `, 1810 | }, { 1811 | about: "CodecEquals with bad marshal", 1812 | checker: qt.CodecEquals( 1813 | "null", 1814 | nil, 1815 | func(x any) ([]byte, error) { return []byte("bad json"), nil }, 1816 | json.Unmarshal, 1817 | ), 1818 | expectedCheckFailure: fmt.Sprintf(` 1819 | error: 1820 | bad check: cannot unmarshal expected contents: %s 1821 | `, mustJSONUnmarshalErr("bad json")), 1822 | expectedNegateFailure: fmt.Sprintf(` 1823 | error: 1824 | bad check: cannot unmarshal expected contents: %s 1825 | `, mustJSONUnmarshalErr("bad json")), 1826 | }, { 1827 | about: "CodecEquals with options", 1828 | checker: qt.CodecEquals( 1829 | `["b", "z", "c", "a"]`, 1830 | []string{"a", "c", "z", "b"}, 1831 | json.Marshal, 1832 | json.Unmarshal, 1833 | cmpopts.SortSlices(func(x, y any) bool { return x.(string) < y.(string) }), 1834 | ), 1835 | expectedNegateFailure: tilde2bq(` 1836 | error: 1837 | unexpected success 1838 | got: 1839 | ~["b", "z", "c", "a"]~ 1840 | want: 1841 | []string{"a", "c", "z", "b"} 1842 | `), 1843 | }, { 1844 | about: "ErrorAs: exact match", 1845 | checker: qt.ErrorAs(targetErr, new(*errTarget)), 1846 | expectedNegateFailure: ` 1847 | error: 1848 | unexpected success 1849 | got: 1850 | e"ptr: target" 1851 | as type: 1852 | *qt_test.errTarget 1853 | `, 1854 | }, { 1855 | about: "ErrorAs: wrapped match", 1856 | checker: qt.ErrorAs(fmt.Errorf("wrapped: %w", targetErr), new(*errTarget)), 1857 | expectedNegateFailure: ` 1858 | error: 1859 | unexpected success 1860 | got: 1861 | e"wrapped: ptr: target" 1862 | as type: 1863 | *qt_test.errTarget 1864 | `, 1865 | }, { 1866 | about: "ErrorAs: fails if nil error", 1867 | checker: qt.ErrorAs(nil, new(*errTarget)), 1868 | expectedCheckFailure: ` 1869 | error: 1870 | got nil error but want non-nil 1871 | got: 1872 | nil 1873 | as type: 1874 | *qt_test.errTarget 1875 | `, 1876 | }, { 1877 | about: "ErrorAs: fails if mismatch", 1878 | checker: qt.ErrorAs(errors.New("other error"), new(*errTarget)), 1879 | expectedCheckFailure: ` 1880 | error: 1881 | wanted type is not found in error chain 1882 | got: 1883 | e"other error" 1884 | as type: 1885 | *qt_test.errTarget 1886 | `, 1887 | }, { 1888 | about: "ErrorAs: fails if mismatch with a non-pointer error implementation", 1889 | checker: qt.ErrorAs(errors.New("other error"), new(errTargetNonPtr)), 1890 | expectedCheckFailure: ` 1891 | error: 1892 | wanted type is not found in error chain 1893 | got: 1894 | e"other error" 1895 | as type: 1896 | qt_test.errTargetNonPtr 1897 | `, 1898 | }, { 1899 | about: "ErrorAs: bad check if invalid as", 1900 | checker: qt.ErrorAs(targetErr, &struct{}{}), 1901 | expectedCheckFailure: ` 1902 | error: 1903 | bad check: errors: *target must be interface or implement error 1904 | `, 1905 | expectedNegateFailure: ` 1906 | error: 1907 | bad check: errors: *target must be interface or implement error 1908 | `, 1909 | }, { 1910 | about: "ErrorIs: exact match", 1911 | checker: qt.ErrorIs(targetErr, targetErr), 1912 | expectedNegateFailure: ` 1913 | error: 1914 | unexpected success 1915 | got: 1916 | e"ptr: target" 1917 | want: 1918 | 1919 | `, 1920 | }, { 1921 | about: "ErrorIs: wrapped match", 1922 | checker: qt.ErrorIs(fmt.Errorf("wrapped: %w", targetErr), targetErr), 1923 | expectedNegateFailure: ` 1924 | error: 1925 | unexpected success 1926 | got: 1927 | e"wrapped: ptr: target" 1928 | want: 1929 | e"ptr: target" 1930 | `, 1931 | }, { 1932 | about: "ErrorIs: fails if nil error", 1933 | checker: qt.ErrorIs(nil, targetErr), 1934 | expectedCheckFailure: ` 1935 | error: 1936 | got nil error but want non-nil 1937 | got: 1938 | nil 1939 | want: 1940 | e"ptr: target" 1941 | `, 1942 | }, { 1943 | about: "ErrorIs: fails if mismatch", 1944 | checker: qt.ErrorIs(errors.New("other error"), targetErr), 1945 | expectedCheckFailure: ` 1946 | error: 1947 | wanted error is not found in error chain 1948 | got: 1949 | e"other error" 1950 | want: 1951 | e"ptr: target" 1952 | `, 1953 | }, { 1954 | about: "ErrorIs: nil to nil match", 1955 | checker: qt.ErrorIs(nil, nil), 1956 | expectedNegateFailure: ` 1957 | error: 1958 | unexpected success 1959 | got: 1960 | nil 1961 | want: 1962 | 1963 | `, 1964 | }, { 1965 | about: "ErrorIs: non-nil to nil mismatch", 1966 | checker: qt.ErrorIs(targetErr, nil), 1967 | expectedCheckFailure: ` 1968 | error: 1969 | wanted error is not found in error chain 1970 | got: 1971 | e"ptr: target" 1972 | want: 1973 | nil 1974 | `, 1975 | }, { 1976 | about: "Not: failure", 1977 | checker: qt.Not(qt.Equals(42, 42)), 1978 | expectedCheckFailure: ` 1979 | error: 1980 | unexpected success 1981 | got: 1982 | int(42) 1983 | want: 1984 | 1985 | `, 1986 | }, { 1987 | about: "Not: IsNil failure", 1988 | checker: qt.Not(qt.IsNil[*int](nil)), 1989 | expectedCheckFailure: ` 1990 | error: 1991 | got nil ptr but want non-nil 1992 | got: 1993 | (*int)(nil) 1994 | `, 1995 | }} 1996 | 1997 | func TestCheckers(t *testing.T) { 1998 | original := qt.TestingVerbose 1999 | defer func() { 2000 | qt.TestingVerbose = original 2001 | }() 2002 | for _, test := range checkerTests { 2003 | *qt.TestingVerbose = func() bool { 2004 | return test.verbose 2005 | } 2006 | t.Run(test.about, func(t *testing.T) { 2007 | tt := &testingT{} 2008 | ok := qt.Check(tt, test.checker) 2009 | checkResult(t, ok, tt.errorString(), test.expectedCheckFailure) 2010 | }) 2011 | t.Run("Not "+test.about, func(t *testing.T) { 2012 | tt := &testingT{} 2013 | ok := qt.Check(tt, qt.Not(test.checker)) 2014 | checkResult(t, ok, tt.errorString(), test.expectedNegateFailure) 2015 | }) 2016 | } 2017 | } 2018 | 2019 | func diff(got, want any, opts ...cmp.Option) string { 2020 | d := cmp.Diff(want, got, opts...) 2021 | return strings.TrimSuffix(qt.Prefixf(" ", "%s", d), "\n") 2022 | } 2023 | 2024 | type jsonErrorMarshaler struct{} 2025 | 2026 | func (jsonErrorMarshaler) MarshalJSON() ([]byte, error) { 2027 | return nil, fmt.Errorf("qt json marshal error") 2028 | } 2029 | 2030 | func mustJSONUnmarshalErr(s string) error { 2031 | var v any 2032 | err := json.Unmarshal([]byte(s), &v) 2033 | if err == nil { 2034 | panic("want JSON error, got nil") 2035 | } 2036 | return err 2037 | } 2038 | 2039 | func tilde2bq(s string) string { 2040 | return strings.Replace(s, "~", "`", -1) 2041 | } 2042 | --------------------------------------------------------------------------------