pax_global_header00006660000000000000000000000064144627576710014535gustar00rootroot0000000000000052 comment=6211e4eaf28ee5ee8285fcc4207ddc7b990a7bad qt-1.101.0/000077500000000000000000000000001446275767100123215ustar00rootroot00000000000000qt-1.101.0/.github/000077500000000000000000000000001446275767100136615ustar00rootroot00000000000000qt-1.101.0/.github/dependabot.yaml000066400000000000000000000005011446275767100166460ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: # Check for updates to GitHub Actions every weekday. interval: "daily" - package-ecosystem: "gomod" directory: "/" schedule: # Check for updates to go modules every weekday. interval: "daily" qt-1.101.0/.github/workflows/000077500000000000000000000000001446275767100157165ustar00rootroot00000000000000qt-1.101.0/.github/workflows/ci.yaml000066400000000000000000000011411446275767100171720ustar00rootroot00000000000000name: CI on: [push, pull_request] jobs: build_test: name: Build and Test strategy: matrix: go: ['1.18.x', '1.19.x', '1.20.x'] runs-on: ubuntu-latest steps: - uses: actions/checkout@v2.4.0 - uses: actions/setup-go@v2.1.5 with: go-version: ${{ matrix.go }} stable: false - uses: actions/cache@v2.1.7 with: path: ~/go/pkg/mod key: ubuntu-go-${{ hashFiles('**/go.sum') }} restore-keys: | ubuntu-go- - name: Test run: go test -race ./... - name: Test Verbose run: go test -race -v ./... qt-1.101.0/.gitignore000066400000000000000000000000101446275767100143000ustar00rootroot00000000000000.vscode qt-1.101.0/LICENSE000066400000000000000000000020571446275767100133320ustar00rootroot00000000000000MIT License Copyright (c) 2017 Canonical Ltd. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. qt-1.101.0/README.md000066400000000000000000000031011446275767100135730ustar00rootroot00000000000000# qt: quicker Go tests `go get github.com/go-quicktest/qt` Package qt provides a collection of Go helpers for writing tests. It uses generics, so requires Go 1.18 at least. For a complete API reference, see the [package documentation](https://pkg.go.dev/github.com/go-quicktest/qt). Quicktest helpers can be easily integrated inside regular Go tests, for instance: ```go import "github.com/go-quicktest/qt" func TestFoo(t *testing.T) { t.Run("numbers", func(t *testing.T) { numbers, err := somepackage.Numbers() qt.Assert(t, qt.DeepEquals(numbers, []int{42, 47}) qt.Assert(t, qt.ErrorMatches(err, "bad wolf")) }) t.Run("nil", func(t *testing.T) { got := somepackage.MaybeNil() qt.Assert(t, qt.IsNil(got), qt.Commentf("value: %v", somepackage.Value)) }) } ``` ### Assertions An assertion looks like this, where `qt.Equals` could be replaced by any available checker. If the assertion fails, the underlying `t.Fatal` method is called to describe the error and abort the test. qt.Assert(t, qt.Equals(someValue, wantValue)) If you don’t want to abort on failure, use `Check` instead, which calls `Error` instead of `Fatal`: qt.Check(t, qt.Equals(someValue, wantValue)) The library provides some base checkers like `Equals`, `DeepEquals`, `Matches`, `ErrorMatches`, `IsNil` and others. More can be added by implementing the Checker interface. ### Other helpers The `Patch` helper makes it a little more convenient to change a global or other variable for the duration of a test. qt-1.101.0/TODO000066400000000000000000000016301446275767100130110ustar00rootroot00000000000000## Possible things to do var defaultRetryStrategy = &retry.Strategy{ Delay: time.Millisecond, MaxDelay: 200 * time.Millisecond, MaxDuration: 5 * time.Second } var defaultStableStrategy = &retry.Strategy{ Delay: time.Millisecond, MaxDuration: 50 * time.Millisecond, } func Eventually[T any](f func() T, checker func(T) Checker, retry *retry.Strategy) Checker func EventuallyStable[T any](f func() T, checker func(T) Checker, retry, stableRetry *retry.Strategy) Checker // QT provides a version of the Assert and Check primitives // that use a customizable Format function. // // For example: // // package mypackage // // import _qt "github.com/go-quicktest/qt" // // var qt = _qt.QT{ // Format: myFormat, // } type QT struct { Format func(interface{}) string } func (qt QT) Assert(t testing.TB, checker Checker, comment ...Comment) func (qt QT) Check(t testing.TB, checker Checker, comment ...Comment) bool qt-1.101.0/checker.go000066400000000000000000000546311446275767100142650ustar00rootroot00000000000000// Licensed under the MIT license, see LICENSE file for details. package qt import ( "encoding/json" "errors" "fmt" "reflect" "regexp" "strings" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/kr/pretty" ) // Checker is implemented by types used as part of Check/Assert invocations. type Checker interface { // Check runs the check for this checker. // On failure, the returned error is printed along with // the checker arguments (obtained by calling Args) // and key-value pairs added by calling the note function. // // If Check returns ErrSilent, neither the checker arguments nor // the error are printed; values with note are still printed. Check(note func(key string, value any)) error // Args returns a slice of all the arguments passed // to the checker. The first argument should always be // the "got" value being checked. Args() []Arg } // Arg holds a single argument to a checker. type Arg struct { Name string Value any } // negatedError is implemented on checkers that want to customize the error that // is returned when they have succeeded but that success has been negated. type negatedError interface { negatedError() error } // Equals returns a Checker checking equality of two comparable values. // // Note that T is not constrained to be comparable because // we also allow comparing interface values which currently // do not satisfy that constraint. func Equals[T any](got, want T) Checker { return &equalsChecker[T]{argPairOf(got, want)} } type equalsChecker[T any] struct { argPair[T, T] } func (c *equalsChecker[T]) Check(note func(key string, value any)) (err error) { defer func() { // A panic is raised when the provided values are interfaces containing // non-comparable values. if r := recover(); r != nil { err = fmt.Errorf("%s", r) } }() if any(c.got) == any(c.want) { return nil } // Customize error message for non-nil errors. if typeOf[T]() == typeOf[error]() { if any(c.want) == nil { return errors.New("got non-nil error") } if any(c.got) == nil { return errors.New("got nil error") } // Show error types when comparing errors with different types. gotType := reflect.TypeOf(c.got) wantType := reflect.TypeOf(c.want) if gotType != wantType { note("got type", Unquoted(gotType.String())) note("want type", Unquoted(wantType.String())) } return errors.New("values are not equal") } // Show line diff when comparing different multi-line strings. if c, ok := any(c).(*equalsChecker[string]); ok { isMultiLine := func(s string) bool { i := strings.Index(s, "\n") return i != -1 && i < len(s)-1 } if isMultiLine(c.got) || isMultiLine(c.want) { diff := cmp.Diff(strings.SplitAfter(c.got, "\n"), strings.SplitAfter(c.want, "\n")) note("line diff (-got +want)", Unquoted(diff)) } } return errors.New("values are not equal") } // DeepEquals returns a Checker checking equality of two values // using cmp.DeepEqual. func DeepEquals[T any](got, want T) Checker { return CmpEquals(got, want) } // CmpEquals is like DeepEquals but allows custom compare options // to be passed too, to allow unexported fields to be compared. // // It can be useful to define your own version that uses a custom // set of compare options. See example for details. func CmpEquals[T any](got, want T, opts ...cmp.Option) Checker { return &cmpEqualsChecker[T]{ argPair: argPairOf(got, want), opts: opts, } } type cmpEqualsChecker[T any] struct { argPair[T, T] opts []cmp.Option } func (c *cmpEqualsChecker[T]) Check(note func(key string, value any)) (err error) { defer func() { // A panic is raised in some cases, for instance when trying to compare // structs with unexported fields and neither AllowUnexported nor // cmpopts.IgnoreUnexported are provided. if r := recover(); r != nil { err = BadCheckf("%s", r) } }() if diff := cmp.Diff(c.got, c.want, c.opts...); diff != "" { // Only output values when the verbose flag is set. note("error", Unquoted("values are not deep equal")) note("diff (-got +want)", Unquoted(diff)) note("got", SuppressedIfLong{c.got}) note("want", SuppressedIfLong{c.want}) return ErrSilent } return nil } // ContentEquals is like DeepEquals but any slices in the compared values will // be sorted before being compared. func ContentEquals[T any](got, want T) Checker { return CmpEquals(got, want, cmpopts.SortSlices(func(x, y any) bool { // TODO frankban: implement a proper sort function. return pretty.Sprint(x) < pretty.Sprint(y) })) } // Matches returns a Checker checking that the provided string matches the // provided regular expression pattern. func Matches[StringOrRegexp string | *regexp.Regexp](got string, want StringOrRegexp) Checker { return &matchesChecker{ got: got, want: want, match: newMatcher(want), } } type matchesChecker struct { got string want any match matcher } func (c *matchesChecker) Check(note func(key string, value any)) error { return c.match(c.got, "value does not match regexp", note) } func (c *matchesChecker) Args() []Arg { return []Arg{{Name: "got value", Value: c.got}, {Name: "regexp", Value: c.want}} } // ErrorMatches returns a Checker checking that the provided value is an error // whose message matches the provided regular expression pattern. func ErrorMatches[StringOrRegexp string | *regexp.Regexp](got error, want StringOrRegexp) Checker { return &errorMatchesChecker{ got: got, want: want, match: newMatcher(want), } } type errorMatchesChecker struct { got error want any match matcher } func (c *errorMatchesChecker) Check(note func(key string, value any)) error { if c.got == nil { return errors.New("got nil error but want non-nil") } return c.match(c.got.Error(), "error does not match regexp", note) } func (c *errorMatchesChecker) Args() []Arg { return []Arg{{Name: "got error", Value: c.got}, {Name: "regexp", Value: c.want}} } // PanicMatches returns a Checker checking that the provided function panics // with a message matching the provided regular expression pattern. func PanicMatches[StringOrRegexp string | *regexp.Regexp](f func(), want StringOrRegexp) Checker { return &panicMatchesChecker{ got: f, want: want, match: newMatcher(want), } } type panicMatchesChecker struct { got func() want any match matcher } func (c *panicMatchesChecker) Check(note func(key string, value any)) (err error) { defer func() { r := recover() if r == nil { err = errors.New("function did not panic") return } msg := fmt.Sprint(r) note("panic value", msg) err = c.match(msg, "panic value does not match regexp", note) }() c.got() return nil } func (c *panicMatchesChecker) Args() []Arg { return []Arg{{Name: "function", Value: c.got}, {Name: "regexp", Value: c.want}} } // IsNil returns a Checker checking that the provided value is equal to nil. // // Note that an interface value containing a nil concrete // type is not considered to be nil. func IsNil[T any](got T) Checker { return isNilChecker[T]{ got: got, } } type isNilChecker[T any] struct { got T } func (c isNilChecker[T]) Check(note func(key string, value any)) error { v := reflect.ValueOf(&c.got).Elem() if !canBeNil(v.Kind()) { return BadCheckf("type %v can never be nil", v.Type()) } if v.IsNil() { return nil } return errors.New("got non-nil value") } func (c isNilChecker[T]) Args() []Arg { return []Arg{{Name: "got", Value: c.got}} } func (c isNilChecker[T]) negatedError() error { v := reflect.ValueOf(c.got) if v.IsValid() { return fmt.Errorf("got nil %s but want non-nil", v.Kind()) } return errors.New("got but want non-nil") } // IsNotNil returns a Checker checking that the provided value is not nil. // IsNotNil(v) is the equivalent of qt.Not(qt.IsNil(v)). func IsNotNil[T any](got T) Checker { return Not(IsNil(got)) } // HasLen returns a Checker checking that the provided value has the given // length. The value may be a slice, array, channel, map or string. func HasLen[T any](got T, n int) Checker { return &hasLenChecker[T]{ got: got, wantLen: n, } } type hasLenChecker[T any] struct { got T wantLen int } func (c *hasLenChecker[T]) Check(note func(key string, value any)) (err error) { // TODO we're deliberately not allowing HasLen(interfaceValue) here. // Perhaps we should? v := reflect.ValueOf(&c.got).Elem() switch v.Kind() { case reflect.Array, reflect.Chan, reflect.Map, reflect.Slice, reflect.String: default: note("got", c.got) return BadCheckf("first argument of type %v has no length", v.Type()) } length := v.Len() note("len(got)", length) if length != c.wantLen { return fmt.Errorf("unexpected length") } return nil } func (c *hasLenChecker[T]) Args() []Arg { return []Arg{{Name: "got", Value: c.got}, {Name: "want length", Value: c.wantLen}} } // Implements returns a Checker checking that the provided value implements the // interface specified by the type parameter. func Implements[I any](got any) Checker { return &implementsChecker{ got: got, want: typeOf[I](), } } type implementsChecker struct { got any want reflect.Type } var emptyInterface = reflect.TypeOf((*any)(nil)).Elem() func (c *implementsChecker) Check(note func(key string, value any)) (err error) { if c.got == nil { note("error", Unquoted("got nil value but want non-nil")) note("got", c.got) return ErrSilent } if c.want.Kind() != reflect.Interface { note("want interface", Unquoted(c.want.String())) return BadCheckf("want an interface type but a concrete type was provided") } gotType := reflect.TypeOf(c.got) if !gotType.Implements(c.want) { return fmt.Errorf("got value does not implement wanted interface") } return nil } func (c *implementsChecker) Args() []Arg { return []Arg{{Name: "got", Value: c.got}, {Name: "want interface", Value: Unquoted(c.want.String())}} } // Satisfies returns a Checker checking that the provided value, when used as // argument of the provided predicate function, causes the function to return // true. func Satisfies[T any](got T, f func(T) bool) Checker { return &satisfiesChecker[T]{ got: got, predicate: f, } } type satisfiesChecker[T any] struct { got T predicate func(T) bool } // Check implements Checker.Check by checking that args[0](got) == true. func (c *satisfiesChecker[T]) Check(note func(key string, value any)) error { if c.predicate(c.got) { return nil } return fmt.Errorf("value does not satisfy predicate function") } func (c *satisfiesChecker[T]) Args() []Arg { return []Arg{{ Name: "got", Value: c.got, }, { Name: "predicate", Value: c.predicate, }} } // IsTrue returns a Checker checking that the provided value is true. func IsTrue[T ~bool](got T) Checker { return Equals(got, true) } // IsFalse returns a Checker checking that the provided value is false. func IsFalse[T ~bool](got T) Checker { return Equals(got, false) } // Not returns a Checker negating the given Checker. func Not(c Checker) Checker { // Not(Not(c)) becomes c. if c, ok := c.(notChecker); ok { return c.Checker } return notChecker{ Checker: c, } } type notChecker struct { Checker } func (c notChecker) Check(note func(key string, value any)) error { err := c.Checker.Check(note) if IsBadCheck(err) { return err } if err != nil { return nil } if c, ok := c.Checker.(negatedError); ok { return c.negatedError() } return errors.New("unexpected success") } // StringContains returns a Checker checking that the given string contains the // given substring. func StringContains[T ~string](got, substr T) Checker { return &stringContainsChecker[T]{ got: got, substr: substr, } } type stringContainsChecker[T ~string] struct { got, substr T } func (c *stringContainsChecker[T]) Check(note func(key string, value any)) error { if strings.Contains(string(c.got), string(c.substr)) { return nil } return errors.New("no substring match found") } func (c *stringContainsChecker[T]) Args() []Arg { return []Arg{{ Name: "got", Value: c.got, }, { Name: "substr", Value: c.substr, }} } // SliceContains returns a Checker that succeeds if the given // slice contains the given element, by comparing for equality. func SliceContains[T any](container []T, elem T) Checker { return SliceAny(container, F2(Equals[T], elem)) } // MapContains returns a Checker that succeeds if the given value is // contained in the values of the given map, by comparing for equality. func MapContains[K comparable, V any](container map[K]V, elem V) Checker { return MapAny(container, F2(Equals[V], elem)) } // SliceAny returns a Checker that uses the given checker to check elements // of a slice. It succeeds if f(v) passes the check for any v in the slice. // // See the F2 function for a way to adapt a regular checker function // to the type expected for the f argument here. // // See also SliceAll and SliceContains. func SliceAny[T any](container []T, f func(elem T) Checker) Checker { return &anyChecker[T]{ newIter: func() containerIter[T] { return newSliceIter(container) }, container: container, elemChecker: f, } } // MapAny returns a Checker that uses checkers returned by f to check values // of a map. It succeeds if f(v) passes the check for any value v in the map. // // See the F2 function for a way to adapt a regular checker function // to the type expected for the f argument here. // // See also MapAll and MapContains. func MapAny[K comparable, V any](container map[K]V, f func(elem V) Checker) Checker { return &anyChecker[V]{ newIter: func() containerIter[V] { return newMapIter(container) }, container: container, elemChecker: f, } } type anyChecker[T any] struct { newIter func() containerIter[T] container any elemChecker func(T) Checker } func (c *anyChecker[T]) Check(note func(key string, value any)) error { for iter := c.newIter(); iter.next(); { // For the time being, discard the notes added by the sub-checker, // because it's not clear what a good behavior would be. // Should we print all the failed check for all elements? If there's only // one element in the container, the answer is probably yes, // but let's leave it for now. checker := c.elemChecker(iter.value()) err := checker.Check( func(key string, value any) {}, ) if err == nil { return nil } if IsBadCheck(err) { return BadCheckf("at %s: %v", iter.key(), err) } } return errors.New("no matching element found") } func (c *anyChecker[T]) Args() []Arg { // We haven't got an instance of the underlying checker, // so just make one by passing the zero value. In general // no checker should panic when being created regardless // of the actual arguments, so that should be OK. args := []Arg{{ Name: "container", Value: c.container, }} if eargs := c.elemChecker(*new(T)).Args(); len(eargs) > 0 { args = append(args, eargs[1:]...) } return args } // SliceAll returns a Checker that uses checkers returned by f // to check elements of a slice. It succeeds if all elements // of the slice pass the check. // On failure it prints the error from the first index that failed. func SliceAll[T any](container []T, f func(elem T) Checker) Checker { return &allChecker[T]{ newIter: func() containerIter[T] { return newSliceIter(container) }, container: container, elemChecker: f, } } // MapAll returns a Checker that uses checkers returned by f to check values // of a map. It succeeds if f(v) passes the check for all values v in the map. func MapAll[K comparable, V any](container map[K]V, f func(elem V) Checker) Checker { return &allChecker[V]{ newIter: func() containerIter[V] { return newMapIter(container) }, container: container, elemChecker: f, } } type allChecker[T any] struct { newIter func() containerIter[T] container any elemChecker func(T) Checker } func (c *allChecker[T]) Check(notef func(key string, value any)) error { for iter := c.newIter(); iter.next(); { // Store any notes added by the checker so // we can add our own note at the start // to say which element failed. var notes []note checker := c.elemChecker(iter.value()) err := checker.Check( func(key string, val any) { notes = append(notes, note{key, val}) }, ) if err == nil { continue } if IsBadCheck(err) { return BadCheckf("at %s: %v", iter.key(), err) } notef("error", Unquoted("mismatch at "+iter.key())) // TODO should we print the whole container value in // verbose mode? if err != ErrSilent { // If the error's not silent, the checker is expecting // the caller to print the error and the value that failed. notef("error", Unquoted(err.Error())) notef("first mismatched element", iter.value()) } for _, n := range notes { notef(n.key, n.value) } return ErrSilent } return nil } func (c *allChecker[T]) Args() []Arg { // We haven't got an instance of the underlying checker, // so just make one by passing the zero value. In general // no checker should panic when being created regardless // of the actual arguments, so that should be OK. args := []Arg{{ Name: "container", Value: c.container, }} if eargs := c.elemChecker(*new(T)).Args(); len(eargs) > 0 { args = append(args, eargs[1:]...) } return args } // JSONEquals returns a Checker that checks whether a string or byte slice is // JSON-equivalent to a Go value. See CodecEquals for more information. // // It uses DeepEquals to do the comparison. If a more sophisticated comparison // is required, use CodecEquals directly. func JSONEquals[T []byte | string](got T, want any) Checker { return CodecEquals(got, want, json.Marshal, json.Unmarshal) } // CodecEquals returns a Checker that checks for codec value equivalence. // // It expects two arguments: a byte slice or a string containing some // codec-marshaled data, and a Go value. // // It uses unmarshal to unmarshal the data into an interface{} value. // It marshals the Go value using marshal, then unmarshals the result into // an any value. // // It then checks that the two interface{} values are deep-equal to one // another, using CmpEquals(opts) to perform the check. // // See JSONEquals for an example of this in use. func CodecEquals[T []byte | string]( got T, want any, marshal func(any) ([]byte, error), unmarshal func([]byte, any) error, opts ...cmp.Option, ) Checker { return &codecEqualChecker[T]{ argPair: argPairOf(got, want), marshal: marshal, unmarshal: unmarshal, opts: opts, } } type codecEqualChecker[T []byte | string] struct { argPair[T, any] marshal func(any) ([]byte, error) unmarshal func([]byte, any) error opts []cmp.Option } func (c *codecEqualChecker[T]) Check(note func(key string, value any)) error { wantContentBytes, err := c.marshal(c.want) if err != nil { return BadCheckf("cannot marshal expected contents: %v", err) } var wantContentVal any if err := c.unmarshal(wantContentBytes, &wantContentVal); err != nil { return BadCheckf("cannot unmarshal expected contents: %v", err) } var gotContentVal any if err := c.unmarshal([]byte(c.got), &gotContentVal); err != nil { return fmt.Errorf("cannot unmarshal obtained contents: %v; %q", err, c.got) } cmpEq := CmpEquals(gotContentVal, wantContentVal, c.opts...).(*cmpEqualsChecker[any]) return cmpEq.Check(note) } // ErrorAs retruns a Checker checking that the error is or wraps a specific // error type. If so, it assigns it to the provided pointer. This is analogous // to calling errors.As. func ErrorAs[T any](got error, want *T) Checker { return &errorAsChecker[T]{ got: got, want: want, } } type errorAsChecker[T any] struct { got error want *T } func (c *errorAsChecker[T]) Check(note func(key string, value any)) (err error) { if c.got == nil { return errors.New("got nil error but want non-nil") } gotErr := c.got.(error) defer func() { // A panic is raised when the target is not a pointer to an interface // or error. if r := recover(); r != nil { err = BadCheckf("%s", r) } }() want := c.want if want == nil { want = new(T) } if !errors.As(gotErr, want) { return errors.New("wanted type is not found in error chain") } return nil } func (c *errorAsChecker[T]) Args() []Arg { return []Arg{{ Name: "got", Value: c.got, }, { Name: "as type", Value: Unquoted(typeOf[T]().String()), }} } // ErrorIs returns a Checker that checks that the error is or wraps a specific // error value. This is analogous to calling errors.Is. func ErrorIs(got, want error) Checker { return &errorIsChecker{ argPair: argPairOf(got, want), } } type errorIsChecker struct { argPair[error, error] } func (c *errorIsChecker) Check(note func(key string, value any)) error { if c.got == nil && c.want != nil { return errors.New("got nil error but want non-nil") } if !errors.Is(c.got, c.want) { return errors.New("wanted error is not found in error chain") } return nil } type matcher = func(got string, msg string, note func(key string, value any)) error // newMatcher returns a matcher function that can be used by checkers when // checking that a string or an error matches the provided StringOrRegexp. func newMatcher[StringOrRegexp string | *regexp.Regexp](regex StringOrRegexp) matcher { var re *regexp.Regexp switch r := any(regex).(type) { case string: re0, err := regexp.Compile("^(" + r + ")$") if err != nil { return func(got string, msg string, note func(key string, value any)) error { note("regexp", r) return BadCheckf("cannot compile regexp: %s", err) } } re = re0 case *regexp.Regexp: re = r } return func(got string, msg string, note func(key string, value any)) error { if re.MatchString(got) { return nil } return errors.New(msg) } } func argPairOf[A, B any](a A, b B) argPair[A, B] { return argPair[A, B]{a, b} } type argPair[A, B any] struct { got A want B } func (p argPair[A, B]) Args() []Arg { return []Arg{{ Name: "got", Value: p.got, }, { Name: "want", Value: p.want, }} } // canBeNil reports whether a value or type of the given kind can be nil. func canBeNil(k reflect.Kind) bool { switch k { case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice: return true } return false } func typeOf[T any]() reflect.Type { return reflect.TypeOf((*T)(nil)).Elem() } func valueAs[T any](v reflect.Value) (r T) { reflect.ValueOf(&r).Elem().Set(v) return } // F2 factors a 2-argument checker function into a single argument function suitable // for passing to an *Any or *All checker. Whenever the returned function is called, // cf is called with arguments (got, want). func F2[Got, Want any](cf func(got Got, want Want) Checker, want Want) func(got Got) Checker { return func(got Got) Checker { return cf(got, want) } } qt-1.101.0/checker_test.go000066400000000000000000001223011446275767100153120ustar00rootroot00000000000000// Licensed under the MIT license, see LICENSE file for details. package qt_test import ( "encoding/json" "errors" "fmt" "regexp" "strings" "testing" "time" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/go-quicktest/qt" ) // errTarget is an error implemented as a pointer. type errTarget struct { msg string } func (e *errTarget) Error() string { return "ptr: " + e.msg } // errTargetNonPtr is an error implemented as a non-pointer. type errTargetNonPtr struct { msg string } func (e errTargetNonPtr) Error() string { return "non ptr: " + e.msg } // Fooer is an interface for testing. type Fooer interface { Foo() } type cmpType struct { Strings []any Ints []int } type InnerJSON struct { First string Second int `json:",omitempty" yaml:",omitempty"` Third map[string]bool `json:",omitempty" yaml:",omitempty"` } type OuterJSON struct { First float64 Second []*InnerJSON `json:"Last,omitempty" yaml:"last,omitempty"` } type boolean bool var ( targetErr = &errTarget{msg: "target"} goTime = time.Date(2012, 3, 28, 0, 0, 0, 0, time.UTC) chInt = func() chan int { ch := make(chan int, 4) ch <- 42 ch <- 47 return ch }() sameInts = cmpopts.SortSlices(func(x, y int) bool { return x < y }) cmpEqualsGot = cmpType{ Strings: []any{"who", "dalek"}, Ints: []int{42, 47}, } cmpEqualsWant = cmpType{ Strings: []any{"who", "dalek"}, Ints: []int{42}, } ) var checkerTests = []struct { about string checker qt.Checker verbose bool expectedCheckFailure string expectedNegateFailure string }{{ about: "Equals: same values", checker: qt.Equals(42, 42), expectedNegateFailure: ` error: unexpected success got: int(42) want: `, }, { about: "Equals: different values", checker: qt.Equals("42", "47"), expectedCheckFailure: ` error: values are not equal got: "42" want: "47" `, }, { about: "Equals: different strings with quotes", checker: qt.Equals(`string "foo"`, `string "bar"`), expectedCheckFailure: tilde2bq(` error: values are not equal got: ~string "foo"~ want: ~string "bar"~ `), }, { about: "Equals: same multiline strings", checker: qt.Equals("a\nmultiline\nstring", "a\nmultiline\nstring"), expectedNegateFailure: ` error: unexpected success got: "a\nmultiline\nstring" want: `}, { about: "Equals: different multi-line strings", checker: qt.Equals("a\nlong\nmultiline\nstring", "just\na\nlong\nmulti-line\nstring\n"), expectedCheckFailure: fmt.Sprintf(` error: values are not equal line diff (-got +want): %s got: "a\nlong\nmultiline\nstring" want: "just\na\nlong\nmulti-line\nstring\n" `, diff([]string{"a\n", "long\n", "multiline\n", "string"}, []string{"just\n", "a\n", "long\n", "multi-line\n", "string\n", ""})), }, { about: "Equals: different single-line strings ending with newline", checker: qt.Equals("foo\n", "bar\n"), expectedCheckFailure: ` error: values are not equal got: "foo\n" want: "bar\n" `, }, { about: "Equals: different strings starting with newline", checker: qt.Equals("\nfoo", "\nbar"), expectedCheckFailure: fmt.Sprintf(` error: values are not equal line diff (-got +want): %s got: "\nfoo" want: "\nbar" `, diff([]string{"\n", "foo"}, []string{"\n", "bar"})), }, { about: "Equals: different types", checker: qt.Equals(42, any("42")), expectedCheckFailure: ` error: values are not equal got: int(42) want: "42" `}, { about: "Equals: nil and nil", checker: qt.Equals(nil, any(nil)), expectedNegateFailure: ` error: unexpected success got: nil want: `, }, { about: "Equals: error is not nil", checker: qt.Equals(error(errBadWolf), error(nil)), expectedCheckFailure: ` error: got non-nil error got: bad wolf file:line want: nil `}, { about: "Equals: error is not nil: not formatted", checker: qt.Equals[error](&errTest{ msg: "bad wolf", }, nil), expectedCheckFailure: ` error: got non-nil error got: e"bad wolf" want: nil `, }, { about: "Equals: error does not guard against nil", checker: qt.Equals[error]((*errTest)(nil), nil), expectedCheckFailure: ` error: got non-nil error got: e want: nil `, }, { about: "Equals: error is not nil: not formatted and with quotes", checker: qt.Equals[error](&errTest{ msg: `failure: "bad wolf"`, }, nil), expectedCheckFailure: tilde2bq(` error: got non-nil error got: e~failure: "bad wolf"~ want: nil `), }, { about: "Equals: different errors with same message", checker: qt.Equals[error](&errTest{ msg: "bad wolf", }, errors.New("bad wolf")), expectedCheckFailure: ` error: values are not equal got type: *qt_test.errTest want type: *errors.errorString got: e"bad wolf" want: `, }, { about: "Equals: different pointer errors with the same message", checker: qt.Equals(targetErr, &errTarget{msg: "target"}), expectedCheckFailure: ` error: values are not equal got: e"ptr: target" want: `, }, { about: "Equals: different pointers with the same formatted output", checker: qt.Equals(new(int), new(int)), expectedCheckFailure: ` error: values are not equal got: &int(0) want: `, }, { about: "Equals: nil struct", checker: qt.Equals[any]((*struct{})(nil), nil), expectedCheckFailure: ` error: values are not equal got: (*struct {})(nil) want: nil `, }, { about: "Equals: different booleans", checker: qt.Equals(true, false), expectedCheckFailure: ` error: values are not equal got: bool(true) want: bool(false) `, }, { about: "Equals: uncomparable types", checker: qt.Equals[any](struct { Ints []int }{ Ints: []int{42, 47}, }, struct { Ints []int }{ Ints: []int{42, 47}, }), expectedCheckFailure: ` error: runtime error: comparing uncomparable type struct { Ints []int } got: struct { Ints []int }{ Ints: {42, 47}, } want: `}, { about: "DeepEquals: same values", checker: qt.DeepEquals(cmpEqualsGot, cmpEqualsGot), expectedNegateFailure: ` error: unexpected success got: qt_test.cmpType{ Strings: { "who", "dalek", }, Ints: {42, 47}, } want: `, }, { about: "DeepEquals: different values", checker: qt.DeepEquals(cmpEqualsGot, cmpEqualsWant), expectedCheckFailure: fmt.Sprintf(` error: values are not deep equal diff (-got +want): %s got: qt_test.cmpType{ Strings: { "who", "dalek", }, Ints: {42, 47}, } want: qt_test.cmpType{ Strings: { "who", "dalek", }, Ints: {42}, } `, diff(cmpEqualsGot, cmpEqualsWant)), }, { about: "DeepEquals: different values: long output", checker: qt.DeepEquals([]any{cmpEqualsWant, cmpEqualsWant}, []any{cmpEqualsWant, cmpEqualsWant, 42}), expectedCheckFailure: fmt.Sprintf(` error: values are not deep equal diff (-got +want): %s got: want: `, diff([]any{cmpEqualsWant, cmpEqualsWant}, []any{cmpEqualsWant, cmpEqualsWant, 42})), }, { about: "DeepEquals: different values: long output and verbose", checker: qt.DeepEquals([]any{cmpEqualsWant, cmpEqualsWant}, []any{cmpEqualsWant, cmpEqualsWant, 42}), verbose: true, expectedCheckFailure: fmt.Sprintf(` error: values are not deep equal diff (-got +want): %s got: []interface {}{ qt_test.cmpType{ Strings: { "who", "dalek", }, Ints: {42}, }, qt_test.cmpType{ Strings: { "who", "dalek", }, Ints: {42}, }, } want: []interface {}{ qt_test.cmpType{ Strings: { "who", "dalek", }, Ints: {42}, }, qt_test.cmpType{ Strings: { "who", "dalek", }, Ints: {42}, }, int(42), } `, diff([]any{cmpEqualsWant, cmpEqualsWant}, []any{cmpEqualsWant, cmpEqualsWant, 42})), }, { about: "CmpEquals: different values, long output", checker: qt.CmpEquals([]any{cmpEqualsWant, "extra line 1", "extra line 2", "extra line 3"}, []any{cmpEqualsWant, "extra line 1"}), expectedCheckFailure: fmt.Sprintf(` error: values are not deep equal diff (-got +want): %s got: want: []interface {}{ qt_test.cmpType{ Strings: { "who", "dalek", }, Ints: {42}, }, "extra line 1", } `, diff([]any{cmpEqualsWant, "extra line 1", "extra line 2", "extra line 3"}, []any{cmpEqualsWant, "extra line 1"})), }, { about: "CmpEquals: different values: long output and verbose", checker: qt.CmpEquals([]any{cmpEqualsWant, "extra line 1", "extra line 2"}, []any{cmpEqualsWant, "extra line 1"}), verbose: true, expectedCheckFailure: fmt.Sprintf(` error: values are not deep equal diff (-got +want): %s got: []interface {}{ qt_test.cmpType{ Strings: { "who", "dalek", }, Ints: {42}, }, "extra line 1", "extra line 2", } want: []interface {}{ qt_test.cmpType{ Strings: { "who", "dalek", }, Ints: {42}, }, "extra line 1", } `, diff([]any{cmpEqualsWant, "extra line 1", "extra line 2"}, []any{cmpEqualsWant, "extra line 1"})), }, { about: "CmpEquals: different values, long output, same number of lines", 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"}), expectedCheckFailure: fmt.Sprintf(` error: values are not deep equal diff (-got +want): %s got: want: `, diff([]any{cmpEqualsWant, "extra line 1", "extra line 2", "extra line 3"}, []any{cmpEqualsWant, "extra line 1", "extra line 2", "extra line three"})), }, { about: "CmpEquals: same values with options", checker: qt.CmpEquals([]int{1, 2, 3}, []int{3, 2, 1}, sameInts), expectedNegateFailure: ` error: unexpected success got: []int{1, 2, 3} want: []int{3, 2, 1} `, }, { about: "CmpEquals: different values with options", checker: qt.CmpEquals([]int{1, 2, 4}, []int{3, 2, 1}, sameInts), expectedCheckFailure: fmt.Sprintf(` error: values are not deep equal diff (-got +want): %s got: []int{1, 2, 4} want: []int{3, 2, 1} `, diff([]int{1, 2, 4}, []int{3, 2, 1}, sameInts)), }, { about: "DeepEquals: structs with unexported fields not allowed", checker: qt.DeepEquals( struct{ answer int }{ answer: 42, }, struct{ answer int }{ answer: 42, }, ), expectedCheckFailure: ` error: bad check: cannot handle unexported field at root.answer: "github.com/go-quicktest/qt_test".(struct { answer int }) consider using a custom Comparer; if you control the implementation of type, you can also consider using an Exporter, AllowUnexported, or cmpopts.IgnoreUnexported `, expectedNegateFailure: ` error: bad check: cannot handle unexported field at root.answer: "github.com/go-quicktest/qt_test".(struct { answer int }) consider using a custom Comparer; if you control the implementation of type, you can also consider using an Exporter, AllowUnexported, or cmpopts.IgnoreUnexported `, }, { about: "CmpEquals: structs with unexported fields ignored", checker: qt.CmpEquals( struct{ answer int }{ answer: 42, }, struct{ answer int }{ answer: 42, }, cmpopts.IgnoreUnexported(struct{ answer int }{})), expectedNegateFailure: ` error: unexpected success got: struct { answer int }{answer:42} want: `, }, { about: "DeepEquals: same times", checker: qt.DeepEquals(goTime, goTime), expectedNegateFailure: ` error: unexpected success got: s"2012-03-28 00:00:00 +0000 UTC" want: `, }, { about: "DeepEquals: different times: verbose", checker: qt.DeepEquals(goTime.Add(24*time.Hour), goTime), verbose: true, expectedCheckFailure: fmt.Sprintf(` error: values are not deep equal diff (-got +want): %s got: s"2012-03-29 00:00:00 +0000 UTC" want: s"2012-03-28 00:00:00 +0000 UTC" `, diff(goTime.Add(24*time.Hour), goTime)), }, { about: "ContentEquals: same values", checker: qt.ContentEquals([]string{"these", "are", "the", "voyages"}, []string{"these", "are", "the", "voyages"}), expectedNegateFailure: ` error: unexpected success got: []string{"these", "are", "the", "voyages"} want: `, }, { about: "ContentEquals: same contents", checker: qt.ContentEquals([]int{1, 2, 3}, []int{3, 2, 1}), expectedNegateFailure: ` error: unexpected success got: []int{1, 2, 3} want: []int{3, 2, 1} `, }, { about: "ContentEquals: same contents on complex slice", checker: qt.ContentEquals( []struct { Strings []any Ints []int }{cmpEqualsGot, cmpEqualsGot, cmpEqualsWant}, []struct { Strings []any Ints []int }{cmpEqualsWant, cmpEqualsGot, cmpEqualsGot}, ), expectedNegateFailure: ` error: unexpected success got: []struct { Strings []interface {}; Ints []int }{ { Strings: { "who", "dalek", }, Ints: {42, 47}, }, { Strings: { "who", "dalek", }, Ints: {42, 47}, }, { Strings: { "who", "dalek", }, Ints: {42}, }, } want: []struct { Strings []interface {}; Ints []int }{ { Strings: { "who", "dalek", }, Ints: {42}, }, { Strings: { "who", "dalek", }, Ints: {42, 47}, }, { Strings: { "who", "dalek", }, Ints: {42, 47}, }, } `}, { about: "ContentEquals: same contents on a nested slice", checker: qt.ContentEquals( struct { Nums []int }{ Nums: []int{1, 2, 3, 4}, }, struct { Nums []int }{ Nums: []int{4, 3, 2, 1}, }, ), expectedNegateFailure: ` error: unexpected success got: struct { Nums []int }{ Nums: {1, 2, 3, 4}, } want: struct { Nums []int }{ Nums: {4, 3, 2, 1}, } `, }, { about: "ContentEquals: slices of different type", checker: qt.ContentEquals[any]([]string{"bad", "wolf"}, []any{"bad", "wolf"}), expectedCheckFailure: fmt.Sprintf(` error: values are not deep equal diff (-got +want): %s got: []string{"bad", "wolf"} want: []interface {}{ "bad", "wolf", } `, diff([]string{"bad", "wolf"}, []any{"bad", "wolf"})), }, { about: "Matches: perfect match", checker: qt.Matches("exterminate", "exterminate"), expectedNegateFailure: ` error: unexpected success got value: "exterminate" regexp: `, }, { about: "Matches: match", checker: qt.Matches("these are the voyages", "these are the .*"), expectedNegateFailure: ` error: unexpected success got value: "these are the voyages" regexp: "these are the .*" `, }, { about: "Matches: mismatch", checker: qt.Matches("voyages", "these are the voyages"), expectedCheckFailure: ` error: value does not match regexp got value: "voyages" regexp: "these are the voyages" `, }, { about: "Matches: empty pattern", checker: qt.Matches("these are the voyages", ""), expectedCheckFailure: ` error: value does not match regexp got value: "these are the voyages" regexp: "" `, }, { about: "Matches: complex pattern", checker: qt.Matches("end of the universe", "bad wolf|end of the .*"), expectedNegateFailure: ` error: unexpected success got value: "end of the universe" regexp: "bad wolf|end of the .*" `, }, { about: "Matches: invalid pattern", checker: qt.Matches("voyages", "("), expectedCheckFailure: ` error: bad check: cannot compile regexp: error parsing regexp: missing closing ): ` + "`^(()$`" + ` regexp: "(" `, expectedNegateFailure: ` error: bad check: cannot compile regexp: error parsing regexp: missing closing ): ` + "`^(()$`" + ` regexp: "(" `, }, { about: "Matches: match with pre-compiled regexp", checker: qt.Matches("resistance is futile", regexp.MustCompile("resistance is (futile|useful)")), expectedNegateFailure: ` error: unexpected success got value: "resistance is futile" regexp: s"resistance is (futile|useful)" `, }, { about: "Matches: mismatch with pre-compiled regexp", checker: qt.Matches("resistance is cool", regexp.MustCompile("resistance is (futile|useful)")), expectedCheckFailure: ` error: value does not match regexp got value: "resistance is cool" regexp: s"resistance is (futile|useful)" `, }, { about: "Matches: match with pre-compiled multi-line regexp", checker: qt.Matches("line 1\nline 2", regexp.MustCompile(`line \d\nline \d`)), expectedNegateFailure: ` error: unexpected success got value: "line 1\nline 2" regexp: s"line \\d\\nline \\d" `, }, { about: "ErrorMatches: perfect match", checker: qt.ErrorMatches(errBadWolf, "bad wolf"), expectedNegateFailure: ` error: unexpected success got error: bad wolf file:line regexp: "bad wolf" `, }, { about: "ErrorMatches: match", checker: qt.ErrorMatches(errBadWolf, "bad .*"), expectedNegateFailure: ` error: unexpected success got error: bad wolf file:line regexp: "bad .*" `, }, { about: "ErrorMatches: mismatch", checker: qt.ErrorMatches(errBadWolf, "exterminate"), expectedCheckFailure: ` error: error does not match regexp got error: bad wolf file:line regexp: "exterminate" `, }, { about: "ErrorMatches: empty pattern", checker: qt.ErrorMatches(errBadWolf, ""), expectedCheckFailure: ` error: error does not match regexp got error: bad wolf file:line regexp: "" `, }, { about: "ErrorMatches: complex pattern", checker: qt.ErrorMatches(errBadWolf, "bad wolf|end of the universe"), expectedNegateFailure: ` error: unexpected success got error: bad wolf file:line regexp: "bad wolf|end of the universe" `, }, { about: "ErrorMatches: invalid pattern", checker: qt.ErrorMatches(errBadWolf, "("), expectedCheckFailure: ` error: bad check: cannot compile regexp: error parsing regexp: missing closing ): ` + "`^(()$`" + ` regexp: "(" `, expectedNegateFailure: ` error: bad check: cannot compile regexp: error parsing regexp: missing closing ): ` + "`^(()$`" + ` regexp: "(" `, }, { about: "ErrorMatches: nil error", checker: qt.ErrorMatches(nil, "some pattern"), expectedCheckFailure: ` error: got nil error but want non-nil got error: nil regexp: "some pattern" `, }, { about: "ErrorMatches: match with pre-compiled regexp", checker: qt.ErrorMatches(errBadWolf, regexp.MustCompile("bad (wolf|dog)")), expectedNegateFailure: ` error: unexpected success got error: bad wolf file:line regexp: s"bad (wolf|dog)" `, }, { about: "ErrorMatches: match with pre-compiled multi-line regexp", checker: qt.ErrorMatches(errBadWolfMultiLine, regexp.MustCompile(`bad (wolf|dog)\nfaulty (logic|statement)`)), expectedNegateFailure: ` error: unexpected success got error: bad wolf faulty logic file:line regexp: s"bad (wolf|dog)\\nfaulty (logic|statement)" `, }, { about: "ErrorMatches: mismatch with pre-compiled regexp", checker: qt.ErrorMatches(errBadWolf, regexp.MustCompile("good (wolf|dog)")), expectedCheckFailure: ` error: error does not match regexp got error: bad wolf file:line regexp: s"good (wolf|dog)" `, }, { about: "PanicMatches: perfect match", checker: qt.PanicMatches(func() { panic("error: bad wolf") }, "error: bad wolf"), expectedNegateFailure: ` error: unexpected success panic value: "error: bad wolf" function: func() {...} regexp: `, }, { about: "PanicMatches: match", checker: qt.PanicMatches(func() { panic("error: bad wolf") }, "error: .*"), expectedNegateFailure: ` error: unexpected success panic value: "error: bad wolf" function: func() {...} regexp: "error: .*" `, }, { about: "PanicMatches: mismatch", checker: qt.PanicMatches(func() { panic("error: bad wolf") }, "error: exterminate"), expectedCheckFailure: ` error: panic value does not match regexp panic value: "error: bad wolf" function: func() {...} regexp: "error: exterminate" `, }, { about: "PanicMatches: empty pattern", checker: qt.PanicMatches(func() { panic("error: bad wolf") }, ""), expectedCheckFailure: ` error: panic value does not match regexp panic value: "error: bad wolf" function: func() {...} regexp: "" `, }, { about: "PanicMatches: complex pattern", checker: qt.PanicMatches(func() { panic("bad wolf") }, "bad wolf|end of the universe"), expectedNegateFailure: ` error: unexpected success panic value: "bad wolf" function: func() {...} regexp: "bad wolf|end of the universe" `, }, { about: "PanicMatches: invalid pattern", checker: qt.PanicMatches(func() { panic("error: bad wolf") }, "("), expectedCheckFailure: ` error: bad check: cannot compile regexp: error parsing regexp: missing closing ): ` + "`^(()$`" + ` panic value: "error: bad wolf" regexp: "(" `, expectedNegateFailure: ` error: bad check: cannot compile regexp: error parsing regexp: missing closing ): ` + "`^(()$`" + ` panic value: "error: bad wolf" regexp: "(" `, }, { about: "PanicMatches: no panic", checker: qt.PanicMatches(func() {}, ".*"), expectedCheckFailure: ` error: function did not panic function: func() {...} regexp: ".*" `, }, { about: "PanicMatches: match with pre-compiled regexp", checker: qt.PanicMatches(func() { panic("error: bad wolf") }, regexp.MustCompile("error: bad (wolf|dog)")), expectedNegateFailure: ` error: unexpected success panic value: "error: bad wolf" function: func() {...} regexp: s"error: bad (wolf|dog)" `, }, { about: "PanicMatches: match with pre-compiled multi-line regexp", checker: qt.PanicMatches(func() { panic("error: bad wolf\nfaulty logic") }, regexp.MustCompile(`error: bad (wolf|dog)\nfaulty (logic|statement)`)), expectedNegateFailure: ` error: unexpected success panic value: "error: bad wolf\nfaulty logic" function: func() {...} regexp: s"error: bad (wolf|dog)\\nfaulty (logic|statement)" `, }, { about: "PanicMatches: mismatch with pre-compiled regexp", checker: qt.PanicMatches(func() { panic("error: bad wolf") }, regexp.MustCompile("good (wolf|dog)")), expectedCheckFailure: ` error: panic value does not match regexp panic value: "error: bad wolf" function: func() {...} regexp: s"good (wolf|dog)" `, }, { about: "IsNil: nil", checker: qt.IsNil(any(nil)), expectedNegateFailure: ` error: got but want non-nil got: nil `, }, { about: "IsNil: nil pointer to struct", checker: qt.IsNil((*struct{})(nil)), expectedNegateFailure: ` error: got nil ptr but want non-nil got: (*struct {})(nil) `, }, { about: "IsNil: nil func", checker: qt.IsNil((func())(nil)), expectedNegateFailure: ` error: got nil func but want non-nil got: func() {...} `, }, { about: "IsNil: nil map", checker: qt.IsNil((map[string]string)(nil)), expectedNegateFailure: ` error: got nil map but want non-nil got: map[string]string{} `, }, { about: "IsNil: nil slice", checker: qt.IsNil(([]int)(nil)), expectedNegateFailure: ` error: got nil slice but want non-nil got: []int(nil) `, }, { about: "IsNil: nil error-implementing type", checker: qt.IsNil(error((*errTest)(nil))), // TODO e isn't great here - perhaps we should // mention the type too. expectedCheckFailure: ` error: got non-nil value got: e `, }, { about: "IsNil: not nil", checker: qt.IsNil([]int{}), expectedCheckFailure: ` error: got non-nil value got: []int{} `, }, { about: "IsNil: error is not nil", checker: qt.IsNil(error(errBadWolf)), expectedCheckFailure: ` error: got non-nil value got: bad wolf file:line `, }, { about: "IsNotNil: success", checker: qt.IsNotNil(any(42)), expectedNegateFailure: ` error: got non-nil value got: int(42) `, }, { about: "IsNotNil: failure", checker: qt.IsNotNil[any](nil), expectedCheckFailure: ` error: got but want non-nil got: nil `, }, { about: "HasLen: arrays with the same length", checker: qt.HasLen([4]string{"these", "are", "the", "voyages"}, 4), expectedNegateFailure: ` error: unexpected success len(got): int(4) got: [4]string{"these", "are", "the", "voyages"} want length: `, }, { about: "HasLen: channels with the same length", checker: qt.HasLen(chInt, 2), expectedNegateFailure: fmt.Sprintf(` error: unexpected success len(got): int(2) got: (chan int)(%v) want length: `, chInt), }, { about: "HasLen: maps with the same length", checker: qt.HasLen(map[string]bool{"true": true}, 1), expectedNegateFailure: ` error: unexpected success len(got): int(1) got: map[string]bool{"true":true} want length: `, }, { about: "HasLen: slices with the same length", checker: qt.HasLen([]int{}, 0), expectedNegateFailure: ` error: unexpected success len(got): int(0) got: []int{} want length: `, }, { about: "HasLen: strings with the same length", checker: qt.HasLen("these are the voyages", 21), expectedNegateFailure: ` error: unexpected success len(got): int(21) got: "these are the voyages" want length: `, }, { about: "HasLen: arrays with different lengths", checker: qt.HasLen([4]string{"these", "are", "the", "voyages"}, 0), expectedCheckFailure: ` error: unexpected length len(got): int(4) got: [4]string{"these", "are", "the", "voyages"} want length: int(0) `, }, { about: "HasLen: channels with different lengths", checker: qt.HasLen(chInt, 4), expectedCheckFailure: fmt.Sprintf(` error: unexpected length len(got): int(2) got: (chan int)(%v) want length: int(4) `, chInt), }, { about: "HasLen: maps with different lengths", checker: qt.HasLen(map[string]bool{"true": true}, 42), expectedCheckFailure: ` error: unexpected length len(got): int(1) got: map[string]bool{"true":true} want length: int(42) `, }, { about: "HasLen: slices with different lengths", checker: qt.HasLen([]int{42, 47}, 1), expectedCheckFailure: ` error: unexpected length len(got): int(2) got: []int{42, 47} want length: int(1) `, }, { about: "HasLen: strings with different lengths", checker: qt.HasLen("these are the voyages", 42), expectedCheckFailure: ` error: unexpected length len(got): int(21) got: "these are the voyages" want length: int(42) `, }, { about: "HasLen: value without a length", checker: qt.HasLen(42, 42), expectedCheckFailure: ` error: bad check: first argument of type int has no length got: int(42) `, expectedNegateFailure: ` error: bad check: first argument of type int has no length got: int(42) `, }, { about: "Implements: implements interface", checker: qt.Implements[error](errBadWolf), expectedNegateFailure: ` error: unexpected success got: bad wolf file:line want interface: error `, }, { about: "Implements: does not implement interface", checker: qt.Implements[Fooer](errBadWolf), expectedCheckFailure: ` error: got value does not implement wanted interface got: bad wolf file:line want interface: qt_test.Fooer `, }, { about: "Implements: fails if got nil", checker: qt.Implements[Fooer](nil), expectedCheckFailure: ` error: got nil value but want non-nil got: nil `, }, { about: "Satisfies: success with an error", checker: qt.Satisfies(qt.BadCheckf("bad wolf"), qt.IsBadCheck), expectedNegateFailure: ` error: unexpected success got: e"bad check: bad wolf" predicate: func(error) bool {...} `, }, { about: "Satisfies: success with an int", checker: qt.Satisfies(42, func(v int) bool { return v == 42 }), expectedNegateFailure: ` error: unexpected success got: int(42) predicate: func(int) bool {...} `, }, { about: "Satisfies: success with nil", checker: qt.Satisfies([]int(nil), func(v []int) bool { return true }), expectedNegateFailure: ` error: unexpected success got: []int(nil) predicate: func([]int) bool {...} `, }, { about: "Satisfies: failure with an error", checker: qt.Satisfies(nil, qt.IsBadCheck), expectedCheckFailure: ` error: value does not satisfy predicate function got: nil predicate: func(error) bool {...} `, }, { about: "Satisfies: failure with a string", checker: qt.Satisfies("bad wolf", func(string) bool { return false }), expectedCheckFailure: ` error: value does not satisfy predicate function got: "bad wolf" predicate: func(string) bool {...} `, }, { about: "IsTrue: success", checker: qt.IsTrue(true), expectedNegateFailure: ` error: unexpected success got: bool(true) want: `, }, { about: "IsTrue: failure", checker: qt.IsTrue(false), expectedCheckFailure: ` error: values are not equal got: bool(false) want: bool(true) `, }, { about: "IsTrue: success with subtype", checker: qt.IsTrue(boolean(true)), expectedNegateFailure: ` error: unexpected success got: qt_test.boolean(true) want: `, }, { about: "IsTrue: failure with subtype", checker: qt.IsTrue(boolean(false)), expectedCheckFailure: ` error: values are not equal got: qt_test.boolean(false) want: qt_test.boolean(true) `, }, { about: "IsFalse: success", checker: qt.IsFalse(false), expectedNegateFailure: ` error: unexpected success got: bool(false) want: `, }, { about: "IsFalse: failure", checker: qt.IsFalse(true), expectedCheckFailure: ` error: values are not equal got: bool(true) want: bool(false) `, }, { about: "StringContains match", checker: qt.StringContains("hello, world", "world"), expectedNegateFailure: ` error: unexpected success got: "hello, world" substr: "world" `, }, { about: "StringContains no match", checker: qt.StringContains("hello, world", "worlds"), expectedCheckFailure: ` error: no substring match found got: "hello, world" substr: "worlds" `}, { about: "SliceContains match", checker: qt.SliceContains([]string{"a", "b", "c"}, "a"), expectedNegateFailure: ` error: unexpected success container: []string{"a", "b", "c"} want: "a" `, }, { about: "SliceContains mismatch", checker: qt.SliceContains([]string{"a", "b", "c"}, "d"), expectedCheckFailure: ` error: no matching element found container: []string{"a", "b", "c"} want: "d" `, }, { about: "Contains with map", checker: qt.MapContains(map[string]string{ "a": "d", "b": "a", }, "d"), expectedNegateFailure: ` error: unexpected success container: map[string]string{"a":"d", "b":"a"} want: "d" `, }, { about: "Contains with map and interface value", checker: qt.MapContains(map[string]any{ "a": "d", "b": "a", }, "d"), expectedNegateFailure: ` error: unexpected success container: map[string]interface {}{ "a": "d", "b": "a", } want: "d" `, }, { about: "All slice equals", checker: qt.SliceAll([]string{"a", "a"}, qt.F2(qt.Equals[string], "a")), expectedNegateFailure: ` error: unexpected success container: []string{"a", "a"} want: "a" `, }, { about: "All slice match", checker: qt.SliceAll([]string{"red", "blue", "green"}, qt.F2(qt.Matches[string], ".*e.*")), expectedNegateFailure: ` error: unexpected success container: []string{"red", "blue", "green"} regexp: ".*e.*" `, }, { about: "All nested match", // TODO this is a bit awkward. Is there something we could do to improve it? checker: qt.SliceAll([][]string{{"hello", "goodbye"}, {"red", "blue"}, {}}, func(elem []string) qt.Checker { return qt.SliceAll(elem, qt.F2(qt.Matches[string], ".*e.*")) }), expectedNegateFailure: ` error: unexpected success container: [][]string{ {"hello", "goodbye"}, {"red", "blue"}, {}, } regexp: ".*e.*" `, }, { about: "All nested mismatch", checker: qt.SliceAll([][]string{{"hello", "goodbye"}, {"black", "blue"}, {}}, func(elem []string) qt.Checker { return qt.SliceAll(elem, qt.F2(qt.Matches[string], ".*e.*")) }), expectedCheckFailure: ` error: mismatch at index 1 error: mismatch at index 0 error: value does not match regexp first mismatched element: "black" `, }, { about: "All slice mismatch", checker: qt.SliceAll([]string{"red", "black"}, qt.F2(qt.Matches[string], ".*e.*")), expectedCheckFailure: ` error: mismatch at index 1 error: value does not match regexp first mismatched element: "black" `, }, { about: "All slice mismatch with DeepEqual", checker: qt.SliceAll([][]string{{"a", "b"}, {"a", "c"}}, qt.F2(qt.DeepEquals[[]string], []string{"a", "b"})), expectedCheckFailure: fmt.Sprintf(` error: mismatch at index 1 error: values are not deep equal diff (-got +want): %s got: []string{"a", "c"} want: []string{"a", "b"} `, diff([]string{"a", "c"}, []string{"a", "b"})), }, { about: "All mismatch with map", checker: qt.MapAll(map[string]string{"a": "red", "b": "black"}, qt.F2(qt.Matches[string], ".*e.*")), expectedCheckFailure: ` error: mismatch at key "b" error: value does not match regexp first mismatched element: "black" `}, { about: "Any no match", checker: qt.SliceAny([]int{}, qt.F2(qt.Equals[int], 5)), expectedCheckFailure: ` error: no matching element found container: []int{} want: int(5) `, }, { about: "JSONEquals simple", checker: qt.JSONEquals( []byte(`{"First": 47.11}`), &OuterJSON{ First: 47.11, }, ), expectedNegateFailure: tilde2bq(` error: unexpected success got: []uint8(~{"First": 47.11}~) want: &qt_test.OuterJSON{ First: 47.11, Second: nil, } `), }, { about: "JSONEquals nested", checker: qt.JSONEquals( `{"First": 47.11, "Last": [{"First": "Hello", "Second": 42}]}`, &OuterJSON{ First: 47.11, Second: []*InnerJSON{ {First: "Hello", Second: 42}, }, }, ), expectedNegateFailure: tilde2bq(` error: unexpected success got: ~{"First": 47.11, "Last": [{"First": "Hello", "Second": 42}]}~ want: &qt_test.OuterJSON{ First: 47.11, Second: { &qt_test.InnerJSON{ First: "Hello", Second: 42, Third: {}, }, }, } `), }, { about: "JSONEquals nested with newline", checker: qt.JSONEquals( `{"First": 47.11, "Last": [{"First": "Hello", "Second": 42}, {"First": "World", "Third": {"F": false}}]}`, &OuterJSON{ First: 47.11, Second: []*InnerJSON{ {First: "Hello", Second: 42}, {First: "World", Third: map[string]bool{ "F": false, }}, }, }, ), expectedNegateFailure: ` error: unexpected success got: "{\"First\": 47.11, \"Last\": [{\"First\": \"Hello\", \"Second\": 42},\n\t\t\t{\"First\": \"World\", \"Third\": {\"F\": false}}]}" want: &qt_test.OuterJSON{ First: 47.11, Second: { &qt_test.InnerJSON{ First: "Hello", Second: 42, Third: {}, }, &qt_test.InnerJSON{ First: "World", Second: 0, Third: {"F":false}, }, }, } `, }, { about: "JSONEquals extra field", checker: qt.JSONEquals( `{"NotThere": 1}`, &OuterJSON{ First: 2, }, ), expectedCheckFailure: fmt.Sprintf(` error: values are not deep equal diff (-got +want): %s got: map[string]interface {}{ "NotThere": float64(1), } want: map[string]interface {}{ "First": float64(2), } `, diff(map[string]any{"NotThere": 1.0}, map[string]any{"First": 2.0})), }, { about: "JSONEquals cannot unmarshal obtained value", checker: qt.JSONEquals([]byte(`{"NotThere": `), nil), expectedCheckFailure: fmt.Sprintf(tilde2bq(` error: cannot unmarshal obtained contents: %s; "{\"NotThere\": " got: []uint8(~{"NotThere": ~) want: nil `), mustJSONUnmarshalErr(`{"NotThere": `)), }, { about: "JSONEquals cannot marshal expected value", checker: qt.JSONEquals([]byte(`null`), jsonErrorMarshaler{}), expectedCheckFailure: ` error: bad check: cannot marshal expected contents: json: error calling MarshalJSON for type qt_test.jsonErrorMarshaler: qt json marshal error `, expectedNegateFailure: ` error: bad check: cannot marshal expected contents: json: error calling MarshalJSON for type qt_test.jsonErrorMarshaler: qt json marshal error `, }, { about: "JSONEquals with []byte", checker: qt.JSONEquals([]byte("null"), nil), expectedNegateFailure: ` error: unexpected success got: []uint8("null") want: nil `, }, { about: "JSONEquals with RawMessage", checker: qt.JSONEquals([]byte("null"), json.RawMessage("null")), expectedNegateFailure: ` error: unexpected success got: []uint8("null") want: json.RawMessage("null") `, }, { about: "CodecEquals with bad marshal", checker: qt.CodecEquals( "null", nil, func(x any) ([]byte, error) { return []byte("bad json"), nil }, json.Unmarshal, ), expectedCheckFailure: fmt.Sprintf(` error: bad check: cannot unmarshal expected contents: %s `, mustJSONUnmarshalErr("bad json")), expectedNegateFailure: fmt.Sprintf(` error: bad check: cannot unmarshal expected contents: %s `, mustJSONUnmarshalErr("bad json")), }, { about: "CodecEquals with options", checker: qt.CodecEquals( `["b", "z", "c", "a"]`, []string{"a", "c", "z", "b"}, json.Marshal, json.Unmarshal, cmpopts.SortSlices(func(x, y any) bool { return x.(string) < y.(string) }), ), expectedNegateFailure: tilde2bq(` error: unexpected success got: ~["b", "z", "c", "a"]~ want: []string{"a", "c", "z", "b"} `), }, { about: "ErrorAs: exact match", checker: qt.ErrorAs(targetErr, new(*errTarget)), expectedNegateFailure: ` error: unexpected success got: e"ptr: target" as type: *qt_test.errTarget `, }, { about: "ErrorAs: wrapped match", checker: qt.ErrorAs(fmt.Errorf("wrapped: %w", targetErr), new(*errTarget)), expectedNegateFailure: ` error: unexpected success got: e"wrapped: ptr: target" as type: *qt_test.errTarget `, }, { about: "ErrorAs: fails if nil error", checker: qt.ErrorAs(nil, new(*errTarget)), expectedCheckFailure: ` error: got nil error but want non-nil got: nil as type: *qt_test.errTarget `, }, { about: "ErrorAs: fails if mismatch", checker: qt.ErrorAs(errors.New("other error"), new(*errTarget)), expectedCheckFailure: ` error: wanted type is not found in error chain got: e"other error" as type: *qt_test.errTarget `, }, { about: "ErrorAs: fails if mismatch with a non-pointer error implementation", checker: qt.ErrorAs(errors.New("other error"), new(errTargetNonPtr)), expectedCheckFailure: ` error: wanted type is not found in error chain got: e"other error" as type: qt_test.errTargetNonPtr `, }, { about: "ErrorAs: bad check if invalid as", checker: qt.ErrorAs(targetErr, &struct{}{}), expectedCheckFailure: ` error: bad check: errors: *target must be interface or implement error `, expectedNegateFailure: ` error: bad check: errors: *target must be interface or implement error `, }, { about: "ErrorIs: exact match", checker: qt.ErrorIs(targetErr, targetErr), expectedNegateFailure: ` error: unexpected success got: e"ptr: target" want: `, }, { about: "ErrorIs: wrapped match", checker: qt.ErrorIs(fmt.Errorf("wrapped: %w", targetErr), targetErr), expectedNegateFailure: ` error: unexpected success got: e"wrapped: ptr: target" want: e"ptr: target" `, }, { about: "ErrorIs: fails if nil error", checker: qt.ErrorIs(nil, targetErr), expectedCheckFailure: ` error: got nil error but want non-nil got: nil want: e"ptr: target" `, }, { about: "ErrorIs: fails if mismatch", checker: qt.ErrorIs(errors.New("other error"), targetErr), expectedCheckFailure: ` error: wanted error is not found in error chain got: e"other error" want: e"ptr: target" `, }, { about: "ErrorIs: nil to nil match", checker: qt.ErrorIs(nil, nil), expectedNegateFailure: ` error: unexpected success got: nil want: `, }, { about: "ErrorIs: non-nil to nil mismatch", checker: qt.ErrorIs(targetErr, nil), expectedCheckFailure: ` error: wanted error is not found in error chain got: e"ptr: target" want: nil `, }, { about: "Not: failure", checker: qt.Not(qt.Equals(42, 42)), expectedCheckFailure: ` error: unexpected success got: int(42) want: `, }, { about: "Not: IsNil failure", checker: qt.Not(qt.IsNil[*int](nil)), expectedCheckFailure: ` error: got nil ptr but want non-nil got: (*int)(nil) `, }} func TestCheckers(t *testing.T) { original := qt.TestingVerbose defer func() { qt.TestingVerbose = original }() for _, test := range checkerTests { *qt.TestingVerbose = func() bool { return test.verbose } t.Run(test.about, func(t *testing.T) { tt := &testingT{} ok := qt.Check(tt, test.checker) checkResult(t, ok, tt.errorString(), test.expectedCheckFailure) }) t.Run("Not "+test.about, func(t *testing.T) { tt := &testingT{} ok := qt.Check(tt, qt.Not(test.checker)) checkResult(t, ok, tt.errorString(), test.expectedNegateFailure) }) } } func diff(x, y any, opts ...cmp.Option) string { d := cmp.Diff(x, y, opts...) return strings.TrimSuffix(qt.Prefixf(" ", "%s", d), "\n") } type jsonErrorMarshaler struct{} func (jsonErrorMarshaler) MarshalJSON() ([]byte, error) { return nil, fmt.Errorf("qt json marshal error") } func mustJSONUnmarshalErr(s string) error { var v any err := json.Unmarshal([]byte(s), &v) if err == nil { panic("want JSON error, got nil") } return err } func tilde2bq(s string) string { return strings.Replace(s, "~", "`", -1) } qt-1.101.0/comment.go000066400000000000000000000014321446275767100143120ustar00rootroot00000000000000// Licensed under the MIT license, see LICENSE file for details. package qt import "fmt" // Commentf returns a test comment whose output is formatted according to // the given format specifier and args. It may be provided as the last argument // to any check or assertion and will be displayed if the check or assertion // fails. func Commentf(format string, args ...any) Comment { return Comment{ format: format, args: args, } } // Comment represents additional information on a check or an assertion which is // displayed when the check or assertion fails. type Comment struct { format string args []any } // String outputs a string formatted according to the stored format specifier // and args. func (c Comment) String() string { return fmt.Sprintf(c.format, c.args...) } qt-1.101.0/comment_test.go000066400000000000000000000012031446275767100153450ustar00rootroot00000000000000// Licensed under the MIT license, see LICENSE file for details. package qt_test import ( "testing" "github.com/go-quicktest/qt" ) func TestCommentf(t *testing.T) { c := qt.Commentf("the answer is %d", 42) comment := c.String() expectedComment := "the answer is 42" if comment != expectedComment { t.Fatalf("comment error:\ngot %q\nwant %q", comment, expectedComment) } } func TestConstantCommentf(t *testing.T) { const expectedComment = "bad wolf" c := qt.Commentf(expectedComment) comment := c.String() if comment != expectedComment { t.Fatalf("constant comment error:\ngot %q\nwant %q", comment, expectedComment) } } qt-1.101.0/error.go000066400000000000000000000020521446275767100140000ustar00rootroot00000000000000// Licensed under the MIT license, see LICENCE file for details. package qt import ( "fmt" ) // BadCheckf returns an error used to report a problem with the checker // invocation or testing execution itself (like wrong number or type of // arguments) rather than a real Check or Assert failure. // This helper can be used when implementing checkers. func BadCheckf(format string, a ...any) error { e := badCheck(fmt.Sprintf(format, a...)) return &e } // IsBadCheck reports whether the given error has been created by BadCheckf. // This helper can be used when implementing checkers. func IsBadCheck(err error) bool { _, ok := err.(*badCheck) return ok } type badCheck string // Error implements the error interface. func (e *badCheck) Error() string { return "bad check: " + string(*e) } // ErrSilent is the error used when there is no need to include in the failure // output the "error" and "check" keys and all the keys automatically // added for args. This helper can be used when implementing checkers. var ErrSilent = fmt.Errorf("silent failure") qt-1.101.0/error_test.go000066400000000000000000000022311446275767100150360ustar00rootroot00000000000000// Licensed under the MIT license, see LICENSE file for details. package qt_test import ( "errors" "fmt" "testing" "github.com/go-quicktest/qt" ) func TestBadCheckf(t *testing.T) { err := qt.BadCheckf("bad %s", "wolf") expectedMessage := "bad check: bad wolf" if err.Error() != expectedMessage { t.Fatalf("error:\ngot %q\nwant %q", err, expectedMessage) } } func TestIsBadCheck(t *testing.T) { err := qt.BadCheckf("bad wolf") assertBool(t, qt.IsBadCheck(err), true) err = errors.New("bad wolf") assertBool(t, qt.IsBadCheck(err), false) } var errBadWolf = &errTest{ msg: "bad wolf", formatted: true, } var errBadWolfMultiLine = &errTest{ msg: "bad wolf\nfaulty logic", formatted: true, } // errTest is an error type used in tests. type errTest struct { msg string formatted bool } // Error implements error. func (err *errTest) Error() string { return err.msg } // Format implements fmt.Formatter. func (err *errTest) Format(f fmt.State, c rune) { if !f.Flag('+') || c != 'v' { fmt.Fprint(f, "unexpected verb for formatting the error") } fmt.Fprint(f, err.Error()) if err.formatted { fmt.Fprint(f, "\n file:line") } } qt-1.101.0/example_test.go000066400000000000000000000154501446275767100153470ustar00rootroot00000000000000package qt_test import ( "errors" "fmt" "io" "math" "net" "os" "testing" "github.com/go-quicktest/qt" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" ) func ExampleComment() { runExampleTest(func(t testing.TB) { a := 42 qt.Assert(t, qt.Equals(a, 42), qt.Commentf("no answer to life, the universe, and everything")) }) // Output: PASS } func ExampleEquals() { runExampleTest(func(t testing.TB) { answer := int64(42) qt.Assert(t, qt.Equals(answer, 42)) }) // Output: PASS } func ExampleDeepEquals() { runExampleTest(func(t testing.TB) { list := []int{42, 47} qt.Assert(t, qt.DeepEquals(list, []int{42, 47})) }) // Output: PASS } func ExampleCmpEquals() { runExampleTest(func(t testing.TB) { list := []int{42, 47} qt.Assert(t, qt.CmpEquals(list, []int{47, 42}, cmpopts.SortSlices(func(i, j int) bool { return i < j }))) }) // Output: PASS } type myStruct struct { a int } func customDeepEquals[T any](got, want T) qt.Checker { return qt.CmpEquals(got, want, cmp.AllowUnexported(myStruct{})) } func ExampleCmpEquals_customfunc() { runExampleTest(func(t testing.TB) { got := &myStruct{ a: 1234, } qt.Assert(t, customDeepEquals(got, &myStruct{ a: 1234, })) }) // Output: PASS } func ExampleContentEquals() { runExampleTest(func(t testing.TB) { got := []int{1, 23, 4, 5} qt.Assert(t, qt.ContentEquals(got, []int{1, 4, 5, 23})) }) // Output: PASS } func ExampleMatches() { runExampleTest(func(t testing.TB) { qt.Assert(t, qt.Matches("these are the voyages", "these are .*")) qt.Assert(t, qt.Matches(net.ParseIP("1.2.3.4").String(), "1.*")) }) // Output: PASS } func ExampleErrorMatches() { runExampleTest(func(t testing.TB) { err := errors.New("bad wolf at the door") qt.Assert(t, qt.ErrorMatches(err, "bad wolf .*")) }) // Output: PASS } func ExamplePanicMatches() { runExampleTest(func(t testing.TB) { divide := func(a, b int) int { return a / b } qt.Assert(t, qt.PanicMatches(func() { divide(5, 0) }, "runtime error: .*")) }) // Output: PASS } func ExampleIsNil() { runExampleTest(func(t testing.TB) { got := (*int)(nil) qt.Assert(t, qt.IsNil(got)) }) // Output: PASS } func ExampleIsNotNil() { runExampleTest(func(t testing.TB) { got := new(int) qt.Assert(t, qt.IsNotNil(got)) // Note that unlike reflection-based APIs, a nil // value inside an interface still counts as non-nil, // just as if we were comparing the actual interface // value against nil. nilValueInInterface := any((*int)(nil)) qt.Assert(t, qt.IsNotNil(nilValueInInterface)) }) // Output: PASS } func ExampleHasLen() { runExampleTest(func(t testing.TB) { qt.Assert(t, qt.HasLen([]int{42, 47}, 2)) myMap := map[string]int{ "a": 13, "b": 4, "c": 10, } qt.Assert(t, qt.HasLen(myMap, 3)) }) // Output: PASS } func ExampleImplements() { runExampleTest(func(t testing.TB) { var myReader struct { io.ReadCloser } qt.Assert(t, qt.Implements[io.ReadCloser](myReader)) }) // Output: PASS } func ExampleSatisfies() { runExampleTest(func(t testing.TB) { // Check that an error from os.Open satisfies os.IsNotExist. _, err := os.Open("/non-existent-file") qt.Assert(t, qt.Satisfies(err, os.IsNotExist)) // Check that a floating point number is a not-a-number. f := math.NaN() qt.Assert(t, qt.Satisfies(f, math.IsNaN)) }) // Output: PASS } func ExampleIsTrue() { runExampleTest(func(t testing.TB) { isValid := func() bool { return true } qt.Assert(t, qt.IsTrue(1 == 1)) qt.Assert(t, qt.IsTrue(isValid())) }) // Output: PASS } func ExampleIsFalse() { runExampleTest(func(t testing.TB) { isValid := func() bool { return false } qt.Assert(t, qt.IsFalse(1 == 0)) qt.Assert(t, qt.IsFalse(isValid())) }) // Output: PASS } func ExampleNot() { runExampleTest(func(t testing.TB) { got := []int{1, 2} qt.Assert(t, qt.Not(qt.IsNil(got))) answer := 13 qt.Assert(t, qt.Not(qt.Equals(answer, 42))) }) // Output: PASS } func ExampleStringContains() { runExampleTest(func(t testing.TB) { qt.Assert(t, qt.StringContains("hello world", "hello")) }) // Output: PASS } func ExampleSliceContains() { runExampleTest(func(t testing.TB) { qt.Assert(t, qt.SliceContains([]int{3, 5, 7, 99}, 99)) qt.Assert(t, qt.SliceContains([]string{"a", "cd", "e"}, "cd")) }) // Output: PASS } func ExampleMapContains() { runExampleTest(func(t testing.TB) { qt.Assert(t, qt.MapContains(map[string]int{ "hello": 1234, }, 1234)) }) // Output: PASS } func ExampleSliceAny() { runExampleTest(func(t testing.TB) { qt.Assert(t, qt.SliceAny([]int{3, 5, 7, 99}, qt.F2(qt.Equals[int], 7))) qt.Assert(t, qt.SliceAny([][]string{{"a", "b"}, {"c", "d"}}, qt.F2(qt.DeepEquals[[]string], []string{"c", "d"}))) }) // Output: PASS } func ExampleMapAny() { runExampleTest(func(t testing.TB) { qt.Assert(t, qt.MapAny(map[string]int{"x": 2, "y": 3}, qt.F2(qt.Equals[int], 3))) }) // Output: PASS } func ExampleSliceAll() { runExampleTest(func(t testing.TB) { qt.Assert(t, qt.SliceAll([]int{3, 5, 8}, func(e int) qt.Checker { return qt.Not(qt.Equals(e, 0)) })) qt.Assert(t, qt.SliceAll([][]string{{"a", "b"}, {"a", "b"}}, qt.F2(qt.DeepEquals[[]string], []string{"a", "b"}))) }) // Output: PASS } func ExampleMapAll() { runExampleTest(func(t testing.TB) { qt.Assert(t, qt.MapAll(map[string]int{ "x": 2, "y": 2, }, qt.F2(qt.Equals[int], 2))) }) // Output: PASS } func ExampleJSONEquals() { runExampleTest(func(t testing.TB) { data := `[1, 2, 3]` qt.Assert(t, qt.JSONEquals(data, []uint{1, 2, 3})) }) // Output: PASS } func ExampleErrorAs() { runExampleTest(func(t testing.TB) { _, err := os.Open("/non-existent-file") // Checking for a specific error type. qt.Assert(t, qt.ErrorAs(err, new(*os.PathError))) qt.Assert(t, qt.ErrorAs[*os.PathError](err, nil)) // Checking fields on a specific error type. var pathError *os.PathError if qt.Check(t, qt.ErrorAs(err, &pathError)) { qt.Assert(t, qt.Equals(pathError.Path, "/non-existent-file")) } }) // Output: PASS } func ExampleErrorIs() { runExampleTest(func(t testing.TB) { _, err := os.Open("/non-existent-file") qt.Assert(t, qt.ErrorIs(err, os.ErrNotExist)) }) // Output: PASS } func runExampleTest(f func(t testing.TB)) { defer func() { if err := recover(); err != nil && err != exampleTestFatal { panic(err) } }() var t exampleTestingT f(&t) if t.failed { fmt.Println("FAIL") } else { fmt.Println("PASS") } } type exampleTestingT struct { testing.TB failed bool } var exampleTestFatal = errors.New("example test fatal error") func (t *exampleTestingT) Helper() {} func (t *exampleTestingT) Error(args ...any) { fmt.Printf("ERROR: %s\n", fmt.Sprint(args...)) t.failed = true } func (t *exampleTestingT) Fatal(args ...any) { fmt.Printf("FATAL: %s\n", fmt.Sprint(args...)) t.failed = true panic(exampleTestFatal) } qt-1.101.0/export_test.go000066400000000000000000000002221446275767100152240ustar00rootroot00000000000000// Licensed under the MIT license, see LICENSE file for details. package qt var ( Prefixf = prefixf TestingVerbose = &testingVerbose ) qt-1.101.0/format.go000066400000000000000000000044171446275767100141460ustar00rootroot00000000000000// Licensed under the MIT license, see LICENSE file for details. package qt import ( "fmt" "reflect" "strconv" "strings" "unicode/utf8" "github.com/kr/pretty" ) // Format formats the given value as a string. It is used to print values in // test failures. func Format(v any) string { switch v := v.(type) { case error: s, ok := checkStringCall(v, v.Error) if !ok { return "e" } if msg := fmt.Sprintf("%+v", v); msg != s { // The error has formatted itself with additional information. // Leave that as is. return msg } return "e" + quoteString(s) case fmt.Stringer: s, ok := checkStringCall(v, v.String) if !ok { return "s" } return "s" + quoteString(s) case string: return quoteString(v) case uintptr, uint, uint8, uint16, uint32, uint64: // Use decimal base (rather than hexadecimal) for representing uint types. return fmt.Sprintf("%T(%d)", v, v) } if bytes, ok := byteSlice(v); ok && bytes != nil && utf8.Valid(bytes) { // It's a top level slice of bytes that's also valid UTF-8. // Ideally, this would happen at deeper levels too, // but this is sufficient for some significant cases // (json.RawMessage for example). return fmt.Sprintf("%T(%s)", v, quoteString(string(bytes))) } // The pretty.Sprint equivalent does not quote string values. return fmt.Sprintf("%# v", pretty.Formatter(v)) } func byteSlice(x any) ([]byte, bool) { v := reflect.ValueOf(x) if !v.IsValid() { return nil, false } t := v.Type() if t.Kind() == reflect.Slice && t.Elem().Kind() == reflect.Uint8 { return v.Bytes(), true } return nil, false } func quoteString(s string) string { // TODO think more about what to do about multi-line strings. if strings.Contains(s, `"`) && !strings.Contains(s, "\n") && strconv.CanBackquote(s) { return "`" + s + "`" } return strconv.Quote(s) } // checkStringCall calls f and returns its result, and reports if the call // succeeded without panicking due to a nil pointer. // If f panics and v is a nil pointer, it returns false. func checkStringCall(v any, f func() string) (s string, ok bool) { defer func() { err := recover() if err == nil { return } if val := reflect.ValueOf(v); val.Kind() == reflect.Ptr && val.IsNil() { ok = false return } panic(err) }() return f(), true } qt-1.101.0/format_test.go000066400000000000000000000056161446275767100152070ustar00rootroot00000000000000// Licensed under the MIT license, see LICENSE file for details. package qt_test import ( "bytes" "testing" "github.com/go-quicktest/qt" ) var formatTests = []struct { about string value any want string }{{ about: "error value", value: errBadWolf, want: "bad wolf\n file:line", }, { about: "error value: not formatted", value: &errTest{ msg: "exterminate!", }, want: `e"exterminate!"`, }, { about: "error value: with quotes", value: &errTest{ msg: `cannot open "/no/such/file"`, }, want: "e`cannot open \"/no/such/file\"`", }, { about: "error value: multi-line", value: &errTest{ msg: `err: "these are the voyages"`, }, want: `e"err:\n\"these are the voyages\""`, }, { about: "error value: with backquotes", value: &errTest{ msg: "cannot `open` \"file\"", }, want: `e"cannot ` + "`open`" + ` \"file\""`, }, { about: "error value: not guarding against nil", value: (*errTest)(nil), want: `e`, }, { about: "stringer", value: bytes.NewBufferString("I am a stringer"), want: `s"I am a stringer"`, }, { about: "stringer: with quotes", value: bytes.NewBufferString(`I say "hello"`), want: "s`I say \"hello\"`", }, { about: "stringer: not guarding against nil", value: (*nilStringer)(nil), want: "s", }, { about: "string", value: "these are the voyages", want: `"these are the voyages"`, }, { about: "string: with quotes", value: `here is a quote: "`, want: "`here is a quote: \"`", }, { about: "string: multi-line", value: `foo "bar" `, want: `"foo\n\"bar\"\n"`, }, { about: "string: with backquotes", value: `"` + "`", want: `"\"` + "`\"", }, { about: "slice", value: []int{1, 2, 3}, want: "[]int{1, 2, 3}", }, { about: "bytes", value: []byte("hello"), want: `[]uint8("hello")`, }, { about: "custom bytes type", value: myBytes("hello"), want: `qt_test.myBytes("hello")`, }, { about: "bytes with backquote", value: []byte(`a "b" c`), want: "[]uint8(`a \"b\" c`)", }, { about: "bytes with invalid utf-8", value: []byte("\xff"), want: "[]uint8{0xff}", }, { about: "nil byte slice", value: []byte(nil), want: "[]uint8(nil)", }, { about: "time", value: goTime, want: `s"2012-03-28 00:00:00 +0000 UTC"`, }, { about: "struct with byte slice", value: struct{ X []byte }{[]byte("x")}, want: "struct { X []uint8 }{\n X: {0x78},\n}", }, { about: "uint64", value: uint64(17), want: "uint64(17)", }, { about: "uint32", value: uint32(17898), want: "uint32(17898)", }, { about: "uintptr", value: uintptr(13), want: "uintptr(13)", }, } func TestFormat(t *testing.T) { for _, test := range formatTests { t.Run(test.about, func(t *testing.T) { got := qt.Format(test.value) if got != test.want { t.Fatalf("format:\ngot %q\nwant %q", got, test.want) } }) } } type myBytes []byte // nilStringer is a stringer not guarding against nil. type nilStringer struct { msg string } func (s *nilStringer) String() string { return s.msg } qt-1.101.0/go.mod000066400000000000000000000007701446275767100134330ustar00rootroot00000000000000module github.com/go-quicktest/qt require ( github.com/google/go-cmp v0.5.9 github.com/kr/pretty v0.3.1 ) require ( github.com/kr/text v0.2.0 // indirect github.com/rogpeppe/go-internal v1.11.0 // indirect ) retract ( v1.14.3 // Contains retractions only. v1.14.2 // Published accidentally. v1.14.1 // Published accidentally. v1.9.0 // Published accidentally. v1.7.0 // Published accidentally. v1.3.0 // Published accidentally. v0.0.3 // First retract attempt, that didn't work. ) go 1.18 qt-1.101.0/go.sum000066400000000000000000000016701446275767100134600ustar00rootroot00000000000000github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= qt-1.101.0/iter.go000066400000000000000000000025121446275767100136130ustar00rootroot00000000000000// Licensed under the MIT license, see LICENSE file for details. package qt import ( "fmt" "reflect" ) // containerIter provides an interface for iterating over a container // (map, slice or array). type containerIter[T any] interface { // next advances to the next item in the container. next() bool // key returns the current key as a string. key() string // value returns the current value. value() T } func newSliceIter[T any](slice []T) *sliceIter[T] { return &sliceIter[T]{ slice: slice, index: -1, } } // sliceIter implements containerIter for slices and arrays. type sliceIter[T any] struct { slice []T index int } func (i *sliceIter[T]) next() bool { i.index++ return i.index < len(i.slice) } func (i *sliceIter[T]) value() T { return i.slice[i.index] } func (i *sliceIter[T]) key() string { return fmt.Sprintf("index %d", i.index) } func newMapIter[K comparable, V any](m map[K]V) containerIter[V] { return mapValueIter[V]{ iter: reflect.ValueOf(m).MapRange(), } } // mapValueIter implements containerIter for maps. type mapValueIter[T any] struct { iter *reflect.MapIter v T } func (i mapValueIter[T]) next() bool { return i.iter.Next() } func (i mapValueIter[T]) key() string { return fmt.Sprintf("key %#v", i.iter.Key()) } func (i mapValueIter[T]) value() T { return valueAs[T](i.iter.Value()) } qt-1.101.0/patch.go000066400000000000000000000010761446275767100137530ustar00rootroot00000000000000// Licensed under the MIT license, see LICENSE file for details. package qt import "testing" // Patch sets a variable to a temporary value for the duration of the test. // // It sets the value pointed to by the given destination to the given // value, which must be assignable to the element type of the destination. // // At the end of the test (see "Deferred execution" in the package docs), the // destination is set back to its original value. func Patch[T any](tb testing.TB, dest *T, value T) { old := *dest *dest = value tb.Cleanup(func() { *dest = old }) } qt-1.101.0/qtsuite/000077500000000000000000000000001446275767100140175ustar00rootroot00000000000000qt-1.101.0/qtsuite/suite.go000066400000000000000000000060341446275767100155020ustar00rootroot00000000000000// Licensed under the MIT license, see LICENSE file for details. /* Package qtsuite allows quicktest to run test suites. A test suite is a value with one or more test methods. For example, the following code defines a suite of test functions that starts an HTTP server before running each test, and tears it down afterwards: type suite struct { url string } func (s *suite) Init(t *testing.T) { hnd := func(w http.ResponseWriter, req *http.Request) { fmt.Fprintf(w, "%s %s", req.Method, req.URL.Path) } srv := httptest.NewServer(http.HandlerFunc(hnd)) t.Cleanup(srv.Close) s.url = srv.URL } func (s *suite) TestGet(t *testing.T) { t.Parallel() resp, err := http.Get(s.url) qt.Assert(t, qt.IsNil(err)) defer resp.Body.Close() b, err := ioutil.ReadAll(resp.Body) qt.Assert(t, qt.IsNil(err)) qt.Assert(t, qt.Equals(string(b), "GET /")) } func (s *suite) TestHead(t *testing.T) { t.Parallel() resp, err := http.Head(s.url + "/path") qt.Assert(t, qt.IsNil(err)) defer resp.Body.Close() b, err := ioutil.ReadAll(resp.Body) qt.Assert(t, qt.IsNil(err)) qt.Assert(t, qt.Equals(string(b), "")) qt.Assert(t, qt.Equals(resp.ContentLength, 10)) } The above code could be invoked from a test function like this: func TestHTTPMethods(t *testing.T) { qtsuite.Run(t, &suite{}) } */ package qtsuite import ( "reflect" "strings" "testing" "unicode" "unicode/utf8" ) // Run runs each test method defined on the given value as a separate // subtest. A test is a method of the form // // func (T) TestXxx(*testing.T) // // where Xxx does not start with a lowercase letter. // // If suite is a pointer, the value pointed to is copied before any // methods are invoked on it: a new copy is made for each test. This // means that it is OK for tests to modify fields in suite concurrently // if desired - it's OK to call t.Parallel(). // // If suite has a method of the form // // func (T) Init(*testing.T) // // this method will be invoked before each test run. func Run(t *testing.T, suite any) { sv := reflect.ValueOf(suite) st := sv.Type() init, hasInit := st.MethodByName("Init") if hasInit && !isValidMethod(init) { t.Fatal("wrong signature for Init, must be Init(*testing.T)") } for i := 0; i < st.NumMethod(); i++ { m := st.Method(i) if !isTestMethod(m) { continue } t.Run(m.Name, func(t *testing.T) { if !isValidMethod(m) { t.Fatalf("wrong signature for %s, must be %s(*testing.T)", m.Name, m.Name) } sv := sv if st.Kind() == reflect.Ptr { sv1 := reflect.New(st.Elem()) sv1.Elem().Set(sv.Elem()) sv = sv1 } args := []reflect.Value{sv, reflect.ValueOf(t)} if hasInit { init.Func.Call(args) } m.Func.Call(args) }) } } var tType = reflect.TypeOf((*testing.T)(nil)) func isTestMethod(m reflect.Method) bool { if !strings.HasPrefix(m.Name, "Test") { return false } r, n := utf8.DecodeRuneInString(m.Name[4:]) return n == 0 || !unicode.IsLower(r) } func isValidMethod(m reflect.Method) bool { return m.Type.NumIn() == 2 && m.Type.NumOut() == 0 && m.Type.In(1) == tType } qt-1.101.0/qtsuite/suite_test.go000066400000000000000000000031301446275767100165330ustar00rootroot00000000000000// Licensed under the MIT license, see LICENSE file for details. package qtsuite_test import ( "testing" "github.com/go-quicktest/qt" "github.com/go-quicktest/qt/qtsuite" ) func TestRunSuite(t *testing.T) { var calls []call qtsuite.Run(t, testSuite{calls: &calls}) qt.Assert(t, qt.DeepEquals(calls, []call{ {"Test1", 0}, {"Test4", 0}, })) } func TestRunSuiteEmbedded(t *testing.T) { var calls []call suite := struct { testSuite }{testSuite: testSuite{calls: &calls}} qtsuite.Run(t, suite) qt.Assert(t, qt.DeepEquals(calls, []call{ {"Test1", 0}, {"Test4", 0}, })) } func TestRunSuitePtr(t *testing.T) { var calls []call qtsuite.Run(t, &testSuite{calls: &calls}) qt.Assert(t, qt.DeepEquals(calls, []call{ {"Init", 0}, {"Test1", 1}, {"Init", 0}, {"Test4", 1}, })) } type testSuite struct { init int calls *[]call } func (s testSuite) addCall(name string) { *s.calls = append(*s.calls, call{Name: name, Init: s.init}) } func (s *testSuite) Init(*testing.T) { s.addCall("Init") s.init++ } func (s testSuite) Test1(*testing.T) { s.addCall("Test1") } func (s testSuite) Test4(*testing.T) { s.addCall("Test4") } func (s testSuite) Testa(*testing.T) { s.addCall("Testa") } type call struct { Name string Init int } // It's not clear how to test this. // //func TestInvalidInit(t *testing.T) { // c := qt.New(t) // tt := &testingT{} // tc := qt.New(tt) // qtsuite.Run(tc, invalidTestSuite{}) // qt.Assert(t, qt.Equals(tt.fatalString(), "wrong signature for Init, must be Init(*testing.T)")) //} // //type invalidTestSuite struct{} // //func (invalidTestSuite) Init() {} //} qt-1.101.0/quicktest.go000066400000000000000000000026201446275767100146640ustar00rootroot00000000000000// Package qt implements assertions and other helpers wrapped around the // standard library's testing types. package qt import ( "testing" ) // Assert checks that the provided argument passes the given check and calls // tb.Fatal otherwise, including any Comment arguments in the failure. func Assert(t testing.TB, checker Checker, comments ...Comment) bool { return check(t, checkParams{ fail: t.Fatal, checker: checker, comments: comments, }) } // Check checks that the provided argument passes the given check and calls // tb.Error otherwise, including any Comment arguments in the failure. func Check(t testing.TB, checker Checker, comments ...Comment) bool { return check(t, checkParams{ fail: t.Error, checker: checker, comments: comments, }) } func check(t testing.TB, p checkParams) bool { t.Helper() rp := reportParams{ comments: p.comments, } // Allow checkers to annotate messages. note := func(key string, value any) { rp.notes = append(rp.notes, note{ key: key, value: value, }) } // Ensure that we have a checker. if p.checker == nil { p.fail(report(BadCheckf("nil checker provided"), rp)) return false } rp.args = p.checker.Args() // Run the check. if err := p.checker.Check(note); err != nil { p.fail(report(err, rp)) return false } return true } type checkParams struct { fail func(...any) checker Checker comments []Comment } qt-1.101.0/quicktest_test.go000066400000000000000000000166171446275767100157360ustar00rootroot00000000000000// Licensed under the MIT license, see LICENCE file for details. package qt_test import ( "bytes" "errors" "fmt" "strings" "testing" "github.com/go-quicktest/qt" ) var qtTests = []struct { about string checker qt.Checker comments []qt.Comment expectedFailure string }{{ about: "success", checker: qt.Equals(42, 42), }, { about: "failure", checker: qt.Equals("42", "47"), expectedFailure: ` error: values are not equal got: "42" want: "47" `, }, { about: "failure with % signs", checker: qt.Equals("42%x", "47%y"), expectedFailure: ` error: values are not equal got: "42%x" want: "47%y" `, }, { about: "failure with comment", checker: qt.Equals(true, false), comments: []qt.Comment{qt.Commentf("apparently %v != %v", true, false)}, expectedFailure: ` error: values are not equal comment: apparently true != false got: bool(true) want: bool(false) `, }, { about: "another failure with comment", checker: qt.IsNil(any(42)), comments: []qt.Comment{qt.Commentf("bad wolf: %d", 42)}, expectedFailure: ` error: got non-nil value comment: bad wolf: 42 got: int(42) `, }, { about: "failure with constant comment", checker: qt.IsNil(any("something")), comments: []qt.Comment{qt.Commentf("these are the voyages")}, expectedFailure: ` error: got non-nil value comment: these are the voyages got: "something" `, }, { about: "failure with empty comment", checker: qt.IsNil(any(47)), comments: []qt.Comment{qt.Commentf("")}, expectedFailure: ` error: got non-nil value got: int(47) `, }, { about: "failure with multiple comments", checker: qt.IsNil(any(42)), comments: []qt.Comment{ qt.Commentf("bad wolf: %d", 42), qt.Commentf("second comment"), }, expectedFailure: ` error: got non-nil value comment: bad wolf: 42 comment: second comment got: int(42) `, }, { about: "nil checker", expectedFailure: ` error: bad check: nil checker provided `, }, { about: "many arguments and notes", checker: &testingChecker{ args: []qt.Arg{{ Name: "arg1", Value: 42, }, { Name: "arg2", Value: "val2", }, { Name: "arg3", Value: "val3", }}, addNotes: func(note func(key string, value any)) { note("note1", "these") note("note2", qt.Unquoted("are")) note("note3", "the") note("note4", "voyages") note("note5", true) }, err: errors.New("bad wolf"), }, expectedFailure: ` error: bad wolf note1: "these" note2: are note3: "the" note4: "voyages" note5: bool(true) arg1: int(42) arg2: "val2" arg3: "val3" `, }, { about: "many arguments and notes with the same value", checker: &testingChecker{ args: []qt.Arg{{ Name: "arg1", Value: "value1", }, { Name: "arg2", Value: "value1", }, { Name: "arg3", Value: []int{42}, }, { Name: "arg4", Value: nil, }}, addNotes: func(note func(key string, value any)) { note("note1", "value1") note("note2", []int{42}) note("note3", "value1") note("note4", nil) }, err: errors.New("bad wolf"), }, expectedFailure: ` error: bad wolf note1: "value1" note2: []int{42} note3: note4: nil arg1: arg2: arg3: arg4: `, }, { about: "bad check with notes", checker: &testingChecker{ args: []qt.Arg{{ Name: "got", Value: 42, }, { Name: "want", Value: "want", }}, addNotes: func(note func(key string, value any)) { note("note", 42) }, err: qt.BadCheckf("bad wolf"), }, expectedFailure: ` error: bad check: bad wolf note: int(42) `, }, { about: "silent failure with notes", checker: &testingChecker{ args: []qt.Arg{{ Name: "got", Value: 42, }, { Name: "want", Value: "want", }}, addNotes: func(note func(key string, value any)) { note("note1", "first note") note("note2", qt.Unquoted("second note")) }, err: qt.ErrSilent, }, expectedFailure: ` note1: "first note" note2: second note `, }} func TestCAssertCheck(t *testing.T) { for _, test := range qtTests { t.Run("Assert: "+test.about, func(t *testing.T) { tt := &testingT{} ok := qt.Assert(tt, test.checker, test.comments...) checkResult(t, ok, tt.fatalString(), test.expectedFailure) if tt.errorString() != "" { t.Fatalf("no error messages expected, but got %q", tt.errorString()) } }) t.Run("Check: "+test.about, func(t *testing.T) { tt := &testingT{} ok := qt.Check(tt, test.checker, test.comments...) checkResult(t, ok, tt.errorString(), test.expectedFailure) if tt.fatalString() != "" { t.Fatalf("no fatal messages expected, but got %q", tt.fatalString()) } }) } } func checkResult(t *testing.T, ok bool, got, want string) { t.Helper() if want != "" { assertPrefix(t, got, want+"stack:\n") assertBool(t, ok, false) return } if got != "" { t.Fatalf("output:\ngot %q\nwant empty", got) } assertBool(t, ok, true) } // testingT implements testing.TB for testing purposes. type testingT struct { testing.TB errorBuf bytes.Buffer fatalBuf bytes.Buffer subTestResult bool subTestName string subTestT *testing.T helperCalls int parallel bool } // Error overrides testing.TB.Error so that messages are collected. func (t *testingT) Error(a ...any) { fmt.Fprint(&t.errorBuf, a...) } // Fatal overrides testing.TB.Fatal so that messages are collected and the // goroutine is not killed. func (t *testingT) Fatal(a ...any) { fmt.Fprint(&t.fatalBuf, a...) } // Parallel overrides testing.TB.Parallel in order to record the call. func (t *testingT) Parallel() { t.parallel = true } // Helper overrides testing.TB.Helper in order to count calls. func (t *testingT) Helper() { t.helperCalls += 1 } // Fatal overrides *testing.T.Fatal so that messages are collected and the // goroutine is not killed. func (t *testingT) Run(name string, f func(t *testing.T)) bool { t.subTestName, t.subTestT = name, &testing.T{} f(t.subTestT) return t.subTestResult } // errorString returns the error message. func (t *testingT) errorString() string { return t.errorBuf.String() } // fatalString returns the fatal error message. func (t *testingT) fatalString() string { return t.fatalBuf.String() } // assertPrefix fails if the got value does not have the given prefix. func assertPrefix(t testing.TB, got, prefix string) { t.Helper() if prefix == "" { t.Fatal("prefix: empty value provided") } if !strings.HasPrefix(got, prefix) { t.Fatalf(`prefix: got %q want %q -------------------- got -------------------- %s -------------------- want ------------------- %s ---------------------------------------------`, got, prefix, got, prefix) } } // assertBool fails if the given boolean values don't match. func assertBool(t testing.TB, got, want bool) { t.Helper() if got != want { t.Fatalf("bool:\ngot %v\nwant %v", got, want) } } // testingChecker is a quicktest.Checker used in tests. It receives the // provided argNames, adds notes via the provided addNotes function, and when // the check is run the provided error is returned. type testingChecker struct { args []qt.Arg addNotes func(note func(key string, value any)) err error } // Check implements quicktest.Checker by returning the stored error. func (c *testingChecker) Check(note func(key string, value any)) error { if c.addNotes != nil { c.addNotes(note) } return c.err } // Args implements quicktest.Checker by returning the stored args. func (c *testingChecker) Args() []qt.Arg { return c.args } qt-1.101.0/report.go000066400000000000000000000151051446275767100141650ustar00rootroot00000000000000// Licensed under the MIT license, see LICENSE file for details. package qt import ( "bytes" "fmt" "go/ast" "go/parser" "go/printer" "go/token" "io" "reflect" "runtime" "strings" "testing" ) // reportParams holds parameters for reporting a test error. type reportParams struct { // paramNames holds the names for the parameters of the checker // (including got as the first name). paramNames []string // args holds all the arguments passed to the checker. args []Arg // comments optionally holds comments passed when performing the check. comments []Comment // notes holds notes added while doing the check. notes []note } // Unquoted indicates that the string must not be pretty printed in the failure // output. This is useful when a checker calls note and does not want the // provided value to be quoted. type Unquoted string // SuppressedIfLong indicates that the value must be suppressed if verbose // testing is off and the pretty printed version of the value is long. This is // useful when a checker calls note and does not want the provided value to be // printed in non-verbose test runs if the value is too long. type SuppressedIfLong struct { // Value holds the original annotated value. Value any } // longValueLines holds the number of lines after which a value is long. const longValueLines = 10 // report generates a failure report for the given error, optionally including // in the output the checker arguments, comment and notes included in the // provided report parameters. func report(err error, p reportParams) string { var buf bytes.Buffer buf.WriteByte('\n') writeError(&buf, err, p) writeStack(&buf) return buf.String() } // writeError writes a pretty formatted output of the given error using the // provided report parameters. func writeError(w io.Writer, err error, p reportParams) { ptrs := make(map[string]any) values := make(map[string]string) printPair := func(key string, value any) { fmt.Fprintln(w, key+":") var v string if u, ok := value.(Unquoted); ok { // Output the raw string without quotes. v = string(u) } else if s, ok := value.(SuppressedIfLong); ok { // Check whether the output is too long and must be suppressed. v = Format(s.Value) if !testingVerbose() { if n := strings.Count(v, "\n"); n > longValueLines { fmt.Fprint(w, prefixf(prefix, "", n)) return } } } else { // Check whether the output has been already seen. v = Format(value) isPtr := reflect.ValueOf(value).Kind() == reflect.Pointer if k := values[v]; k != "" { if previousValue, ok := ptrs[k]; ok && isPtr && previousValue != value { fmt.Fprint(w, prefixf(prefix, "", k)) return } fmt.Fprint(w, prefixf(prefix, "", k)) return } if isPtr { ptrs[key] = value } } values[v] = key fmt.Fprint(w, prefixf(prefix, "%s", v)) } // Write the checker error. if err != ErrSilent { printPair("error", Unquoted(err.Error())) } // Write comments if provided. for _, c := range p.comments { if comment := c.String(); comment != "" { printPair("comment", Unquoted(comment)) } } // Write notes if present. for _, n := range p.notes { printPair(n.key, n.value) } if IsBadCheck(err) || err == ErrSilent { // For errors in the checker invocation or for silent errors, do not // show output from args. return } // Write provided args. for _, arg := range p.args { printPair(arg.Name, arg.Value) } } // testingVerbose is defined as a variable for testing. var testingVerbose = func() bool { return testing.Verbose() } // writeStack writes the traceback information for the current failure into the // provided writer. func writeStack(w io.Writer) { fmt.Fprintln(w, "stack:") pc := make([]uintptr, 8) sg := &stmtGetter{ fset: token.NewFileSet(), files: make(map[string]*ast.File, 8), config: &printer.Config{ Mode: printer.UseSpaces, Tabwidth: 4, }, } runtime.Callers(5, pc) frames := runtime.CallersFrames(pc) thisPackage := reflect.TypeOf(Unquoted("")).PkgPath() + "." for { frame, more := frames.Next() if strings.HasPrefix(frame.Function, "testing.") { // Stop before getting back to stdlib test runner calls. break } if fname := strings.TrimPrefix(frame.Function, thisPackage); fname != frame.Function { if ast.IsExported(fname) { // Continue without printing frames for quicktest exported API. continue } // Stop when entering quicktest internal calls. // This is useful for instance when using qtsuite. break } fmt.Fprint(w, prefixf(prefix, "%s:%d", frame.File, frame.Line)) if strings.HasSuffix(frame.File, ".go") { stmt, err := sg.Get(frame.File, frame.Line) if err != nil { fmt.Fprint(w, prefixf(prefix+prefix, "<%s>", err)) } else { fmt.Fprint(w, prefixf(prefix+prefix, "%s", stmt)) } } if !more { // There are no more callers. break } } } type stmtGetter struct { fset *token.FileSet files map[string]*ast.File config *printer.Config } // Get returns the lines of code of the statement at the given file and line. func (sg *stmtGetter) Get(file string, line int) (string, error) { f := sg.files[file] if f == nil { var err error f, err = parser.ParseFile(sg.fset, file, nil, parser.ParseComments) if err != nil { return "", fmt.Errorf("cannot parse source file: %s", err) } sg.files[file] = f } var stmt string ast.Inspect(f, func(n ast.Node) bool { if n == nil || stmt != "" { return false } pos := sg.fset.Position(n.Pos()).Line end := sg.fset.Position(n.End()).Line // Go < v1.9 reports the line where the statements ends, not the line // where it begins. if line == pos || line == end { var buf bytes.Buffer // TODO: include possible comment after the statement. sg.config.Fprint(&buf, sg.fset, &printer.CommentedNode{ Node: n, Comments: f.Comments, }) stmt = buf.String() return false } return pos < line && line <= end }) return stmt, nil } // prefixf formats the given string with the given args. It also inserts the // final newline if needed and indentation with the given prefix. func prefixf(prefix, format string, args ...any) string { var buf []byte s := strings.TrimSuffix(fmt.Sprintf(format, args...), "\n") for _, line := range strings.Split(s, "\n") { buf = append(buf, prefix...) buf = append(buf, line...) buf = append(buf, '\n') } return string(buf) } // note holds a key/value annotation. type note struct { key string value any } // prefix is the string used to indent blocks of output. const prefix = " " qt-1.101.0/report_test.go000066400000000000000000000066421446275767100152320ustar00rootroot00000000000000// Licensed under the MIT license, see LICENSE file for details. package qt_test import ( "runtime" "strings" "testing" "github.com/go-quicktest/qt" ) // The tests in this file rely on their own source code lines. func TestReportOutput(t *testing.T) { tt := &testingT{} qt.Assert(tt, qt.Equals(42, 47)) want := ` error: values are not equal got: int(42) want: int(47) stack: $file:17 qt.Assert(tt, qt.Equals(42, 47)) ` assertReport(t, tt, want) } func f1(t testing.TB) { f2(t) } func f2(t testing.TB) { qt.Assert(t, qt.IsNil([]int{})) // Real assertion here! } func TestIndirectReportOutput(t *testing.T) { tt := &testingT{} f1(tt) want := ` error: got non-nil value got: []int{} stack: $file:37 qt.Assert(t, qt.IsNil([]int{})) $file:33 f2(t) $file:42 f1(tt) ` assertReport(t, tt, want) } func TestMultilineReportOutput(t *testing.T) { tt := &testingT{} qt.Assert(tt, qt.Equals( "this string", // Comment 1. "another string", ), qt.Commentf("a comment"), // Comment 2. ) // Comment 3. want := ` error: values are not equal comment: a comment got: "this string" want: "another string" stack: $file:61 qt.Assert(tt, qt.Equals( "this string", // Comment 1. "another string", ), qt.Commentf("a comment"), // Comment 2. ) ` assertReport(t, tt, want) } func TestCmpReportOutput(t *testing.T) { tt := &testingT{} gotExamples := []*reportExample{{ AnInt: 42, }, { AnInt: 47, }, { AnInt: 1, }, { AnInt: 2, }} wantExamples := []*reportExample{{ AnInt: 42, }, { AnInt: 47, }, { AnInt: 2, }, { AnInt: 1, }, {}} qt.Assert(tt, qt.DeepEquals(gotExamples, wantExamples)) want := ` error: values are not deep equal diff (-got +want): []*qt_test.reportExample{ &{AnInt: 42}, &{AnInt: 47}, + &{AnInt: 2}, &{AnInt: 1}, - &{AnInt: 2}, + &{}, } got: []*qt_test.reportExample{ &qt_test.reportExample{AnInt:42}, &qt_test.reportExample{AnInt:47}, &qt_test.reportExample{AnInt:1}, &qt_test.reportExample{AnInt:2}, } want: []*qt_test.reportExample{ &qt_test.reportExample{AnInt:42}, &qt_test.reportExample{AnInt:47}, &qt_test.reportExample{AnInt:2}, &qt_test.reportExample{AnInt:1}, &qt_test.reportExample{}, } stack: $file:110 qt.Assert(tt, qt.DeepEquals(gotExamples, wantExamples)) ` assertReport(t, tt, want) } func TestTopLevelAssertReportOutput(t *testing.T) { tt := &testingT{} qt.Assert(tt, qt.Equals(42, 47)) want := ` error: values are not equal got: int(42) want: int(47) stack: $file:147 qt.Assert(tt, qt.Equals(42, 47)) ` assertReport(t, tt, want) } func assertReport(t *testing.T, tt *testingT, want string) { t.Helper() got := strings.Replace(tt.fatalString(), "\t", " ", -1) // go-cmp can include non-breaking spaces in its output. got = strings.Replace(got, "\u00a0", " ", -1) // Adjust for file names in different systems. _, file, _, ok := runtime.Caller(0) assertBool(t, ok, true) want = strings.Replace(want, "$file", file, -1) if got != want { t.Fatalf(`failure: %q %q ------------------------------ got ------------------------------ %s------------------------------ want ----------------------------- %s-----------------------------------------------------------------`, got, want, got, want) } } type reportExample struct { AnInt int }