pax_global_header00006660000000000000000000000064145431331160014513gustar00rootroot0000000000000052 comment=808914b41fd7a74b97d3f3254cc46b669489a2c0 golang-github-maxatome-go-testdeep-1.14.0/000077500000000000000000000000001454313311600203545ustar00rootroot00000000000000golang-github-maxatome-go-testdeep-1.14.0/.github/000077500000000000000000000000001454313311600217145ustar00rootroot00000000000000golang-github-maxatome-go-testdeep-1.14.0/.github/workflows/000077500000000000000000000000001454313311600237515ustar00rootroot00000000000000golang-github-maxatome-go-testdeep-1.14.0/.github/workflows/ci.yml000066400000000000000000000076521454313311600251010ustar00rootroot00000000000000name: Build on: push: branches: [ master ] pull_request: branches: [ master ] workflow_dispatch: jobs: test: strategy: matrix: go-version: [1.16.x, 1.17.x, 1.18.x, 1.19.x, 1.20.x, tip] full-tests: [false] include: - go-version: 1.21.x full-tests: true runs-on: ubuntu-latest steps: - name: Setup go run: | curl -sL https://raw.githubusercontent.com/maxatome/install-go/v3.5/install-go.pl | perl - ${{ matrix.go-version }} $HOME/go - name: Checkout code uses: actions/checkout@v2 - name: Miscellaneous checks if: matrix.full-tests run: | ./tools/gen_funcs.pl git diff --exit-code if fgrep 'interface{}' $(find . -name '*.go' -not -name any.go -not -name any_test.go); then echo '*** At least one interface{} occurrence found. Use any instead.' false fi - name: Linting if: matrix.full-tests run: | curl -sL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $HOME/go/bin v1.54.1 $HOME/go/bin/golangci-lint run --max-issues-per-linter 0 \ --max-same-issues 0 \ --exclude="unused-parameter: parameter '[^']+' seems to be unused, consider removing or renaming it as _" \ -E asciicheck \ -E bidichk \ -E durationcheck \ -E exportloopref \ -E gocritic \ -E godot \ -E goimports \ -E misspell \ -E prealloc \ -E revive \ -E unconvert \ -E whitespace \ ./... - name: Testing continue-on-error: ${{ matrix.go-version == 'tip' }} run: | go version go env if [ -z "$(go env GOPROXY)" ]; then echo "Fix empty GOPROXY" export GOPROXY=https://proxy.golang.org,direct fi if [ ${{ matrix.full-tests }} = true ]; then cover_flags="-covermode=atomic -coverprofile=coverage" GO_TEST_FLAGS="$cover_flags.out" GO_TEST_SAFE_FLAGS="$cover_flags-safe.out" GO_TEST_RACE_FLAGS="$cover_flags-race.out" GO_TEST_RACE_SAFE_FLAGS="$cover_flags-race-safe.out" fi export GORACE="halt_on_error=1" echo "CLASSIC ===========================================" go test $GO_TEST_FLAGS ./... echo "SAFE ==============================================" go test -tags safe $GO_TEST_SAFE_FLAGS ./... echo "RACE ==============================================" go test -race $GO_TEST_RACE_FLAGS ./... echo "RACE + SAFE =======================================" go test -race -tags safe $GO_TEST_RACE_SAFE_FLAGS ./... - name: Reporting if: matrix.full-tests env: COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | go install github.com/mattn/goveralls@v0.0.11 go install github.com/wadey/gocovmerge@latest gocovmerge coverage.out \ coverage-safe.out \ coverage-race.out \ coverage-race-safe.out | egrep -v '^github\.com/maxatome/go-testdeep/internal/json/parser\.go:' > coverage.out goveralls -coverprofile=coverage.out -service=github golang-github-maxatome-go-testdeep-1.14.0/.golangci.yml000066400000000000000000000000221454313311600227320ustar00rootroot00000000000000run: go: '1.16' golang-github-maxatome-go-testdeep-1.14.0/LICENSE000066400000000000000000000024461454313311600213670ustar00rootroot00000000000000BSD 2-Clause License Copyright (c) 2018, Maxime Soulé All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. golang-github-maxatome-go-testdeep-1.14.0/README.md000066400000000000000000000577001454313311600216440ustar00rootroot00000000000000go-testdeep =========== [![Build Status](https://github.com/maxatome/go-testdeep/workflows/Build/badge.svg?branch=master)](https://github.com/maxatome/go-testdeep/actions?query=workflow%3ABuild) [![Coverage Status](https://coveralls.io/repos/github/maxatome/go-testdeep/badge.svg?branch=master)](https://coveralls.io/github/maxatome/go-testdeep?branch=master) [![Go Report Card](https://goreportcard.com/badge/github.com/maxatome/go-testdeep)](https://goreportcard.com/report/github.com/maxatome/go-testdeep) [![GoDoc](https://pkg.go.dev/badge/github.com/maxatome/go-testdeep)](https://pkg.go.dev/github.com/maxatome/go-testdeep/td) [![Version](https://img.shields.io/github/tag/maxatome/go-testdeep.svg)](https://github.com/maxatome/go-testdeep/releases) [![Mentioned in Awesome Go](https://awesome.re/mentioned-badge.svg)](https://github.com/avelino/awesome-go/#testing) ![go-testdeep](https://github.com/maxatome/go-testdeep-site/raw/master/docs_src/static/images/logo.png) **Extremely flexible golang deep comparison, extends the go testing package.** Currently supports go 1.16 → 1.21. - [Latest news](#latest-news) - [Synopsis](#synopsis) - [Description](#description) - [Installation](#installation) - [Functions](https://go-testdeep.zetta.rocks/functions/) - [Available operators](https://go-testdeep.zetta.rocks/operators/) - [Helpers](#helpers) - [`tdhttp` or HTTP API testing helper](https://pkg.go.dev/github.com/maxatome/go-testdeep/helpers/tdhttp) - [`tdsuite` or testing suite helper](https://pkg.go.dev/github.com/maxatome/go-testdeep/helpers/tdsuite) - [`tdutil` aka the helper of helpers](https://pkg.go.dev/github.com/maxatome/go-testdeep/helpers/tdutil) - [See also](#see-also) - [License](#license) - [FAQ](https://go-testdeep.zetta.rocks/faq/) ## Latest news - 2023/03/18: [v1.13.0 release](https://github.com/maxatome/go-testdeep/releases/tag/v1.13.0); - 2022/08/07: [v1.12.0 release](https://github.com/maxatome/go-testdeep/releases/tag/v1.12.0); - 2022/01/05: [v1.11.0 release](https://github.com/maxatome/go-testdeep/releases/tag/v1.11.0); - see [commits history](https://github.com/maxatome/go-testdeep/commits/master) for other/older changes. ## Synopsis Make golang tests easy, from simplest usage: ```go import ( "testing" "github.com/maxatome/go-testdeep/td" ) func TestMyFunc(t *testing.T) { td.Cmp(t, MyFunc(), &Info{Name: "Alice", Age: 42}) } ``` To a bit more complex one, allowing flexible comparisons using [TestDeep operators](https://go-testdeep.zetta.rocks/operators/): ```go import ( "testing" "github.com/maxatome/go-testdeep/td" ) func TestMyFunc(t *testing.T) { td.Cmp(t, MyFunc(), td.Struct( &Info{Name: "Alice"}, td.StructFields{ "Age": td.Between(40, 45), }, )) } ``` Or anchoring operators directly in literals, as in: ```go import ( "testing" "github.com/maxatome/go-testdeep/td" ) func TestMyFunc(tt *testing.T) { t := td.NewT(tt) t.Cmp(MyFunc(), &Info{ Name: "Alice", Age: t.Anchor(td.Between(40, 45)).(int), }) } ``` To most complex one, allowing to easily test HTTP API routes, using flexible [operators](https://go-testdeep.zetta.rocks/operators/): ```go import ( "testing" "time" "github.com/maxatome/go-testdeep/helpers/tdhttp" "github.com/maxatome/go-testdeep/td" ) type Person struct { ID uint64 `json:"id"` Name string `json:"name"` Age int `json:"age"` CreatedAt time.Time `json:"created_at"` } func TestMyApi(t *testing.T) { var id uint64 var createdAt time.Time testAPI := tdhttp.NewTestAPI(t, myAPI) // ← ① testAPI.PostJSON("/person", Person{Name: "Bob", Age: 42}). // ← ② Name("Create a new Person"). CmpStatus(http.StatusCreated). // ← ③ CmpJSONBody(td.JSON(` // Note that comments are allowed { "id": $id, // set by the API/DB "name": "Alice", "age": Between(40, 45), // ← ④ "created_at": "$createdAt", // set by the API/DB }`, td.Tag("id", td.Catch(&id, td.NotZero())), // ← ⑤ td.Tag("createdAt", td.All( // ← ⑥ td.HasSuffix("Z"), // ← ⑦ td.Smuggle(func(s string) (time.Time, error) { // ← ⑧ return time.Parse(time.RFC3339Nano, s) }, td.Catch(&createdAt, td.Between(testAPI.SentAt(), time.Now()))), // ← ⑨ )), )) if !testAPI.Failed() { t.Logf("The new Person ID is %d and was created at %s", id, createdAt) } } ``` 1. the API handler ready to be tested; 2. the POST request with automatic JSON marshalling; 3. the expected response HTTP status should be `http.StatusCreated` and the line just below, the body should match the [`JSON`] operator; 4. some operators can be embedded, like [`Between`] here; 5. for the `$id` placeholder, [`Catch`] its value: put it in `id` variable and check it is [`NotZero`]; 6. for the `$createdAt` placeholder, use the [`All`] operator. It combines several operators like a AND; 7. check that `$createdAt` date ends with "Z" using [`HasSuffix`]. As we expect a RFC3339 date, we require it in UTC time zone; 8. convert `$createdAt` date into a `time.Time` using a custom function thanks to the [`Smuggle`] operator; 9. then [`Catch`] the resulting value: put it in `createdAt` variable and check it is greater or equal than `testAPI.SentAt()` (the time just before the request is handled) and lesser or equal than `time.Now()`. See [`tdhttp`] helper or the [FAQ](https://go-testdeep.zetta.rocks/faq/#what-about-testing-the-response-using-my-api) for details about HTTP API testing. Example of produced error in case of mismatch: ![error output](https://github.com/maxatome/go-testdeep-site/raw/master/docs_src/static/images/colored-output.svg) ## Description go-testdeep is historically a go rewrite and adaptation of wonderful [Test::Deep perl](https://metacpan.org/pod/Test::Deep). In golang, comparing data structure is usually done using [reflect.DeepEqual](https://golang.org/pkg/reflect/#DeepEqual) or using a package that uses this function behind the scene. This function works very well, but it is not flexible. Both compared structures must match exactly and when a difference is returned, it is up to the caller to display it. Not easy when comparing big data structures. The purpose of go-testdeep, via the [`td` package](https://pkg.go.dev/github.com/maxatome/go-testdeep/td) and its [helpers](https://pkg.go.dev/github.com/maxatome/go-testdeep/helpers), is to do its best to introduce this missing flexibility using ["operators"](https://go-testdeep.zetta.rocks/operators/), when the expected value (or one of its component) cannot be matched exactly, mixed with some useful [comparison functions](https://go-testdeep.zetta.rocks/functions/). **See [go-testdeep.zetta.rocks](https://go-testdeep.zetta.rocks/) for details.** ## Installation ```sh $ go get github.com/maxatome/go-testdeep ``` ## Helpers The goal of helpers is to make use of `go-testdeep` even more powerful by providing common features using [TestDeep operators](https://go-testdeep.zetta.rocks/operators/) behind the scene. ### `tdhttp` or HTTP API testing helper The package `github.com/maxatome/go-testdeep/helpers/tdhttp` provides some functions to easily test HTTP handlers. See [`tdhttp`] documentation for details or [FAQ](https://go-testdeep.zetta.rocks/faq/#what-about-testing-the-response-using-my-api) for an example of use. ### `tdsuite` or testing suite helper The package `github.com/maxatome/go-testdeep/helpers/tdsuite` adds tests suite feature to go-testdeep in a non-intrusive way, but easily and powerfully. A tests suite is a set of tests run sequentially that share some data. Some hooks can be set to be automatically called before the suite is run, before, after and/or between each test, and at the end of the suite. See [`tdsuite`] documentation for details. ### `tdutil` aka the helper of helpers The package `github.com/maxatome/go-testdeep/helpers/tdutil` allows to write unit tests for go-testdeep helpers and so provides some helpful functions. See [`tdutil`](https://pkg.go.dev/github.com/maxatome/go-testdeep/helpers/tdutil) for details. ## See also - [testify](https://github.com/stretchr/testify): a toolkit with common assertions and mocks that plays nicely with the standard library - [go-cmp](https://github.com/google/go-cmp): package for comparing Go values in tests ## License `go-testdeep` is released under the BSD-style license found in the [`LICENSE`](LICENSE) file in the root directory of this source tree. Internal function `deepValueEqual` is based on `deepValueEqual` from [`reflect` golang package](https://golang.org/pkg/reflect/) licensed under the BSD-style license found in the [`LICENSE` file in the golang repository](https://github.com/golang/go/blob/master/LICENSE). Uses two files (`bypass.go` & `bypasssafe.go`) from [Go-spew](https://github.com/davecgh/go-spew) which is licensed under the [copyfree](http://copyfree.org) ISC License. [Public Domain Gopher](https://github.com/egonelbre/gophers) provided by [Egon Elbre](http://egonelbre.com/). The Go gopher was designed by [Renee French](https://reneefrench.blogspot.com/). ## FAQ See [FAQ](https://go-testdeep.zetta.rocks/faq/). [`T`]: https://pkg.go.dev/github.com/maxatome/go-testdeep/td#T [`TestDeep`]: https://pkg.go.dev/github.com/maxatome/go-testdeep/td#TestDeep [`Cmp`]: https://pkg.go.dev/github.com/maxatome/go-testdeep/td#Cmp [`tdhttp`]: https://pkg.go.dev/github.com/maxatome/go-testdeep/helpers/tdhttp [`tdsuite`]: https://pkg.go.dev/github.com/maxatome/go-testdeep/helpers/tdsuite [`tdutil`]: https://pkg.go.dev/github.com/maxatome/go-testdeep/helpers/tdutil [`BeLax` config flag]: https://pkg.go.dev/github.com/maxatome/go-testdeep/td#ContextConfig.BeLax [`error`]: https://pkg.go.dev/builtin#error [`fmt.Stringer`]: https://pkg.go.dev/fmt/#Stringer [`time.Time`]: https://pkg.go.dev/time/#Time [`math.NaN`]: https://pkg.go.dev/math/#NaN [`All`]: https://go-testdeep.zetta.rocks/operators/all/ [`Any`]: https://go-testdeep.zetta.rocks/operators/any/ [`Array`]: https://go-testdeep.zetta.rocks/operators/array/ [`ArrayEach`]: https://go-testdeep.zetta.rocks/operators/arrayeach/ [`Bag`]: https://go-testdeep.zetta.rocks/operators/bag/ [`Between`]: https://go-testdeep.zetta.rocks/operators/between/ [`Cap`]: https://go-testdeep.zetta.rocks/operators/cap/ [`Catch`]: https://go-testdeep.zetta.rocks/operators/catch/ [`Code`]: https://go-testdeep.zetta.rocks/operators/code/ [`Contains`]: https://go-testdeep.zetta.rocks/operators/contains/ [`ContainsKey`]: https://go-testdeep.zetta.rocks/operators/containskey/ [`Delay`]: https://go-testdeep.zetta.rocks/operators/delay/ [`Empty`]: https://go-testdeep.zetta.rocks/operators/empty/ [`ErrorIs`]: https://go-testdeep.zetta.rocks/operators/erroris/ [`First`]: https://go-testdeep.zetta.rocks/operators/first/ [`Grep`]: https://go-testdeep.zetta.rocks/operators/grep/ [`Gt`]: https://go-testdeep.zetta.rocks/operators/gt/ [`Gte`]: https://go-testdeep.zetta.rocks/operators/gte/ [`HasPrefix`]: https://go-testdeep.zetta.rocks/operators/hasprefix/ [`HasSuffix`]: https://go-testdeep.zetta.rocks/operators/hassuffix/ [`Ignore`]: https://go-testdeep.zetta.rocks/operators/ignore/ [`Isa`]: https://go-testdeep.zetta.rocks/operators/isa/ [`JSON`]: https://go-testdeep.zetta.rocks/operators/json/ [`JSONPointer`]: https://go-testdeep.zetta.rocks/operators/jsonpointer/ [`Keys`]: https://go-testdeep.zetta.rocks/operators/keys/ [`Last`]: https://go-testdeep.zetta.rocks/operators/last/ [`Lax`]: https://go-testdeep.zetta.rocks/operators/lax/ [`Len`]: https://go-testdeep.zetta.rocks/operators/len/ [`Lt`]: https://go-testdeep.zetta.rocks/operators/lt/ [`Lte`]: https://go-testdeep.zetta.rocks/operators/lte/ [`Map`]: https://go-testdeep.zetta.rocks/operators/map/ [`MapEach`]: https://go-testdeep.zetta.rocks/operators/mapeach/ [`N`]: https://go-testdeep.zetta.rocks/operators/n/ [`NaN`]: https://go-testdeep.zetta.rocks/operators/nan/ [`Nil`]: https://go-testdeep.zetta.rocks/operators/nil/ [`None`]: https://go-testdeep.zetta.rocks/operators/none/ [`Not`]: https://go-testdeep.zetta.rocks/operators/not/ [`NotAny`]: https://go-testdeep.zetta.rocks/operators/notany/ [`NotEmpty`]: https://go-testdeep.zetta.rocks/operators/notempty/ [`NotNaN`]: https://go-testdeep.zetta.rocks/operators/notnan/ [`NotNil`]: https://go-testdeep.zetta.rocks/operators/notnil/ [`NotZero`]: https://go-testdeep.zetta.rocks/operators/notzero/ [`PPtr`]: https://go-testdeep.zetta.rocks/operators/pptr/ [`Ptr`]: https://go-testdeep.zetta.rocks/operators/ptr/ [`Re`]: https://go-testdeep.zetta.rocks/operators/re/ [`ReAll`]: https://go-testdeep.zetta.rocks/operators/reall/ [`Recv`]: https://go-testdeep.zetta.rocks/operators/recv/ [`Set`]: https://go-testdeep.zetta.rocks/operators/set/ [`Shallow`]: https://go-testdeep.zetta.rocks/operators/shallow/ [`Slice`]: https://go-testdeep.zetta.rocks/operators/slice/ [`Smuggle`]: https://go-testdeep.zetta.rocks/operators/smuggle/ [`SStruct`]: https://go-testdeep.zetta.rocks/operators/sstruct/ [`String`]: https://go-testdeep.zetta.rocks/operators/string/ [`Struct`]: https://go-testdeep.zetta.rocks/operators/struct/ [`SubBagOf`]: https://go-testdeep.zetta.rocks/operators/subbagof/ [`SubJSONOf`]: https://go-testdeep.zetta.rocks/operators/subjsonof/ [`SubMapOf`]: https://go-testdeep.zetta.rocks/operators/submapof/ [`SubSetOf`]: https://go-testdeep.zetta.rocks/operators/subsetof/ [`SuperBagOf`]: https://go-testdeep.zetta.rocks/operators/superbagof/ [`SuperJSONOf`]: https://go-testdeep.zetta.rocks/operators/superjsonof/ [`SuperMapOf`]: https://go-testdeep.zetta.rocks/operators/supermapof/ [`SuperSetOf`]: https://go-testdeep.zetta.rocks/operators/supersetof/ [`SuperSliceOf`]: https://go-testdeep.zetta.rocks/operators/supersliceof/ [`Tag`]: https://go-testdeep.zetta.rocks/operators/tag/ [`TruncTime`]: https://go-testdeep.zetta.rocks/operators/trunctime/ [`Values`]: https://go-testdeep.zetta.rocks/operators/values/ [`Zero`]: https://go-testdeep.zetta.rocks/operators/zero/ [`CmpAll`]: https://go-testdeep.zetta.rocks/operators/all/#cmpall-shortcut [`CmpAny`]: https://go-testdeep.zetta.rocks/operators/any/#cmpany-shortcut [`CmpArray`]: https://go-testdeep.zetta.rocks/operators/array/#cmparray-shortcut [`CmpArrayEach`]: https://go-testdeep.zetta.rocks/operators/arrayeach/#cmparrayeach-shortcut [`CmpBag`]: https://go-testdeep.zetta.rocks/operators/bag/#cmpbag-shortcut [`CmpBetween`]: https://go-testdeep.zetta.rocks/operators/between/#cmpbetween-shortcut [`CmpCap`]: https://go-testdeep.zetta.rocks/operators/cap/#cmpcap-shortcut [`CmpCode`]: https://go-testdeep.zetta.rocks/operators/code/#cmpcode-shortcut [`CmpContains`]: https://go-testdeep.zetta.rocks/operators/contains/#cmpcontains-shortcut [`CmpContainsKey`]: https://go-testdeep.zetta.rocks/operators/containskey/#cmpcontainskey-shortcut [`CmpEmpty`]: https://go-testdeep.zetta.rocks/operators/empty/#cmpempty-shortcut [`CmpErrorIs`]: https://go-testdeep.zetta.rocks/operators/erroris/#cmperroris-shortcut [`CmpFirst`]: https://go-testdeep.zetta.rocks/operators/first/#cmpfirst-shortcut [`CmpGrep`]: https://go-testdeep.zetta.rocks/operators/grep/#cmpgrep-shortcut [`CmpGt`]: https://go-testdeep.zetta.rocks/operators/gt/#cmpgt-shortcut [`CmpGte`]: https://go-testdeep.zetta.rocks/operators/gte/#cmpgte-shortcut [`CmpHasPrefix`]: https://go-testdeep.zetta.rocks/operators/hasprefix/#cmphasprefix-shortcut [`CmpHasSuffix`]: https://go-testdeep.zetta.rocks/operators/hassuffix/#cmphassuffix-shortcut [`CmpIsa`]: https://go-testdeep.zetta.rocks/operators/isa/#cmpisa-shortcut [`CmpJSON`]: https://go-testdeep.zetta.rocks/operators/json/#cmpjson-shortcut [`CmpJSONPointer`]: https://go-testdeep.zetta.rocks/operators/jsonpointer/#cmpjsonpointer-shortcut [`CmpKeys`]: https://go-testdeep.zetta.rocks/operators/keys/#cmpkeys-shortcut [`CmpLast`]: https://go-testdeep.zetta.rocks/operators/last/#cmplast-shortcut [`CmpLax`]: https://go-testdeep.zetta.rocks/operators/lax/#cmplax-shortcut [`CmpLen`]: https://go-testdeep.zetta.rocks/operators/len/#cmplen-shortcut [`CmpLt`]: https://go-testdeep.zetta.rocks/operators/lt/#cmplt-shortcut [`CmpLte`]: https://go-testdeep.zetta.rocks/operators/lte/#cmplte-shortcut [`CmpMap`]: https://go-testdeep.zetta.rocks/operators/map/#cmpmap-shortcut [`CmpMapEach`]: https://go-testdeep.zetta.rocks/operators/mapeach/#cmpmapeach-shortcut [`CmpN`]: https://go-testdeep.zetta.rocks/operators/n/#cmpn-shortcut [`CmpNaN`]: https://go-testdeep.zetta.rocks/operators/nan/#cmpnan-shortcut [`CmpNil`]: https://go-testdeep.zetta.rocks/operators/nil/#cmpnil-shortcut [`CmpNone`]: https://go-testdeep.zetta.rocks/operators/none/#cmpnone-shortcut [`CmpNot`]: https://go-testdeep.zetta.rocks/operators/not/#cmpnot-shortcut [`CmpNotAny`]: https://go-testdeep.zetta.rocks/operators/notany/#cmpnotany-shortcut [`CmpNotEmpty`]: https://go-testdeep.zetta.rocks/operators/notempty/#cmpnotempty-shortcut [`CmpNotNaN`]: https://go-testdeep.zetta.rocks/operators/notnan/#cmpnotnan-shortcut [`CmpNotNil`]: https://go-testdeep.zetta.rocks/operators/notnil/#cmpnotnil-shortcut [`CmpNotZero`]: https://go-testdeep.zetta.rocks/operators/notzero/#cmpnotzero-shortcut [`CmpPPtr`]: https://go-testdeep.zetta.rocks/operators/pptr/#cmppptr-shortcut [`CmpPtr`]: https://go-testdeep.zetta.rocks/operators/ptr/#cmpptr-shortcut [`CmpRe`]: https://go-testdeep.zetta.rocks/operators/re/#cmpre-shortcut [`CmpReAll`]: https://go-testdeep.zetta.rocks/operators/reall/#cmpreall-shortcut [`CmpRecv`]: https://go-testdeep.zetta.rocks/operators/recv/#cmprecv-shortcut [`CmpSet`]: https://go-testdeep.zetta.rocks/operators/set/#cmpset-shortcut [`CmpShallow`]: https://go-testdeep.zetta.rocks/operators/shallow/#cmpshallow-shortcut [`CmpSlice`]: https://go-testdeep.zetta.rocks/operators/slice/#cmpslice-shortcut [`CmpSmuggle`]: https://go-testdeep.zetta.rocks/operators/smuggle/#cmpsmuggle-shortcut [`CmpSStruct`]: https://go-testdeep.zetta.rocks/operators/sstruct/#cmpsstruct-shortcut [`CmpString`]: https://go-testdeep.zetta.rocks/operators/string/#cmpstring-shortcut [`CmpStruct`]: https://go-testdeep.zetta.rocks/operators/struct/#cmpstruct-shortcut [`CmpSubBagOf`]: https://go-testdeep.zetta.rocks/operators/subbagof/#cmpsubbagof-shortcut [`CmpSubJSONOf`]: https://go-testdeep.zetta.rocks/operators/subjsonof/#cmpsubjsonof-shortcut [`CmpSubMapOf`]: https://go-testdeep.zetta.rocks/operators/submapof/#cmpsubmapof-shortcut [`CmpSubSetOf`]: https://go-testdeep.zetta.rocks/operators/subsetof/#cmpsubsetof-shortcut [`CmpSuperBagOf`]: https://go-testdeep.zetta.rocks/operators/superbagof/#cmpsuperbagof-shortcut [`CmpSuperJSONOf`]: https://go-testdeep.zetta.rocks/operators/superjsonof/#cmpsuperjsonof-shortcut [`CmpSuperMapOf`]: https://go-testdeep.zetta.rocks/operators/supermapof/#cmpsupermapof-shortcut [`CmpSuperSetOf`]: https://go-testdeep.zetta.rocks/operators/supersetof/#cmpsupersetof-shortcut [`CmpSuperSliceOf`]: https://go-testdeep.zetta.rocks/operators/supersliceof/#cmpsupersliceof-shortcut [`CmpTruncTime`]: https://go-testdeep.zetta.rocks/operators/trunctime/#cmptrunctime-shortcut [`CmpValues`]: https://go-testdeep.zetta.rocks/operators/values/#cmpvalues-shortcut [`CmpZero`]: https://go-testdeep.zetta.rocks/operators/zero/#cmpzero-shortcut [`T.All`]: https://go-testdeep.zetta.rocks/operators/all/#tall-shortcut [`T.Any`]: https://go-testdeep.zetta.rocks/operators/any/#tany-shortcut [`T.Array`]: https://go-testdeep.zetta.rocks/operators/array/#tarray-shortcut [`T.ArrayEach`]: https://go-testdeep.zetta.rocks/operators/arrayeach/#tarrayeach-shortcut [`T.Bag`]: https://go-testdeep.zetta.rocks/operators/bag/#tbag-shortcut [`T.Between`]: https://go-testdeep.zetta.rocks/operators/between/#tbetween-shortcut [`T.Cap`]: https://go-testdeep.zetta.rocks/operators/cap/#tcap-shortcut [`T.Code`]: https://go-testdeep.zetta.rocks/operators/code/#tcode-shortcut [`T.Contains`]: https://go-testdeep.zetta.rocks/operators/contains/#tcontains-shortcut [`T.ContainsKey`]: https://go-testdeep.zetta.rocks/operators/containskey/#tcontainskey-shortcut [`T.Empty`]: https://go-testdeep.zetta.rocks/operators/empty/#tempty-shortcut [`T.CmpErrorIs`]: https://go-testdeep.zetta.rocks/operators/erroris/#tcmperroris-shortcut [`T.First`]: https://go-testdeep.zetta.rocks/operators/first/#tfirst-shortcut [`T.Grep`]: https://go-testdeep.zetta.rocks/operators/grep/#tgrep-shortcut [`T.Gt`]: https://go-testdeep.zetta.rocks/operators/gt/#tgt-shortcut [`T.Gte`]: https://go-testdeep.zetta.rocks/operators/gte/#tgte-shortcut [`T.HasPrefix`]: https://go-testdeep.zetta.rocks/operators/hasprefix/#thasprefix-shortcut [`T.HasSuffix`]: https://go-testdeep.zetta.rocks/operators/hassuffix/#thassuffix-shortcut [`T.Isa`]: https://go-testdeep.zetta.rocks/operators/isa/#tisa-shortcut [`T.JSON`]: https://go-testdeep.zetta.rocks/operators/json/#tjson-shortcut [`T.JSONPointer`]: https://go-testdeep.zetta.rocks/operators/jsonpointer/#tjsonpointer-shortcut [`T.Keys`]: https://go-testdeep.zetta.rocks/operators/keys/#tkeys-shortcut [`T.Last`]: https://go-testdeep.zetta.rocks/operators/last/#tlast-shortcut [`T.CmpLax`]: https://go-testdeep.zetta.rocks/operators/lax/#tcmplax-shortcut [`T.Len`]: https://go-testdeep.zetta.rocks/operators/len/#tlen-shortcut [`T.Lt`]: https://go-testdeep.zetta.rocks/operators/lt/#tlt-shortcut [`T.Lte`]: https://go-testdeep.zetta.rocks/operators/lte/#tlte-shortcut [`T.Map`]: https://go-testdeep.zetta.rocks/operators/map/#tmap-shortcut [`T.MapEach`]: https://go-testdeep.zetta.rocks/operators/mapeach/#tmapeach-shortcut [`T.N`]: https://go-testdeep.zetta.rocks/operators/n/#tn-shortcut [`T.NaN`]: https://go-testdeep.zetta.rocks/operators/nan/#tnan-shortcut [`T.Nil`]: https://go-testdeep.zetta.rocks/operators/nil/#tnil-shortcut [`T.None`]: https://go-testdeep.zetta.rocks/operators/none/#tnone-shortcut [`T.Not`]: https://go-testdeep.zetta.rocks/operators/not/#tnot-shortcut [`T.NotAny`]: https://go-testdeep.zetta.rocks/operators/notany/#tnotany-shortcut [`T.NotEmpty`]: https://go-testdeep.zetta.rocks/operators/notempty/#tnotempty-shortcut [`T.NotNaN`]: https://go-testdeep.zetta.rocks/operators/notnan/#tnotnan-shortcut [`T.NotNil`]: https://go-testdeep.zetta.rocks/operators/notnil/#tnotnil-shortcut [`T.NotZero`]: https://go-testdeep.zetta.rocks/operators/notzero/#tnotzero-shortcut [`T.PPtr`]: https://go-testdeep.zetta.rocks/operators/pptr/#tpptr-shortcut [`T.Ptr`]: https://go-testdeep.zetta.rocks/operators/ptr/#tptr-shortcut [`T.Re`]: https://go-testdeep.zetta.rocks/operators/re/#tre-shortcut [`T.ReAll`]: https://go-testdeep.zetta.rocks/operators/reall/#treall-shortcut [`T.Recv`]: https://go-testdeep.zetta.rocks/operators/recv/#trecv-shortcut [`T.Set`]: https://go-testdeep.zetta.rocks/operators/set/#tset-shortcut [`T.Shallow`]: https://go-testdeep.zetta.rocks/operators/shallow/#tshallow-shortcut [`T.Slice`]: https://go-testdeep.zetta.rocks/operators/slice/#tslice-shortcut [`T.Smuggle`]: https://go-testdeep.zetta.rocks/operators/smuggle/#tsmuggle-shortcut [`T.SStruct`]: https://go-testdeep.zetta.rocks/operators/sstruct/#tsstruct-shortcut [`T.String`]: https://go-testdeep.zetta.rocks/operators/string/#tstring-shortcut [`T.Struct`]: https://go-testdeep.zetta.rocks/operators/struct/#tstruct-shortcut [`T.SubBagOf`]: https://go-testdeep.zetta.rocks/operators/subbagof/#tsubbagof-shortcut [`T.SubJSONOf`]: https://go-testdeep.zetta.rocks/operators/subjsonof/#tsubjsonof-shortcut [`T.SubMapOf`]: https://go-testdeep.zetta.rocks/operators/submapof/#tsubmapof-shortcut [`T.SubSetOf`]: https://go-testdeep.zetta.rocks/operators/subsetof/#tsubsetof-shortcut [`T.SuperBagOf`]: https://go-testdeep.zetta.rocks/operators/superbagof/#tsuperbagof-shortcut [`T.SuperJSONOf`]: https://go-testdeep.zetta.rocks/operators/superjsonof/#tsuperjsonof-shortcut [`T.SuperMapOf`]: https://go-testdeep.zetta.rocks/operators/supermapof/#tsupermapof-shortcut [`T.SuperSetOf`]: https://go-testdeep.zetta.rocks/operators/supersetof/#tsupersetof-shortcut [`T.SuperSliceOf`]: https://go-testdeep.zetta.rocks/operators/supersliceof/#tsupersliceof-shortcut [`T.TruncTime`]: https://go-testdeep.zetta.rocks/operators/trunctime/#ttrunctime-shortcut [`T.Values`]: https://go-testdeep.zetta.rocks/operators/values/#tvalues-shortcut [`T.Zero`]: https://go-testdeep.zetta.rocks/operators/zero/#tzero-shortcut golang-github-maxatome-go-testdeep-1.14.0/any.go000066400000000000000000000004241454313311600214720ustar00rootroot00000000000000// Copyright (c) 2022, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. //go:build !go1.18 // +build !go1.18 package testdeep type any = interface{} golang-github-maxatome-go-testdeep-1.14.0/any_test.go000066400000000000000000000004311454313311600225270ustar00rootroot00000000000000// Copyright (c) 2022, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. //go:build !go1.18 // +build !go1.18 package testdeep_test type any = interface{} golang-github-maxatome-go-testdeep-1.14.0/go.mod000066400000000000000000000001471454313311600214640ustar00rootroot00000000000000module github.com/maxatome/go-testdeep go 1.18 require github.com/davecgh/go-spew v1.1.1 // indirect golang-github-maxatome-go-testdeep-1.14.0/go.sum000066400000000000000000000002531454313311600215070ustar00rootroot00000000000000github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= golang-github-maxatome-go-testdeep-1.14.0/helpers/000077500000000000000000000000001454313311600220165ustar00rootroot00000000000000golang-github-maxatome-go-testdeep-1.14.0/helpers/nocolor/000077500000000000000000000000001454313311600234715ustar00rootroot00000000000000golang-github-maxatome-go-testdeep-1.14.0/helpers/nocolor/nocolor.go000066400000000000000000000010061454313311600254700ustar00rootroot00000000000000// Copyright (c) 2021, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. // Package nocolor is only intended to easily disable coloring output // of failure reports, typically useful in golang playground. // // Simply import it, and nothing else: // // import _ "github.com/maxatome/go-testdeep/helpers/nocolor" package nocolor import "os" func init() { os.Setenv("TESTDEEP_COLOR", "off") } golang-github-maxatome-go-testdeep-1.14.0/helpers/nocolor/nocolor_test.go000066400000000000000000000007301454313311600265320ustar00rootroot00000000000000// Copyright (c) 2021, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package nocolor_test import ( "os" "testing" _ "github.com/maxatome/go-testdeep/helpers/nocolor" ) func TestNocolor(t *testing.T) { tdColor := os.Getenv("TESTDEEP_COLOR") if tdColor != "off" { t.Errorf(`TESTDEEP_COLOR expected to be "off" but is %q`, tdColor) } } golang-github-maxatome-go-testdeep-1.14.0/helpers/tdhttp/000077500000000000000000000000001454313311600233255ustar00rootroot00000000000000golang-github-maxatome-go-testdeep-1.14.0/helpers/tdhttp/any.go000066400000000000000000000004221454313311600244410ustar00rootroot00000000000000// Copyright (c) 2022, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. //go:build !go1.18 // +build !go1.18 package tdhttp type any = interface{} golang-github-maxatome-go-testdeep-1.14.0/helpers/tdhttp/any_test.go000066400000000000000000000004271454313311600255050ustar00rootroot00000000000000// Copyright (c) 2022, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. //go:build !go1.18 // +build !go1.18 package tdhttp_test type any = interface{} golang-github-maxatome-go-testdeep-1.14.0/helpers/tdhttp/doc.go000066400000000000000000000035601454313311600244250ustar00rootroot00000000000000// Copyright (c) 2020, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. // Package tdhttp, from [go-testdeep], provides some functions to easily // test HTTP handlers. // // Combined to [td] package it provides powerful testing features. // // # TestAPI // // The better way to test HTTP APIs using this package. // // ta := tdhttp.NewTestAPI(t, mux) // // ta.Get("/person/42", "Accept", "application/xml"). // CmpStatus(http.StatusOK). // CmpHeader(td.ContainsKey("X-Custom-Header")). // CmpCookie(td.SuperBagOf(td.Smuggle("Name", "cookie_session"))). // CmpXMLBody(Person{ // ID: ta.Anchor(td.NotZero(), uint64(0)).(uint64), // Name: "Bob", // Age: 26, // }) // // ta.Get("/person/42", "Accept", "application/json"). // CmpStatus(http.StatusOK). // CmpHeader(td.ContainsKey("X-Custom-Header")). // CmpCookies(td.SuperBagOf(td.Struct(&http.Cookie{Name: "cookie_session"}, nil))). // CmpJSONBody(td.JSON(` // { // "id": $1, // "name": "Bob", // "age": 26 // }`, // td.NotZero())) // // See the full example below. // // # Cmp…Response functions // // Historically, it was the only way to test HTTP APIs using // this package. // // ok := tdhttp.CmpJSONResponse(t, // tdhttp.Get("/person/42"), // myAPI.ServeHTTP, // Response{ // Status: http.StatusOK, // Header: td.ContainsKey("X-Custom-Header"), // Cookies: td.SuperBagOf(td.Smuggle("Name", "cookie_session")), // Body: Person{ // ID: 42, // Name: "Bob", // Age: 26, // }, // }, // "/person/{id} route") // // It now uses [TestAPI] behind the scene. It is better to directly // use [TestAPI] and its methods instead, as it is more flexible and // readable. // // [go-testdeep]: https://go-testdeep.zetta.rocks/ package tdhttp golang-github-maxatome-go-testdeep-1.14.0/helpers/tdhttp/example_test.go000066400000000000000000000217371454313311600263600ustar00rootroot00000000000000// Copyright (c) 2020, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package tdhttp_test import ( "encoding/json" "encoding/xml" "fmt" "io" "net/http" "net/url" "strconv" "strings" "sync" "testing" "time" "github.com/maxatome/go-testdeep/helpers/tdhttp" "github.com/maxatome/go-testdeep/td" ) func Example() { t := &testing.T{} // Our API handle Persons with 3 routes: // - POST /person // - GET /person/{personID} // - DELETE /person/{personID} // Person describes a person. type Person struct { ID int64 `json:"id,omitempty" xml:"ID,omitempty"` Name string `json:"name" xml:"Name"` Age int `json:"age" xml:"Age"` CreatedAt *time.Time `json:"created_at,omitempty" xml:"CreatedAt,omitempty"` } // Error is returned to the client in case of error. type Error struct { Mesg string `json:"message" xml:"Message"` Code int `json:"code" xml:"Code"` } // Our µDB :) var mu sync.Mutex personByID := map[int64]*Person{} personByName := map[string]*Person{} var lastID int64 // reply is a helper to send responses. reply := func(w http.ResponseWriter, status int, contentType string, body any) { if body == nil { w.WriteHeader(status) return } w.Header().Set("Content-Type", contentType) w.WriteHeader(status) switch contentType { case "application/json": json.NewEncoder(w).Encode(body) //nolint: errcheck case "application/xml": xml.NewEncoder(w).Encode(body) //nolint: errcheck default: // text/plain fmt.Fprintf(w, "%+v", body) } } // Our API mux := http.NewServeMux() // POST /person mux.HandleFunc("/person", func(w http.ResponseWriter, req *http.Request) { if req.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } if req.Body == nil { http.Error(w, "Bad request", http.StatusBadRequest) return } defer req.Body.Close() var in Person var contentType string switch req.Header.Get("Content-Type") { case "application/json": err := json.NewDecoder(req.Body).Decode(&in) if err != nil { http.Error(w, "Bad request", http.StatusBadRequest) return } case "application/xml": err := xml.NewDecoder(req.Body).Decode(&in) if err != nil { http.Error(w, "Bad request", http.StatusBadRequest) return } case "application/x-www-form-urlencoded": b, err := io.ReadAll(req.Body) if err != nil { http.Error(w, "Bad request", http.StatusBadRequest) return } v, err := url.ParseQuery(string(b)) if err != nil { http.Error(w, "Bad request", http.StatusBadRequest) return } in.Name = v.Get("name") in.Age, err = strconv.Atoi(v.Get("age")) if err != nil { http.Error(w, "Bad request", http.StatusBadRequest) return } default: http.Error(w, "Unsupported media type", http.StatusUnsupportedMediaType) return } contentType = req.Header.Get("Accept") if in.Name == "" || in.Age <= 0 { reply(w, http.StatusBadRequest, contentType, Error{ Mesg: "Empty name or bad age", Code: http.StatusBadRequest, }) return } mu.Lock() defer mu.Unlock() if personByName[in.Name] != nil { reply(w, http.StatusConflict, contentType, Error{ Mesg: "Person already exists", Code: http.StatusConflict, }) return } lastID++ in.ID = lastID now := time.Now() in.CreatedAt = &now personByID[in.ID] = &in personByName[in.Name] = &in reply(w, http.StatusCreated, contentType, in) }) // GET /person/{id} // DELETE /person/{id} mux.HandleFunc("/person/", func(w http.ResponseWriter, req *http.Request) { id, err := strconv.ParseInt(strings.TrimPrefix(req.URL.Path, "/person/"), 10, 64) if err != nil { http.Error(w, "Bad request", http.StatusBadRequest) return } accept := req.Header.Get("Accept") mu.Lock() defer mu.Unlock() if personByID[id] == nil { reply(w, http.StatusNotFound, accept, Error{ Mesg: "Person does not exist", Code: http.StatusNotFound, }) return } switch req.Method { case http.MethodGet: reply(w, http.StatusOK, accept, personByID[id]) case http.MethodDelete: delete(personByID, id) reply(w, http.StatusNoContent, "", nil) default: http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) } }) // // Let's test our API // ta := tdhttp.NewTestAPI(t, mux) // Re-usable custom operator to check Content-Type header contentTypeIs := func(ct string) td.TestDeep { return td.SuperMapOf(http.Header{"Content-Type": []string{ct}}, nil) } // // Person not found // ta.Get("/person/42", "Accept", "application/json"). Name("GET /person/42 - JSON"). CmpStatus(404). CmpHeader(contentTypeIs("application/json")). CmpJSONBody(Error{ Mesg: "Person does not exist", Code: 404, }) fmt.Println("GET /person/42 - JSON:", !ta.Failed()) ta.Get("/person/42", "Accept", "application/xml"). Name("GET /person/42 - XML"). CmpStatus(404). CmpHeader(contentTypeIs("application/xml")). CmpXMLBody(Error{ Mesg: "Person does not exist", Code: 404, }) fmt.Println("GET /person/42 - XML:", !ta.Failed()) ta.Get("/person/42", "Accept", "text/plain"). Name("GET /person/42 - raw"). CmpStatus(404). CmpHeader(contentTypeIs("text/plain")). CmpBody("{Mesg:Person does not exist Code:404}") fmt.Println("GET /person/42 - raw:", !ta.Failed()) // // Create a Person // var bobID int64 ta.PostXML("/person", Person{Name: "Bob", Age: 32}, "Accept", "application/xml"). Name("POST /person - XML"). CmpStatus(201). CmpHeader(contentTypeIs("application/xml")). CmpXMLBody(Person{ // using operator anchoring directly in literal ID: ta.A(td.Catch(&bobID, td.NotZero()), int64(0)).(int64), Name: "Bob", Age: 32, CreatedAt: ta.A(td.Ptr(td.Between(ta.SentAt(), time.Now()))).(*time.Time), }) fmt.Printf("POST /person - XML: %t → Bob ID=%d\n", !ta.Failed(), bobID) var aliceID int64 ta.PostJSON("/person", Person{Name: "Alice", Age: 35}, "Accept", "application/json"). Name("POST /person - JSON"). CmpStatus(201). CmpHeader(contentTypeIs("application/json")). CmpJSONBody(td.JSON(` // using JSON operator (yes comment allowed in JSON!) { "id": $1, "name": "Alice", "age": 35, "created_at": $2 }`, td.Catch(&aliceID, td.NotZero()), td.Smuggle(func(date string) (time.Time, error) { return time.Parse(time.RFC3339Nano, date) }, td.Between(ta.SentAt(), time.Now())))) fmt.Printf("POST /person - JSON: %t → Alice ID=%d\n", !ta.Failed(), aliceID) var brittID int64 ta.PostForm("/person", url.Values{ "name": []string{"Britt"}, "age": []string{"29"}, }, "Accept", "text/plain"). Name("POST /person - raw"). CmpStatus(201). CmpHeader(contentTypeIs("text/plain")). // using Re (= Regexp) operator CmpBody(td.Re(`\{ID:(\d+) Name:Britt Age:29 CreatedAt:.*\}\z`, td.Smuggle(func(groups []string) (int64, error) { return strconv.ParseInt(groups[0], 10, 64) }, td.Catch(&brittID, td.NotZero())))) fmt.Printf("POST /person - raw: %t → Britt ID=%d\n", !ta.Failed(), brittID) // // Get a Person // ta.Get(fmt.Sprintf("/person/%d", aliceID), "Accept", "application/xml"). Name("GET Alice - XML (ID #%d)", aliceID). CmpStatus(200). CmpHeader(contentTypeIs("application/xml")). CmpXMLBody(td.SStruct( // using SStruct operator Person{ ID: aliceID, Name: "Alice", Age: 35, }, td.StructFields{ "CreatedAt": td.Ptr(td.NotZero()), }, )) fmt.Println("GET XML Alice:", !ta.Failed()) ta.Get(fmt.Sprintf("/person/%d", aliceID), "Accept", "application/json"). Name("GET Alice - JSON (ID #%d)", aliceID). CmpStatus(200). CmpHeader(contentTypeIs("application/json")). CmpJSONBody(td.JSON(` // using JSON operator (yes comment allowed in JSON!) { "id": $1, "name": "Alice", "age": 35, "created_at": $2 }`, aliceID, td.Not(td.Re(`^0001-01-01`)), // time is not 0001-01-01… aka zero time.Time )) fmt.Println("GET JSON Alice:", !ta.Failed()) // // Delete a Person // ta.Delete(fmt.Sprintf("/person/%d", aliceID), nil). Name("DELETE Alice (ID #%d)", aliceID). CmpStatus(204). CmpHeader(td.Not(td.ContainsKey("Content-Type"))). NoBody() fmt.Println("DELETE Alice:", !ta.Failed()) // Check Alice is deleted ta.Get(fmt.Sprintf("/person/%d", aliceID), "Accept", "application/json"). Name("GET (deleted) Alice - JSON (ID #%d)", aliceID). CmpStatus(404). CmpHeader(contentTypeIs("application/json")). CmpJSONBody(td.JSON(` { "message": "Person does not exist", "code": 404 }`)) fmt.Println("Alice is not found anymore:", !ta.Failed()) // Output: // GET /person/42 - JSON: true // GET /person/42 - XML: true // GET /person/42 - raw: true // POST /person - XML: true → Bob ID=1 // POST /person - JSON: true → Alice ID=2 // POST /person - raw: true → Britt ID=3 // GET XML Alice: true // GET JSON Alice: true // DELETE Alice: true // Alice is not found anymore: true } golang-github-maxatome-go-testdeep-1.14.0/helpers/tdhttp/http.go000066400000000000000000000263711454313311600246440ustar00rootroot00000000000000// Copyright (c) 2019, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package tdhttp import ( "encoding/json" "encoding/xml" "fmt" "net/http" "reflect" "testing" "github.com/maxatome/go-testdeep/helpers/tdutil" "github.com/maxatome/go-testdeep/internal/trace" "github.com/maxatome/go-testdeep/td" ) func init() { trace.IgnorePackage() } // Response is used by Cmp*Response functions to make the HTTP // response match easier. Each field, can be a [td.TestDeep] operator // as well as the exact expected value. type Response struct { Status any // is the expected status (ignored if nil) Header any // is the expected header (ignored if nil) Cookies any // is the expected cookies (ignored if nil) Body any // is the expected body (expected to be empty if nil) } func cmpMarshaledResponse(tb testing.TB, req *http.Request, handler func(w http.ResponseWriter, r *http.Request), acceptEmptyBody bool, unmarshal func([]byte, any) error, expectedResp Response, args ...any, ) bool { tb.Helper() if testName := tdutil.BuildTestName(args...); testName != "" { tb.Log(testName) } t := td.NewT(tb) defer t.AnchorsPersistTemporarily()() ta := NewTestAPI(t, http.HandlerFunc(handler)).Request(req) // Check status, nil = ignore if expectedResp.Status != nil { ta.CmpStatus(expectedResp.Status) } // Check header, nil = ignore if expectedResp.Header != nil { ta.CmpHeader(expectedResp.Header) } // Check cookie, nil = ignore if expectedResp.Cookies != nil { ta.CmpCookies(expectedResp.Cookies) } if expectedResp.Body == nil { ta.NoBody() } else { ta.cmpMarshaledBody(acceptEmptyBody, unmarshal, expectedResp.Body) } return !ta.Failed() } // CmpMarshaledResponse is the base function used by some others in // tdhttp package. req is launched against handler. The response body // is unmarshaled using unmarshal. The response is then tested against // expectedResp. // // args... are optional and allow to name the test, a t.Log() done // before starting any test. If len(args) > 1 and the first item of // args is a string and contains a '%' rune then [fmt.Fprintf] is used // to compose the name, else args are passed to [fmt.Fprint]. // // It returns true if the tests succeed, false otherwise. // // See [TestAPI] type and its methods for more flexible tests. func CmpMarshaledResponse(t testing.TB, req *http.Request, handler func(w http.ResponseWriter, r *http.Request), unmarshal func([]byte, any) error, expectedResp Response, args ...any, ) bool { t.Helper() return cmpMarshaledResponse(t, req, handler, false, unmarshal, expectedResp, args...) } // CmpResponse is used to match a []byte or string response body. req // is launched against handler. If expectedResp.Body is non-nil, the // response body is converted to []byte or string, depending on the // expectedResp.Body type. The response is then tested against // expectedResp. // // args... are optional and allow to name the test, a t.Log() done // before starting any test. If len(args) > 1 and the first item of // args is a string and contains a '%' rune then [fmt.Fprintf] is used // to compose the name, else args are passed to [fmt.Fprint]. // // It returns true if the tests succeed, false otherwise. // // ok := tdhttp.CmpResponse(t, // tdhttp.Get("/test"), // myAPI.ServeHTTP, // Response{ // Status: http.StatusOK, // Header: td.ContainsKey("X-Custom-Header"), // Body: "OK!\n", // }, // "/test route") // // Response.Status, Response.Header and Response.Body fields can all // be [td.TestDeep] operators as it is for Response.Header field // here. Otherwise, Response.Status should be an int, Response.Header // a [http.Header] and Response.Body a []byte or a string. // // See [TestAPI] type and its methods for more flexible tests. func CmpResponse(t testing.TB, req *http.Request, handler func(w http.ResponseWriter, r *http.Request), expectedResp Response, args ...any) bool { t.Helper() return cmpMarshaledResponse(t, req, handler, true, func(body []byte, target any) error { switch t := target.(type) { case *string: *t = string(body) case *[]byte: *t = body case *any: *t = body default: // cmpMarshaledBody (behind cmpMarshaledResponse) always calls // us with target as a pointer return fmt.Errorf( "CmpResponse only accepts expectedResp.Body be a []byte, a string or a TestDeep operator allowing to match these types, but not type %s", reflect.TypeOf(target).Elem()) } return nil }, expectedResp, args...) } // CmpJSONResponse is used to match a JSON response body. req is // launched against handler. If expectedResp.Body is non-nil, the // response body is [json.Unmarshal]'ed. The response is then tested // against expectedResp. // // args... are optional and allow to name the test, a t.Log() done // before starting any test. If len(args) > 1 and the first item of // args is a string and contains a '%' rune then [fmt.Fprintf] is used // to compose the name, else args are passed to [fmt.Fprint]. // // It returns true if the tests succeed, false otherwise. // // ok := tdhttp.CmpJSONResponse(t, // tdhttp.Get("/person/42"), // myAPI.ServeHTTP, // Response{ // Status: http.StatusOK, // Header: td.ContainsKey("X-Custom-Header"), // Body: Person{ // ID: 42, // Name: "Bob", // Age: 26, // }, // }, // "/person/{id} route") // // Response.Status, Response.Header and Response.Body fields can all // be [td.TestDeep] operators as it is for Response.Header field // here. Otherwise, Response.Status should be an int, Response.Header // a [http.Header] and Response.Body any type one can // [json.Unmarshal] into. // // If Response.Status and Response.Header are omitted (or nil), they // are not tested. // // If Response.Body is omitted (or nil), it means the body response has to be // empty. If you want to ignore the body response, use [td.Ignore] // explicitly. // // See [TestAPI] type and its methods for more flexible tests. func CmpJSONResponse(t testing.TB, req *http.Request, handler func(w http.ResponseWriter, r *http.Request), expectedResp Response, args ...any, ) bool { t.Helper() return CmpMarshaledResponse(t, req, handler, json.Unmarshal, expectedResp, args...) } // CmpXMLResponse is used to match an XML response body. req // is launched against handler. If expectedResp.Body is // non-nil, the response body is [xml.Unmarshal]'ed. The response is // then tested against expectedResp. // // args... are optional and allow to name the test, a t.Log() done // before starting any test. If len(args) > 1 and the first item of // args is a string and contains a '%' rune then [fmt.Fprintf] is used // to compose the name, else args are passed to [fmt.Fprint]. // // It returns true if the tests succeed, false otherwise. // // ok := tdhttp.CmpXMLResponse(t, // tdhttp.Get("/person/42"), // myAPI.ServeHTTP, // Response{ // Status: http.StatusOK, // Header: td.ContainsKey("X-Custom-Header"), // Body: Person{ // ID: 42, // Name: "Bob", // Age: 26, // }, // }, // "/person/{id} route") // // Response.Status, Response.Header and Response.Body fields can all // be [td.TestDeep] operators as it is for Response.Header field // here. Otherwise, Response.Status should be an int, Response.Header // a [http.Header] and Response.Body any type one can [xml.Unmarshal] // into. // // If Response.Status and Response.Header are omitted (or nil), they // are not tested. // // If Response.Body is omitted (or nil), it means the body response // has to be empty. If you want to ignore the body response, use // [td.Ignore] explicitly. // // See [TestAPI] type and its methods for more flexible tests. func CmpXMLResponse(t testing.TB, req *http.Request, handler func(w http.ResponseWriter, r *http.Request), expectedResp Response, args ...any, ) bool { t.Helper() return CmpMarshaledResponse(t, req, handler, xml.Unmarshal, expectedResp, args...) } // CmpMarshaledResponseFunc returns a function ready to be used with // [testing.T.Run], calling [CmpMarshaledResponse] behind the scene. As it // is intended to be used in conjunction with [testing.T.Run] which // names the sub-test, the test name part (args...) is voluntary // omitted. // // t.Run("Subtest name", tdhttp.CmpMarshaledResponseFunc( // tdhttp.Get("/text"), // mux.ServeHTTP, // tdhttp.Response{ // Status: http.StatusOK, // })) // // See [CmpMarshaledResponse] for details. // // See [TestAPI] type and its methods for more flexible tests. func CmpMarshaledResponseFunc(req *http.Request, handler func(w http.ResponseWriter, r *http.Request), unmarshal func([]byte, any) error, expectedResp Response) func(t *testing.T) { return func(t *testing.T) { t.Helper() CmpMarshaledResponse(t, req, handler, unmarshal, expectedResp) } } // CmpResponseFunc returns a function ready to be used with // [testing.T.Run], calling [CmpResponse] behind the scene. As it is // intended to be used in conjunction with [testing.T.Run] which names // the sub-test, the test name part (args...) is voluntary omitted. // // t.Run("Subtest name", tdhttp.CmpResponseFunc( // tdhttp.Get("/text"), // mux.ServeHTTP, // tdhttp.Response{ // Status: http.StatusOK, // })) // // See [CmpResponse] documentation for details. // // See [TestAPI] type and its methods for more flexible tests. func CmpResponseFunc(req *http.Request, handler func(w http.ResponseWriter, r *http.Request), expectedResp Response) func(t *testing.T) { return func(t *testing.T) { t.Helper() CmpResponse(t, req, handler, expectedResp) } } // CmpJSONResponseFunc returns a function ready to be used with // [testing.T.Run], calling [CmpJSONResponse] behind the scene. As it is // intended to be used in conjunction with [testing.T.Run] which names // the sub-test, the test name part (args...) is voluntary omitted. // // t.Run("Subtest name", tdhttp.CmpJSONResponseFunc( // tdhttp.Get("/json"), // mux.ServeHTTP, // tdhttp.Response{ // Status: http.StatusOK, // Body: JResp{Comment: "expected comment!"}, // })) // // See [CmpJSONResponse] documentation for details. // // See [TestAPI] type and its methods for more flexible tests. func CmpJSONResponseFunc(req *http.Request, handler func(w http.ResponseWriter, r *http.Request), expectedResp Response) func(t *testing.T) { return func(t *testing.T) { t.Helper() CmpJSONResponse(t, req, handler, expectedResp) } } // CmpXMLResponseFunc returns a function ready to be used with // [testing.T.Run], calling [CmpXMLResponse] behind the scene. As it is // intended to be used in conjunction with [testing.T.Run] which names // the sub-test, the test name part (args...) is voluntary omitted. // // t.Run("Subtest name", tdhttp.CmpXMLResponseFunc( // tdhttp.Get("/xml"), // mux.ServeHTTP, // tdhttp.Response{ // Status: http.StatusOK, // Body: JResp{Comment: "expected comment!"}, // })) // // See [CmpXMLResponse] documentation for details. // // See [TestAPI] type and its methods for more flexible tests. func CmpXMLResponseFunc(req *http.Request, handler func(w http.ResponseWriter, r *http.Request), expectedResp Response) func(t *testing.T) { return func(t *testing.T) { t.Helper() CmpXMLResponse(t, req, handler, expectedResp) } } golang-github-maxatome-go-testdeep-1.14.0/helpers/tdhttp/http_test.go000066400000000000000000000466071454313311600257070ustar00rootroot00000000000000// Copyright (c) 2019, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package tdhttp_test import ( "fmt" "net/http" "net/http/httptest" "os" "regexp" "strings" "testing" "github.com/maxatome/go-testdeep/helpers/tdhttp" "github.com/maxatome/go-testdeep/helpers/tdutil" "github.com/maxatome/go-testdeep/internal/color" "github.com/maxatome/go-testdeep/td" ) func TestMain(m *testing.M) { color.SaveState() os.Exit(m.Run()) } type CmpResponseTest struct { Name string Handler func(w http.ResponseWriter, r *http.Request) Success bool ExpectedResp tdhttp.Response ExpectedLogs []string } func TestCmpResponse(tt *testing.T) { handlerNonEmpty := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Add("X-TestDeep", "foobar") w.WriteHeader(242) fmt.Fprintln(w, "text response") }) cookie := http.Cookie{Name: "Cookies-Testdeep", Value: "foobar"} handlerWithCokies := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Add("X-TestDeep", "cookies") http.SetCookie(w, &cookie) w.WriteHeader(242) }) handlerEmpty := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Add("X-TestDeep", "zip") w.WriteHeader(424) }) t := td.NewT(tt) for _, curTest := range []CmpResponseTest{ // Non-empty success { Name: "string body only", Handler: handlerNonEmpty, Success: true, ExpectedResp: tdhttp.Response{Body: "text response\n"}, }, { Name: "[]byte status + body", Handler: handlerNonEmpty, Success: true, ExpectedResp: tdhttp.Response{ Status: 242, Body: []byte("text response\n"), }, }, { Name: "[]byte status + header + body", Handler: handlerNonEmpty, Success: true, ExpectedResp: tdhttp.Response{ Status: 242, Header: http.Header{"X-Testdeep": []string{"foobar"}}, Body: []byte("text response\n"), }, }, { Name: "with TestDeep operators", Handler: handlerNonEmpty, Success: true, ExpectedResp: tdhttp.Response{ Status: td.Between(200, 300), Header: td.ContainsKey("X-Testdeep"), Body: td.All( td.Isa(""), // enforces TypeBehind → string td.Contains("response"), ), }, }, { Name: "ignore body explicitly", Handler: handlerNonEmpty, Success: true, ExpectedResp: tdhttp.Response{ Status: 242, Header: http.Header{"X-Testdeep": []string{"foobar"}}, Body: td.Ignore(), }, }, { Name: "body just not empty", Handler: handlerNonEmpty, Success: true, ExpectedResp: tdhttp.Response{ Status: 242, Header: http.Header{"X-Testdeep": []string{"foobar"}}, Body: td.NotEmpty(), }, }, // Non-empty failures { Name: "bad status", Handler: handlerNonEmpty, Success: false, ExpectedResp: tdhttp.Response{Status: 243, Body: td.NotEmpty()}, ExpectedLogs: []string{ `~ Failed test 'status code should match' \s+Response.Status: values differ \s+got: 242 \s+expected: 243`, `~ Received response: \s+\x60(?s:.+?) \s+ \s+text response \s+\x60 `, // check the complete body is shown }, }, { Name: "bad body", Handler: handlerNonEmpty, Success: false, ExpectedResp: tdhttp.Response{Body: "BAD!"}, ExpectedLogs: []string{ `~ Failed test 'body contents is OK' \s+Response.Body: values differ \s+got: \x60text response \s+\x60 \s+expected: "BAD!"`, }, }, { Name: "bad body, as non-empty", Handler: handlerNonEmpty, Success: false, ExpectedResp: tdhttp.Response{}, ExpectedLogs: []string{ `~ Failed test 'body should be empty' \s+Response.Body is not empty \s+got: not empty \s+expected: empty`, `~ Received response: \s+\x60(?s:.+?) \s+ \s+text response \s+\x60 `, // check the complete body is shown }, }, { Name: "bad header", Handler: handlerNonEmpty, Success: false, ExpectedResp: tdhttp.Response{ Header: http.Header{"X-Testdeep": []string{"zzz"}}, Body: td.NotEmpty(), }, ExpectedLogs: []string{ `~ Failed test 'header should match' \s+Response.Header\["X-Testdeep"\]\[0\]: values differ \s+got: "foobar" \s+expected: "zzz"`, }, }, { Name: "bad cookies", Handler: handlerWithCokies, Success: false, ExpectedResp: tdhttp.Response{ Cookies: []*http.Cookie{{Name: "Cookies-Testdeep", Value: "squalala"}}, Body: td.Empty(), }, ExpectedLogs: []string{ `~ Failed test 'cookies should match' \s+Response.Cookie\[0\]\.Value: values differ \s+got: "foobar" \s+expected: "squalala"`, }, }, { Name: "good cookies", Handler: handlerWithCokies, Success: true, ExpectedResp: tdhttp.Response{ Header: http.Header{ "X-Testdeep": []string{"cookies"}, "Set-Cookie": []string{cookie.String()}, }, Cookies: []*http.Cookie{&cookie}, Body: td.Empty(), }, }, { Name: "cannot unmarshal", Handler: handlerNonEmpty, Success: false, ExpectedResp: tdhttp.Response{Body: 123}, ExpectedLogs: []string{ `~ Failed test 'body unmarshaling' \s+unmarshal\(Response\.Body\): should NOT be an error \s+got: .*Cmp(Response|Body) only accepts expected(Resp\.)?Body be a \[\]byte, a string or a TestDeep operator allowing to match these types, but not type int.* \s+expected: nil`, `~ Received response: \s+\x60(?s:.+?) \s+ \s+text response \s+\x60 `, // check the complete body is shown }, }, // Empty success { Name: "empty body", Handler: handlerEmpty, Success: true, ExpectedResp: tdhttp.Response{ Status: 424, Header: http.Header{"X-Testdeep": []string{"zip"}}, Body: nil, }, }, // Empty failures { Name: "should not be empty", Handler: handlerEmpty, Success: false, ExpectedResp: tdhttp.Response{Body: "NOT EMPTY!"}, ExpectedLogs: []string{ `~ Failed test 'body contents is OK' \s+Response.Body: values differ \s+got: "" \s+expected: "NOT EMPTY!"`, }, }, } { t.Run(curTest.Name, func(t *td.T) { testCmpResponse(t, tdhttp.CmpResponse, "CmpResponse", curTest) }) t.Run(curTest.Name+" TestAPI", func(t *td.T) { testTestAPI(t, (*tdhttp.TestAPI).CmpBody, "CmpBody", curTest) }) } } func TestCmpJSONResponse(tt *testing.T) { handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Add("X-TestDeep", "foobar") w.WriteHeader(242) fmt.Fprintln(w, `{"name":"Bob"}`) }) type JResp struct { Name string `json:"name"` } t := td.NewT(tt) for _, curTest := range []CmpResponseTest{ // Success { Name: "JSON OK", Handler: handler, Success: true, ExpectedResp: tdhttp.Response{ Status: 242, Header: http.Header{"X-Testdeep": []string{"foobar"}}, Body: JResp{Name: "Bob"}, }, }, { Name: "JSON ptr OK", Handler: handler, Success: true, ExpectedResp: tdhttp.Response{ Status: 242, Header: http.Header{"X-Testdeep": []string{"foobar"}}, Body: &JResp{Name: "Bob"}, }, }, // Failure { Name: "JSON failure", Handler: handler, Success: false, ExpectedResp: tdhttp.Response{ Body: 123, }, ExpectedLogs: []string{ `~ Failed test 'body unmarshaling' \s+unmarshal\(Response\.Body\): should NOT be an error \s+got: .*cannot unmarshal object into Go value of type int.* \s+expected: nil`, `~ Received response: \s+\x60(?s:.+?) \s+ \s+\{"name":"Bob"\} \s+\x60 `, // check the complete body is shown }, }, } { t.Run(curTest.Name, func(t *td.T) { testCmpResponse(t, tdhttp.CmpJSONResponse, "CmpJSONResponse", curTest) }) t.Run(curTest.Name+" TestAPI", func(t *td.T) { testTestAPI(t, (*tdhttp.TestAPI).CmpJSONBody, "CmpJSONBody", curTest) }) } } func TestCmpJSONResponseAnchor(tt *testing.T) { t := td.NewT(tt) handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(242) fmt.Fprintln(w, `{"name":"Bob"}`) }) req := httptest.NewRequest("GET", "/path", nil) type JResp struct { Name string `json:"name"` } // With *td.T tdhttp.CmpJSONResponse(t, req, handler, tdhttp.Response{ Status: 242, Body: JResp{ Name: t.A(td.Re("(?i)bob"), "").(string), }, }) // With *testing.T tdhttp.CmpJSONResponse(tt, req, handler, tdhttp.Response{ Status: 242, Body: JResp{ Name: t.A(td.Re("(?i)bob"), "").(string), }, }) func() { defer t.AnchorsPersistTemporarily()() op := t.A(td.Re("(?i)bob"), "").(string) // All calls should succeed, as op persists tdhttp.CmpJSONResponse(t, req, handler, tdhttp.Response{ Status: 242, Body: JResp{Name: op}, }) tdhttp.CmpJSONResponse(t, req, handler, tdhttp.Response{ Status: 242, Body: JResp{Name: op}, }) // Even with the original *testing.T instance (here tt) tdhttp.CmpJSONResponse(tt, req, handler, tdhttp.Response{ Status: 242, Body: JResp{Name: op}, }) }() // Failures t.FailureIsFatal().False(t.DoAnchorsPersist()) // just to be sure mt := td.NewT(tdutil.NewT("tdhttp_persistence_test")) op := mt.A(td.Re("(?i)bob"), "").(string) // First call should succeed t.True(tdhttp.CmpJSONResponse(mt, req, handler, tdhttp.Response{ Status: 242, Body: JResp{Name: op}, })) // Second one should fail, as previously anchored operator has been reset t.False(tdhttp.CmpJSONResponse(mt, req, handler, tdhttp.Response{ Status: 242, Body: JResp{Name: op}, })) } func TestCmpXMLResponse(tt *testing.T) { handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Add("X-TestDeep", "foobar") w.WriteHeader(242) fmt.Fprintln(w, `Bob`) }) type XResp struct { Name string `xml:"name"` } t := td.NewT(tt) for _, curTest := range []CmpResponseTest{ // Success { Name: "XML OK", Handler: handler, Success: true, ExpectedResp: tdhttp.Response{ Status: 242, Header: http.Header{"X-Testdeep": []string{"foobar"}}, Body: XResp{Name: "Bob"}, }, }, { Name: "XML ptr OK", Handler: handler, Success: true, ExpectedResp: tdhttp.Response{ Status: 242, Header: http.Header{"X-Testdeep": []string{"foobar"}}, Body: &XResp{Name: "Bob"}, }, }, // Failure { Name: "XML failure", Handler: handler, Success: false, ExpectedResp: tdhttp.Response{ // xml.Unmarshal does not raise an error when trying to // unmarshal in an int, as json does... Body: func() {}, }, ExpectedLogs: []string{ `~ Failed test 'body unmarshaling' \s+unmarshal\(Response\.Body\): should NOT be an error \s+got: .*unknown type func\(\).* \s+expected: nil`, `~ Received response: \s+\x60(?s:.+?) \s+ \s+Bob \s+\x60 `, // check the complete body is shown }, }, } { t.Run(curTest.Name, func(t *td.T) { testCmpResponse(t, tdhttp.CmpXMLResponse, "CmpXMLResponse", curTest) }) t.Run(curTest.Name+" TestAPI", func(t *td.T) { testTestAPI(t, (*tdhttp.TestAPI).CmpXMLBody, "CmpXMLBody", curTest) }) } } var logsViz = strings.NewReplacer( " ", "·", "\t", "→", "\r", "", ) func testLogs(t *td.T, mockT *tdutil.T, curTest CmpResponseTest) { t.Helper() dumpLogs := !t.Cmp(mockT.Failed(), !curTest.Success, "test failure") for _, expectedLog := range curTest.ExpectedLogs { if strings.HasPrefix(expectedLog, "~") { re := regexp.MustCompile(expectedLog[1:]) if !re.MatchString(mockT.LogBuf()) { t.Errorf(`logs do not match "%s" regexp`, re) dumpLogs = true } } else if !strings.Contains(mockT.LogBuf(), expectedLog) { t.Errorf(`"%s" not found in test logs`, expectedLog) dumpLogs = true } } if dumpLogs { t.Errorf(`Test logs: "%s"`, logsViz.Replace(mockT.LogBuf())) } } func testCmpResponse(t *td.T, cmp func(testing.TB, *http.Request, func(http.ResponseWriter, *http.Request), tdhttp.Response, ...any) bool, cmpName string, curTest CmpResponseTest, ) { t.Helper() mockT := tdutil.NewT(cmpName) t.Cmp(cmp(mockT, httptest.NewRequest("GET", "/path", nil), curTest.Handler, curTest.ExpectedResp), curTest.Success) testLogs(t, mockT, curTest) } func testTestAPI(t *td.T, cmpBody func(*tdhttp.TestAPI, any) *tdhttp.TestAPI, cmpName string, curTest CmpResponseTest, ) { t.Helper() mockT := tdutil.NewT(cmpName) ta := tdhttp.NewTestAPI(mockT, http.HandlerFunc(curTest.Handler)). Get("/path") if curTest.ExpectedResp.Status != nil { ta.CmpStatus(curTest.ExpectedResp.Status) } if curTest.ExpectedResp.Header != nil { ta.CmpHeader(curTest.ExpectedResp.Header) } if curTest.ExpectedResp.Cookies != nil { ta.CmpCookies(curTest.ExpectedResp.Cookies) } cmpBody(ta, curTest.ExpectedResp.Body) t.Cmp(ta.Failed(), !curTest.Success) testLogs(t, mockT, curTest) } func TestMux(t *testing.T) { mux := http.NewServeMux() // GET /text mux.HandleFunc("/text", func(w http.ResponseWriter, req *http.Request) { if req.Method != "GET" { http.NotFound(w, req) return } w.Header().Set("Content-Type", "text/plain") w.WriteHeader(http.StatusOK) fmt.Fprintf(w, "Text result!") }) // GET /json mux.HandleFunc("/json", func(w http.ResponseWriter, req *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) if req.Method != "GET" { fmt.Fprintf(w, `{"code":404,"message":"Not found"}`) return } fmt.Fprintf(w, `{"comment":"JSON result!"}`) }) // GET /xml mux.HandleFunc("/xml", func(w http.ResponseWriter, req *http.Request) { if req.Method != "GET" { http.NotFound(w, req) return } w.Header().Set("Content-Type", "application/xml") w.WriteHeader(http.StatusOK) fmt.Fprintf(w, `XML result!`) }) // // Check GET /text route t.Run("/text route", func(t *testing.T) { tdhttp.CmpResponse(t, tdhttp.NewRequest("GET", "/text", nil), mux.ServeHTTP, tdhttp.Response{ Status: http.StatusOK, Header: td.SuperMapOf(http.Header{ "Content-Type": []string{"text/plain"}, }, nil), Body: "Text result!", }, "GET /text should return 200 + text/plain + Text result!") tdhttp.CmpResponse(t, tdhttp.NewRequest("PATCH", "/text", nil), mux.ServeHTTP, tdhttp.Response{ Status: http.StatusNotFound, Body: td.Ignore(), }, "PATCH /text should return Not Found") t.Run("/text route via CmpResponseFunc", tdhttp.CmpResponseFunc( tdhttp.NewRequest("GET", "/text", nil), mux.ServeHTTP, tdhttp.Response{ Status: http.StatusOK, Header: td.SuperMapOf(http.Header{ "Content-Type": []string{"text/plain"}, }, nil), Body: "Text result!", })) t.Run("/text route via CmpMarshaledResponseFunc", tdhttp.CmpMarshaledResponseFunc( tdhttp.NewRequest("GET", "/text", nil), mux.ServeHTTP, func(body []byte, target any) error { *target.(*string) = string(body) return nil }, tdhttp.Response{ Status: http.StatusOK, Header: td.SuperMapOf(http.Header{ "Content-Type": []string{"text/plain"}, }, nil), Body: "Text result!", })) }) // // Check GET /json t.Run("/json route", func(t *testing.T) { type JResp struct { Comment string `json:"comment"` } tdhttp.CmpJSONResponse(t, tdhttp.NewRequest("GET", "/json", nil), mux.ServeHTTP, tdhttp.Response{ Status: http.StatusOK, Header: td.SuperMapOf(http.Header{ "Content-Type": []string{"application/json"}, }, nil), Body: JResp{Comment: "JSON result!"}, }, "GET /json should return 200 + application/json + comment=JSON result!") tdhttp.CmpJSONResponse(t, tdhttp.NewRequest("GET", "/json", nil), mux.ServeHTTP, tdhttp.Response{ Status: http.StatusOK, Header: td.SuperMapOf(http.Header{ "Content-Type": []string{"application/json"}, }, nil), Body: td.Struct(JResp{}, td.StructFields{ "Comment": td.Contains("result!"), }), }, "GET /json should return 200 + application/json + comment=~result!") t.Run("/json route via CmpJSONResponseFunc", tdhttp.CmpJSONResponseFunc( tdhttp.NewRequest("GET", "/json", nil), mux.ServeHTTP, tdhttp.Response{ Status: http.StatusOK, Header: td.SuperMapOf(http.Header{ "Content-Type": []string{"application/json"}, }, nil), Body: JResp{Comment: "JSON result!"}, })) // We expect to receive a specific message, but a complete // different one is received (here a Not Found, as PUT is used). // In this case, a log message tell us that nothing has been set // during unmarshaling AND the received body should be dumped t.Run("zeroed body", func(tt *testing.T) { t := td.NewT(tt) mockT := tdutil.NewT("zeroed_body") ok := tdhttp.CmpJSONResponse(mockT, tdhttp.NewRequest("PUT", "/json", nil), mux.ServeHTTP, tdhttp.Response{ Status: http.StatusOK, Header: td.SuperMapOf(http.Header{ "Content-Type": []string{"application/json"}, }, nil), Body: JResp{Comment: "JSON result!"}, }) t.False(ok) t.True(mockT.Failed()) t.Contains(mockT.LogBuf(), "nothing has been set during unmarshaling") t.Contains(mockT.LogBuf(), "Received response:") }) }) // // Check GET /xml t.Run("/xml route", func(t *testing.T) { type XResp struct { Comment string `xml:"comment"` } tdhttp.CmpXMLResponse(t, tdhttp.NewRequest("GET", "/xml", nil), mux.ServeHTTP, tdhttp.Response{ Status: http.StatusOK, Header: td.SuperMapOf(http.Header{ "Content-Type": []string{"application/xml"}, }, nil), Body: XResp{Comment: "XML result!"}, }, "GET /xml should return 200 + application/xml + comment=XML result!") tdhttp.CmpXMLResponse(t, tdhttp.NewRequest("GET", "/xml", nil), mux.ServeHTTP, tdhttp.Response{ Status: http.StatusOK, Header: td.SuperMapOf(http.Header{ "Content-Type": []string{"application/xml"}, }, nil), Body: td.Struct(XResp{}, td.StructFields{ "Comment": td.Contains("result!"), }), }, "GET /xml should return 200 + application/xml + comment=~result!") t.Run("/xml route via CmpXMLResponseFunc", tdhttp.CmpXMLResponseFunc( tdhttp.NewRequest("GET", "/xml", nil), mux.ServeHTTP, tdhttp.Response{ Status: http.StatusOK, Header: td.SuperMapOf(http.Header{ "Content-Type": []string{"application/xml"}, }, nil), Body: td.Struct(XResp{}, td.StructFields{ "Comment": td.Contains("result!"), }), })) // We expect to receive a specific message into a not specific // type (behind the TestDeep operator). The XML unmarshaling // fails, so a log message should tell us to be more clear // concerning the expected body type (in case it is the origin of // the problem). t.Run("Unmarshal is failing", func(tt *testing.T) { t := td.NewT(tt) mockT := tdutil.NewT("Unmarshal_is_failing") ok := tdhttp.CmpXMLResponse(mockT, tdhttp.NewRequest("PUT", "/xml", nil), mux.ServeHTTP, tdhttp.Response{ Status: http.StatusOK, Header: td.SuperMapOf(http.Header{ "Content-Type": []string{"application/xml"}, }, nil), // This TestDeep operators combination is absurd. It is only // intended to avoid CmpXMLResponse detects the expected body // type Body: td.Any(XResp{Comment: "XML result!"}, 12), }) t.False(ok) t.True(mockT.Failed()) t.Contains(mockT.LogBuf(), "Cannot guess the body expected type as Any TestDeep") t.Contains(mockT.LogBuf(), "You can try All(Isa(EXPECTED_TYPE), Any(…)) to disambiguate…") t.Contains(mockT.LogBuf(), "Received response:") }) }) } golang-github-maxatome-go-testdeep-1.14.0/helpers/tdhttp/internal/000077500000000000000000000000001454313311600251415ustar00rootroot00000000000000golang-github-maxatome-go-testdeep-1.14.0/helpers/tdhttp/internal/response.go000066400000000000000000000036231454313311600273320ustar00rootroot00000000000000// Copyright (c) 2021, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package internal import ( "bytes" "net/http" "net/http/httputil" "testing" "unicode/utf8" ) // canBackquote is the same as strconv.CanBackquote but works on // []byte and accepts '\n' and '\r'. func canBackquote(b []byte) bool { for len(b) > 0 { r, wid := utf8.DecodeRune(b) b = b[wid:] if wid > 1 { if r == '\ufeff' { return false // BOMs are invisible and should not be quoted. } continue // All other multibyte runes are correctly encoded and assumed printable. } if r == utf8.RuneError { return false } if (r < ' ' && r != '\t' && r != '\n' && r != '\r') || r == '`' || r == '\u007F' { return false } } return true } func replaceCrLf(b []byte) []byte { return bytes.ReplaceAll(b, []byte("\r\n"), []byte("\n")) } func backquote(b []byte) ([]byte, bool) { // if there is as many \r\n as \n, replace all occurrences by \n // so we can conveniently print the buffer inside `…`. crnl := bytes.Count(b, []byte("\r\n")) cr := bytes.Count(b, []byte("\r")) if crnl != 0 { nl := bytes.Count(b, []byte("\n")) if crnl != nl || crnl != cr { return nil, false } return replaceCrLf(b), true } return b, cr == 0 } // DumpResponse logs "resp" using Logf method of "t". // // It tries to produce a result as readable as possible first using // backquotes then falling back to double-quotes. func DumpResponse(t testing.TB, resp *http.Response) { t.Helper() const label = "Received response:\n" b, _ := httputil.DumpResponse(resp, true) if canBackquote(b) { bodyPos := bytes.Index(b, []byte("\r\n\r\n")) if body, ok := backquote(b[bodyPos+4:]); ok { headers := replaceCrLf(b[:bodyPos]) t.Logf(label+"`%s\n\n%s`", headers, body) return } } t.Logf(label+"%q", b) } golang-github-maxatome-go-testdeep-1.14.0/helpers/tdhttp/internal/response_test.go000066400000000000000000000046431454313311600303740ustar00rootroot00000000000000// Copyright (c) 2021, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package internal_test import ( "bytes" "io" "net/http" "testing" "github.com/maxatome/go-testdeep/helpers/tdhttp/internal" "github.com/maxatome/go-testdeep/internal/test" "github.com/maxatome/go-testdeep/td" ) func newResponse(body string) *http.Response { return &http.Response{ Status: "200 OK", StatusCode: 200, Proto: "HTTP/1.0", ProtoMajor: 1, ProtoMinor: 0, Header: http.Header{ "A": []string{"foo"}, "B": []string{"bar"}, }, Body: io.NopCloser(bytes.NewBufferString(body)), } } func inBQ(s string) string { return "`" + s + "`" } func TestDumpResponse(t *testing.T) { tb := test.NewTestingTB("TestDumpResponse") internal.DumpResponse(tb, newResponse("one-line")) td.Cmp(t, tb.LastMessage(), `Received response: `+inBQ(`HTTP/1.0 200 OK A: foo B: bar one-line`)) tb.ResetMessages() internal.DumpResponse(tb, newResponse("multi\r\nlines\r\nand\ttabs héhé")) td.Cmp(t, tb.LastMessage(), `Received response: `+inBQ(`HTTP/1.0 200 OK A: foo B: bar multi lines `+"and\ttabs héhé")) tb.ResetMessages() internal.DumpResponse(tb, newResponse("multi\nlines\nand\ttabs héhé")) td.Cmp(t, tb.LastMessage(), `Received response: `+inBQ(`HTTP/1.0 200 OK A: foo B: bar multi lines `+"and\ttabs héhé")) // one \r more in body tb.ResetMessages() internal.DumpResponse(tb, newResponse("multi\r\nline\r")) td.Cmp(t, tb.LastMessage(), `Received response: "HTTP/1.0 200 OK\r\nA: foo\r\nB: bar\r\n\r\nmulti\r\nline\r"`) // BOM tb.ResetMessages() internal.DumpResponse(tb, newResponse("\ufeff")) td.Cmp(t, tb.LastMessage(), `Received response: "HTTP/1.0 200 OK\r\nA: foo\r\nB: bar\r\n\r\n\ufeff"`) // Rune error tb.ResetMessages() internal.DumpResponse(tb, newResponse("\xf4\x9f\xbf\xbf")) td.Cmp(t, tb.LastMessage(), `Received response: "HTTP/1.0 200 OK\r\nA: foo\r\nB: bar\r\n\r\n\xf4\x9f\xbf\xbf"`) // ` tb.ResetMessages() internal.DumpResponse(tb, newResponse("he`o")) td.Cmp(t, tb.LastMessage(), `Received response: "HTTP/1.0 200 OK\r\nA: foo\r\nB: bar\r\n\r\nhe`+"`"+`o"`) // 0x7f tb.ResetMessages() internal.DumpResponse(tb, newResponse("\x7f")) td.Cmp(t, tb.LastMessage(), td.Re(`Received response: "HTTP/1.0 200 OK\\r\\nA: foo\\r\\nB: bar\\r\\n\\r\\n(\\u007f|\\x7f)"`)) } golang-github-maxatome-go-testdeep-1.14.0/helpers/tdhttp/multipart.go000066400000000000000000000132341454313311600257000ustar00rootroot00000000000000// Copyright (c) 2021, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package tdhttp import ( "bytes" "io" "net/http" "os" "path/filepath" "strings" ) const ( defaultMediaType = "multipart/form-data" defaultBoundary = "go-testdeep-42" ) // MultipartBody is a body of a multipart/form-data HTTP request (by // default, or any other multipart/… body, see MediaType field) as // defined in [RFC 2046] to be used as a [io.Reader] body of // [http.Request] and so compliant with [RFC 2388]. It implements // [io.Reader] and can only be read once. See [PostMultipartFormData] // and [TestAPI.PostMultipartFormData] for examples of use. // // [RFC 2046]: https://tools.ietf.org/html/rfc2046 // [RFC 2388]: https://tools.ietf.org/html/rfc2388 type MultipartBody struct { MediaType string // type to use instead of default "multipart/form-data" Boundary string // boundary to use between parts. Automatically initialized when calling ContentType(). Parts []*MultipartPart // parts composing this multipart/… body. content io.Reader } // Read implements [io.Reader] interface. func (b *MultipartBody) Read(p []byte) (n int, err error) { if b.content == nil { if b.Boundary == "" { b.Boundary = defaultBoundary } between := []byte("\r\n--" + b.Boundary + "\r\n") first := between[2:] end := []byte("\r\n--" + b.Boundary + "--\r\n") readers := make([]io.Reader, 0, len(b.Parts)*2+3) readers = append(readers, bytes.NewReader(first)) for i, part := range b.Parts { if i > 0 { readers = append(readers, bytes.NewReader(between)) } readers = append(readers, part) } readers = append(readers, bytes.NewReader(end)) b.content = io.MultiReader(readers...) } return b.content.Read(p) } // ContentType returns the Content-Type header to use. As it contains // the boundary, it is initialized first if it is still empty. By // default the media type is multipart/form-data but it can be // overridden using the MediaType field. // // m.MediaType = "multipart/mixed" // ct := m.ContentType() func (b *MultipartBody) ContentType() string { mt := b.MediaType if mt == "" { mt = defaultMediaType } if b.Boundary == "" { b.Boundary = defaultBoundary } return mt + `; boundary="` + b.Boundary + `"` } // MultipartPart is a part in a [MultipartBody] body. It implements io.Reader // and can only be read once. type MultipartPart struct { Name string // is "name" in Content-Disposition. If empty, Content-Disposition header is omitted. Filename string // is optional. If set it is "filename" in Content-Disposition. Content io.Reader // is the body section of the part. Header http.Header // is the header of the part and is optional. It is automatically initialized when needed. headerDone bool } // NewMultipartPart returns a new [MultipartPart] based on body // content. If body is nil, it means there is no body at all. func NewMultipartPart(name string, body io.Reader, contentType ...string) *MultipartPart { p := MultipartPart{ Name: name, Content: body, } if len(contentType) > 0 { p.Header = http.Header{"Content-Type": contentType[:1]} } return &p } // NewMultipartPartFile returns a new [MultipartPart] based on // filePath content. If filePath cannot be opened, an error is // returned on first Read() call. func NewMultipartPartFile(name string, filePath string, contentType ...string) *MultipartPart { p := NewMultipartPart(name, &fileReader{filePath: filePath}, contentType...) p.Filename = filepath.Base(filePath) return p } // NewMultipartPartString returns a new [MultipartPart] based on body content. func NewMultipartPartString(name string, body string, contentType ...string) *MultipartPart { return NewMultipartPart(name, strings.NewReader(body), contentType...) } // NewMultipartPartBytes returns a new [MultipartPart] based on body content. func NewMultipartPartBytes(name string, body []byte, contentType ...string) *MultipartPart { return NewMultipartPart(name, bytes.NewReader(body), contentType...) } // Read implements [io.Reader] interface. func (p *MultipartPart) Read(b []byte) (n int, err error) { if !p.headerDone { // Header not yet computed if p.Header == nil { p.Header = http.Header{} } if p.Name != "" && p.Header.Get("Content-Disposition") == "" { val := `form-data; name="` + p.Name + `"` if p.Filename != "" { val += `; filename="` + p.Filename + `"` } p.Header.Set("Content-Disposition", val) } readers := make([]io.Reader, 1, 3) if p.Content != nil { if p.Header.Get("Content-Type") == "" { var head bytes.Buffer copied, err := io.CopyN(&head, p.Content, 512) if err != nil && err != io.EOF { return 0, err } if copied > 0 { p.Header.Set("Content-Type", http.DetectContentType(head.Bytes())) readers = append(readers, &head, p.Content) } } else { readers = append(readers, p.Content) } } var header bytes.Buffer p.Header.Write(&header) //nolint: errcheck if len(readers) > 1 { header.WriteString("\r\n") } readers[0] = &header p.Content = io.MultiReader(readers...) p.headerDone = true } return p.Content.Read(b) } type fileReader struct { filePath string file *os.File err error } func (f *fileReader) Read(b []byte) (n int, err error) { if f.err != nil { return 0, f.err } if f.file == nil { file, err := os.Open(f.filePath) if err != nil { f.err = err return 0, err } f.file = file } n, err = f.file.Read(b) if err != nil { // At EOF, (*os.File).Read() returns 0, io.EOF f.err = err f.file.Close() f.file = nil } return } golang-github-maxatome-go-testdeep-1.14.0/helpers/tdhttp/multipart_test.go000066400000000000000000000173201454313311600267370ustar00rootroot00000000000000// Copyright (c) 2021, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package tdhttp_test import ( "bytes" "io" "mime/multipart" "net/http" "os" "path/filepath" "strings" "testing" "github.com/maxatome/go-testdeep/helpers/tdhttp" "github.com/maxatome/go-testdeep/td" ) func TestMultipartPart(t *testing.T) { assert, require := td.AssertRequire(t) check := func(part *tdhttp.MultipartPart, expected string) { t.Helper() var final bytes.Buffer // Read in 2 times to be sure Read() can be called several times _, err := io.CopyN(&final, part, 5) if assert.CmpNoError(err) { _, err := io.Copy(&final, part) if assert.CmpNoError(err) { assert.Cmp(final.String(), strings.ReplaceAll(expected, "%CR", "\r")) } } } // Full empty b, err := io.ReadAll(&tdhttp.MultipartPart{}) assert.CmpNoError(err) assert.Len(b, 0) // Without name part := tdhttp.MultipartPart{ Content: strings.NewReader("hey!\nyo!"), } check(&part, `Content-Type: text/plain; charset=utf-8%CR %CR hey! yo!`) // Without body part = tdhttp.MultipartPart{ Name: "nobody", } check(&part, `Content-Disposition: form-data; name="nobody"%CR `) // Without header part = tdhttp.MultipartPart{ Name: "pipo", Content: strings.NewReader("hey!\nyo!"), } check(&part, `Content-Disposition: form-data; name="pipo"%CR Content-Type: text/plain; charset=utf-8%CR %CR hey! yo!`) // With header part = tdhttp.MultipartPart{ Name: "pipo", Content: strings.NewReader("hey!\nyo!"), Header: http.Header{ "Pipo": []string{"bingo"}, "Content-Type": []string{"text/rococo; charset=utf-8"}, }, } check(&part, `Content-Disposition: form-data; name="pipo"%CR Content-Type: text/rococo; charset=utf-8%CR Pipo: bingo%CR %CR hey! yo!`) // Without name & body, but with header part = tdhttp.MultipartPart{ Header: http.Header{ "Pipo": []string{"bingo"}, }, } check(&part, `Pipo: bingo%CR `) // io.Reader check(tdhttp.NewMultipartPart("io", strings.NewReader("hey!\nyo!")), `Content-Disposition: form-data; name="io"%CR Content-Type: text/plain; charset=utf-8%CR %CR hey! yo!`) // io.Reader + Content-Type check(tdhttp.NewMultipartPart("io", strings.NewReader("hey!\nyo!"), "text/rococo; charset=utf-8"), `Content-Disposition: form-data; name="io"%CR Content-Type: text/rococo; charset=utf-8%CR %CR hey! yo!`) // String check(tdhttp.NewMultipartPartString("pipo", "hey!\nyo!"), `Content-Disposition: form-data; name="pipo"%CR Content-Type: text/plain; charset=utf-8%CR %CR hey! yo!`) // String + Content-Type check(tdhttp.NewMultipartPartString("pipo", "hey!\nyo!", "text/rococo; charset=utf-8"), `Content-Disposition: form-data; name="pipo"%CR Content-Type: text/rococo; charset=utf-8%CR %CR hey! yo!`) // Bytes check(tdhttp.NewMultipartPartBytes("pipo", []byte("hey!\nyo!")), `Content-Disposition: form-data; name="pipo"%CR Content-Type: text/plain; charset=utf-8%CR %CR hey! yo!`) // Bytes + Content-Type check(tdhttp.NewMultipartPartBytes("pipo", []byte("hey!\nyo!"), "text/rococo; charset=utf-8"), `Content-Disposition: form-data; name="pipo"%CR Content-Type: text/rococo; charset=utf-8%CR %CR hey! yo!`) // With file name dir, err := os.MkdirTemp("", "multipart") require.CmpNoError(err) defer os.RemoveAll(dir) filePath := filepath.Join(dir, "body.txt") require.CmpNoError(os.WriteFile(filePath, []byte("hey!\nyo!"), 0666)) check(tdhttp.NewMultipartPartFile("pipo", filePath), `Content-Disposition: form-data; name="pipo"; filename="body.txt"%CR Content-Type: text/plain; charset=utf-8%CR %CR hey! yo!`) // With file name + Content-Type check(tdhttp.NewMultipartPartFile("pipo", filePath, "text/rococo; charset=utf-8"), `Content-Disposition: form-data; name="pipo"; filename="body.txt"%CR Content-Type: text/rococo; charset=utf-8%CR %CR hey! yo!`) // Error during os.Open _, err = io.ReadAll( tdhttp.NewMultipartPartFile("pipo", filepath.Join(dir, "unknown.xxx")), ) assert.CmpError(err) } func TestMultipartBody(t *testing.T) { assert, require := td.AssertRequire(t) dir, err := os.MkdirTemp("", "multipart") require.CmpNoError(err) defer os.RemoveAll(dir) filePath := filepath.Join(dir, "body.txt") require.CmpNoError(os.WriteFile(filePath, []byte("hey!\nyo!"), 0666)) for _, boundary := range []struct{ in, out string }{ {in: "", out: "go-testdeep-42"}, {in: "BoUnDaRy", out: "BoUnDaRy"}, } { multi := tdhttp.MultipartBody{ Boundary: boundary.in, Parts: []*tdhttp.MultipartPart{ { Name: "pipo", Content: strings.NewReader("pipo!\nbingo!"), }, tdhttp.NewMultipartPartFile("file", filePath), tdhttp.NewMultipartPartString("string", "zip!\nzap!"), tdhttp.NewMultipartPartBytes("bytes", []byte(`{"ola":"hello"}`), "application/json"), tdhttp.NewMultipartPart("io", nil), tdhttp.NewMultipartPart("", nil), }, } expected := `--` + boundary.out + `%CR Content-Disposition: form-data; name="pipo"%CR Content-Type: text/plain; charset=utf-8%CR %CR pipo! bingo!%CR --` + boundary.out + `%CR Content-Disposition: form-data; name="file"; filename="body.txt"%CR Content-Type: text/plain; charset=utf-8%CR %CR hey! yo!%CR --` + boundary.out + `%CR Content-Disposition: form-data; name="string"%CR Content-Type: text/plain; charset=utf-8%CR %CR zip! zap!%CR --` + boundary.out + `%CR Content-Disposition: form-data; name="bytes"%CR Content-Type: application/json%CR %CR {"ola":"hello"}%CR --` + boundary.out + `%CR Content-Disposition: form-data; name="io"%CR %CR --` + boundary.out + `%CR %CR --` + boundary.out + `--%CR ` var final bytes.Buffer // Read in 2 times to be sure Read() can be called several times _, err = io.CopyN(&final, &multi, 10) if !assert.CmpNoError(err) { continue } _, err := io.Copy(&final, &multi) if !assert.CmpNoError(err) { continue } if !assert.Cmp(final.String(), strings.ReplaceAll(expected, "%CR", "\r")) { continue } rd := multipart.NewReader(&final, boundary.out) // 0 part, err := rd.NextPart() if assert.CmpNoError(err) { assert.Cmp(part.FormName(), "pipo") assert.Cmp(part.FileName(), "") assert.Smuggle(part, io.ReadAll, td.String("pipo!\nbingo!")) } // 1 part, err = rd.NextPart() if assert.CmpNoError(err) { assert.Cmp(part.FormName(), "file") assert.Cmp(part.FileName(), "body.txt") assert.Smuggle(part, io.ReadAll, td.String("hey!\nyo!")) } // 2 part, err = rd.NextPart() if assert.CmpNoError(err) { assert.Cmp(part.FormName(), "string") assert.Cmp(part.FileName(), "") assert.Smuggle(part, io.ReadAll, td.String("zip!\nzap!")) } // 3 part, err = rd.NextPart() if assert.CmpNoError(err) { assert.Cmp(part.FormName(), "bytes") assert.Cmp(part.FileName(), "") assert.Smuggle(part, io.ReadAll, td.String(`{"ola":"hello"}`)) } // 4 part, err = rd.NextPart() if assert.CmpNoError(err) { assert.Cmp(part.FormName(), "io") assert.Cmp(part.FileName(), "") assert.Smuggle(part, io.ReadAll, td.String("")) } // 5 part, err = rd.NextPart() if assert.CmpNoError(err) { assert.Cmp(part.FormName(), "") assert.Cmp(part.FileName(), "") assert.Smuggle(part, io.ReadAll, td.String("")) } // EOF _, err = rd.NextPart() assert.Cmp(err, io.EOF) } multi := tdhttp.MultipartBody{} td.Cmp(t, multi.ContentType(), `multipart/form-data; boundary="go-testdeep-42"`) td.Cmp(t, multi.Boundary, "go-testdeep-42", "Boundary field set with default value") td.CmpEmpty(t, multi.MediaType, "MediaType field NOT set") multi.Boundary = "BoUnDaRy" td.Cmp(t, multi.ContentType(), `multipart/form-data; boundary="BoUnDaRy"`) multi.MediaType = "multipart/mixed" td.Cmp(t, multi.ContentType(), `multipart/mixed; boundary="BoUnDaRy"`) } golang-github-maxatome-go-testdeep-1.14.0/helpers/tdhttp/q.go000066400000000000000000000060351454313311600241200ustar00rootroot00000000000000// Copyright (c) 2021, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package tdhttp import ( "errors" "fmt" "net/url" "reflect" "strconv" "github.com/maxatome/go-testdeep/internal/color" ) // Q allows to easily declare query parameters for use in [NewRequest] // and related [http.Request] builders, as [Get] for example: // // req := tdhttp.Get("/path", tdhttp.Q{ // "id": []int64{1234, 4567}, // "dryrun": true, // }) // // See [NewRequest] for several examples of use. // // Accepted types as values are: // - [fmt.Stringer] // - string // - int, int8, int16, int32, int64 // - uint, uint8, uint16, uint32, uint64 // - float32, float64 // - bool // - slice or array of any type above, plus any // - pointer on any type above, plus any or any other pointer type Q map[string]any var _ URLValuesEncoder = Q(nil) // AddTo adds the q contents to qp. func (q Q) AddTo(qp url.Values) error { for param, value := range q { // Ignore nil values if value == nil { continue } err := q.addParamTo(param, reflect.ValueOf(value), true, qp) if err != nil { return err } } return nil } // Values returns a [url.Values] instance corresponding to q. It panics // if a value cannot be converted. func (q Q) Values() url.Values { qp := make(url.Values, len(q)) err := q.AddTo(qp) if err != nil { panic(errors.New(color.Bad(err.Error()))) } return qp } // Encode does the same as [url.Values.Encode] does. So quoting its doc, // it encodes the values into “URL encoded” form ("bar=baz&foo=quux") // sorted by key. // // It panics if a value cannot be converted. func (q Q) Encode() string { return q.Values().Encode() } func (q Q) addParamTo(param string, v reflect.Value, allowArray bool, qp url.Values) error { var str string for { if s, ok := v.Interface().(fmt.Stringer); ok { qp.Add(param, s.String()) return nil } switch v.Kind() { case reflect.Slice, reflect.Array: if !allowArray { return fmt.Errorf("%s is only allowed at the root level for param %q", v.Kind(), param) } for i, l := 0, v.Len(); i < l; i++ { err := q.addParamTo(param, v.Index(i), false, qp) if err != nil { return err } } return nil case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: str = strconv.FormatInt(v.Int(), 10) case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: str = strconv.FormatUint(v.Uint(), 10) case reflect.Float32, reflect.Float64: str = strconv.FormatFloat(v.Float(), 'g', -1, 64) case reflect.String: str = v.String() case reflect.Bool: str = strconv.FormatBool(v.Bool()) case reflect.Ptr, reflect.Interface: if !v.IsNil() { v = v.Elem() continue } return nil // mimic url.Values behavior ⇒ ignore default: return fmt.Errorf("don't know how to add type %s (%s) to param %q", v.Type(), v.Kind(), param) } qp.Add(param, str) return nil } } golang-github-maxatome-go-testdeep-1.14.0/helpers/tdhttp/q_test.go000066400000000000000000000062761454313311600251660ustar00rootroot00000000000000// Copyright (c) 2021, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package tdhttp_test import ( "net/url" "testing" "github.com/maxatome/go-testdeep/helpers/tdhttp" "github.com/maxatome/go-testdeep/td" ) type qTest1 struct{} func (qTest1) String() string { return "qTest1!" } type qTest2 struct{} func (*qTest2) String() string { return "qTest2!" } func TestQ(t *testing.T) { q := tdhttp.Q{ "str1": "v1", "str2": []string{"v20", "v21"}, "int1": 1234, "int2": []int{1, 2, 3}, "uint1": uint(1234), "uint2": [3]uint{1, 2, 3}, "float1": 1.2, "float2": []float64{1.2, 3.4}, "bool1": true, "bool2": [2]bool{true, false}, } td.Cmp(t, q.Values(), url.Values{ "str1": []string{"v1"}, "str2": []string{"v20", "v21"}, "int1": []string{"1234"}, "int2": []string{"1", "2", "3"}, "uint1": []string{"1234"}, "uint2": []string{"1", "2", "3"}, "float1": []string{"1.2"}, "float2": []string{"1.2", "3.4"}, "bool1": []string{"true"}, "bool2": []string{"true", "false"}, }) // Auto deref pointers num := 123 pnum := &num ppnum := &pnum q = tdhttp.Q{ "pnum": pnum, "ppnum": ppnum, "pppnum": &ppnum, "slice": []***int{&ppnum, &ppnum}, "pslice": &[]***int{&ppnum, &ppnum}, "array": [2]***int{&ppnum, &ppnum}, "parray": &[2]***int{&ppnum, &ppnum}, } td.Cmp(t, q.Values(), url.Values{ "pnum": []string{"123"}, "ppnum": []string{"123"}, "pppnum": []string{"123"}, "slice": []string{"123", "123"}, "pslice": []string{"123", "123"}, "array": []string{"123", "123"}, "parray": []string{"123", "123"}, }) // Auto deref interfaces q = tdhttp.Q{ "all": []any{ "string", -1, int8(-2), int16(-3), int32(-4), int64(-5), uint(1), uint8(2), uint16(3), uint32(4), uint64(5), float32(6), float64(7), true, ppnum, (*int)(nil), // ignored nil, // ignored qTest1{}, &qTest1{}, // does not implement fmt.Stringer, but qTest does // qTest2{} panics as it does not implement fmt.Stringer, see Errors below &qTest2{}, }, } td.Cmp(t, q.Values(), url.Values{ "all": []string{ "string", "-1", "-2", "-3", "-4", "-5", "1", "2", "3", "4", "5", "6", "7", "true", "123", "qTest1!", "qTest1!", "qTest2!", }, }) // nil case pnum = nil q = tdhttp.Q{ "nil1": &ppnum, "nil2": (*int)(nil), "nil3": nil, "nil4": []*int{nil, nil}, "nil5": ([]int)(nil), "nil6": []any{nil, nil}, } td.Cmp(t, q.Values(), url.Values{}) q = tdhttp.Q{ "id": []int{12, 34}, "draft": true, } td.Cmp(t, q.Encode(), "draft=true&id=12&id=34") // Errors td.CmpPanic(t, func() { (tdhttp.Q{"panic": map[string]bool{}}).Values() }, td.Contains(`don't know how to add type map[string]bool (map) to param "panic"`)) td.CmpPanic(t, func() { (tdhttp.Q{"panic": qTest2{}}).Values() }, td.Contains(`don't know how to add type tdhttp_test.qTest2 (struct) to param "panic"`)) td.CmpPanic(t, func() { (tdhttp.Q{"panic": []any{[]int{}}}).Values() }, td.Contains(`slice is only allowed at the root level for param "panic"`)) } golang-github-maxatome-go-testdeep-1.14.0/helpers/tdhttp/request.go000066400000000000000000000472161454313311600253560ustar00rootroot00000000000000// Copyright (c) 2019-2022, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package tdhttp import ( "bytes" "encoding/base64" "encoding/json" "encoding/xml" "errors" "io" "net/http" "net/http/httptest" "net/url" "strings" "github.com/maxatome/go-testdeep/internal/color" "github.com/maxatome/go-testdeep/internal/flat" "github.com/maxatome/go-testdeep/internal/types" ) func newRequest(method string, target string, body io.Reader, headersQueryParams []any) (*http.Request, error) { header := http.Header{} qp := url.Values{} var cookies []*http.Cookie headersQueryParams = flat.Interfaces(headersQueryParams...) for i := 0; i < len(headersQueryParams); i++ { switch cur := headersQueryParams[i].(type) { case string: i++ var val string if i < len(headersQueryParams) { var ok bool if val, ok = headersQueryParams[i].(string); !ok { return nil, errors.New(color.Bad( `header "%s" should have a string value, not a %T (@ headersQueryParams[%d])`, cur, headersQueryParams[i], i)) } } header.Add(cur, val) case http.Header: for k, v := range cur { k = http.CanonicalHeaderKey(k) header[k] = append(header[k], v...) } case *http.Cookie: cookies = append(cookies, cur) case http.Cookie: cookies = append(cookies, &cur) case url.Values: for k, v := range cur { qp[k] = append(qp[k], v...) } case Q: err := cur.AddTo(qp) if err != nil { return nil, errors.New(color.Bad( "headersQueryParams... tdhttp.Q bad parameter: %s (@ headersQueryParams[%d])", err, i)) } default: return nil, errors.New(color.Bad( "headersQueryParams... can only contains string, http.Header, http.Cookie, url.Values and tdhttp.Q, not %T (@ headersQueryParams[%d])", cur, i)) } } // Parse path even when no query params to have consistent error // messages when using query params or not u, err := url.Parse(target) if err != nil { return nil, errors.New(color.Bad("target is not a valid path: %s", err)) } if len(qp) > 0 { if u.RawQuery != "" { u.RawQuery += "&" } u.RawQuery += qp.Encode() target = u.String() } req := httptest.NewRequest(method, target, body) for k, v := range header { req.Header[k] = append(req.Header[k], v...) } for _, c := range cookies { req.AddCookie(c) } return req, nil } // BasicAuthHeader returns a new [http.Header] with only Authorization // key set, compliant with HTTP Basic Authentication using user and // password. It is provided as a facility to build request in one // line: // // ta.Get("/path", tdhttp.BasicAuthHeader("max", "5ecr3T")) // // instead of: // // req := tdhttp.Get("/path") // req.SetBasicAuth("max", "5ecr3T") // ta.Request(req) // // See [http.Request.SetBasicAuth] for details. func BasicAuthHeader(user, password string) http.Header { return http.Header{ "Authorization": []string{ "Basic " + base64.StdEncoding.EncodeToString([]byte(user+":"+password)), }, } } func get(target string, headersQueryParams ...any) (*http.Request, error) { return newRequest(http.MethodGet, target, nil, headersQueryParams) } func head(target string, headersQueryParams ...any) (*http.Request, error) { return newRequest(http.MethodHead, target, nil, headersQueryParams) } func options(target string, body io.Reader, headersQueryParams ...any) (*http.Request, error) { return newRequest(http.MethodOptions, target, body, headersQueryParams) } func post(target string, body io.Reader, headersQueryParams ...any) (*http.Request, error) { return newRequest(http.MethodPost, target, body, headersQueryParams) } func postForm(target string, data URLValuesEncoder, headersQueryParams ...any) (*http.Request, error) { var body string if data != nil { body = data.Encode() } return newRequest( http.MethodPost, target, strings.NewReader(body), append(headersQueryParams, "Content-Type", "application/x-www-form-urlencoded"), ) } func postMultipartFormData(target string, data *MultipartBody, headersQueryParams ...any) (*http.Request, error) { return newRequest( http.MethodPost, target, data, append(headersQueryParams, "Content-Type", data.ContentType()), ) } func put(target string, body io.Reader, headersQueryParams ...any) (*http.Request, error) { return newRequest(http.MethodPut, target, body, headersQueryParams) } func patch(target string, body io.Reader, headersQueryParams ...any) (*http.Request, error) { return newRequest(http.MethodPatch, target, body, headersQueryParams) } func del(target string, body io.Reader, headersQueryParams ...any) (*http.Request, error) { return newRequest(http.MethodDelete, target, body, headersQueryParams) } // NewRequest creates a new HTTP request as [httptest.NewRequest] // does, with the ability to immediately add some headers and/or some // query parameters. // // Headers can be added using string pairs as in: // // req := tdhttp.NewRequest("POST", "/pdf", body, // "Content-type", "application/pdf", // "X-Test", "value", // ) // // or using [http.Header] as in: // // req := tdhttp.NewRequest("POST", "/pdf", body, // http.Header{"Content-type": []string{"application/pdf"}}, // ) // // or using [BasicAuthHeader] as in: // // req := tdhttp.NewRequest("POST", "/pdf", body, // tdhttp.BasicAuthHeader("max", "5ecr3T"), // ) // // or using [http.Cookie] (pointer or not, behind the scene, // [http.Request.AddCookie] is used) as in: // // req := tdhttp.NewRequest("POST", "/pdf", body, // http.Cookie{Name: "cook1", Value: "val1"}, // &http.Cookie{Name: "cook2", Value: "val2"}, // ) // // Several header sources are combined: // // req := tdhttp.NewRequest("POST", "/pdf", body, // "Content-type", "application/pdf", // http.Header{"X-Test": []string{"value1"}}, // "X-Test", "value2", // http.Cookie{Name: "cook1", Value: "val1"}, // tdhttp.BasicAuthHeader("max", "5ecr3T"), // &http.Cookie{Name: "cook2", Value: "val2"}, // ) // // Produces the following [http.Header]: // // http.Header{ // "Authorization": []string{"Basic bWF4OjVlY3IzVA=="}, // "Content-type": []string{"application/pdf"}, // "Cookie": []string{"cook1=val1; cook2=val2"}, // "X-Test": []string{"value1", "value2"}, // } // // A string slice or a map can be flatened as well. As [NewRequest] expects // ...any, [td.Flatten] can help here too: // // strHeaders := map[string]string{ // "X-Length": "666", // "X-Foo": "bar", // } // req := tdhttp.NewRequest("POST", "/pdf", body, td.Flatten(strHeaders)) // // Or combined with forms seen above: // // req := tdhttp.NewRequest("POST", "/pdf", body, // "Content-type", "application/pdf", // http.Header{"X-Test": []string{"value1"}}, // td.Flatten(strHeaders), // "X-Test", "value2", // http.Cookie{Name: "cook1", Value: "val1"}, // tdhttp.BasicAuthHeader("max", "5ecr3T"), // &http.Cookie{Name: "cook2", Value: "val2"}, // ) // // Header keys are always canonicalized using [http.CanonicalHeaderKey]. // // Query parameters can be added using [url.Values] or more flexible // [Q], as in: // // req := tdhttp.NewRequest("GET", "/pdf", // url.Values{ // "param": {"val"}, // "names": {"bob", "alice"}, // }, // "X-Test": "a header in the middle", // tdhttp.Q{ // "limit": 20, // "ids": []int64{456, 789}, // "details": true, // }, // ) // // All [url.Values] and [Q] instances are combined to produce the // final query string to use. The previous example produces the // following target: // // /pdf?details=true&ids=456&ids=789&limit=20&names=bob&names=alice¶m=val // // If target already contains a query string, it is reused: // // req := tdhttp.NewRequest("GET", "/pdf?limit=10", tdhttp.Q{"details": true}) // // produces the following target: // // /path?details=true&limit=10 // // Behind the scene, [url.Values.Encode] is used, so the parameters // are always sorted by key. If you want a specific order, then do not // use [url.Values] nor [Q] instances, but compose target by yourself. // // See [Q] documentation to learn how values are stringified. func NewRequest(method, target string, body io.Reader, headersQueryParams ...any) *http.Request { req, err := newRequest(method, target, body, headersQueryParams) if err != nil { panic(err) } return req } // Get creates a new HTTP GET. It is a shortcut for: // // tdhttp.NewRequest(http.MethodGet, target, nil, headersQueryParams...) // // See [NewRequest] for all possible formats accepted in headersQueryParams. func Get(target string, headersQueryParams ...any) *http.Request { req, err := get(target, headersQueryParams...) if err != nil { panic(err) } return req } // Head creates a new HTTP HEAD. It is a shortcut for: // // tdhttp.NewRequest(http.MethodHead, target, nil, headersQueryParams...) // // See [NewRequest] for all possible formats accepted in headersQueryParams. func Head(target string, headersQueryParams ...any) *http.Request { req, err := head(target, headersQueryParams...) if err != nil { panic(err) } return req } // Options creates a HTTP OPTIONS. It is a shortcut for: // // tdhttp.NewRequest(http.MethodOptions, target, body, headersQueryParams...) // // See [NewRequest] for all possible formats accepted in headersQueryParams. func Options(target string, body io.Reader, headersQueryParams ...any) *http.Request { req, err := options(target, body, headersQueryParams...) if err != nil { panic(err) } return req } // Post creates a HTTP POST. It is a shortcut for: // // tdhttp.NewRequest(http.MethodPost, target, body, headersQueryParams...) // // See [NewRequest] for all possible formats accepted in headersQueryParams. func Post(target string, body io.Reader, headersQueryParams ...any) *http.Request { req, err := post(target, body, headersQueryParams...) if err != nil { panic(err) } return req } // URLValuesEncoder is an interface [PostForm] and [TestAPI.PostForm] data // must implement. // Encode can be called to generate a "URL encoded" form such as // ("bar=baz&foo=quux") sorted by key. // // [url.Values] and [Q] implement this interface. type URLValuesEncoder interface { Encode() string } // PostForm creates a HTTP POST with data's keys and values // URL-encoded as the request body. "Content-Type" header is // automatically set to "application/x-www-form-urlencoded". Other // headers can be added via headersQueryParams, as in: // // req := tdhttp.PostForm("/data", // url.Values{ // "param1": []string{"val1", "val2"}, // "param2": []string{"zip"}, // }, // "X-Foo", "Foo-value", // "X-Zip", "Zip-value", // ) // // See [NewRequest] for all possible formats accepted in headersQueryParams. func PostForm(target string, data URLValuesEncoder, headersQueryParams ...any) *http.Request { req, err := postForm(target, data, headersQueryParams...) if err != nil { panic(err) } return req } // PostMultipartFormData creates a HTTP POST multipart request, like // multipart/form-data one for example. See [MultipartBody] type for // details. "Content-Type" header is automatically set depending on // data.MediaType (defaults to "multipart/form-data") and data.Boundary // (defaults to "go-testdeep-42"). Other headers can be added via // headersQueryParams, as in: // // req := tdhttp.PostMultipartFormData("/data", // &tdhttp.MultipartBody{ // // "multipart/form-data" by default // Parts: []*tdhttp.MultipartPart{ // tdhttp.NewMultipartPartString("type", "Sales"), // tdhttp.NewMultipartPartFile("report", "report.json", "application/json"), // }, // }, // "X-Foo", "Foo-value", // "X-Zip", "Zip-value", // ) // // and with a different media type: // // req := tdhttp.PostMultipartFormData("/data", // &tdhttp.MultipartBody{ // MediaType: "multipart/mixed", // Parts: []*tdhttp.MultipartPart{ // tdhttp.NewMultipartPartString("type", "Sales"), // tdhttp.NewMultipartPartFile("report", "report.json", "application/json"), // }, // }, // "X-Foo", "Foo-value", // "X-Zip", "Zip-value", // ) // // See [NewRequest] for all possible formats accepted in headersQueryParams. func PostMultipartFormData(target string, data *MultipartBody, headersQueryParams ...any) *http.Request { req, err := postMultipartFormData(target, data, headersQueryParams...) if err != nil { panic(err) } return req } // Put creates a HTTP PUT. It is a shortcut for: // // tdhttp.NewRequest(http.MethodPut, target, body, headersQueryParams...) // // See [NewRequest] for all possible formats accepted in headersQueryParams. func Put(target string, body io.Reader, headersQueryParams ...any) *http.Request { req, err := put(target, body, headersQueryParams...) if err != nil { panic(err) } return req } // Patch creates a HTTP PATCH. It is a shortcut for: // // tdhttp.NewRequest(http.MethodPatch, target, body, headersQueryParams...) // // See [NewRequest] for all possible formats accepted in headersQueryParams. func Patch(target string, body io.Reader, headersQueryParams ...any) *http.Request { req, err := patch(target, body, headersQueryParams...) if err != nil { panic(err) } return req } // Delete creates a HTTP DELETE. It is a shortcut for: // // tdhttp.NewRequest(http.MethodDelete, target, body, headersQueryParams...) // // See [NewRequest] for all possible formats accepted in headersQueryParams. func Delete(target string, body io.Reader, headersQueryParams ...any) *http.Request { req, err := del(target, body, headersQueryParams...) if err != nil { panic(err) } return req } func newJSONRequest(method, target string, body any, headersQueryParams ...any) (*http.Request, error) { b, err := json.Marshal(body) if err != nil { if opErr, ok := types.AsOperatorNotJSONMarshallableError(err); ok { var plus string switch op := opErr.Operator(); op { case "JSON", "SubJSONOf", "SuperJSONOf": plus = ", use json.RawMessage() instead" } return nil, errors.New(color.Bad("JSON encoding failed: %s%s", err, plus)) } return nil, errors.New(color.Bad("%s", err)) } return newRequest( method, target, bytes.NewReader(b), append(headersQueryParams, "Content-Type", "application/json"), ) } // NewJSONRequest creates a new HTTP request with body marshaled to // JSON. "Content-Type" header is automatically set to // "application/json". Other headers can be added via headersQueryParams, as in: // // req := tdhttp.NewJSONRequest("POST", "/data", body, // "X-Foo", "Foo-value", // "X-Zip", "Zip-value", // ) // // See [NewRequest] for all possible formats accepted in headersQueryParams. func NewJSONRequest(method, target string, body any, headersQueryParams ...any) *http.Request { req, err := newJSONRequest(method, target, body, headersQueryParams...) if err != nil { panic(err) } return req } // PostJSON creates a HTTP POST with body marshaled to // JSON. "Content-Type" header is automatically set to // "application/json". It is a shortcut for: // // tdhttp.NewJSONRequest(http.MethodPost, target, body, headersQueryParams...) // // See [NewRequest] for all possible formats accepted in headersQueryParams. func PostJSON(target string, body any, headersQueryParams ...any) *http.Request { req, err := newJSONRequest(http.MethodPost, target, body, headersQueryParams...) if err != nil { panic(err) } return req } // PutJSON creates a HTTP PUT with body marshaled to // JSON. "Content-Type" header is automatically set to // "application/json". It is a shortcut for: // // tdhttp.NewJSONRequest(http.MethodPut, target, body, headersQueryParams...) // // See [NewRequest] for all possible formats accepted in headersQueryParams. func PutJSON(target string, body any, headersQueryParams ...any) *http.Request { req, err := newJSONRequest(http.MethodPut, target, body, headersQueryParams...) if err != nil { panic(err) } return req } // PatchJSON creates a HTTP PATCH with body marshaled to // JSON. "Content-Type" header is automatically set to // "application/json". It is a shortcut for: // // tdhttp.NewJSONRequest(http.MethodPatch, target, body, headersQueryParams...) // // See [NewRequest] for all possible formats accepted in headersQueryParams. func PatchJSON(target string, body any, headersQueryParams ...any) *http.Request { req, err := newJSONRequest(http.MethodPatch, target, body, headersQueryParams...) if err != nil { panic(err) } return req } // DeleteJSON creates a HTTP DELETE with body marshaled to // JSON. "Content-Type" header is automatically set to // "application/json". It is a shortcut for: // // tdhttp.NewJSONRequest(http.MethodDelete, target, body, headersQueryParams...) // // See [NewRequest] for all possible formats accepted in headersQueryParams. func DeleteJSON(target string, body any, headersQueryParams ...any) *http.Request { req, err := newJSONRequest(http.MethodDelete, target, body, headersQueryParams...) if err != nil { panic(err) } return req } func newXMLRequest(method, target string, body any, headersQueryParams ...any) (*http.Request, error) { b, err := xml.Marshal(body) if err != nil { return nil, errors.New(color.Bad("XML encoding failed: %s", err)) } return newRequest( method, target, bytes.NewReader(b), append(headersQueryParams, "Content-Type", "application/xml"), ) } // NewXMLRequest creates a new HTTP request with body marshaled to // XML. "Content-Type" header is automatically set to // "application/xml". Other headers can be added via headersQueryParams, as in: // // req := tdhttp.NewXMLRequest("POST", "/data", body, // "X-Foo", "Foo-value", // "X-Zip", "Zip-value", // ) // // See [NewRequest] for all possible formats accepted in headersQueryParams. func NewXMLRequest(method, target string, body any, headersQueryParams ...any) *http.Request { req, err := newXMLRequest(method, target, body, headersQueryParams...) if err != nil { panic(err) } return req } // PostXML creates a HTTP POST with body marshaled to // XML. "Content-Type" header is automatically set to // "application/xml". It is a shortcut for: // // tdhttp.NewXMLRequest(http.MethodPost, target, body, headersQueryParams...) // // See [NewRequest] for all possible formats accepted in headersQueryParams. func PostXML(target string, body any, headersQueryParams ...any) *http.Request { req, err := newXMLRequest(http.MethodPost, target, body, headersQueryParams...) if err != nil { panic(err) } return req } // PutXML creates a HTTP PUT with body marshaled to // XML. "Content-Type" header is automatically set to // "application/xml". It is a shortcut for: // // tdhttp.NewXMLRequest(http.MethodPut, target, body, headersQueryParams...) // // See [NewRequest] for all possible formats accepted in headersQueryParams. func PutXML(target string, body any, headersQueryParams ...any) *http.Request { req, err := newXMLRequest(http.MethodPut, target, body, headersQueryParams...) if err != nil { panic(err) } return req } // PatchXML creates a HTTP PATCH with body marshaled to // XML. "Content-Type" header is automatically set to // "application/xml". It is a shortcut for: // // tdhttp.NewXMLRequest(http.MethodPatch, target, body, headersQueryParams...) // // See [NewRequest] for all possible formats accepted in headersQueryParams. func PatchXML(target string, body any, headersQueryParams ...any) *http.Request { req, err := newXMLRequest(http.MethodPatch, target, body, headersQueryParams...) if err != nil { panic(err) } return req } // DeleteXML creates a HTTP DELETE with body marshaled to // XML. "Content-Type" header is automatically set to // "application/xml". It is a shortcut for: // // tdhttp.NewXMLRequest(http.MethodDelete, target, body, headersQueryParams...) // // See [NewRequest] for all possible formats accepted in headersQueryParams. func DeleteXML(target string, body any, headersQueryParams ...any) *http.Request { req, err := newXMLRequest(http.MethodDelete, target, body, headersQueryParams...) if err != nil { panic(err) } return req } golang-github-maxatome-go-testdeep-1.14.0/helpers/tdhttp/request_test.go000066400000000000000000000347731454313311600264210ustar00rootroot00000000000000// Copyright (c) 2019-2023, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package tdhttp_test import ( "io" "net/http" "net/url" "testing" "github.com/maxatome/go-testdeep/helpers/tdhttp" "github.com/maxatome/go-testdeep/td" ) func TestBasicAuthHeader(t *testing.T) { td.Cmp(t, tdhttp.BasicAuthHeader("max", "5ecr3T"), http.Header{"Authorization": []string{"Basic bWF4OjVlY3IzVA=="}}) } func TestNewRequest(tt *testing.T) { t := td.NewT(tt) t.Run("NewRequest", func(t *td.T) { req := tdhttp.NewRequest("GET", "/path", nil, "Foo", "Bar", "Zip", "Test") t.Cmp(req.Header, http.Header{ "Foo": []string{"Bar"}, "Zip": []string{"Test"}, }) }) t.Run("NewRequest last header value less", func(t *td.T) { req := tdhttp.NewRequest("GET", "/path", nil, "Foo", "Bar", "Zip") t.Cmp(req.Header, http.Header{ "Foo": []string{"Bar"}, "Zip": []string{""}, }) }) t.Run("NewRequest header http.Header", func(t *td.T) { req := tdhttp.NewRequest("GET", "/path", nil, http.Header{ "Foo": []string{"Bar"}, "Zip": []string{"Test"}, }) t.Cmp(req.Header, http.Header{ "Foo": []string{"Bar"}, "Zip": []string{"Test"}, }) }) t.Run("NewRequest header http.Cookie", func(t *td.T) { req := tdhttp.NewRequest("GET", "/path", nil, &http.Cookie{Name: "cook1", Value: "val1"}, http.Cookie{Name: "cook2", Value: "val2"}, ) t.Cmp(req.Header, http.Header{"Cookie": []string{"cook1=val1; cook2=val2"}}) }) t.Run("NewRequest header flattened", func(t *td.T) { req := tdhttp.NewRequest("GET", "/path", nil, td.Flatten([]string{ "Foo", "Bar", "Zip", "Test", }), td.Flatten(map[string]string{ "Pipo": "Bingo", "Hey": "Yo", }), ) t.Cmp(req.Header, http.Header{ "Foo": []string{"Bar"}, "Zip": []string{"Test"}, "Pipo": []string{"Bingo"}, "Hey": []string{"Yo"}, }) }) t.Run("NewRequest header combined", func(t *td.T) { req := tdhttp.NewRequest("GET", "/path", nil, "H1", "V1", http.Header{ "H1": []string{"V2"}, "H2": []string{"V1", "V2"}, }, "H2", "V3", td.Flatten([]string{ "H2", "V4", "H3", "V1", "H3", "V2", }), td.Flatten(map[string]string{ "H2": "V5", "H3": "V3", }), ) t.Cmp(req.Header, http.Header{ "H1": []string{"V1", "V2"}, "H2": []string{"V1", "V2", "V3", "V4", "V5"}, "H3": []string{"V1", "V2", "V3"}, }) }) t.Run("NewRequest and query params", func(t *td.T) { req := tdhttp.NewRequest("GET", "/path", nil, url.Values{"p1": []string{"a", "b"}}, url.Values{"p2": []string{"a", "b"}}, tdhttp.Q{"p1": "c", "p2": []string{"c", "d"}}, tdhttp.Q{"p1": 123, "p3": true}, ) t.Cmp(req.URL.String(), "/path?p1=a&p1=b&p1=c&p1=123&p2=a&p2=b&p2=c&p2=d&p3=true") // Query param already set in path req = tdhttp.NewRequest("GET", "/path?already=true", nil, tdhttp.Q{"p1": 123, "p3": true}, ) t.Cmp(req.URL.String(), "/path?already=true&p1=123&p3=true") }) t.Run("NewRequest panics", func(t *td.T) { t.CmpPanic( func() { tdhttp.NewRequest("GET", "/path", nil, "H", "V", true) }, td.HasPrefix("headersQueryParams... can only contains string, http.Header, http.Cookie, url.Values and tdhttp.Q, not bool (@ headersQueryParams[2])")) t.CmpPanic( func() { tdhttp.NewRequest("GET", "/path", nil, "H1", true) }, td.HasPrefix(`header "H1" should have a string value, not a bool (@ headersQueryParams[1])`)) t.CmpPanic( func() { tdhttp.Get("/path", true) }, td.HasPrefix("headersQueryParams... can only contains string, http.Header, http.Cookie, url.Values and tdhttp.Q, not bool (@ headersQueryParams[0])")) t.CmpPanic( func() { tdhttp.Head("/path", true) }, td.HasPrefix("headersQueryParams... can only contains string, http.Header, http.Cookie, url.Values and tdhttp.Q, not bool (@ headersQueryParams[0])")) t.CmpPanic( func() { tdhttp.Options("/path", nil, true) }, td.HasPrefix("headersQueryParams... can only contains string, http.Header, http.Cookie, url.Values and tdhttp.Q, not bool (@ headersQueryParams[0])")) t.CmpPanic( func() { tdhttp.Post("/path", nil, true) }, td.HasPrefix("headersQueryParams... can only contains string, http.Header, http.Cookie, url.Values and tdhttp.Q, not bool (@ headersQueryParams[0])")) t.CmpPanic( func() { tdhttp.PostForm("/path", nil, true) }, td.HasPrefix("headersQueryParams... can only contains string, http.Header, http.Cookie, url.Values and tdhttp.Q, not bool (@ headersQueryParams[0])")) t.CmpPanic( func() { tdhttp.PostMultipartFormData("/path", &tdhttp.MultipartBody{}, true) }, td.HasPrefix("headersQueryParams... can only contains string, http.Header, http.Cookie, url.Values and tdhttp.Q, not bool (@ headersQueryParams[0])")) t.CmpPanic( func() { tdhttp.Patch("/path", nil, true) }, td.HasPrefix("headersQueryParams... can only contains string, http.Header, http.Cookie, url.Values and tdhttp.Q, not bool (@ headersQueryParams[0])")) t.CmpPanic( func() { tdhttp.Put("/path", nil, true) }, td.HasPrefix("headersQueryParams... can only contains string, http.Header, http.Cookie, url.Values and tdhttp.Q, not bool (@ headersQueryParams[0])")) t.CmpPanic( func() { tdhttp.Delete("/path", nil, true) }, td.HasPrefix("headersQueryParams... can only contains string, http.Header, http.Cookie, url.Values and tdhttp.Q, not bool (@ headersQueryParams[0])")) // Bad target t.CmpPanic( func() { tdhttp.NewRequest("GET", ":/badpath", nil) }, td.HasPrefix(`target is not a valid path: `)) // Q error t.CmpPanic( func() { tdhttp.Get("/", tdhttp.Q{"bad": map[string]bool{}}) }, td.HasPrefix(`headersQueryParams... tdhttp.Q bad parameter: don't know how to add type map[string]bool (map) to param "bad" (@ headersQueryParams[0])`)) }) // Get t.Cmp(tdhttp.Get("/path", "Foo", "Bar"), td.Struct( &http.Request{ Method: "GET", Header: http.Header{"Foo": []string{"Bar"}}, }, td.StructFields{ "URL": td.String("/path"), })) // Head t.Cmp(tdhttp.Head("/path", "Foo", "Bar"), td.Struct( &http.Request{ Method: "HEAD", Header: http.Header{"Foo": []string{"Bar"}}, }, td.StructFields{ "URL": td.String("/path"), })) // Options t.Cmp(tdhttp.Options("/path", nil, "Foo", "Bar"), td.Struct( &http.Request{ Method: "OPTIONS", Header: http.Header{"Foo": []string{"Bar"}}, }, td.StructFields{ "URL": td.String("/path"), })) // Post t.Cmp(tdhttp.Post("/path", nil, "Foo", "Bar"), td.Struct( &http.Request{ Method: "POST", Header: http.Header{"Foo": []string{"Bar"}}, }, td.StructFields{ "URL": td.String("/path"), })) // PostForm - url.Values t.Cmp( tdhttp.PostForm("/path", url.Values{ "param1": []string{"val1", "val2"}, "param2": []string{"zip"}, }, "Foo", "Bar"), td.Struct( &http.Request{ Method: "POST", Header: http.Header{ "Content-Type": []string{"application/x-www-form-urlencoded"}, "Foo": []string{"Bar"}, }, }, td.StructFields{ "URL": td.String("/path"), "Body": td.Smuggle( io.ReadAll, []byte("param1=val1¶m1=val2¶m2=zip"), ), })) // PostForm - tdhttp.Q t.Cmp( tdhttp.PostForm("/path", tdhttp.Q{ "param1": "val1", "param2": "val2", }, "Foo", "Bar"), td.Struct( &http.Request{ Method: "POST", Header: http.Header{ "Content-Type": []string{"application/x-www-form-urlencoded"}, "Foo": []string{"Bar"}, }, }, td.StructFields{ "URL": td.String("/path"), "Body": td.Smuggle( io.ReadAll, []byte("param1=val1¶m2=val2"), ), })) // PostForm - nil data t.Cmp( tdhttp.PostForm("/path", nil, "Foo", "Bar"), td.Struct( &http.Request{ Method: "POST", Header: http.Header{ "Content-Type": []string{"application/x-www-form-urlencoded"}, "Foo": []string{"Bar"}, }, }, td.StructFields{ "URL": td.String("/path"), "Body": td.Smuggle( io.ReadAll, []byte{}, ), })) // PostMultipartFormData req := tdhttp.PostMultipartFormData("/path", &tdhttp.MultipartBody{ Boundary: "BoUnDaRy", Parts: []*tdhttp.MultipartPart{ tdhttp.NewMultipartPartString("p1", "body1!"), tdhttp.NewMultipartPartString("p2", "body2!"), }, }, "Foo", "Bar") t.Cmp(req, td.Struct( &http.Request{ Method: "POST", Header: http.Header{ "Content-Type": []string{`multipart/form-data; boundary="BoUnDaRy"`}, "Foo": []string{"Bar"}, }, }, td.StructFields{ "URL": td.String("/path"), })) if t.CmpNoError(req.ParseMultipartForm(10000)) { t.Cmp(req.PostFormValue("p1"), "body1!") t.Cmp(req.PostFormValue("p2"), "body2!") } // Put t.Cmp(tdhttp.Put("/path", nil, "Foo", "Bar"), td.Struct( &http.Request{ Method: "PUT", Header: http.Header{"Foo": []string{"Bar"}}, }, td.StructFields{ "URL": td.String("/path"), })) // Patch t.Cmp(tdhttp.Patch("/path", nil, "Foo", "Bar"), td.Struct( &http.Request{ Method: "PATCH", Header: http.Header{"Foo": []string{"Bar"}}, }, td.StructFields{ "URL": td.String("/path"), })) // Delete t.Cmp(tdhttp.Delete("/path", nil, "Foo", "Bar"), td.Struct( &http.Request{ Method: "DELETE", Header: http.Header{"Foo": []string{"Bar"}}, }, td.StructFields{ "URL": td.String("/path"), })) } type TestStruct struct { Name string `json:"name" xml:"name"` } func TestNewJSONRequest(tt *testing.T) { t := td.NewT(tt) t.Run("NewJSONRequest", func(t *td.T) { req := tdhttp.NewJSONRequest("GET", "/path", TestStruct{ Name: "Bob", }, "Foo", "Bar", "Zip", "Test") t.String(req.Header.Get("Content-Type"), "application/json") t.String(req.Header.Get("Foo"), "Bar") t.String(req.Header.Get("Zip"), "Test") body, err := io.ReadAll(req.Body) if t.CmpNoError(err, "read request body") { t.String(string(body), `{"name":"Bob"}`) } }) t.Run("NewJSONRequest panic", func(t *td.T) { t.CmpPanic( func() { tdhttp.NewJSONRequest("GET", "/path", func() {}) }, td.Contains("json: unsupported type: func()")) t.CmpPanic( func() { tdhttp.PostJSON("/path", func() {}) }, td.Contains("json: unsupported type: func()")) t.CmpPanic( func() { tdhttp.PutJSON("/path", func() {}) }, td.Contains("json: unsupported type: func()")) t.CmpPanic( func() { tdhttp.PatchJSON("/path", func() {}) }, td.Contains("json: unsupported type: func()")) t.CmpPanic( func() { tdhttp.DeleteJSON("/path", func() {}) }, td.Contains("json: unsupported type: func()")) t.CmpPanic( func() { tdhttp.NewJSONRequest("GET", "/path", td.JSONPointer("/a", 0)) }, td.Contains("JSON encoding failed: json: error calling MarshalJSON for type *td.tdJSONPointer: JSONPointer TestDeep operator cannot be json.Marshal'led")) // Common user mistake t.CmpPanic( func() { tdhttp.NewJSONRequest("GET", "/path", td.JSON(`{}`)) }, td.Contains(`JSON encoding failed: json: error calling MarshalJSON for type *td.tdJSON: JSON TestDeep operator cannot be json.Marshal'led, use json.RawMessage() instead`)) }) // Post t.Cmp(tdhttp.PostJSON("/path", 42, "Foo", "Bar"), td.Struct( &http.Request{ Method: "POST", Header: http.Header{ "Foo": []string{"Bar"}, "Content-Type": []string{"application/json"}, }, }, td.StructFields{ "URL": td.String("/path"), })) // Put t.Cmp(tdhttp.PutJSON("/path", 42, "Foo", "Bar"), td.Struct( &http.Request{ Method: "PUT", Header: http.Header{ "Foo": []string{"Bar"}, "Content-Type": []string{"application/json"}, }, }, td.StructFields{ "URL": td.String("/path"), })) // Patch t.Cmp(tdhttp.PatchJSON("/path", 42, "Foo", "Bar"), td.Struct( &http.Request{ Method: "PATCH", Header: http.Header{ "Foo": []string{"Bar"}, "Content-Type": []string{"application/json"}, }, }, td.StructFields{ "URL": td.String("/path"), })) // Delete t.Cmp(tdhttp.DeleteJSON("/path", 42, "Foo", "Bar"), td.Struct( &http.Request{ Method: "DELETE", Header: http.Header{ "Foo": []string{"Bar"}, "Content-Type": []string{"application/json"}, }, }, td.StructFields{ "URL": td.String("/path"), })) } func TestNewXMLRequest(tt *testing.T) { t := td.NewT(tt) t.Run("NewXMLRequest", func(t *td.T) { req := tdhttp.NewXMLRequest("GET", "/path", TestStruct{ Name: "Bob", }, "Foo", "Bar", "Zip", "Test") t.String(req.Header.Get("Content-Type"), "application/xml") t.String(req.Header.Get("Foo"), "Bar") t.String(req.Header.Get("Zip"), "Test") body, err := io.ReadAll(req.Body) if t.CmpNoError(err, "read request body") { t.String(string(body), `Bob`) } }) t.Run("NewXMLRequest panic", func(t *td.T) { t.CmpPanic( func() { tdhttp.NewXMLRequest("GET", "/path", func() {}) }, td.Contains("XML encoding failed")) t.CmpPanic( func() { tdhttp.PostXML("/path", func() {}) }, td.Contains("XML encoding failed")) t.CmpPanic( func() { tdhttp.PutXML("/path", func() {}) }, td.Contains("XML encoding failed")) t.CmpPanic( func() { tdhttp.PatchXML("/path", func() {}) }, td.Contains("XML encoding failed")) t.CmpPanic( func() { tdhttp.DeleteXML("/path", func() {}) }, td.Contains("XML encoding failed")) }) // Post t.Cmp(tdhttp.PostXML("/path", 42, "Foo", "Bar"), td.Struct( &http.Request{ Method: "POST", Header: http.Header{ "Foo": []string{"Bar"}, "Content-Type": []string{"application/xml"}, }, }, td.StructFields{ "URL": td.String("/path"), })) // Put t.Cmp(tdhttp.PutXML("/path", 42, "Foo", "Bar"), td.Struct( &http.Request{ Method: "PUT", Header: http.Header{ "Foo": []string{"Bar"}, "Content-Type": []string{"application/xml"}, }, }, td.StructFields{ "URL": td.String("/path"), })) // Patch t.Cmp(tdhttp.PatchXML("/path", 42, "Foo", "Bar"), td.Struct( &http.Request{ Method: "PATCH", Header: http.Header{ "Foo": []string{"Bar"}, "Content-Type": []string{"application/xml"}, }, }, td.StructFields{ "URL": td.String("/path"), })) // Delete t.Cmp(tdhttp.DeleteXML("/path", 42, "Foo", "Bar"), td.Struct( &http.Request{ Method: "DELETE", Header: http.Header{ "Foo": []string{"Bar"}, "Content-Type": []string{"application/xml"}, }, }, td.StructFields{ "URL": td.String("/path"), })) } golang-github-maxatome-go-testdeep-1.14.0/helpers/tdhttp/test_api.go000066400000000000000000001054211454313311600254670ustar00rootroot00000000000000// Copyright (c) 2020-2022, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package tdhttp import ( "encoding/json" "encoding/xml" "fmt" "io" "net/http" "net/http/httptest" "reflect" "runtime" "strings" "testing" "time" "github.com/maxatome/go-testdeep/helpers/tdhttp/internal" "github.com/maxatome/go-testdeep/helpers/tdutil" "github.com/maxatome/go-testdeep/internal/color" "github.com/maxatome/go-testdeep/internal/ctxerr" "github.com/maxatome/go-testdeep/internal/types" "github.com/maxatome/go-testdeep/td" ) type failed uint8 const ( responseFailed failed = 1 << iota statusFailed headerFailed trailerFailed cookiesFailed bodyFailed ) // TestAPI allows to test one HTTP API. See [NewTestAPI] function to // create a new instance and get some examples of use. type TestAPI struct { t *td.T handler http.Handler name string sentAt time.Time response *httptest.ResponseRecorder failed failed // autoDumpResponse dumps the received response when a test fails. autoDumpResponse bool responseDumped bool } // NewTestAPI creates a [TestAPI] that can be used to test routes of the // API behind handler. // // tdhttp.NewTestAPI(t, mux). // Get("/test"). // CmpStatus(200). // CmpBody("OK!") // // Several routes can be tested with the same instance as in: // // ta := tdhttp.NewTestAPI(t, mux) // // ta.Get("/test"). // CmpStatus(200). // CmpBody("OK!") // // ta.Get("/ping"). // CmpStatus(200). // CmpBody("pong") // // Note that tb can be a [*testing.T] as well as a [*td.T]. func NewTestAPI(tb testing.TB, handler http.Handler) *TestAPI { return &TestAPI{ t: td.NewT(tb), handler: handler, } } // With creates a new [*TestAPI] instance copied from t, but resetting // the [testing.TB] instance the tests are based on to tb. The // returned instance is independent from t, sharing only the same // handler. // // It is typically used when the [TestAPI] instance is “reused” in // sub-tests, as in: // // func TestMyAPI(t *testing.T) { // ta := tdhttp.NewTestAPI(t, MyAPIHandler()) // // ta.Get("/test").CmpStatus(200) // // t.Run("errors", func (t *testing.T) { // ta := ta.With(t) // // ta.Get("/test?bad=1").CmpStatus(400) // ta.Get("/test?bad=buzz").CmpStatus(400) // } // // ta.Get("/next").CmpStatus(200) // } // // Note that tb can be a [*testing.T] as well as a [*td.T]. // // See [TestAPI.Run] for another way to handle subtests. func (ta *TestAPI) With(tb testing.TB) *TestAPI { return &TestAPI{ t: td.NewT(tb), handler: ta.handler, autoDumpResponse: ta.autoDumpResponse, } } // T returns the internal instance of [*td.T]. func (ta *TestAPI) T() *td.T { return ta.t } // Run runs f as a subtest of t called name. func (ta *TestAPI) Run(name string, f func(ta *TestAPI)) bool { return ta.t.Run(name, func(tdt *td.T) { f(NewTestAPI(tdt, ta.handler)) }) } // AutoDumpResponse allows to dump the HTTP response when the first // error is encountered after a request. // // ta.AutoDumpResponse() // ta.AutoDumpResponse(true) // // both enable the dump. func (ta *TestAPI) AutoDumpResponse(enable ...bool) *TestAPI { ta.autoDumpResponse = len(enable) == 0 || enable[0] return ta } // Name allows to name the series of tests that follow. This name is // used as a prefix for all following tests, in case of failure to // qualify each test. If len(args) > 1 and the first item of args is // a string and contains a '%' rune then [fmt.Fprintf] is used to // compose the name, else args are passed to [fmt.Fprint]. func (ta *TestAPI) Name(args ...any) *TestAPI { ta.name = tdutil.BuildTestName(args...) if ta.name != "" { ta.name += ": " } return ta } // Request sends a new HTTP request to the tested API. Any Cmp* or // [TestAPI.NoBody] methods can now be called. // // Note that [TestAPI.Failed] status is reset just after this call. func (ta *TestAPI) Request(req *http.Request) *TestAPI { ta.response = httptest.NewRecorder() ta.failed = 0 ta.sentAt = time.Now().Truncate(0) ta.responseDumped = false ta.handler.ServeHTTP(ta.response, req) return ta } func (ta *TestAPI) checkRequestSent() bool { ta.t.Helper() // If no request has been sent, display a nice error message return ta.t.RootName("Request"). Code(ta.response != nil, func(sent bool) error { if sent { return nil } return &ctxerr.Error{ Message: "%% not sent!", Summary: ctxerr.NewSummary("A request must be sent before testing status, header, body or full response"), } }, ta.name+"request is sent") } // Failed returns true if any Cmp* or [TestAPI.NoBody] method failed since last // request sending. func (ta *TestAPI) Failed() bool { return ta.failed != 0 } // Get sends a HTTP GET to the tested API. Any Cmp* or [TestAPI.NoBody] methods // can now be called. // // Note that [TestAPI.Failed] status is reset just after this call. // // See [NewRequest] for all possible formats accepted in headersQueryParams. func (ta *TestAPI) Get(target string, headersQueryParams ...any) *TestAPI { ta.t.Helper() req, err := get(target, headersQueryParams...) if err != nil { ta.t.Fatal(err) } return ta.Request(req) } // Head sends a HTTP HEAD to the tested API. Any Cmp* or [TestAPI.NoBody] methods // can now be called. // // Note that [TestAPI.Failed] status is reset just after this call. // // See [NewRequest] for all possible formats accepted in headersQueryParams. func (ta *TestAPI) Head(target string, headersQueryParams ...any) *TestAPI { ta.t.Helper() req, err := head(target, headersQueryParams...) if err != nil { ta.t.Fatal(err) } return ta.Request(req) } // Options sends a HTTP OPTIONS to the tested API. Any Cmp* or // [TestAPI.NoBody] methods can now be called. // // Note that [TestAPI.Failed] status is reset just after this call. // // See [NewRequest] for all possible formats accepted in headersQueryParams. func (ta *TestAPI) Options(target string, body io.Reader, headersQueryParams ...any) *TestAPI { ta.t.Helper() req, err := options(target, body, headersQueryParams...) if err != nil { ta.t.Fatal(err) } return ta.Request(req) } // Post sends a HTTP POST to the tested API. Any Cmp* or // [TestAPI.NoBody] methods can now be called. // // Note that [TestAPI.Failed] status is reset just after this call. // // See [NewRequest] for all possible formats accepted in headersQueryParams. func (ta *TestAPI) Post(target string, body io.Reader, headersQueryParams ...any) *TestAPI { ta.t.Helper() req, err := post(target, body, headersQueryParams...) if err != nil { ta.t.Fatal(err) } return ta.Request(req) } // PostForm sends a HTTP POST with data's keys and values URL-encoded // as the request body to the tested API. "Content-Type" header is // automatically set to "application/x-www-form-urlencoded". Any Cmp* // or [TestAPI.NoBody] methods can now be called. // // Note that [TestAPI.Failed] status is reset just after this call. // // See [NewRequest] for all possible formats accepted in headersQueryParams. func (ta *TestAPI) PostForm(target string, data URLValuesEncoder, headersQueryParams ...any) *TestAPI { ta.t.Helper() req, err := postForm(target, data, headersQueryParams...) if err != nil { ta.t.Fatal(err) } return ta.Request(req) } // PostMultipartFormData sends a HTTP POST multipart request, like // multipart/form-data one for example. See [MultipartBody] type for // details. "Content-Type" header is automatically set depending on // data.MediaType (defaults to "multipart/form-data") and // data.Boundary (defaults to "go-testdeep-42"). Any Cmp* or // [TestAPI.NoBody] methods can now be called. // // Note that [TestAPI.Failed] status is reset just after this call. // // ta.PostMultipartFormData("/data", // &tdhttp.MultipartBody{ // // "multipart/form-data" by default // Parts: []*tdhttp.MultipartPart{ // tdhttp.NewMultipartPartString("type", "Sales"), // tdhttp.NewMultipartPartFile("report", "report.json", "application/json"), // }, // }, // "X-Foo", "Foo-value", // "X-Zip", "Zip-value", // ) // // See [NewRequest] for all possible formats accepted in headersQueryParams. func (ta *TestAPI) PostMultipartFormData(target string, data *MultipartBody, headersQueryParams ...any) *TestAPI { ta.t.Helper() req, err := postMultipartFormData(target, data, headersQueryParams...) if err != nil { ta.t.Fatal(err) } return ta.Request(req) } // Put sends a HTTP PUT to the tested API. Any Cmp* or [TestAPI.NoBody] methods // can now be called. // // Note that [TestAPI.Failed] status is reset just after this call. // // See [NewRequest] for all possible formats accepted in headersQueryParams. func (ta *TestAPI) Put(target string, body io.Reader, headersQueryParams ...any) *TestAPI { ta.t.Helper() req, err := put(target, body, headersQueryParams...) if err != nil { ta.t.Fatal(err) } return ta.Request(req) } // Patch sends a HTTP PATCH to the tested API. Any Cmp* or [TestAPI.NoBody] methods // can now be called. // // Note that [TestAPI.Failed] status is reset just after this call. // // See [NewRequest] for all possible formats accepted in headersQueryParams. func (ta *TestAPI) Patch(target string, body io.Reader, headersQueryParams ...any) *TestAPI { ta.t.Helper() req, err := patch(target, body, headersQueryParams...) if err != nil { ta.t.Fatal(err) } return ta.Request(req) } // Delete sends a HTTP DELETE to the tested API. Any Cmp* or [TestAPI.NoBody] methods // can now be called. // // Note that [TestAPI.Failed] status is reset just after this call. // // See [NewRequest] for all possible formats accepted in headersQueryParams. func (ta *TestAPI) Delete(target string, body io.Reader, headersQueryParams ...any) *TestAPI { ta.t.Helper() req, err := del(target, body, headersQueryParams...) if err != nil { ta.t.Fatal(err) } return ta.Request(req) } // NewJSONRequest sends a HTTP request with body marshaled to // JSON. "Content-Type" header is automatically set to // "application/json". Any Cmp* or [TestAPI.NoBody] methods can now be called. // // Note that [TestAPI.Failed] status is reset just after this call. // // See [NewRequest] for all possible formats accepted in headersQueryParams. func (ta *TestAPI) NewJSONRequest(method, target string, body any, headersQueryParams ...any) *TestAPI { ta.t.Helper() req, err := newJSONRequest(method, target, body, headersQueryParams...) if err != nil { ta.t.Fatal(err) } return ta.Request(req) } // PostJSON sends a HTTP POST with body marshaled to // JSON. "Content-Type" header is automatically set to // "application/json". Any Cmp* or [TestAPI.NoBody] methods can now be called. // // Note that [TestAPI.Failed] status is reset just after this call. // // See [NewRequest] for all possible formats accepted in headersQueryParams. func (ta *TestAPI) PostJSON(target string, body any, headersQueryParams ...any) *TestAPI { ta.t.Helper() req, err := newJSONRequest(http.MethodPost, target, body, headersQueryParams...) if err != nil { ta.t.Fatal(err) } return ta.Request(req) } // PutJSON sends a HTTP PUT with body marshaled to // JSON. "Content-Type" header is automatically set to // "application/json". Any Cmp* or [TestAPI.NoBody] methods can now be called. // // Note that [TestAPI.Failed] status is reset just after this call. // // See [NewRequest] for all possible formats accepted in headersQueryParams. func (ta *TestAPI) PutJSON(target string, body any, headersQueryParams ...any) *TestAPI { ta.t.Helper() req, err := newJSONRequest(http.MethodPut, target, body, headersQueryParams...) if err != nil { ta.t.Fatal(err) } return ta.Request(req) } // PatchJSON sends a HTTP PATCH with body marshaled to // JSON. "Content-Type" header is automatically set to // "application/json". Any Cmp* or [TestAPI.NoBody] methods can now be called. // // Note that [TestAPI.Failed] status is reset just after this call. // // See [NewRequest] for all possible formats accepted in headersQueryParams. func (ta *TestAPI) PatchJSON(target string, body any, headersQueryParams ...any) *TestAPI { ta.t.Helper() req, err := newJSONRequest(http.MethodPatch, target, body, headersQueryParams...) if err != nil { ta.t.Fatal(err) } return ta.Request(req) } // DeleteJSON sends a HTTP DELETE with body marshaled to // JSON. "Content-Type" header is automatically set to // "application/json". Any Cmp* or [TestAPI.NoBody] methods can now be called. // // Note that [TestAPI.Failed] status is reset just after this call. // // See [NewRequest] for all possible formats accepted in headersQueryParams. func (ta *TestAPI) DeleteJSON(target string, body any, headersQueryParams ...any) *TestAPI { ta.t.Helper() req, err := newJSONRequest(http.MethodDelete, target, body, headersQueryParams...) if err != nil { ta.t.Fatal(err) } return ta.Request(req) } // NewXMLRequest sends a HTTP request with body marshaled to // XML. "Content-Type" header is automatically set to // "application/xml". Any Cmp* or [TestAPI.NoBody] methods can now be called. // // Note that [TestAPI.Failed] status is reset just after this call. // // See [NewRequest] for all possible formats accepted in headersQueryParams. func (ta *TestAPI) NewXMLRequest(method, target string, body any, headersQueryParams ...any) *TestAPI { ta.t.Helper() req, err := newXMLRequest(method, target, body, headersQueryParams...) if err != nil { ta.t.Fatal(err) } return ta.Request(req) } // PostXML sends a HTTP POST with body marshaled to // XML. "Content-Type" header is automatically set to // "application/xml". Any Cmp* or [TestAPI.NoBody] methods can now be called. // // Note that [TestAPI.Failed] status is reset just after this call. // // See [NewRequest] for all possible formats accepted in headersQueryParams. func (ta *TestAPI) PostXML(target string, body any, headersQueryParams ...any) *TestAPI { ta.t.Helper() req, err := newXMLRequest(http.MethodPost, target, body, headersQueryParams...) if err != nil { ta.t.Fatal(err) } return ta.Request(req) } // PutXML sends a HTTP PUT with body marshaled to // XML. "Content-Type" header is automatically set to // "application/xml". Any Cmp* or [TestAPI.NoBody] methods can now be called. // // Note that [TestAPI.Failed] status is reset just after this call. // // See [NewRequest] for all possible formats accepted in headersQueryParams. func (ta *TestAPI) PutXML(target string, body any, headersQueryParams ...any) *TestAPI { ta.t.Helper() req, err := newXMLRequest(http.MethodPut, target, body, headersQueryParams...) if err != nil { ta.t.Fatal(err) } return ta.Request(req) } // PatchXML sends a HTTP PATCH with body marshaled to // XML. "Content-Type" header is automatically set to // "application/xml". Any Cmp* or [TestAPI.NoBody] methods can now be called. // // Note that [TestAPI.Failed] status is reset just after this call. // // See [NewRequest] for all possible formats accepted in headersQueryParams. func (ta *TestAPI) PatchXML(target string, body any, headersQueryParams ...any) *TestAPI { ta.t.Helper() req, err := newXMLRequest(http.MethodPatch, target, body, headersQueryParams...) if err != nil { ta.t.Fatal(err) } return ta.Request(req) } // DeleteXML sends a HTTP DELETE with body marshaled to // XML. "Content-Type" header is automatically set to // "application/xml". Any Cmp* or [TestAPI.NoBody] methods can now be called. // // Note that [TestAPI.Failed] status is reset just after this call. // // See [NewRequest] for all possible formats accepted in headersQueryParams. func (ta *TestAPI) DeleteXML(target string, body any, headersQueryParams ...any) *TestAPI { ta.t.Helper() req, err := newXMLRequest(http.MethodDelete, target, body, headersQueryParams...) if err != nil { ta.t.Fatal(err) } return ta.Request(req) } // CmpResponse tests the last request response status against // expectedResponse. expectedResponse can be a *http.Response or more // probably a [td.TestDeep] operator. // // ta := tdhttp.NewTestAPI(t, mux) // // ta.Get("/test"). // CmpResponse(td.Struct( // &http.Response{Status: http.StatusOK}, td.StructFields{ // "Header": td.SuperMapOf(http.Header{"X-Test": {"pipo"}}), // "ContentLength": td.Gt(10), // })) // // Some tests can be hard to achieve using operators chaining. In this // case, the [td.Code] operator can be used to take the full control // over the extractions and comparisons to do: // // ta.Get("/test"). // CmpResponse(td.Code(func (assert, require *td.T, r *http.Response) { // token, err := ParseToken(r.Header.Get("X-Token")) // require.CmpNoError(err) // // baseURL,err := url.Parse(r.Header.Get("X-Base-URL")) // require.CmpNoError(err) // // assert.Cmp(baseURL.Query().Get("id"), token.ID) // })) // // It fails if no request has been sent yet. func (ta *TestAPI) CmpResponse(expectedResponse any) *TestAPI { defer ta.t.AnchorsPersistTemporarily()() ta.t.Helper() if !ta.checkRequestSent() { ta.failed |= responseFailed return ta } if !ta.t.RootName("Response"). Cmp(ta.response.Result(), expectedResponse, ta.name+"full response should match") { ta.failed |= responseFailed if ta.autoDumpResponse { ta.dumpResponse() } } return ta } // CmpStatus tests the last request response status against // expectedStatus. expectedStatus can be an int to match a fixed HTTP // status code, or a [td.TestDeep] operator. // // ta := tdhttp.NewTestAPI(t, mux) // // ta.Get("/test"). // CmpStatus(http.StatusOK) // // ta.PostJSON("/new", map[string]string{"name": "Bob"}). // CmpStatus(td.Between(200, 202)) // // It fails if no request has been sent yet. func (ta *TestAPI) CmpStatus(expectedStatus any) *TestAPI { defer ta.t.AnchorsPersistTemporarily()() ta.t.Helper() if !ta.checkRequestSent() { ta.failed |= statusFailed return ta } if !ta.t.RootName("Response.Status"). CmpLax(ta.response.Code, expectedStatus, ta.name+"status code should match") { ta.failed |= statusFailed if ta.autoDumpResponse { ta.dumpResponse() } } return ta } // CmpHeader tests the last request response header against // expectedHeader. expectedHeader can be a [http.Header] or a // [td.TestDeep] operator. Keep in mind that if it is a [http.Header], // it has to match exactly the response header. Often only the // presence of a header key is needed: // // ta := tdhttp.NewTestAPI(t, mux). // PostJSON("/new", map[string]string{"name": "Bob"}). // CmdStatus(201). // CmpHeader(td.ContainsKey("X-Custom")) // // or some specific key, value pairs: // // ta.CmpHeader(td.SuperMapOf( // http.Header{ // "X-Account": []string{"Bob"}, // }, // td.MapEntries{ // "X-Token": td.Bag(td.Re(`^[a-z0-9-]{32}\z`)), // }), // ) // // Note that CmpHeader calls can be chained: // // ta.CmpHeader(td.ContainsKey("X-Account")). // CmpHeader(td.ContainsKey("X-Token")) // // instead of doing all tests in one call as [td.All] operator allows it: // // ta.CmpHeader(td.All( // td.ContainsKey("X-Account"), // td.ContainsKey("X-Token"), // )) // // It fails if no request has been sent yet. func (ta *TestAPI) CmpHeader(expectedHeader any) *TestAPI { defer ta.t.AnchorsPersistTemporarily()() ta.t.Helper() if !ta.checkRequestSent() { ta.failed |= headerFailed return ta } if !ta.t.RootName("Response.Header"). CmpLax(ta.response.Result().Header, expectedHeader, ta.name+"header should match") { ta.failed |= headerFailed if ta.autoDumpResponse { ta.dumpResponse() } } return ta } // CmpTrailer tests the last request response trailer against // expectedTrailer. expectedTrailer can be a [http.Header] or a // [td.TestDeep] operator. Keep in mind that if it is a [http.Header], // it has to match exactly the response trailer. Often only the // presence of a trailer key is needed: // // ta := tdhttp.NewTestAPI(t, mux). // PostJSON("/new", map[string]string{"name": "Bob"}). // CmdStatus(201). // CmpTrailer(td.ContainsKey("X-Custom")) // // or some specific key, value pairs: // // ta.CmpTrailer(td.SuperMapOf( // http.Header{ // "X-Account": []string{"Bob"}, // }, // td.MapEntries{ // "X-Token": td.Re(`^[a-z0-9-]{32}\z`), // }), // ) // // Note that CmpTrailer calls can be chained: // // ta.CmpTrailer(td.ContainsKey("X-Account")). // CmpTrailer(td.ContainsKey("X-Token")) // // instead of doing all tests in one call as [td.All] operator allows it: // // ta.CmpTrailer(td.All( // td.ContainsKey("X-Account"), // td.ContainsKey("X-Token"), // )) // // It fails if no request has been sent yet. // // Note that until go1.19, it does not handle multiple values in // a single Trailer header field. func (ta *TestAPI) CmpTrailer(expectedTrailer any) *TestAPI { defer ta.t.AnchorsPersistTemporarily()() ta.t.Helper() if !ta.checkRequestSent() { ta.failed |= trailerFailed return ta } if !ta.t.RootName("Response.Trailer"). CmpLax(ta.response.Result().Trailer, expectedTrailer, ta.name+"trailer should match") { ta.failed |= trailerFailed if ta.autoDumpResponse { ta.dumpResponse() } } return ta } // CmpCookies tests the last request response cookies against // expectedCookies. expectedCookies can be a [][*http.Cookie] or a // [td.TestDeep] operator. Keep in mind that if it is a // [][*http.Cookie], it has to match exactly the response // cookies. Often only the presence of a cookie key is needed: // // ta := tdhttp.NewTestAPI(t, mux). // PostJSON("/login", map[string]string{"name": "Bob", "password": "Sponge"}). // CmdStatus(200). // CmpCookies(td.SuperBagOf(td.Struct(&http.Cookie{Name: "cookie_session"}, nil))). // CmpCookies(td.SuperBagOf(td.Smuggle("Name", "cookie_session"))) // shorter // // To make tests easier, [http.Cookie.Raw] and [http.Cookie.RawExpires] fields // of each [*http.Cookie] are zeroed before doing the comparison. So no need // to fill them when comparing against a simple literal as in: // // ta := tdhttp.NewTestAPI(t, mux). // PostJSON("/login", map[string]string{"name": "Bob", "password": "Sponge"}). // CmdStatus(200). // CmpCookies([]*http.Cookies{ // {Name: "cookieName1", Value: "cookieValue1"}, // {Name: "cookieName2", Value: "cookieValue2"}, // }) // // It fails if no request has been sent yet. func (ta *TestAPI) CmpCookies(expectedCookies any) *TestAPI { defer ta.t.AnchorsPersistTemporarily()() ta.t.Helper() if !ta.checkRequestSent() { ta.failed |= cookiesFailed return ta } // Empty Raw* fields to make comparisons easier cookies := ta.response.Result().Cookies() for _, c := range cookies { c.RawExpires, c.Raw = "", "" } if !ta.t.RootName("Response.Cookie"). CmpLax(cookies, expectedCookies, ta.name+"cookies should match") { ta.failed |= cookiesFailed if ta.autoDumpResponse { ta.dumpResponse() } } return ta } // findCmpXBodyCaller finds the oldest Cmp* method called. func findCmpXBodyCaller() string { var ( fn string pc [20]uintptr found bool ) if num := runtime.Callers(5, pc[:]); num > 0 { frames := runtime.CallersFrames(pc[:num]) for { frame, more := frames.Next() if pos := strings.Index(frame.Function, "tdhttp.(*TestAPI).Cmp"); pos > 0 { fn = frame.Function[pos+18:] found = true } else if found { more = false } if !more { break } } } return fn } func (ta *TestAPI) cmpMarshaledBody( acceptEmptyBody bool, unmarshal func([]byte, any) error, expectedBody any, ) *TestAPI { defer ta.t.AnchorsPersistTemporarily()() ta.t.Helper() if !ta.checkRequestSent() { ta.failed |= bodyFailed return ta } if !acceptEmptyBody && !ta.t.RootName("Response body").Code(ta.response.Body.Bytes(), func(b []byte) error { if len(b) > 0 { return nil } return &ctxerr.Error{ Message: "%% is empty!", Summary: ctxerr.NewSummary( "Body cannot be empty when using " + findCmpXBodyCaller()), } }, ta.name+"body should not be empty") { ta.failed |= bodyFailed if ta.autoDumpResponse { ta.dumpResponse() } return ta } tt := ta.t.RootName("Response.Body") var bodyType reflect.Type // If expectedBody is a TestDeep operator, try to ask it the type // behind it. It should work in most cases (typically Struct(), // Map() & Slice()). var unknownExpectedType, showRawBody bool op, ok := expectedBody.(td.TestDeep) if ok { bodyType = op.TypeBehind() if bodyType == nil { // As the expected body type cannot be guessed, try to // unmarshal in an any bodyType = types.Interface unknownExpectedType = true // Special case for Ignore & NotEmpty operators switch op.GetLocation().Func { case "Ignore", "NotEmpty": showRawBody = (ta.failed & statusFailed) != 0 // Show real body if status failed } } } else { bodyType = reflect.TypeOf(expectedBody) if bodyType == nil { bodyType = types.Interface } } // For unmarshaling below, body must be a pointer bodyPtr := reflect.New(bodyType) // Try to unmarshal body if !tt.RootName("unmarshal(Response.Body)"). CmpNoError(unmarshal(ta.response.Body.Bytes(), bodyPtr.Interface()), ta.name+"body unmarshaling") { // If unmarshal failed, perhaps it's coz the expected body type // is unknown? if unknownExpectedType { tt.Logf("Cannot guess the body expected type as %[1]s TestDeep\n"+ "operator does not know the type behind it.\n"+ "You can try All(Isa(EXPECTED_TYPE), %[1]s(…)) to disambiguate…", op.GetLocation().Func) } showRawBody = true // let's show its real body contents ta.failed |= bodyFailed } else if !tt.Cmp(bodyPtr.Elem().Interface(), expectedBody, ta.name+"body contents is OK") { // Try to catch bad body expected type when nothing has been set // to non-zero during unmarshaling body. In this case, require // to show raw body contents. if len(ta.response.Body.Bytes()) > 0 && td.EqDeeply(bodyPtr.Interface(), reflect.New(bodyType).Interface()) { showRawBody = true tt.Log("Hmm… It seems nothing has been set during unmarshaling…") } ta.failed |= bodyFailed } if showRawBody || ((ta.failed&bodyFailed) != 0 && ta.autoDumpResponse) { ta.dumpResponse() } return ta } // CmpMarshaledBody tests that the last request response body can be // unmarshaled using unmarshal function and then, that it matches // expectedBody. expectedBody can be any type unmarshal function can // handle, or a [td.TestDeep] operator. // // See [TestAPI.CmpJSONBody] and [TestAPI.CmpXMLBody] sources for // examples of use. // // It fails if no request has been sent yet. func (ta *TestAPI) CmpMarshaledBody(unmarshal func([]byte, any) error, expectedBody any) *TestAPI { ta.t.Helper() return ta.cmpMarshaledBody(false, unmarshal, expectedBody) } // CmpBody tests the last request response body against // expectedBody. expectedBody can be a []byte, a string or a // [td.TestDeep] operator. // // ta := tdhttp.NewTestAPI(t, mux) // // ta.Get("/test"). // CmpStatus(http.StatusOK). // CmpBody("OK!\n") // // ta.Get("/test"). // CmpStatus(http.StatusOK). // CmpBody(td.Contains("OK")) // // It fails if no request has been sent yet. func (ta *TestAPI) CmpBody(expectedBody any) *TestAPI { ta.t.Helper() if expectedBody == nil { return ta.NoBody() } return ta.cmpMarshaledBody( true, // accept empty body func(body []byte, target any) error { switch target := target.(type) { case *string: *target = string(body) case *[]byte: *target = body case *any: *target = body default: // cmpMarshaledBody always calls us with target as a pointer return fmt.Errorf( "CmpBody only accepts expectedBody be a []byte, a string or a TestDeep operator allowing to match these types, but not type %s", reflect.TypeOf(target).Elem()) } return nil }, expectedBody) } // CmpJSONBody tests that the last request response body can be // [json.Unmarshal]'ed and that it matches expectedBody. expectedBody // can be any type one can [json.Unmarshal] into, or a [td.TestDeep] // operator. // // ta := tdhttp.NewTestAPI(t, mux) // // ta.Get("/person/42"). // CmpStatus(http.StatusOK). // CmpJSONBody(Person{ // ID: 42, // Name: "Bob", // Age: 26, // }) // // ta.PostJSON("/person", Person{Name: "Bob", Age: 23}). // CmpStatus(http.StatusCreated). // CmpJSONBody(td.SStruct( // Person{ // Name: "Bob", // Age: 26, // }, // td.StructFields{ // "ID": td.NotZero(), // })) // // The same with anchoring, and so without [td.SStruct]: // // ta := tdhttp.NewTestAPI(tt, mux) // // ta.PostJSON("/person", Person{Name: "Bob", Age: 23}). // CmpStatus(http.StatusCreated). // CmpJSONBody(Person{ // ID: ta.Anchor(td.NotZero(), uint64(0)).(uint64), // Name: "Bob", // Age: 26, // }) // // The same using [td.JSON]: // // ta.PostJSON("/person", Person{Name: "Bob", Age: 23}). // CmpStatus(http.StatusCreated). // CmpJSONBody(td.JSON(` // { // "id": NotZero(), // "name": "Bob", // "age": 26 // }`)) // // It fails if no request has been sent yet. func (ta *TestAPI) CmpJSONBody(expectedBody any) *TestAPI { ta.t.Helper() return ta.CmpMarshaledBody(json.Unmarshal, expectedBody) } // CmpXMLBody tests that the last request response body can be // [xml.Unmarshal]'ed and that it matches expectedBody. expectedBody // can be any type one can [xml.Unmarshal] into, or a [td.TestDeep] // operator. // // ta := tdhttp.NewTestAPI(t, mux) // // ta.Get("/person/42"). // CmpStatus(http.StatusOK). // CmpXMLBody(Person{ // ID: 42, // Name: "Bob", // Age: 26, // }) // // ta.Get("/person/43"). // CmpStatus(http.StatusOK). // CmpXMLBody(td.SStruct( // Person{ // Name: "Bob", // Age: 26, // }, // td.StructFields{ // "ID": td.NotZero(), // })) // // The same with anchoring: // // ta := tdhttp.NewTestAPI(tt, mux) // // ta.Get("/person/42"). // CmpStatus(http.StatusOK). // CmpXMLBody(Person{ // ID: ta.Anchor(td.NotZero(), uint64(0)).(uint64), // Name: "Bob", // Age: 26, // }) // // It fails if no request has been sent yet. func (ta *TestAPI) CmpXMLBody(expectedBody any) *TestAPI { ta.t.Helper() return ta.CmpMarshaledBody(xml.Unmarshal, expectedBody) } // NoBody tests that the last request response body is empty. // // It fails if no request has been sent yet. func (ta *TestAPI) NoBody() *TestAPI { defer ta.t.AnchorsPersistTemporarily()() ta.t.Helper() if !ta.checkRequestSent() { ta.failed |= bodyFailed return ta } ok := ta.t.RootName("Response.Body"). Code(len(ta.response.Body.Bytes()) == 0, func(empty bool) error { if empty { return nil } return &ctxerr.Error{ Message: "%% is not empty", Got: types.RawString("not empty"), Expected: types.RawString("empty"), } }, "body should be empty") if !ok { ta.failed |= bodyFailed // Systematically dump response, no AutoDumpResponse needed ta.dumpResponse() } return ta } // Or executes function fn if ta.Failed() is true at the moment it is called. // // fn can have several types: // - func(body string) or func(t *td.T, body string) // → fn is called with response body as a string. // If no response has been received yet, body is ""; // - func(body []byte) or func(t *td.T, body []byte) // → fn is called with response body as a []byte. // If no response has been received yet, body is nil; // - func(t *td.T, resp *httptest.ResponseRecorder) // → fn is called with the internal object containing the response. // See net/http/httptest for details. // If no response has been received yet, resp is nil. // // If fn type is not one of these types, it calls ta.T().Fatal(). func (ta *TestAPI) Or(fn any) *TestAPI { ta.t.Helper() switch fn := fn.(type) { case func(string): if ta.Failed() { var body string if ta.response != nil && ta.response.Body != nil { body = ta.response.Body.String() } fn(body) } case func(*td.T, string): if ta.Failed() { var body string if ta.response != nil && ta.response.Body != nil { body = ta.response.Body.String() } fn(ta.t, body) } case func([]byte): if ta.Failed() { var body []byte if ta.response != nil && ta.response.Body != nil { body = ta.response.Body.Bytes() } fn(body) } case func(*td.T, []byte): if ta.Failed() { var body []byte if ta.response != nil && ta.response.Body != nil { body = ta.response.Body.Bytes() } fn(ta.t, body) } case func(*td.T, *httptest.ResponseRecorder): if ta.Failed() { fn(ta.t, ta.response) } default: ta.t.Fatal(color.BadUsage( "Or(func([*td.T,]string) | func([*td.T,][]byte) | func(*td.T,*httptest.ResponseRecorder))", fn, 1, true)) } return ta } // OrDumpResponse dumps the response if at least one previous test failed. // // ta := tdhttp.NewTestAPI(t, handler) // // ta.Get("/foo"). // CmpStatus(200). // OrDumpResponse(). // if status check failed, dumps the response // CmpBody("bar") // if it fails, the response is not dumped // // ta.Get("/foo"). // CmpStatus(200). // CmpBody("bar"). // OrDumpResponse() // dumps the response if status and/or body checks fail // // See [TestAPI.AutoDumpResponse] method to automatize this dump. func (ta *TestAPI) OrDumpResponse() *TestAPI { if ta.Failed() { ta.dumpResponse() } return ta } func (ta *TestAPI) dumpResponse() { if ta.responseDumped { return } ta.t.Helper() if ta.response != nil { ta.responseDumped = true internal.DumpResponse(ta.t, ta.response.Result()) return } ta.t.Logf("No response received yet") } // Anchor returns a typed value allowing to anchor the [td.TestDeep] // operator operator in a go classic literal like a struct, slice, // array or map value. // // ta := tdhttp.NewTestAPI(tt, mux) // // ta.Get("/person/42"). // CmpStatus(http.StatusOK). // CmpJSONBody(Person{ // ID: ta.Anchor(td.NotZero(), uint64(0)).(uint64), // Name: "Bob", // Age: 26, // }) // // See [td.T.Anchor] for details. // // See [TestAPI.A] method for a shorter synonym of Anchor. func (ta *TestAPI) Anchor(operator td.TestDeep, model ...any) any { return ta.t.Anchor(operator, model...) } // A is a synonym for [TestAPI.Anchor]. It returns a typed value allowing to // anchor the [td.TestDeep] operator in a go classic literal // like a struct, slice, array or map value. // // ta := tdhttp.NewTestAPI(tt, mux) // // ta.Get("/person/42"). // CmpStatus(http.StatusOK). // CmpJSONBody(Person{ // ID: ta.A(td.NotZero(), uint64(0)).(uint64), // Name: "Bob", // Age: 26, // }) // // See [td.T.Anchor] for details. func (ta *TestAPI) A(operator td.TestDeep, model ...any) any { return ta.Anchor(operator, model...) } // SentAt returns the time just before the last request is handled. It // can be used to check the time a route sets and returns, as in: // // ta.PostJSON("/person/42", Person{Name: "Bob", Age: 23}). // CmpStatus(http.StatusCreated). // CmpJSONBody(Person{ // ID: ta.A(td.NotZero(), uint64(0)).(uint64), // Name: "Bob", // Age: 23, // CreatedAt: ta.A(td.Between(ta.SentAt(), time.Now())).(time.Time), // }) // // checks that CreatedAt field is included between the time when the // request has been sent, and the time when the comparison occurs. func (ta *TestAPI) SentAt() time.Time { return ta.sentAt } golang-github-maxatome-go-testdeep-1.14.0/helpers/tdhttp/test_api_test.go000066400000000000000000001074021454313311600265270ustar00rootroot00000000000000// Copyright (c) 2020, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package tdhttp_test import ( "encoding/json" "fmt" "io" "net/http" "net/http/httptest" "net/url" "strings" "testing" "time" "github.com/maxatome/go-testdeep/helpers/tdhttp" "github.com/maxatome/go-testdeep/helpers/tdutil" "github.com/maxatome/go-testdeep/td" ) func server() *http.ServeMux { mux := http.NewServeMux() mux.HandleFunc("/any", func(w http.ResponseWriter, req *http.Request) { w.Header().Set("X-TestDeep-Method", req.Method) if req.Method == "HEAD" { w.WriteHeader(http.StatusOK) return } w.Header().Set("Content-Type", "text/plain") w.WriteHeader(http.StatusOK) fmt.Fprintf(w, "%s!", req.Method) if req.ContentLength != 0 { w.Write([]byte("\n---\n")) //nolint: errcheck io.Copy(w, req.Body) //nolint: errcheck } }) mux.HandleFunc("/any/json", func(w http.ResponseWriter, req *http.Request) { w.Header().Set("X-TestDeep-Method", req.Method) if req.Method == "HEAD" { w.WriteHeader(http.StatusOK) return } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) m := map[string]any{ "method": req.Method, } if req.ContentLength != 0 { var body any if err := json.NewDecoder(req.Body).Decode(&body); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } m["body"] = body } json.NewEncoder(w).Encode(m) //nolint: errcheck }) mux.HandleFunc("/mirror/json", func(w http.ResponseWriter, req *http.Request) { w.Header().Set("X-TestDeep-Method", req.Method) if req.Method == "HEAD" { w.WriteHeader(http.StatusOK) return } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) io.Copy(w, req.Body) //nolint: errcheck }) mux.HandleFunc("/any/xml", func(w http.ResponseWriter, req *http.Request) { w.Header().Set("X-TestDeep-Method", req.Method) if req.Method == "HEAD" { w.WriteHeader(http.StatusOK) return } w.Header().Set("Content-Type", "application/xml") w.WriteHeader(http.StatusOK) fmt.Fprintf(w, `%s`, req.Method) if req.ContentLength != 0 { io.Copy(w, req.Body) //nolint: errcheck } w.Write([]byte(``)) //nolint: errcheck }) mux.HandleFunc("/any/cookies", func(w http.ResponseWriter, req *http.Request) { w.Header().Set("X-TestDeep-Method", req.Method) if req.Method == "HEAD" { w.WriteHeader(http.StatusOK) return } w.Header().Set("Content-Type", "text/plain") http.SetCookie(w, &http.Cookie{ Name: "first", Value: "cookie1", MaxAge: 123456, Expires: time.Date(2021, time.August, 12, 11, 22, 33, 0, time.UTC), }) http.SetCookie(w, &http.Cookie{ Name: "second", Value: "cookie2", MaxAge: 654321, }) w.WriteHeader(http.StatusOK) fmt.Fprintf(w, "%s!", req.Method) if req.ContentLength != 0 { w.Write([]byte("\n---\n")) //nolint: errcheck io.Copy(w, req.Body) //nolint: errcheck } }) mux.HandleFunc("/any/trailer", func(w http.ResponseWriter, req *http.Request) { w.Header().Set("Trailer", "X-TestDeep-Method") w.Header().Add("Trailer", "X-TestDeep-Foo") io.WriteString(w, "Hey!") //nolint: errcheck w.Header().Set("X-TestDeep-Method", req.Method) w.Header().Set("X-TestDeep-Foo", "bar") }) return mux } func TestNewTestAPI(t *testing.T) { mux := server() containsKey := td.ContainsKey("X-Testdeep-Method") t.Run("No error", func(t *testing.T) { mockT := tdutil.NewT("test") td.CmpFalse(t, tdhttp.NewTestAPI(mockT, mux). Head("/any"). CmpStatus(200). CmpHeader(containsKey). CmpHeader(td.SuperMapOf(http.Header{}, td.MapEntries{ "X-Testdeep-Method": td.Bag(td.Re(`(?i)^head\z`)), })). NoBody(). CmpResponse(td.Code(func(assert *td.T, resp *http.Response) { assert.Cmp(resp.StatusCode, 200) assert.Cmp(resp.Header, containsKey) assert.Smuggle(resp.Body, io.ReadAll, td.Empty()) })). Failed()) td.CmpEmpty(t, mockT.LogBuf()) mockT = tdutil.NewT("test") td.CmpFalse(t, tdhttp.NewTestAPI(mockT, mux). Head("/any"). CmpStatus(200). CmpHeader(containsKey). CmpBody(td.Empty()). CmpResponse(td.Code(func(assert *td.T, resp *http.Response) { assert.Cmp(resp.StatusCode, 200) assert.Cmp(resp.Header, containsKey) assert.Smuggle(resp.Body, io.ReadAll, td.Empty()) })). Failed()) td.CmpEmpty(t, mockT.LogBuf()) mockT = tdutil.NewT("test") td.CmpFalse(t, tdhttp.NewTestAPI(mockT, mux). Get("/any"). CmpStatus(200). CmpHeader(containsKey). CmpBody("GET!"). CmpResponse(td.Code(func(assert *td.T, resp *http.Response) { assert.Cmp(resp.StatusCode, 200) assert.Cmp(resp.Header, containsKey) assert.Smuggle(resp.Body, io.ReadAll, td.String("GET!")) })). Failed()) td.CmpEmpty(t, mockT.LogBuf()) mockT = tdutil.NewT("test") td.CmpFalse(t, tdhttp.NewTestAPI(mockT, mux). Get("/any"). CmpStatus(200). CmpHeader(containsKey). CmpBody(td.Contains("GET")). CmpResponse(td.Code(func(assert *td.T, resp *http.Response) { assert.Cmp(resp.StatusCode, 200) assert.Cmp(resp.Header, containsKey) assert.Smuggle(resp.Body, io.ReadAll, td.Contains("GET")) })). Failed()) td.CmpEmpty(t, mockT.LogBuf()) mockT = tdutil.NewT("test") td.CmpFalse(t, tdhttp.NewTestAPI(mockT, mux). Options("/any", strings.NewReader("OPTIONS body")). CmpStatus(200). CmpHeader(containsKey). CmpBody("OPTIONS!\n---\nOPTIONS body"). CmpResponse(td.Code(func(assert *td.T, resp *http.Response) { assert.Cmp(resp.StatusCode, 200) assert.Cmp(resp.Header, containsKey) assert.Smuggle(resp.Body, io.ReadAll, td.String("OPTIONS!\n---\nOPTIONS body")) })). Failed()) td.CmpEmpty(t, mockT.LogBuf()) mockT = tdutil.NewT("test") td.CmpFalse(t, tdhttp.NewTestAPI(mockT, mux). Post("/any", strings.NewReader("POST body")). CmpStatus(200). CmpHeader(containsKey). CmpBody("POST!\n---\nPOST body"). CmpResponse(td.Code(func(assert *td.T, resp *http.Response) { assert.Cmp(resp.StatusCode, 200) assert.Cmp(resp.Header, containsKey) assert.Smuggle(resp.Body, io.ReadAll, td.String("POST!\n---\nPOST body")) })). Failed()) td.CmpEmpty(t, mockT.LogBuf()) mockT = tdutil.NewT("test") td.CmpFalse(t, tdhttp.NewTestAPI(mockT, mux). PostForm("/any", url.Values{"p1": []string{"v1"}, "p2": []string{"v2"}}). CmpStatus(200). CmpHeader(containsKey). CmpBody("POST!\n---\np1=v1&p2=v2"). CmpResponse(td.Code(func(assert *td.T, resp *http.Response) { assert.Cmp(resp.StatusCode, 200) assert.Cmp(resp.Header, containsKey) assert.Smuggle(resp.Body, io.ReadAll, td.String("POST!\n---\np1=v1&p2=v2")) })). Failed()) td.CmpEmpty(t, mockT.LogBuf()) mockT = tdutil.NewT("test") td.CmpFalse(t, tdhttp.NewTestAPI(mockT, mux). PostForm("/any", tdhttp.Q{"p1": "v1", "p2": "v2"}). CmpStatus(200). CmpHeader(containsKey). CmpBody("POST!\n---\np1=v1&p2=v2"). CmpResponse(td.Code(func(assert *td.T, resp *http.Response) { assert.Cmp(resp.StatusCode, 200) assert.Cmp(resp.Header, containsKey) assert.Smuggle(resp.Body, io.ReadAll, td.String("POST!\n---\np1=v1&p2=v2")) })). Failed()) td.CmpEmpty(t, mockT.LogBuf()) mockT = tdutil.NewT("test") td.CmpFalse(t, tdhttp.NewTestAPI(mockT, mux). PostMultipartFormData("/any", &tdhttp.MultipartBody{ Boundary: "BoUnDaRy", Parts: []*tdhttp.MultipartPart{ tdhttp.NewMultipartPartString("pipo", "bingo"), }, }). CmpStatus(200). CmpHeader(containsKey). CmpBody(strings.ReplaceAll( `POST! --- --BoUnDaRy%CR Content-Disposition: form-data; name="pipo"%CR Content-Type: text/plain; charset=utf-8%CR %CR bingo%CR --BoUnDaRy--%CR `, "%CR", "\r")). Failed()) td.CmpEmpty(t, mockT.LogBuf()) mockT = tdutil.NewT("test") td.CmpFalse(t, tdhttp.NewTestAPI(mockT, mux). Put("/any", strings.NewReader("PUT body")). CmpStatus(200). CmpHeader(containsKey). CmpBody("PUT!\n---\nPUT body"). Failed()) td.CmpEmpty(t, mockT.LogBuf()) mockT = tdutil.NewT("test") td.CmpFalse(t, tdhttp.NewTestAPI(mockT, mux). Patch("/any", strings.NewReader("PATCH body")). CmpStatus(200). CmpHeader(containsKey). CmpBody("PATCH!\n---\nPATCH body"). Failed()) td.CmpEmpty(t, mockT.LogBuf()) mockT = tdutil.NewT("test") td.CmpFalse(t, tdhttp.NewTestAPI(mockT, mux). Delete("/any", strings.NewReader("DELETE body")). CmpStatus(200). CmpHeader(containsKey). CmpBody("DELETE!\n---\nDELETE body"). Failed()) td.CmpEmpty(t, mockT.LogBuf()) }) t.Run("No JSON error", func(t *testing.T) { requestBody := map[string]any{"hey": 123} expectedBody := func(m string) td.TestDeep { return td.JSON(`{"method": $1, "body": {"hey": 123}}`, m) } mockT := tdutil.NewT("test") td.CmpFalse(t, tdhttp.NewTestAPI(mockT, mux). NewJSONRequest("GET", "/mirror/json", json.RawMessage(`null`)). CmpStatus(200). CmpHeader(containsKey). CmpJSONBody(nil). Failed()) td.CmpEmpty(t, mockT.LogBuf()) mockT = tdutil.NewT("test") td.CmpFalse(t, tdhttp.NewTestAPI(mockT, mux). NewJSONRequest("ZIP", "/any/json", requestBody). CmpStatus(200). CmpHeader(containsKey). CmpJSONBody(expectedBody("ZIP")). Failed()) td.CmpEmpty(t, mockT.LogBuf()) mockT = tdutil.NewT("test") td.CmpFalse(t, tdhttp.NewTestAPI(mockT, mux). NewJSONRequest("ZIP", "/any/json", requestBody). CmpStatus(200). CmpHeader(containsKey). CmpJSONBody(td.JSONPointer("/body/hey", 123)). Failed()) td.CmpEmpty(t, mockT.LogBuf()) mockT = tdutil.NewT("test") td.CmpFalse(t, tdhttp.NewTestAPI(mockT, mux). PostJSON("/any/json", requestBody). CmpStatus(200). CmpHeader(containsKey). CmpJSONBody(expectedBody("POST")). Failed()) td.CmpEmpty(t, mockT.LogBuf()) mockT = tdutil.NewT("test") td.CmpFalse(t, tdhttp.NewTestAPI(mockT, mux). PutJSON("/any/json", requestBody). CmpStatus(200). CmpHeader(containsKey). CmpJSONBody(expectedBody("PUT")). Failed()) td.CmpEmpty(t, mockT.LogBuf()) mockT = tdutil.NewT("test") td.CmpFalse(t, tdhttp.NewTestAPI(mockT, mux). PatchJSON("/any/json", requestBody). CmpStatus(200). CmpHeader(containsKey). CmpJSONBody(expectedBody("PATCH")). Failed()) td.CmpEmpty(t, mockT.LogBuf()) mockT = tdutil.NewT("test") td.CmpFalse(t, tdhttp.NewTestAPI(mockT, mux). DeleteJSON("/any/json", requestBody). CmpStatus(200). CmpHeader(containsKey). CmpJSONBody(expectedBody("DELETE")). Failed()) td.CmpEmpty(t, mockT.LogBuf()) // With anchors type ReqBody struct { Hey int `json:"hey"` } type Resp struct { Method string `json:"method"` ReqBody ReqBody `json:"body"` } mockT = tdutil.NewT("test") tt := td.NewT(mockT) td.CmpFalse(t, tdhttp.NewTestAPI(mockT, mux). DeleteJSON("/any/json", requestBody). CmpStatus(200). CmpHeader(containsKey). CmpJSONBody(Resp{ Method: tt.A(td.Re(`^(?i)delete\z`), "").(string), ReqBody: ReqBody{ Hey: tt.A(td.Between(120, 130)).(int), }, }). Failed()) td.CmpEmpty(t, mockT.LogBuf()) // JSON and root operator (here SuperMapOf) mockT = tdutil.NewT("test") td.CmpFalse(t, tdhttp.NewTestAPI(mockT, mux). PostJSON("/any/json", true). CmpStatus(200). CmpJSONBody(td.JSON(`SuperMapOf({"body":Ignore()})`)). Failed()) td.CmpEmpty(t, mockT.LogBuf()) // td.Bag+td.JSON mockT = tdutil.NewT("test") td.CmpFalse(t, tdhttp.NewTestAPI(mockT, mux). PostJSON("/mirror/json", json.RawMessage(`[{"name":"Bob"},{"name":"Alice"}]`)). CmpStatus(200). CmpJSONBody(td.Bag( td.JSON(`{"name":"Alice"}`), td.JSON(`{"name":"Bob"}`), )). Failed()) td.CmpEmpty(t, mockT.LogBuf()) // td.Bag+literal type People struct { Name string `json:"name"` } mockT = tdutil.NewT("test") td.CmpFalse(t, tdhttp.NewTestAPI(mockT, mux). PostJSON("/mirror/json", json.RawMessage(`[{"name":"Bob"},{"name":"Alice"}]`)). CmpStatus(200). CmpJSONBody(td.Bag(People{"Alice"}, People{"Bob"})). Failed()) td.CmpEmpty(t, mockT.LogBuf()) }) t.Run("No XML error", func(t *testing.T) { type XBody struct { Hey int `xml:"hey"` } type XResp struct { Method string `xml:"method"` ReqBody *XBody `xml:"XBody"` } requestBody := XBody{Hey: 123} expectedBody := func(m string) XResp { return XResp{ Method: m, ReqBody: &requestBody, } } mockT := tdutil.NewT("test") td.CmpFalse(t, tdhttp.NewTestAPI(mockT, mux). NewXMLRequest("ZIP", "/any/xml", requestBody). CmpStatus(200). CmpHeader(containsKey). CmpXMLBody(expectedBody("ZIP")). Failed()) td.CmpEmpty(t, mockT.LogBuf()) mockT = tdutil.NewT("test") td.CmpFalse(t, tdhttp.NewTestAPI(mockT, mux). PostXML("/any/xml", requestBody). CmpStatus(200). CmpHeader(containsKey). CmpXMLBody(expectedBody("POST")). Failed()) td.CmpEmpty(t, mockT.LogBuf()) mockT = tdutil.NewT("test") td.CmpFalse(t, tdhttp.NewTestAPI(mockT, mux). PutXML("/any/xml", requestBody). CmpStatus(200). CmpHeader(containsKey). CmpXMLBody(expectedBody("PUT")). Failed()) td.CmpEmpty(t, mockT.LogBuf()) mockT = tdutil.NewT("test") td.CmpFalse(t, tdhttp.NewTestAPI(mockT, mux). PatchXML("/any/xml", requestBody). CmpStatus(200). CmpHeader(containsKey). CmpXMLBody(expectedBody("PATCH")). Failed()) td.CmpEmpty(t, mockT.LogBuf()) mockT = tdutil.NewT("test") td.CmpFalse(t, tdhttp.NewTestAPI(mockT, mux). DeleteXML("/any/xml", requestBody). CmpStatus(200). CmpHeader(containsKey). CmpXMLBody(expectedBody("DELETE")). Failed()) td.CmpEmpty(t, mockT.LogBuf()) // With anchors mockT = tdutil.NewT("test") tt := td.NewT(mockT) td.CmpFalse(tt, tdhttp.NewTestAPI(mockT, mux). DeleteXML("/any/xml", requestBody). CmpStatus(200). CmpHeader(containsKey). CmpXMLBody(XResp{ Method: tt.A(td.Re(`^(?i)delete\z`), "").(string), ReqBody: &XBody{ Hey: tt.A(td.Between(120, 130)).(int), }, }). Failed()) td.CmpEmpty(t, mockT.LogBuf()) }) t.Run("Cookies", func(t *testing.T) { mockT := tdutil.NewT("test") td.CmpFalse(t, tdhttp.NewTestAPI(mockT, mux). Get("/any/cookies"). CmpCookies([]*http.Cookie{ { Name: "first", Value: "cookie1", MaxAge: 123456, Expires: time.Date(2021, time.August, 12, 11, 22, 33, 0, time.UTC), }, { Name: "second", Value: "cookie2", MaxAge: 654321, }, }). Failed()) td.CmpEmpty(t, mockT.LogBuf()) mockT = tdutil.NewT("test") td.CmpTrue(t, tdhttp.NewTestAPI(mockT, mux). Get("/any/cookies"). CmpCookies([]*http.Cookie{ { Name: "first", Value: "cookie1", MaxAge: 123456, Expires: time.Date(2021, time.August, 12, 11, 22, 33, 0, time.UTC), }, }). Failed()) td.CmpContains(t, mockT.LogBuf(), "Failed test 'cookies should match'") td.CmpContains(t, mockT.LogBuf(), "Response.Cookie: comparing slices, from index #1") // 2 cookies are here whatever their order is using Bag mockT = tdutil.NewT("test") td.CmpFalse(t, tdhttp.NewTestAPI(mockT, mux). Get("/any/cookies"). CmpCookies(td.Bag( td.Smuggle("Name", "second"), td.Smuggle("Name", "first"), )). Failed()) td.CmpEmpty(t, mockT.LogBuf()) // Testing only Name & Value whatever their order is using Bag mockT = tdutil.NewT("test") td.CmpFalse(t, tdhttp.NewTestAPI(mockT, mux). Get("/any/cookies"). CmpCookies(td.Bag( td.Struct(&http.Cookie{Name: "first", Value: "cookie1"}, nil), td.Struct(&http.Cookie{Name: "second", Value: "cookie2"}, nil), )). Failed()) td.CmpEmpty(t, mockT.LogBuf()) // Testing the presence of only one using SuperBagOf mockT = tdutil.NewT("test") td.CmpFalse(t, tdhttp.NewTestAPI(mockT, mux). Get("/any/cookies"). CmpCookies(td.SuperBagOf( td.Struct(&http.Cookie{Name: "first", Value: "cookie1"}, nil), )). Failed()) td.CmpEmpty(t, mockT.LogBuf()) // Testing only the number of cookies mockT = tdutil.NewT("test") td.CmpFalse(t, tdhttp.NewTestAPI(mockT, mux). Get("/any/cookies"). CmpCookies(td.Len(2)). Failed()) td.CmpEmpty(t, mockT.LogBuf()) // Error followed by a success: Failed() should return true anyway mockT = tdutil.NewT("test") td.CmpTrue(t, tdhttp.NewTestAPI(mockT, mux). Get("/any"). CmpCookies(td.Len(100)). // fails CmpCookies(td.Len(2)). // succeeds Failed()) td.CmpContains(t, mockT.LogBuf(), "Failed test 'cookies should match'") // AutoDumpResponse mockT = tdutil.NewT("test") td.CmpTrue(t, tdhttp.NewTestAPI(mockT, mux). AutoDumpResponse(). Get("/any/cookies"). Name("my test"). CmpCookies(td.Len(100)). Failed()) td.CmpContains(t, mockT.LogBuf(), "Failed test 'my test: cookies should match'") td.CmpContains(t, mockT.LogBuf(), "Response.Cookie: bad length") td.Cmp(t, mockT.LogBuf(), td.Contains("Received response:\n")) // Request not sent mockT = tdutil.NewT("test") ta := tdhttp.NewTestAPI(mockT, mux). Name("my test"). CmpCookies(td.Len(2)) td.CmpTrue(t, ta.Failed()) td.CmpContains(t, mockT.LogBuf(), "Failed test 'my test: request is sent'\n") td.CmpContains(t, mockT.LogBuf(), "Request not sent!\n") td.CmpContains(t, mockT.LogBuf(), "A request must be sent before testing status, header, body or full response\n") td.CmpNot(t, mockT.LogBuf(), td.Contains("No response received yet\n")) }) t.Run("Trailer", func(t *testing.T) { mockT := tdutil.NewT("test") td.CmpFalse(t, tdhttp.NewTestAPI(mockT, mux). Get("/any"). CmpStatus(200). CmpTrailer(nil). // No trailer at all Failed()) mockT = tdutil.NewT("test") td.CmpFalse(t, tdhttp.NewTestAPI(mockT, mux). Get("/any/trailer"). CmpStatus(200). CmpTrailer(containsKey). Failed()) mockT = tdutil.NewT("test") td.CmpFalse(t, tdhttp.NewTestAPI(mockT, mux). Get("/any/trailer"). CmpStatus(200). CmpTrailer(http.Header{ "X-Testdeep-Method": {"GET"}, "X-Testdeep-Foo": {"bar"}, }). Failed()) // AutoDumpResponse mockT = tdutil.NewT("test") td.CmpTrue(t, tdhttp.NewTestAPI(mockT, mux). AutoDumpResponse(). Get("/any/trailer"). Name("my test"). CmpTrailer(http.Header{}). Failed()) td.CmpContains(t, mockT.LogBuf(), "Failed test 'my test: trailer should match'") td.Cmp(t, mockT.LogBuf(), td.Contains("Received response:\n")) // OrDumpResponse mockT = tdutil.NewT("test") td.CmpTrue(t, tdhttp.NewTestAPI(mockT, mux). Get("/any/trailer"). Name("my test"). CmpTrailer(http.Header{}). OrDumpResponse(). OrDumpResponse(). // only one log Failed()) td.CmpContains(t, mockT.LogBuf(), "Failed test 'my test: trailer should match'") logPos := strings.Index(mockT.LogBuf(), "Received response:\n") if td.Cmp(t, logPos, td.Gte(0)) { // Only one occurrence td.Cmp(t, strings.Index(mockT.LogBuf()[logPos+1:], "Received response:\n"), -1) } mockT = tdutil.NewT("test") ta := tdhttp.NewTestAPI(mockT, mux). Name("my test"). CmpTrailer(http.Header{}) td.CmpTrue(t, ta.Failed()) td.CmpContains(t, mockT.LogBuf(), "Failed test 'my test: request is sent'\n") td.CmpContains(t, mockT.LogBuf(), "Request not sent!\n") td.CmpContains(t, mockT.LogBuf(), "A request must be sent before testing status, header, body or full response\n") td.CmpNot(t, mockT.LogBuf(), td.Contains("No response received yet\n")) end := len(mockT.LogBuf()) ta.OrDumpResponse() td.CmpContains(t, mockT.LogBuf()[end:], "No response received yet\n") }) t.Run("Status error", func(t *testing.T) { mockT := tdutil.NewT("test") td.CmpTrue(t, tdhttp.NewTestAPI(mockT, mux). Get("/any"). CmpStatus(400). Failed()) td.CmpContains(t, mockT.LogBuf(), "Failed test 'status code should match'") // Error followed by a success: Failed() should return true anyway mockT = tdutil.NewT("test") td.CmpTrue(t, tdhttp.NewTestAPI(mockT, mux). Get("/any"). CmpStatus(400). // fails CmpStatus(200). // succeeds Failed()) td.CmpContains(t, mockT.LogBuf(), "Failed test 'status code should match'") mockT = tdutil.NewT("test") td.CmpTrue(t, tdhttp.NewTestAPI(mockT, mux). Get("/any"). Name("my test"). CmpStatus(400). Failed()) td.CmpContains(t, mockT.LogBuf(), "Failed test 'my test: status code should match'") td.CmpNot(t, mockT.LogBuf(), td.Contains("Received response:\n")) // AutoDumpResponse mockT = tdutil.NewT("test") td.CmpTrue(t, tdhttp.NewTestAPI(mockT, mux). AutoDumpResponse(). Get("/any"). Name("my test"). CmpStatus(400). Failed()) td.CmpContains(t, mockT.LogBuf(), "Failed test 'my test: status code should match'") td.Cmp(t, mockT.LogBuf(), td.Contains("Received response:\n")) // OrDumpResponse mockT = tdutil.NewT("test") td.CmpTrue(t, tdhttp.NewTestAPI(mockT, mux). Get("/any"). Name("my test"). CmpStatus(400). OrDumpResponse(). OrDumpResponse(). // only one log Failed()) td.CmpContains(t, mockT.LogBuf(), "Failed test 'my test: status code should match'") logPos := strings.Index(mockT.LogBuf(), "Received response:\n") if td.Cmp(t, logPos, td.Gte(0)) { // Only one occurrence td.Cmp(t, strings.Index(mockT.LogBuf()[logPos+1:], "Received response:\n"), -1) } mockT = tdutil.NewT("test") ta := tdhttp.NewTestAPI(mockT, mux). Name("my test"). CmpStatus(400) td.CmpTrue(t, ta.Failed()) td.CmpContains(t, mockT.LogBuf(), "Failed test 'my test: request is sent'\n") td.CmpContains(t, mockT.LogBuf(), "Request not sent!\n") td.CmpContains(t, mockT.LogBuf(), "A request must be sent before testing status, header, body or full response\n") td.CmpNot(t, mockT.LogBuf(), td.Contains("No response received yet\n")) end := len(mockT.LogBuf()) ta.OrDumpResponse() td.CmpContains(t, mockT.LogBuf()[end:], "No response received yet\n") }) t.Run("Header error", func(t *testing.T) { mockT := tdutil.NewT("test") td.CmpTrue(t, tdhttp.NewTestAPI(mockT, mux). Get("/any"). CmpHeader(td.Not(containsKey)). Failed()) td.CmpContains(t, mockT.LogBuf(), "Failed test 'header should match'") // Error followed by a success: Failed() should return true anyway mockT = tdutil.NewT("test") td.CmpTrue(t, tdhttp.NewTestAPI(mockT, mux). Get("/any"). CmpHeader(td.Not(containsKey)). // fails CmpHeader(td.Ignore()). // succeeds Failed()) td.CmpContains(t, mockT.LogBuf(), "Failed test 'header should match'") mockT = tdutil.NewT("test") td.CmpTrue(t, tdhttp.NewTestAPI(mockT, mux). Get("/any"). Name("my test"). CmpHeader(td.Not(containsKey)). Failed()) td.CmpContains(t, mockT.LogBuf(), "Failed test 'my test: header should match'") td.CmpNot(t, mockT.LogBuf(), td.Contains("Received response:\n")) // AutoDumpResponse mockT = tdutil.NewT("test") td.CmpTrue(t, tdhttp.NewTestAPI(mockT, mux). AutoDumpResponse(). Get("/any"). Name("my test"). CmpHeader(td.Not(containsKey)). Failed()) td.CmpContains(t, mockT.LogBuf(), "Failed test 'my test: header should match'") td.Cmp(t, mockT.LogBuf(), td.Contains("Received response:\n")) mockT = tdutil.NewT("test") td.CmpTrue(t, tdhttp.NewTestAPI(mockT, mux). Name("my test"). CmpHeader(td.Not(containsKey)). Failed()) td.CmpContains(t, mockT.LogBuf(), "Failed test 'my test: request is sent'\n") td.CmpContains(t, mockT.LogBuf(), "Request not sent!\n") td.CmpContains(t, mockT.LogBuf(), "A request must be sent before testing status, header, body or full response\n") }) t.Run("Body error", func(t *testing.T) { mockT := tdutil.NewT("test") td.CmpTrue(t, tdhttp.NewTestAPI(mockT, mux). Get("/any"). CmpBody("xxx"). Failed()) td.CmpContains(t, mockT.LogBuf(), "Failed test 'body contents is OK'") td.CmpContains(t, mockT.LogBuf(), "Response.Body: values differ\n") td.CmpContains(t, mockT.LogBuf(), `expected: "xxx"`) td.CmpContains(t, mockT.LogBuf(), `got: "GET!"`) // Error followed by a success: Failed() should return true anyway mockT = tdutil.NewT("test") td.CmpTrue(t, tdhttp.NewTestAPI(mockT, mux). Get("/any"). CmpBody("xxx"). // fails CmpBody(td.Ignore()). // succeeds Failed()) // Without AutoDumpResponse mockT = tdutil.NewT("test") td.CmpTrue(t, tdhttp.NewTestAPI(mockT, mux). Get("/any"). Name("my test"). CmpBody("xxx"). Failed()) td.CmpContains(t, mockT.LogBuf(), "Failed test 'my test: body contents is OK'") td.CmpNot(t, mockT.LogBuf(), td.Contains("Received response:\n")) // AutoDumpResponse mockT = tdutil.NewT("test") td.CmpTrue(t, tdhttp.NewTestAPI(mockT, mux). AutoDumpResponse(). Get("/any"). Name("my test"). CmpBody("xxx"). Failed()) td.CmpContains(t, mockT.LogBuf(), "Failed test 'my test: body contents is OK'") td.Cmp(t, mockT.LogBuf(), td.Contains("Received response:\n")) mockT = tdutil.NewT("test") td.CmpTrue(t, tdhttp.NewTestAPI(mockT, mux). Name("my test"). CmpBody("xxx"). Failed()) td.CmpContains(t, mockT.LogBuf(), "Failed test 'my test: request is sent'\n") td.CmpContains(t, mockT.LogBuf(), "Request not sent!\n") td.CmpContains(t, mockT.LogBuf(), "A request must be sent before testing status, header, body or full response\n") // NoBody mockT = tdutil.NewT("test") td.CmpTrue(t, tdhttp.NewTestAPI(mockT, mux). Name("my test"). NoBody(). Failed()) td.CmpContains(t, mockT.LogBuf(), "Failed test 'my test: request is sent'\n") td.CmpContains(t, mockT.LogBuf(), "Request not sent!\n") td.CmpContains(t, mockT.LogBuf(), "A request must be sent before testing status, header, body or full response\n") td.CmpNot(t, mockT.LogBuf(), td.Contains("Received response:\n")) // Error followed by a success: Failed() should return true anyway mockT = tdutil.NewT("test") td.CmpTrue(t, tdhttp.NewTestAPI(mockT, mux). Name("my test"). Head("/any"). CmpBody("fail"). // fails NoBody(). // succeeds Failed()) // No JSON body mockT = tdutil.NewT("test") td.CmpTrue(t, tdhttp.NewTestAPI(mockT, mux). Head("/any"). CmpStatus(200). CmpHeader(containsKey). CmpJSONBody(json.RawMessage(`{}`)). Failed()) td.CmpContains(t, mockT.LogBuf(), "Failed test 'body should not be empty'") td.CmpContains(t, mockT.LogBuf(), "Response body is empty!") td.CmpContains(t, mockT.LogBuf(), "Body cannot be empty when using CmpJSONBody") td.CmpNot(t, mockT.LogBuf(), td.Contains("Received response:\n")) // Error followed by a success: Failed() should return true anyway mockT = tdutil.NewT("test") td.CmpTrue(t, tdhttp.NewTestAPI(mockT, mux). Get("/any/json"). CmpStatus(200). CmpHeader(containsKey). CmpJSONBody(json.RawMessage(`{}`)). // fails CmpJSONBody(td.Ignore()). // succeeds Failed()) // No JSON body + AutoDumpResponse mockT = tdutil.NewT("test") td.CmpTrue(t, tdhttp.NewTestAPI(mockT, mux). AutoDumpResponse(). Head("/any"). CmpStatus(200). CmpHeader(containsKey). CmpJSONBody(json.RawMessage(`{}`)). Failed()) td.CmpContains(t, mockT.LogBuf(), "Failed test 'body should not be empty'") td.CmpContains(t, mockT.LogBuf(), "Response body is empty!") td.CmpContains(t, mockT.LogBuf(), "Body cannot be empty when using CmpJSONBody") td.Cmp(t, mockT.LogBuf(), td.Contains("Received response:\n")) // No XML body mockT = tdutil.NewT("test") td.CmpTrue(t, tdhttp.NewTestAPI(mockT, mux). Head("/any"). CmpStatus(200). CmpHeader(containsKey). CmpXMLBody(struct{ Test string }{}). Failed()) td.CmpContains(t, mockT.LogBuf(), "Failed test 'body should not be empty'") td.CmpContains(t, mockT.LogBuf(), "Response body is empty!") td.CmpContains(t, mockT.LogBuf(), "Body cannot be empty when using CmpXMLBody") }) t.Run("Response error", func(t *testing.T) { mockT := tdutil.NewT("test") td.CmpTrue(t, tdhttp.NewTestAPI(mockT, mux). Get("/any"). CmpResponse(nil). Failed()) td.CmpContains(t, mockT.LogBuf(), "Failed test 'full response should match'") td.CmpContains(t, mockT.LogBuf(), "Response: values differ") td.CmpContains(t, mockT.LogBuf(), "got: (*http.Response)(") td.CmpContains(t, mockT.LogBuf(), "expected: nil") // Error followed by a success: Failed() should return true anyway mockT = tdutil.NewT("test") td.CmpTrue(t, tdhttp.NewTestAPI(mockT, mux). Get("/any"). CmpResponse(nil). // fails CmpResponse(td.Ignore()). // succeeds Failed()) // Without AutoDumpResponse mockT = tdutil.NewT("test") td.CmpTrue(t, tdhttp.NewTestAPI(mockT, mux). Get("/any"). Name("my test"). CmpResponse(nil). Failed()) td.CmpContains(t, mockT.LogBuf(), "Failed test 'my test: full response should match'") td.CmpNot(t, mockT.LogBuf(), td.Contains("Received response:\n")) // AutoDumpResponse mockT = tdutil.NewT("test") td.CmpTrue(t, tdhttp.NewTestAPI(mockT, mux). AutoDumpResponse(). Get("/any"). Name("my test"). CmpResponse(nil). Failed()) td.CmpContains(t, mockT.LogBuf(), "Failed test 'my test: full response should match'") td.Cmp(t, mockT.LogBuf(), td.Contains("Received response:\n")) mockT = tdutil.NewT("test") td.CmpTrue(t, tdhttp.NewTestAPI(mockT, mux). Name("my test"). CmpResponse(nil). Failed()) td.CmpContains(t, mockT.LogBuf(), "Failed test 'my test: request is sent'\n") td.CmpContains(t, mockT.LogBuf(), "Request not sent!\n") td.CmpContains(t, mockT.LogBuf(), "A request must be sent before testing status, header, body or full response\n") }) t.Run("Request error", func(t *testing.T) { var ta *tdhttp.TestAPI checkFatal := func(fn func()) { mockT := tdutil.NewT("test") td.CmpTrue(t, mockT.CatchFailNow(func() { ta = tdhttp.NewTestAPI(mockT, mux) fn() })) td.Cmp(t, mockT.LogBuf(), td.Contains("headersQueryParams... can only contains string, http.Header, http.Cookie, url.Values and tdhttp.Q, not bool"), ) } empty := strings.NewReader("") checkFatal(func() { ta.Get("/path", true) }) checkFatal(func() { ta.Head("/path", true) }) checkFatal(func() { ta.Options("/path", empty, true) }) checkFatal(func() { ta.Post("/path", empty, true) }) checkFatal(func() { ta.PostForm("/path", nil, true) }) checkFatal(func() { ta.PostMultipartFormData("/path", &tdhttp.MultipartBody{}, true) }) checkFatal(func() { ta.Put("/path", empty, true) }) checkFatal(func() { ta.Patch("/path", empty, true) }) checkFatal(func() { ta.Delete("/path", empty, true) }) checkFatal(func() { ta.NewJSONRequest("ZIP", "/path", nil, true) }) checkFatal(func() { ta.PostJSON("/path", nil, true) }) checkFatal(func() { ta.PutJSON("/path", nil, true) }) checkFatal(func() { ta.PatchJSON("/path", nil, true) }) checkFatal(func() { ta.DeleteJSON("/path", nil, true) }) checkFatal(func() { ta.NewXMLRequest("ZIP", "/path", nil, true) }) checkFatal(func() { ta.PostXML("/path", nil, true) }) checkFatal(func() { ta.PutXML("/path", nil, true) }) checkFatal(func() { ta.PatchXML("/path", nil, true) }) checkFatal(func() { ta.DeleteXML("/path", nil, true) }) }) } func TestWith(t *testing.T) { mux := server() ta := tdhttp.NewTestAPI(tdutil.NewT("test1"), mux) td.CmpFalse(t, ta.Head("/any").CmpStatus(200).Failed()) nt := tdutil.NewT("test2") nta := ta.With(nt) td.Cmp(t, nta.T(), td.Not(td.Shallow(ta.T()))) td.CmpTrue(t, nta.CmpStatus(200).Failed()) // as no request sent yet td.CmpContains(t, nt.LogBuf(), "A request must be sent before testing status, header, body or full response") td.CmpFalse(t, ta.CmpStatus(200).Failed()) // request already sent, so OK nt = tdutil.NewT("test3") nta = ta.With(nt) td.CmpTrue(t, nta.Head("/any"). CmpStatus(400). OrDumpResponse(). Failed()) td.CmpContains(t, nt.LogBuf(), "Response.Status: values differ") td.CmpContains(t, nt.LogBuf(), "X-Testdeep-Method: HEAD") // Header dumped } func TestOr(t *testing.T) { mux := server() t.Run("Success", func(t *testing.T) { var orCalled bool for i, fn := range []any{ func(body string) { orCalled = true }, func(t *td.T, body string) { orCalled = true }, func(body []byte) { orCalled = true }, func(t *td.T, body []byte) { orCalled = true }, func(t *td.T, r *httptest.ResponseRecorder) { orCalled = true }, } { orCalled = false // As CmpStatus succeeds, Or function is not called td.CmpFalse(t, tdhttp.NewTestAPI(tdutil.NewT("test"), mux). Head("/any"). CmpStatus(200). Or(fn). Failed(), "Not failed #%d", i) td.CmpFalse(t, orCalled, "called #%d", i) } }) t.Run("No request sent", func(t *testing.T) { var ok, orCalled bool for i, fn := range []any{ func(body string) { orCalled = true; ok = body == "" }, func(t *td.T, body string) { orCalled = true; ok = t != nil && body == "" }, func(body []byte) { orCalled = true; ok = body == nil }, func(t *td.T, body []byte) { orCalled = true; ok = t != nil && body == nil }, func(t *td.T, r *httptest.ResponseRecorder) { orCalled = true; ok = t != nil && r == nil }, } { orCalled, ok = false, false // Check status without sending a request → fail td.CmpTrue(t, tdhttp.NewTestAPI(tdutil.NewT("test"), mux). CmpStatus(123). Or(fn). Failed(), "Failed #%d", i) td.CmpTrue(t, orCalled, "called #%d", i) td.CmpTrue(t, ok, "OK #%d", i) } }) t.Run("Empty bodies", func(t *testing.T) { var ok, orCalled bool for i, fn := range []any{ func(body string) { orCalled = true; ok = body == "" }, func(t *td.T, body string) { orCalled = true; ok = t != nil && body == "" }, func(body []byte) { orCalled = true; ok = body == nil }, func(t *td.T, body []byte) { orCalled = true; ok = t != nil && body == nil }, func(t *td.T, r *httptest.ResponseRecorder) { orCalled = true ok = t != nil && r != nil && r.Body.Len() == 0 }, } { orCalled, ok = false, false // HEAD /any = no body + CmpStatus fails td.CmpTrue(t, tdhttp.NewTestAPI(tdutil.NewT("test"), mux). Head("/any"). CmpStatus(123). Or(fn). Failed(), "Failed #%d", i) td.CmpTrue(t, orCalled, "called #%d", i) td.CmpTrue(t, ok, "OK #%d", i) } }) t.Run("Body", func(t *testing.T) { var ok, orCalled bool for i, fn := range []any{ func(body string) { orCalled = true; ok = body == "GET!" }, func(t *td.T, body string) { orCalled = true; ok = t != nil && body == "GET!" }, func(body []byte) { orCalled = true; ok = string(body) == "GET!" }, func(t *td.T, body []byte) { orCalled = true; ok = t != nil && string(body) == "GET!" }, func(t *td.T, r *httptest.ResponseRecorder) { orCalled = true ok = t != nil && r != nil && r.Body.String() == "GET!" }, } { orCalled, ok = false, false // GET /any = "GET!" body + CmpStatus fails td.CmpTrue(t, tdhttp.NewTestAPI(tdutil.NewT("test"), mux). Get("/any"). CmpStatus(123). Or(fn). Failed(), "Failed #%d", i) td.CmpTrue(t, orCalled, "called #%d", i) td.CmpTrue(t, ok, "OK #%d", i) } }) tt := tdutil.NewT("test") ta := tdhttp.NewTestAPI(tt, mux) if td.CmpTrue(t, tt.CatchFailNow(func() { ta.Or(123) })) { td.CmpContains(t, tt.LogBuf(), "usage: Or(func([*td.T,]string) | func([*td.T,][]byte) | func(*td.T,*httptest.ResponseRecorder)), but received int as 1st parameter") } } func TestRun(t *testing.T) { mux := server() ta := tdhttp.NewTestAPI(tdutil.NewT("test"), mux) ok := ta.Run("Test", func(ta *tdhttp.TestAPI) { td.CmpFalse(t, ta.Get("/any").CmpStatus(200).Failed()) }) td.CmpTrue(t, ok) ok = ta.Run("Test", func(ta *tdhttp.TestAPI) { td.CmpTrue(t, ta.Get("/any").CmpStatus(123).Failed()) }) td.CmpFalse(t, ok) } golang-github-maxatome-go-testdeep-1.14.0/helpers/tdsuite/000077500000000000000000000000001454313311600234775ustar00rootroot00000000000000golang-github-maxatome-go-testdeep-1.14.0/helpers/tdsuite/any.go000066400000000000000000000004231454313311600246140ustar00rootroot00000000000000// Copyright (c) 2022, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. //go:build !go1.18 // +build !go1.18 package tdsuite type any = interface{} golang-github-maxatome-go-testdeep-1.14.0/helpers/tdsuite/any_test.go000066400000000000000000000004301454313311600256510ustar00rootroot00000000000000// Copyright (c) 2022, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. //go:build !go1.18 // +build !go1.18 package tdsuite_test type any = interface{} golang-github-maxatome-go-testdeep-1.14.0/helpers/tdsuite/doc.go000066400000000000000000000101241454313311600245710ustar00rootroot00000000000000// Copyright (c) 2021, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. // Package tdsuite adds tests suite feature to [go-testdeep] in a // non-intrusive way, but easily and powerfully. // // A tests suite is a set of tests run sequentially that share some data. // // Some hooks can be set to be automatically called before the suite // is run, before, after and/or between each test, and at the end of // the suite. // // In addition, a test can discontinue the suite. // // Giving a suite using a MySuite type, the test methods have the form: // // func (s *MySuite) TestXxx(t *td.T) // func (s *MySuite) TestXxx(assert, require *td.T) // // where Xxx does not start with a lowercase letter. Each test method // is run in a subtest, the method name serves to identify the // subtest. // // A test method can return a bool, as in: // // func (s *MySuite) TestXxx(t *td.T) bool // func (s *MySuite) TestXxx(assert, require *td.T) bool // // in this case, returning false means discontinuing the suite without // any error. Consider it as a skip feature. // // A test method can instead return an error, as in: // // func (s *MySuite) TestXxx(t *td.T) error // func (s *MySuite) TestXxx(assert, require *td.T) error // // in this case, returning a non-nil error marks the test as having // failed, logs the error and discontinues the suite. // // A test method can also return a tuple (bool, error), as in: // // func (s *MySuite) TestXxx(t *td.T) (bool, error) // func (s *MySuite) TestXxx(assert, require *td.T) (bool, error) // // in this case, both returned values are independent. Returning a // false boolean means discontinuing the suite while returning a // non-nil error marks the test as having failed and logs the // error. So: // // Returning do... // (false, nil) continue the suite, do not log anything // (false, ERROR) continue the suite, marks the test as failed & log ERROR // (true, nil) discontinue the suite & log the discontinuation // (true, ERROR) discontinue the suite & log the discontinuation, marks the // test as failed & log ERROR // // Test methods are run in lexicographic order. // // # Very simple tests suite // // Used typically to group tests and benefit from already instanciated // [*td.T] instances. // // import ( // "testing" // // "github.com/maxatome/go-testdeep/td" // "github.com/maxatome/go-testdeep/helpers/tdsuite" // ) // // type MySuite struct{} // // func (s MySuite) TestDB(assert, require *td.T) { // db, err := initDB() // require.CmpNoError(err) // assert.CmpNoError(db.Ping()) // } // // func (s MySuite) TestPerson(assert *td.T) { // person := Getperson("Bob") // assert.Cmp(person, Person{Name: "Bob", Age: 44}) // } // // // TestMySuite is the go test entry point. // func TestMySuite(t *testing.T) { // tdsuite.Run(t, MySuite{}) // } // // # Suite setup and other hooks // // In most cases, a suite is used for sharing information between // tests. The type of the suite can implement several methods that are // called before, after and/or between tests. // // type SuiteDB struct{ // DB *sql.DB // } // // // Setup is called once before any test runs. // func (s *SuiteDB) Setup(t *td.T) error { // db, err := sql.Open(driver, dataSourceName) // s.DB = db // return err // automatically logged + failure if non-nil // } // // // Destroy is called after all tests are run. // // Destroy is not called if Setup returned an error. // func (s *SuiteDB) Destroy(t *td.T) error { // return s.DB.Close() // automatically logged + failure if non-nil // } // // func (s *SuiteDB) TestPerson(assert, require *td.T) { // person, err := GetPerson(s.DB, "Bob") // require.CmpNoError(err) // assert.Cmp(person, Person{Name: "Bob", Age: 44}) // } // // // TestMySuite is the go test entry point. // func TestSuiteDB(t *testing.T) { // tdsuite.Run(t, &SuiteDB{}) // } // // See documentation below for other possible hooks: [PreTest], [PostTest] // and [BetweenTests]. // // [go-testdeep]: https://go-testdeep.zetta.rocks/ package tdsuite golang-github-maxatome-go-testdeep-1.14.0/helpers/tdsuite/suite.go000066400000000000000000000255751454313311600251750ustar00rootroot00000000000000// Copyright (c) 2021, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package tdsuite import ( "fmt" "reflect" "strings" "testing" "unicode" "unicode/utf8" "github.com/maxatome/go-testdeep/internal/types" "github.com/maxatome/go-testdeep/td" ) var tType = reflect.TypeOf((*td.T)(nil)) // Setup is an interface a tests suite can implement. When running the // tests suite, Setup method is called once before any test runs. If // Setup returns an error, the tests suite aborts: no tests are run. // // t.Cleanup() can be called in Setup method. It can replace the // definition of a [Destroy] method. It can also be used together, in // this case cleanup registered functions are called after [Destroy]. type Setup interface { Setup(t *td.T) error } // PreTest is an interface a tests suite can implement. PreTest method // is called before each test is run, in the same subtest as the test // itself. If PreTest returns an error, the subtest aborts: the test // is not run. // // t.Cleanup() can be called in PreTest method. It can replace the // definition of a [PostTest] method. It can also be used together, in // this case cleanup registered functions are called after [PostTest]. type PreTest interface { PreTest(t *td.T, testName string) error } // PostTest is an interface a tests suite can implement. PostTest // method is called after each test is run, in the same subtest as // the test itself. If [PreTest] interface is implemented and [PreTest] // method returned an error, PostTest is never called. type PostTest interface { PostTest(t *td.T, testName string) error } // BetweenTests is an interface a tests suite can // implement. BetweenTests method is called between 2 tests. If it // returns an error, the tests suite aborts: no more tests are run. type BetweenTests interface { BetweenTests(t *td.T, previousTestName, nextTestName string) error } // Destroy is an interface a tests suite can implement. When running // the tests suite, Destroy method is called once after all tests // ran. If [Setup] interface is implemented and [Setup] method returned an // error, Destroy is never called. type Destroy interface { Destroy(t *td.T) error } func emptyPrePostTest(t *td.T, testName string) error { return nil } func emptyBetweenTests(t *td.T, prev, next string) error { return nil } // isTest returns true if "name" is a valid test name. // Derived from go sources in cmd/go/internal/load/test.go. func isTest(name string) bool { if !strings.HasPrefix(name, "Test") { return false } if len(name) == 4 { // "Test" is ok return true } r, _ := utf8.DecodeRuneInString(name[4:]) return !unicode.IsLower(r) } // shouldContinue returns true if the tests suite should continue // based on ret, the value(s) returned by a test call. func shouldContinue(t *td.T, testName string, ret []reflect.Value) bool { var ( err error cont bool ) switch len(ret) { case 0: return true case 1: switch v := ret[0].Interface().(type) { case bool: return v case error: // non-nil error cont, err = false, v default: // nil error return true } default: cont = ret[0].Interface().(bool) err, _ = ret[1].Interface().(error) // nil error fails conversion } if err != nil { t.Helper() t.Errorf("%s error: %s", testName, err) } return cont } // Run runs the tests suite suite using tb as base testing // framework. tb is typically a [*testing.T] as in: // // func TestSuite(t *testing.T) { // tdsuite.Run(t, &Suite{}) // } // // but it can also be a [*td.T] of course. // // config can be used to alter the internal [*td.T] instance. See // [td.ContextConfig] for detailed options, as: // // func TestSuite(t *testing.T) { // tdsuite.Run(t, &Suite{}, td.ContextConfig{ // UseEqual: true, // use the Equal method to compare if available // BeLax: true, // able to compare different but convertible types // }) // } // // Run returns true if all the tests succeeded, false otherwise. // // Note that if suite is not an empty struct, it should be a pointer // if its contents has to be altered by hooks & tests methods. // // If suite is a pointer, it has access to non-pointer & pointer // methods hooks & tests. If suite is not a pointer, it only has // access to non-pointer methods hooks & tests. func Run(tb testing.TB, suite any, config ...td.ContextConfig) bool { t := td.NewT(tb, config...) t.Helper() if suite == nil { t.Fatal("Run(): suite parameter cannot be nil") return false // only for tests } typ := reflect.TypeOf(suite) // The suite is not a pointer and in its pointer version it has // access to more method. Check the user isn't making a mistake by // not passing a pointer if possibleMistakes := diffWithPtrMethods(typ); len(possibleMistakes) > 0 { t.Logf("Run(): several methods are not accessible as suite is not a pointer but %T: %s", suite, strings.Join(possibleMistakes, ", ")) } var methods []int for i, num := 0, typ.NumMethod(); i < num; i++ { m := typ.Method(i) if isTest(m.Name) { mt := m.Type if mt.IsVariadic() { t.Logf("Run(): method %T.%s skipped, variadic parameters not supported", suite, m.Name) continue } // Check input parameters switch mt.NumIn() { case 2: // TestXxx(*td.T) if mt.In(1) != tType { t.Logf("Run(): method %T.%s skipped, unrecognized parameter type %s. Only *td.T allowed", suite, m.Name, mt.In(1)) continue } case 3: // TestXxx(*td.T, *td.T) if mt.In(1) != tType || mt.In(2) != tType { var log string if mt.In(1) != tType { if mt.In(2) != tType { log = fmt.Sprintf("parameters types (%s, %s)", mt.In(1), mt.In(2)) } else { log = fmt.Sprintf("first parameter type %s", mt.In(1)) } } else { log = fmt.Sprintf("second parameter type %s", mt.In(2)) } t.Logf("Run(): method %T.%s skipped, unrecognized %s. Only (*td.T, *td.T) allowed", suite, m.Name, log) continue } case 1: t.Logf("Run(): method %T.%s skipped, no input parameters", suite, m.Name) continue default: t.Logf("Run(): method %T.%s skipped, too many parameters", suite, m.Name) continue } // Check output parameters switch mt.NumOut() { case 0: case 1: switch mt.Out(0) { case types.Bool, types.Error: default: t.Fatalf("Run(): method %T.%s returns %s value. Only bool or error are allowed", suite, m.Name, mt.Out(0)) return false // only for tests } case 2: if mt.Out(0) != types.Bool || mt.Out(1) != types.Error { t.Fatalf("Run(): method %T.%s returns (%s, %s) values. Only (bool, error) is allowed", suite, m.Name, mt.Out(0), mt.Out(1)) return false // only for tests } default: t.Fatalf("Run(): method %T.%s returns %d values. Only 0, 1 (bool or error) or 2 (bool, error) values are allowed", suite, m.Name, mt.NumOut()) return false // only for tests } methods = append(methods, i) } } if len(methods) == 0 { t.Fatalf("Run(): no test methods found for type %T", suite) return false // only for tests } run(t, suite, methods) return !t.Failed() } func run(t *td.T, suite any, methods []int) { t.Helper() suiteType := reflect.TypeOf(suite) // setup if s, ok := suite.(Setup); ok { if err := s.Setup(t); err != nil { t.Errorf("%T suite setup error: %s", suite, err) return } } else if _, exists := suiteType.MethodByName("Setup"); exists { t.Errorf("%T suite has a Setup method but it does not match Setup(t *td.T) error", suite) } // destroy if s, ok := suite.(Destroy); ok { defer func() { if err := s.Destroy(t); err != nil { t.Errorf("%T suite destroy error: %s", suite, err) } }() } else if _, exists := suiteType.MethodByName("Destroy"); exists { t.Errorf("%T suite has a Destroy method but it does not match Destroy(t *td.T) error", suite) } preTest := emptyPrePostTest if s, ok := suite.(PreTest); ok { preTest = s.PreTest } else if _, exists := suiteType.MethodByName("PreTest"); exists { t.Errorf("%T suite has a PreTest method but it does not match PreTest(t *td.T, testName string) error", suite) } postTest := emptyPrePostTest if s, ok := suite.(PostTest); ok { postTest = s.PostTest } else if _, exists := suiteType.MethodByName("PostTest"); exists { t.Errorf("%T suite has a PostTest method but it does not match PostTest(t *td.T, testName string) error", suite) } between := emptyBetweenTests if s, ok := suite.(BetweenTests); ok { between = s.BetweenTests } else if _, exists := suiteType.MethodByName("BetweenTests"); exists { t.Errorf("%T suite has a BetweenTests method but it does not match BetweenTests(t *td.T, previousTestName, nextTestName string) error", suite) } vs := reflect.ValueOf(suite) typ := reflect.TypeOf(suite) for i, method := range methods { m := typ.Method(method) mt := m.Type call := vs.Method(method).Call cont := true if mt.NumIn() == 2 { t.Run(m.Name, func(t *td.T) { if err := preTest(t, m.Name); err != nil { t.Errorf("%s pre-test error: %s", m.Name, err) return } defer func() { if err := postTest(t, m.Name); err != nil { t.Errorf("%s post-test error: %s", m.Name, err) } }() cont = shouldContinue(t, m.Name, call([]reflect.Value{reflect.ValueOf(t)})) }) } else { t.RunAssertRequire(m.Name, func(assert, require *td.T) { if err := preTest(assert, m.Name); err != nil { assert.Errorf("%s pre-test error: %s", m.Name, err) return } defer func() { if err := postTest(assert, m.Name); err != nil { assert.Errorf("%s post-test error: %s", m.Name, err) } }() cont = shouldContinue(assert, m.Name, call([]reflect.Value{ reflect.ValueOf(assert), reflect.ValueOf(require), })) }) } if !cont { t.Logf("%s required discontinuing suite tests", m.Name) break } if i != len(methods)-1 { next := typ.Method(methods[i+1]).Name if err := between(t, m.Name, next); err != nil { t.Errorf("%s / %s between-tests error: %s", m.Name, next, err) break } } } } func diffWithPtrMethods(typ reflect.Type) []string { if typ.Kind() == reflect.Ptr { return nil } ptyp := reflect.PtrTo(typ) if typ.NumMethod() == ptyp.NumMethod() { return nil } keep := func(m reflect.Method) bool { switch m.Name { case "Setup", "PreTest", "PostTest", "BetweenTests", "Destroy": return true default: return isTest(m.Name) } } var nonPtrMethods []string for i, num := 0, typ.NumMethod(); i < num; i++ { if m := typ.Method(i); keep(m) { nonPtrMethods = append(nonPtrMethods, m.Name) } } var onlyPtrMethods []string for ni, pi, num := 0, 0, ptyp.NumMethod(); pi < num; pi++ { if m := ptyp.Method(pi); keep(m) { if ni >= len(nonPtrMethods) || nonPtrMethods[ni] != m.Name { onlyPtrMethods = append(onlyPtrMethods, m.Name) } else { ni++ } } } return onlyPtrMethods } golang-github-maxatome-go-testdeep-1.14.0/helpers/tdsuite/suite_test.go000066400000000000000000000553341454313311600262300ustar00rootroot00000000000000// Copyright (c) 2021, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package tdsuite_test import ( "errors" "runtime" "strings" "testing" "github.com/maxatome/go-testdeep/helpers/tdsuite" "github.com/maxatome/go-testdeep/internal/test" "github.com/maxatome/go-testdeep/td" ) type base struct { calls []string } func (b *base) rec(plus ...string) { pc, _, _, _ := runtime.Caller(1) name := runtime.FuncForPC(pc).Name() pos := strings.LastIndexByte(name, '.') if name[pos+1:] == "func1" { // Cleanup() npos := strings.LastIndexByte(name[:pos], '.') name = name[npos+1:pos] + ".Cleanup" } else { name = name[pos+1:] } if len(plus) > 0 { name += "+" + strings.Join(plus, "+") } b.calls = append(b.calls, name) } func (b *base) clean() { b.calls = b.calls[:0] } // Mini has only tests, no hooks. type Mini struct{ base } func (m *Mini) Test1(t *td.T) { m.rec() } func (m *Mini) Test2(assert *td.T, require *td.T) { m.rec() } // Full has tests and all possible hooks. type Full struct{ base } func (f *Full) Setup(t *td.T) error { f.rec(); return nil } func (f *Full) PreTest(t *td.T, tn string) error { f.rec(tn); return nil } func (f *Full) PostTest(t *td.T, tn string) error { f.rec(tn); return nil } func (f *Full) BetweenTests(t *td.T, prev, next string) error { f.rec(prev, next) return nil } func (f *Full) Destroy(t *td.T) error { f.rec(); return nil } func (f *Full) Test1(t *td.T) { f.rec() } func (f *Full) Test2(assert *td.T, require *td.T) { f.rec() } func (f *Full) Test3(t *td.T) { f.rec() } func (f *Full) Testimony(t *td.T) { f.rec() } // not a test method var ( _ tdsuite.Setup = (*Full)(nil) _ tdsuite.PreTest = (*Full)(nil) _ tdsuite.PostTest = (*Full)(nil) _ tdsuite.BetweenTests = (*Full)(nil) _ tdsuite.Destroy = (*Full)(nil) ) // FullBrokenHooks has all possible hooks, but wrongly defined, as they don't // match the hook interfaces. type FullBrokenHooks struct{} func (*FullBrokenHooks) Setup() error { return nil } func (*FullBrokenHooks) PreTest(t *td.T, testName *string) error { return nil } func (*FullBrokenHooks) PostTest(t *td.T, testName string) {} func (*FullBrokenHooks) BetweenTests(t *td.T, prev, next *string) error { return nil } func (*FullBrokenHooks) Destroy(t *td.T) {} func (*FullBrokenHooks) Test1(_ *td.T) {} // FullNoPtr has hooks & tests as non-pointer & pointer methods. type FullNoPtr struct{} var traceFullNoPtr base func (f FullNoPtr) Setup(t *td.T) error { traceFullNoPtr.rec(); return nil } func (f FullNoPtr) PreTest(t *td.T, tn string) error { traceFullNoPtr.rec(tn); return nil } func (f *FullNoPtr) PostTest(t *td.T, tn string) error { traceFullNoPtr.rec(tn); return nil } func (f FullNoPtr) BetweenTests(t *td.T, prev, next string) error { traceFullNoPtr.rec(prev, next) return nil } func (f FullNoPtr) Destroy(t *td.T) error { traceFullNoPtr.rec(); return nil } func (f FullNoPtr) Test1(t *td.T) { traceFullNoPtr.rec() } func (f *FullNoPtr) Test2(assert *td.T, require *td.T) { traceFullNoPtr.rec() } func (f FullNoPtr) Test3(t *td.T) { traceFullNoPtr.rec() } func (f *FullNoPtr) Testimony(t *td.T) { traceFullNoPtr.rec() } // not a test method // ErrNone has no tests. type ErrNone struct{} // ErrOut1 has a Test method with bad return type. type ErrOut1 struct{} func (f ErrOut1) Test(t *td.T) int { return 0 } // ErrOut2a has a Test method with bad return types. type ErrOut2a struct{} func (f ErrOut2a) Test(t *td.T) (bool, int) { return false, 0 } // ErrOut2b has a Test method with bad return types. type ErrOut2b struct{} func (f ErrOut2b) Test(t *td.T) (int, error) { return 0, nil } // ErrOut has a Test method with bad return types. type ErrOut struct{} func (f ErrOut) Test(t *td.T) (int, int, int) { return 1, 2, 3 } // Skip has several skipped Test methods. type Skip struct{ base } func (s *Skip) Test1Param(i int) {} func (s *Skip) Test2ParamsA(i, j int) {} func (s *Skip) Test2ParamsB(i int, require *td.T) {} func (s *Skip) Test2ParamsC(assert *td.T, i int) {} func (s *Skip) Test3Params(t *td.T, i, j int) {} func (s *Skip) TestNoParams() {} func (s *Skip) TestOK(t *td.T) { s.rec() } func (s *Skip) TestVariadic(t ...*td.T) {} func TestRun(t *testing.T) { t.Run("Mini", func(t *testing.T) { suite := Mini{} td.CmpTrue(t, tdsuite.Run(t, &suite)) td.Cmp(t, suite.calls, []string{"Test1", "Test2"}) }) t.Run("Full ptr", func(t *testing.T) { suite := Full{} td.CmpTrue(t, tdsuite.Run(t, &suite)) ok := td.Cmp(t, suite.calls, []string{ "Setup", /**/ "PreTest+Test1", /**/ "Test1", /**/ "PostTest+Test1", "BetweenTests+Test1+Test2", /**/ "PreTest+Test2", /**/ "Test2", /**/ "PostTest+Test2", "BetweenTests+Test2+Test3", /**/ "PreTest+Test3", /**/ "Test3", /**/ "PostTest+Test3", "Destroy", }) if !ok { for _, c := range suite.calls { switch c[0] { case 'S', 'B', 'D': t.Log(c) default: t.Log(" ", c) } } } }) t.Run("Without ptr: only non-ptr methods", func(t *testing.T) { defer traceFullNoPtr.clean() suite := FullNoPtr{} tb := test.NewTestingTB("TestWithoutPtr") td.CmpTrue(t, tdsuite.Run(tb, suite)) // non-ptr ok := td.Cmp(t, traceFullNoPtr.calls, []string{ "Setup", /**/ "PreTest+Test1", /**/ "Test1", // /**/ "PostTest+Test1", // PostTest is a ptr method // Test2 is a ptr method // "BetweenTests+Test1+Test2", // /**/ "PreTest+Test2", // /**/ "Test2", // /**/ "PostTest+Test2", // "BetweenTests+Test2+Test3", "BetweenTests+Test1+Test3", /**/ "PreTest+Test3", /**/ "Test3", // /**/ "PostTest+Test3", // PostTest is a ptr method "Destroy", }) if !ok { for _, c := range traceFullNoPtr.calls { switch c[0] { case 'S', 'B', 'D': t.Log(c) default: t.Log(" ", c) } } } // Yes it is a bit ugly td.CmpEmpty(t, tb.ContainsMessages("Run(): several methods are not accessible as suite is not a pointer but tdsuite_test.FullNoPtr: PostTest, Test2")) }) t.Run("With ptr: all ptr & non-ptr methods", func(t *testing.T) { defer traceFullNoPtr.clean() suite := FullNoPtr{} td.CmpTrue(t, tdsuite.Run(t, &suite)) // ptr ok := td.Cmp(t, traceFullNoPtr.calls, []string{ "Setup", /**/ "PreTest+Test1", /**/ "Test1", /**/ "PostTest+Test1", "BetweenTests+Test1+Test2", /**/ "PreTest+Test2", /**/ "Test2", /**/ "PostTest+Test2", "BetweenTests+Test2+Test3", /**/ "PreTest+Test3", /**/ "Test3", /**/ "PostTest+Test3", "Destroy", }) if !ok { for _, c := range traceFullNoPtr.calls { switch c[0] { case 'S', 'B', 'D': t.Log(c) default: t.Log(" ", c) } } } }) t.Run("ErrNil", func(t *testing.T) { tb := test.NewTestingTB("TestNil") tb.CatchFatal(func() { tdsuite.Run(tb, nil) }) td.CmpTrue(t, tb.IsFatal) td.Cmp(t, tb.LastMessage(), "Run(): suite parameter cannot be nil") }) t.Run("ErrNone", func(t *testing.T) { suite := ErrNone{} tb := test.NewTestingTB("TestErrNone") tb.CatchFatal(func() { tdsuite.Run(tb, suite) }) td.CmpTrue(t, tb.IsFatal) td.Cmp(t, tb.LastMessage(), "Run(): no test methods found for type tdsuite_test.ErrNone") }) t.Run("Full-no-ptr", func(t *testing.T) { suite := Full{} tb := test.NewTestingTB("Full-no-ptr") tb.CatchFatal(func() { tdsuite.Run(tb, suite) }) td.CmpTrue(t, tb.IsFatal) td.Cmp(t, tb.Messages, []string{ "Run(): several methods are not accessible as suite is not a pointer but tdsuite_test.Full: BetweenTests, Destroy, PostTest, PreTest, Setup, Test1, Test2, Test3", "Run(): no test methods found for type tdsuite_test.Full", }) }) t.Run("ErrOut1", func(t *testing.T) { suite := ErrOut1{} tb := test.NewTestingTB("TestErrOut1") tb.CatchFatal(func() { tdsuite.Run(tb, suite) }) td.CmpTrue(t, tb.IsFatal) td.Cmp(t, tb.LastMessage(), "Run(): method tdsuite_test.ErrOut1.Test returns int value. Only bool or error are allowed") }) t.Run("ErrOut2a", func(t *testing.T) { suite := ErrOut2a{} tb := test.NewTestingTB("TestErrOut2a") tb.CatchFatal(func() { tdsuite.Run(tb, suite) }) td.CmpTrue(t, tb.IsFatal) td.Cmp(t, tb.LastMessage(), "Run(): method tdsuite_test.ErrOut2a.Test returns (bool, int) values. Only (bool, error) is allowed") }) t.Run("ErrOut2b", func(t *testing.T) { suite := ErrOut2b{} tb := test.NewTestingTB("TestErrOut2b") tb.CatchFatal(func() { tdsuite.Run(tb, suite) }) td.CmpTrue(t, tb.IsFatal) td.Cmp(t, tb.LastMessage(), "Run(): method tdsuite_test.ErrOut2b.Test returns (int, error) values. Only (bool, error) is allowed") }) t.Run("ErrOut", func(t *testing.T) { suite := ErrOut{} tb := test.NewTestingTB("TestErrOut") tb.CatchFatal(func() { tdsuite.Run(tb, suite) }) td.CmpTrue(t, tb.IsFatal) td.Cmp(t, tb.LastMessage(), "Run(): method tdsuite_test.ErrOut.Test returns 3 values. Only 0, 1 (bool or error) or 2 (bool, error) values are allowed") }) t.Run("Skip", func(t *testing.T) { suite := Skip{} tb := test.NewTestingTB("TestSkip") tdsuite.Run(tb, &suite) test.IsFalse(t, tb.IsFatal) td.Cmp(t, suite.calls, []string{"TestOK"}) const p = "Run(): method *tdsuite_test.Skip." td.Cmp(t, tb.Messages, []string{ p + "Test1Param skipped, unrecognized parameter type int. Only *td.T allowed", p + "Test2ParamsA skipped, unrecognized parameters types (int, int). Only (*td.T, *td.T) allowed", p + "Test2ParamsB skipped, unrecognized first parameter type int. Only (*td.T, *td.T) allowed", p + "Test2ParamsC skipped, unrecognized second parameter type int. Only (*td.T, *td.T) allowed", p + "Test3Params skipped, too many parameters", p + "TestNoParams skipped, no input parameters", p + "TestVariadic skipped, variadic parameters not supported", "++++ TestOK", // (*T).Run() log as test.TestingTB has no Run() method }) }) } // Error allows to raise errors. type Error struct { base setup bool destroy bool betweenTests bool preTest int postTest int testBool [2]bool testError [2]bool testBoolErrorBool [2]bool testBoolErrorErr [2]bool } func (e *Error) Setup(t *td.T) error { if e.setup { return errors.New("Setup error") } return nil } func (e *Error) PreTest(t *td.T, tn string) error { if e.preTest > 0 { e.preTest-- if e.preTest == 0 { return errors.New("PreTest error") } } return nil } func (e *Error) PostTest(t *td.T, tn string) error { if e.postTest > 0 { e.postTest-- if e.postTest == 0 { return errors.New("PostTest error") } } return nil } func (e *Error) BetweenTests(t *td.T, prev, next string) error { if e.betweenTests { return errors.New("BetweenTests error") } return nil } func (e *Error) Destroy(t *td.T) error { if e.destroy { return errors.New("Destroy error") } return nil } // 1 param methods. func (e *Error) Test1Bool(t *td.T) bool { e.rec() return !e.testBool[0] } func (e *Error) Test1Error(t *td.T) error { e.rec() if e.testError[0] { return errors.New("Test1Error error") } return nil } func (e *Error) Test1BoolError(t *td.T) (b bool, err error) { e.rec() b = !e.testBoolErrorBool[0] if e.testBoolErrorErr[0] { err = errors.New("Test1BoolError error") } return } func (e *Error) Test1Z(t *td.T) { e.rec() } // 2 params methods. func (e *Error) Test2Bool(assert, require *td.T) bool { e.rec() return !e.testBool[1] } func (e *Error) Test2Error(assert, require *td.T) error { e.rec() if e.testError[1] { return errors.New("Test2Error error") } return nil } func (e *Error) Test2BoolError(assert, require *td.T) (b bool, err error) { e.rec() b = !e.testBoolErrorBool[1] if e.testBoolErrorErr[1] { err = errors.New("Test2BoolError error") } return } func (e *Error) Test2Z(assert, require *td.T) { e.rec() } func TestRunErrors(t *testing.T) { t.Run("Setup", func(t *testing.T) { suite := Error{setup: true} tb := test.NewTestingTB("TestError") td.CmpFalse(t, tdsuite.Run(tb, &suite)) td.CmpFalse(t, tb.IsFatal) td.Cmp(t, tb.Messages, []string{ "*tdsuite_test.Error suite setup error: Setup error", }) }) t.Run("Destroy", func(t *testing.T) { suite := Error{destroy: true} tb := test.NewTestingTB("TestError") td.CmpFalse(t, tdsuite.Run(tb, &suite)) td.CmpFalse(t, tb.IsFatal) td.Cmp(t, tb.Messages, []string{ "++++ Test1Bool", "++++ Test1BoolError", "++++ Test1Error", "++++ Test1Z", // "++++ Test2Bool", "++++ Test2BoolError", "++++ Test2Error", "++++ Test2Z", "*tdsuite_test.Error suite destroy error: Destroy error", }) }) t.Run("PreTest", func(t *testing.T) { t.Run("1 param", func(t *testing.T) { suite := Error{preTest: 2} tb := test.NewTestingTB("TestError") td.CmpFalse(t, tdsuite.Run(tb, &suite)) td.CmpFalse(t, tb.IsFatal) td.Cmp(t, tb.Messages, []string{ "++++ Test1Bool", "++++ Test1BoolError", "Test1BoolError pre-test error: PreTest error", "++++ Test1Error", "++++ Test1Z", // "++++ Test2Bool", "++++ Test2BoolError", "++++ Test2Error", "++++ Test2Z", }) }) t.Run("2 params", func(t *testing.T) { suite := Error{preTest: 6} tb := test.NewTestingTB("TestError") td.CmpFalse(t, tdsuite.Run(tb, &suite)) td.CmpFalse(t, tb.IsFatal) td.Cmp(t, tb.Messages, []string{ "++++ Test1Bool", "++++ Test1BoolError", "++++ Test1Error", "++++ Test1Z", // "++++ Test2Bool", "++++ Test2BoolError", "Test2BoolError pre-test error: PreTest error", "++++ Test2Error", "++++ Test2Z", }) }) }) t.Run("PostTest", func(t *testing.T) { t.Run("1 param", func(t *testing.T) { suite := Error{postTest: 3} tb := test.NewTestingTB("TestError") td.CmpFalse(t, tdsuite.Run(tb, &suite)) td.CmpFalse(t, tb.IsFatal) td.Cmp(t, tb.Messages, []string{ "++++ Test1Bool", "++++ Test1BoolError", "++++ Test1Error", "Test1Error post-test error: PostTest error", "++++ Test1Z", // "++++ Test2Bool", "++++ Test2BoolError", "++++ Test2Error", "++++ Test2Z", }) }) t.Run("2 params", func(t *testing.T) { suite := Error{postTest: 7} tb := test.NewTestingTB("TestError") td.CmpFalse(t, tdsuite.Run(tb, &suite)) td.CmpFalse(t, tb.IsFatal) td.Cmp(t, tb.Messages, []string{ "++++ Test1Bool", "++++ Test1BoolError", "++++ Test1Error", "++++ Test1Z", // "++++ Test2Bool", "++++ Test2BoolError", "++++ Test2Error", "Test2Error post-test error: PostTest error", "++++ Test2Z", }) }) }) t.Run("BetweenTests", func(t *testing.T) { suite := Error{betweenTests: true} tb := test.NewTestingTB("TestError") td.CmpFalse(t, tdsuite.Run(tb, &suite)) td.CmpFalse(t, tb.IsFatal) td.Cmp(t, tb.Messages, []string{ "++++ Test1Bool", "Test1Bool / Test1BoolError between-tests error: BetweenTests error", }) }) t.Run("InvalidHooks", func(t *testing.T) { tb := test.NewTestingTB("TestError") td.CmpFalse(t, tdsuite.Run(tb, &FullBrokenHooks{})) td.CmpFalse(t, tb.IsFatal) name := "*tdsuite_test.FullBrokenHooks" td.Cmp(t, tb.Messages, []string{ name + " suite has a Setup method but it does not match Setup(t *td.T) error", name + " suite has a Destroy method but it does not match Destroy(t *td.T) error", name + " suite has a PreTest method but it does not match PreTest(t *td.T, testName string) error", name + " suite has a PostTest method but it does not match PostTest(t *td.T, testName string) error", name + " suite has a BetweenTests method but it does not match BetweenTests(t *td.T, previousTestName, nextTestName string) error", "++++ Test1", }) }) t.Run("Stop_after_TestBool", func(t *testing.T) { t.Run("1 param", func(t *testing.T) { suite := Error{testBool: [2]bool{true, false}} tb := test.NewTestingTB("TestError") td.CmpTrue(t, tdsuite.Run(tb, &suite)) // returning false is not an error td.CmpFalse(t, tb.IsFatal) td.Cmp(t, tb.Messages, []string{ "++++ Test1Bool", "Test1Bool required discontinuing suite tests", }) }) t.Run("2 params", func(t *testing.T) { suite := Error{testBool: [2]bool{false, true}} tb := test.NewTestingTB("TestError") td.CmpTrue(t, tdsuite.Run(tb, &suite)) // returning false is not an error td.CmpFalse(t, tb.IsFatal) td.Cmp(t, tb.Messages, []string{ "++++ Test1Bool", "++++ Test1BoolError", "++++ Test1Error", "++++ Test1Z", // "++++ Test2Bool", "Test2Bool required discontinuing suite tests", }) }) }) t.Run("TestBoolError", func(t *testing.T) { t.Run("Stop after", func(t *testing.T) { t.Run("1 param", func(t *testing.T) { suite := Error{testBoolErrorBool: [2]bool{true, false}} tb := test.NewTestingTB("TestError") td.CmpTrue(t, tdsuite.Run(tb, &suite)) // returning false is not an error td.CmpFalse(t, tb.IsFatal) td.Cmp(t, tb.Messages, []string{ "++++ Test1Bool", "++++ Test1BoolError", "Test1BoolError required discontinuing suite tests", }) }) t.Run("2 params", func(t *testing.T) { suite := Error{testBoolErrorBool: [2]bool{false, true}} tb := test.NewTestingTB("TestError") td.CmpTrue(t, tdsuite.Run(tb, &suite)) // returning false is not an error td.CmpFalse(t, tb.IsFatal) td.Cmp(t, tb.Messages, []string{ "++++ Test1Bool", "++++ Test1BoolError", "++++ Test1Error", "++++ Test1Z", // "++++ Test2Bool", "++++ Test2BoolError", "Test2BoolError required discontinuing suite tests", }) }) }) t.Run("Error but continue", func(t *testing.T) { t.Run("1 param", func(t *testing.T) { suite := Error{testBoolErrorErr: [2]bool{true, false}} tb := test.NewTestingTB("TestError") td.CmpFalse(t, tdsuite.Run(tb, &suite)) td.CmpFalse(t, tb.IsFatal) td.Cmp(t, tb.Messages, []string{ "++++ Test1Bool", "++++ Test1BoolError", "Test1BoolError error: Test1BoolError error", "++++ Test1Error", "++++ Test1Z", // "++++ Test2Bool", "++++ Test2BoolError", "++++ Test2Error", "++++ Test2Z", }) }) t.Run("2 params", func(t *testing.T) { suite := Error{testBoolErrorErr: [2]bool{false, true}} tb := test.NewTestingTB("TestError") td.CmpFalse(t, tdsuite.Run(tb, &suite)) td.CmpFalse(t, tb.IsFatal) td.Cmp(t, tb.Messages, []string{ "++++ Test1Bool", "++++ Test1BoolError", "++++ Test1Error", "++++ Test1Z", // "++++ Test2Bool", "++++ Test2BoolError", "Test2BoolError error: Test2BoolError error", "++++ Test2Error", "++++ Test2Z", }) }) }) t.Run("Error and stop after", func(t *testing.T) { t.Run("1 param", func(t *testing.T) { suite := Error{ testBoolErrorBool: [2]bool{true, false}, testBoolErrorErr: [2]bool{true, false}, } tb := test.NewTestingTB("TestError") td.CmpFalse(t, tdsuite.Run(tb, &suite)) td.CmpFalse(t, tb.IsFatal) td.Cmp(t, tb.Messages, []string{ "++++ Test1Bool", "++++ Test1BoolError", "Test1BoolError error: Test1BoolError error", "Test1BoolError required discontinuing suite tests", }) }) t.Run("2 params", func(t *testing.T) { suite := Error{ testBoolErrorBool: [2]bool{false, true}, testBoolErrorErr: [2]bool{false, true}, } tb := test.NewTestingTB("TestError") td.CmpFalse(t, tdsuite.Run(tb, &suite)) td.CmpFalse(t, tb.IsFatal) td.Cmp(t, tb.Messages, []string{ "++++ Test1Bool", "++++ Test1BoolError", "++++ Test1Error", "++++ Test1Z", // "++++ Test2Bool", "++++ Test2BoolError", "Test2BoolError error: Test2BoolError error", "Test2BoolError required discontinuing suite tests", }) }) }) }) t.Run("Error_for_TestError", func(t *testing.T) { t.Run("1 param", func(t *testing.T) { suite := Error{testError: [2]bool{true, false}} tb := test.NewTestingTB("TestError") td.CmpFalse(t, tdsuite.Run(tb, &suite)) td.CmpFalse(t, tb.IsFatal) td.Cmp(t, tb.Messages, []string{ "++++ Test1Bool", "++++ Test1BoolError", "++++ Test1Error", "Test1Error error: Test1Error error", "Test1Error required discontinuing suite tests", }) }) t.Run("2 params", func(t *testing.T) { suite := Error{testError: [2]bool{false, true}} tb := test.NewTestingTB("TestError") td.CmpFalse(t, tdsuite.Run(tb, &suite)) td.CmpFalse(t, tb.IsFatal) td.Cmp(t, tb.Messages, []string{ "++++ Test1Bool", "++++ Test1BoolError", "++++ Test1Error", "++++ Test1Z", // "++++ Test2Bool", "++++ Test2BoolError", "++++ Test2Error", "Test2Error error: Test2Error error", "Test2Error required discontinuing suite tests", }) }) }) } // FullCleanup has tests and all possible hooks. type FullCleanup struct{ base } func (f *FullCleanup) Setup(t *td.T) error { f.rec(); return nil } func (f *FullCleanup) PreTest(t *td.T, tn string) error { f.rec(tn) t.Cleanup(func() { f.rec(tn) }) return nil } func (f *FullCleanup) PostTest(t *td.T, tn string) error { f.rec(tn) t.Cleanup(func() { f.rec(tn) }) return nil } func (f *FullCleanup) BetweenTests(t *td.T, prev, next string) error { f.rec(prev, next) return nil } func (f *FullCleanup) Destroy(t *td.T) error { f.rec(); return nil } func (f *FullCleanup) Test1(t *td.T) { f.rec() t.Cleanup(func() { f.rec() }) } func (f *FullCleanup) Test2(assert *td.T, require *td.T) { f.rec() assert.Cleanup(func() { f.rec() }) } func (f *FullCleanup) Test3(t *td.T) { f.rec() t.Cleanup(func() { f.rec() }) } func (f *FullCleanup) Testimony(t *td.T) {} // not a test method var ( _ tdsuite.Setup = (*FullCleanup)(nil) _ tdsuite.PreTest = (*FullCleanup)(nil) _ tdsuite.PostTest = (*FullCleanup)(nil) _ tdsuite.BetweenTests = (*FullCleanup)(nil) _ tdsuite.Destroy = (*FullCleanup)(nil) ) func TestRunCleanup(t *testing.T) { t.Run("Full", func(t *testing.T) { suite := FullCleanup{} td.CmpTrue(t, tdsuite.Run(t, &suite)) ok := td.Cmp(t, suite.calls, []string{ "Setup", /**/ "PreTest+Test1", /**/ "Test1", /**/ "PostTest+Test1", /**/ "PostTest.Cleanup+Test1", /**/ "Test1.Cleanup", /**/ "PreTest.Cleanup+Test1", "BetweenTests+Test1+Test2", /**/ "PreTest+Test2", /**/ "Test2", /**/ "PostTest+Test2", /**/ "PostTest.Cleanup+Test2", /**/ "Test2.Cleanup", /**/ "PreTest.Cleanup+Test2", "BetweenTests+Test2+Test3", /**/ "PreTest+Test3", /**/ "Test3", /**/ "PostTest+Test3", /**/ "PostTest.Cleanup+Test3", /**/ "Test3.Cleanup", /**/ "PreTest.Cleanup+Test3", "Destroy", }) if !ok { for _, c := range suite.calls { switch c[0] { case 'S', 'B', 'D': t.Log(c) default: t.Log(" ", c) } } } }) } golang-github-maxatome-go-testdeep-1.14.0/helpers/tdutil/000077500000000000000000000000001454313311600233235ustar00rootroot00000000000000golang-github-maxatome-go-testdeep-1.14.0/helpers/tdutil/any.go000066400000000000000000000004221454313311600244370ustar00rootroot00000000000000// Copyright (c) 2022, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. //go:build !go1.18 // +build !go1.18 package tdutil type any = interface{} golang-github-maxatome-go-testdeep-1.14.0/helpers/tdutil/any_test.go000066400000000000000000000004271454313311600255030ustar00rootroot00000000000000// Copyright (c) 2022, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. //go:build !go1.18 // +build !go1.18 package tdutil_test type any = interface{} golang-github-maxatome-go-testdeep-1.14.0/helpers/tdutil/map.go000066400000000000000000000040061454313311600244270ustar00rootroot00000000000000// Copyright (c) 2018, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package tdutil import ( "reflect" "sort" "github.com/maxatome/go-testdeep/internal/visited" ) // MapSortedKeys returns a slice of all sorted keys of map m. It // panics if m's [reflect.Kind] is not [reflect.Map]. func MapSortedKeys(m reflect.Value) []reflect.Value { ks := m.MapKeys() sort.Sort(SortableValues(ks)) return ks } type kv struct { key reflect.Value value reflect.Value } type kvSlice struct { v visited.Visited s []kv } func newKvSlice(l int) *kvSlice { s := kvSlice{} if l > 0 { s.s = make([]kv, 0, l) if l > 1 { s.v = visited.NewVisited() } } return &s } func (s *kvSlice) Len() int { return len(s.s) } func (s *kvSlice) Less(i, j int) bool { return cmp(s.v, s.s[i].key, s.s[j].key) < 0 } func (s *kvSlice) Swap(i, j int) { s.s[i], s.s[j] = s.s[j], s.s[i] } // MapEach calls fn for each key/value pair of map m. If fn // returns false, it will not be called again. func MapEach(m reflect.Value, fn func(k, v reflect.Value) bool) bool { kvs := newKvSlice(m.Len()) iter := m.MapRange() for iter.Next() { kvs.s = append(kvs.s, kv{key: iter.Key(), value: iter.Value()}) } sort.Sort(kvs) for _, kv := range kvs.s { if !fn(kv.key, kv.value) { return false } } return true } // MapEachValue calls fn for each value of map m. If fn returns // false, it will not be called again. func MapEachValue(m reflect.Value, fn func(k reflect.Value) bool) bool { iter := m.MapRange() for iter.Next() { if !fn(iter.Value()) { return false } } return true } // MapSortedValues returns a slice of all sorted values of map m. It // panics if m's [reflect.Kind] is not [reflect.Map]. func MapSortedValues(m reflect.Value) []reflect.Value { vs := make([]reflect.Value, 0, m.Len()) iter := m.MapRange() for iter.Next() { vs = append(vs, iter.Value()) } sort.Sort(SortableValues(vs)) return vs } golang-github-maxatome-go-testdeep-1.14.0/helpers/tdutil/map_private_test.go000066400000000000000000000024701454313311600272230ustar00rootroot00000000000000// Copyright (c) 2018, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package tdutil import ( "reflect" "sort" "testing" ) func TestKvSlice(t *testing.T) { t.Run("len=0", func(t *testing.T) { kvs := newKvSlice(0) if kvs.s != nil || kvs.v != nil { t.Errorf("newKvSlice failed: %v", *kvs) } sort.Sort(kvs) }) t.Run("len=1", func(t *testing.T) { kvs := newKvSlice(1) if kvs.s == nil || kvs.v != nil { t.Errorf("newKvSlice failed: %v", *kvs) } kvs.s = append(kvs.s, kv{ key: reflect.ValueOf("a"), value: reflect.ValueOf(1), }) sort.Sort(kvs) }) t.Run("len>1", func(t *testing.T) { kvs := newKvSlice(3) if kvs.s == nil || kvs.v == nil { t.Errorf("newKvSlice failed: %v", *kvs) } kvs.s = append(kvs.s, kv{ key: reflect.ValueOf("b"), value: reflect.ValueOf(2), }, kv{ key: reflect.ValueOf("c"), value: reflect.ValueOf(3), }, kv{ key: reflect.ValueOf("a"), value: reflect.ValueOf(1), }, ) sort.Sort(kvs) if kvs.s[0].key.String() != "a" || kvs.s[1].key.String() != "b" || kvs.s[2].key.String() != "c" { t.Errorf("Sort failed: [%v, %v, %v]", kvs.s[0].key, kvs.s[1].key, kvs.s[2].key) } }) } golang-github-maxatome-go-testdeep-1.14.0/helpers/tdutil/map_test.go000066400000000000000000000054001454313311600254650ustar00rootroot00000000000000// Copyright (c) 2018, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package tdutil_test import ( "reflect" "sort" "testing" "github.com/maxatome/go-testdeep/helpers/tdutil" ) func TestMap(t *testing.T) { m := map[string]int{"a": 1, "b": 2, "c": 3} t.Run("MapEach", func(t *testing.T) { type kv struct { key string value int } var s []kv ok := tdutil.MapEach(reflect.ValueOf(m), func(k, v reflect.Value) bool { s = append(s, kv{ key: k.Interface().(string), value: v.Interface().(int), }) return true }) if !ok { t.Error("MapEach returned false") } sort.Slice(s, func(i, j int) bool { return s[i].key < s[j].key }) if len(s) != 3 || s[0] != (kv{key: "a", value: 1}) || s[1] != (kv{key: "b", value: 2}) || s[2] != (kv{key: "c", value: 3}) { t.Errorf("MapEach failed: %v", s) } }) t.Run("MapEach short circuit", func(t *testing.T) { called := 0 ok := tdutil.MapEach(reflect.ValueOf(m), func(k, v reflect.Value) bool { called++ return false }) if ok { t.Error("MapEach returned true") } if called != 1 { t.Errorf("MapEach callback called %d times instead of 1", called) } }) t.Run("MapEachValue", func(t *testing.T) { var s []int ok := tdutil.MapEachValue(reflect.ValueOf(m), func(v reflect.Value) bool { s = append(s, v.Interface().(int)) return true }) if !ok { t.Error("MapEachValue returned false") } sort.Ints(s) if len(s) != 3 || s[0] != 1 || s[1] != 2 || s[2] != 3 { t.Errorf("MapEachValue failed: %v", s) } }) t.Run("MapEachValue short circuit", func(t *testing.T) { called := 0 ok := tdutil.MapEachValue(reflect.ValueOf(m), func(v reflect.Value) bool { called++ return false }) if ok { t.Error("MapEachValue returned true") } if called != 1 { t.Errorf("MapEachValue callback called %d times instead of 1", called) } }) t.Run("MapSortedValues", func(t *testing.T) { vs := tdutil.MapSortedValues(reflect.ValueOf(m)) if len(vs) != 3 || vs[0].Int() != 1 || vs[1].Int() != 2 || vs[2].Int() != 3 { t.Errorf("MapSortedValues failed: %v", vs) } // nil map var mn map[string]int vs = tdutil.MapSortedKeys(reflect.ValueOf(mn)) if len(vs) != 0 { t.Errorf("MapSortedValues failed: %v", vs) } }) t.Run("MapSortedKeys", func(t *testing.T) { ks := tdutil.MapSortedKeys(reflect.ValueOf(m)) if len(ks) != 3 || ks[0].String() != "a" || ks[1].String() != "b" || ks[2].String() != "c" { t.Errorf("MapSortedKeys failed: %v", ks) } // nil map var mn map[string]int ks = tdutil.MapSortedKeys(reflect.ValueOf(mn)) if len(ks) != 0 { t.Errorf("MapSortedKeys failed: %v", ks) } }) } golang-github-maxatome-go-testdeep-1.14.0/helpers/tdutil/name.go000066400000000000000000000023261454313311600245750ustar00rootroot00000000000000// Copyright (c) 2019, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package tdutil import ( "fmt" "io" "strings" ) // BuildTestName builds a string from given args. // // If optional first args is a string containing at least one %, args // are passed as is to [fmt.Fprintf], else they are passed to [fmt.Fprint]. func BuildTestName(args ...any) string { if len(args) == 0 { return "" } var b strings.Builder FbuildTestName(&b, args...) return b.String() } // FbuildTestName builds a string from given args. // // If optional first args is a string containing at least one %, args // are passed as is to [fmt.Fprintf], else they are passed to [fmt.Fprint]. func FbuildTestName(w io.Writer, args ...any) { if len(args) == 0 { return } str, ok := args[0].(string) if ok && len(args) > 1 { if pos := strings.IndexRune(str, '%'); pos >= 0 && pos < len(str)-1 { fmt.Fprintf(w, str, args[1:]...) //nolint: errcheck return } } // create a new slice to fool govet and avoid "call has possible // formatting directive" errors fmt.Fprint(w, args[:]...) //nolint: errcheck,gocritic } golang-github-maxatome-go-testdeep-1.14.0/helpers/tdutil/name_test.go000066400000000000000000000021741454313311600256350ustar00rootroot00000000000000// Copyright (c) 2019, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package tdutil_test import ( "testing" "github.com/maxatome/go-testdeep/helpers/tdutil" ) func TestBuildTestName(t *testing.T) { for i, curTest := range []struct { params []any expected string }{ { params: []any{}, expected: "", }, { params: []any{"foobar"}, expected: "foobar", }, { params: []any{"foobar %d"}, expected: "foobar %d", }, { params: []any{"foobar %", 12}, expected: "foobar %12", }, { params: []any{"foo", "bar"}, expected: "foobar", }, { params: []any{123, "zip"}, expected: "123zip", }, { params: []any{123, 456}, expected: "123 456", }, { params: []any{"foo(%d) bar(%s)", 123, "zip"}, expected: "foo(123) bar(zip)", }, } { name := tdutil.BuildTestName(curTest.params...) if name != curTest.expected { t.Errorf(`BuildTestName#%d == "%s" but ≠ "%s"`, i, name, curTest.expected) } } tdutil.FbuildTestName(nil) } golang-github-maxatome-go-testdeep-1.14.0/helpers/tdutil/sort.go000066400000000000000000000060641454313311600246470ustar00rootroot00000000000000// Copyright (c) 2019, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package tdutil import ( "math" "reflect" "github.com/maxatome/go-testdeep/internal/visited" ) func cmpRet(less, gt bool) int { if less { return -1 } if gt { return 1 } return 0 } func cmpFloat(a, b float64) int { if math.IsNaN(a) { return -1 } if math.IsNaN(b) { return 1 } return cmpRet(a < b, a > b) } // cmp returns -1 if a < b, 1 if a > b, 0 if a == b. func cmp(v visited.Visited, a, b reflect.Value) int { if !a.IsValid() { if !b.IsValid() { return 0 } return -1 } if !b.IsValid() { return 1 } if at, bt := a.Type(), b.Type(); at != bt { sat, sbt := at.String(), bt.String() return cmpRet(sat < sbt, sat > sbt) } // Avoid looping forever on cyclic references if v.Record(a, b) { return 0 } switch a.Kind() { case reflect.Bool: if a.Bool() { if b.Bool() { return 0 } return 1 } if b.Bool() { return -1 } return 0 case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: na, nb := a.Int(), b.Int() return cmpRet(na < nb, na > nb) case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: na, nb := a.Uint(), b.Uint() return cmpRet(na < nb, na > nb) case reflect.Float32, reflect.Float64: return cmpFloat(a.Float(), b.Float()) case reflect.Complex64, reflect.Complex128: na, nb := a.Complex(), b.Complex() fa, fb := real(na), real(nb) if r := cmpFloat(fa, fb); r != 0 { return r } return cmpFloat(imag(na), imag(nb)) case reflect.String: sa, sb := a.String(), b.String() return cmpRet(sa < sb, sa > sb) case reflect.Array: for i := 0; i < a.Len(); i++ { if r := cmp(v, a.Index(i), b.Index(i)); r != 0 { return r } } return 0 case reflect.Slice: al, bl := a.Len(), b.Len() maxl := al if al > bl { maxl = bl } for i := 0; i < maxl; i++ { if r := cmp(v, a.Index(i), b.Index(i)); r != 0 { return r } } return cmpRet(al < bl, al > bl) case reflect.Interface: if a.IsNil() { if b.IsNil() { return 0 } return -1 } if b.IsNil() { return 1 } return cmp(v, a.Elem(), b.Elem()) case reflect.Struct: for i, m := 0, a.NumField(); i < m; i++ { if r := cmp(v, a.Field(i), b.Field(i)); r != 0 { return r } } return 0 case reflect.Ptr: if a.Pointer() == b.Pointer() { return 0 } if a.IsNil() { return -1 } if b.IsNil() { return 1 } return cmp(v, a.Elem(), b.Elem()) case reflect.Map: // consider shorter maps are before longer ones al, bl := a.Len(), b.Len() if r := cmpRet(al < bl, al > bl); r != 0 { return r } // then fallback on pointers comparison. How to say a map is // before another one otherwise? fallthrough case reflect.Func, reflect.Chan, reflect.UnsafePointer: pa, pb := a.Pointer(), b.Pointer() return cmpRet(pa < pb, pa > pb) default: panic("don't know how to compare " + a.Kind().String()) } } golang-github-maxatome-go-testdeep-1.14.0/helpers/tdutil/sort_test.go000066400000000000000000000110111454313311600256720ustar00rootroot00000000000000// Copyright (c) 2019, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package tdutil import ( "math" "reflect" "testing" "github.com/maxatome/go-testdeep/internal/visited" ) func TestSortCmp(t *testing.T) { checkCmp := func(a, b any, expected int) { t.Helper() got := cmp(visited.NewVisited(), reflect.ValueOf(a), reflect.ValueOf(b)) if got != expected { t.Errorf("cmp() failed: got=%d expected=%d\n", got, expected) } } // IsValid checkCmp(nil, 12, -1) checkCmp(nil, nil, 0) checkCmp(12, nil, 1) // type mismatch: int is before string checkCmp(42, "str", -1) checkCmp("str", 42, 1) // bool checkCmp(true, true, 0) checkCmp(true, false, 1) checkCmp(false, true, -1) checkCmp(false, false, 0) // int checkCmp(12, 42, -1) checkCmp(42, 12, 1) checkCmp(12, 12, 0) checkCmp(int8(12), int8(42), -1) checkCmp(int8(42), int8(12), 1) checkCmp(int8(12), int8(12), 0) checkCmp(int16(12), int16(42), -1) checkCmp(int16(42), int16(12), 1) checkCmp(int16(12), int16(12), 0) checkCmp(int32(12), int32(42), -1) checkCmp(int32(42), int32(12), 1) checkCmp(int32(12), int32(12), 0) checkCmp(int64(12), int64(42), -1) checkCmp(int64(42), int64(12), 1) checkCmp(int64(12), int64(12), 0) // uint checkCmp(uint(12), uint(42), -1) checkCmp(uint(42), uint(12), 1) checkCmp(uint(12), uint(12), 0) checkCmp(uint8(12), uint8(42), -1) checkCmp(uint8(42), uint8(12), 1) checkCmp(uint8(12), uint8(12), 0) checkCmp(uint16(12), uint16(42), -1) checkCmp(uint16(42), uint16(12), 1) checkCmp(uint16(12), uint16(12), 0) checkCmp(uint32(12), uint32(42), -1) checkCmp(uint32(42), uint32(12), 1) checkCmp(uint32(12), uint32(12), 0) checkCmp(uint64(12), uint64(42), -1) checkCmp(uint64(42), uint64(12), 1) checkCmp(uint64(12), uint64(12), 0) checkCmp(uintptr(12), uintptr(42), -1) checkCmp(uintptr(42), uintptr(12), 1) checkCmp(uintptr(12), uintptr(12), 0) // float checkCmp(float32(12), float32(42), -1) checkCmp(float32(42), float32(12), 1) checkCmp(float32(12), float32(12), 0) checkCmp(float64(12), float64(42), -1) checkCmp(float64(42), float64(12), 1) checkCmp(float64(12), float64(12), 0) checkCmp(float64(12), float64(12), 0) checkCmp(math.NaN(), float64(12), -1) checkCmp(math.NaN(), math.NaN(), -1) checkCmp(float64(12), math.NaN(), 1) // complex checkCmp(complex(12, 0), complex(42, 0), -1) checkCmp(complex(42, 0), complex(12, 0), 1) checkCmp(complex(0, 12), complex(0, 42), -1) checkCmp(complex(0, 42), complex(0, 12), 1) checkCmp(complex(12, 0), complex(12, 0), 0) checkCmp(complex(float32(12), 0), complex(float32(42), 0), -1) checkCmp(complex(float32(42), 0), complex(float32(12), 0), 1) checkCmp(complex(float32(0), 12), complex(float32(0), 42), -1) checkCmp(complex(float32(0), 42), complex(float32(0), 12), 1) checkCmp(complex(float32(12), 0), complex(float32(12), 0), 0) // string checkCmp("aaa", "bbb", -1) checkCmp("bbb", "aaa", 1) checkCmp("aaa", "aaa", 0) // array checkCmp([3]byte{1, 2, 3}, [3]byte{3, 1, 2}, -1) checkCmp([3]byte{3, 1, 2}, [3]byte{1, 2, 3}, 1) checkCmp([3]byte{1, 2, 3}, [3]byte{1, 2, 3}, 0) // slice checkCmp([]byte{1, 2, 3}, []byte{3, 1, 2}, -1) checkCmp([]byte{3, 1, 2}, []byte{1, 2, 3}, 1) checkCmp([]byte{1, 2, 3}, []byte{1, 2, 3}, 0) checkCmp([]byte{1, 2, 3}, []byte{1, 2, 3, 4}, -1) checkCmp([]byte{1, 2, 3, 4}, []byte{1, 2, 3}, 1) // interface checkCmp([]any{1}, []any{3}, -1) checkCmp([]any{3}, []any{1}, 1) checkCmp([]any{1}, []any{1}, 0) checkCmp([]any{nil}, []any{nil}, 0) checkCmp([]any{nil}, []any{1}, -1) checkCmp([]any{1}, []any{nil}, 1) // struct type myStruct struct { n int s string p *myStruct } checkCmp(myStruct{n: 12, s: "a"}, myStruct{n: 12, s: "b"}, -1) checkCmp(myStruct{n: 12, s: "b"}, myStruct{n: 12, s: "a"}, 1) checkCmp(myStruct{n: 12, s: "a"}, myStruct{n: 12, s: "a"}, 0) // ptr a, b := 12, 42 checkCmp(&a, &a, 0) checkCmp((*int)(nil), (*int)(nil), 0) checkCmp(&a, &b, -1) checkCmp((*int)(nil), &b, -1) checkCmp(&b, &a, 1) checkCmp(&b, (*int)(nil), 1) // map ma, mb := map[int]bool{12: true}, map[int]bool{12: true, 13: false} checkCmp(ma, mb, -1) checkCmp(mb, ma, 1) checkCmp(ma, ma, 0) checkCmp((map[int]bool)(nil), (map[int]bool)(nil), 0) checkCmp((map[int]bool)(nil), ma, -1) checkCmp(ma, (map[int]bool)(nil), 1) // cyclic references protection pa := &myStruct{n: 42, p: &myStruct{n: 18}} pa.p.p = pa.p pb := &myStruct{n: 42, p: &myStruct{n: 18}} pb.p.p = pb.p checkCmp(pa, pb, 0) } golang-github-maxatome-go-testdeep-1.14.0/helpers/tdutil/sort_values.go000066400000000000000000000035201454313311600262200ustar00rootroot00000000000000// Copyright (c) 2019, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package tdutil import ( "reflect" "sort" "github.com/maxatome/go-testdeep/internal/visited" ) // SortableValues is used to allow the sorting of a [][reflect.Value] // slice. It is used with the standard sort package: // // vals := []reflect.Value{a, b, c, d} // sort.Sort(SortableValues(vals)) // // vals contents now sorted // // Replace [sort.Sort] by [sort.Stable] for a stable sort. See [sort] // documentation. // // Sorting rules are as follows: // - nil is always lower // - different types are sorted by their name // - false is lesser than true // - float and int numbers are sorted by their value // - complex numbers are sorted by their real, then by their imaginary parts // - strings are sorted by their value // - map: shorter length is lesser, then sorted by address // - functions, channels and unsafe pointer are sorted by their address // - struct: comparison is spread to each field // - pointer: comparison is spread to the pointed value // - arrays: comparison is spread to each item // - slice: comparison is spread to each item, then shorter length is lesser // - interface: comparison is spread to the value // // Cyclic references are correctly handled. func SortableValues(s []reflect.Value) sort.Interface { r := &rValues{ Slice: s, } if len(s) > 1 { r.Visited = visited.NewVisited() } return r } type rValues struct { Visited visited.Visited Slice []reflect.Value } func (v *rValues) Len() int { return len(v.Slice) } func (v *rValues) Less(i, j int) bool { return cmp(v.Visited, v.Slice[i], v.Slice[j]) < 0 } func (v *rValues) Swap(i, j int) { v.Slice[i], v.Slice[j] = v.Slice[j], v.Slice[i] } golang-github-maxatome-go-testdeep-1.14.0/helpers/tdutil/sort_values_test.go000066400000000000000000000014571454313311600272660ustar00rootroot00000000000000// Copyright (c) 2019, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package tdutil_test import ( "reflect" "sort" "testing" "github.com/maxatome/go-testdeep/helpers/tdutil" ) func TestSortValues(t *testing.T) { s := []reflect.Value{ reflect.ValueOf(4), reflect.ValueOf(3), reflect.ValueOf(1), } sort.Sort(tdutil.SortableValues(s)) if s[0].Int() != 1 || s[1].Int() != 3 || s[2].Int() != 4 { t.Errorf("sort error: [ %v, %v, %v ]", s[0].Int(), s[1].Int(), s[2].Int()) } s = []reflect.Value{ reflect.ValueOf(42), } sort.Sort(tdutil.SortableValues(s)) if s[0].Int() != 42 { t.Errorf("sort error: [ %v ]", s[0].Int()) } sort.Sort(tdutil.SortableValues(nil)) } golang-github-maxatome-go-testdeep-1.14.0/helpers/tdutil/string.go000066400000000000000000000021111454313311600251530ustar00rootroot00000000000000// Copyright (c) 2019, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package tdutil import ( "strings" "unicode" "github.com/davecgh/go-spew/spew" ) // FormatString formats s to a printable string, trying to enclose it // in double-quotes or back-quotes and defaulting to using [SpewString]. func FormatString(s string) string { var unquotable, unbackquotable bool for _, chr := range s { if !unicode.IsPrint(chr) { if chr != '\n' { return SpewString(s) } unquotable = true if unbackquotable { break } continue } if chr == '"' { unquotable = true if unbackquotable { break } } else if chr == '`' { unbackquotable = true if unquotable { break } } } if unquotable { if unbackquotable { return SpewString(s) } return "`" + s + "`" } return `"` + s + `"` } // SpewString uses [spew.Sdump] to format val. func SpewString(val any) string { return strings.TrimRight(spew.Sdump(val), "\n") } golang-github-maxatome-go-testdeep-1.14.0/helpers/tdutil/string_test.go000066400000000000000000000022161454313311600262200ustar00rootroot00000000000000// Copyright (c) 2019, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package tdutil_test import ( "testing" "github.com/maxatome/go-testdeep/helpers/tdutil" ) func TestFormatString(t *testing.T) { for _, curTest := range []struct { paramGot string expected string }{ {paramGot: "foobar", expected: `"foobar"`}, {paramGot: "foo\rbar", expected: `(string) (len=7) "foo\rbar"`}, {paramGot: "foo\u2028bar", expected: `(string) (len=9) "foo\u2028bar"`}, {paramGot: `foo"bar`, expected: "`foo\"bar`"}, {paramGot: "foo\n\"bar", expected: "`foo\n\"bar`"}, {paramGot: "foo`\"\nbar", expected: "(string) (len=9) \"foo`\\\"\\nbar\""}, {paramGot: "foo`\n\"bar", expected: "(string) (len=9) \"foo`\\n\\\"bar\""}, {paramGot: "foo\n`\"bar", expected: "(string) (len=9) \"foo\\n`\\\"bar\""}, {paramGot: "foo\n\"`bar", expected: "(string) (len=9) \"foo\\n\\\"`bar\""}, } { got := tdutil.FormatString(curTest.paramGot) if got != curTest.expected { t.Errorf(`got "%s" ≠ expected "%s"`, got, curTest.expected) } } } golang-github-maxatome-go-testdeep-1.14.0/helpers/tdutil/t.go000066400000000000000000000041471454313311600241230ustar00rootroot00000000000000// Copyright (c) 2019, 2020, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. // Package tdutil allows to write unit tests for go-testdeep helpers // and so provides some helpful functions. // // It is not intended to be used in tests outside go-testdeep and its // helpers perimeter. package tdutil import ( "reflect" "testing" ) // T can be used in tests, to test [testing.T] behavior as it overrides // [testing.T.Run] method. type T struct { testing.T name string } type tFailedNow struct{} // NewT returns a new [*T] instance. name is the string returned by // method Name. func NewT(name string) *T { return &T{name: name} } // Run is a simplified version of [testing.T.Run] method, without edge // cases. func (t *T) Run(name string, f func(*testing.T)) bool { t.CatchFailNow(func() { f(&t.T) }) return !t.Failed() } // Name returns the name of the running test (in fact the one set by [NewT]). func (t *T) Name() string { return t.name } // LogBuf is an ugly hack allowing to access internal [testing.T] log // buffer. Keep cool, it is only used for internal unit tests. func (t *T) LogBuf() string { return string(reflect.ValueOf(t.T).FieldByName("output").Bytes()) //nolint: govet } // FailNow simulates the original [testing.T.FailNow] using // panic. [T.CatchFailNow] should be used to properly intercept it. func (t *T) FailNow() { t.Fail() panic(tFailedNow{}) } // Fatal simulates the original [testing.T.Fatal]. func (t *T) Fatal(args ...any) { t.Helper() t.Error(args...) t.FailNow() } // Fatal simulates the original [testing.T.Fatalf]. func (t *T) Fatalf(format string, args ...any) { t.Helper() t.Errorf(format, args...) t.FailNow() } // CatchFailNow returns true if a [T.FailNow], [T.Fatal] or [T.Fatalf] call // occurred during the execution of fn. func (t *T) CatchFailNow(fn func()) (failNowOccurred bool) { defer func() { if x := recover(); x != nil { _, failNowOccurred = x.(tFailedNow) if !failNowOccurred { panic(x) // rethrow } } }() fn() return } golang-github-maxatome-go-testdeep-1.14.0/helpers/tdutil/t_test.go000066400000000000000000000040121454313311600251510ustar00rootroot00000000000000// Copyright (c) 2019, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package tdutil_test import ( "strings" "testing" "github.com/maxatome/go-testdeep/helpers/tdutil" "github.com/maxatome/go-testdeep/internal/test" ) func TestT(t *testing.T) { mockT := tdutil.NewT("hey!") if n := mockT.Name(); n != "hey!" { t.Errorf(`Test name is not correct, got: %s, expected: hey!`, n) } mockT.Log("Hey this is a log message!") buf := mockT.LogBuf() if !strings.HasSuffix(buf, "Hey this is a log message!\n") { t.Errorf(`LogBuf does not work as expected: "%s"`, buf) } } func TestFailNow(t *testing.T) { mockT := tdutil.NewT("hey!") test.IsFalse(t, mockT.CatchFailNow(func() {})) test.IsTrue(t, mockT.CatchFailNow(func() { mockT.FailNow() })) test.IsTrue(t, mockT.CatchFailNow(func() { mockT.Fatal("Ouch!") })) test.IsTrue(t, mockT.CatchFailNow(func() { mockT.Fatalf("Ouch!") })) // No FailNow() but panic() var ( panicked, failNowOccurred bool panicParam any ) func() { defer func() { panicParam = recover() }() panicked = true failNowOccurred = mockT.CatchFailNow(func() { panic("Boom!") }) panicked = false }() test.IsFalse(t, failNowOccurred) if test.IsTrue(t, panicked) { panicStr, ok := panicParam.(string) if test.IsTrue(t, ok) { test.EqualStr(t, panicStr, "Boom!") } } } func TestRun(t *testing.T) { for i, curTest := range []struct { fn func(*testing.T) expected bool }{ { fn: func(*testing.T) {}, expected: true, }, { fn: func(t *testing.T) { t.Error("An error occurred!") }, expected: false, }, } { mockT := &tdutil.T{} var called bool res := mockT.Run("testname", func(t *testing.T) { called = true curTest.fn(t) }) if !called { t.Errorf("Run#%d func not called", i) } if res != curTest.expected { t.Errorf("Run#%d returned %v ≠ expected %v", i, res, curTest.expected) } } } golang-github-maxatome-go-testdeep-1.14.0/internal/000077500000000000000000000000001454313311600221705ustar00rootroot00000000000000golang-github-maxatome-go-testdeep-1.14.0/internal/anchors/000077500000000000000000000000001454313311600236255ustar00rootroot00000000000000golang-github-maxatome-go-testdeep-1.14.0/internal/anchors/anchor.go000066400000000000000000000160561454313311600254360ustar00rootroot00000000000000// Copyright (c) 2020, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package anchors import ( "errors" "fmt" "math" "reflect" "sync" ) type anchor struct { Anchor reflect.Value // Anchor is the generated value used as anchor Operator reflect.Value // Operator is a td.TestDeep behind } // Info gathers all anchors information. type Info struct { sync.Mutex index int persist bool anchors map[any]anchor } // NewInfo returns a new instance of [*Info]. func NewInfo() *Info { return &Info{ anchors: map[any]anchor{}, } } // AddAnchor anchors a new operator op, with type typ then returns the // anchor value. func (i *Info) AddAnchor(typ reflect.Type, op reflect.Value) (reflect.Value, error) { i.Lock() defer i.Unlock() anc, key, err := i.build(typ) if err != nil { return reflect.Value{}, err } if i.anchors == nil { i.anchors = map[any]anchor{} } i.anchors[key] = anchor{ Anchor: anc, Operator: op, } return anc, nil } // DoAnchorsPersist returns true if anchors are persistent across tests. func (i *Info) DoAnchorsPersist() bool { i.Lock() defer i.Unlock() return i.persist } // SetAnchorsPersist enables or disables anchors persistence. func (i *Info) SetAnchorsPersist(persist bool) { i.Lock() defer i.Unlock() i.persist = persist } // ResetAnchors removes all anchors if persistence is disabled or // force is true. func (i *Info) ResetAnchors(force bool) { i.Lock() defer i.Unlock() if !i.persist || force { for k := range i.anchors { delete(i.anchors, k) } i.index = 0 } } func (i *Info) nextIndex() (n int) { n = i.index i.index++ return } // ResolveAnchor checks whether the passed value matches an anchored // operator or not. If yes, this operator is returned with true. If // no, the value is returned as is with false. func (i *Info) ResolveAnchor(v reflect.Value) (reflect.Value, bool) { if i == nil || !v.CanInterface() { return v, false } // Shortcut i.Lock() la := len(i.anchors) i.Unlock() if la == 0 { return v, false } var key any sw: switch v.Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr, reflect.Float32, reflect.Float64, reflect.Complex64, reflect.Complex128, reflect.String: key = v.Interface() case reflect.Chan, reflect.Map, reflect.Slice, reflect.Ptr: key = v.Pointer() case reflect.Struct: typ := v.Type() if typ.Comparable() { // Check for anchorable types. No need of 2 passes here. for _, at := range AnchorableTypes { if typ == at.typ || at.typ.ConvertibleTo(typ) { // 1.17 ok as struct here key = v.Interface() break sw } } } fallthrough default: return v, false } i.Lock() defer i.Unlock() if anchor, ok := i.anchors[key]; ok { return anchor.Operator, true } return v, false } func (i *Info) setInt(typ reflect.Type, min int64) (reflect.Value, any) { nvm := reflect.New(typ).Elem() nvm.SetInt(min + int64(i.nextIndex())) return nvm, nvm.Interface() } func (i *Info) setUint(typ reflect.Type, max uint64) (reflect.Value, any) { nvm := reflect.New(typ).Elem() nvm.SetUint(max - uint64(i.nextIndex())) return nvm, nvm.Interface() } func (i *Info) setFloat(typ reflect.Type, min float64) (reflect.Value, any) { nvm := reflect.New(typ).Elem() nvm.SetFloat(min + float64(i.nextIndex())) return nvm, nvm.Interface() } func (i *Info) setComplex(typ reflect.Type, min float64) (reflect.Value, any) { nvm := reflect.New(typ).Elem() min += float64(i.nextIndex()) nvm.SetComplex(complex(min, min)) return nvm, nvm.Interface() } // build builds a new value of type "typ" and returns it under two // forms: // - the new value itself as a reflect.Value; // - an any usable as a key in an AnchorsSet map. // // It returns an error if "typ" kind is not recognized or if it is a // non-anchorable struct. func (i *Info) build(typ reflect.Type) (reflect.Value, any, error) { // For each numeric type, anchor the operator on a number close to // the limit of this type, but not at the extreme limit to avoid // edge cases where these limits are used in real world and so avoid // collisions switch typ.Kind() { case reflect.Int: nvm, iface := i.setInt(typ, int64(^int(^uint(0)>>1))+1004293) return nvm, iface, nil case reflect.Int8: nvm, iface := i.setInt(typ, math.MinInt8+13) return nvm, iface, nil case reflect.Int16: nvm, iface := i.setInt(typ, math.MinInt16+1049) return nvm, iface, nil case reflect.Int32: nvm, iface := i.setInt(typ, math.MinInt32+1004293) return nvm, iface, nil case reflect.Int64: nvm, iface := i.setInt(typ, math.MinInt64+1000424443) return nvm, iface, nil case reflect.Uint: nvm, iface := i.setUint(typ, uint64(^uint(0))-1004293) return nvm, iface, nil case reflect.Uint8: nvm, iface := i.setUint(typ, math.MaxUint8-29) return nvm, iface, nil case reflect.Uint16: nvm, iface := i.setUint(typ, math.MaxUint16-2099) return nvm, iface, nil case reflect.Uint32: nvm, iface := i.setUint(typ, math.MaxUint32-2008571) return nvm, iface, nil case reflect.Uint64: nvm, iface := i.setUint(typ, math.MaxUint64-2000848901) return nvm, iface, nil case reflect.Uintptr: nvm, iface := i.setUint(typ, uint64(^uintptr(0))-2000848901) return nvm, iface, nil case reflect.Float32: nvm, iface := i.setFloat(typ, -(1<<24)+104243) return nvm, iface, nil case reflect.Float64: nvm, iface := i.setFloat(typ, -(1<<53)+100004243) return nvm, iface, nil case reflect.Complex64: nvm, iface := i.setComplex(typ, -(1<<24)+104243) return nvm, iface, nil case reflect.Complex128: nvm, iface := i.setComplex(typ, -(1<<53)+100004243) return nvm, iface, nil case reflect.String: nvm := reflect.New(typ).Elem() nvm.SetString(fmt.Sprintf("", i.nextIndex())) return nvm, nvm.Interface(), nil case reflect.Chan: nvm := reflect.MakeChan(typ, 0) return nvm, nvm.Pointer(), nil case reflect.Map: nvm := reflect.MakeMap(typ) return nvm, nvm.Pointer(), nil case reflect.Slice: nvm := reflect.MakeSlice(typ, 0, 1) // cap=1 to avoid same ptr below return nvm, nvm.Pointer(), nil case reflect.Ptr: nvm := reflect.New(typ.Elem()) return nvm, nvm.Pointer(), nil case reflect.Struct: // First pass for the exact type for _, at := range AnchorableTypes { if typ == at.typ { nvm := at.builder.Call([]reflect.Value{reflect.ValueOf(i.nextIndex())})[0] return nvm, nvm.Interface(), nil } } // Second pass for convertible type for _, at := range AnchorableTypes { if at.typ.ConvertibleTo(typ) { nvm := at.builder.Call([]reflect.Value{reflect.ValueOf(i.nextIndex())})[0]. Convert(typ) return nvm, nvm.Interface(), nil } } return reflect.Value{}, nil, errors.New(typ.String() + " struct type is not supported as an anchor. Try AddAnchorableStructType") default: return reflect.Value{}, nil, errors.New(typ.Kind().String() + " kind is not supported as an anchor") } } golang-github-maxatome-go-testdeep-1.14.0/internal/anchors/anchor_test.go000066400000000000000000000135431454313311600264730ustar00rootroot00000000000000// Copyright (c) 2020, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package anchors_test import ( "reflect" "testing" "time" "github.com/maxatome/go-testdeep/internal/anchors" "github.com/maxatome/go-testdeep/internal/test" ) func TestInfo(t *testing.T) { i := anchors.NewInfo() test.IsFalse(t, i.DoAnchorsPersist()) i.SetAnchorsPersist(true) test.IsTrue(t, i.DoAnchorsPersist()) i.SetAnchorsPersist(false) test.IsFalse(t, i.DoAnchorsPersist()) } func TestBuildResolveAnchor(t *testing.T) { var i anchors.Info checkResolveAnchor := func(t *testing.T, val any, opName string) { t.Helper() v1, err := i.AddAnchor(reflect.TypeOf(val), reflect.ValueOf(opName+" (1)")) if !test.NoError(t, err, "first anchor") { return } v2, err := i.AddAnchor(reflect.TypeOf(val), reflect.ValueOf(opName+" (2)")) if !test.NoError(t, err, "second anchor") { return } op, found := i.ResolveAnchor(v1) test.IsTrue(t, found, "first anchor found") test.EqualStr(t, op.String(), opName+" (1)", "first anchor operator OK") op, found = i.ResolveAnchor(v2) test.IsTrue(t, found, "second anchor found") test.EqualStr(t, op.String(), opName+" (2)", "second anchor operator OK") } t.Run("AddAnchor basic types", func(t *testing.T) { checkResolveAnchor(t, 0, "int") checkResolveAnchor(t, int8(0), "int8") checkResolveAnchor(t, int16(0), "int16") checkResolveAnchor(t, int32(0), "int32") checkResolveAnchor(t, int64(0), "int64") checkResolveAnchor(t, uint(0), "uint") checkResolveAnchor(t, uint8(0), "uint8") checkResolveAnchor(t, uint16(0), "uint16") checkResolveAnchor(t, uint32(0), "uint32") checkResolveAnchor(t, uint64(0), "uint64") checkResolveAnchor(t, uintptr(0), "uintptr") checkResolveAnchor(t, float32(0), "float32") checkResolveAnchor(t, float64(0), "float64") checkResolveAnchor(t, complex(float32(0), 0), "complex64") checkResolveAnchor(t, complex(float64(0), 0), "complex128") checkResolveAnchor(t, "", "string") checkResolveAnchor(t, (chan int)(nil), "chan") checkResolveAnchor(t, (map[string]bool)(nil), "map") checkResolveAnchor(t, ([]int)(nil), "slice") checkResolveAnchor(t, (*time.Time)(nil), "pointer") }) t.Run("AddAnchor", func(t *testing.T) { oldAnchorableTypes := anchors.AnchorableTypes defer func() { anchors.AnchorableTypes = oldAnchorableTypes }() type ok struct{ index int } // AddAnchor for ok type err := anchors.AddAnchorableStructType(func(nextAnchor int) ok { return ok{index: 1000 + nextAnchor} }) if err != nil { t.Fatalf("AddAnchorableStructType failed: %s", err) } checkResolveAnchor(t, ok{}, "ok{}") // AddAnchor for ok convertible type type okConvert ok checkResolveAnchor(t, okConvert{}, "okConvert{}") // Replace ok type err = anchors.AddAnchorableStructType(func(nextAnchor int) ok { return ok{index: 2000 + nextAnchor} }) if err != nil { t.Fatalf("AddAnchorableStructType failed: %s", err) } if len(anchors.AnchorableTypes) != 2 { t.Fatalf("Bad number of anchored type: got=%d expected=2", len(anchors.AnchorableTypes)) } checkResolveAnchor(t, ok{}, "ok{}") // AddAnchor for builtin time.Time type checkResolveAnchor(t, time.Time{}, "time.Time{}") // AddAnchor for unknown type _, err = i.AddAnchor(reflect.TypeOf(func() {}), reflect.ValueOf(123)) if test.Error(t, err) { test.EqualStr(t, err.Error(), "func kind is not supported as an anchor") } // AddAnchor for unknown struct type _, err = i.AddAnchor(reflect.TypeOf(struct{}{}), reflect.ValueOf(123)) if test.Error(t, err) { test.EqualStr(t, err.Error(), "struct {} struct type is not supported as an anchor. Try AddAnchorableStructType") } // Struct not comparable type notComparable struct{ s []int } v := reflect.ValueOf(notComparable{s: []int{42}}) op, found := i.ResolveAnchor(v) test.IsFalse(t, found) if !reflect.DeepEqual(v.Interface(), op.Interface()) { test.EqualErrorMessage(t, op.Interface(), v.Interface()) } // Struct comparable but not anchored v = reflect.ValueOf(struct{}{}) op, found = i.ResolveAnchor(v) test.IsFalse(t, found) if !reflect.DeepEqual(v.Interface(), op.Interface()) { test.EqualErrorMessage(t, op.Interface(), v.Interface()) } // Struct anchored once, but not for this value v = reflect.ValueOf(ok{index: 42424242}) op, found = i.ResolveAnchor(v) test.IsFalse(t, found) if !reflect.DeepEqual(v.Interface(), op.Interface()) { test.EqualErrorMessage(t, op.Interface(), v.Interface()) } // Kind not supported v = reflect.ValueOf(true) op, found = i.ResolveAnchor(v) test.IsFalse(t, found) if !reflect.DeepEqual(v.Interface(), op.Interface()) { test.EqualErrorMessage(t, op.Interface(), v.Interface()) } }) t.Run("ResetAnchors", func(t *testing.T) { v, err := i.AddAnchor(reflect.TypeOf(12), reflect.ValueOf("zip")) if !test.NoError(t, err) { return } op, found := i.ResolveAnchor(v) test.IsTrue(t, found) test.EqualStr(t, op.String(), "zip") i.SetAnchorsPersist(true) i.ResetAnchors(false) op, found = i.ResolveAnchor(v) test.IsTrue(t, found) test.EqualStr(t, op.String(), "zip") i.ResetAnchors(true) _, found = i.ResolveAnchor(reflect.ValueOf(42)) test.IsFalse(t, found) i.SetAnchorsPersist(false) v, err = i.AddAnchor(reflect.TypeOf(12), reflect.ValueOf("xxx")) if !test.NoError(t, err) { return } op, found = i.ResolveAnchor(v) test.IsTrue(t, found) test.EqualStr(t, op.String(), "xxx") i.ResetAnchors(false) _, found = i.ResolveAnchor(reflect.ValueOf(42)) test.IsFalse(t, found) }) t.Run("skip", func(t *testing.T) { var i *anchors.Info _, found := i.ResolveAnchor(reflect.ValueOf(42)) test.IsFalse(t, found) i = &anchors.Info{} _, found = i.ResolveAnchor(reflect.ValueOf(42)) test.IsFalse(t, found) }) } golang-github-maxatome-go-testdeep-1.14.0/internal/anchors/any.go000066400000000000000000000004231454313311600247420ustar00rootroot00000000000000// Copyright (c) 2022, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. //go:build !go1.18 // +build !go1.18 package anchors type any = interface{} golang-github-maxatome-go-testdeep-1.14.0/internal/anchors/any_test.go000066400000000000000000000004301454313311600257770ustar00rootroot00000000000000// Copyright (c) 2022, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. //go:build !go1.18 // +build !go1.18 package anchors_test type any = interface{} golang-github-maxatome-go-testdeep-1.14.0/internal/anchors/types.go000066400000000000000000000045511454313311600253250ustar00rootroot00000000000000// Copyright (c) 2020, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package anchors import ( "errors" "fmt" "math" "reflect" "time" "github.com/maxatome/go-testdeep/internal/types" ) type anchorableType struct { typ reflect.Type builder reflect.Value } // AnchorableTypes contains all non-native types that can be // anchorable. See [AddAnchorableStructType] to add a new type to it. var AnchorableTypes []anchorableType func init() { AddAnchorableStructType(func(nextAnchor int) time.Time { //nolint: errcheck return time.Unix(math.MaxInt64-1000424443-int64(nextAnchor), 42) }) } // AddAnchorableStructType declares a struct type as anchorable. fn // is a function allowing to return a unique and identifiable instance // of the struct type. // // fn has to have the following signature: // // func (nextAnchor int) TYPE // // TYPE is the struct type to make anchorable and nextAnchor is an // index to allow to differentiate several instances of the same type. // // For example, the [time.Time] type which is anchorable by default, // is declared as: // // AddAnchorableStructType(func (nextAnchor int) time.Time { // return time.Unix(math.MaxInt64-1000424443-int64(nextAnchor), 42) // }) // // Just as a note, the 1000424443 constant allows to avoid to flirt // with the [math.MaxInt64] extreme limit and so avoid possible // collision with real world values. // // It returns an error if the provided fn is not a function or if it // has not the expected signature (see above). func AddAnchorableStructType(fn any) error { vfn := reflect.ValueOf(fn) if vfn.Kind() == reflect.Func { fnType := vfn.Type() if !fnType.IsVariadic() && fnType.NumIn() == 1 && fnType.NumOut() == 1 && fnType.In(0) == types.Int && fnType.Out(0).Kind() == reflect.Struct { typ := fnType.Out(0) if !typ.Comparable() { return fmt.Errorf( "type %s is not comparable, it cannot be anchorable", typ) } for i, at := range AnchorableTypes { if at.typ == typ { AnchorableTypes[i].builder = vfn return nil } } AnchorableTypes = append(AnchorableTypes, anchorableType{ typ: typ, builder: vfn, }) return nil } } return errors.New("usage: AddAnchorableStructType(func (nextAnchor int) STRUCT_TYPE)") } golang-github-maxatome-go-testdeep-1.14.0/internal/anchors/types_test.go000066400000000000000000000033101454313311600263540ustar00rootroot00000000000000// Copyright (c) 2020-2022, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package anchors_test import ( "strings" "testing" "github.com/maxatome/go-testdeep/internal/anchors" ) func TestAddAnchorableStructType(t *testing.T) { oldAnchorableTypes := anchors.AnchorableTypes defer func() { anchors.AnchorableTypes = oldAnchorableTypes }() type ok struct{ index int } type notComparable struct{ s []int } //nolint: unused // Usage error cases for i, fn := range []any{ 12, func(x ...int) {}, func(x, y int) {}, func(x int) (int, int) { return 0, 0 }, func(x byte) int { return 0 }, func(x int) int { return 0 }, } { err := anchors.AddAnchorableStructType(fn) if err == nil { t.Fatalf("#%d function should return an error", i) } if !strings.HasPrefix(err.Error(), "usage: ") { t.Errorf("#%d function returned: `%s` instead of usage", i, err) } } // Not comparable struct err := anchors.AddAnchorableStructType(func(nextAnchor int) notComparable { return notComparable{} }) if err == nil { t.Fatal("function should return an error") } if err.Error() != "type anchors_test.notComparable is not comparable, it cannot be anchorable" { t.Errorf("function returned: `%s` instead of not comparable error", err) } // Comparable struct => OK err = anchors.AddAnchorableStructType(func(nextAnchor int) ok { return ok{index: 1000 + nextAnchor} }) if err != nil { t.Fatalf("AddAnchorableStructType failed: %s", err) } if len(anchors.AnchorableTypes) != 2 { t.Fatalf("Bad number of anchored type: got=%d expected=2", len(anchors.AnchorableTypes)) } } golang-github-maxatome-go-testdeep-1.14.0/internal/color/000077500000000000000000000000001454313311600233065ustar00rootroot00000000000000golang-github-maxatome-go-testdeep-1.14.0/internal/color/any.go000066400000000000000000000004211454313311600244210ustar00rootroot00000000000000// Copyright (c) 2022, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. //go:build !go1.18 // +build !go1.18 package color type any = interface{} golang-github-maxatome-go-testdeep-1.14.0/internal/color/any_test.go000066400000000000000000000004261454313311600254650ustar00rootroot00000000000000// Copyright (c) 2022, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. //go:build !go1.18 // +build !go1.18 package color_test type any = interface{} golang-github-maxatome-go-testdeep-1.14.0/internal/color/color.go000066400000000000000000000151701454313311600247570ustar00rootroot00000000000000// Copyright (c) 2019, 2020 Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package color import ( "fmt" "os" "reflect" "strings" "sync" ) const ( // EnvColor is the name of the environment variable allowing to // enable/disable coloring feature. EnvColor = "TESTDEEP_COLOR" // EnvColorTestName is the name of the environment variable // containing the color of test names in error reports. EnvColorTestName = "TESTDEEP_COLOR_TEST_NAME" // EnvColorTitle is the name of the environment variable // containing the color of failure reason in error reports. EnvColorTitle = "TESTDEEP_COLOR_TITLE" // EnvColorOK is the name of the environment variable // containing the color of "expected" in error reports. EnvColorOK = "TESTDEEP_COLOR_OK" // EnvColorBad is the name of the environment variable // containing the color of "got" in error reports. EnvColorBad = "TESTDEEP_COLOR_BAD" ) var ( // TestNameOn contains the ANSI color escape sequence to turn test // name color on. TestNameOn string // TestNameOff contains the ANSI color escape sequence to turn test // name color off. TestNameOff string // TitleOn contains the ANSI color escape sequence to turn title color on. TitleOn string // TitleOff contains the ANSI color escape sequence to turn title color off. TitleOff string // OKOn contains the ANSI color escape sequence to turn "expected" color on. OKOn string // OKOnBold contains the ANSI color escape sequence to turn // "expected" color and bold on. OKOnBold string // OKOff contains the ANSI color escape sequence to turn "expected" color off. OKOff string // BadOn contains the ANSI color escape sequence to turn "got" color on. BadOn string // BadOnBold contains the ANSI color escape sequence to turn "got" // color and bold on. BadOnBold string // BadOff contains the ANSI color escape sequence to turn "got" color off. BadOff string ) var initOnce sync.Once // Init initializes all the colors from the environment. It can be // called several times concurrently, but only the first call is // effective. func Init() { initOnce.Do(func() { _, TestNameOn, TestNameOff = FromEnv(EnvColorTestName, "yellow") _, TitleOn, TitleOff = FromEnv(EnvColorTitle, "cyan") OKOn, OKOnBold, OKOff = FromEnv(EnvColorOK, "green") BadOn, BadOnBold, BadOff = FromEnv(EnvColorBad, "red") }) } // SaveState saves the "TESTDEEP_COLOR" environment variable // value, sets it to "on" (if true passed as on) or "off" (if on not // passed or set to false), resets the colors initialization and // returns a function to be called in a defer statement. Only intended // to be used in tests like: // // defer color.SaveState()() // // It is not thread-safe. func SaveState(on ...bool) func() { colorState, set := os.LookupEnv(EnvColor) if len(on) == 0 || !on[0] { os.Setenv(EnvColor, "off") //nolint: errcheck } else { os.Setenv(EnvColor, "on") //nolint: errcheck } initOnce = sync.Once{} return func() { if set { os.Setenv(EnvColor, colorState) //nolint: errcheck } else { os.Unsetenv(EnvColor) //nolint: errcheck } initOnce = sync.Once{} } } // On returns true if coloring feature is enabled. func On() bool { switch os.Getenv(EnvColor) { case "on", "": return true default: // "off" or any other value return false } } var colors = map[string]byte{ "black": '0', "red": '1', "green": '2', "yellow": '3', "blue": '4', "magenta": '5', "cyan": '6', "white": '7', "gray": '7', } // FromEnv returns the light, bold and end ANSI sequences for the // color contained in the environment variable env. defaultColor is // used if the environment variable does exist or is empty. // // If coloring is disabled, returns "", "", "". func FromEnv(env, defaultColor string) (string, string, string) { var color string if On() { if curColor := os.Getenv(env); curColor != "" { color = curColor } else { color = defaultColor } } if color == "" { return "", "", "" } names := strings.SplitN(color, ":", 2) light := [...]byte{ // 0 1 2 4 4 5 6 '\x1b', '[', '0', ';', '3', 'y', 'm', // foreground // 7 8 9 10 11 '\x1b', '[', '4', 'z', 'm', // background } bold := [...]byte{ // 0 1 2 4 4 5 6 '\x1b', '[', '1', ';', '3', 'y', 'm', // foreground // 7 8 9 10 11 '\x1b', '[', '4', 'z', 'm', // background } var start, end int // Foreground if names[0] != "" { c := colors[names[0]] if c == 0 { c = colors[defaultColor] } light[5] = c bold[5] = c end = 7 } else { start = 7 } // Background if len(names) > 1 && names[1] != "" { c := colors[names[1]] if c != 0 { light[10] = c bold[10] = c end = 12 } } return string(light[start:end]), string(bold[start:end]), "\x1b[0m" } // AppendTestNameOn enables test name color in b. func AppendTestNameOn(b *strings.Builder) { Init() b.WriteString(TestNameOn) } // AppendTestNameOff disables test name color in b. func AppendTestNameOff(b *strings.Builder) { Init() b.WriteString(TestNameOff) } // Bad returns a string surrounded by BAD color. If len(args) is > 0, // s and args are given to fmt.Sprintf. // // Typically used in panic() when the user made a mistake. func Bad(s string, args ...any) string { Init() if len(args) == 0 { return BadOnBold + s + BadOff } return fmt.Sprintf(BadOnBold+s+BadOff, args...) } // BadUsage returns a string surrounded by BAD color to notice the // user he passes a bad parameter to a function. Typically used in a // panic(). func BadUsage(usage string, param any, pos int, kind bool) string { Init() var b strings.Builder fmt.Fprintf(&b, "%susage: %s, but received ", BadOnBold, usage) if param == nil { b.WriteString("nil") } else { t := reflect.TypeOf(param) if kind && t.String() != t.Kind().String() { fmt.Fprintf(&b, "%s (%s)", t, t.Kind()) } else { b.WriteString(t.String()) } } b.WriteString(" as ") switch pos { case 1: b.WriteString("1st") case 2: b.WriteString("2nd") case 3: b.WriteString("3rd") default: fmt.Fprintf(&b, "%dth", pos) } b.WriteString(" parameter") b.WriteString(BadOff) return b.String() } // TooManyParams returns a string surrounded by BAD color to notice // the user he called a variadic function with too many // parameters. Typically used in a panic(). func TooManyParams(usage string) string { Init() return BadOnBold + "usage: " + usage + ", too many parameters" + BadOff } // UnBad returns s with bad color prefix & suffix removed. func UnBad(s string) string { return strings.TrimSuffix(strings.TrimPrefix(s, BadOnBold), BadOff) } golang-github-maxatome-go-testdeep-1.14.0/internal/color/color_test.go000066400000000000000000000115221454313311600260130ustar00rootroot00000000000000// Copyright (c) 2019, 2020 Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package color_test import ( "os" "strings" "testing" "github.com/maxatome/go-testdeep/internal/color" "github.com/maxatome/go-testdeep/internal/test" ) func TestColor(t *testing.T) { defer color.SaveState()() // off for _, flag := range []string{"off", "xxbad"} { os.Setenv("TESTDEEP_COLOR", flag) os.Setenv("MY_TEST_COLOR", "green") light, bold, off := color.FromEnv("MY_TEST_COLOR", "red") test.EqualStr(t, light, "") test.EqualStr(t, bold, "") test.EqualStr(t, off, "") var b strings.Builder color.AppendTestNameOn(&b) test.EqualInt(t, b.Len(), 0) color.AppendTestNameOff(&b) test.EqualInt(t, b.Len(), 0) } // on colorTestNameOnSave, colorTestNameOffSave := color.TestNameOn, color.TestNameOff defer func() { color.TestNameOn, color.TestNameOff = colorTestNameOnSave, colorTestNameOffSave }() for _, flag := range []string{"on", ""} { os.Setenv("TESTDEEP_COLOR", flag) os.Setenv("MY_TEST_COLOR", "") light, bold, off := color.FromEnv("MY_TEST_COLOR", "red") test.EqualStr(t, light, "\x1b[0;31m") test.EqualStr(t, bold, "\x1b[1;31m") test.EqualStr(t, off, "\x1b[0m") // on + override os.Setenv("MY_TEST_COLOR", "green") light, bold, off = color.FromEnv("MY_TEST_COLOR", "red") test.EqualStr(t, light, "\x1b[0;32m") test.EqualStr(t, bold, "\x1b[1;32m") test.EqualStr(t, off, "\x1b[0m") // on + override including background os.Setenv("MY_TEST_COLOR", "green:magenta") light, bold, off = color.FromEnv("MY_TEST_COLOR", "red") test.EqualStr(t, light, "\x1b[0;32m\x1b[45m") test.EqualStr(t, bold, "\x1b[1;32m\x1b[45m") test.EqualStr(t, off, "\x1b[0m") // on + override including background only os.Setenv("MY_TEST_COLOR", ":magenta") light, bold, off = color.FromEnv("MY_TEST_COLOR", "red") test.EqualStr(t, light, "\x1b[45m") test.EqualStr(t, bold, "\x1b[45m") test.EqualStr(t, off, "\x1b[0m") // on + bad colors os.Setenv("MY_TEST_COLOR", "foo:bar") light, bold, off = color.FromEnv("MY_TEST_COLOR", "red") test.EqualStr(t, light, "\x1b[0;31m") // red test.EqualStr(t, bold, "\x1b[1;31m") // bold red test.EqualStr(t, off, "\x1b[0m") // Color test name _, color.TestNameOn, color.TestNameOff = color.FromEnv(color.EnvColorTitle, "yellow") var b strings.Builder color.AppendTestNameOn(&b) test.EqualStr(t, b.String(), "\x1b[1;33m") color.AppendTestNameOff(&b) test.EqualStr(t, b.String(), "\x1b[1;33m\x1b[0m") } } func TestSaveState(t *testing.T) { check := func(expected string) { t.Helper() test.EqualStr(t, os.Getenv("TESTDEEP_COLOR"), expected) } defer color.SaveState()() check("off") func() { defer color.SaveState(true)() check("on") }() check("off") func() { defer color.SaveState(false)() check("off") }() check("off") os.Unsetenv("TESTDEEP_COLOR") checkDoesNotExist := func() { t.Helper() _, exists := os.LookupEnv("TESTDEEP_COLOR") test.IsFalse(t, exists) } func() { defer color.SaveState(true)() check("on") }() checkDoesNotExist() func() { defer color.SaveState(false)() check("off") }() checkDoesNotExist() } func TestBad(t *testing.T) { defer color.SaveState()() test.EqualStr(t, color.Bad("test"), "test") test.EqualStr(t, color.Bad("test %d", 123), "test 123") } func TestBadUsage(t *testing.T) { defer color.SaveState()() test.EqualStr(t, color.BadUsage("Zzz(STRING)", nil, 1, true), "usage: Zzz(STRING), but received nil as 1st parameter") test.EqualStr(t, color.BadUsage("Zzz(STRING)", 42, 1, true), "usage: Zzz(STRING), but received int as 1st parameter") test.EqualStr(t, color.BadUsage("Zzz(STRING)", []int{}, 1, true), "usage: Zzz(STRING), but received []int (slice) as 1st parameter") test.EqualStr(t, color.BadUsage("Zzz(STRING)", []int{}, 1, false), "usage: Zzz(STRING), but received []int as 1st parameter") test.EqualStr(t, color.BadUsage("Zzz(STRING)", nil, 1, true), "usage: Zzz(STRING), but received nil as 1st parameter") test.EqualStr(t, color.BadUsage("Zzz(STRING)", nil, 2, true), "usage: Zzz(STRING), but received nil as 2nd parameter") test.EqualStr(t, color.BadUsage("Zzz(STRING)", nil, 3, true), "usage: Zzz(STRING), but received nil as 3rd parameter") test.EqualStr(t, color.BadUsage("Zzz(STRING)", nil, 4, true), "usage: Zzz(STRING), but received nil as 4th parameter") } func TestTooManyParams(t *testing.T) { defer color.SaveState()() test.EqualStr(t, color.TooManyParams("Zzz(PARAM)"), "usage: Zzz(PARAM), too many parameters") } func TestUnBad(t *testing.T) { defer color.SaveState(true)() const mesg = "test" s := color.Bad(mesg) if s == mesg { t.Errorf("Bad should produce colored output: %s ≠ %s", s, mesg) } test.EqualStr(t, color.UnBad(s), mesg) } golang-github-maxatome-go-testdeep-1.14.0/internal/ctxerr/000077500000000000000000000000001454313311600234775ustar00rootroot00000000000000golang-github-maxatome-go-testdeep-1.14.0/internal/ctxerr/any.go000066400000000000000000000004221454313311600246130ustar00rootroot00000000000000// Copyright (c) 2022, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. //go:build !go1.18 // +build !go1.18 package ctxerr type any = interface{} golang-github-maxatome-go-testdeep-1.14.0/internal/ctxerr/any_test.go000066400000000000000000000004271454313311600256570ustar00rootroot00000000000000// Copyright (c) 2022, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. //go:build !go1.18 // +build !go1.18 package ctxerr_test type any = interface{} golang-github-maxatome-go-testdeep-1.14.0/internal/ctxerr/context.go000066400000000000000000000120131454313311600255070ustar00rootroot00000000000000// Copyright (c) 2018, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package ctxerr import ( "testing" "github.com/maxatome/go-testdeep/internal/anchors" "github.com/maxatome/go-testdeep/internal/hooks" "github.com/maxatome/go-testdeep/internal/location" "github.com/maxatome/go-testdeep/internal/visited" ) // Context is used internally to keep track of the Cmp in-depth // traversal. type Context struct { Path Path Visited visited.Visited CurOperator location.GetLocationer Depth int // 0 ≤ MaxErrors ≤ 1 stops when first error encoutered (without the // "Too many errors" error); // MaxErrors > 1 stops when MaxErrors'th error encoutered (with a // last "Too many errors" error); // < 0 do not stop until comparison ends. MaxErrors int Errors *[]*Error Anchors *anchors.Info Hooks *hooks.Info OriginalTB testing.TB // only used by Code operator // If true, the contents of the returned *Error will not be // checked. Can be used to avoid filling Error{} with expensive // computations. BooleanError bool // See ContextConfig.FailureIsFatal for details. FailureIsFatal bool // See ContextConfig.UseEqual for details. UseEqual bool // See ContextConfig.BeLax for details. BeLax bool // See ContextConfig.IgnoreUnexported for details. IgnoreUnexported bool // See ContextConfig.TestDeepInGotOK for details. TestDeepInGotOK bool } // InitErrors initializes [Context] *Errors slice, if MaxErrors < 0 or // MaxErrors > 1. func (c *Context) InitErrors() { if c.MaxErrors != 0 && c.MaxErrors != 1 { var errors []*Error c.Errors = &errors } } // ResetErrors returns a new [Context] without any Error set. func (c Context) ResetErrors() (newc Context) { newc = c newc.InitErrors() return } // CollectError collects an error in the context. It returns an error // if the collector is full, nil otherwise. // // In boolean context, it ignores the passed error and returns the // [BooleanError]. func (c Context) CollectError(err *Error) *Error { if err == nil { return nil } // The boolean error must not be altered! if c.BooleanError { return BooleanError } // Error context not initialized yet if err.Context.Depth == 0 { err.Context = c } if !err.Location.IsInitialized() && c.CurOperator != nil { err.Location = c.CurOperator.GetLocation() } // Stop when first error encoutered if c.Errors == nil { return err } // Skip it if already encountered as Re in JSON(`[$1,$1]`, Re(123)) for _, cur := range *c.Errors { if cur == err { return nil } } // Else, accumulate... *c.Errors = append(*c.Errors, err) if c.MaxErrors >= 0 && len(*c.Errors) >= c.MaxErrors { *c.Errors = append(*c.Errors, ErrTooManyErrors) return c.MergeErrors() } return nil } // MergeErrors merges all collected errors in the first one and // returns it. It returns nil if no errors have been collected. func (c Context) MergeErrors() *Error { if c.Errors == nil || len(*c.Errors) == 0 { return nil } if len(*c.Errors) > 1 { for idx, last := 0, len(*c.Errors)-2; idx <= last; idx++ { (*c.Errors)[idx].Next = (*c.Errors)[idx+1] } } return (*c.Errors)[0] } // CannotCompareError returns a generic error used when the access of // unexported fields cannot be overridden. func (c Context) CannotCompareError() *Error { if c.BooleanError { return BooleanError } return &Error{ Message: "cannot compare", Summary: NewSummary("unexported field that cannot be overridden"), } } // AddCustomLevel creates a new [Context] from current one plus pathAdd. func (c Context) AddCustomLevel(pathAdd string) (newc Context) { newc = c newc.Path = newc.Path.AddCustomLevel(pathAdd) newc.Depth++ return } // AddField creates a new [Context] from current one plus "." + field. func (c Context) AddField(field string) (newc Context) { newc = c newc.Path = newc.Path.AddField(field) newc.Depth++ return } // AddArrayIndex creates a new [Context] from current one plus an array // dereference for index-th item. func (c Context) AddArrayIndex(index int) (newc Context) { newc = c newc.Path = newc.Path.AddArrayIndex(index) newc.Depth++ return } // AddMapKey creates a new [Context] from current one plus a map // dereference for key key. func (c Context) AddMapKey(key any) (newc Context) { newc = c newc.Path = newc.Path.AddMapKey(key) newc.Depth++ return } // AddPtr creates a new [Context] from current one plus a pointer dereference. func (c Context) AddPtr(num int) (newc Context) { newc = c newc.Path = newc.Path.AddPtr(num) newc.Depth++ return } // AddFunctionCall creates a new [Context] from current one inside a // function call. func (c Context) AddFunctionCall(fn string) (newc Context) { newc = c newc.Path = newc.Path.AddFunctionCall(fn) newc.Depth++ return } // ResetPath creates a new [Context] from current one but reinitializing Path. func (c Context) ResetPath(newRoot string) (newc Context) { newc = c newc.Path = NewPath(newRoot) newc.Depth++ return } golang-github-maxatome-go-testdeep-1.14.0/internal/ctxerr/context_test.go000066400000000000000000000160471454313311600265610ustar00rootroot00000000000000// Copyright (c) 2018, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package ctxerr_test import ( "testing" "github.com/maxatome/go-testdeep/internal/ctxerr" "github.com/maxatome/go-testdeep/internal/location" "github.com/maxatome/go-testdeep/internal/test" ) func TestContext(t *testing.T) { for _, maxErrors := range []int{0, 1} { ctx := ctxerr.Context{ MaxErrors: maxErrors, } ctx.InitErrors() if ctx.Errors != nil { t.Errorf("Errors is non-nil for MaxErrors %d", maxErrors) } } for _, maxErrors := range []int{-1, 2} { ctx := ctxerr.Context{ MaxErrors: maxErrors, } ctx.InitErrors() if ctx.Errors == nil { t.Errorf("Errors is nil for MaxErrors %d", maxErrors) continue } *ctx.Errors = append(*ctx.Errors, &ctxerr.Error{}) newc := ctx.ResetErrors() if newc.Errors == nil { t.Errorf("after ResetErrors, new Errors is nil for MaxErrors %d", maxErrors) continue } if len(*newc.Errors) > 0 { t.Errorf("after ResetErrors, new Errors is not empty for MaxErrors %d", maxErrors) } if ctx.Errors == nil { t.Errorf("after ResetErrors, old Errors is nil for MaxErrors %d", maxErrors) continue } } } type MyGetLocationer struct{} func (g MyGetLocationer) GetLocation() location.Location { return location.Location{ File: "context_test.go", Func: "MyFunc", Line: 42, } } func TestContextMergeErrors(t *testing.T) { // No errors to merge ctx := ctxerr.Context{} if ctx.MergeErrors() != nil { t.Error("ctx.MergeErrors() returned a *Error") } errors := []*ctxerr.Error{} ctx = ctxerr.Context{ Errors: &errors, } if ctx.MergeErrors() != nil { t.Error("ctx.MergeErrors() returned a *Error") } // Only 1 error to merge => itself firstErr := &ctxerr.Error{} errors = []*ctxerr.Error{firstErr} ctx = ctxerr.Context{ Errors: &errors, } if ctx.MergeErrors() != firstErr { t.Error("ctx.MergeErrors() did not return the only one error") } // Several errors to merge secondErr, thirdErr := &ctxerr.Error{}, &ctxerr.Error{} errors = []*ctxerr.Error{firstErr, secondErr, thirdErr} ctx = ctxerr.Context{ Errors: &errors, } if ctx.MergeErrors() != firstErr { t.Error("ctx.MergeErrors() did not return the first error") return } if firstErr.Next != secondErr { t.Error("ctx.MergeErrors() second error is not linked to first one") return } if secondErr.Next != thirdErr { t.Error("ctx.MergeErrors() third error is not linked to second one") return } if thirdErr.Next != nil { t.Error("ctx.MergeErrors() third error has a non-nil Next!") } } func TestContextCollectError(t *testing.T) { // // Only one error kept ctx := ctxerr.Context{} if ctx.CollectError(nil) != nil { t.Error("ctx.CollectError(nil) returned non-nil *Error") } err := ctxerr.Context{BooleanError: true}.CollectError(&ctxerr.Error{}) if err != ctxerr.BooleanError { t.Error("boolean-ctx.CollectError(X) did not return BooleanError") } // !err.Location.IsInitialized() + ctx.CurOperator == nil origErr := &ctxerr.Error{} err = ctx.CollectError(origErr) if err != origErr { t.Error("ctx.CollectError(err) != err") } // !err.Location.IsInitialized() + ctx.CurOperator != nil ctx.CurOperator = MyGetLocationer{} origErr = &ctxerr.Error{} err = ctx.CollectError(origErr) if err != origErr { t.Error("ctx.CollectError(err) != err") } test.EqualInt(t, err.Location.Line, 42, // see MyGetLocationer.GetLocation() "ctx.CollectError(err) initialized err.Location") // err.Location.IsInitialized() origErr = &ctxerr.Error{ Location: location.Location{ File: "zz.go", Func: "ErrFunc", Line: 24, }, } err = ctx.CollectError(origErr) if err != origErr { t.Error("ctx.CollectError(err) != err") } test.EqualInt(t, err.Location.Line, 24, "ctx.CollectError(err) did not touch err.Location") // // 2 errors kept max errors := []*ctxerr.Error{} ctx = ctxerr.Context{ Errors: &errors, MaxErrors: 2, } origErr = &ctxerr.Error{} if ctx.CollectError(origErr) != nil { // 1st error is accumulated t.Error("ctx.CollectError(err) != nil") return } secondErr := &ctxerr.Error{} if ctx.CollectError(secondErr) != origErr { t.Error("ctx.CollectError(err) != origErr") return } if origErr.Next != secondErr { t.Error("origErr.Next != secondErr") return } if secondErr.Next != ctxerr.ErrTooManyErrors { t.Error("secondErr.Next != ErrTooManyErrors") return } // // All errors kept errors = nil ctx = ctxerr.Context{ Errors: &errors, MaxErrors: -1, } for i := 0; i < 100; i++ { if ctx.CollectError(&ctxerr.Error{}) != nil { // 1st error is accumulated t.Errorf("#%d: ctx.CollectError(err) != nil", i) return } } if len(errors) != 100 { t.Errorf("Only %d errors accumulated instead of 100", len(errors)) } // // Do not collect 2 times the same error errors = nil ctx = ctxerr.Context{ Errors: &errors, MaxErrors: -1, } ctx.CollectError(&ctxerr.Error{}) //nolint: errcheck x := &ctxerr.Error{} ctx.CollectError(x) //nolint: errcheck ctx.CollectError(&ctxerr.Error{}) //nolint: errcheck ctx.CollectError(x) //nolint: errcheck ctx.CollectError(x) //nolint: errcheck ctx.CollectError(&ctxerr.Error{}) //nolint: errcheck if len(errors) != 4 { t.Errorf("%d errors accumulated instead of 4", len(errors)) } } func TestCannotCompareError(t *testing.T) { ctx := ctxerr.Context{BooleanError: true} err := ctx.CannotCompareError() if err != ctxerr.BooleanError { t.Error("CannotCompareError does not return ctxerr.BooleanError") } ctx = ctxerr.Context{} err = ctx.CannotCompareError() test.EqualStr(t, err.Message, "cannot compare") } func TestContextPath(t *testing.T) { ctx := ctxerr.Context{Path: ctxerr.NewPath("DATA")} ctx = ctx.AddField("field") test.EqualStr(t, ctx.Path.String(), "DATA.field") test.EqualInt(t, ctx.Depth, 1) ctx = ctx.AddPtr(2) test.EqualStr(t, ctx.Path.String(), "**DATA.field") test.EqualInt(t, ctx.Depth, 2) ctx = ctx.AddField("another") test.EqualStr(t, ctx.Path.String(), "(*DATA.field).another") test.EqualInt(t, ctx.Depth, 3) ctx = ctx.AddCustomLevel("→cust") test.EqualStr(t, ctx.Path.String(), "(*DATA.field).another→cust") test.EqualInt(t, ctx.Depth, 4) ctx = ctxerr.Context{Path: ctxerr.NewPath("DATA")} ctx = ctx.AddArrayIndex(18) test.EqualStr(t, ctx.Path.String(), "DATA[18]") test.EqualInt(t, ctx.Depth, 1) ctx = ctxerr.Context{Path: ctxerr.NewPath("DATA")} ctx = ctx.AddMapKey("foo") test.EqualStr(t, ctx.Path.String(), `DATA["foo"]`) // special case of util.ToString() test.EqualInt(t, ctx.Depth, 1) ctx = ctxerr.Context{Path: ctxerr.NewPath("DATA")} ctx = ctx.AddMapKey(12) test.EqualStr(t, ctx.Path.String(), `DATA[12]`) test.EqualInt(t, ctx.Depth, 1) ctx = ctxerr.Context{Path: ctxerr.NewPath("DATA")} ctx = ctx.AddFunctionCall("foobar") test.EqualStr(t, ctx.Path.String(), "foobar(DATA)") test.EqualInt(t, ctx.Depth, 1) ctx = ctx.ResetPath("NEW") test.EqualStr(t, ctx.Path.String(), "NEW") test.EqualInt(t, ctx.Depth, 2) } golang-github-maxatome-go-testdeep-1.14.0/internal/ctxerr/error.go000066400000000000000000000136511454313311600251650ustar00rootroot00000000000000// Copyright (c) 2018-2021, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package ctxerr import ( "reflect" "strings" "github.com/maxatome/go-testdeep/internal/color" "github.com/maxatome/go-testdeep/internal/location" "github.com/maxatome/go-testdeep/internal/types" "github.com/maxatome/go-testdeep/internal/util" ) // Error represents errors generated by td (go-testdeep) functions. type Error struct { // Context when the error occurred Context Context // Message describes the error Message string // Got value Got any // Expected value Expected any // If not nil, Summary is used to display summary instead of using // Got + Expected fields Summary ErrorSummary // If initialized, location of TestDeep operator originator of the error Location location.Location // If defined, the current Error comes from this Error Origin *Error // If defined, points to the next Error Next *Error } // BooleanError is the [*Error] returned when an error occurs in a // boolean context. var BooleanError = &Error{} // ErrTooManyErrors is chained to the last error encountered when // the maximum number of errors has been reached. var ErrTooManyErrors = &Error{ Message: "Too many errors (use TESTDEEP_MAX_ERRORS=-1 to see all)", } // TypeMismatch returns a "type mismatch" error. It is the caller // responsibility to check that both types differ. // // If they resolve to the same name (via their String method), it // tries to deeply dump the full package name of each type. // // It works pretty well with the exception of identical anomymous // structs in 2 different packages with the same last name: in this // case reflect does not allow us to retrieve the package from which // each type comes. // // package foo // in a/ // var Foo struct { a int } // // package foo // in b/ // var Foo struct { a int } // // package ctxerr // import( // a_foo "a/foo" // b_foo "b/foo" // ) // … // TypeMismatch(reflect.TypeOf(a_foo.Foo), reflect.TypeOf(b_foo.Foo)) // // returns an error producing: // // type mismatch // got: struct { a int } // expected: struct { a int } func TypeMismatch(got, expected reflect.Type) *Error { gs, es := got.String(), expected.String() if gs == es { gs, es = util.TypeFullName(got), util.TypeFullName(expected) } return &Error{ Message: "type mismatch", Got: types.RawString(gs), Expected: types.RawString(es), } } // Error implements error interface. func (e *Error) Error() string { buf := strings.Builder{} e.Append(&buf, "", true) return buf.String() } // ErrorWithoutColors is the same as [Error.Error] but guarantees the // resulting string does not contain any ANSI color escape sequences. func (e *Error) ErrorWithoutColors() string { buf := strings.Builder{} e.Append(&buf, "", false) return buf.String() } // Append appends the a contents to buf using prefix prefix for each // line. func (e *Error) Append(buf *strings.Builder, prefix string, colorized bool) { if e == BooleanError { return } var badOn, badOff, okOn, okOff string if colorized { color.Init() badOn, badOff = color.BadOn, color.BadOff okOn, okOff = color.OKOn, color.OKOff } var writeEolPrefix func() if prefix != "" { eolPrefix := make([]byte, 1+len(prefix)) eolPrefix[0] = '\n' copy(eolPrefix[1:], prefix) writeEolPrefix = func() { buf.Write(eolPrefix) } buf.WriteString(prefix) } else { writeEolPrefix = func() { buf.WriteByte('\n') } } if e == ErrTooManyErrors { if colorized { buf.WriteString(color.TitleOn) } buf.WriteString(e.Message) if colorized { buf.WriteString(color.TitleOff) } return } if colorized { buf.WriteString(color.TitleOn) } if pos := strings.Index(e.Message, "%%"); pos >= 0 { buf.WriteString(e.Message[:pos]) buf.WriteString(e.Context.Path.String()) buf.WriteString(e.Message[pos+2:]) } else { buf.WriteString(e.Context.Path.String()) buf.WriteString(": ") buf.WriteString(e.Message) } if colorized { buf.WriteString(color.TitleOff) } if e.Summary != nil { buf.WriteByte('\n') e.Summary.AppendSummary(buf, prefix+"\t", colorized) } else { writeEolPrefix() if colorized { buf.WriteString(color.BadOnBold) } buf.WriteString("\t got: ") util.IndentColorizeStringIn(buf, e.GotString(), prefix+"\t ", badOn, badOff) writeEolPrefix() if colorized { buf.WriteString(color.OKOnBold) } buf.WriteString("\texpected: ") util.IndentColorizeStringIn(buf, e.ExpectedString(), prefix+"\t ", okOn, okOff) } // This error comes from another one if e.Origin != nil { writeEolPrefix() buf.WriteString("Originates from following error:\n") e.Origin.Append(buf, prefix+"\t", colorized) } if e.Location.IsInitialized() && !e.Location.BehindCmp && // no need to log Cmp* func (e.Next == nil || e.Next.Location != e.Location) { writeEolPrefix() buf.WriteString("[under operator ") buf.WriteString(e.Location.String()) buf.WriteByte(']') } if e.Next != nil { buf.WriteByte('\n') e.Next.Append(buf, prefix, colorized) // next error at same level } } // GotString returns the string corresponding to the Got // field. Returns the empty string if the e Summary field is not nil. func (e *Error) GotString() string { if e.Summary != nil { return "" } return util.ToString(e.Got) } // ExpectedString returns the string corresponding to the Expected // field. Returns the empty string if the e Summary field is not nil. func (e *Error) ExpectedString() string { if e.Summary != nil { return "" } return util.ToString(e.Expected) } // SummaryString returns the string corresponding to the Summary field // without any ANSI color escape sequences. Returns the empty string // if the e Summary field is nil. func (e *Error) SummaryString() string { if e.Summary == nil { return "" } var buf strings.Builder e.Summary.AppendSummary(&buf, "", false) return buf.String() } golang-github-maxatome-go-testdeep-1.14.0/internal/ctxerr/error_test.go000066400000000000000000000152001454313311600262140ustar00rootroot00000000000000// Copyright (c) 2018-2021, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package ctxerr_test import ( "reflect" "testing" "github.com/maxatome/go-testdeep/internal/color" "github.com/maxatome/go-testdeep/internal/ctxerr" "github.com/maxatome/go-testdeep/internal/location" "github.com/maxatome/go-testdeep/internal/test" "github.com/maxatome/go-testdeep/internal/types" ) func TestError(t *testing.T) { defer color.SaveState()() checkWithoutColors := func(err *ctxerr.Error) { t.Helper() test.EqualStr(t, err.ErrorWithoutColors(), err.Error()) defer color.SaveState(true)() test.IsTrue(t, err.ErrorWithoutColors() != err.Error()) } err := ctxerr.Error{ Context: ctxerr.Context{ Path: ctxerr.NewPath("DATA").AddArrayIndex(12).AddField("Field"), }, Message: "error message", Got: 1, Expected: 2, } test.EqualStr(t, err.Error(), `DATA[12].Field: error message got: 1 expected: 2`) checkWithoutColors(&err) test.EqualStr(t, err.GotString(), "1") test.EqualStr(t, err.ExpectedString(), "2") test.EqualStr(t, err.SummaryString(), "") err.Message = "Value of %% differ" test.EqualStr(t, err.Error(), `Value of DATA[12].Field differ got: 1 expected: 2`) checkWithoutColors(&err) err.Message = "Path at end: %%" test.EqualStr(t, err.Error(), `Path at end: DATA[12].Field got: 1 expected: 2`) checkWithoutColors(&err) err.Message = "%% <- the path!" test.EqualStr(t, err.Error(), `DATA[12].Field <- the path! got: 1 expected: 2`) checkWithoutColors(&err) err = ctxerr.Error{ Context: ctxerr.Context{ Path: ctxerr.NewPath("DATA").AddArrayIndex(12).AddField("Field"), }, Message: "error message", Got: 1, Expected: 2, Location: location.Location{ File: "file.go", Func: "Operator", Line: 23, }, } test.EqualStr(t, err.Error(), `DATA[12].Field: error message got: 1 expected: 2 [under operator Operator at file.go:23]`) checkWithoutColors(&err) err = ctxerr.Error{ Context: ctxerr.Context{ Path: ctxerr.NewPath("DATA").AddArrayIndex(12).AddField("Field"), }, Message: "error message", Summary: ctxerr.NewSummary("666"), Location: location.Location{ File: "file.go", Func: "Operator", Line: 23, }, Origin: &ctxerr.Error{ Context: ctxerr.Context{ Path: ctxerr.NewPath("DATA").AddArrayIndex(12).AddField("Field").AddCustomLevel(""), }, Message: "origin error message", Summary: ctxerr.NewSummary("42"), Location: location.Location{ File: "file2.go", Func: "SubOperator", Line: 236, }, }, } test.EqualStr(t, err.Error(), `DATA[12].Field: error message 666 Originates from following error: DATA[12].Field: origin error message 42 [under operator SubOperator at file2.go:236] [under operator Operator at file.go:23]`) checkWithoutColors(&err) test.EqualStr(t, err.GotString(), "") test.EqualStr(t, err.ExpectedString(), "") test.EqualStr(t, err.SummaryString(), "666") err = ctxerr.Error{ Context: ctxerr.Context{ Path: ctxerr.NewPath("DATA").AddArrayIndex(12).AddField("Field"), }, Message: "error message", Summary: ctxerr.NewSummary("666"), Location: location.Location{ File: "file.go", Func: "Operator", Line: 23, }, Origin: &ctxerr.Error{ Context: ctxerr.Context{ Path: ctxerr.NewPath("DATA").AddArrayIndex(12).AddField("Field").AddCustomLevel(""), }, Message: "origin error message", Summary: ctxerr.NewSummary("42"), Location: location.Location{ File: "file2.go", Func: "SubOperator", Line: 236, }, }, // Next error at same location Next: &ctxerr.Error{ Context: ctxerr.Context{ Path: ctxerr.NewPath("DATA").AddArrayIndex(13).AddField("Field"), }, Message: "error message", Summary: ctxerr.NewSummary("888"), Location: location.Location{ File: "file.go", Func: "Operator", Line: 23, }, }, } test.EqualStr(t, err.Error(), `DATA[12].Field: error message 666 Originates from following error: DATA[12].Field: origin error message 42 [under operator SubOperator at file2.go:236] DATA[13].Field: error message 888 [under operator Operator at file.go:23]`) checkWithoutColors(&err) err = ctxerr.Error{ Context: ctxerr.Context{Path: ctxerr.NewPath("DATA").AddArrayIndex(12).AddField("Field")}, Message: "error message", Summary: ctxerr.NewSummary("666"), Location: location.Location{ File: "file.go", Func: "Operator", Line: 23, }, Origin: &ctxerr.Error{ Context: ctxerr.Context{ Path: ctxerr.NewPath("DATA").AddArrayIndex(12).AddField("Field").AddCustomLevel(""), }, Message: "origin error message", Summary: ctxerr.NewSummary("42"), Location: location.Location{ File: "file2.go", Func: "SubOperator", Line: 236, }, }, // Next error at different location Next: &ctxerr.Error{ Context: ctxerr.Context{ Path: ctxerr.NewPath("DATA").AddArrayIndex(13).AddField("Field"), }, Message: "error message", Summary: ctxerr.NewSummary("888"), Location: location.Location{ File: "file.go", Func: "Operator", Line: 24, }, }, } test.EqualStr(t, err.Error(), `DATA[12].Field: error message 666 Originates from following error: DATA[12].Field: origin error message 42 [under operator SubOperator at file2.go:236] [under operator Operator at file.go:23] DATA[13].Field: error message 888 [under operator Operator at file.go:24]`) checkWithoutColors(&err) // // ErrTooManyErrors test.EqualStr(t, ctxerr.ErrTooManyErrors.Error(), `Too many errors (use TESTDEEP_MAX_ERRORS=-1 to see all)`) } func TestTypeMismatch(t *testing.T) { rErr := ctxerr.TypeMismatch(reflect.TypeOf(0), reflect.TypeOf("")) test.EqualStr(t, rErr.Message, "type mismatch") test.EqualStr(t, string(rErr.Got.(types.RawString)), `int`) test.EqualStr(t, string(rErr.Expected.(types.RawString)), `string`) // It is the caller responsibility to check that both types // differ. To ease testing we can pass twice the same type, it is // the same as passing 2 different types but with the same short // name (a/too.Type vs b/foo.Type), util.TypeFullName() is called // for both types. rErr = ctxerr.TypeMismatch(reflect.TypeOf(0), reflect.TypeOf(0)) test.EqualStr(t, rErr.Message, "type mismatch") test.EqualStr(t, string(rErr.Got.(types.RawString)), `int`) test.EqualStr(t, string(rErr.Expected.(types.RawString)), `int`) } func TestBooleanError(t *testing.T) { if ctxerr.BooleanError.Error() != "" { t.Errorf("BooleanError should stringify to empty string, not `%s'", ctxerr.BooleanError.Error()) } } golang-github-maxatome-go-testdeep-1.14.0/internal/ctxerr/op_error.go000066400000000000000000000050701454313311600256570ustar00rootroot00000000000000// Copyright (c) 2021 Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package ctxerr import ( "fmt" "reflect" "strings" "github.com/maxatome/go-testdeep/internal/types" ) // OpBadUsage returns a string to notice the user he passed a bad // parameter to an operator constructor. func OpBadUsage(op, usage string, param any, pos int, kind bool) *Error { var b strings.Builder fmt.Fprintf(&b, "usage: %s%s, but received ", op, usage) if param == nil { b.WriteString("nil") } else { t := reflect.TypeOf(param) if kind && t.String() != t.Kind().String() { fmt.Fprintf(&b, "%s (%s)", t, t.Kind()) } else { b.WriteString(t.String()) } } b.WriteString(" as ") switch pos { case 1: b.WriteString("1st") case 2: b.WriteString("2nd") case 3: b.WriteString("3rd") default: fmt.Fprintf(&b, "%dth", pos) } b.WriteString(" parameter") return &Error{ Message: "bad usage of " + op + " operator", Summary: NewSummary(b.String()), } } // OpTooManyParams returns an [*Error] to notice the user he called a // variadic operator constructor with too many parameters. func OpTooManyParams(op, usage string) *Error { return &Error{ Message: "bad usage of " + op + " operator", Summary: NewSummary("usage: " + op + usage + ", too many parameters"), } } // OpBad returns an [*Error] to notice the user a bad operator // constructor usage. If len(args) is > 0, s and args are given to // [fmt.Sprintf]. func OpBad(op, s string, args ...any) *Error { if len(args) > 0 { s = fmt.Sprintf(s, args...) } return &Error{ Message: "bad usage of " + op + " operator", Summary: NewSummary(s), } } // BadKind returns a “bad kind” [*Error], saying got kind does not // match kind(s) listed in okKinds. It is the caller responsibility to // check the kinds compatibility. got can be invalid, in this case it // is displayed as nil. func BadKind(got reflect.Value, okKinds string) *Error { return &Error{ Message: "bad kind", Got: types.RawString(types.KindType(got)), Expected: types.RawString(okKinds), } } // NilPointer returns a “nil pointer” [*Error], saying got value is a // nil pointer instead of what expected lists. It is the caller // responsibility to check got contains a nil pointer. got should not // be invalid. func NilPointer(got reflect.Value, expected string) *Error { return &Error{ Message: "nil pointer", Got: types.RawString("nil " + types.KindType(got)), Expected: types.RawString(expected), } } golang-github-maxatome-go-testdeep-1.14.0/internal/ctxerr/op_error_test.go000066400000000000000000000065341454313311600267240ustar00rootroot00000000000000// Copyright (c) 2021-2022 Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package ctxerr_test import ( "reflect" "testing" "github.com/maxatome/go-testdeep/internal/color" "github.com/maxatome/go-testdeep/internal/ctxerr" "github.com/maxatome/go-testdeep/internal/test" ) const prefix = ": bad usage of Zzz operator\n\t" func TestOpBadUsage(t *testing.T) { defer color.SaveState()() test.EqualStr(t, ctxerr.OpBadUsage("Zzz", "(STRING)", nil, 1, true).Error(), prefix+"usage: Zzz(STRING), but received nil as 1st parameter") test.EqualStr(t, ctxerr.OpBadUsage("Zzz", "(STRING)", 42, 1, true).Error(), prefix+"usage: Zzz(STRING), but received int as 1st parameter") test.EqualStr(t, ctxerr.OpBadUsage("Zzz", "(STRING)", []int{}, 1, true).Error(), prefix+"usage: Zzz(STRING), but received []int (slice) as 1st parameter") test.EqualStr(t, ctxerr.OpBadUsage("Zzz", "(STRING)", []int{}, 1, false).Error(), prefix+"usage: Zzz(STRING), but received []int as 1st parameter") test.EqualStr(t, ctxerr.OpBadUsage("Zzz", "(STRING)", nil, 1, true).Error(), prefix+"usage: Zzz(STRING), but received nil as 1st parameter") test.EqualStr(t, ctxerr.OpBadUsage("Zzz", "(STRING)", nil, 2, true).Error(), prefix+"usage: Zzz(STRING), but received nil as 2nd parameter") test.EqualStr(t, ctxerr.OpBadUsage("Zzz", "(STRING)", nil, 3, true).Error(), prefix+"usage: Zzz(STRING), but received nil as 3rd parameter") test.EqualStr(t, ctxerr.OpBadUsage("Zzz", "(STRING)", nil, 4, true).Error(), prefix+"usage: Zzz(STRING), but received nil as 4th parameter") } func TestOpTooManyParams(t *testing.T) { defer color.SaveState()() test.EqualStr(t, ctxerr.OpTooManyParams("Zzz", "(PARAM)").Error(), prefix+"usage: Zzz(PARAM), too many parameters") } func TestBad(t *testing.T) { defer color.SaveState()() test.EqualStr(t, ctxerr.OpBad("Zzz", "test").Error(), prefix+"test") test.EqualStr(t, ctxerr.OpBad("Zzz", "test %d", 123).Error(), prefix+"test 123") } func TestBadKind(t *testing.T) { defer color.SaveState()() expected := func(got string) string { return ": bad kind\n\t got: " + got + "\n\texpected: some kinds" } test.EqualStr(t, ctxerr.BadKind(reflect.ValueOf(42), "some kinds").Error(), expected("int")) test.EqualStr(t, ctxerr.BadKind(reflect.ValueOf(&[]int{}), "some kinds").Error(), expected("*slice (*[]int type)")) test.EqualStr(t, ctxerr.BadKind(reflect.ValueOf((***int)(nil)), "some kinds").Error(), expected("***int")) test.EqualStr(t, ctxerr.BadKind(reflect.ValueOf(nil), "some kinds").Error(), expected("nil")) } func TestNilPointer(t *testing.T) { defer color.SaveState()() expected := func(got string) string { return ": nil pointer\n\t got: nil " + got + "\n\texpected: non-nil blah blah" } test.EqualStr(t, ctxerr.NilPointer(reflect.ValueOf((*int)(nil)), "non-nil blah blah").Error(), expected("*int")) test.EqualStr(t, ctxerr.NilPointer(reflect.ValueOf((*[]int)(nil)), "non-nil blah blah").Error(), expected("*slice (*[]int type)")) test.EqualStr(t, ctxerr.NilPointer(reflect.ValueOf((***int)(nil)), "non-nil blah blah").Error(), expected("***int")) test.EqualStr(t, ctxerr.NilPointer(reflect.ValueOf(nil), "non-nil blah blah").Error(), expected("nil")) } golang-github-maxatome-go-testdeep-1.14.0/internal/ctxerr/path.go000066400000000000000000000066431454313311600247730ustar00rootroot00000000000000// Copyright (c) 2019, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package ctxerr import ( "strconv" "strings" "github.com/maxatome/go-testdeep/internal/util" ) // Path defines a structure depth path, typically used to mark a // position during a deep traversal in case of error. type Path []pathLevel type pathLevelKind uint8 type pathLevel struct { Content string Pointers int Kind pathLevelKind } const ( levelStruct pathLevelKind = iota levelArray levelMap levelFunc levelCustom ) // NewPath returns a new [Path] initialized with root root node. func NewPath(root string) Path { return Path{ { Kind: levelCustom, Content: root, }, } } // Len returns the number of levels, excluding pointers ones. func (p Path) Len() int { return len(p) } // Equal returns true if p and o are equal, false otherwise. func (p Path) Equal(o Path) bool { if len(p) != len(o) { return false } for i := len(p) - 1; i >= 0; i-- { if p[i] != o[i] { return false } } return true } func (p Path) addLevel(level pathLevel) Path { np := make(Path, len(p), len(p)+1) copy(np, p) return append(np, level) } // Copy returns a new [Path], exact but independent copy of p. func (p Path) Copy() Path { if p == nil { return nil } np := make(Path, len(p)) copy(np, p) return np } // AddField adds a level corresponding to a struct field. func (p Path) AddField(field string) Path { if p == nil { return nil } np := p.addLevel(pathLevel{ Kind: levelStruct, Content: field, }) if len(np) > 1 && np[len(np)-2].Pointers > 0 { np[len(np)-2].Pointers-- } return np } // AddArrayIndex adds a level corresponding to an array index. func (p Path) AddArrayIndex(index int) Path { if p == nil { return nil } return p.addLevel(pathLevel{ Kind: levelArray, Content: strconv.Itoa(index), }) } // AddMapKey adds a level corresponding to a map key. func (p Path) AddMapKey(key any) Path { if p == nil { return nil } return p.addLevel(pathLevel{ Kind: levelMap, Content: util.ToString(key), }) } // AddPtr adds num pointers levels. func (p Path) AddPtr(num int) Path { if p == nil { return nil } np := p.Copy() // Do not check len(np) > 0, as it should np[len(np)-1].Pointers += num return np } // AddFunctionCall adds a level corresponding to a function call. func (p Path) AddFunctionCall(fn string) Path { if p == nil { return nil } return p.addLevel(pathLevel{ Kind: levelFunc, Content: fn, }) } // AddCustomLevel adds a custom level. func (p Path) AddCustomLevel(custom string) Path { if p == nil { return nil } return p.addLevel(pathLevel{ Kind: levelCustom, Content: custom, }) } func (p Path) String() string { if len(p) == 0 { return "" } var str string for i, level := range p { var ptrs string if level.Pointers > 0 { ptrs = strings.Repeat("*", level.Pointers) } if level.Kind == levelFunc { str = ptrs + level.Content + "(" + str + ")" } else { if i > 0 && p[i-1].Pointers > 0 { // Last level contains pointer(s), protect them str = ptrs + "(" + str + ")" } else { str = ptrs + str } switch level.Kind { case levelStruct: str += "." + level.Content case levelArray, levelMap: str += "[" + level.Content + "]" default: str += level.Content } } } return str } golang-github-maxatome-go-testdeep-1.14.0/internal/ctxerr/path_test.go000066400000000000000000000106701454313311600260250ustar00rootroot00000000000000// Copyright (c) 2019, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package ctxerr_test import ( "testing" "github.com/maxatome/go-testdeep/internal/ctxerr" "github.com/maxatome/go-testdeep/internal/test" ) func TestPath(t *testing.T) { for i, testCase := range []struct { Path ctxerr.Path Expected string }{ { Path: ctxerr.Path(nil), Expected: "", }, { Path: ctxerr.Path{}, Expected: "", }, { Path: ctxerr.NewPath("DATA"), Expected: "DATA", }, { Path: ctxerr.NewPath("DATA").AddField("field"), Expected: "DATA.field", }, { Path: ctxerr.NewPath("DATA").AddPtr(1), Expected: "*DATA", }, { Path: ctxerr.NewPath("DATA").AddPtr(2), Expected: "**DATA", }, { Path: ctxerr.NewPath("DATA").AddPtr(1).AddField("field"), Expected: "DATA.field", }, { Path: ctxerr.NewPath("DATA").AddPtr(2).AddField("field"), Expected: "(*DATA).field", }, { Path: ctxerr.NewPath("DATA"). AddPtr(1). AddField("field"). AddArrayIndex(42), Expected: "DATA.field[42]", }, { Path: ctxerr.NewPath("DATA"). AddPtr(1). AddArrayIndex(42), Expected: "(*DATA)[42]", }, { Path: ctxerr.NewPath("DATA"). AddPtr(1). AddField("field"). AddPtr(1). AddArrayIndex(42), Expected: "(*DATA.field)[42]", }, { Path: ctxerr.NewPath("DATA"). AddPtr(1). AddField("field1"). AddPtr(1). AddField("field2"). AddPtr(1). AddArrayIndex(42), Expected: "(*DATA.field1.field2)[42]", }, { Path: ctxerr.NewPath("DATA"). AddPtr(1). AddArrayIndex(42). AddPtr(1), Expected: "*(*DATA)[42]", }, { Path: ctxerr.NewPath("DATA"). AddPtr(1). AddArrayIndex(42). AddPtr(1). AddField("field"), Expected: "(*DATA)[42].field", }, { Path: ctxerr.NewPath("DATA"). AddPtr(1). AddMapKey("key"). AddPtr(1), Expected: `*(*DATA)["key"]`, }, { Path: ctxerr.NewPath("DATA"). AddPtr(1). AddMapKey("key"). AddPtr(1). AddField("field"), Expected: `(*DATA)["key"].field`, }, { Path: ctxerr.NewPath("DATA"). AddPtr(2). AddField("field1"). AddPtr(3). AddField("field2"). AddPtr(1). AddArrayIndex(42), Expected: "(*(**(*DATA).field1).field2)[42]", }, { Path: ctxerr.NewPath("DATA"). AddPtr(2). AddField("field1"). AddPtr(3). AddFunctionCall("FUNC"). AddArrayIndex(42), Expected: "FUNC(***(*DATA).field1)[42]", }, { Path: ctxerr.NewPath("DATA"). AddPtr(2). AddField("field1"). AddPtr(3). AddFunctionCall("FUNC"). AddArrayIndex(42). AddMapKey("key"), Expected: `FUNC(***(*DATA).field1)[42]["key"]`, }, { Path: ctxerr.NewPath("DATA"). AddPtr(2). AddField("field1"). AddPtr(3). AddFunctionCall("FUNC"). AddArrayIndex(42). AddMapKey("key"). AddCustomLevel("→panic"), Expected: `FUNC(***(*DATA).field1)[42]["key"]→panic`, }, { Path: ctxerr.NewPath("DATA"). AddPtr(2). AddField("field1"). AddPtr(3). AddFunctionCall("FUNC"). AddPtr(2). AddArrayIndex(42). AddMapKey("key"). AddCustomLevel("→panic"), Expected: `(**FUNC(***(*DATA).field1))[42]["key"]→panic`, }, } { test.EqualStr(t, testCase.Path.String(), testCase.Expected, "test case #%d", i) } var nilPath ctxerr.Path for i, newPath := range []ctxerr.Path{ nilPath.Copy(), nilPath.AddField("foo"), nilPath.AddArrayIndex(42), nilPath.AddMapKey("bar"), nilPath.AddPtr(12), nilPath.AddFunctionCall("zip"), nilPath.AddCustomLevel("custom"), } { if newPath != nil { t.Errorf("at #%d, got=%p expected=nil", i, newPath) } } } func TestEqual(t *testing.T) { path := ctxerr.NewPath("DATA"). AddPtr(2). AddField("field1") test.EqualInt(t, path.Len(), 2) test.IsTrue(t, path.Equal(ctxerr.NewPath("DATA").AddPtr(1).AddPtr(1).AddField("field1"))) test.IsFalse(t, path.Equal(ctxerr.NewPath("DATA"))) test.IsFalse(t, path.Equal(ctxerr.NewPath("DATA").AddPtr(2).AddField("field2"))) } /* func BenchmarkStringString(b *testing.B) { path := ctxerr.NewPath("DATA"). AddField("field1") b.ResetTimer() for i := 0; i < b.N; i++ { path.String() } } func BenchmarkStringByte(b *testing.B) { path := ctxerr.NewPath("DATA"). AddField("field1") b.ResetTimer() for i := 0; i < b.N; i++ { path.Stringx() } } */ golang-github-maxatome-go-testdeep-1.14.0/internal/ctxerr/summary.go000066400000000000000000000067421454313311600255340ustar00rootroot00000000000000// Copyright (c) 2019, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package ctxerr import ( "strings" "github.com/maxatome/go-testdeep/internal/color" "github.com/maxatome/go-testdeep/internal/util" ) // ErrorSummary is the interface used to render error summaries. See // Error.Summary. type ErrorSummary interface { AppendSummary(buf *strings.Builder, prefix string, colorized bool) } // ErrorSummaryItem implements the [ErrorSummary] interface and allows // to render a labeled value. // // With explanation set: // // Label: value // Explanation // // With an empty explantion: // // Label: value type ErrorSummaryItem struct { Label string Value string Explanation string } var _ ErrorSummary = ErrorSummaryItem{} // AppendSummary implements the [ErrorSummary] interface. func (s ErrorSummaryItem) AppendSummary(buf *strings.Builder, prefix string, colorized bool) { buf.WriteString(prefix) badOn, badOff := "", "" if colorized { color.Init() badOn, badOff = color.BadOn, color.BadOff buf.WriteString(color.BadOnBold) } buf.WriteString(s.Label) buf.WriteString(": ") util.IndentColorizeStringIn(buf, s.Value, prefix+strings.Repeat(" ", len(s.Label)+2), badOn, badOff) if s.Explanation != "" { buf.WriteByte('\n') buf.WriteString(prefix) util.IndentColorizeStringIn(buf, s.Explanation, prefix, badOn, badOff) } } // ErrorSummaryItems implements the [ErrorSummary] interface and // allows to render summaries with several labeled values. For example: // // Missing 6 items: the 6 items... // Extra 2 items: the 2 items... type ErrorSummaryItems []ErrorSummaryItem var _ ErrorSummary = (ErrorSummaryItems)(nil) // AppendSummary implements [ErrorSummary] interface. func (s ErrorSummaryItems) AppendSummary(buf *strings.Builder, prefix string, colorized bool) { maxLen := 0 for _, item := range s { if len(item.Label) > maxLen { maxLen = len(item.Label) } } for idx, item := range s { if idx > 0 { buf.WriteByte('\n') } if len(item.Label) < maxLen { item.Label = strings.Repeat(" ", maxLen-len(item.Label)) + item.Label } item.AppendSummary(buf, prefix, colorized) } } type errorSummaryString string var _ ErrorSummary = errorSummaryString("") func (s errorSummaryString) AppendSummary(buf *strings.Builder, prefix string, colorized bool) { badOn, badOff := "", "" if colorized { color.Init() badOn, badOff = color.BadOn, color.BadOff } buf.WriteString(prefix) util.IndentColorizeStringIn(buf, string(s), prefix, badOn, badOff) } // NewSummary returns an ErrorSummary composed by the simple string s. func NewSummary(s string) ErrorSummary { return errorSummaryString(s) } // NewSummaryReason returns an [ErrorSummary] meaning that the value got // failed for an (optional) reason. // // With a given reason "it is not nil", the generated summary is: // // value: the_got_value // it failed coz: it is not nil // // If reason is empty, the generated summary is: // // value: the_got_value // it failed but didn't say why func NewSummaryReason(got any, reason string) ErrorSummary { if reason == "" { return ErrorSummaryItem{ Label: " value", // keep 2 indent spaces Value: util.ToString(got), Explanation: "it failed but didn't say why", } } return ErrorSummaryItems{ { Label: "value", Value: util.ToString(got), }, { Label: "it failed coz", Value: reason, }, } } golang-github-maxatome-go-testdeep-1.14.0/internal/ctxerr/summary_test.go000066400000000000000000000100651454313311600265640ustar00rootroot00000000000000// Copyright (c) 2019, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package ctxerr_test import ( "strings" "testing" "github.com/maxatome/go-testdeep/internal/color" "github.com/maxatome/go-testdeep/internal/ctxerr" "github.com/maxatome/go-testdeep/internal/test" ) func errorSummaryToString(s ctxerr.ErrorSummary, prefix string, uncolorized bool) string { var buf strings.Builder s.AppendSummary(&buf, prefix, !uncolorized) return buf.String() } func TestErrorSummary(t *testing.T) { defer color.SaveState()() var expectedColorized bool r := func(s string) string { if s[0] == '\n' { s = s[1:] } var repl *strings.Replacer if expectedColorized { repl = strings.NewReplacer( "*", "\x1b[1;31m", // bold red "+", "\x1b[0;31m", // red light "^", "\x1b[0m", // red off "~", "", // just ignore, for vertical alignment purpose ) } else { repl = strings.NewReplacer( "*", "", // bold red "+", "", // red light "^", "", // red off "~", "", // just ignore, for vertical alignment purpose ) } return repl.Replace(s) } testCases := []struct { name string envColorized bool forceUncolorized bool expectedColorized bool }{ { name: "no color via env", envColorized: false, expectedColorized: false, }, { name: "colorized", envColorized: true, expectedColorized: true, }, { name: "colorized, but force uncolorized", envColorized: true, forceUncolorized: true, expectedColorized: false, }, { name: "no color via env and force uncolorized", envColorized: false, forceUncolorized: true, expectedColorized: false, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { expectedColorized = tc.expectedColorized color.SaveState(tc.envColorized) t.Logf("colorized=%t force=%t expected=%t", tc.envColorized, tc.forceUncolorized, tc.expectedColorized) // // errorSummaryString summary := ctxerr.NewSummary("foobar") test.EqualStr(t, errorSummaryToString(summary, "", tc.forceUncolorized), r(`+foobar^`)) test.EqualStr(t, errorSummaryToString(summary, "----", tc.forceUncolorized), r(`----+foobar^`)) summary = ctxerr.NewSummary("foo\nbar") test.EqualStr(t, errorSummaryToString(summary, "----", tc.forceUncolorized), r(` ----+foo^ ----+bar^`)) // // ErrorSummaryItem summary = ctxerr.ErrorSummaryItem{ Label: "the_label", Value: "foo\nbar", } test.EqualStr(t, errorSummaryToString(summary, "----", tc.forceUncolorized), r(` ----*the_label: +foo^ ----~ +bar^`)) summary = ctxerr.ErrorSummaryItem{ Label: "the_label", Value: "foo\nbar", Explanation: "And the\nexplanation...", } test.EqualStr(t, errorSummaryToString(summary, "----", tc.forceUncolorized), r(` ----*the_label: +foo^ ----~ +bar^ ----+And the^ ----+explanation...^`)) // // ErrorSummaryItems summary = ctxerr.ErrorSummaryItems{ { Label: "first label", Value: "foo\nbar", Explanation: "And the\nexplanation...", }, { Label: "2nd label", Value: "zip\nzap", }, { Label: "3rd big label", Value: "666", }, } test.EqualStr(t, errorSummaryToString(summary, "----", tc.forceUncolorized), r(` ----* first label: +foo^ ----~ +bar^ ----+And the^ ----+explanation...^ ----* 2nd label: +zip^ ----~ +zap^ ----*3rd big label: +666^`)) // // NewSummaryReason summary = ctxerr.NewSummaryReason(666, "") test.EqualStr(t, errorSummaryToString(summary, "----", tc.forceUncolorized), r(` ----* value: +666^ ----+it failed but didn't say why^`)) summary = ctxerr.NewSummaryReason(666, "evil number not accepted!") test.EqualStr(t, errorSummaryToString(summary, "----", tc.forceUncolorized), r(` ----* value: +666^ ----*it failed coz: +evil number not accepted!^`)) }) } } golang-github-maxatome-go-testdeep-1.14.0/internal/dark/000077500000000000000000000000001454313311600231115ustar00rootroot00000000000000golang-github-maxatome-go-testdeep-1.14.0/internal/dark/any.go000066400000000000000000000004201454313311600242230ustar00rootroot00000000000000// Copyright (c) 2022, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. //go:build !go1.18 // +build !go1.18 package dark type any = interface{} golang-github-maxatome-go-testdeep-1.14.0/internal/dark/any_test.go000066400000000000000000000004251454313311600252670ustar00rootroot00000000000000// Copyright (c) 2022, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. //go:build !go1.18 // +build !go1.18 package dark_test type any = interface{} golang-github-maxatome-go-testdeep-1.14.0/internal/dark/bypass.go000066400000000000000000000114321454313311600247420ustar00rootroot00000000000000// DO NOT EDIT!!! AUTOMATICALLY COPIED FROM // https://github.com/davecgh/go-spew/blob/master/spew/bypass.go // Copyright (c) 2015-2016 Dave Collins // // Permission to use, copy, modify, and distribute this software for any // purpose with or without fee is hereby granted, provided that the above // copyright notice and this permission notice appear in all copies. // // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. // NOTE: Due to the following build constraints, this file will only be compiled // when the code is not running on Google App Engine, compiled by GopherJS, and // "-tags safe" is not added to the go build command line. The "disableunsafe" // tag is deprecated and thus should not be used. // Go versions prior to 1.4 are disabled because they use a different layout // for interfaces which make the implementation of unsafeReflectValue more complex. //go:build !js && !appengine && !safe && !disableunsafe && go1.4 // +build !js,!appengine,!safe,!disableunsafe,go1.4 package dark import ( "reflect" "unsafe" ) const ( // UnsafeDisabled is a build-time constant which specifies whether or // not access to the unsafe package is available. UnsafeDisabled = false // ptrSize is the size of a pointer on the current arch. ptrSize = unsafe.Sizeof((*byte)(nil)) ) type flag uintptr var ( // flagRO indicates whether the value field of a reflect.Value // is read-only. flagRO flag // flagAddr indicates whether the address of the reflect.Value's // value may be taken. flagAddr flag ) // flagKindMask holds the bits that make up the kind // part of the flags field. In all the supported versions, // it is in the lower 5 bits. const flagKindMask = flag(0x1f) // Different versions of Go have used different // bit layouts for the flags type. This table // records the known combinations. var okFlags = []struct { ro, addr flag }{{ // From Go 1.4 to 1.5 ro: 1 << 5, addr: 1 << 7, }, { // Up to Go tip. ro: 1<<5 | 1<<6, addr: 1 << 8, }} var flagValOffset = func() uintptr { field, ok := reflect.TypeOf(reflect.Value{}).FieldByName("flag") if !ok { panic("reflect.Value has no flag field") } return field.Offset }() // flagField returns a pointer to the flag field of a reflect.Value. func flagField(v *reflect.Value) *flag { return (*flag)(unsafe.Pointer(uintptr(unsafe.Pointer(v)) + flagValOffset)) } // unsafeReflectValue converts the passed reflect.Value into a one that bypasses // the typical safety restrictions preventing access to unaddressable and // unexported data. It works by digging the raw pointer to the underlying // value out of the protected value and generating a new unprotected (unsafe) // reflect.Value to it. // // This allows us to check for implementations of the Stringer and error // interfaces to be used for pretty printing ordinarily unaddressable and // inaccessible values such as unexported struct fields. func unsafeReflectValue(v reflect.Value) reflect.Value { if !v.IsValid() || (v.CanInterface() && v.CanAddr()) { return v } flagFieldPtr := flagField(&v) *flagFieldPtr &^= flagRO *flagFieldPtr |= flagAddr return v } // Sanity checks against future reflect package changes // to the type or semantics of the Value.flag field. func init() { field, ok := reflect.TypeOf(reflect.Value{}).FieldByName("flag") if !ok { panic("reflect.Value has no flag field") } if field.Type.Kind() != reflect.TypeOf(flag(0)).Kind() { panic("reflect.Value flag field has changed kind") } type t0 int var t struct { A t0 // t0 will have flagEmbedRO set. t0 // a will have flagStickyRO set a t0 } vA := reflect.ValueOf(t).FieldByName("A") va := reflect.ValueOf(t).FieldByName("a") vt0 := reflect.ValueOf(t).FieldByName("t0") // Infer flagRO from the difference between the flags // for the (otherwise identical) fields in t. flagPublic := *flagField(&vA) flagWithRO := *flagField(&va) | *flagField(&vt0) flagRO = flagPublic ^ flagWithRO // Infer flagAddr from the difference between a value // taken from a pointer and not. vPtrA := reflect.ValueOf(&t).Elem().FieldByName("A") flagNoPtr := *flagField(&vA) flagPtr := *flagField(&vPtrA) flagAddr = flagNoPtr ^ flagPtr // Check that the inferred flags tally with one of the known versions. for _, f := range okFlags { if flagRO == f.ro && flagAddr == f.addr { return } } panic("reflect.Value read-only flag has changed semantics") } golang-github-maxatome-go-testdeep-1.14.0/internal/dark/bypasssafe.go000066400000000000000000000035751454313311600256120ustar00rootroot00000000000000// DO NOT EDIT!!! AUTOMATICALLY COPIED FROM // https://github.com/davecgh/go-spew/blob/master/spew/bypasssafe.go // Copyright (c) 2015-2016 Dave Collins // // Permission to use, copy, modify, and distribute this software for any // purpose with or without fee is hereby granted, provided that the above // copyright notice and this permission notice appear in all copies. // // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. // NOTE: Due to the following build constraints, this file will only be compiled // when the code is running on Google App Engine, compiled by GopherJS, or // "-tags safe" is added to the go build command line. The "disableunsafe" // tag is deprecated and thus should not be used. //go:build js || appengine || safe || disableunsafe || !go1.4 // +build js appengine safe disableunsafe !go1.4 package dark import "reflect" const ( // UnsafeDisabled is a build-time constant which specifies whether or // not access to the unsafe package is available. UnsafeDisabled = true ) // unsafeReflectValue typically converts the passed reflect.Value into a one // that bypasses the typical safety restrictions preventing access to // unaddressable and unexported data. However, doing this relies on access to // the unsafe package. This is a stub version which simply returns the passed // reflect.Value when the unsafe package is not available. func unsafeReflectValue(v reflect.Value) reflect.Value { return v } golang-github-maxatome-go-testdeep-1.14.0/internal/dark/copy.go000066400000000000000000000076501454313311600244220ustar00rootroot00000000000000// Copyright (c) 2018, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package dark import ( "reflect" "unicode" "unicode/utf8" "github.com/maxatome/go-testdeep/helpers/tdutil" ) // CopyValue does its best to copy val in a new [reflect.Value] instance. func CopyValue(val reflect.Value) (reflect.Value, bool) { if val.Kind() == reflect.Ptr { if val.IsNil() { newPtrVal := reflect.New(val.Type()) return newPtrVal.Elem(), true } refVal, ok := CopyValue(val.Elem()) if !ok { return reflect.Value{}, false } newPtrVal := reflect.New(refVal.Type()) newPtrVal.Elem().Set(refVal) return newPtrVal, true } var newVal reflect.Value switch val.Kind() { case reflect.Bool: newPtrVal := reflect.New(val.Type()) newVal = newPtrVal.Elem() newVal.SetBool(val.Bool()) case reflect.Complex64, reflect.Complex128: newPtrVal := reflect.New(val.Type()) newVal = newPtrVal.Elem() newVal.SetComplex(val.Complex()) case reflect.Float32, reflect.Float64: newPtrVal := reflect.New(val.Type()) newVal = newPtrVal.Elem() newVal.SetFloat(val.Float()) case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: newPtrVal := reflect.New(val.Type()) newVal = newPtrVal.Elem() newVal.SetInt(val.Int()) case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: newPtrVal := reflect.New(val.Type()) newVal = newPtrVal.Elem() newVal.SetUint(val.Uint()) case reflect.Array: newPtrVal := reflect.New(val.Type()) newVal = newPtrVal.Elem() var ( item reflect.Value ok bool ) for i := val.Len() - 1; i >= 0; i-- { item, ok = CopyValue(val.Index(i)) if !ok { return reflect.Value{}, false } newVal.Index(i).Set(item) } case reflect.Slice: if val.IsNil() { newPtrVal := reflect.New(val.Type()) return newPtrVal.Elem(), true } newVal = reflect.MakeSlice(val.Type(), val.Len(), val.Cap()) var ( item reflect.Value ok bool ) for i := val.Len() - 1; i >= 0; i-- { item, ok = CopyValue(val.Index(i)) if !ok { return reflect.Value{}, false } newVal.Index(i).Set(item) } case reflect.Map: if val.IsNil() { newPtrVal := reflect.New(val.Type()) return newPtrVal.Elem(), true } newVal = reflect.MakeMapWithSize(val.Type(), val.Len()) var ( key, value reflect.Value ok bool ) if !tdutil.MapEach(val, func(k, v reflect.Value) bool { key, ok = CopyValue(k) if !ok { return false } value, ok = CopyValue(v) if !ok { return false } newVal.SetMapIndex(key, value) return true }) { return reflect.Value{}, false } case reflect.Interface: if val.IsNil() { newPtrVal := reflect.New(val.Type()) return newPtrVal.Elem(), true } newPtrVal := reflect.New(val.Type()) newVal = newPtrVal.Elem() refVal, ok := CopyValue(val.Elem()) if !ok { return reflect.Value{}, false } newVal.Set(refVal) case reflect.String: newPtrVal := reflect.New(val.Type()) newVal = newPtrVal.Elem() newVal.SetString(val.String()) case reflect.Struct: // First, check if all fields are public sType := val.Type() for i, n := 0, val.NumField(); i < n; i++ { r, _ := utf8.DecodeRuneInString(sType.Field(i).Name) if !unicode.IsUpper(r) { return reflect.Value{}, false } } // OK all fields are public newPtrVal := reflect.New(sType) newVal = newPtrVal.Elem() var ( fieldIdx []int fieldVal reflect.Value ok bool ) for i, n := 0, val.NumField(); i < n; i++ { fieldIdx = sType.Field(i).Index fieldVal, ok = CopyValue(val.FieldByIndex(fieldIdx)) if !ok { return reflect.Value{}, false // Should not happen as already checked } newVal.FieldByIndex(fieldIdx).Set(fieldVal) } // Does not handle Chan, Func and UnsafePointer default: return reflect.Value{}, false } return newVal, true } golang-github-maxatome-go-testdeep-1.14.0/internal/dark/copy_test.go000066400000000000000000000073411454313311600254560ustar00rootroot00000000000000// Copyright (c) 2018-2022, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package dark_test import ( "reflect" "testing" "github.com/maxatome/go-testdeep/internal/dark" "github.com/maxatome/go-testdeep/internal/test" ) func checkFieldValueOK(t *testing.T, s reflect.Value, fieldName string, value any, ) { t.Helper() testName := "field " + fieldName fieldOrig := s.FieldByName(fieldName) test.IsFalse(t, fieldOrig.CanInterface(), testName+" + fieldOrig.CanInterface()") fieldCopy, ok := dark.CopyValue(fieldOrig) if test.IsTrue(t, ok, "Can copy "+testName) { if test.IsTrue(t, fieldCopy.CanInterface(), testName+" + fieldCopy.CanInterface()") { test.IsTrue(t, reflect.DeepEqual(fieldCopy.Interface(), value), testName+" + fieldCopy contents") } } } func checkFieldValueNOK(t *testing.T, s reflect.Value, fieldName string) { t.Helper() testName := "field " + fieldName fieldOrig := s.FieldByName(fieldName) test.IsFalse(t, fieldOrig.CanInterface(), testName+" + fieldOrig.CanInterface()") _, ok := dark.CopyValue(fieldOrig) test.IsFalse(t, ok, "Could not copy "+testName) } func TestCopyValue(t *testing.T) { // Note that even if all the fields are public, a Struct cannot be copied type SubPublic struct { Public int } type SubPrivate struct { private int //nolint: unused,megacheck,staticcheck } type Private struct { boolean bool integer int uinteger uint cplx complex128 flt float64 str string array [3]any slice []any hash map[any]any pint *int iface any fn func() } // // Copy OK num := 123 private := Private{ boolean: true, integer: 42, cplx: complex(2, -2), flt: 1.234, str: "foobar", array: [3]any{1, 2, SubPublic{Public: 3}}, slice: append(make([]any, 0, 10), 4, 5, SubPublic{Public: 6}), hash: map[any]any{ "foo": &SubPublic{Public: 34}, SubPublic{Public: 78}: 42, }, pint: &num, iface: &num, } privateStruct := reflect.ValueOf(private) checkFieldValueOK(t, privateStruct, "boolean", private.boolean) checkFieldValueOK(t, privateStruct, "integer", private.integer) checkFieldValueOK(t, privateStruct, "uinteger", private.uinteger) checkFieldValueOK(t, privateStruct, "cplx", private.cplx) checkFieldValueOK(t, privateStruct, "flt", private.flt) checkFieldValueOK(t, privateStruct, "str", private.str) checkFieldValueOK(t, privateStruct, "array", private.array) checkFieldValueOK(t, privateStruct, "slice", private.slice) checkFieldValueOK(t, privateStruct, "hash", private.hash) checkFieldValueOK(t, privateStruct, "pint", private.pint) checkFieldValueOK(t, privateStruct, "iface", private.iface) // // Not able to copy... private = Private{ array: [3]any{1, 2, SubPrivate{}}, slice: append(make([]any, 0, 10), &SubPrivate{}, &SubPrivate{}), hash: map[any]any{"foo": &SubPrivate{}}, iface: &SubPrivate{}, fn: func() {}, } privateStruct = reflect.ValueOf(private) checkFieldValueNOK(t, privateStruct, "array") checkFieldValueNOK(t, privateStruct, "slice") checkFieldValueNOK(t, privateStruct, "hash") checkFieldValueNOK(t, privateStruct, "iface") checkFieldValueNOK(t, privateStruct, "fn") private.hash = map[any]any{SubPrivate{}: 123} privateStruct = reflect.ValueOf(private) checkFieldValueNOK(t, privateStruct, "hash") // // nil cases private = Private{} privateStruct = reflect.ValueOf(private) checkFieldValueOK(t, privateStruct, "slice", private.slice) checkFieldValueOK(t, privateStruct, "hash", private.hash) checkFieldValueOK(t, privateStruct, "pint", private.pint) checkFieldValueOK(t, privateStruct, "iface", private.iface) } golang-github-maxatome-go-testdeep-1.14.0/internal/dark/interface.go000066400000000000000000000026361454313311600254070ustar00rootroot00000000000000// Copyright (c) 2018, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package dark import ( "reflect" ) // GetInterface does its best to return the data behind val. If force // is true, it tries to bypass golang protections using the unsafe // package. // // It returns (nil, false) if the data behind val can not be retrieved // as an any interface (aka struct private + non-copyable field). var GetInterface = func(val reflect.Value, force bool) (any, bool) { if !val.IsValid() { return nil, true } if val.CanInterface() { return val.Interface(), true } if force { val = unsafeReflectValue(val) if val.CanInterface() { return val.Interface(), true } } // For some types, we can copy them in new visitable reflect.Value instances copyVal, ok := CopyValue(val) if ok && copyVal.CanInterface() { return copyVal.Interface(), true } // For others, in environments where "unsafe" package is not // available, we cannot go further return nil, false } // MustGetInterface does its best to return the data behind val. If it // fails (struct private + non-copyable field), it panics. func MustGetInterface(val reflect.Value) any { ret, ok := GetInterface(val, true) if ok { return ret } panic("dark.GetInterface() does not handle private " + val.Kind().String() + " kind") } golang-github-maxatome-go-testdeep-1.14.0/internal/dark/interface_test.go000066400000000000000000000037771454313311600264550ustar00rootroot00000000000000// Copyright (c) 2018, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package dark import ( "reflect" "testing" "github.com/maxatome/go-testdeep/internal/test" ) func TestGetInterface(t *testing.T) { type Private struct { private *Private //nolint: megacheck privInt int } // Cases not tested by TestEqualOthers() s := Private{ private: &Private{}, privInt: 42, } // // GetInterface val, ok := GetInterface(reflect.ValueOf(nil), false) if val != nil { test.EqualErrorMessage(t, val, "nil") } test.IsTrue(t, ok) val, ok = GetInterface(reflect.ValueOf(123), false) if test.IsTrue(t, ok) { valInt, ok := val.(int) if test.IsTrue(t, ok) { test.EqualInt(t, valInt, 123) } } _, ok = GetInterface(reflect.ValueOf(s).Field(0), false) test.IsFalse(t, ok, "private field") val, ok = GetInterface(reflect.ValueOf(s).Field(1), false) if test.IsTrue(t, ok, "private field, BUT contents can be copied") { valInt, ok := val.(int) if test.IsTrue(t, ok) { test.EqualInt(t, valInt, 42) } } _, ok = GetInterface(reflect.ValueOf(s).Field(0), true) if UnsafeDisabled { test.IsFalse(t, ok, "unsafe package is disabled, GetInterface should fail") } else { test.IsTrue(t, ok, "unsafe package is available, GetInterface should succeed") } // // MustGetInterface val = MustGetInterface(reflect.ValueOf(123)) valInt, ok := val.(int) if test.IsTrue(t, ok) { test.EqualInt(t, valInt, 123) } if UnsafeDisabled { test.CheckPanic(t, func() { MustGetInterface(reflect.ValueOf(s).Field(0)) }, "dark.GetInterface() does not handle private") } else { val = MustGetInterface(reflect.ValueOf(s).Field(0)) if val == nil { test.EqualErrorMessage(t, val, "non-nil") } } // Private field BUT contents can be copied val = MustGetInterface(reflect.ValueOf(s).Field(1)) valInt, ok = val.(int) if test.IsTrue(t, ok) { test.EqualInt(t, valInt, 42) } } golang-github-maxatome-go-testdeep-1.14.0/internal/flat/000077500000000000000000000000001454313311600231165ustar00rootroot00000000000000golang-github-maxatome-go-testdeep-1.14.0/internal/flat/any.go000066400000000000000000000004201454313311600242300ustar00rootroot00000000000000// Copyright (c) 2022, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. //go:build !go1.18 // +build !go1.18 package flat type any = interface{} golang-github-maxatome-go-testdeep-1.14.0/internal/flat/any_test.go000066400000000000000000000004251454313311600252740ustar00rootroot00000000000000// Copyright (c) 2022, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. //go:build !go1.18 // +build !go1.18 package flat_test type any = interface{} golang-github-maxatome-go-testdeep-1.14.0/internal/flat/slice.go000066400000000000000000000101121454313311600245370ustar00rootroot00000000000000// Copyright (c) 2020, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package flat import ( "reflect" "github.com/maxatome/go-testdeep/helpers/tdutil" ) var sliceType = reflect.TypeOf(Slice{}) // Slice allows to flatten any slice, array or map. type Slice struct { Slice any } // isFlat returns true if no flat.Slice items can be contained in // f.Slice, so this Slice is already flattened. func (f Slice) isFlat() bool { t := reflect.TypeOf(f.Slice).Elem() return t != sliceType && t.Kind() != reflect.Interface } func subLen(v reflect.Value) int { if v.Kind() == reflect.Interface { v = v.Elem() } if s, ok := v.Interface().(Slice); ok { return s.len() } return 1 } func (f Slice) len() int { fv := reflect.ValueOf(f.Slice) l := 0 if fv.Kind() == reflect.Map { if f.isFlat() { return fv.Len() * 2 } tdutil.MapEach(fv, func(k, v reflect.Value) bool { l += 1 + subLen(v) return true }) return l } fvLen := fv.Len() if f.isFlat() { return fvLen } for i := 0; i < fvLen; i++ { l += subLen(fv.Index(i)) } return l } func subAppendValuesTo(sv []reflect.Value, v reflect.Value) []reflect.Value { if v.Kind() == reflect.Interface { v = v.Elem() } if s, ok := v.Interface().(Slice); ok { return s.appendValuesTo(sv) } return append(sv, v) } func (f Slice) appendValuesTo(sv []reflect.Value) []reflect.Value { fv := reflect.ValueOf(f.Slice) if fv.Kind() == reflect.Map { if f.isFlat() { tdutil.MapEach(fv, func(k, v reflect.Value) bool { sv = append(sv, k, v) return true }) return sv } tdutil.MapEach(fv, func(k, v reflect.Value) bool { sv = append(sv, k) sv = subAppendValuesTo(sv, v) return true }) return sv } fvLen := fv.Len() if f.isFlat() { for i := 0; i < fvLen; i++ { sv = append(sv, fv.Index(i)) } return sv } for i := 0; i < fvLen; i++ { sv = subAppendValuesTo(sv, fv.Index(i)) } return sv } func subAppendTo(si []any, v reflect.Value) []any { if v.Kind() == reflect.Interface { v = v.Elem() } i := v.Interface() if s, ok := i.(Slice); ok { return s.appendTo(si) } return append(si, i) } func (f Slice) appendTo(si []any) []any { fv := reflect.ValueOf(f.Slice) if fv.Kind() == reflect.Map { if f.isFlat() { tdutil.MapEach(fv, func(k, v reflect.Value) bool { si = append(si, k.Interface(), v.Interface()) return true }) return si } tdutil.MapEach(fv, func(k, v reflect.Value) bool { si = append(si, k.Interface()) si = subAppendTo(si, v) return true }) return si } fvLen := fv.Len() if f.isFlat() { for i := 0; i < fvLen; i++ { si = append(si, fv.Index(i).Interface()) } return si } for i := 0; i < fvLen; i++ { si = subAppendTo(si, fv.Index(i)) } return si } // Len returns the number of items contained in items. Nested Slice // items are counted as if they are flattened. It returns true if at // least one [Slice] item is found, false otherwise. func Len(items []any) (int, bool) { l := len(items) flattened := true for _, item := range items { if subf, ok := item.(Slice); ok { l += subf.len() - 1 flattened = false } } return l, flattened } // Values returns the items values as a slice of // [reflect.Value]. Nested [Slice] items are flattened. func Values(items []any) []reflect.Value { l, flattened := Len(items) if flattened { sv := make([]reflect.Value, l) for i, item := range items { sv[i] = reflect.ValueOf(item) } return sv } sv := make([]reflect.Value, 0, l) for _, item := range items { if f, ok := item.(Slice); ok { sv = f.appendValuesTo(sv) } else { sv = append(sv, reflect.ValueOf(item)) } } return sv } // Interfaces returns the items values as a slice of // any. Nested [Slice] items are flattened. func Interfaces(items ...any) []any { l, flattened := Len(items) if flattened { return items } si := make([]any, 0, l) for _, item := range items { if f, ok := item.(Slice); ok { si = f.appendTo(si) } else { si = append(si, item) } } return si } golang-github-maxatome-go-testdeep-1.14.0/internal/flat/slice_test.go000066400000000000000000000044201454313311600256030ustar00rootroot00000000000000// Copyright (c) 2020, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package flat_test import ( "testing" "github.com/maxatome/go-testdeep/internal/flat" "github.com/maxatome/go-testdeep/internal/test" ) func TestLen(t *testing.T) { num, flattened := flat.Len([]any{1, 2, 3, 4}) test.EqualInt(t, num, 4) test.IsTrue(t, flattened) num, flattened = flat.Len([]any{ 1, 2, flat.Slice{Slice: []int{3, 4, 5, 6}}, flat.Slice{Slice: map[int]int{-1: -2, -3: -4}}, 7, flat.Slice{ Slice: []any{ flat.Slice{Slice: []int{8, 9}}, flat.Slice{Slice: []int{10, 11}}, flat.Slice{Slice: map[int]any{ -5: -6, -7: flat.Slice{Slice: []int{-8, -9, -10}}, }}, }, }, 12, flat.Slice{Slice: map[any]any{ -11: flat.Slice{Slice: []int{-12, -13}}, }}, }) test.EqualInt(t, num, 12+13) test.IsFalse(t, flattened) } func TestValues(t *testing.T) { sv := flat.Values(nil) test.IsTrue(t, sv != nil) test.EqualInt(t, len(sv), 0) sv = flat.Values([]any{1, 2}) if test.EqualInt(t, len(sv), 2) { test.EqualInt(t, int(sv[0].Int()), 1) test.EqualInt(t, int(sv[1].Int()), 2) } sv = flat.Values([]any{ 1, 2, flat.Slice{Slice: []int{3, 4, 5, 6}}, 7, flat.Slice{ Slice: []any{ flat.Slice{Slice: []int{8, 9}}, flat.Slice{Slice: []any{10, 11}}, 12, 13, }, }, 14, flat.Slice{ Slice: map[int]any{ 15: flat.Slice{Slice: map[int]int{16: 17}}, }, }, }) if test.EqualInt(t, len(sv), 17) { for i, v := range sv { test.EqualInt(t, int(v.Int()), i+1) } } } func TestInterfaces(t *testing.T) { si := flat.Interfaces() test.IsTrue(t, si == nil) si = flat.Interfaces(1, 2) if test.EqualInt(t, len(si), 2) { test.EqualInt(t, si[0].(int), 1) test.EqualInt(t, si[1].(int), 2) } si = flat.Interfaces( 1, 2, flat.Slice{Slice: []int{3, 4, 5, 6}}, 7, flat.Slice{ Slice: []any{ flat.Slice{Slice: []int{8, 9}}, flat.Slice{Slice: []any{10, 11}}, 12, 13, }, }, 14, flat.Slice{ Slice: map[int]any{ 15: flat.Slice{Slice: map[int]int{16: 17}}, }, }, ) if test.EqualInt(t, len(si), 17) { for i, iface := range si { test.EqualInt(t, iface.(int), i+1) } } } golang-github-maxatome-go-testdeep-1.14.0/internal/hooks/000077500000000000000000000000001454313311600233135ustar00rootroot00000000000000golang-github-maxatome-go-testdeep-1.14.0/internal/hooks/any.go000066400000000000000000000004211454313311600244260ustar00rootroot00000000000000// Copyright (c) 2022, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. //go:build !go1.18 // +build !go1.18 package hooks type any = interface{} golang-github-maxatome-go-testdeep-1.14.0/internal/hooks/any_test.go000066400000000000000000000004261454313311600254720ustar00rootroot00000000000000// Copyright (c) 2022, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. //go:build !go1.18 // +build !go1.18 package hooks_test type any = interface{} golang-github-maxatome-go-testdeep-1.14.0/internal/hooks/hooks.go000066400000000000000000000153261454313311600247740ustar00rootroot00000000000000// Copyright (c) 2020, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package hooks import ( "errors" "fmt" "reflect" "sync" "github.com/maxatome/go-testdeep/internal/types" ) type properties struct { cmp reflect.Value smuggle reflect.Value ignoreUnexported bool useEqual bool } // Info gathers all hooks information. type Info struct { sync.Mutex props map[reflect.Type]properties } // NewInfo returns a new instance of *Info. func NewInfo() *Info { return &Info{ props: map[reflect.Type]properties{}, } } var ErrBoolean = errors.New("CmpHook(got, expected) failed") // Copy returns a new instance of [*Info] with the same hooks as i. As // a special case, if i is nil, returned instance is non-nil. func (i *Info) Copy() *Info { ni := NewInfo() if i == nil { return ni } i.Lock() defer i.Unlock() if len(i.props) == 0 { return ni } ni.props = make(map[reflect.Type]properties, len(i.props)) for t, p := range i.props { ni.props[t] = p } return ni } // AddCmpHooks records new Cmp hooks using functions contained in fns. // // Each function in fns has to be a function with the following // possible signatures: // // func (got A, expected A) bool // func (got A, expected A) error // // First arg is always “got”, and second is always “expected”. // // A cannot be an interface. This retriction can be removed in the // future, if really needed. // // It returns an error if an item of fns is not a function or if its // signature does not match the expected ones. func (i *Info) AddCmpHooks(fns []any) error { for n, fn := range fns { vfn := reflect.ValueOf(fn) if vfn.Kind() != reflect.Func { return fmt.Errorf("expects a function, not a %s (@%d)", vfn.Kind(), n) } ft := vfn.Type() if !ft.IsVariadic() && ft.NumIn() == 2 && ft.NumOut() == 1 && ft.In(0) == ft.In(1) && ft.In(0).Kind() != reflect.Interface && (ft.Out(0) == types.Bool || ft.Out(0) == types.Error) { i.Lock() prop := i.props[ft.In(0)] prop.cmp = vfn i.props[ft.In(0)] = prop i.Unlock() continue } return fmt.Errorf("expects: func (T, T) bool|error not %s (@%d)", ft, n) } return nil } // Cmp checks if a Cmp hook exists matching got and expected types. // // If no, it returns (false, nil) // // If yes, it calls it and returns (true, nil) if it succeeds, // (true, ) if it fails. If the hook returns a false bool, the // error returned is [ErrBoolean]. func (i *Info) Cmp(got, expected reflect.Value) (bool, error) { if i == nil { return false, nil } tg := got.Type() i.Lock() prop, ok := i.props[tg] i.Unlock() if !ok || !prop.cmp.IsValid() { return false, nil } if !expected.Type().AssignableTo(prop.cmp.Type().In(1)) { return false, nil } res := prop.cmp.Call([]reflect.Value{got, expected})[0] if res.Kind() == reflect.Bool { if res.Bool() { return true, nil } return true, ErrBoolean } err, _ := res.Interface().(error) return true, err } // AddSmuggleHooks records new Smuggle hooks using functions contained // in fns. // // Each function in fns has to be a function with the following // possible signatures: // // func (got A) B // func (got A) (B, error) // // A cannot be an interface. This retriction can be removed in the // future, if really needed. // // B can be an interface. // // It returns an error if an item of fns is not a function or if its // signature does not match the expected ones. func (i *Info) AddSmuggleHooks(fns []any) error { for n, fn := range fns { vfn := reflect.ValueOf(fn) if vfn.Kind() != reflect.Func { return fmt.Errorf("expects a function, not a %s (@%d)", vfn.Kind(), n) } ft := vfn.Type() if !ft.IsVariadic() && ft.NumIn() == 1 && ft.In(0).Kind() != reflect.Interface && (ft.NumOut() == 1 || (ft.NumOut() == 2 && ft.Out(1) == types.Error)) && ft.Out(0).Kind() != reflect.Interface { i.Lock() prop := i.props[ft.In(0)] prop.smuggle = vfn i.props[ft.In(0)] = prop i.Unlock() continue } return fmt.Errorf("expects: func (A) (B[, error]) not %s (@%d)", ft, n) } return nil } // Smuggle checks if a Smuggle hook exists matching *got type. // // If no, it returns (false, nil) // // If yes, it calls it and returns (true, nil) if it succeeds, // (true, ) if it fails. func (i *Info) Smuggle(got *reflect.Value) (bool, error) { if i == nil { return false, nil } tg := got.Type() i.Lock() prop, ok := i.props[tg] i.Unlock() if !ok || !prop.smuggle.IsValid() { return false, nil } res := prop.smuggle.Call([]reflect.Value{*got}) if len(res) == 2 { if err, _ := res[1].Interface().(error); err != nil { return true, err } } *got = res[0] return true, nil } // AddUseEqual records types of values contained in ts as using // Equal method. ts can also contain [reflect.Type] instances. func (i *Info) AddUseEqual(ts []any) error { if len(ts) == 0 { return nil } for n, typ := range ts { t, ok := typ.(reflect.Type) if !ok { t = reflect.TypeOf(typ) ts[n] = t } equal, ok := t.MethodByName("Equal") if !ok { return fmt.Errorf("expects type %s owns an Equal method (@%d)", t, n) } ft := equal.Type if ft.IsVariadic() || ft.NumIn() != 2 || ft.NumOut() != 1 || !ft.In(0).AssignableTo(ft.In(1)) || ft.Out(0) != types.Bool { return fmt.Errorf("expects type %[1]s Equal method signature be Equal(%[1]s) bool (@%[2]d)", t, n) } } i.Lock() defer i.Unlock() for _, typ := range ts { t := typ.(reflect.Type) prop := i.props[t] prop.useEqual = true i.props[t] = prop } return nil } // UseEqual returns true if the type t needs to use its Equal method // to be compared. func (i *Info) UseEqual(t reflect.Type) bool { if i == nil { return false } i.Lock() defer i.Unlock() return i.props[t].useEqual } // AddIgnoreUnexported records types of values contained in ts as ignoring // unexported struct fields. ts can also contain [reflect.Type] instances. func (i *Info) AddIgnoreUnexported(ts []any) error { if len(ts) == 0 { return nil } for n, typ := range ts { t, ok := typ.(reflect.Type) if !ok { t = reflect.TypeOf(typ) ts[n] = t } if t.Kind() != reflect.Struct { return fmt.Errorf("expects type %s be a struct, not a %s (@%d)", t, t.Kind(), n) } } i.Lock() defer i.Unlock() for _, typ := range ts { t := typ.(reflect.Type) prop := i.props[t] prop.ignoreUnexported = true i.props[t] = prop } return nil } // IgnoreUnexported returns true if the unexported fields of the type // t have to be ignored. func (i *Info) IgnoreUnexported(t reflect.Type) bool { if i == nil { return false } i.Lock() defer i.Unlock() return i.props[t].ignoreUnexported } golang-github-maxatome-go-testdeep-1.14.0/internal/hooks/hooks_test.go000066400000000000000000000244361454313311600260350ustar00rootroot00000000000000// Copyright (c) 2020, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package hooks_test import ( "errors" "net" "reflect" "strconv" "strings" "testing" "time" "github.com/maxatome/go-testdeep/internal/hooks" "github.com/maxatome/go-testdeep/internal/test" ) func TestAddCmpHooks(t *testing.T) { for _, tst := range []struct { name string cmp any err string }{ { name: "not a function", cmp: "zip", err: "expects a function, not a string (@1)", }, { name: "no variadic", cmp: func(a []byte, b ...byte) bool { return true }, err: "expects: func (T, T) bool|error not func([]uint8, ...uint8) bool (@1)", }, { name: "in", cmp: func(a, b, c int) bool { return true }, err: "expects: func (T, T) bool|error not func(int, int, int) bool (@1)", }, { name: "out", cmp: func(a, b int) {}, err: "expects: func (T, T) bool|error not func(int, int) (@1)", }, { name: "type mismatch", cmp: func(a int, b bool) bool { return true }, err: "expects: func (T, T) bool|error not func(int, bool) bool (@1)", }, { name: "interface", cmp: func(a, b any) bool { return true }, err: "expects: func (T, T) bool|error not func(interface {}, interface {}) bool (@1)", }, { name: "bad return", cmp: func(a, b int) int { return 0 }, err: "expects: func (T, T) bool|error not func(int, int) int (@1)", }, } { i := hooks.NewInfo() err := i.AddCmpHooks([]any{ func(a, b bool) bool { return true }, tst.cmp, }) if test.Error(t, err, tst.name) { if !strings.Contains(err.Error(), tst.err) { t.Errorf("<%s> does not contain <%s> for %s", err, tst.err, tst.name) } } } } func TestCmp(t *testing.T) { t.Run("bool", func(t *testing.T) { var i *hooks.Info handled, err := i.Cmp(reflect.ValueOf(12), reflect.ValueOf(12)) test.NoError(t, err) test.IsFalse(t, handled) i = hooks.NewInfo() err = i.AddCmpHooks([]any{func(a, b int) bool { return a == b }}) test.NoError(t, err) handled, err = i.Cmp(reflect.ValueOf(12), reflect.ValueOf(12)) test.NoError(t, err) test.IsTrue(t, handled) handled, err = i.Cmp(reflect.ValueOf(12), reflect.ValueOf(34)) if err != hooks.ErrBoolean { test.EqualErrorMessage(t, err, hooks.ErrBoolean) } test.IsTrue(t, handled) handled, err = i.Cmp(reflect.ValueOf(12), reflect.ValueOf("twelve")) test.NoError(t, err) test.IsFalse(t, handled) handled, err = i.Cmp(reflect.ValueOf("twelve"), reflect.ValueOf("twelve")) test.NoError(t, err) test.IsFalse(t, handled) handled, err = (*hooks.Info)(nil).Cmp(reflect.ValueOf(1), reflect.ValueOf(2)) test.NoError(t, err) test.IsFalse(t, handled) }) t.Run("error", func(t *testing.T) { i := hooks.NewInfo() diffErr := errors.New("a≠b") err := i.AddCmpHooks([]any{ func(a, b int) error { if a == b { return nil } return diffErr }, }) test.NoError(t, err) handled, err := i.Cmp(reflect.ValueOf(12), reflect.ValueOf(12)) test.NoError(t, err) test.IsTrue(t, handled) handled, err = i.Cmp(reflect.ValueOf(12), reflect.ValueOf(34)) if err != diffErr { test.EqualErrorMessage(t, err, diffErr) } test.IsTrue(t, handled) }) } func TestSmuggle(t *testing.T) { var i *hooks.Info got := reflect.ValueOf(123) handled, err := i.Smuggle(&got) test.NoError(t, err) test.IsFalse(t, handled) i = hooks.NewInfo() err = i.AddSmuggleHooks([]any{func(a int) bool { return a != 0 }}) test.NoError(t, err) got = reflect.ValueOf(123) handled, err = i.Smuggle(&got) test.NoError(t, err) test.IsTrue(t, handled) if test.EqualInt(t, int(got.Kind()), int(reflect.Bool)) { test.IsTrue(t, got.Bool()) } got = reflect.ValueOf("biz") handled, err = i.Smuggle(&got) test.NoError(t, err) test.IsFalse(t, handled) test.EqualStr(t, got.String(), "biz") err = i.AddSmuggleHooks([]any{strconv.Atoi}) test.NoError(t, err) got = reflect.ValueOf("123") handled, err = i.Smuggle(&got) test.NoError(t, err) test.IsTrue(t, handled) if test.EqualInt(t, int(got.Kind()), int(reflect.Int)) { test.EqualInt(t, int(got.Int()), 123) } got = reflect.ValueOf("NotANumber") handled, err = i.Smuggle(&got) test.Error(t, err) test.IsTrue(t, handled) } func TestAddSmuggleHooks(t *testing.T) { for _, tst := range []struct { name string smuggle any err string }{ { name: "not a function", smuggle: "zip", err: "expects a function, not a string (@1)", }, { name: "no variadic", smuggle: func(a ...byte) bool { return true }, err: "expects: func (A) (B[, error]) not func(...uint8) bool (@1)", }, { name: "in", smuggle: func(a, b int) bool { return true }, err: "expects: func (A) (B[, error]) not func(int, int) bool (@1)", }, { name: "interface", smuggle: func(a any) bool { return true }, err: "expects: func (A) (B[, error]) not func(interface {}) bool (@1)", }, { name: "out", smuggle: func(a int) {}, err: "expects: func (A) (B[, error]) not func(int) (@1)", }, { name: "bad return", smuggle: func(a int) (int, int) { return 0, 0 }, err: "expects: func (A) (B[, error]) not func(int) (int, int) (@1)", }, { name: "return interface", smuggle: func(a int) any { return 0 }, err: "expects: func (A) (B[, error]) not func(int) interface {} (@1)", }, { name: "return interface, error", smuggle: func(a int) (any, error) { return 0, nil }, err: "expects: func (A) (B[, error]) not func(int) (interface {}, error) (@1)", }, } { i := hooks.NewInfo() err := i.AddSmuggleHooks([]any{ func(a int) bool { return true }, tst.smuggle, }) if test.Error(t, err, tst.name) { if !strings.Contains(err.Error(), tst.err) { t.Errorf("<%s> does not contain <%s> for %s", err, tst.err, tst.name) } } } } func TestUseEqual(t *testing.T) { var i *hooks.Info test.IsFalse(t, i.UseEqual(reflect.TypeOf(42))) i = hooks.NewInfo() test.IsFalse(t, i.UseEqual(reflect.TypeOf(42))) test.NoError(t, i.AddUseEqual([]any{})) test.NoError(t, i.AddUseEqual([]any{time.Time{}, net.IP{}})) test.IsTrue(t, i.UseEqual(reflect.TypeOf(time.Time{}))) test.IsTrue(t, i.UseEqual(reflect.TypeOf(net.IP{}))) } func TestAddUseEqual(t *testing.T) { for _, tst := range []struct { name string typ any err string }{ { name: "no Equal() method", typ: &testing.T{}, err: "expects type *testing.T owns an Equal method (@1)", }, { name: "variadic Equal() method", typ: badEqualVariadic{}, err: "expects type hooks_test.badEqualVariadic Equal method signature be Equal(hooks_test.badEqualVariadic) bool (@1)", }, { name: "bad NumIn Equal() method", typ: badEqualNumIn{}, err: "expects type hooks_test.badEqualNumIn Equal method signature be Equal(hooks_test.badEqualNumIn) bool (@1)", }, { name: "bad NumOut Equal() method", typ: badEqualNumOut{}, err: "expects type hooks_test.badEqualNumOut Equal method signature be Equal(hooks_test.badEqualNumOut) bool (@1)", }, { name: "In(0) not assignable to In(1) Equal() method", typ: badEqualInAssign{}, err: "expects type hooks_test.badEqualInAssign Equal method signature be Equal(hooks_test.badEqualInAssign) bool (@1)", }, { name: "Equal() method don't return bool", typ: badEqualOutType{}, err: "expects type hooks_test.badEqualOutType Equal method signature be Equal(hooks_test.badEqualOutType) bool (@1)", }, } { i := hooks.NewInfo() err := i.AddUseEqual([]any{time.Time{}, tst.typ}) if test.Error(t, err, tst.name) { if !strings.Contains(err.Error(), tst.err) { t.Errorf("<%s> does not contain <%s> for %s", err, tst.err, tst.name) } } } } func TestIgnoreUnexported(t *testing.T) { var i *hooks.Info test.IsFalse(t, i.IgnoreUnexported(reflect.TypeOf(struct{}{}))) i = hooks.NewInfo() test.IsFalse(t, i.IgnoreUnexported(reflect.TypeOf(struct{}{}))) test.NoError(t, i.AddIgnoreUnexported([]any{})) test.NoError(t, i.AddIgnoreUnexported([]any{testing.T{}, time.Time{}})) test.IsTrue(t, i.IgnoreUnexported(reflect.TypeOf(time.Time{}))) test.IsTrue(t, i.IgnoreUnexported(reflect.TypeOf(testing.T{}))) } func TestAddIgnoreUnexported(t *testing.T) { i := hooks.NewInfo() err := i.AddIgnoreUnexported([]any{time.Time{}, 0}) if test.Error(t, err) { test.EqualStr(t, err.Error(), "expects type int be a struct, not a int (@1)") } } func TestCopy(t *testing.T) { var orig *hooks.Info ni := orig.Copy() if ni == nil { t.Errorf("Copy should never return nil, even for a nil instance") } orig = hooks.NewInfo() copy1 := orig.Copy() if copy1 == nil { t.Errorf("Copy should never return nil") } hookedBool := false test.NoError(t, copy1.AddSmuggleHooks([]any{ func(in bool) bool { hookedBool = true; return in }, })) gotBool := reflect.ValueOf(true) // orig instance does not have any hook handled, _ := orig.Smuggle(&gotBool) test.IsFalse(t, hookedBool) test.IsFalse(t, handled) // new bool smuggle hook OK hookedBool = false handled, _ = copy1.Smuggle(&gotBool) test.IsTrue(t, hookedBool) test.IsTrue(t, handled) copy2 := copy1.Copy() if copy2 == nil { t.Errorf("Copy should never return nil") } hookedInt := false test.NoError(t, copy2.AddSmuggleHooks([]any{ func(in int) int { hookedInt = true; return in }, })) // bool smuggle hook inherited from copy1 hookedBool = false handled, _ = copy2.Smuggle(&gotBool) test.IsTrue(t, hookedBool) test.IsTrue(t, handled) gotInt := reflect.ValueOf(123) // new int smuggle hook not available in copy1 instance hookedInt = false handled, _ = copy1.Smuggle(&gotInt) test.IsFalse(t, hookedInt) test.IsFalse(t, handled) // new int smuggle hook OK hookedInt = false handled, _ = copy2.Smuggle(&gotInt) test.IsTrue(t, hookedInt) test.IsTrue(t, handled) test.IsTrue(t, handled) } type badEqualVariadic struct{} func (badEqualVariadic) Equal(a ...badEqualVariadic) bool { return false } type badEqualNumIn struct{} func (badEqualNumIn) Equal(a badEqualNumIn, b badEqualNumIn) bool { return false } type badEqualNumOut struct{} func (badEqualNumOut) Equal(a badEqualNumOut) {} type badEqualInAssign struct{} func (badEqualInAssign) Equal(a int) bool { return false } type badEqualOutType struct{} func (badEqualOutType) Equal(a badEqualOutType) int { return 42 } golang-github-maxatome-go-testdeep-1.14.0/internal/json/000077500000000000000000000000001454313311600231415ustar00rootroot00000000000000golang-github-maxatome-go-testdeep-1.14.0/internal/json/.gitignore000066400000000000000000000000111454313311600251210ustar00rootroot00000000000000y.output golang-github-maxatome-go-testdeep-1.14.0/internal/json/any.go000066400000000000000000000004201454313311600242530ustar00rootroot00000000000000// Copyright (c) 2022, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. //go:build !go1.18 // +build !go1.18 package json type any = interface{} golang-github-maxatome-go-testdeep-1.14.0/internal/json/any_test.go000066400000000000000000000004251454313311600253170ustar00rootroot00000000000000// Copyright (c) 2022, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. //go:build !go1.18 // +build !go1.18 package json_test type any = interface{} golang-github-maxatome-go-testdeep-1.14.0/internal/json/lex.go000066400000000000000000000416401454313311600242650ustar00rootroot00000000000000// Copyright (c) 2020-2022, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package json import ( "bytes" "errors" "fmt" "math/big" "strconv" "strings" "unicode" "unicode/utf8" "github.com/maxatome/go-testdeep/internal/util" ) const delimiters = " \t\r\n,}]()" func init() { yyErrorVerbose = true } type Position struct { bpos int Pos int Line int Col int } func (p Position) incHoriz(bytes int, runes ...int) Position { p.bpos += bytes r := bytes if len(runes) > 0 { r = runes[0] } p.Pos += r p.Col += r return p } func (p Position) String() string { return fmt.Sprintf("at line %d:%d (pos %d)", p.Line, p.Col, p.Pos) } type json struct { buf []byte pos Position lastTokenPos Position stackPos []Position curSize int curRune rune value any errs []*Error opts ParseOpts } type ParseOpts struct { Placeholders []any PlaceholdersByName map[string]any OpFn func(Operator, Position) (any, error) } func Parse(buf []byte, opts ...ParseOpts) (any, error) { j := json{ buf: buf, pos: Position{Line: 1}, } if len(opts) > 0 { j.opts = opts[0] } if !j.parse() { if len(j.errs) == 1 { return nil, j.errs[0] } errStr := bytes.NewBufferString(j.errs[0].Error()) for _, err := range j.errs[1:] { errStr.WriteByte('\n') errStr.WriteString(err.Error()) } return nil, errors.New(errStr.String()) } return j.value, nil } // parse returns true if no errors occurred during parsing. func (j *json) parse() bool { yyParse(j) return len(j.errs) == 0 } // Lex implements yyLexer interface. func (j *json) Lex(lval *yySymType) int { return j.nextToken(lval) } // Error implements yyLexer interface. func (j *json) Error(s string) { if len(j.errs) == 0 || !j.errs[len(j.errs)-1].fatal { const syntaxErrorUnexpected = "syntax error: unexpected " if s == syntaxErrorUnexpected+"$unk" { switch { case unicode.IsPrint(j.curRune): s = syntaxErrorUnexpected + "'" + string(j.curRune) + "'" case j.curRune <= 0xffff: s = fmt.Sprintf(syntaxErrorUnexpected+`'\u%04x'`, j.curRune) default: s = fmt.Sprintf(syntaxErrorUnexpected+`'\U%08x'`, j.curRune) } } else if strings.HasPrefix(s, syntaxErrorUnexpected+"$end") { s = strings.Replace(s, "$end", "EOF", 1) } j.fatal(s, j.lastTokenPos) } } func (j *json) newOperator(name string, params []any) any { if name == "" { return nil // an operator error is in progress } opPos := j.popPos() op, err := j.getOperator(Operator{Name: name, Params: params}, opPos) if err != nil { j.fatal(err.Error(), opPos) return nil } return op } func (j *json) pushPos(pos Position) { j.stackPos = append(j.stackPos, pos) } func (j *json) popPos() Position { last := len(j.stackPos) - 1 pos := j.stackPos[last] j.stackPos = j.stackPos[:last] return pos } func (j *json) moveHoriz(bytes int, runes ...int) { j.pos = j.pos.incHoriz(bytes, runes...) j.curSize = 0 } func (j *json) getOperator(operator Operator, opPos Position) (any, error) { if j.opts.OpFn == nil { return nil, fmt.Errorf("unknown operator %q", operator.Name) } return j.opts.OpFn(operator, opPos) } func (j *json) nextToken(lval *yySymType) int { if !j.skipWs() { return 0 } j.lastTokenPos = j.pos r, _ := j.getRune() switch r { case '"': firstPos := j.pos.incHoriz(1) s, ok := j.parseString() if !ok { return 0 } return j.analyzeStringContent(s, firstPos, lval) case 'r': // raw string, aka r!str! or r (ws possible bw r & start delim) if !j.skipWs() { j.fatal("cannot find r start delimiter") return 0 } firstPos := j.pos.incHoriz(1) s, ok := j.parseRawString() if !ok { return 0 } return j.analyzeStringContent(s, firstPos, lval) case 'n': // null if j.remain() >= 4 && bytes.Equal(j.buf[j.pos.bpos+1:j.pos.bpos+4], []byte(`ull`)) { j.skip(3) lval.value = nil return NULL } case 't': // true if j.remain() >= 4 && bytes.Equal(j.buf[j.pos.bpos+1:j.pos.bpos+4], []byte(`rue`)) { j.skip(3) lval.value = true return TRUE } case 'f': // false if j.remain() >= 5 && bytes.Equal(j.buf[j.pos.bpos+1:j.pos.bpos+5], []byte(`alse`)) { //nolint: misspell j.skip(4) lval.value = false return FALSE } case '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '.': // '+' & '.' are not normally accepted by JSON spec n, ok := j.parseNumber() if !ok { return 0 } lval.value = n return NUMBER case '$': var dollarToken string end := bytes.IndexAny(j.buf[j.pos.bpos+1:], delimiters) if end >= 0 { dollarToken = string(j.buf[j.pos.bpos+1 : j.pos.bpos+1+end]) } else { dollarToken = string(j.buf[j.pos.bpos+1:]) } if dollarToken == "" { return '$' } token, value := j.parseDollarToken(dollarToken, j.pos, false) if token == OPERATOR { lval.string = value.(string) return OPERATOR } lval.value = value j.moveHoriz(1+len(dollarToken), 1+utf8.RuneCountInString(dollarToken)) return token default: if r >= 'A' && r <= 'Z' { operator, ok := j.parseOperator() if !ok { return 0 } j.pushPos(j.lastTokenPos) lval.string = operator return OPERATOR } } return int(r) } func hex(b []byte) (rune, bool) { var r rune for i := 0; i < 4; i++ { r <<= 4 switch { case b[i] >= '0' && b[i] <= '9': r += rune(b[i]) - '0' case b[i] >= 'a' && b[i] <= 'f': r += rune(b[i]) - 'a' + 10 case b[i] >= 'A' && b[i] <= 'F': r += rune(b[i]) - 'A' + 10 default: return 0, false } } return r, true } func (j *json) parseString() (string, bool) { // j.buf[j.pos.bpos] == '"' → caller responsibility var b *strings.Builder from := j.pos.bpos + 1 savePos := j.pos appendBuffer := func(r rune) { if b == nil { b = &strings.Builder{} b.Write(j.buf[from : j.pos.bpos-1]) } b.WriteRune(r) } str: for { r, ok := j.getRune() if !ok { break } switch r { case '"': if b == nil { return string(j.buf[from:j.pos.bpos]), true } return b.String(), true case '\\': r, ok := j.getRune() if !ok { break str } switch r { case '"', '\\', '/': appendBuffer(r) case 'b': appendBuffer('\b') case 'f': appendBuffer('\f') case 'n': appendBuffer('\n') case 'r': appendBuffer('\r') case 't': appendBuffer('\t') case 'u': if j.remain() >= 5 { r, ok = hex(j.buf[j.pos.bpos+1 : j.pos.bpos+5]) if ok { appendBuffer(r) j.pos = j.pos.incHoriz(4) break } } fallthrough default: j.fatal("invalid escape sequence") return "", false } default: //nolint: gocritic if r < ' ' || r > utf8.MaxRune { j.fatal("invalid character in string") return "", false } fallthrough case '\n', '\r', '\t': // not normally accepted by JSON spec if b != nil { b.WriteRune(r) } } } j.fatal("unterminated string", savePos) return "", false } func (j *json) parseRawString() (string, bool) { // j.buf[j.pos.bpos] == first non-ws rune after 'r' → caller responsibility savePos := j.pos startDelim, _ := j.getRune() // cannot fail, caller called j.skipWs() var endDelim rune switch startDelim { case '(': endDelim = ')' case '{': endDelim = '}' case '[': endDelim = ']' case '<': endDelim = '>' default: if startDelim == '_' || (!unicode.IsPunct(startDelim) && !unicode.IsSymbol(startDelim)) { j.fatal(fmt.Sprintf("invalid r delimiter %q, should be either a punctuation or a symbol rune, excluding '_'", startDelim)) return "", false } endDelim = startDelim } from := j.pos.bpos + j.curSize for innerDelim := 0; ; { r, ok := j.getRune() if !ok { break } switch r { case startDelim: if startDelim == endDelim { return string(j.buf[from:j.pos.bpos]), true } innerDelim++ case endDelim: if innerDelim == 0 { return string(j.buf[from:j.pos.bpos]), true } innerDelim-- case '\n', '\r', '\t': // accept these raw bytes default: if r < ' ' || r > utf8.MaxRune { j.fatal("invalid character in raw string") return "", false } } } j.fatal("unterminated raw string", savePos) return "", false } // analyzeStringContent checks whether s contains $ prefix or not. If // yes, it tries to parse it. func (j *json) analyzeStringContent(s string, strPos Position, lval *yySymType) int { if len(s) <= 1 || !strings.HasPrefix(s, "$") { lval.string = s return STRING } // Double $$ at start of strings escape a $ if strings.HasPrefix(s[1:], "$") { lval.string = s[1:] return STRING } // Check for placeholder ($1 or $name) or operator call as $^Empty // or $^Re(q<\d+>) token, value := j.parseDollarToken(s[1:], strPos, true) // in string, j.parseDollarToken can never return an OPERATOR // token. In case an operator is embedded in string, a SUB_PARSER is // returned instead. lval.value = value return token } const ( numInt = 1 << iota numFloat numGoExt ) var numBytes = [...]uint8{ '+': numInt, '-': numInt, '0': numInt, '1': numInt, '2': numInt, '3': numInt, '4': numInt, '5': numInt, '6': numInt, '7': numInt, '8': numInt, '9': numInt, '_': numGoExt, // bases 2, 8, 16 'b': numInt, 'B': numInt, 'o': numInt, 'O': numInt, 'x': numInt, 'X': numInt, 'a': numInt, 'A': numInt, 'c': numInt, 'C': numInt, 'd': numInt, 'D': numInt, 'e': numInt | numFloat, 'E': numInt | numFloat, 'f': numInt, 'F': numInt, // floats '.': numFloat, 'p': numFloat, 'P': numFloat, } func (j *json) parseNumber() (float64, bool) { // j.buf[j.pos.bpos] == '[-+0-9.]' → caller responsibility numKind := numBytes[j.buf[j.pos.bpos]] i := j.pos.bpos + 1 for l := len(j.buf); i < l; i++ { b := int(j.buf[i]) if b >= len(numBytes) || numBytes[b] == 0 { break } numKind |= numBytes[b] } s := string(j.buf[j.pos.bpos:i]) var ( f float64 err error ) // Differentiate float/int parsing to accept old octal notation: // 0600 → 384 as int64, but 600 as float64 if (numKind & numFloat) != 0 { // strconv.ParseFloat does not handle "_" var bf *big.Float bf, _, err = new(big.Float).Parse(s, 0) if err == nil { f, _ = bf.Float64() } } else { // numInt and/or numGoExt var i64 int64 i64, err = strconv.ParseInt(s, 0, 64) if err == nil { f = float64(i64) } } if err != nil { j.fatal("invalid number") return 0, false } j.curSize = 0 j.pos = j.pos.incHoriz(i - j.pos.bpos) return f, true } // parseDollarToken parses a $123 or $tag or $^Operator or // $^Operator(PARAMS…) token. dollarToken is never empty, does not // contain '$' and dollarPos is the '$' position. func (j *json) parseDollarToken(dollarToken string, dollarPos Position, inString bool) (int, any) { firstRune, _ := utf8.DecodeRuneInString(dollarToken) // Test for $123 if firstRune >= '0' && firstRune <= '9' { np, err := strconv.ParseUint(dollarToken, 10, 64) if err != nil { j.error("invalid numeric placeholder", dollarPos) return PLACEHOLDER, nil // continue parsing } if np == 0 { j.error( fmt.Sprintf(`invalid numeric placeholder "$%s", it should start at "$1"`, dollarToken), dollarPos) return PLACEHOLDER, nil // continue parsing } if numParams := len(j.opts.Placeholders); np > uint64(numParams) { switch numParams { case 0: j.error( fmt.Sprintf(`numeric placeholder "$%s", but no params given`, dollarToken), dollarPos) case 1: j.error( fmt.Sprintf(`numeric placeholder "$%s", but only one param given`, dollarToken), dollarPos) default: j.error( fmt.Sprintf(`numeric placeholder "$%s", but only %d params given`, dollarToken, numParams), dollarPos) } return PLACEHOLDER, nil // continue parsing } return PLACEHOLDER, j.opts.Placeholders[np-1] } // Test for operator call $^Operator or $^Operator(…) if firstRune == '^' { nextRune, _ := utf8.DecodeRuneInString(dollarToken[1:]) if nextRune < 'A' || nextRune > 'Z' { j.error(`$^ must be followed by an operator name`, dollarPos) if inString { return SUB_PARSER, nil // continue parsing } return OPERATOR, "" // continue parsing } if inString { jr := json{ buf: []byte(dollarToken[1:]), pos: Position{ Pos: dollarPos.Pos + 2, Line: dollarPos.Line, Col: dollarPos.Col + 2, }, opts: j.opts, } if !jr.parse() { j.errs = append(j.errs, jr.errs...) return SUB_PARSER, nil // continue parsing } return SUB_PARSER, jr.value } j.moveHoriz(2) j.lastTokenPos = j.pos operator, ok := j.parseOperator() if !ok { return OPERATOR, "" } j.pushPos(j.lastTokenPos) return OPERATOR, operator } // Test for $tag err := util.CheckTag(dollarToken) if err != nil { j.error( fmt.Sprintf(`bad placeholder "$%s"`, dollarToken), dollarPos) return PLACEHOLDER, nil // continue parsing } op, ok := j.opts.PlaceholdersByName[dollarToken] if !ok { j.error( fmt.Sprintf(`unknown placeholder "$%s"`, dollarToken), dollarPos) // continue parsing } return PLACEHOLDER, op } func (j *json) parseOperator() (string, bool) { // j.buf[j.pos.bpos] == '[A-Z]' → caller responsibility i := j.pos.bpos + 1 l := len(j.buf) for ; i < l; i++ { if bytes.ContainsAny(j.buf[i:i+1], delimiters) { break } if r := j.buf[i]; (r < 'A' || r > 'Z') && (r < 'a' || r > 'z') { j.fatal(fmt.Sprintf(`invalid operator name %q`, string(j.buf[j.pos.bpos:i+1]))) j.moveHoriz(i - j.pos.bpos) return "", false } } s := string(j.buf[j.pos.bpos:i]) j.moveHoriz(i - j.pos.bpos) return s, true } func (j *json) skipWs() bool { ws: for { r, ok := j.getRune() if !ok { return false } switch r { case ' ', '\n', '\r', '\t': case '/': if j.remain() < 2 { break ws } switch j.buf[j.pos.bpos+1] { case '/': // comment till eol j.curSize = 0 if end := indexAfterEol(j.buf[j.pos.bpos+2:]); end >= 0 { lineLen := 2 + utf8.RuneCount(j.buf[j.pos.bpos+2:j.pos.bpos+2+end]) j.pos.Pos += lineLen j.pos.Line++ j.pos.Col = 0 j.pos.bpos += 2 + end continue ws } lineLen := 2 + utf8.RuneCount(j.buf[j.pos.bpos+2:]) j.pos.Pos += lineLen j.pos.Col += lineLen j.pos.bpos = len(j.buf) // till eof return false case '*': // multi-lines comment j.curSize = 0 if end := bytes.Index(j.buf[j.pos.bpos+2:], []byte("*/")); end >= 0 { comment := j.buf[j.pos.bpos+2 : j.pos.bpos+2+end] commentLen := 4 + utf8.RuneCount(comment) // Count \r\n as only one rune if crnl := bytes.Count(comment, []byte("\r\n")); crnl > 0 { commentLen -= crnl } j.pos.Pos += commentLen j.pos.bpos += 4 + end nLines := countEol(comment) if nLines > 0 { j.pos.Line += nLines j.pos.Col = len(comment) - bytes.LastIndexAny(comment, "\r\n") + 1 } else { j.pos.Col += commentLen } continue ws } j.fatal("multi-lines comment not terminated") return false default: break ws } default: break ws } } j.curSize = 0 return true } // indexAfterEol returns the index of the byte just after the first // instance of an end-of-line ('\n' alone, '\r' alone or "\r\n") in // buf, or -1 if no end-of-line is found. func indexAfterEol(buf []byte) int { // new line for: // - \n alone // - \r\n // - \r alone for i, b := range buf { switch b { case '\n': return i + 1 case '\r': if i+1 == len(buf) || buf[i+1] != '\n' { return i + 1 } return i + 2 } } return -1 } // countEol returns the number of end-of-line ('\n' alone, '\r' alone // or "\r\n") occurrences in buf. func countEol(buf []byte) int { // new line for: // - \n alone // - \r\n // - \r alone num := 0 for { eol := indexAfterEol(buf) if eol < 0 { return num } buf = buf[eol:] num++ } } func (j *json) getRune() (rune, bool) { if j.curSize > 0 { // new line for: // - \n alone // - \r\n (+ consider it as one rune) // - \r alone switch j.buf[j.pos.bpos] { case '\n': if j.pos.bpos > 0 && j.buf[j.pos.bpos-1] == '\r' { // \r\n → already handled break } fallthrough case '\r': j.pos.Line++ j.pos.Col = 0 j.pos.Pos++ default: j.pos.Col++ j.pos.Pos++ } j.pos.bpos += j.curSize j.curSize = 0 } if j.remain() == 0 { return 0, false } r, size := utf8.DecodeRune(j.buf[j.pos.bpos:]) j.curSize = size j.curRune = r return r, true } func (j *json) skip(n int) { j.pos.Pos += n j.pos.Col += n j.pos.bpos += n } func (j *json) remain() int { return len(j.buf) - j.pos.bpos } func (j *json) appendError(mesg string, fatal bool, pos ...Position) { err := Error{ mesg: mesg, Pos: j.pos, fatal: fatal, } if len(pos) > 0 { err.Pos = pos[0] } j.errs = append(j.errs, &err) } func (j *json) error(mesg string, pos ...Position) { j.appendError(mesg, false, pos...) } func (j *json) fatal(mesg string, pos ...Position) { j.appendError(mesg, true, pos...) } type Error struct { mesg string Pos Position fatal bool } func (e *Error) Error() string { return e.mesg + " " + e.Pos.String() } golang-github-maxatome-go-testdeep-1.14.0/internal/json/marshal.go000066400000000000000000000071731454313311600251270ustar00rootroot00000000000000// Copyright (c) 2021, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package json import ( "bytes" ejson "encoding/json" "fmt" "math" "sort" "strconv" "unicode/utf8" ) type marshaler struct { buf *bytes.Buffer indent int tmp []byte } // Marshal returns the JSON encoding of v. It differs from // [encoding/json.Marshal] as it only handles map[string]any, // []any, bool, float64, string, nil and // [encoding/json.Marshaler] values. It also accepts "invalid" JSON data // returned by MarshalJSON method. func Marshal(v any, indent int) ([]byte, error) { m := marshaler{ indent: indent, buf: &bytes.Buffer{}, } err := m.marshal(v) if err != nil { return nil, err } return m.buf.Bytes(), nil } // AppendMarshal does the same as [Marshal] but appends the JSON // encoding to buf. func AppendMarshal(buf *bytes.Buffer, v any, indent int) error { m := marshaler{ indent: indent, buf: buf, } return m.marshal(v) } func (m *marshaler) marshal(v any) error { if v == nil { m.buf.WriteString("null") return nil } switch vt := v.(type) { case map[string]any: if len(vt) == 0 { if vt == nil { m.buf.WriteString("null") } else { m.buf.WriteString("{}") } break } m.indent += 2 keys := make([]string, 0, len(vt)) for k := range vt { keys = append(keys, k) } sort.Strings(keys) beg := "{\n" for _, k := range keys { m.buf.WriteString(beg) beg = ",\n" saveIndent, lenBefore := m.indent, m.buf.Len() fmt.Fprintf(m.buf, `%*s%q: `, m.indent, "", k) m.indent = utf8.RuneCount(m.buf.Bytes()[lenBefore:]) if err := m.marshal(vt[k]); err != nil { return err } m.indent = saveIndent } m.indent -= 2 fmt.Fprintf(m.buf, "\n%*s}", m.indent, "") case []any: if len(vt) == 0 { if vt == nil { m.buf.WriteString("null") } else { m.buf.WriteString("[]") } break } m.indent += 2 beg := "[\n" for _, v := range vt { m.buf.WriteString(beg) beg = ",\n" fmt.Fprintf(m.buf, "%*s", m.indent, "") if err := m.marshal(v); err != nil { return err } } m.indent -= 2 fmt.Fprintf(m.buf, "\n%*s]", m.indent, "") case string: fmt.Fprintf(m.buf, `%q`, vt) case float64: m.marshalFloat64(vt) case bool: if vt { m.buf.WriteString("true") } else { m.buf.WriteString("false") } case ejson.Marshaler: b, err := vt.MarshalJSON() if err != nil { return err } repl := bytes.Repeat([]byte(" "), 1+m.indent) repl[0] = '\n' m.buf.Write(bytes.ReplaceAll(b, []byte("\n"), repl)) default: return fmt.Errorf("Cannot marshal %T", vt) } return nil } func (m *marshaler) marshalFloat64(f float64) { // Contrary to JSON standard we accept to marshal NaN and ±Inf if math.IsInf(f, 0) || math.IsNaN(f) { m.tmp = strconv.AppendFloat(m.tmp[:0], f, 'g', -1, 64) m.buf.Write(m.tmp) return } // Remainder based on encoding/json.floatEncoder.encode() // Convert as if by ES6 number to string conversion. // This matches most other JSON generators. // See golang.org/issue/6384 and golang.org/issue/14135. // Like fmt %g, but the exponent cutoffs are different // and exponents themselves are not padded to two digits. abs := math.Abs(f) fmt := byte('f') if abs != 0 { if abs < 1e-6 || abs >= 1e21 { fmt = 'e' } } m.tmp = strconv.AppendFloat(m.tmp[:0], f, fmt, -1, 64) if fmt == 'e' { // clean up e-09 to e-9 n := len(m.tmp) if n >= 4 && m.tmp[n-4] == 'e' && m.tmp[n-3] == '-' && m.tmp[n-2] == '0' { m.tmp[n-2] = m.tmp[n-1] m.tmp = m.tmp[:n-1] } } m.buf.Write(m.tmp) } golang-github-maxatome-go-testdeep-1.14.0/internal/json/marshal_test.go000066400000000000000000000052321454313311600261600ustar00rootroot00000000000000// Copyright (c) 2021, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package json_test import ( "bytes" "errors" "math" "testing" "github.com/maxatome/go-testdeep/internal/json" "github.com/maxatome/go-testdeep/internal/test" ) type marshalTest bool func (m marshalTest) MarshalJSON() ([]byte, error) { if m { return []byte("marshal\ntest"), nil } return nil, errors.New("marshalling error") } func TestMarshal(t *testing.T) { for i, tst := range []struct { in any expected string }{ { in: float64(123), expected: "123", }, { in: math.NaN(), expected: "NaN", }, { in: math.Inf(1), expected: "+Inf", }, { in: 1e-7, expected: "1e-7", }, { in: 1e22, expected: "1e+22", }, { in: "foobar", expected: `"foobar"`, }, { in: true, expected: `true`, }, { in: false, expected: `false`, }, { in: nil, expected: `null`, }, { in: (map[string]any)(nil), expected: `null`, }, { in: map[string]any{}, expected: `{}`, }, { in: map[string]any{"z": float64(123), "a": float64(890)}, expected: `{ "a": 890, "z": 123 }`, }, { in: map[string]any{ "label": map[string]any{"age": float64(12), "name": "Bob"}, "zip": float64(456), }, expected: `{ "label": { "age": 12, "name": "Bob" }, "zip": 456 }`, }, { in: ([]any)(nil), expected: `null`, }, { in: []any{}, expected: `[]`, }, { in: []any{"a", float64(123)}, expected: `[ "a", 123 ]`, }, { in: marshalTest(true), expected: "marshal\ntest", }, { in: []any{float64(1), marshalTest(true), float64(3)}, expected: `[ 1, marshal test, 3 ]`, }, } { b, err := json.Marshal(tst.in, 0) test.NoError(t, err, "#%d", i) test.EqualStr(t, string(b), tst.expected, "#%d", i) } for i, in := range []any{ marshalTest(false), map[string]any{"z": float64(123), "a": marshalTest(false)}, []any{"a", marshalTest(false)}, } { _, err := json.Marshal(in, 0) if test.Error(t, err, "#%d", i) { test.EqualStr(t, err.Error(), "marshalling error", "#%d", i) } } _, err := json.Marshal(123, 0) if test.Error(t, err) { test.EqualStr(t, err.Error(), "Cannot marshal int") } } func TestAppendMarshal(t *testing.T) { var buf bytes.Buffer buf.WriteString("<<") err := json.AppendMarshal(&buf, "foo", 0) test.NoError(t, err) buf.WriteString(">>") test.EqualStr(t, buf.String(), `<<"foo">>`) } golang-github-maxatome-go-testdeep-1.14.0/internal/json/parser.go000066400000000000000000000273301454313311600247710ustar00rootroot00000000000000// Code generated by goyacc -l -o parser.go parser.y. DO NOT EDIT. // Copyright (c) 2020-2022, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package json import __yyfmt__ "fmt" type Placeholder struct { Num int Name string } type Operator struct { Name string Params []any } type member struct { key string value any } func finalize(l yyLexer, value any) { l.(*json).value = value } type yySymType struct { yys int object map[string]any member member array []any string string value any } const TRUE = 57346 const FALSE = 57347 const NULL = 57348 const NUMBER = 57349 const PLACEHOLDER = 57350 const SUB_PARSER = 57351 const STRING = 57352 const OPERATOR = 57353 var yyToknames = [...]string{ "$end", "error", "$unk", "TRUE", "FALSE", "NULL", "NUMBER", "PLACEHOLDER", "SUB_PARSER", "STRING", "OPERATOR", "'{'", "'}'", "','", "':'", "'['", "']'", "'('", "')'", } var yyStatenames = [...]string{} const yyEofCode = 1 const yyErrCode = 2 const yyInitialStackSize = 16 var yyExca = [...]int{ -1, 1, 1, -1, -2, 0, } const yyPrivate = 57344 const yyLast = 86 var yyAct = [...]int{ 22, 2, 9, 10, 11, 8, 12, 6, 7, 15, 13, 21, 24, 37, 14, 27, 5, 39, 38, 9, 10, 11, 8, 12, 6, 7, 15, 13, 34, 23, 36, 14, 25, 26, 30, 18, 31, 4, 36, 9, 10, 11, 8, 12, 6, 7, 15, 13, 17, 3, 1, 14, 35, 9, 10, 11, 8, 12, 6, 7, 15, 13, 33, 0, 0, 14, 20, 9, 10, 11, 8, 12, 6, 7, 15, 13, 0, 19, 29, 14, 32, 28, 19, 0, 0, 16, } var yyPact = [...]int{ 63, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, -1000, 72, 49, -6, -1000, 19, -1000, 0, -1000, 64, -1000, -1000, 15, -1000, 67, 63, -1000, 35, -1000, -1, -1000, -1000, -1000, -1000, -1000, -2, -1000, -1000, } var yyPgo = [...]int{ 0, 50, 49, 48, 35, 37, 11, 29, 0, 16, } var yyR1 = [...]int{ 0, 1, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 2, 2, 2, 3, 3, 4, 5, 5, 5, 6, 6, 7, 7, 7, 9, 9, } var yyR2 = [...]int{ 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 3, 4, 1, 3, 3, 2, 3, 4, 1, 3, 2, 3, 4, 2, 1, } var yyChk = [...]int{ -1000, -1, -8, -2, -5, -9, 9, 10, 7, 4, 5, 6, 8, 12, 16, 11, 13, -3, -4, 10, 17, -6, -8, -7, 18, 13, 14, 15, 17, 14, 19, -6, 13, -4, -8, 17, -8, 14, 19, 19, } var yyDef = [...]int{ 0, -2, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 0, 0, 27, 12, 0, 15, 0, 18, 0, 21, 26, 0, 13, 0, 0, 19, 0, 23, 0, 14, 16, 17, 20, 22, 0, 24, 25, } var yyTok1 = [...]int{ 1, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 18, 19, 3, 3, 14, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 15, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 16, 3, 17, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 12, 3, 13, } var yyTok2 = [...]int{ 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, } var yyTok3 = [...]int{ 0, } var yyErrorMessages = [...]struct { state int token int msg string }{} /* parser for yacc output */ var ( yyDebug = 0 yyErrorVerbose = false ) type yyLexer interface { Lex(lval *yySymType) int Error(s string) } type yyParser interface { Parse(yyLexer) int Lookahead() int } type yyParserImpl struct { lval yySymType stack [yyInitialStackSize]yySymType char int } func (p *yyParserImpl) Lookahead() int { return p.char } func yyNewParser() yyParser { return &yyParserImpl{} } const yyFlag = -1000 func yyTokname(c int) string { if c >= 1 && c-1 < len(yyToknames) { if yyToknames[c-1] != "" { return yyToknames[c-1] } } return __yyfmt__.Sprintf("tok-%v", c) } func yyStatname(s int) string { if s >= 0 && s < len(yyStatenames) { if yyStatenames[s] != "" { return yyStatenames[s] } } return __yyfmt__.Sprintf("state-%v", s) } func yyErrorMessage(state, lookAhead int) string { const TOKSTART = 4 if !yyErrorVerbose { return "syntax error" } for _, e := range yyErrorMessages { if e.state == state && e.token == lookAhead { return "syntax error: " + e.msg } } res := "syntax error: unexpected " + yyTokname(lookAhead) // To match Bison, suggest at most four expected tokens. expected := make([]int, 0, 4) // Look for shiftable tokens. base := yyPact[state] for tok := TOKSTART; tok-1 < len(yyToknames); tok++ { if n := base + tok; n >= 0 && n < yyLast && yyChk[yyAct[n]] == tok { if len(expected) == cap(expected) { return res } expected = append(expected, tok) } } if yyDef[state] == -2 { i := 0 for yyExca[i] != -1 || yyExca[i+1] != state { i += 2 } // Look for tokens that we accept or reduce. for i += 2; yyExca[i] >= 0; i += 2 { tok := yyExca[i] if tok < TOKSTART || yyExca[i+1] == 0 { continue } if len(expected) == cap(expected) { return res } expected = append(expected, tok) } // If the default action is to accept or reduce, give up. if yyExca[i+1] != 0 { return res } } for i, tok := range expected { if i == 0 { res += ", expecting " } else { res += " or " } res += yyTokname(tok) } return res } func yylex1(lex yyLexer, lval *yySymType) (char, token int) { token = 0 char = lex.Lex(lval) if char <= 0 { token = yyTok1[0] goto out } if char < len(yyTok1) { token = yyTok1[char] goto out } if char >= yyPrivate { if char < yyPrivate+len(yyTok2) { token = yyTok2[char-yyPrivate] goto out } } for i := 0; i < len(yyTok3); i += 2 { token = yyTok3[i+0] if token == char { token = yyTok3[i+1] goto out } } out: if token == 0 { token = yyTok2[1] /* unknown char */ } if yyDebug >= 3 { __yyfmt__.Printf("lex %s(%d)\n", yyTokname(token), uint(char)) } return char, token } func yyParse(yylex yyLexer) int { return yyNewParser().Parse(yylex) } func (yyrcvr *yyParserImpl) Parse(yylex yyLexer) int { var yyn int var yyVAL yySymType var yyDollar []yySymType _ = yyDollar // silence set and not used yyS := yyrcvr.stack[:] Nerrs := 0 /* number of errors */ Errflag := 0 /* error recovery flag */ yystate := 0 yyrcvr.char = -1 yytoken := -1 // yyrcvr.char translated into internal numbering defer func() { // Make sure we report no lookahead when not parsing. yystate = -1 yyrcvr.char = -1 yytoken = -1 }() yyp := -1 goto yystack ret0: return 0 ret1: return 1 yystack: /* put a state and value onto the stack */ if yyDebug >= 4 { __yyfmt__.Printf("char %v in %v\n", yyTokname(yytoken), yyStatname(yystate)) } yyp++ if yyp >= len(yyS) { nyys := make([]yySymType, len(yyS)*2) copy(nyys, yyS) yyS = nyys } yyS[yyp] = yyVAL yyS[yyp].yys = yystate yynewstate: yyn = yyPact[yystate] if yyn <= yyFlag { goto yydefault /* simple state */ } if yyrcvr.char < 0 { yyrcvr.char, yytoken = yylex1(yylex, &yyrcvr.lval) } yyn += yytoken if yyn < 0 || yyn >= yyLast { goto yydefault } yyn = yyAct[yyn] if yyChk[yyn] == yytoken { /* valid shift */ yyrcvr.char = -1 yytoken = -1 yyVAL = yyrcvr.lval yystate = yyn if Errflag > 0 { Errflag-- } goto yystack } yydefault: /* default state action */ yyn = yyDef[yystate] if yyn == -2 { if yyrcvr.char < 0 { yyrcvr.char, yytoken = yylex1(yylex, &yyrcvr.lval) } /* look through exception table */ xi := 0 for { if yyExca[xi+0] == -1 && yyExca[xi+1] == yystate { break } xi += 2 } for xi += 2; ; xi += 2 { yyn = yyExca[xi+0] if yyn < 0 || yyn == yytoken { break } } yyn = yyExca[xi+1] if yyn < 0 { goto ret0 } } if yyn == 0 { /* error ... attempt to resume parsing */ switch Errflag { case 0: /* brand new error */ yylex.Error(yyErrorMessage(yystate, yytoken)) Nerrs++ if yyDebug >= 1 { __yyfmt__.Printf("%s", yyStatname(yystate)) __yyfmt__.Printf(" saw %s\n", yyTokname(yytoken)) } fallthrough case 1, 2: /* incompletely recovered error ... try again */ Errflag = 3 /* find a state where "error" is a legal shift action */ for yyp >= 0 { yyn = yyPact[yyS[yyp].yys] + yyErrCode if yyn >= 0 && yyn < yyLast { yystate = yyAct[yyn] /* simulate a shift of "error" */ if yyChk[yystate] == yyErrCode { goto yystack } } /* the current p has no shift on "error", pop stack */ if yyDebug >= 2 { __yyfmt__.Printf("error recovery pops state %d\n", yyS[yyp].yys) } yyp-- } /* there is no state on the stack with an error shift ... abort */ goto ret1 case 3: /* no shift yet; clobber input char */ if yyDebug >= 2 { __yyfmt__.Printf("error recovery discards %s\n", yyTokname(yytoken)) } if yytoken == yyEofCode { goto ret1 } yyrcvr.char = -1 yytoken = -1 goto yynewstate /* try again in the same state */ } } /* reduction by production yyn */ if yyDebug >= 2 { __yyfmt__.Printf("reduce %v in:\n\t%v\n", yyn, yyStatname(yystate)) } yynt := yyn yypt := yyp _ = yypt // guard against "declared and not used" yyp -= yyR2[yyn] // yyp is now the index of $0. Perform the default action. Iff the // reduced production is ε, $1 is possibly out of range. if yyp+1 >= len(yyS) { nyys := make([]yySymType, len(yyS)*2) copy(nyys, yyS) yyS = nyys } yyVAL = yyS[yyp+1] /* consult goto table to find next state */ yyn = yyR1[yyn] yyg := yyPgo[yyn] yyj := yyg + yyS[yyp].yys + 1 if yyj >= yyLast { yystate = yyAct[yyg] } else { yystate = yyAct[yyj] if yyChk[yystate] != -yyn { yystate = yyAct[yyg] } } // dummy call; replaced with literal code switch yynt { case 1: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.value = yyDollar[1].value finalize(yylex, yyVAL.value) } case 2: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.value = yyDollar[1].object } case 3: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.value = yyDollar[1].array } case 4: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.value = yyDollar[1].value } case 5: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.value = yyDollar[1].value } case 6: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.value = yyDollar[1].string } case 12: yyDollar = yyS[yypt-2 : yypt+1] { yyVAL.object = map[string]any{} } case 13: yyDollar = yyS[yypt-3 : yypt+1] { yyVAL.object = yyDollar[2].object } case 14: yyDollar = yyS[yypt-4 : yypt+1] { yyVAL.object = yyDollar[2].object } case 15: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.object = map[string]any{ yyDollar[1].member.key: yyDollar[1].member.value, } } case 16: yyDollar = yyS[yypt-3 : yypt+1] { yyDollar[1].object[yyDollar[3].member.key] = yyDollar[3].member.value yyVAL.object = yyDollar[1].object } case 17: yyDollar = yyS[yypt-3 : yypt+1] { yyVAL.member = member{ key: yyDollar[1].string, value: yyDollar[3].value, } } case 18: yyDollar = yyS[yypt-2 : yypt+1] { yyVAL.array = []any{} } case 19: yyDollar = yyS[yypt-3 : yypt+1] { yyVAL.array = yyDollar[2].array } case 20: yyDollar = yyS[yypt-4 : yypt+1] { yyVAL.array = yyDollar[2].array } case 21: yyDollar = yyS[yypt-1 : yypt+1] { yyVAL.array = []any{yyDollar[1].value} } case 22: yyDollar = yyS[yypt-3 : yypt+1] { yyVAL.array = append(yyDollar[1].array, yyDollar[3].value) } case 23: yyDollar = yyS[yypt-2 : yypt+1] { yyVAL.array = []any{} } case 24: yyDollar = yyS[yypt-3 : yypt+1] { yyVAL.array = yyDollar[2].array } case 25: yyDollar = yyS[yypt-4 : yypt+1] { yyVAL.array = yyDollar[2].array } case 26: yyDollar = yyS[yypt-2 : yypt+1] { op := yylex.(*json).newOperator(yyDollar[1].string, yyDollar[2].array) if op == nil { return 1 } yyVAL.value = op } case 27: yyDollar = yyS[yypt-1 : yypt+1] { op := yylex.(*json).newOperator(yyDollar[1].string, nil) if op == nil { return 1 } yyVAL.value = op } } goto yystack /* stack new state and value */ } golang-github-maxatome-go-testdeep-1.14.0/internal/json/parser.y000066400000000000000000000057171454313311600246410ustar00rootroot00000000000000%{ // Copyright (c) 2020-2022, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package json type Placeholder struct { Num int Name string } type Operator struct { Name string Params []any } type member struct { key string value any } func finalize(l yyLexer, value any) { l.(*json).value = value } %} %union { object map[string]any member member array []any string string value any } %start json %token TRUE FALSE NULL NUMBER PLACEHOLDER SUB_PARSER %token STRING OPERATOR %type object members %type member %type array elements op_params %type json value operator %% json: value { $$ = $1 finalize(yylex, $$) } value: object { $$ = $1 } | array { $$ = $1 } | operator { $$ = $1 } | SUB_PARSER { $$ = $1 } | STRING { $$ = $1 } | NUMBER | TRUE | FALSE | NULL | PLACEHOLDER ; object: '{' '}' { $$ = map[string]any{} } | '{' members '}' { $$ = $2 } | '{' members ',' '}' // not JSON spec but useful { $$ = $2 } members: member { $$ = map[string]any{ $1.key: $1.value, } } | members ',' member { $1[$3.key] = $3.value $$ = $1 } member: STRING ':' value { $$ = member{ key: $1, value: $3, } } array: '[' ']' { $$ = []any{} } | '[' elements ']' { $$ = $2 } | '[' elements ',' ']' // not JSON spec but useful { $$ = $2 } elements: value { $$ = []any{$1} } | elements ',' value { $$ = append($1, $3) } op_params: '(' ')' { $$ = []any{} } | '(' elements ')' { $$ = $2 } | '(' elements ',' ')' { $$ = $2 } operator: OPERATOR op_params { op := yylex.(*json).newOperator($1, $2) if op == nil { return 1 } $$ = op } | OPERATOR { op := yylex.(*json).newOperator($1, nil) if op == nil { return 1 } $$ = op } %% golang-github-maxatome-go-testdeep-1.14.0/internal/json/parser_test.go000066400000000000000000000446661454313311600260430ustar00rootroot00000000000000// Copyright (c) 2020-2022, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package json_test import ( ejson "encoding/json" "fmt" "reflect" "strings" "testing" "github.com/davecgh/go-spew/spew" "github.com/maxatome/go-testdeep/internal/json" "github.com/maxatome/go-testdeep/internal/test" ) func checkJSON(t *testing.T, gotJSON, expectedJSON string) { t.Helper() var expected any err := ejson.Unmarshal([]byte(expectedJSON), &expected) if err != nil { t.Fatalf("bad JSON: %s", err) } got, err := json.Parse([]byte(gotJSON)) if !test.NoError(t, err, "json.Parse succeeds") { return } if !reflect.DeepEqual(got, expected) { test.EqualErrorMessage(t, strings.TrimRight(spew.Sdump(got), "\n"), strings.TrimRight(spew.Sdump(expected), "\n"), "got matches expected", ) } } func TestJSON(t *testing.T) { t.Run("Basics", func(t *testing.T) { for i, js := range []string{ `true`, ` true `, "\t\nfalse \n ", ` null `, `{}`, `[]`, ` 123.456 `, ` 123.456e4 `, ` 123.456E-4 `, ` -123e-4 `, `0`, `""`, `"123.456$"`, ` "foo bar \" \\ \/ \b \f \n\r \t \u20ac \u10e6 \u10E6 héhô" `, `"\""`, `"\\"`, `"\/"`, `"\b"`, `"\f"`, `"\n"`, `"\r"`, `"\t"`, `"\u20ac"`, `"zz\""`, `"zz\\"`, `"zz\/"`, `"zz\b"`, `"zz\f"`, `"zz\n"`, `"zz\r"`, `"zz\t"`, `"zz\u20ac"`, `["74.99 \u20ac"]`, `{"text": "74.99 \u20ac"}`, `[ 1, 2,3, 4 ]`, `{"foo":{"bar":true},"zip":1234}`, } { js := []byte(js) var expected any err := ejson.Unmarshal(js, &expected) if err != nil { t.Fatalf("#%d, bad JSON: %s", i, err) } got, err := json.Parse(js) if !test.NoError(t, err, "#%d, json.Parse succeeds", i) { continue } if !reflect.DeepEqual(got, expected) { test.EqualErrorMessage(t, strings.TrimRight(spew.Sdump(got), "\n"), strings.TrimRight(spew.Sdump(expected), "\n"), "#%d is OK", i, ) } } }) t.Run("JSON spec infringements", func(t *testing.T) { for _, tc := range []struct{ got, expected string }{ // "," is accepted just before non-empty "}" or "]" {`{"foo": "bar", }`, `{"foo":"bar"}`}, {`{"foo":"bar",}`, `{"foo":"bar"}`}, {`[ 1, 2, 3, ]`, `[1,2,3]`}, {`[ 1,2,3,]`, `[1,2,3]`}, // No need to escape \n, \r & \t {"\"\n\r\t\"", `"\n\r\t"`}, // Extend to golang accepted numbers // as int64 {`+42`, `42`}, {`0600`, `384`}, {`-0600`, `-384`}, {`+0600`, `384`}, {`0xBadFace`, `195951310`}, {`-0xBadFace`, `-195951310`}, {`+0xBadFace`, `195951310`}, // as float64 {`0600.123`, `600.123`}, // float64 can not be an octal number {`0600.`, `600`}, // float64 can not be an octal number {`.25`, `0.25`}, {`+123.`, `123`}, // Extend to golang 1.13 accepted numbers // as int64 {`4_2`, `42`}, {`+4_2`, `42`}, {`-4_2`, `-42`}, {`0b101010`, `42`}, {`-0b101010`, `-42`}, {`+0b101010`, `42`}, {`0b10_1010`, `42`}, {`-0b_10_1010`, `-42`}, {`+0b10_10_10`, `42`}, {`0B101010`, `42`}, {`-0B101010`, `-42`}, {`+0B101010`, `42`}, {`0B10_1010`, `42`}, {`-0B_10_1010`, `-42`}, {`+0B10_10_10`, `42`}, {`0_600`, `384`}, {`-0_600`, `-384`}, {`+0_600`, `384`}, {`0o600`, `384`}, {`0o_600`, `384`}, {`-0o600`, `-384`}, {`-0o6_00`, `-384`}, {`+0o600`, `384`}, {`+0o60_0`, `384`}, {`0O600`, `384`}, {`0O_600`, `384`}, {`-0O600`, `-384`}, {`-0O6_00`, `-384`}, {`+0O600`, `384`}, {`+0O60_0`, `384`}, {`0xBad_Face`, `195951310`}, {`-0x_Bad_Face`, `-195951310`}, {`+0xBad_Face`, `195951310`}, {`0XBad_Face`, `195951310`}, {`-0X_Bad_Face`, `-195951310`}, {`+0XBad_Face`, `195951310`}, // as float64 {`0_600.123`, `600.123`}, // float64 can not be an octal number {`1_5.`, `15`}, {`0.15e+0_2`, `15`}, {`0x1p-2`, `0.25`}, {`0x2.p10`, `2048`}, {`0x1.Fp+0`, `1.9375`}, {`0X.8p-0`, `0.5`}, {`0X_1FFFP-16`, `0.1249847412109375`}, // Raw strings {`r"pipo"`, `"pipo"`}, {`r "pipo"`, `"pipo"`}, {"r\n'pipo'", `"pipo"`}, {`r%pipo%`, `"pipo"`}, {`r·pipo·`, `"pipo"`}, {"r`pipo`", `"pipo"`}, {`r/pipo/`, `"pipo"`}, {"r //comment\n`pipo`", `"pipo"`}, // comments accepted bw r and string {"r//comment\n`pipo`", `"pipo"`}, {"r/*comment\n*/|pipo|", `"pipo"`}, {"r(p\ni\rp\to)", `"p\ni\rp\to"`}, // accepted raw whitespaces {`r@pi\po\@`, `"pi\\po\\"`}, // backslash has no meaning // balanced delimiters {`r(p(i(hey)p)o)`, `"p(i(hey)p)o"`}, {`r{p{i{hey}p}o}`, `"p{i{hey}p}o"`}, {`r[p[i[hey]p]o]`, `"p[i[hey]p]o"`}, {`rp>o>`, `"pp>o"`}, {`r(pipo)`, `"pipo"`}, {"r \t\n(pipo)", `"pipo"`}, {`r{pipo}`, `"pipo"`}, {`r[pipo]`, `"pipo"`}, {`r`, `"pipo"`}, // Not balanced {`r)pipo)`, `"pipo"`}, {`r}pipo}`, `"pipo"`}, {`r]pipo]`, `"pipo"`}, {`r>pipo>`, `"pipo"`}, } { t.Run(tc.got, func(t *testing.T) { checkJSON(t, tc.got, tc.expected) }) } }) t.Run("Special string cases", func(t *testing.T) { for i, tst := range []struct{ in, expected string }{ { in: `"$"`, expected: `$`, }, { in: `"$$"`, expected: `$`, }, { in: `"$$toto"`, expected: `$toto`, }, } { got, err := json.Parse([]byte(tst.in)) if !test.NoError(t, err, "#%d, json.Parse succeeds", i) { continue } if !reflect.DeepEqual(got, tst.expected) { test.EqualErrorMessage(t, strings.TrimRight(spew.Sdump(got), "\n"), strings.TrimRight(spew.Sdump(tst.expected), "\n"), "#%d is OK", i, ) } } }) t.Run("Placeholder cases", func(t *testing.T) { for i, js := range []string{ ` $2 `, ` "$2" `, ` $ph `, ` "$ph" `, ` $héhé `, ` "$héhé" `, } { got, err := json.Parse([]byte(js), json.ParseOpts{ Placeholders: []any{"foo", "bar"}, PlaceholdersByName: map[string]any{ "ph": "bar", "héhé": "bar", }, }) if !test.NoError(t, err, "#%d, json.Parse succeeds", i) { continue } if !reflect.DeepEqual(got, `bar`) { test.EqualErrorMessage(t, strings.TrimRight(spew.Sdump(got), "\n"), strings.TrimRight(spew.Sdump(`bar`), "\n"), "#%d is OK", i, ) } } }) t.Run("Comments", func(t *testing.T) { for i, js := range []string{ " // comment\ntrue", " true // comment\n ", " true // comment\n", " true // comment", " /* comment\nmulti\nline */true", " true /* comment\nmulti\nline */", " true /* comment\nmulti\nline */ \t", " true /* comment\nmulti\nline */ // comment", "/**///\ntrue/**/", } { for j, s := range []string{ js, strings.ReplaceAll(js, "\n", "\r"), strings.ReplaceAll(js, "\n", "\r\n"), } { got, err := json.Parse([]byte(s)) if !test.NoError(t, err, "#%d/%d, json.Parse succeeds", i, j) { continue } if !reflect.DeepEqual(got, true) { test.EqualErrorMessage(t, got, true, "#%d/%d is OK", i, j, ) } } } }) t.Run("OK", func(t *testing.T) { opts := json.ParseOpts{ OpFn: func(op json.Operator, pos json.Position) (any, error) { if op.Name == "KnownOp" { return "OK", nil } return nil, fmt.Errorf("hmm weird operator %q", op.Name) }, } for _, js := range []string{ `[ KnownOp ]`, `[ KnownOp() ]`, `[ $^KnownOp() ]`, `[ $^KnownOp ]`, `[ KnownOp($^KnownOp) ]`, `[ KnownOp( $^KnownOp() ) ]`, `[ $^KnownOp(KnownOp) ]`, } { _, err := json.Parse([]byte(js), opts) test.NoError(t, err, "json.Parse OK", js) } }) t.Run("Reentrant parser", func(t *testing.T) { opts := json.ParseOpts{ OpFn: func(op json.Operator, pos json.Position) (any, error) { if op.Name == "KnownOp" { return "OK", nil } return nil, fmt.Errorf("hmm weird operator %q", op.Name) }, } for _, js := range []string{ `[ "$^KnownOp(1, 2, 3)" ]`, `[ "$^KnownOp(1, 2, 3) " ]`, `[ "$^KnownOp(r<$^KnownOp(11, 12)>, 2, KnownOp(31, 32))" ]`, } { _, err := json.Parse([]byte(js), opts) test.NoError(t, err, "json.Parse OK", js) } }) t.Run("Errors", func(t *testing.T) { for i, tst := range []struct{ nam, js, err string }{ // comment { nam: "unterminated comment", js: " \n /* unterminated", err: "multi-lines comment not terminated at line 2:3 (pos 5)", }, { nam: "/ at EOF", js: " \n /", err: "syntax error: unexpected '/' at line 2:1 (pos 3)", }, { nam: "/toto", js: " \n /toto", err: "syntax error: unexpected '/' at line 2:1 (pos 3)", }, // string { nam: "unterminated string+multi lines", js: "/* multi\nline\ncomment */ \"...", err: "unterminated string at line 3:11 (pos 25)", }, { nam: "unterminated string", js: ` "unterminated\`, err: "unterminated string at line 1:2 (pos 2)", }, { nam: "bad escape", js: `"bad escape \a"`, err: "invalid escape sequence at line 1:13 (pos 13)", }, { nam: `bad escape \u`, js: `"bad échappe \u123t"`, err: "invalid escape sequence at line 1:14 (pos 14)", }, { nam: "bad rune", js: "\"bad rune \007\"", err: "invalid character in string at line 1:10 (pos 10)", }, // number { nam: "bad number", js: " \n 123.345.45", err: "invalid number at line 2:1 (pos 4)", }, // dollar token { nam: "dollar at EOF", js: " $", err: "syntax error: unexpected '$' at line 1:2 (pos 2)", }, { nam: "dollar alone", js: " $ ", err: "syntax error: unexpected '$' at line 1:2 (pos 2)", }, { nam: "multi lines+dollar at EOF", js: " \n 123.345$", err: "syntax error: unexpected '$' at line 2:8 (pos 11)", }, { nam: "bad num placeholder", js: ` $123a `, err: "invalid numeric placeholder at line 1:2 (pos 2)", }, { nam: "bad num placeholder in string", js: ` "$123a" `, err: "invalid numeric placeholder at line 1:3 (pos 3)", }, { nam: "bad 0 placeholder", js: ` $00 `, err: `invalid numeric placeholder "$00", it should start at "$1" at line 1:2 (pos 2)`, }, { nam: "bad 0 placeholder in string", js: ` "$00" `, err: `invalid numeric placeholder "$00", it should start at "$1" at line 1:3 (pos 3)`, }, { nam: "placeholder/params mismatch", js: ` $1 `, err: `numeric placeholder "$1", but no params given at line 1:2 (pos 2)`, }, { nam: "placeholder in string/params mismatch", js: `[ "$1", 1, 2 ] `, err: `numeric placeholder "$1", but no params given at line 1:3 (pos 3)`, }, { nam: "invalid operator in string", js: ` "$^UnknownAndBad>" `, err: `invalid operator name "UnknownAndBad>" at line 1:4 (pos 4)`, }, { nam: "unknown operator close paren", js: ` UnknownAndBad)`, err: `unknown operator "UnknownAndBad" at line 1:1 (pos 1)`, }, { nam: "unknown operator close paren in string", js: ` "$^UnknownAndBad)" `, err: `unknown operator "UnknownAndBad" at line 1:4 (pos 4)`, }, { nam: "op and syntax error", js: ` KnownOp)`, err: `syntax error: unexpected ')' at line 1:8 (pos 8)`, }, { nam: "op in string and syntax error", js: ` "$^KnownOp)" `, err: `syntax error: unexpected ')' at line 1:11 (pos 11)`, }, { nam: "op paren in string and syntax error", js: ` "$^KnownOp())" `, err: `syntax error: unexpected ')' at line 1:13 (pos 13)`, }, { nam: "invalid $^", js: ` $^. `, err: `$^ must be followed by an operator name at line 1:2 (pos 2)`, }, { nam: "invalid $^ in string", js: ` "$^."`, err: `$^ must be followed by an operator name at line 1:2 (pos 2)`, }, { nam: "invalid $^ at EOF", js: ` $^`, err: `$^ must be followed by an operator name at line 1:2 (pos 2)`, }, { nam: "invalid $^ in string at EOF", js: ` "$^"`, err: `$^ must be followed by an operator name at line 1:2 (pos 2)`, }, { nam: "bad placeholder", js: ` $tag%`, err: `bad placeholder "$tag%" at line 1:2 (pos 2)`, }, { nam: "bad placeholder in string", js: ` "$tag%"`, err: `bad placeholder "$tag%" at line 1:3 (pos 3)`, }, { nam: "unknown placeholder", js: ` $tag`, err: `unknown placeholder "$tag" at line 1:2 (pos 2)`, }, { nam: "unknown placeholder in string", js: ` "$tag"`, err: `unknown placeholder "$tag" at line 1:3 (pos 3)`, }, // operator { nam: "invalid operator", js: " AnyOpé", err: `invalid operator name "AnyOp\xc3" at line 1:2 (pos 2)`, }, { nam: "invalid $^operator", js: " $^AnyOpé", err: `invalid operator name "AnyOp\xc3" at line 1:4 (pos 4)`, }, { nam: "invalid $^operator in string", js: ` "$^AnyOpé"`, err: `invalid operator name "AnyOp\xc3" at line 1:5 (pos 5)`, }, { nam: "unknown operator", js: " AnyOp", err: `unknown operator "AnyOp" at line 1:2 (pos 2)`, }, { nam: "unknown operator paren", js: " AnyOp()", err: `unknown operator "AnyOp" at line 1:2 (pos 2)`, }, { nam: "unknown $^operator", js: "$^AnyOp", err: `unknown operator "AnyOp" at line 1:2 (pos 2)`, }, { nam: "unknown $^operator paren", js: "$^AnyOp()", err: `unknown operator "AnyOp" at line 1:2 (pos 2)`, }, { nam: "unknown $^operator in string", js: `"$^AnyOp"`, err: `unknown operator "AnyOp" at line 1:3 (pos 3)`, }, { nam: "unknown $^operator paren in string", js: `"$^AnyOp()"`, err: `unknown operator "AnyOp" at line 1:3 (pos 3)`, }, { nam: "unknown $^operator in rawstring", js: `r<$^AnyOp>`, err: `unknown operator "AnyOp" at line 1:4 (pos 4)`, }, { nam: "unknown $^operator paren in rawstring", js: `r<$^AnyOp()>`, err: `unknown operator "AnyOp" at line 1:4 (pos 4)`, }, // syntax error { nam: "syntax error num+bool", js: " \n 123.345true", err: "syntax error: unexpected TRUE at line 2:8 (pos 11)", }, { nam: "syntax error num+%", js: " \n 123.345%", err: "syntax error: unexpected '%' at line 2:8 (pos 11)", }, { nam: "syntax error num+ESC", js: " \n 123.345\x1f", err: `syntax error: unexpected '\u001f' at line 2:8 (pos 11)`, }, { nam: "syntax error num+unicode", js: " \n 123.345\U0002f500", err: `syntax error: unexpected '\U0002f500' at line 2:8 (pos 11)`, }, // multiple errors { nam: "multi errors placeholders", js: "[$1,$2,", err: `numeric placeholder "$1", but no params given at line 1:1 (pos 1) numeric placeholder "$2", but no params given at line 1:4 (pos 4) syntax error: unexpected EOF at line 1:6 (pos 6)`, }, { nam: "multi errors placeholder+operator", js: `[$1,"$^Unknown1()","$^Unknown2()"]`, err: `numeric placeholder "$1", but no params given at line 1:1 (pos 1) invalid operator name "Unknown1" at line 1:7 (pos 7) invalid operator name "Unknown2" at line 1:22 (pos 22)`, }, // raw strings { nam: "rawstring start delimiter", js: " \n r ", err: `cannot find r start delimiter at line 2:7 (pos 10)`, }, { nam: "rawstring start delimiter EOF", js: " \n r", err: `cannot find r start delimiter at line 2:4 (pos 7)`, }, { nam: "rawstring bad delimiter", js: ` rxpipox`, err: `invalid r delimiter 'x', should be either a punctuation or a symbol rune, excluding '_' at line 1:3 (pos 3)`, }, { nam: "rawstring bad underscore delimiter", js: ` r_pipo_`, err: `invalid r delimiter '_', should be either a punctuation or a symbol rune, excluding '_' at line 1:3 (pos 3)`, }, { nam: "rawstring bad rune", js: " r:bad rune \007:", err: `invalid character in raw string at line 1:13 (pos 13)`, }, { nam: "unterminated rawstring", js: ` r!pipo...`, err: `unterminated raw string at line 1:3 (pos 3)`, }, } { t.Run(tst.nam, func(t *testing.T) { opts := json.ParseOpts{ OpFn: func(op json.Operator, pos json.Position) (any, error) { if op.Name == "KnownOp" { return "OK", nil } return nil, fmt.Errorf("unknown operator %q", op.Name) }, } _, err := json.Parse([]byte(tst.js), opts) if test.Error(t, err, `#%d \n, json.Parse fails`, i) { test.EqualStr(t, err.Error(), tst.err, `#%d \n, err OK`, i) } _, err = json.Parse([]byte(strings.ReplaceAll(tst.js, "\n", "\r")), opts) if test.Error(t, err, `#%d \r, json.Parse fails`, i) { test.EqualStr(t, err.Error(), tst.err, `#%d \r, err OK`, i) } _, err = json.Parse([]byte(strings.ReplaceAll(tst.js, "\n", "\r\n")), opts) if test.Error(t, err, `#%d \r\n, json.Parse fails`, i) { test.EqualStr(t, err.Error(), tst.err, `#%d \r\n, err OK`, i) } }) } _, err := json.Parse( []byte(`[$2]`), json.ParseOpts{Placeholders: []any{1}}, ) if test.Error(t, err) { test.EqualStr(t, err.Error(), `numeric placeholder "$2", but only one param given at line 1:1 (pos 1)`) } _, err = json.Parse( []byte(`[$3]`), json.ParseOpts{Placeholders: []any{1, 2}}, ) if test.Error(t, err) { test.EqualStr(t, err.Error(), `numeric placeholder "$3", but only 2 params given at line 1:1 (pos 1)`) } for _, js := range []string{ ` KnownOp( AnyOp() )`, ` KnownOp( AnyOp )`, ` KnownOp("$^AnyOp()" )`, ` KnownOp("$^AnyOp" )`, ` KnownOp( $^AnyOp() )`, ` $^KnownOp( AnyOp )`, ` "$^KnownOp( AnyOp )"`, ` "$^KnownOp( AnyOp() )"`, ` "$^KnownOp( $^AnyOp() )"`, `"$^KnownOp(r'$^AnyOp()')"`, } { t.Run(js, func(t *testing.T) { var anyOpPos json.Position _, err = json.Parse([]byte(js), json.ParseOpts{ OpFn: func(op json.Operator, pos json.Position) (any, error) { if op.Name == "KnownOp" { return "OK", nil } anyOpPos = pos return nil, fmt.Errorf("hmm weird operator %q", op.Name) }, }) if test.Error(t, err, "json.Parse fails") { test.EqualInt(t, anyOpPos.Pos, 15) test.EqualInt(t, anyOpPos.Line, 1) test.EqualInt(t, anyOpPos.Col, 15) test.EqualStr(t, err.Error(), `hmm weird operator "AnyOp" at line 1:15 (pos 15)`) } }) } }) t.Run("no operators", func(t *testing.T) { _, err := json.Parse([]byte(" Operator")) if test.Error(t, err, "json.Parse fails") { test.EqualStr(t, err.Error(), `unknown operator "Operator" at line 1:2 (pos 2)`) } }) } golang-github-maxatome-go-testdeep-1.14.0/internal/location/000077500000000000000000000000001454313311600240005ustar00rootroot00000000000000golang-github-maxatome-go-testdeep-1.14.0/internal/location/location.go000066400000000000000000000031751454313311600261450ustar00rootroot00000000000000// Copyright (c) 2018-2021, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package location import ( "fmt" "runtime" "strings" ) // Location records a place in a source file. type Location struct { File string // File name Func string // Function name Line int // Line number inside file Inside string // Inside is used when Location is inside something else BehindCmp bool // BehindCmp is true when operator is behind a Cmp* function } // GetLocationer is the interface that wraps the basic GetLocation method. type GetLocationer interface { GetLocation() Location } // New returns a new [Location]. callDepth is the number of // stack frames to ascend to get the calling function (Func field), // added to 1 to get the File & Line fields. // // If the location can not be determined, ok is false and location is // not valid. func New(callDepth int) (loc Location, ok bool) { _, loc.File, loc.Line, ok = runtime.Caller(callDepth + 1) if !ok { return } if index := strings.LastIndexAny(loc.File, `/\`); index >= 0 { loc.File = loc.File[index+1:] } pc, _, _, ok := runtime.Caller(callDepth) if !ok { return } loc.Func = runtime.FuncForPC(pc).Name() return } // IsInitialized returns true if l is initialized // (e.g. [NewLocation] called without an error), false otherwise. func (l Location) IsInitialized() bool { return l.File != "" } // Implements [fmt.Stringer]. func (l Location) String() string { return fmt.Sprintf("%s %sat %s:%d", l.Func, l.Inside, l.File, l.Line) } golang-github-maxatome-go-testdeep-1.14.0/internal/test/000077500000000000000000000000001454313311600231475ustar00rootroot00000000000000golang-github-maxatome-go-testdeep-1.14.0/internal/test/any.go000066400000000000000000000004201454313311600242610ustar00rootroot00000000000000// Copyright (c) 2022, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. //go:build !go1.18 // +build !go1.18 package test type any = interface{} golang-github-maxatome-go-testdeep-1.14.0/internal/test/any_test.go000066400000000000000000000004251454313311600253250ustar00rootroot00000000000000// Copyright (c) 2022, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. //go:build !go1.18 // +build !go1.18 package test_test type any = interface{} golang-github-maxatome-go-testdeep-1.14.0/internal/test/check.go000066400000000000000000000061211454313311600245530ustar00rootroot00000000000000// Copyright (c) 2018, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package test import ( "strings" "testing" "unicode" "github.com/davecgh/go-spew/spew" "github.com/maxatome/go-testdeep/helpers/tdutil" ) // EqualErrorMessage prints a test error message of the form: // // Message // Failed test // got: got_value // expected: expected_value func EqualErrorMessage(t *testing.T, got, expected any, args ...any) { t.Helper() testName := tdutil.BuildTestName(args...) if testName != "" { testName = " '" + testName + "'" } t.Errorf(`Failed test%s got: %v expected: %v`, testName, got, expected) } func spewIfNeeded(s string) string { for _, chr := range s { if !unicode.IsPrint(chr) { return strings.TrimRight(spew.Sdump(s), "\n") } } return s } // EqualStr checks that got equals expected. func EqualStr(t *testing.T, got, expected string, args ...any) bool { if got == expected { return true } t.Helper() EqualErrorMessage(t, spewIfNeeded(got), spewIfNeeded(expected), args...) return false } // EqualInt checks that got equals expected. func EqualInt(t *testing.T, got, expected int, args ...any) bool { if got == expected { return true } t.Helper() EqualErrorMessage(t, got, expected, args...) return false } // EqualBool checks that got equals expected. func EqualBool(t *testing.T, got, expected bool, args ...any) bool { if got == expected { return true } t.Helper() EqualErrorMessage(t, got, expected, args...) return false } // IsTrue checks that got is true. func IsTrue(t *testing.T, got bool, args ...any) bool { if got { return true } t.Helper() EqualErrorMessage(t, false, true, args...) return false } // IsFalse checks that got is false. func IsFalse(t *testing.T, got bool, args ...any) bool { if !got { return true } t.Helper() EqualErrorMessage(t, true, false, args...) return false } // CheckPanic checks that fn() panics and that the panic() arg is a // string that contains contains. func CheckPanic(t *testing.T, fn func(), contains string) bool { t.Helper() var ( panicked bool panicParam any ) func() { defer func() { panicParam = recover() }() panicked = true fn() panicked = false }() if !panicked { t.Error("panic() did not occur") return false } panicStr, ok := panicParam.(string) if !ok { t.Errorf("panic() occurred but recover()d %T type (%v) instead of string", panicParam, panicParam) return false } if !strings.Contains(panicStr, contains) { t.Errorf("panic() string `%s'\ndoes not contain `%s'", panicStr, contains) return false } return true } // NoError checks that err is nil. func NoError(t *testing.T, err error, args ...any) bool { if err == nil { return true } t.Helper() EqualErrorMessage(t, err, nil, args...) return false } // Error checks that err is non-nil. func Error(t *testing.T, err error, args ...any) bool { if err != nil { return true } t.Helper() EqualErrorMessage(t, err, "", args...) return false } golang-github-maxatome-go-testdeep-1.14.0/internal/test/types.go000066400000000000000000000120251454313311600246420ustar00rootroot00000000000000// Copyright (c) 2018, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package test import ( "fmt" "runtime" "strings" "testing" "github.com/maxatome/go-testdeep/internal/trace" ) // TestingT is a type implementing td.TestingT intended to be used in // tests. type TestingT struct { Messages []string IsFatal bool HasFailed bool } type testingFatal string // NewTestingT returns a new instance of [*TestingT]. func NewTestingT() *TestingT { return &TestingT{} } // Error mocks [testing.T] Error method. func (t *TestingT) Error(args ...any) { t.Messages = append(t.Messages, fmt.Sprint(args...)) t.IsFatal = false t.HasFailed = true } // Fatal mocks [testing.T.Fatal] method. func (t *TestingT) Fatal(args ...any) { t.Messages = append(t.Messages, fmt.Sprint(args...)) t.IsFatal = true t.HasFailed = true panic(testingFatal(t.Messages[len(t.Messages)-1])) } func (t *TestingT) CatchFatal(fn func()) (fatalStr string) { panicked := true trace.IgnorePackage() defer func() { trace.UnignorePackage() if panicked { if x := recover(); x != nil { if str, ok := x.(testingFatal); ok { fatalStr = string(str) } else { panic(x) // rethrow } } } }() fn() panicked = false return } // ContainsMessages checks expectedMsgs are all present in Messages, in // this order. It stops when a message is not found and returns the // remaining messages. func (t *TestingT) ContainsMessages(expectedMsgs ...string) []string { curExp := 0 for _, msg := range t.Messages { for { if curExp == len(expectedMsgs) { return nil } pos := strings.Index(msg, expectedMsgs[curExp]) if pos < 0 { break } msg = msg[pos+len(expectedMsgs[curExp]):] curExp++ } } return expectedMsgs[curExp:] } // Helper mocks [testing.T.Helper] method. func (t *TestingT) Helper() { // Do nothing } // LastMessage returns the last message. func (t *TestingT) LastMessage() string { if len(t.Messages) == 0 { return "" } return t.Messages[len(t.Messages)-1] } // ResetMessages resets the messages. func (t *TestingT) ResetMessages() { t.Messages = t.Messages[:0] } // TestingTB is a type implementing [testing.TB] intended to be used in // tests. type TestingTB struct { TestingT name string testing.TB cleanup func() } // NewTestingTB returns a new instance of [*TestingTB]. func NewTestingTB(name string) *TestingTB { return &TestingTB{name: name} } // Cleanup mocks [testing.T.Cleanup] method. Not thread-safe but we // don't care in tests. func (t *TestingTB) Cleanup(fn func()) { old := t.cleanup t.cleanup = func() { if old != nil { defer old() } fn() } runtime.SetFinalizer(t, func(t *TestingTB) { t.cleanup() }) } // Fatal mocks [testing.T.Error] method. func (t *TestingTB) Error(args ...any) { t.TestingT.Error(args...) } // Errorf mocks [testing.T.Errorf] method. func (t *TestingTB) Errorf(format string, args ...any) { t.TestingT.Error(fmt.Sprintf(format, args...)) } // Fail mocks [testing.T.Fail] method. func (t *TestingTB) Fail() { t.HasFailed = true } // FailNow mocks [testing.T.FailNow] method. func (t *TestingTB) FailNow() { t.HasFailed = true t.IsFatal = true } // Failed mocks [testing.T.Failed] method. func (t *TestingTB) Failed() bool { return t.HasFailed } // Fatal mocks [testing.T.Fatal] method. func (t *TestingTB) Fatal(args ...any) { t.TestingT.Fatal(args...) } // Fatalf mocks [testing.T.Fatalf] method. func (t *TestingTB) Fatalf(format string, args ...any) { t.TestingT.Fatal(fmt.Sprintf(format, args...)) } // Helper mocks [testing.T.Helper] method. func (t *TestingTB) Helper() { // Do nothing } // Log mocks [testing.T.Log] method. func (t *TestingTB) Log(args ...any) { t.Messages = append(t.Messages, fmt.Sprint(args...)) } // Logf mocks [testing.T.Logf] method. func (t *TestingTB) Logf(format string, args ...any) { t.Log(fmt.Sprintf(format, args...)) } // Name mocks [testing.T.Name] method. func (t *TestingTB) Name() string { return t.name } // Skip mocks [testing.T.Skip] method. func (t *TestingTB) Skip(args ...any) {} // SkipNow mocks [testing.T.SkipNow] method. func (t *TestingTB) SkipNow() {} // Skipf mocks [testing.T.Skipf] method. func (t *TestingTB) Skipf(format string, args ...any) {} // Skipped mocks [testing.T.Skipped] method. func (t *TestingTB) Skipped() bool { return false } // ParallelTestingTB is a type implementing [testing.TB] and a // Parallel() method intended to be used in tests. type ParallelTestingTB struct { IsParallel bool *TestingTB } // NewParallelTestingTB returns a new instance of [*ParallelTestingTB]. func NewParallelTestingTB(name string) *ParallelTestingTB { return &ParallelTestingTB{TestingTB: NewTestingTB(name)} } // Parallel mocks the [testing.T.Parallel] method. Not thread-safe. func (t *ParallelTestingTB) Parallel() { if t.IsParallel { // testing.T.Parallel() panics if called multiple times for the // same test. panic("testing: t.Parallel called multiple times") } t.IsParallel = true } golang-github-maxatome-go-testdeep-1.14.0/internal/trace/000077500000000000000000000000001454313311600232665ustar00rootroot00000000000000golang-github-maxatome-go-testdeep-1.14.0/internal/trace/stack.go000066400000000000000000000032561454313311600247300ustar00rootroot00000000000000// Copyright (c) 2021, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package trace import ( "fmt" "io" "strings" ) // Level is a level when retrieving a stack trace. type Level struct { Package string Func string FileLine string } // Stack is a simple stack trace. type Stack []Level // Match returns true if the ith level of s matches pkg (if not empty) // and any function in anyFunc. // // If anyFunc is empty, only the package is tested. // // If a function in anyFunc ends with "*", only the prefix is checked. func (s Stack) Match(i int, pkg string, anyFunc ...string) bool { if i < 0 { i = len(s) + i } if i < 0 || i >= len(s) { return false } level := s[i] if pkg != "" && level.Package != pkg { return false } if len(anyFunc) == 0 { return true } for _, fn := range anyFunc { if strings.HasSuffix(fn, "*") { if strings.HasPrefix(level.Func, fn[:len(fn)-1]) { return true } } else if level.Func == fn { return true } } return false } // IsRelevant returns true if the stack contains more than one level, // or if the single level has a path with at least one directory. func (s Stack) IsRelevant() bool { return len(s) > 1 || (len(s) > 0 && strings.ContainsAny(s[0].FileLine, `/\`)) } // Dump writes the stack to w. func (s Stack) Dump(w io.Writer) { fnMaxLen := 0 for _, level := range s { if len(level.Func) > fnMaxLen { fnMaxLen = len(level.Func) } } fnMaxLen += 2 nl := "" for _, level := range s { fmt.Fprintf(w, "%s\t%-*s %s", nl, fnMaxLen, level.Func+"()", level.FileLine) nl = "\n" } } golang-github-maxatome-go-testdeep-1.14.0/internal/trace/stack_test.go000066400000000000000000000032141454313311600257610ustar00rootroot00000000000000// Copyright (c) 2021, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package trace_test import ( "bytes" "testing" "github.com/maxatome/go-testdeep/internal/test" "github.com/maxatome/go-testdeep/internal/trace" ) func TestStackMatch(t *testing.T) { s := trace.Stack{ {Package: "A", Func: "Aaa.func1"}, {Package: "A", Func: "Aaa.func2"}, {Package: "B", Func: "Bbb"}, {Package: "C", Func: "Ccc"}, } test.IsFalse(t, s.Match(100, "A")) test.IsFalse(t, s.Match(-100, "A")) test.IsFalse(t, s.Match(3, "B")) test.IsFalse(t, s.Match(-1, "B")) test.IsTrue(t, s.Match(3, "C")) test.IsTrue(t, s.Match(-1, "C")) test.IsFalse(t, s.Match(1, "A", "Aaa.func3", "Aaa.func1")) test.IsTrue(t, s.Match(1, "A", "Aaa.func3", "Aaa.func2")) test.IsTrue(t, s.Match(1, "A", "Aaa.func3", "Aaa.func*")) } func TestStackIsRelevant(t *testing.T) { s := trace.Stack{} test.IsFalse(t, s.IsRelevant()) s = trace.Stack{ {FileLine: "xxx.go:456"}, } test.IsFalse(t, s.IsRelevant()) s = trace.Stack{ {FileLine: "xxx.go:456"}, {FileLine: "yyy.go:789"}, } test.IsTrue(t, s.IsRelevant()) s = trace.Stack{ {FileLine: "xxx/yyy.go:456"}, } test.IsTrue(t, s.IsRelevant()) s = trace.Stack{ {FileLine: `xxx\yyy.go:456`}, } test.IsTrue(t, s.IsRelevant()) } func TestStackDump(t *testing.T) { s := trace.Stack{ {Func: "Pipo", FileLine: "xxx.go:456"}, {Func: "Bingo", FileLine: "yyy.go:789"}, } b := bytes.NewBufferString("Stack:\n") s.Dump(b) test.EqualStr(t, b.String(), `Stack: Pipo() xxx.go:456 Bingo() yyy.go:789`) } golang-github-maxatome-go-testdeep-1.14.0/internal/trace/trace.go000066400000000000000000000121261454313311600247150ustar00rootroot00000000000000// Copyright (c) 2021, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package trace import ( "fmt" "go/build" "os" "path/filepath" "runtime" "strings" ) var ( ignorePkg = map[string]struct{}{} goPaths []string goModDir string ) func getPackage(skip ...int) string { sk := 2 if len(skip) > 0 { sk += skip[0] } pc, _, _, ok := runtime.Caller(sk) if ok { fn := runtime.FuncForPC(pc) if fn != nil { pkg, _ := SplitPackageFunc(fn.Name()) return pkg } } return "" } // IgnorePackage records the calling package as ignored one in trace. func IgnorePackage(skip ...int) bool { if pkg := getPackage(skip...); pkg != "" { ignorePkg[pkg] = struct{}{} return true } return false } // UnignorePackage cancels a previous use of [IgnorePackage], so the // calling package is no longer ignored. Only intended to be used in // go-testdeep internal tests. func UnignorePackage(skip ...int) bool { if pkg := getPackage(skip...); pkg != "" { delete(ignorePkg, pkg) return true } return false } // IsIgnoredPackage returns true if pkg is ignored, false // otherwise. Only intended to be used in go-testdeep internal tests. func IsIgnoredPackage(pkg string) (ok bool) { _, ok = ignorePkg[pkg] return } // FindGoModDir finds the closest directory containing go.mod file // starting from directory in. func FindGoModDir(in string) string { for { _, err := os.Stat(filepath.Join(in, "go.mod")) if err == nil { // Do not accept /tmp/go.mod if in != os.TempDir() { return in + string(filepath.Separator) } return "" } nd := filepath.Dir(in) if nd == in { return "" } in = nd } } // FindGoModDirLinks finds the closest directory containing go.mod // file starting from directory in after cleaning it. If not found, // expands symlinks and re-searches. func FindGoModDirLinks(in string) string { in = filepath.Clean(in) if gm := FindGoModDir(in); gm != "" { return gm } lin, err := filepath.EvalSymlinks(in) if err == nil && lin != in { return FindGoModDir(lin) } return "" } // Reset resets the ignored packages map plus cached mod and GOPATH // directories ([Init] should be called again). Only intended to be // used in go-testdeep internal tests. func Reset() { ignorePkg = map[string]struct{}{} goPaths = nil goModDir = "" } // Init initializes trace global variables. func Init() { // GOPATH directories goPaths = nil for _, dir := range filepath.SplitList(build.Default.GOPATH) { dir = filepath.Clean(dir) goPaths = append(goPaths, filepath.Join(dir, "pkg", "mod")+string(filepath.Separator), filepath.Join(dir, "src")+string(filepath.Separator), ) } if wd, err := os.Getwd(); err == nil { // go.mod directory goModDir = FindGoModDirLinks(wd) } } // Frames is the interface corresponding to type returned by // [runtime.CallersFrames]. See [CallersFrames] variable. type Frames interface { Next() (frame runtime.Frame, more bool) } // CallersFrames is only intended to be used in go-testdeep internal // tests to cover all cases. var CallersFrames = func(callers []uintptr) Frames { return runtime.CallersFrames(callers) } // Retrieve retrieves a trace and returns it. func Retrieve(skip int, endFunction string) Stack { var trace Stack var pc [40]uintptr if num := runtime.Callers(skip+2, pc[:]); num > 0 { checkIgnore := true frames := CallersFrames(pc[:num]) for { frame, more := frames.Next() fn := frame.Function if fn == endFunction { break } var pkg string if fn == "" { if frame.File == "" { if more { continue } break } fn = "" } else { pkg, fn = SplitPackageFunc(fn) if checkIgnore && IsIgnoredPackage(pkg) { if more { continue } break } checkIgnore = false } file := strings.TrimPrefix(frame.File, goModDir) if file == frame.File { for _, dir := range goPaths { file = strings.TrimPrefix(frame.File, dir) if file != frame.File { break } } if file == frame.File { file = strings.TrimPrefix(frame.File, build.Default.GOROOT) if file != frame.File { file = filepath.Join("$GOROOT", file) } } } level := Level{ Package: pkg, Func: fn, } if file != "" { level.FileLine = fmt.Sprintf("%s:%d", file, frame.Line) } trace = append(trace, level) if !more { break } } } return trace } // SplitPackageFunc splits a fully qualified function name into its // package and function parts: // // "foo/bar/test.fn" → "foo/bar/test", "fn" // "foo/bar/test.X.fn" → "foo/bar/test", "X.fn" // "foo/bar/test.(*X).fn" → "foo/bar/test", "(*X).fn" // "foo/bar/test.(*X).fn.func1" → "foo/bar/test", "(*X).fn.func1" // "weird" → "", "weird" func SplitPackageFunc(fn string) (string, string) { sp := strings.LastIndexByte(fn, '/') if sp < 0 { sp = 0 // std package } dp := strings.IndexByte(fn[sp:], '.') if dp < 0 { return "", fn } return fn[:sp+dp], fn[sp+dp+1:] } golang-github-maxatome-go-testdeep-1.14.0/internal/trace/trace_test.go000066400000000000000000000163311454313311600257560ustar00rootroot00000000000000// Copyright (c) 2021, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package trace_test import ( "go/build" "os" "path/filepath" "runtime" "testing" "github.com/maxatome/go-testdeep/internal/test" "github.com/maxatome/go-testdeep/internal/trace" ) func TestIgnorePackage(t *testing.T) { const ourPkg = "github.com/maxatome/go-testdeep/internal/trace_test" trace.Reset() test.IsFalse(t, trace.IsIgnoredPackage(ourPkg)) test.IsTrue(t, trace.IgnorePackage()) test.IsTrue(t, trace.IsIgnoredPackage(ourPkg)) test.IsTrue(t, trace.UnignorePackage()) test.IsFalse(t, trace.IsIgnoredPackage(ourPkg)) test.IsTrue(t, trace.IgnorePackage()) test.IsTrue(t, trace.IsIgnoredPackage(ourPkg)) test.IsFalse(t, trace.IgnorePackage(300)) test.IsFalse(t, trace.UnignorePackage(300)) } func TestFindGoModDir(t *testing.T) { tmp, err := os.MkdirTemp("", "go-testdeep") if err != nil { t.Fatalf("TempDir() failed: %s", err) } final := filepath.Join(tmp, "a", "b", "c", "d", "e") err = os.MkdirAll(final, 0755) if err != nil { t.Fatalf("MkdirAll(%s) failed: %s", final, err) } defer os.RemoveAll(tmp) test.EqualStr(t, trace.FindGoModDir(final), "") t.Run("/tmp/.../a/b/c/go.mod", func(t *testing.T) { goMod := filepath.Join(tmp, "a", "b", "c", "go.mod") err := os.WriteFile(goMod, nil, 0644) if err != nil { t.Fatalf("WriteFile(%s) failed: %s", goMod, err) } defer os.Remove(goMod) test.EqualStr(t, trace.FindGoModDir(final), filepath.Join(tmp, "a", "b", "c")+string(filepath.Separator), ) }) t.Run("/tmp/go.mod", func(t *testing.T) { goMod := filepath.Join(os.TempDir(), "go.mod") if _, err := os.Stat(goMod); err != nil { if !os.IsNotExist(err) { t.Fatalf("Stat(%s) failed: %s", goMod, err) } err := os.WriteFile(goMod, nil, 0644) if err != nil { t.Fatalf("WriteFile(%s) failed: %s", goMod, err) } defer os.Remove(goMod) } test.EqualStr(t, trace.FindGoModDir(final), "") }) } func TestFindGoModDirLinks(t *testing.T) { tmp, err := os.MkdirTemp("", "go-testdeep") if err != nil { t.Fatalf("TempDir() failed: %s", err) } goModDir := filepath.Join(tmp, "a", "b", "c") truePath := filepath.Join(goModDir, "d", "e") linkPath := filepath.Join(tmp, "a", "b", "e") err = os.MkdirAll(truePath, 0755) if err != nil { t.Fatalf("MkdirAll(%s) failed: %s", truePath, err) } defer os.RemoveAll(tmp) err = os.Symlink(truePath, linkPath) if err != nil { t.Fatalf("Symlink(%s, %s) failed: %s", truePath, linkPath, err) } goMod := filepath.Join(goModDir, "go.mod") err = os.WriteFile(goMod, nil, 0644) if err != nil { t.Fatalf("WriteFile(%s) failed: %s", goMod, err) } defer os.Remove(goMod) goModDir += string(filepath.Separator) // Simple FindGoModDir test.EqualStr(t, trace.FindGoModDir(truePath), goModDir) test.EqualStr(t, trace.FindGoModDir(linkPath), "") // not found // FindGoModDirLinks test.EqualStr(t, trace.FindGoModDirLinks(truePath), goModDir) test.EqualStr(t, trace.FindGoModDirLinks(linkPath), goModDir) test.EqualStr(t, trace.FindGoModDirLinks(tmp), "") } func TestSplitPackageFunc(t *testing.T) { pkg, fn := trace.SplitPackageFunc("testing.Fatal") test.EqualStr(t, pkg, "testing") test.EqualStr(t, fn, "Fatal") pkg, fn = trace.SplitPackageFunc("github.com/maxatome/go-testdeep/td.Cmp") test.EqualStr(t, pkg, "github.com/maxatome/go-testdeep/td") test.EqualStr(t, fn, "Cmp") pkg, fn = trace.SplitPackageFunc("foo/bar/test.(*T).Cmp") test.EqualStr(t, pkg, "foo/bar/test") test.EqualStr(t, fn, "(*T).Cmp") pkg, fn = trace.SplitPackageFunc("foo/bar/test.(*X).c.func1") test.EqualStr(t, pkg, "foo/bar/test") test.EqualStr(t, fn, "(*X).c.func1") pkg, fn = trace.SplitPackageFunc("foo/bar/test.(*X).c.func1") test.EqualStr(t, pkg, "foo/bar/test") test.EqualStr(t, fn, "(*X).c.func1") pkg, fn = trace.SplitPackageFunc("foobar") test.EqualStr(t, pkg, "") test.EqualStr(t, fn, "foobar") pkg, fn = trace.SplitPackageFunc("") test.EqualStr(t, pkg, "") test.EqualStr(t, fn, "") } func d(end string) []trace.Level { return trace.Retrieve(0, end) } func c(end string) []trace.Level { return d(end) } func b(end string) []trace.Level { return c(end) } func a(end string) []trace.Level { return b(end) } func TestZRetrieve(t *testing.T) { trace.Reset() levels := a("testing.tRunner") if !test.EqualInt(t, len(levels), 5) || !test.EqualStr(t, levels[0].Func, "d") || !test.EqualStr(t, levels[0].Package, "github.com/maxatome/go-testdeep/internal/trace_test") || !test.EqualStr(t, levels[1].Func, "c") || !test.EqualStr(t, levels[1].Package, "github.com/maxatome/go-testdeep/internal/trace_test") || !test.EqualStr(t, levels[2].Func, "b") || !test.EqualStr(t, levels[2].Package, "github.com/maxatome/go-testdeep/internal/trace_test") || !test.EqualStr(t, levels[3].Func, "a") || !test.EqualStr(t, levels[3].Package, "github.com/maxatome/go-testdeep/internal/trace_test") || !test.EqualStr(t, levels[4].Func, "TestZRetrieve") || !test.EqualStr(t, levels[4].Package, "github.com/maxatome/go-testdeep/internal/trace_test") { t.Errorf("%#v", levels) } levels = trace.Retrieve(0, "unknown.unknown") maxLevels := len(levels) test.IsTrue(t, maxLevels > 2) test.EqualStr(t, levels[len(levels)-1].Func, "goexit") // runtime.goexit for i := range levels { test.IsTrue(t, trace.IgnorePackage(i)) } levels = trace.Retrieve(0, "unknown.unknown") test.EqualInt(t, len(levels), 0) // Init GOPATH filter trace.Reset() trace.Init() test.IsTrue(t, trace.IgnorePackage()) levels = trace.Retrieve(0, "unknown.unknown") test.EqualInt(t, len(levels), maxLevels-1) } type FakeFrames struct { frames []runtime.Frame cur int } func (f *FakeFrames) Next() (runtime.Frame, bool) { if f.cur >= len(f.frames) { return runtime.Frame{}, false } f.cur++ return f.frames[f.cur-1], f.cur < len(f.frames) } func TestZRetrieveFake(t *testing.T) { saveCallersFrames, saveGOPATH := trace.CallersFrames, build.Default.GOPATH defer func() { trace.CallersFrames, build.Default.GOPATH = saveCallersFrames, saveGOPATH }() var fakeFrames FakeFrames trace.CallersFrames = func(_ []uintptr) trace.Frames { return &fakeFrames } build.Default.GOPATH = "/foo/bar" trace.Reset() trace.Init() fakeFrames = FakeFrames{ frames: []runtime.Frame{ {}, {Function: "", File: "/foo/bar/src/zip/zip.go", Line: 23}, {Function: "", File: "/foo/bar/pkg/mod/zzz/zzz.go", Line: 42}, {Function: "", File: "/bar/foo.go", Line: 34}, {Function: "pkg.MyFunc"}, {}, }, } levels := trace.Retrieve(0, "pipo") if test.EqualInt(t, len(levels), 4) { test.EqualStr(t, levels[0].Func, "") test.EqualStr(t, levels[0].Package, "") test.EqualStr(t, levels[0].FileLine, "zip/zip.go:23") test.EqualStr(t, levels[1].Func, "") test.EqualStr(t, levels[1].Package, "") test.EqualStr(t, levels[1].FileLine, "zzz/zzz.go:42") test.EqualStr(t, levels[2].Func, "") test.EqualStr(t, levels[2].Package, "") test.EqualStr(t, levels[2].FileLine, "/bar/foo.go:34") test.EqualStr(t, levels[3].Func, "MyFunc") test.EqualStr(t, levels[3].Package, "pkg") test.EqualStr(t, levels[3].FileLine, "") } else { t.Errorf("%#v", levels) } } golang-github-maxatome-go-testdeep-1.14.0/internal/types/000077500000000000000000000000001454313311600233345ustar00rootroot00000000000000golang-github-maxatome-go-testdeep-1.14.0/internal/types/any.go000066400000000000000000000004211454313311600244470ustar00rootroot00000000000000// Copyright (c) 2022, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. //go:build !go1.18 // +build !go1.18 package types type any = interface{} golang-github-maxatome-go-testdeep-1.14.0/internal/types/any_test.go000066400000000000000000000004261454313311600255130ustar00rootroot00000000000000// Copyright (c) 2022, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. //go:build !go1.18 // +build !go1.18 package types_test type any = interface{} golang-github-maxatome-go-testdeep-1.14.0/internal/types/order.go000066400000000000000000000031711454313311600250000ustar00rootroot00000000000000// Copyright (c) 2021-2022, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package types import ( "reflect" "github.com/maxatome/go-testdeep/internal/dark" ) // NewOrder returns a function able to compare 2 non-nil values of type t. // It returns nil if the type t is not comparable. func NewOrder(t reflect.Type) func(a, b reflect.Value) int { // Compare(T) int if m, ok := cmpMethod("Compare", t, Int); ok { return func(va, vb reflect.Value) int { // use dark.MustGetInterface() to bypass possible private fields ret := m.Call([]reflect.Value{ reflect.ValueOf(dark.MustGetInterface(va)), reflect.ValueOf(dark.MustGetInterface(vb)), }) return int(ret[0].Int()) } } // Less(T) bool if m, ok := cmpMethod("Less", t, Bool); ok { return func(va, vb reflect.Value) int { // use dark.MustGetInterface() to bypass possible private fields va = reflect.ValueOf(dark.MustGetInterface(va)) vb = reflect.ValueOf(dark.MustGetInterface(vb)) ret := m.Call([]reflect.Value{va, vb}) if ret[0].Bool() { // a < b return -1 } ret = m.Call([]reflect.Value{vb, va}) if ret[0].Bool() { // b < a return 1 } return 0 } } return nil } func cmpMethod(name string, in, out reflect.Type) (reflect.Value, bool) { if equal, ok := in.MethodByName(name); ok { ft := equal.Type if !ft.IsVariadic() && ft.NumIn() == 2 && ft.NumOut() == 1 && ft.In(0) == in && ft.In(1) == in && ft.Out(0) == out { return equal.Func, true } } return reflect.Value{}, false } golang-github-maxatome-go-testdeep-1.14.0/internal/types/order_test.go000066400000000000000000000046711454313311600260450ustar00rootroot00000000000000// Copyright (c) 2022, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package types_test import ( "reflect" "testing" "github.com/maxatome/go-testdeep/internal/test" "github.com/maxatome/go-testdeep/internal/types" ) type compareType int func (i compareType) Compare(j compareType) int { if i < j { return -1 } if i > j { return 1 } return 0 } type lessType int func (i lessType) Less(j lessType) bool { return i < j } type badType1 int func (i badType1) Compare(j ...badType1) int { return 0 } // IsVariadic() func (i badType1) Less(j, k badType1) bool { return false } // NumIn() == 3 type badType2 int func (i badType2) Compare() int { return 0 } // NumIn() == 1 func (i badType2) Less(j badType2) {} // NumOut() == 0 type badType3 int func (i badType3) Compare(j badType3) (int, int) { return 0, 0 } // NumOut() == 2 func (i badType3) Less(j int) bool { return false } // In(1) ≠ in type badType4 int func (i badType4) Compare(j badType4) bool { return false } // Out(0) ≠ out func (i badType4) Less(j badType4) int { return 0 } // Out(0) ≠ out func TestOrder(t *testing.T) { if types.NewOrder(reflect.TypeOf(0)) != nil { t.Error("types.NewOrder(int) returned non-nil func") } fn := types.NewOrder(reflect.TypeOf(compareType(0))) if fn == nil { t.Error("types.NewOrder(compareType) returned nil func") } else { a, b := reflect.ValueOf(compareType(1)), reflect.ValueOf(compareType(2)) test.EqualInt(t, fn(a, b), -1) test.EqualInt(t, fn(b, a), 1) test.EqualInt(t, fn(a, a), 0) } fn = types.NewOrder(reflect.TypeOf(lessType(0))) if fn == nil { t.Error("types.NewOrder(lessType) returned nil func") } else { a, b := reflect.ValueOf(lessType(1)), reflect.ValueOf(lessType(2)) test.EqualInt(t, fn(a, b), -1) test.EqualInt(t, fn(b, a), 1) test.EqualInt(t, fn(a, a), 0) } if types.NewOrder(reflect.TypeOf(badType1(0))) != nil { t.Error("types.NewOrder(badType1) returned non-nil func") } if types.NewOrder(reflect.TypeOf(badType2(0))) != nil { t.Error("types.NewOrder(badType2) returned non-nil func") } if types.NewOrder(reflect.TypeOf(badType3(0))) != nil { t.Error("types.NewOrder(badType3) returned non-nil func") } if types.NewOrder(reflect.TypeOf(badType4(0))) != nil { t.Error("types.NewOrder(badType4) returned non-nil func") } } golang-github-maxatome-go-testdeep-1.14.0/internal/types/reflect.go000066400000000000000000000057761454313311600253260ustar00rootroot00000000000000// Copyright (c) 2020-2022, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package types import ( "encoding/json" "fmt" "reflect" "strings" "time" ) var ( Bool = reflect.TypeOf(false) Interface = reflect.TypeOf((*any)(nil)).Elem() SliceInterface = reflect.TypeOf(([]any)(nil)) FmtStringer = reflect.TypeOf((*fmt.Stringer)(nil)).Elem() Error = reflect.TypeOf((*error)(nil)).Elem() JsonUnmarshaler = reflect.TypeOf((*json.Unmarshaler)(nil)).Elem() //nolint: revive Time = reflect.TypeOf(time.Time{}) Int = reflect.TypeOf(int(0)) Uint8 = reflect.TypeOf(uint8(0)) Rune = reflect.TypeOf(rune(0)) String = reflect.TypeOf("") ) // IsStruct returns true if t is a struct or a pointer on a struct // (whatever the number of chained pointers), false otherwise. func IsStruct(t reflect.Type) bool { for { switch t.Kind() { case reflect.Struct: return true case reflect.Ptr: t = t.Elem() default: return false } } } // IsTypeOrConvertible returns (true, false) if v type == target, // (true, true) if v if convertible to target type, (false, false) // otherwise. // // It handles go 1.17 slice to array pointer convertibility. func IsTypeOrConvertible(v reflect.Value, target reflect.Type) (bool, bool) { if v.Type() == target { return true, false } if IsConvertible(v, target) { return true, true } return false, false } // IsConvertible returns true if v is convertible to target type, // false otherwise. // // It handles go 1.17 slice to array pointer convertibility. // It handles go 1.20 slice to array convertibility. func IsConvertible(v reflect.Value, target reflect.Type) bool { if v.Type().ConvertibleTo(target) { tk := target.Kind() if v.Kind() != reflect.Slice || (tk != reflect.Ptr && tk != reflect.Array) || // Since go 1.17, a slice can be convertible to a pointer to an // array, but Convert() may still panic if the slice length is lesser // than array pointed one (tk == reflect.Ptr && (target.Elem().Kind() != reflect.Array || v.Len() >= target.Elem().Len())) || // Since go 1.20, a slice can also be convertible to an array, but // Convert() may still panic if the slice length is lesser than // array one (tk == reflect.Array && v.Len() >= target.Len()) { return true } } return false } // KindType returns the kind of val as a string. If the kind is // [reflect.Ptr], a "*" is used as prefix of kind of // val.Type().Elem(), and so on. If the final kind differs from // val.Type(), the type is appended inside parenthesis. func KindType(val reflect.Value) string { if !val.IsValid() { return "nil" } nptr := 0 typ := val.Type() for typ.Kind() == reflect.Ptr { nptr++ typ = typ.Elem() } kind := strings.Repeat("*", nptr) + typ.Kind().String() if typ := val.Type().String(); kind != typ { kind += " (" + typ + " type)" } return kind } golang-github-maxatome-go-testdeep-1.14.0/internal/types/reflect_go117_test.go000066400000000000000000000023031454313311600272620ustar00rootroot00000000000000// Copyright (c) 2021, 2022, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. //go:build go1.17 && !go1.20 // +build go1.17,!go1.20 package types_test import ( "reflect" "testing" "github.com/maxatome/go-testdeep/internal/test" "github.com/maxatome/go-testdeep/internal/types" ) // go1.17 allows to convert []T to *[n]T. func TestIsTypeOrConvertible_go117(t *testing.T) { type ArrP *[5]int ok, convertible := types.IsTypeOrConvertible( reflect.ValueOf([]int{1, 2, 3, 4, 5}), reflect.TypeOf((ArrP)(nil))) test.IsTrue(t, ok) test.IsTrue(t, convertible) ok, convertible = types.IsTypeOrConvertible( reflect.ValueOf([]int{1, 2, 3, 4}), // not enough items reflect.TypeOf((ArrP)(nil))) test.IsFalse(t, ok) test.IsFalse(t, convertible) ok, convertible = types.IsTypeOrConvertible( reflect.ValueOf([]int{1, 2, 3, 4, 5}), reflect.TypeOf(&struct{}{})) test.IsFalse(t, ok) test.IsFalse(t, convertible) ok, convertible = types.IsTypeOrConvertible( reflect.ValueOf([]int{1, 2, 3, 4, 5}), reflect.TypeOf([5]int{})) test.IsFalse(t, ok) test.IsFalse(t, convertible) } golang-github-maxatome-go-testdeep-1.14.0/internal/types/reflect_go120_test.go000066400000000000000000000026371454313311600272660ustar00rootroot00000000000000// Copyright (c) 2022, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. //go:build go1.20 // +build go1.20 package types_test import ( "reflect" "testing" "github.com/maxatome/go-testdeep/internal/test" "github.com/maxatome/go-testdeep/internal/types" ) // go1.17 allows to convert []T to *[n]T. // go1.20 allows to convert []T to [n]T. func TestIsTypeOrConvertible_go117(t *testing.T) { type ArrP *[5]int type Arr [5]int // 1.17 ok, convertible := types.IsTypeOrConvertible( reflect.ValueOf([]int{1, 2, 3, 4, 5}), reflect.TypeOf((ArrP)(nil))) test.IsTrue(t, ok) test.IsTrue(t, convertible) // 1.20 ok, convertible = types.IsTypeOrConvertible( reflect.ValueOf([]int{1, 2, 3, 4, 5}), reflect.TypeOf([5]int{})) test.IsTrue(t, ok) test.IsTrue(t, convertible) // 1.20 ok, convertible = types.IsTypeOrConvertible( reflect.ValueOf([]int{1, 2, 3, 4, 5}), reflect.TypeOf(Arr{})) test.IsTrue(t, ok) test.IsTrue(t, convertible) ok, convertible = types.IsTypeOrConvertible( reflect.ValueOf([]int{1, 2, 3, 4}), // not enough items reflect.TypeOf((ArrP)(nil))) test.IsFalse(t, ok) test.IsFalse(t, convertible) ok, convertible = types.IsTypeOrConvertible( reflect.ValueOf([]int{1, 2, 3, 4, 5}), reflect.TypeOf(&struct{}{})) test.IsFalse(t, ok) test.IsFalse(t, convertible) } golang-github-maxatome-go-testdeep-1.14.0/internal/types/reflect_test.go000066400000000000000000000035101454313311600263450ustar00rootroot00000000000000// Copyright (c) 2020-2022, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package types_test import ( "reflect" "testing" "github.com/maxatome/go-testdeep/internal/test" "github.com/maxatome/go-testdeep/internal/types" ) func TestIsStruct(t *testing.T) { s := struct{}{} ps := &s pps := &ps m := map[string]struct{}{} for i, test := range []struct { val any ok bool }{ {val: s, ok: true}, {val: ps, ok: true}, {val: pps, ok: true}, {val: &pps, ok: true}, {val: m, ok: false}, {val: &m, ok: false}, } { if types.IsStruct(reflect.TypeOf(test.val)) != test.ok { t.Errorf("#%d IsStruct() mismatch as ≠ %t", i, test.ok) } } } func TestIsTypeOrConvertible(t *testing.T) { type MyInt int ok, convertible := types.IsTypeOrConvertible(reflect.ValueOf(123), reflect.TypeOf(123)) test.IsTrue(t, ok) test.IsFalse(t, convertible) ok, convertible = types.IsTypeOrConvertible(reflect.ValueOf(123), reflect.TypeOf(123.45)) test.IsTrue(t, ok) test.IsTrue(t, convertible) ok, convertible = types.IsTypeOrConvertible(reflect.ValueOf(123), reflect.TypeOf(MyInt(123))) test.IsTrue(t, ok) test.IsTrue(t, convertible) ok, convertible = types.IsTypeOrConvertible(reflect.ValueOf("xx"), reflect.TypeOf(123)) test.IsFalse(t, ok) test.IsFalse(t, convertible) } func TestKindType(t *testing.T) { for _, tc := range []struct { val any expected string }{ {nil, "nil"}, {42, "int"}, {(*int)(nil), "*int"}, {(*[]int)(nil), "*slice (*[]int type)"}, {(***int)(nil), "***int"}, } { vval := reflect.ValueOf(tc.val) name := "nil" if tc.val != nil { name = vval.Type().String() } t.Run(name, func(t *testing.T) { test.EqualStr(t, types.KindType(vval), tc.expected) }) } } golang-github-maxatome-go-testdeep-1.14.0/internal/types/types.go000066400000000000000000000043761454313311600250410ustar00rootroot00000000000000// Copyright (c) 2018, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package types import ( "encoding/json" "strconv" ) // TestDeepStringer is a TestDeep specific interface for objects which // know how to stringify themselves. type TestDeepStringer interface { _TestDeep() String() string } // TestDeepStamp is a useful type providing the _TestDeep() method // needed to implement [TestDeepStringer] interface. type TestDeepStamp struct{} func (t TestDeepStamp) _TestDeep() {} // RawString implements [TestDeepStringer] interface. type RawString string func (s RawString) _TestDeep() {} func (s RawString) String() string { return string(s) } // RawInt implements [TestDeepStringer] interface. type RawInt int func (i RawInt) _TestDeep() {} func (i RawInt) String() string { return strconv.Itoa(int(i)) } var _ = []TestDeepStringer{RawString(""), RawInt(0)} // OperatorNotJSONMarshallableError implements error interface. It // is returned by (*td.TestDeep).MarshalJSON() to notice the user an // operator cannot be JSON Marshal'ed. type OperatorNotJSONMarshallableError string // Error implements error interface. func (e OperatorNotJSONMarshallableError) Error() string { return string(e) + " TestDeep operator cannot be json.Marshal'led" } // Operator returns the operator behind this error. func (e OperatorNotJSONMarshallableError) Operator() string { return string(e) } // AsOperatorNotJSONMarshallableError checks that err is or contains // an [OperatorNotJSONMarshallableError] and if yes, returns it and // true. func AsOperatorNotJSONMarshallableError(err error) (OperatorNotJSONMarshallableError, bool) { switch err := err.(type) { case OperatorNotJSONMarshallableError: return err, true case *json.MarshalerError: if err, ok := err.Err.(OperatorNotJSONMarshallableError); ok { return err, true } } return "", false } type RecvKind bool const ( _ RecvKind = (iota & 1) == 0 RecvNothing RecvClosed ) func (r RecvKind) _TestDeep() {} func (r RecvKind) String() string { if r == RecvNothing { return "nothing received on channel" } return "channel is closed" } var _ = []TestDeepStringer{RecvNothing, RecvClosed} golang-github-maxatome-go-testdeep-1.14.0/internal/types/types_private_test.go000066400000000000000000000006231454313311600276210ustar00rootroot00000000000000// Copyright (c) 2020-2022, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package types import ( "testing" ) // Only for coverage... func TestTypes(t *testing.T) { (TestDeepStamp{})._TestDeep() RawString("")._TestDeep() RawInt(0)._TestDeep() RecvNothing._TestDeep() } golang-github-maxatome-go-testdeep-1.14.0/internal/types/types_test.go000066400000000000000000000042751454313311600260760ustar00rootroot00000000000000// Copyright (c) 2021-2022, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package types_test import ( "encoding/json" "errors" "testing" "github.com/maxatome/go-testdeep/internal/types" ) var _ error = types.OperatorNotJSONMarshallableError("") func TestOperatorNotJSONMarshallableError(t *testing.T) { e := types.OperatorNotJSONMarshallableError("Pipo") if e.Error() != "Pipo TestDeep operator cannot be json.Marshal'led" { t.Errorf("unexpected %q", e.Error()) } if e.Operator() != "Pipo" { t.Errorf("unexpected %q", e.Operator()) } t.Run("AsOperatorNotJSONMarshallableError", func(t *testing.T) { ne, ok := types.AsOperatorNotJSONMarshallableError(e) if !ok { t.Error("AsOperatorNotJSONMarshallableError() returned false") return } if ne != e { t.Errorf("AsOperatorNotJSONMarshallableError(): %q ≠ %q", ne.Error(), e.Error()) } other := errors.New("Other error") _, ok = types.AsOperatorNotJSONMarshallableError(other) if ok { t.Error("AsOperatorNotJSONMarshallableError() returned true") return } je := &json.MarshalerError{Err: e} ne, ok = types.AsOperatorNotJSONMarshallableError(je) if !ok { t.Error("AsOperatorNotJSONMarshallableError() returned false") return } if ne != e { t.Errorf("AsOperatorNotJSONMarshallableError(): %q ≠ %q", ne.Error(), e.Error()) } je.Err = other _, ok = types.AsOperatorNotJSONMarshallableError(je) if ok { t.Error("AsOperatorNotJSONMarshallableError() returned true") return } }) } func TestRawString(t *testing.T) { s := types.RawString("foo") if str := s.String(); str != "foo" { t.Errorf("Very weird, got %s", str) } } func TestRawInt(t *testing.T) { i := types.RawInt(42) if str := i.String(); str != "42" { t.Errorf("Very weird, got %s", str) } } func TestRecvKind(t *testing.T) { s := types.RecvNothing.String() if s != "nothing received on channel" { t.Errorf(`got: %q / expected: "nothing received on channel"`, s) } s = types.RecvClosed.String() if s != "channel is closed" { t.Errorf(`got: %q / expected: "channel is closed"`, s) } } golang-github-maxatome-go-testdeep-1.14.0/internal/util/000077500000000000000000000000001454313311600231455ustar00rootroot00000000000000golang-github-maxatome-go-testdeep-1.14.0/internal/util/any.go000066400000000000000000000004201454313311600242570ustar00rootroot00000000000000// Copyright (c) 2022, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. //go:build !go1.18 // +build !go1.18 package util type any = interface{} golang-github-maxatome-go-testdeep-1.14.0/internal/util/any_test.go000066400000000000000000000004251454313311600253230ustar00rootroot00000000000000// Copyright (c) 2022, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. //go:build !go1.18 // +build !go1.18 package util_test type any = interface{} golang-github-maxatome-go-testdeep-1.14.0/internal/util/json_pointer.go000066400000000000000000000040651454313311600262120ustar00rootroot00000000000000// Copyright (c) 2020, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package util import ( "strconv" "strings" ) var jsonPointerEsc = strings.NewReplacer("~0", "~", "~1", "/") const ( ErrJSONPointerInvalid = "invalid JSON pointer" ErrJSONPointerKeyNotFound = "key not found" ErrJSONPointerArrayNoIndex = "array but not an index in JSON pointer" ErrJSONPointerArrayOutOfRange = "out of array range" ErrJSONPointerArrayBadType = "not a map nor an array" ) type JSONPointerError struct { Type string Pointer string } func (e *JSONPointerError) Error() string { if e.Pointer == "" { return e.Type } return e.Type + " @" + e.Pointer } // JSONPointer returns the value corresponding to JSON pointer // pointer in v as [RFC 6901] specifies it. To be searched, v has // to contains map[string]any or []any values. All // other types fail to be searched. // // [RFC 6901]: https://tools.ietf.org/html/rfc6901 func JSONPointer(v any, pointer string) (any, error) { if !strings.HasPrefix(pointer, "/") { if pointer == "" { return v, nil } return nil, &JSONPointerError{Type: ErrJSONPointerInvalid} } pos := 0 for _, part := range strings.Split(pointer[1:], "/") { pos += 1 + len(part) part = jsonPointerEsc.Replace(part) switch tv := v.(type) { case map[string]any: var ok bool v, ok = tv[part] if !ok { return nil, &JSONPointerError{ Type: ErrJSONPointerKeyNotFound, Pointer: pointer[:pos], } } case []any: i, err := strconv.Atoi(part) if err != nil || i < 0 { return nil, &JSONPointerError{ Type: ErrJSONPointerArrayNoIndex, Pointer: pointer[:pos], } } if i >= len(tv) { return nil, &JSONPointerError{ Type: ErrJSONPointerArrayOutOfRange, Pointer: pointer[:pos], } } v = tv[i] default: return nil, &JSONPointerError{ Type: ErrJSONPointerArrayBadType, Pointer: pointer[:pos], } } } return v, nil } golang-github-maxatome-go-testdeep-1.14.0/internal/util/json_pointer_test.go000066400000000000000000000036651454313311600272560ustar00rootroot00000000000000// Copyright (c) 2020, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package util_test import ( "encoding/json" "reflect" "testing" "github.com/maxatome/go-testdeep/internal/util" ) func TestJSONPointer(t *testing.T) { var ref any err := json.Unmarshal([]byte(` { "foo": ["bar", "baz"], "": 0, "a/b": 1, "c%d": 2, "e^f": 3, "g|h": 4, "i\\j": 5, "k\"l": 6, " ": 7, "m~n": 8 }`), &ref) if err != nil { t.Fatalf("json.Unmarshal failed: %s", err) } checkOK := func(pointer string, expected any) { t.Helper() got, err := util.JSONPointer(ref, pointer) if !reflect.DeepEqual(got, expected) { t.Errorf("got: %v expected: %v", got, expected) } if err != nil { t.Errorf("error <%s> received instead of nil", err) } } checkErr := func(pointer, errExpected string) { t.Helper() got, err := util.JSONPointer(ref, pointer) if got != nil { t.Errorf("got: %v expected: nil", got) } if err == nil { t.Errorf("error nil received instead of <%s>", errExpected) } else if err.Error() != errExpected { t.Errorf("error <%s> received instead of <%s>", err, errExpected) } } checkOK(``, ref) checkOK(`/foo`, []any{"bar", "baz"}) checkOK(`/foo/0`, "bar") checkOK(`/`, float64(0)) checkOK(`/a~1b`, float64(1)) checkOK(`/c%d`, float64(2)) checkOK(`/e^f`, float64(3)) checkOK(`/g|h`, float64(4)) checkOK(`/i\j`, float64(5)) checkOK(`/k"l`, float64(6)) checkOK(`/ `, float64(7)) checkOK(`/m~0n`, float64(8)) checkErr("x", "invalid JSON pointer") checkErr("/8", "key not found @/8") checkErr("/foo/-1/pipo", "array but not an index in JSON pointer @/foo/-1") checkErr("/foo/bingo/pipo", "array but not an index in JSON pointer @/foo/bingo") checkErr("/foo/2/pipo", "out of array range @/foo/2") checkErr("/foo/1/pipo", "not a map nor an array @/foo/1/pipo") } golang-github-maxatome-go-testdeep-1.14.0/internal/util/string.go000066400000000000000000000120131454313311600247770ustar00rootroot00000000000000// Copyright (c) 2018-2022, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package util import ( "bytes" "fmt" "io" "reflect" "strconv" "strings" "github.com/maxatome/go-testdeep/helpers/tdutil" "github.com/maxatome/go-testdeep/internal/dark" "github.com/maxatome/go-testdeep/internal/types" ) // ToString does its best to stringify val. func ToString(val any) string { if val == nil { return "nil" } switch tval := val.(type) { case reflect.Value: newVal, ok := dark.GetInterface(tval, true) if ok { return ToString(newVal) } case []reflect.Value: var buf strings.Builder SliceToString(&buf, tval) return buf.String() // no "(string) " prefix for printable strings case string: return tdutil.FormatString(tval) // no "(int) " prefix for ints case int: return strconv.Itoa(tval) // no "(float64) " prefix for float64s case float64: s := strconv.FormatFloat(tval, 'g', -1, 64) if strings.ContainsAny(s, "e.IN") { // I for Inf, N for NaN return s } return s + ".0" // to distinguish from ints // no "(bool) " prefix for booleans case bool: return TernStr(tval, "true", "false") case types.TestDeepStringer: return tval.String() } return tdutil.SpewString(val) } // IndentString indents str lines (from 2nd one = 1st line is not // indented) by indent. func IndentString(str, indent string) string { return strings.ReplaceAll(str, "\n", "\n"+indent) } // IndentStringIn indents str lines (from 2nd one = 1st line is not // indented) by indent and write it to w. func IndentStringIn(w io.Writer, str, indent string) { repl := strings.NewReplacer("\n", "\n"+indent) repl.WriteString(w, str) //nolint: errcheck } // IndentColorizeStringIn indents str lines (from 2nd one = 1st line // is not indented) by indent and write it to w. Before each end of // line, colOff is inserted, and after each indent on new line, colOn // is inserted. func IndentColorizeStringIn(w io.Writer, str, indent, colOn, colOff string) { if str != "" { if colOn == "" && colOff == "" { IndentStringIn(w, str, indent) return } repl := strings.NewReplacer("\n", colOff+"\n"+indent+colOn) io.WriteString(w, colOn) //nolint: errcheck repl.WriteString(w, str) //nolint: errcheck io.WriteString(w, colOff) //nolint: errcheck } } // SliceToString stringifies items slice into buf then returns buf. func SliceToString(buf *strings.Builder, items []reflect.Value) *strings.Builder { buf.WriteByte('(') begLine := strings.LastIndexByte(buf.String(), '\n') + 1 prefix := strings.Repeat(" ", buf.Len()-begLine) if len(items) < 2 { if len(items) > 0 { buf.WriteString(IndentString(ToString(items[0]), prefix)) } } else { buf.WriteString(IndentString(ToString(items[0]), prefix)) for _, item := range items[1:] { buf.WriteString(",\n") buf.WriteString(prefix) buf.WriteString(IndentString(ToString(item), prefix)) } } buf.WriteByte(')') return buf } // TypeFullName returns the t type name with packages fully visible // instead of the last package part in t.String(). func TypeFullName(t reflect.Type) string { var b bytes.Buffer typeFullName(&b, t) return b.String() } func typeFullName(b *bytes.Buffer, t reflect.Type) { if t.Name() != "" { if pkg := t.PkgPath(); pkg != "" { fmt.Fprintf(b, "%s.", pkg) } b.WriteString(t.Name()) return } switch t.Kind() { case reflect.Ptr: b.WriteByte('*') typeFullName(b, t.Elem()) case reflect.Slice: b.WriteString("[]") typeFullName(b, t.Elem()) case reflect.Array: fmt.Fprintf(b, "[%d]", t.Len()) typeFullName(b, t.Elem()) case reflect.Map: b.WriteString("map[") typeFullName(b, t.Key()) b.WriteByte(']') typeFullName(b, t.Elem()) case reflect.Struct: b.WriteString("struct {") if num := t.NumField(); num > 0 { for i := 0; i < num; i++ { sf := t.Field(i) if !sf.Anonymous { b.WriteByte(' ') b.WriteString(sf.Name) } b.WriteByte(' ') typeFullName(b, sf.Type) b.WriteByte(';') } b.Truncate(b.Len() - 1) b.WriteByte(' ') } b.WriteByte('}') case reflect.Func: b.WriteString("func(") if num := t.NumIn(); num > 0 { for i := 0; i < num; i++ { if i == num-1 && t.IsVariadic() { b.WriteString("...") typeFullName(b, t.In(i).Elem()) } else { typeFullName(b, t.In(i)) } b.WriteString(", ") } b.Truncate(b.Len() - 2) } b.WriteByte(')') if num := t.NumOut(); num > 0 { if num == 1 { b.WriteByte(' ') } else { b.WriteString(" (") } for i := 0; i < num; i++ { typeFullName(b, t.Out(i)) b.WriteString(", ") } b.Truncate(b.Len() - 2) if num > 1 { b.WriteByte(')') } } case reflect.Chan: switch t.ChanDir() { case reflect.RecvDir: b.WriteString("<-chan ") case reflect.SendDir: b.WriteString("chan<- ") case reflect.BothDir: b.WriteString("chan ") } typeFullName(b, t.Elem()) default: // Fallback to default implementation b.WriteString(t.String()) } } golang-github-maxatome-go-testdeep-1.14.0/internal/util/string_test.go000066400000000000000000000142101454313311600260370ustar00rootroot00000000000000// Copyright (c) 2018-2022, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package util_test import ( "bytes" "math" "reflect" "runtime" "strings" "testing" "github.com/maxatome/go-testdeep/internal/test" "github.com/maxatome/go-testdeep/internal/types" "github.com/maxatome/go-testdeep/internal/util" ) type myTestDeepStringer struct { types.TestDeepStamp } func (m myTestDeepStringer) String() string { return "TesT!" } func TestToString(t *testing.T) { for _, curTest := range []struct { paramGot any expected string }{ {paramGot: nil, expected: "nil"}, {paramGot: "foobar", expected: `"foobar"`}, {paramGot: "foo\rbar", expected: `(string) (len=7) "foo\rbar"`}, {paramGot: "foo\u2028bar", expected: `(string) (len=9) "foo\u2028bar"`}, {paramGot: `foo"bar`, expected: "`foo\"bar`"}, {paramGot: "foo\n\"bar", expected: "`foo\n\"bar`"}, {paramGot: "foo`\"\nbar", expected: "(string) (len=9) \"foo`\\\"\\nbar\""}, {paramGot: "foo`\n\"bar", expected: "(string) (len=9) \"foo`\\n\\\"bar\""}, {paramGot: "foo\n`\"bar", expected: "(string) (len=9) \"foo\\n`\\\"bar\""}, {paramGot: "foo\n\"`bar", expected: "(string) (len=9) \"foo\\n\\\"`bar\""}, {paramGot: reflect.ValueOf("foobar"), expected: `"foobar"`}, { paramGot: []reflect.Value{reflect.ValueOf("foo"), reflect.ValueOf("bar")}, expected: `("foo", "bar")`, }, {paramGot: types.RawString("test"), expected: "test"}, {paramGot: types.RawInt(42), expected: "42"}, {paramGot: myTestDeepStringer{}, expected: "TesT!"}, {paramGot: 42, expected: "42"}, {paramGot: true, expected: "true"}, {paramGot: false, expected: "false"}, {paramGot: int64(42), expected: "(int64) 42"}, {paramGot: float64(42), expected: "42.0"}, {paramGot: float64(42.56), expected: "42.56"}, {paramGot: float64(4e56), expected: "4e+56"}, {paramGot: math.Inf(1), expected: "+Inf"}, {paramGot: math.Inf(-1), expected: "-Inf"}, {paramGot: math.NaN(), expected: "NaN"}, } { test.EqualStr(t, util.ToString(curTest.paramGot), curTest.expected) } } func TestIndentString(t *testing.T) { for _, curTest := range []struct { ParamGot string Expected string }{ {ParamGot: "", Expected: ""}, {ParamGot: "pipo", Expected: "pipo"}, {ParamGot: "pipo\nbingo\nzip", Expected: "pipo\n-bingo\n-zip"}, } { test.EqualStr(t, util.IndentString(curTest.ParamGot, "-"), curTest.Expected) var buf bytes.Buffer util.IndentStringIn(&buf, curTest.ParamGot, "-") test.EqualStr(t, buf.String(), curTest.Expected) buf.Reset() util.IndentColorizeStringIn(&buf, curTest.ParamGot, "-", "", "") test.EqualStr(t, buf.String(), curTest.Expected) } for _, curTest := range []struct { ParamGot string Expected string }{ {ParamGot: "", Expected: ""}, {ParamGot: "pipo", Expected: "<>"}, {ParamGot: "pipo\nbingo\nzip", Expected: "<>\n-<>\n-<>"}, } { var buf bytes.Buffer util.IndentColorizeStringIn(&buf, curTest.ParamGot, "-", "<<", ">>") test.EqualStr(t, buf.String(), curTest.Expected) } } func TestSliceToBuffer(t *testing.T) { for _, curTest := range []struct { BufInit string Items []any Expected string }{ {BufInit: ">", Items: nil, Expected: ">()"}, {BufInit: ">", Items: []any{"pipo"}, Expected: `>("pipo")`}, { BufInit: ">", Items: []any{"pipo", "bingo", "zip"}, Expected: `>("pipo", "bingo", "zip")`, }, { BufInit: "List\n of\nitems:\n>", Items: []any{"pipo", "bingo", "zip"}, Expected: `List of items: >("pipo", "bingo", "zip")`, }, } { var items []reflect.Value if curTest.Items != nil { items = make([]reflect.Value, len(curTest.Items)) for i, val := range curTest.Items { items[i] = reflect.ValueOf(val) } } var buf strings.Builder buf.WriteString(curTest.BufInit) test.EqualStr(t, util.SliceToString(&buf, items).String(), curTest.Expected) } } func TestTypeFullName(t *testing.T) { // our full package name pc, _, _, _ := runtime.Caller(0) pkg := strings.TrimSuffix(runtime.FuncForPC(pc).Name(), ".TestTypeFullName") test.EqualStr(t, util.TypeFullName(reflect.TypeOf(123)), "int") test.EqualStr(t, util.TypeFullName(reflect.TypeOf([]int{})), "[]int") test.EqualStr(t, util.TypeFullName(reflect.TypeOf([3]int{})), "[3]int") test.EqualStr(t, util.TypeFullName(reflect.TypeOf((**float64)(nil))), "**float64") test.EqualStr(t, util.TypeFullName(reflect.TypeOf(map[int]float64{})), "map[int]float64") test.EqualStr(t, util.TypeFullName(reflect.TypeOf(struct{}{})), "struct {}") test.EqualStr(t, util.TypeFullName(reflect.TypeOf(struct { a int b bool }{})), "struct { a int; b bool }") test.EqualStr(t, util.TypeFullName(reflect.TypeOf(struct { s struct{ a []int } b bool }{})), "struct { s struct { a []int }; b bool }") type anon struct{ a []int } //nolint: unused test.EqualStr(t, util.TypeFullName(reflect.TypeOf(struct { anon b bool }{})), "struct { "+pkg+".anon; b bool }") test.EqualStr(t, util.TypeFullName(reflect.TypeOf(func() {})), "func()") test.EqualStr(t, util.TypeFullName(reflect.TypeOf(func(a int) {})), "func(int)") test.EqualStr(t, util.TypeFullName(reflect.TypeOf(func(a int, b ...bool) rune { return 0 })), "func(int, ...bool) int32") test.EqualStr(t, util.TypeFullName(reflect.TypeOf(func() (int, bool, int) { return 0, true, 0 })), "func() (int, bool, int)") test.EqualStr(t, util.TypeFullName(reflect.TypeOf(func() {})), "func()") test.EqualStr(t, util.TypeFullName(reflect.TypeOf(func(a int) {})), "func(int)") test.EqualStr(t, util.TypeFullName(reflect.TypeOf(func(a int, b ...bool) rune { return 0 })), "func(int, ...bool) int32") test.EqualStr(t, util.TypeFullName(reflect.TypeOf(func() (int, bool, int) { return 0, true, 0 })), "func() (int, bool, int)") test.EqualStr(t, util.TypeFullName(reflect.TypeOf((<-chan []int)(nil))), "<-chan []int") test.EqualStr(t, util.TypeFullName(reflect.TypeOf((chan<- []int)(nil))), "chan<- []int") test.EqualStr(t, util.TypeFullName(reflect.TypeOf((chan []int)(nil))), "chan []int") test.EqualStr(t, util.TypeFullName(reflect.TypeOf((*any)(nil))), "*interface {}") } golang-github-maxatome-go-testdeep-1.14.0/internal/util/tag.go000066400000000000000000000015651454313311600242560ustar00rootroot00000000000000// Copyright (c) 2019, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package util import ( "errors" "unicode" ) // ErrTagEmpty is the error returned by [CheckTag] for an empty tag. var ErrTagEmpty = errors.New("A tag cannot be empty") // ErrTagInvalid is the error returned by [CheckTag] for an invalid tag. var ErrTagInvalid = errors.New("Invalid tag, should match (Letter|_)(Letter|_|Number)*") // CheckTag checks that tag is a valid tag (see operator [Tag]) or not. // // [Tag]: https://go-testdeep.zetta.rocks/operators/tag/ func CheckTag(tag string) error { if tag == "" { return ErrTagEmpty } for i, r := range tag { if !(unicode.IsLetter(r) || r == '_' || (i > 0 && unicode.IsNumber(r))) { return ErrTagInvalid } } return nil } golang-github-maxatome-go-testdeep-1.14.0/internal/util/tag_test.go000066400000000000000000000017721454313311600253150ustar00rootroot00000000000000// Copyright (c) 2019, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package util_test import ( "testing" "github.com/maxatome/go-testdeep/internal/util" ) func TestCheckTag(t *testing.T) { tags := []string{ "tag12", "_1é", "a9", "a", "é൫", "é", "_", } for _, tag := range tags { if err := util.CheckTag(tag); err != nil { t.Errorf("check(%s) failed: %s", tag, err) } } tagsInfo := []struct { tag string err error }{ {tag: "", err: util.ErrTagEmpty}, {tag: "൫a", err: util.ErrTagInvalid}, {tag: "9a", err: util.ErrTagInvalid}, {tag: "é ", err: util.ErrTagInvalid}, } for _, info := range tagsInfo { err := util.CheckTag(info.tag) if err == nil { t.Errorf("check(%s) should not succeed", info.tag) } else if err != info.err { t.Errorf(`check(%s) returned "%s" intead of expected "%s"`, info.tag, err, info.err) } } } golang-github-maxatome-go-testdeep-1.14.0/internal/util/utils.go000066400000000000000000000007361454313311600246420ustar00rootroot00000000000000// Copyright (c) 2018, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package util // TernRune returns a if cond is true, b otherwise. func TernRune(cond bool, a, b rune) rune { if cond { return a } return b } // TernStr returns a if cond is true, b otherwise. func TernStr(cond bool, a, b string) string { if cond { return a } return b } golang-github-maxatome-go-testdeep-1.14.0/internal/util/utils_test.go000066400000000000000000000011371454313311600256750ustar00rootroot00000000000000// Copyright (c) 2018, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package util_test import ( "testing" "github.com/maxatome/go-testdeep/internal/test" "github.com/maxatome/go-testdeep/internal/util" ) func TestTern(t *testing.T) { test.EqualStr(t, util.TernStr(true, "A", "B"), "A") test.EqualStr(t, util.TernStr(false, "A", "B"), "B") test.EqualInt(t, int(util.TernRune(true, 'A', 'B')), int('A')) test.EqualInt(t, int(util.TernRune(false, 'A', 'B')), int('B')) } golang-github-maxatome-go-testdeep-1.14.0/internal/visited/000077500000000000000000000000001454313311600236375ustar00rootroot00000000000000golang-github-maxatome-go-testdeep-1.14.0/internal/visited/any.go000066400000000000000000000004231454313311600247540ustar00rootroot00000000000000// Copyright (c) 2022, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. //go:build !go1.18 // +build !go1.18 package visited type any = interface{} golang-github-maxatome-go-testdeep-1.14.0/internal/visited/any_test.go000066400000000000000000000004301454313311600260110ustar00rootroot00000000000000// Copyright (c) 2022, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. //go:build !go1.18 // +build !go1.18 package visited_test type any = interface{} golang-github-maxatome-go-testdeep-1.14.0/internal/visited/visited.go000066400000000000000000000037321454313311600256420ustar00rootroot00000000000000// Copyright (c) 2019, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package visited import ( "reflect" ) // visitKey is used by ctxerr.Context and its Visited map to handle // cyclic references. type visitedKey struct { a1 uintptr a2 uintptr typ reflect.Type } // Visited allows to remember couples of same type pointers, typically // to not do the same action twice if the couple has already been seen. type Visited map[visitedKey]bool // NewVisited returns a new [Visited] instance. func NewVisited() Visited { return Visited{} } // Record checks and, if needed, records a new entry for (got, // expected) couple. It returns true if got & expected are pointers // and have already been seen together. It returns false otherwise. // It is the caller responsibility to check that got and expected // types are the same. func (v Visited) Record(got, expected reflect.Value) bool { var addr1, addr2 uintptr switch got.Kind() { // Pointer() can not be used for interfaces and for slices the // returned address is the array behind the slice, use UnsafeAddr() // instead case reflect.Slice, reflect.Interface: if got.IsNil() || expected.IsNil() || !got.CanAddr() || !expected.CanAddr() { return false } addr1 = got.UnsafeAddr() addr2 = expected.UnsafeAddr() // For maps and pointers use Pointer() to automatically handle // indirect pointers case reflect.Map, reflect.Ptr: if got.IsNil() || expected.IsNil() { return false } addr1 = got.Pointer() addr2 = expected.Pointer() default: return false } if addr1 > addr2 { // Canonicalize order to reduce number of entries in v. // Assumes non-moving garbage collector. addr1, addr2 = addr2, addr1 } k := visitedKey{ a1: addr1, a2: addr2, typ: got.Type(), } if v[k] { return true // references already seen } // Remember for later. v[k] = true return false } golang-github-maxatome-go-testdeep-1.14.0/internal/visited/visited_test.go000066400000000000000000000060241454313311600266760ustar00rootroot00000000000000// Copyright (c) 2019, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package visited_test import ( "reflect" "testing" "github.com/maxatome/go-testdeep/internal/test" "github.com/maxatome/go-testdeep/internal/visited" ) func TestVisited(t *testing.T) { t.Run("not a pointer", func(t *testing.T) { v := visited.NewVisited() a, b := 1, 2 test.IsFalse(t, v.Record(reflect.ValueOf(a), reflect.ValueOf(b))) test.IsFalse(t, v.Record(reflect.ValueOf(a), reflect.ValueOf(b))) }) t.Run("map", func(t *testing.T) { v := visited.NewVisited() a, b := map[string]bool{}, map[string]bool{} f := func(m map[string]bool) reflect.Value { return reflect.ValueOf(m) } test.IsFalse(t, v.Record(f(a), f(b))) test.IsTrue(t, v.Record(f(a), f(b))) test.IsTrue(t, v.Record(f(b), f(a))) // nil maps are not recorded b = nil test.IsFalse(t, v.Record(f(a), f(b))) test.IsFalse(t, v.Record(f(a), f(b))) test.IsFalse(t, v.Record(f(b), f(a))) test.IsFalse(t, v.Record(f(b), f(a))) }) t.Run("pointer", func(t *testing.T) { v := visited.NewVisited() type S struct { p *S ok bool } a, b := &S{}, &S{} a.p = &S{ok: true} b.p = &S{ok: false} f := func(m *S) reflect.Value { return reflect.ValueOf(m) } test.IsFalse(t, v.Record(f(a), f(b))) test.IsTrue(t, v.Record(f(a), f(b))) test.IsTrue(t, v.Record(f(b), f(a))) test.IsFalse(t, v.Record(f(a.p), f(b.p))) test.IsTrue(t, v.Record(f(a.p), f(b.p))) test.IsTrue(t, v.Record(f(b.p), f(a.p))) // nil pointers are not recorded b = nil test.IsFalse(t, v.Record(f(a), f(b))) test.IsFalse(t, v.Record(f(a), f(b))) test.IsFalse(t, v.Record(f(b), f(a))) test.IsFalse(t, v.Record(f(b), f(a))) }) // Visited.Record() needs its slice or interface param be // addressable, that's why we use a struct pointer below t.Run("slice", func(t *testing.T) { v := visited.NewVisited() type vSlice struct{ s []string } a, b := &vSlice{s: []string{}}, &vSlice{[]string{}} f := func(vm *vSlice) reflect.Value { return reflect.ValueOf(vm).Elem().Field(0) } test.IsFalse(t, v.Record(f(a), f(b))) test.IsTrue(t, v.Record(f(a), f(b))) test.IsTrue(t, v.Record(f(b), f(a))) // nil slices are not recorded b = &vSlice{} test.IsFalse(t, v.Record(f(a), f(b))) test.IsFalse(t, v.Record(f(a), f(b))) test.IsFalse(t, v.Record(f(b), f(a))) test.IsFalse(t, v.Record(f(b), f(a))) }) t.Run("interface", func(t *testing.T) { v := visited.NewVisited() type vIf struct{ i any } a, b := &vIf{i: 42}, &vIf{i: 24} f := func(vm *vIf) reflect.Value { return reflect.ValueOf(vm).Elem().Field(0) } test.IsFalse(t, v.Record(f(a), f(b))) test.IsTrue(t, v.Record(f(a), f(b))) test.IsTrue(t, v.Record(f(b), f(a))) // nil interfaces are not recorded b = &vIf{} test.IsFalse(t, v.Record(f(a), f(b))) test.IsFalse(t, v.Record(f(a), f(b))) test.IsFalse(t, v.Record(f(b), f(a))) test.IsFalse(t, v.Record(f(b), f(a))) }) } golang-github-maxatome-go-testdeep-1.14.0/td/000077500000000000000000000000001454313311600207635ustar00rootroot00000000000000golang-github-maxatome-go-testdeep-1.14.0/td/any.go000066400000000000000000000004161454313311600221020ustar00rootroot00000000000000// Copyright (c) 2022, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. //go:build !go1.18 // +build !go1.18 package td type any = interface{} golang-github-maxatome-go-testdeep-1.14.0/td/any_test.go000066400000000000000000000004231454313311600231370ustar00rootroot00000000000000// Copyright (c) 2022, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. //go:build !go1.18 // +build !go1.18 package td_test type any = interface{} golang-github-maxatome-go-testdeep-1.14.0/td/check_test.go000066400000000000000000000226061454313311600234340ustar00rootroot00000000000000// Copyright (c) 2018, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td_test import ( "fmt" "os" "reflect" "regexp" "strings" "testing" "github.com/maxatome/go-testdeep/helpers/tdutil" "github.com/maxatome/go-testdeep/internal/color" "github.com/maxatome/go-testdeep/internal/ctxerr" "github.com/maxatome/go-testdeep/internal/dark" "github.com/maxatome/go-testdeep/internal/test" "github.com/maxatome/go-testdeep/td" ) func TestMain(m *testing.M) { color.SaveState() os.Exit(m.Run()) } type MyStructBase struct { ValBool bool } type MyStructMid struct { MyStructBase ValStr string } type MyStruct struct { MyStructMid ValInt int Ptr *int } func (s *MyStruct) MyString() string { return "!" } type MyInterface interface { MyString() string } type MyStringer struct{} func (s MyStringer) String() string { return "pipo bingo" } type expectedError struct { Path expectedErrorMatch Message expectedErrorMatch Got expectedErrorMatch Expected expectedErrorMatch Summary expectedErrorMatch Located bool Under expectedErrorMatch Origin *expectedError } type expectedErrorMatch struct { Exact string Match *regexp.Regexp Contain string } func mustBe(str string) expectedErrorMatch { return expectedErrorMatch{Exact: str} } func mustMatch(str string) expectedErrorMatch { return expectedErrorMatch{Match: regexp.MustCompile(str)} } func mustContain(str string) expectedErrorMatch { return expectedErrorMatch{Contain: str} } func indent(str string, numSpc int) string { return strings.ReplaceAll(str, "\n", "\n\t"+strings.Repeat(" ", numSpc)) } func fullError(err *ctxerr.Error) string { return strings.ReplaceAll(err.Error(), "\n", "\n\t> ") } func cmpErrorStr(t *testing.T, err *ctxerr.Error, got string, expected expectedErrorMatch, fieldName string, args ...any, ) bool { t.Helper() if expected.Exact != "" && got != expected.Exact { t.Errorf(`%sError.%s mismatch got: %s expected: %s Full error: > %s`, tdutil.BuildTestName(args...), fieldName, indent(got, 10), indent(expected.Exact, 10), fullError(err)) return false } if expected.Contain != "" && !strings.Contains(got, expected.Contain) { t.Errorf(`%sError.%s mismatch got: %s should contain: %s Full error: > %s`, tdutil.BuildTestName(args...), fieldName, indent(got, 16), indent(expected.Contain, 16), fullError(err)) return false } if expected.Match != nil && !expected.Match.MatchString(got) { t.Errorf(`%sError.%s mismatch got: %s should match: %s Full error: > %s`, tdutil.BuildTestName(args...), fieldName, indent(got, 14), indent(expected.Match.String(), 14), fullError(err)) return false } return true } func matchError(t *testing.T, err *ctxerr.Error, expectedError expectedError, expectedIsTestDeep bool, args ...any, ) bool { t.Helper() if !cmpErrorStr(t, err, err.Message, expectedError.Message, "Message", args...) { return false } if !cmpErrorStr(t, err, err.Context.Path.String(), expectedError.Path, "Context.Path", args...) { return false } if !cmpErrorStr(t, err, err.GotString(), expectedError.Got, "Got", args...) { return false } if !cmpErrorStr(t, err, err.ExpectedString(), expectedError.Expected, "Expected", args...) { return false } if !cmpErrorStr(t, err, err.SummaryString(), expectedError.Summary, "Summary", args...) { return false } // under serr, under := err.Error(), "" if pos := strings.Index(serr, "\n[under operator "); pos > 0 { under = serr[pos+2:] under = under[:strings.IndexByte(under, ']')] } if !cmpErrorStr(t, err, under, expectedError.Under, "[under operator …]", args...) { return false } // If expected is a TestDeep, the Location should be set if expectedIsTestDeep { expectedError.Located = true } if expectedError.Located != err.Location.IsInitialized() { t.Errorf(`%sLocation of the origin of the error got: %v expected: %v`, tdutil.BuildTestName(args...), err.Location.IsInitialized(), expectedError.Located) return false } if expectedError.Located && !strings.HasSuffix(err.Location.File, "_test.go") { t.Errorf(`%sFile of the origin of the error got: line %d of %s expected: *_test.go`, tdutil.BuildTestName(args...), err.Location.Line, err.Location.File) return false } if expectedError.Origin != nil { if err.Origin == nil { t.Errorf(`%sError should originate from another Error`, tdutil.BuildTestName(args...)) return false } return matchError(t, err.Origin, *expectedError.Origin, expectedIsTestDeep, args...) } if err.Origin != nil { t.Errorf(`%sError should NOT originate from another Error`, tdutil.BuildTestName(args...)) return false } return true } func _checkError(t *testing.T, got, expected any, expectedError expectedError, args ...any, ) bool { t.Helper() err := td.EqDeeplyError(got, expected) if err == nil { t.Errorf("%sAn Error should have occurred", tdutil.BuildTestName(args...)) return false } _, expectedIsTestDeep := expected.(td.TestDeep) if !matchError(t, err.(*ctxerr.Error), expectedError, expectedIsTestDeep, args...) { return false } if td.EqDeeply(got, expected) { t.Errorf(`%sBoolean context failed got: true expected: false`, tdutil.BuildTestName(args...)) return false } return true } func ifaceExpectedError(t *testing.T, expectedError expectedError) expectedError { t.Helper() if !strings.Contains(expectedError.Path.Exact, "DATA") { return expectedError } newExpectedError := expectedError newExpectedError.Path.Exact = strings.Replace(expectedError.Path.Exact, "DATA", "DATA.Iface", 1) if newExpectedError.Origin != nil { newOrigin := ifaceExpectedError(t, *newExpectedError.Origin) newExpectedError.Origin = &newOrigin } return newExpectedError } // checkError calls _checkError twice. The first time with the same // parameters, the second time in an any context. func checkError(t *testing.T, got, expected any, expectedError expectedError, args ...any, ) bool { t.Helper() if ok := _checkError(t, got, expected, expectedError, args...); !ok { return false } type tmpStruct struct { Iface any } return _checkError(t, tmpStruct{Iface: got}, td.Struct( tmpStruct{}, td.StructFields{ "Iface": expected, }), ifaceExpectedError(t, expectedError), args...) } func checkErrorForEach(t *testing.T, gotList []any, expected any, expectedError expectedError, args ...any, ) (ret bool) { t.Helper() globalTestName := tdutil.BuildTestName(args...) ret = true for idx, got := range gotList { testName := fmt.Sprintf("Got #%d", idx) if globalTestName != "" { testName += ", " + globalTestName } ret = checkError(t, got, expected, expectedError, testName) && ret } return } // customCheckOK calls chk twice. The first time with the same // parameters, the second time in an any context. func customCheckOK(t *testing.T, chk func(t *testing.T, got, expected any, args ...any) bool, got, expected any, args ...any, ) bool { t.Helper() if ok := chk(t, got, expected, args...); !ok { return false } type tmpStruct struct { Iface any } // Dirty hack to force got be passed as an interface kind return chk(t, tmpStruct{Iface: got}, td.Struct( tmpStruct{}, td.StructFields{ "Iface": expected, }), args...) } func _checkOK(t *testing.T, got, expected any, args ...any, ) bool { t.Helper() if !td.Cmp(t, got, expected, args...) { return false } if !td.EqDeeply(got, expected) { t.Errorf(`%sBoolean context failed got: false expected: true`, tdutil.BuildTestName(args...)) return false } if err := td.EqDeeplyError(got, expected); err != nil { t.Errorf(`%sEqDeeplyError returned an error: %s`, tdutil.BuildTestName(args...), err) return false } return true } // checkOK calls _checkOK twice. The first time with the same // parameters, the second time in an any context. func checkOK(t *testing.T, got, expected any, args ...any, ) bool { t.Helper() return customCheckOK(t, _checkOK, got, expected, args...) } func checkOKOrPanicIfUnsafeDisabled(t *testing.T, got, expected any, args ...any, ) (ret bool) { t.Helper() cmp := func() { t.Helper() ret = _checkOK(t, got, expected, args...) } // Should panic if unsafe package is not available if dark.UnsafeDisabled { return test.CheckPanic(t, cmp, "dark.GetInterface() does not handle private ") } cmp() return } func checkOKForEach(t *testing.T, gotList []any, expected any, args ...any, ) (ret bool) { t.Helper() globalTestName := tdutil.BuildTestName(args...) ret = true for idx, got := range gotList { testName := fmt.Sprintf("Got #%d", idx) if globalTestName != "" { testName += ", " + globalTestName } ret = checkOK(t, got, expected, testName) && ret } return } func equalTypes(t *testing.T, got td.TestDeep, expected any, args ...any) bool { gotType := got.TypeBehind() expectedType, ok := expected.(reflect.Type) if !ok { expectedType = reflect.TypeOf(expected) } if gotType == expectedType { return true } var gotStr, expectedStr string if gotType == nil { gotStr = "nil" } else { gotStr = gotType.String() } if expected == nil { expectedStr = "nil" } else { expectedStr = expectedType.String() } t.Helper() t.Errorf(`%sFailed test got: %s expected: %s`, tdutil.BuildTestName(args...), gotStr, expectedStr) return false } golang-github-maxatome-go-testdeep-1.14.0/td/cmp_deeply.go000066400000000000000000000140041454313311600234320ustar00rootroot00000000000000// Copyright (c) 2018, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td import ( "reflect" "strings" "github.com/maxatome/go-testdeep/helpers/tdutil" "github.com/maxatome/go-testdeep/internal/color" "github.com/maxatome/go-testdeep/internal/ctxerr" "github.com/maxatome/go-testdeep/internal/flat" "github.com/maxatome/go-testdeep/internal/trace" ) func init() { trace.Init() trace.IgnorePackage() } // stripTrace removes go-testdeep useless calls in a trace returned by // trace.Retrieve() to make it clearer for the reader. func stripTrace(s trace.Stack) trace.Stack { if len(s) == 0 { return s } const ( tdPkg = "github.com/maxatome/go-testdeep/td" tdhttpPkg = "github.com/maxatome/go-testdeep/helpers/tdhttp" tdsuitePkg = "github.com/maxatome/go-testdeep/helpers/tdsuite" ) // Remove useless possible (*T).Run() or (*T).RunAssertRequire() first call if s.Match(-1, tdPkg, "(*T).Run.func1", "(*T).RunAssertRequire.func1") { // Remove useless tdhttp (*TestAPI).Run() call // // ✓ xxx Subtest.func1() // ✗ …/tdhttp (*TestAPI).Run.func1 // ✗ …/td (*T).Run.func1() if s.Match(-2, tdhttpPkg, "(*TestAPI).Run.func1") { return s[:len(s)-2] } // Remove useless tdsuite calls // // ✓ xxx Suite.TestSuite // ✗ reflect Value.call // ✗ reflect Value.Call // ✗ …/tdsuite run.func2 // ✗ …/td (*T).Run.func1() or (*T).RunAssertRequire.func1() // // or for PostTest // ✓ xxx Suite.PostTest // ✗ …/tdsuite run.func2.1 // ✗ …/tdsuite run.func2 // ✗ …/td (*T).Run.func1() or (*T).RunAssertRequire.func1() if s.Match(-2, tdsuitePkg, "run.func*") { // PostTest if s.Match(-3, tdsuitePkg, "run.func*") && len(s) > 4 && strings.HasSuffix(s[len(s)-4].Func, ".PostTest") { return s[:len(s)-3] } for i := len(s) - 3; i >= 1; i-- { if !s.Match(i, "reflect") { return s[:i+1] } } return nil } return s[:len(s)-1] } // Remove testing.Cleanup() stack // // ✓ xxx TestCleanup.func2 // ✗ testing (*common).Cleanup.func1 // ✗ testing (*common).runCleanup // ✗ testing tRunner.func2 if s.Match(-1, "testing", "tRunner.func*") && s.Match(-2, "testing", "(*common).runCleanup") && s.Match(-3, "testing", "(*common).Cleanup.func1") { return s[:len(s)-3] } // Remove tdsuite pre-Setup/BetweenTests/Destroy stack // // ✓ xxx Suite.Destroy // ✗ …/tdsuite run.func1 // ✗ …/tdsuite run // ✗ …/tdsuite Run // ✓ xxx TestSuiteDestroy if !s.Match(-1, tdsuitePkg) && s.Match(-2, tdsuitePkg, "Run") { for i := len(s) - 3; i >= 0; i-- { if !s.Match(i, tdsuitePkg) { s[i+1] = s[len(s)-1] return s[:i+2] } } return s[:1] } return s } func formatError(t TestingT, isFatal bool, err *ctxerr.Error, args ...any) { t.Helper() const failedTest = "Failed test" args = flat.Interfaces(args...) var buf strings.Builder color.AppendTestNameOn(&buf) if len(args) == 0 { buf.WriteString(failedTest) } else { buf.WriteString(failedTest + " '") tdutil.FbuildTestName(&buf, args...) buf.WriteString("'") } color.AppendTestNameOff(&buf) buf.WriteString("\n") err.Append(&buf, "", true) // Stask trace if s := stripTrace(trace.Retrieve(0, "testing.tRunner")); s.IsRelevant() { buf.WriteString("\nThis is how we got here:\n") s.Dump(&buf) } if isFatal { t.Fatal(buf.String()) } else { t.Error(buf.String()) } } func cmpDeeply(ctx ctxerr.Context, t TestingT, got, expected any, args ...any, ) bool { err := deepValueEqualFinal(ctx, reflect.ValueOf(got), reflect.ValueOf(expected)) if err == nil { return true } t.Helper() formatError(t, ctx.FailureIsFatal, err, args...) return false } // S returns a string based on args as Cmp* functions do with their // own args parameter to name their test. So behind the scene, // [tdutil.BuildTestName] is used. // // If len(args) > 1 and the first item of args is a string and // contains a '%' rune then [fmt.Fprintf] is used to compose the // returned string, else args are passed to [fmt.Fprint]. // // It can be used as a shorter [fmt.Sprintf]: // // t.Run(fmt.Sprintf("Foo #%d", i), func(t *td.T) {}) // t.Run(td.S("Foo #%d", i), func(t *td.T) {}) // // or to print any values as [fmt.Sprint] handles them: // // a, ok := []int{1, 2, 3}, true // t.Run(fmt.Sprint(a, ok), func(t *td.T) {}) // t.Run(td.S(a, ok), func(t *td.T) {}) // // The only gain is less characters to type. func S(args ...any) string { return tdutil.BuildTestName(args...) } // Cmp returns true if got matches expected. expected can // be the same type as got is, or contains some [TestDeep] // operators. If got does not match expected, it returns false and // the reason of failure is logged with the help of t Error() // method. // // got := "foobar" // td.Cmp(t, got, "foobar") // succeeds // td.Cmp(t, got, td.HasPrefix("foo")) // succeeds // // If t is a [*T] then its Config is inherited, so: // // td.Cmp(td.Require(t), got, 42) // // is the same as: // // td.Require(t).Cmp(got, 42) // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func Cmp(t TestingT, got, expected any, args ...any) bool { t.Helper() return cmpDeeply(newContext(t), t, got, expected, args...) } // CmpDeeply works the same as [Cmp] and is still available for // compatibility purpose. Use shorter [Cmp] in new code. // // got := "foobar" // td.CmpDeeply(t, got, "foobar") // succeeds // td.CmpDeeply(t, got, td.HasPrefix("foo")) // succeeds func CmpDeeply(t TestingT, got, expected any, args ...any) bool { t.Helper() return cmpDeeply(newContext(t), t, got, expected, args...) } golang-github-maxatome-go-testdeep-1.14.0/td/cmp_deeply_test.go000066400000000000000000000230131454313311600244710ustar00rootroot00000000000000// Copyright (c) 2018, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td import ( "bytes" "reflect" "testing" "github.com/maxatome/go-testdeep/internal/ctxerr" "github.com/maxatome/go-testdeep/internal/test" "github.com/maxatome/go-testdeep/internal/trace" ) func TestStripTrace(t *testing.T) { check := func(got, expected trace.Stack) { got = stripTrace(got) if !reflect.DeepEqual(got, expected) { t.Helper() t.Errorf("\n got: %#v\nexpected: %#v", got, expected) } } check(nil, nil) s := trace.Stack{ {Package: "test", Func: "A"}, } check(s, s) s = trace.Stack{ {Package: "test", Func: "A"}, {Package: "test", Func: "TestSimple"}, } check(s, s) // inside testing.Cleanup() call s = trace.Stack{ {Package: "test", Func: "TestCleanup.func2"}, {Package: "testing", Func: "(*common).Cleanup.func1"}, {Package: "testing", Func: "(*common).runCleanup"}, {Package: "testing", Func: "tRunner.func2"}, } check(s, s[:1]) // // td // // td.(*T).Run() call s = trace.Stack{ {Package: "test", Func: "A"}, {Package: "test", Func: "TestSubtestTd.func1"}, {Package: "github.com/maxatome/go-testdeep/td", Func: "(*T).Run.func1"}, } check(s, s[:2]) // td.(*T).RunAssertRequire() call s = trace.Stack{ {Package: "test", Func: "A"}, {Package: "test", Func: "TestSubtestTd.func1"}, {Package: "github.com/maxatome/go-testdeep/td", Func: "(*T).RunAssertRequire.func1"}, } check(s, s[:2]) // // tdhttp // // tdhttp.(*TestAPI).Run() call s = trace.Stack{ {Package: "test", Func: "A"}, {Package: "test", Func: "TestSubtestTd.func1"}, {Package: "github.com/maxatome/go-testdeep/helpers/tdhttp", Func: "(*TestAPI).Run.func1"}, {Package: "github.com/maxatome/go-testdeep/td", Func: "(*T).Run.func1"}, } check(s, s[:2]) // // tdsuite // // tdsuite.Run() call → TestSuite(*td.T) s = trace.Stack{ {Package: "test", Func: "A"}, {Package: "test", Func: "Suite.TestSuite"}, {Package: "reflect", Func: "Value.call"}, {Package: "reflect", Func: "Value.Call"}, {Package: "github.com/maxatome/go-testdeep/helpers/tdsuite", Func: "run.func2"}, {Package: "github.com/maxatome/go-testdeep/td", Func: "(*T).Run.func1"}, } check(s, s[:2]) // tdsuite.Run() call → TestSuite(assert, require *td.T) s = trace.Stack{ {Package: "test", Func: "A"}, {Package: "test", Func: "Suite.TestSuite"}, {Package: "reflect", Func: "Value.call"}, {Package: "reflect", Func: "Value.Call"}, {Package: "github.com/maxatome/go-testdeep/helpers/tdsuite", Func: "run.func1"}, {Package: "github.com/maxatome/go-testdeep/td", Func: "(*T).RunAssertRequire.func1"}, } check(s, s[:2]) // tdsuite.Run() call → Suite.Setup() s = trace.Stack{ {Package: "test", Func: "A"}, {Package: "test", Func: "Suite.Setup"}, {Package: "github.com/maxatome/go-testdeep/helpers/tdsuite", Func: "run"}, {Package: "github.com/maxatome/go-testdeep/helpers/tdsuite", Func: "Run"}, {Package: "test", Func: "TestSuiteSetup"}, } check(s, append(s[:2:2], s[4])) // tdsuite.Run() call → Suite.PreTest() s = trace.Stack{ {Package: "test", Func: "A"}, {Package: "test", Func: "Suite.PreTest"}, {Package: "github.com/maxatome/go-testdeep/helpers/tdsuite", Func: "run.func2"}, {Package: "github.com/maxatome/go-testdeep/td", Func: "(*T).Run.func1"}, } check(s, s[:2]) // tdsuite.Run() call → Suite.PostTest() s = trace.Stack{ {Package: "test", Func: "A"}, {Package: "test", Func: "Suite.PostTest"}, {Package: "github.com/maxatome/go-testdeep/helpers/tdsuite", Func: "run.func2.1"}, {Package: "github.com/maxatome/go-testdeep/helpers/tdsuite", Func: "run.func2"}, {Package: "github.com/maxatome/go-testdeep/td", Func: "(*T).Run.func1"}, } check(s, s[:2]) // tdsuite.Run() call → Suite.BetweenTests() s = trace.Stack{ {Package: "test", Func: "A"}, {Package: "test", Func: "Suite.BetweenTests"}, {Package: "github.com/maxatome/go-testdeep/helpers/tdsuite", Func: "run"}, {Package: "github.com/maxatome/go-testdeep/helpers/tdsuite", Func: "Run"}, {Package: "test", Func: "TestSuiteBetweenTests"}, } check(s, append(s[:2:2], s[4])) // tdsuite.Run() call → Suite.Destroy() s = trace.Stack{ {Package: "test", Func: "A"}, {Package: "test", Func: "Suite.Destroy"}, {Package: "github.com/maxatome/go-testdeep/helpers/tdsuite", Func: "run.func1"}, {Package: "github.com/maxatome/go-testdeep/helpers/tdsuite", Func: "run"}, {Package: "github.com/maxatome/go-testdeep/helpers/tdsuite", Func: "Run"}, {Package: "test", Func: "TestSuiteDestroy"}, } check(s, append(s[:2:2], s[5])) // Improbable cases s = trace.Stack{ {Package: "github.com/maxatome/go-testdeep/helpers/tdsuite", Func: "Run"}, {Package: "test", Func: "TestSuiteDestroy"}, } check(s, s[:1]) s = trace.Stack{ {Package: "github.com/maxatome/go-testdeep/helpers/tdsuite", Func: "y"}, {Package: "github.com/maxatome/go-testdeep/helpers/tdsuite", Func: "x"}, {Package: "github.com/maxatome/go-testdeep/helpers/tdsuite", Func: "Run"}, {Package: "test", Func: "TestSuiteDestroy"}, } check(s, s[:1]) s = trace.Stack{ {Package: "test", Func: "Suite.TestXxx"}, {Package: "github.com/maxatome/go-testdeep/helpers/tdsuite", Func: "Run"}, {Package: "test", Func: "TestSuiteDestroy"}, } check(s, append(s[:1:1], s[2])) s = trace.Stack{ {Package: "reflect", Func: "Value.call"}, {Package: "reflect", Func: "Value.Call"}, {Package: "github.com/maxatome/go-testdeep/helpers/tdsuite", Func: "run.func1"}, {Package: "github.com/maxatome/go-testdeep/td", Func: "(*T).RunAssertRequire.func1"}, } check(s, nil) s = trace.Stack{ {Package: "test", Func: "A"}, {Package: "test", Func: "Suite.TestSuite"}, {Package: "github.com/maxatome/go-testdeep/helpers/tdsuite", Func: "run.func1"}, {Package: "github.com/maxatome/go-testdeep/td", Func: "(*T).RunAssertRequire.func1"}, } check(s, s[:2]) } func TestFormatError(t *testing.T) { err := &ctxerr.Error{ Context: newContext(nil), Message: "test error message", Summary: ctxerr.NewSummary("test error summary"), } nonStringName := bytes.NewBufferString("zip!") for _, fatal := range []bool{false, true} { // // Without args ttt := test.NewTestingT() ttt.CatchFatal(func() { formatError(ttt, fatal, err) }) test.EqualStr(t, ttt.LastMessage(), `Failed test DATA: test error message test error summary`) test.EqualBool(t, ttt.IsFatal, fatal) // // With one arg ttt = test.NewTestingT() ttt.CatchFatal(func() { formatError(ttt, fatal, err, "foo bar!") }) test.EqualStr(t, ttt.LastMessage(), `Failed test 'foo bar!' DATA: test error message test error summary`) test.EqualBool(t, ttt.IsFatal, fatal) ttt = test.NewTestingT() ttt.CatchFatal(func() { formatError(ttt, fatal, err, nonStringName) }) test.EqualStr(t, ttt.LastMessage(), `Failed test 'zip!' DATA: test error message test error summary`) test.EqualBool(t, ttt.IsFatal, fatal) // // With several args & Printf format ttt = test.NewTestingT() ttt.CatchFatal(func() { formatError(ttt, fatal, err, "hello %d!", 123) }) test.EqualStr(t, ttt.LastMessage(), `Failed test 'hello 123!' DATA: test error message test error summary`) test.EqualBool(t, ttt.IsFatal, fatal) // // With several args & Printf format + Flatten ttt = test.NewTestingT() ttt.CatchFatal(func() { formatError(ttt, fatal, err, "hello %s → %d/%d!", "bob", Flatten([]int{123, 125})) }) test.EqualStr(t, ttt.LastMessage(), `Failed test 'hello bob → 123/125!' DATA: test error message test error summary`) test.EqualBool(t, ttt.IsFatal, fatal) // // With several args without Printf format ttt = test.NewTestingT() ttt.CatchFatal(func() { formatError(ttt, fatal, err, "hello ", "world! ", 123) }) test.EqualStr(t, ttt.LastMessage(), `Failed test 'hello world! 123' DATA: test error message test error summary`) test.EqualBool(t, ttt.IsFatal, fatal) // // With several args without Printf format + Flatten ttt = test.NewTestingT() ttt.CatchFatal(func() { formatError(ttt, fatal, err, "hello ", "world! ", Flatten([]int{123, 125})) }) test.EqualStr(t, ttt.LastMessage(), `Failed test 'hello world! 123 125' DATA: test error message test error summary`) test.EqualBool(t, ttt.IsFatal, fatal) ttt = test.NewTestingT() ttt.CatchFatal(func() { formatError(ttt, fatal, err, nonStringName, "hello ", "world! ", 123) }) test.EqualStr(t, ttt.LastMessage(), `Failed test 'zip!hello world! 123' DATA: test error message test error summary`) test.EqualBool(t, ttt.IsFatal, fatal) } } func TestS(t *testing.T) { for i, curTest := range []struct { params []any expected string }{ { params: []any{}, expected: "", }, { params: []any{"pipo", "bingo"}, expected: "pipobingo", }, { params: []any{"pipo %d %s", 42, "bingo"}, expected: "pipo 42 bingo", }, { params: []any{"pipo %d"}, expected: "pipo %d", }, { params: []any{"pipo %", 42}, expected: "pipo %42", }, { params: []any{42, 666}, expected: "42 666", }, } { test.EqualStr(t, S(curTest.params...), curTest.expected, "#%d", i) } } func TestCmp(t *testing.T) { tt := test.NewTestingTB(t.Name()) test.IsTrue(t, Cmp(tt, 1, 1)) test.IsFalse(t, tt.Failed()) tt = test.NewTestingTB(t.Name()) test.IsFalse(t, Cmp(tt, 1, 2)) test.IsTrue(t, tt.Failed()) } func TestCmpDeeply(t *testing.T) { tt := test.NewTestingTB(t.Name()) test.IsTrue(t, CmpDeeply(tt, 1, 1)) test.IsFalse(t, tt.Failed()) tt = test.NewTestingTB(t.Name()) test.IsFalse(t, CmpDeeply(tt, 1, 2)) test.IsTrue(t, tt.Failed()) } golang-github-maxatome-go-testdeep-1.14.0/td/cmp_funcs.go000066400000000000000000001421771454313311600233030ustar00rootroot00000000000000// Copyright (c) 2018-2022, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. // // DO NOT EDIT!!! AUTOMATICALLY GENERATED!!! package td import ( "time" ) // allOperators lists the 67 operators. // nil means not usable in JSON(). var allOperators = map[string]any{ "All": All, "Any": Any, "Array": nil, "ArrayEach": ArrayEach, "Bag": Bag, "Between": Between, "Cap": nil, "Catch": nil, "Code": nil, "Contains": Contains, "ContainsKey": ContainsKey, "Delay": nil, "Empty": Empty, "ErrorIs": nil, "First": First, "Grep": Grep, "Gt": Gt, "Gte": Gte, "HasPrefix": HasPrefix, "HasSuffix": HasSuffix, "Ignore": Ignore, "Isa": nil, "JSON": nil, "JSONPointer": JSONPointer, "Keys": Keys, "Last": Last, "Lax": nil, "Len": Len, "Lt": Lt, "Lte": Lte, "Map": nil, "MapEach": MapEach, "N": N, "NaN": NaN, "Nil": Nil, "None": None, "Not": Not, "NotAny": NotAny, "NotEmpty": NotEmpty, "NotNaN": NotNaN, "NotNil": NotNil, "NotZero": NotZero, "PPtr": nil, "Ptr": nil, "Re": Re, "ReAll": ReAll, "Recv": nil, "SStruct": nil, "Set": Set, "Shallow": nil, "Slice": nil, "Smuggle": nil, "String": nil, "Struct": nil, "SubBagOf": SubBagOf, "SubJSONOf": nil, "SubMapOf": SubMapOf, "SubSetOf": SubSetOf, "SuperBagOf": SuperBagOf, "SuperJSONOf": nil, "SuperMapOf": SuperMapOf, "SuperSetOf": SuperSetOf, "SuperSliceOf": nil, "Tag": nil, "TruncTime": nil, "Values": Values, "Zero": Zero, } // CmpAll is a shortcut for: // // td.Cmp(t, got, td.All(expectedValues...), args...) // // See [All] for details. // // Returns true if the test is OK, false if it fails. // // If t is a [*T] then its Config field is inherited. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func CmpAll(t TestingT, got any, expectedValues []any, args ...any) bool { t.Helper() return Cmp(t, got, All(expectedValues...), args...) } // CmpAny is a shortcut for: // // td.Cmp(t, got, td.Any(expectedValues...), args...) // // See [Any] for details. // // Returns true if the test is OK, false if it fails. // // If t is a [*T] then its Config field is inherited. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func CmpAny(t TestingT, got any, expectedValues []any, args ...any) bool { t.Helper() return Cmp(t, got, Any(expectedValues...), args...) } // CmpArray is a shortcut for: // // td.Cmp(t, got, td.Array(model, expectedEntries), args...) // // See [Array] for details. // // Returns true if the test is OK, false if it fails. // // If t is a [*T] then its Config field is inherited. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func CmpArray(t TestingT, got, model any, expectedEntries ArrayEntries, args ...any) bool { t.Helper() return Cmp(t, got, Array(model, expectedEntries), args...) } // CmpArrayEach is a shortcut for: // // td.Cmp(t, got, td.ArrayEach(expectedValue), args...) // // See [ArrayEach] for details. // // Returns true if the test is OK, false if it fails. // // If t is a [*T] then its Config field is inherited. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func CmpArrayEach(t TestingT, got, expectedValue any, args ...any) bool { t.Helper() return Cmp(t, got, ArrayEach(expectedValue), args...) } // CmpBag is a shortcut for: // // td.Cmp(t, got, td.Bag(expectedItems...), args...) // // See [Bag] for details. // // Returns true if the test is OK, false if it fails. // // If t is a [*T] then its Config field is inherited. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func CmpBag(t TestingT, got any, expectedItems []any, args ...any) bool { t.Helper() return Cmp(t, got, Bag(expectedItems...), args...) } // CmpBetween is a shortcut for: // // td.Cmp(t, got, td.Between(from, to, bounds), args...) // // See [Between] for details. // // [Between] optional parameter bounds is here mandatory. // [BoundsInIn] value should be passed to mimic its absence in // original [Between] call. // // Returns true if the test is OK, false if it fails. // // If t is a [*T] then its Config field is inherited. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func CmpBetween(t TestingT, got, from, to any, bounds BoundsKind, args ...any) bool { t.Helper() return Cmp(t, got, Between(from, to, bounds), args...) } // CmpCap is a shortcut for: // // td.Cmp(t, got, td.Cap(expectedCap), args...) // // See [Cap] for details. // // Returns true if the test is OK, false if it fails. // // If t is a [*T] then its Config field is inherited. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func CmpCap(t TestingT, got, expectedCap any, args ...any) bool { t.Helper() return Cmp(t, got, Cap(expectedCap), args...) } // CmpCode is a shortcut for: // // td.Cmp(t, got, td.Code(fn), args...) // // See [Code] for details. // // Returns true if the test is OK, false if it fails. // // If t is a [*T] then its Config field is inherited. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func CmpCode(t TestingT, got, fn any, args ...any) bool { t.Helper() return Cmp(t, got, Code(fn), args...) } // CmpContains is a shortcut for: // // td.Cmp(t, got, td.Contains(expectedValue), args...) // // See [Contains] for details. // // Returns true if the test is OK, false if it fails. // // If t is a [*T] then its Config field is inherited. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func CmpContains(t TestingT, got, expectedValue any, args ...any) bool { t.Helper() return Cmp(t, got, Contains(expectedValue), args...) } // CmpContainsKey is a shortcut for: // // td.Cmp(t, got, td.ContainsKey(expectedValue), args...) // // See [ContainsKey] for details. // // Returns true if the test is OK, false if it fails. // // If t is a [*T] then its Config field is inherited. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func CmpContainsKey(t TestingT, got, expectedValue any, args ...any) bool { t.Helper() return Cmp(t, got, ContainsKey(expectedValue), args...) } // CmpEmpty is a shortcut for: // // td.Cmp(t, got, td.Empty(), args...) // // See [Empty] for details. // // Returns true if the test is OK, false if it fails. // // If t is a [*T] then its Config field is inherited. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func CmpEmpty(t TestingT, got any, args ...any) bool { t.Helper() return Cmp(t, got, Empty(), args...) } // CmpErrorIs is a shortcut for: // // td.Cmp(t, got, td.ErrorIs(expectedError), args...) // // See [ErrorIs] for details. // // Returns true if the test is OK, false if it fails. // // If t is a [*T] then its Config field is inherited. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func CmpErrorIs(t TestingT, got, expectedError any, args ...any) bool { t.Helper() return Cmp(t, got, ErrorIs(expectedError), args...) } // CmpFirst is a shortcut for: // // td.Cmp(t, got, td.First(filter, expectedValue), args...) // // See [First] for details. // // Returns true if the test is OK, false if it fails. // // If t is a [*T] then its Config field is inherited. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func CmpFirst(t TestingT, got, filter, expectedValue any, args ...any) bool { t.Helper() return Cmp(t, got, First(filter, expectedValue), args...) } // CmpGrep is a shortcut for: // // td.Cmp(t, got, td.Grep(filter, expectedValue), args...) // // See [Grep] for details. // // Returns true if the test is OK, false if it fails. // // If t is a [*T] then its Config field is inherited. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func CmpGrep(t TestingT, got, filter, expectedValue any, args ...any) bool { t.Helper() return Cmp(t, got, Grep(filter, expectedValue), args...) } // CmpGt is a shortcut for: // // td.Cmp(t, got, td.Gt(minExpectedValue), args...) // // See [Gt] for details. // // Returns true if the test is OK, false if it fails. // // If t is a [*T] then its Config field is inherited. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func CmpGt(t TestingT, got, minExpectedValue any, args ...any) bool { t.Helper() return Cmp(t, got, Gt(minExpectedValue), args...) } // CmpGte is a shortcut for: // // td.Cmp(t, got, td.Gte(minExpectedValue), args...) // // See [Gte] for details. // // Returns true if the test is OK, false if it fails. // // If t is a [*T] then its Config field is inherited. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func CmpGte(t TestingT, got, minExpectedValue any, args ...any) bool { t.Helper() return Cmp(t, got, Gte(minExpectedValue), args...) } // CmpHasPrefix is a shortcut for: // // td.Cmp(t, got, td.HasPrefix(expected), args...) // // See [HasPrefix] for details. // // Returns true if the test is OK, false if it fails. // // If t is a [*T] then its Config field is inherited. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func CmpHasPrefix(t TestingT, got any, expected string, args ...any) bool { t.Helper() return Cmp(t, got, HasPrefix(expected), args...) } // CmpHasSuffix is a shortcut for: // // td.Cmp(t, got, td.HasSuffix(expected), args...) // // See [HasSuffix] for details. // // Returns true if the test is OK, false if it fails. // // If t is a [*T] then its Config field is inherited. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func CmpHasSuffix(t TestingT, got any, expected string, args ...any) bool { t.Helper() return Cmp(t, got, HasSuffix(expected), args...) } // CmpIsa is a shortcut for: // // td.Cmp(t, got, td.Isa(model), args...) // // See [Isa] for details. // // Returns true if the test is OK, false if it fails. // // If t is a [*T] then its Config field is inherited. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func CmpIsa(t TestingT, got, model any, args ...any) bool { t.Helper() return Cmp(t, got, Isa(model), args...) } // CmpJSON is a shortcut for: // // td.Cmp(t, got, td.JSON(expectedJSON, params...), args...) // // See [JSON] for details. // // Returns true if the test is OK, false if it fails. // // If t is a [*T] then its Config field is inherited. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func CmpJSON(t TestingT, got, expectedJSON any, params []any, args ...any) bool { t.Helper() return Cmp(t, got, JSON(expectedJSON, params...), args...) } // CmpJSONPointer is a shortcut for: // // td.Cmp(t, got, td.JSONPointer(ptr, expectedValue), args...) // // See [JSONPointer] for details. // // Returns true if the test is OK, false if it fails. // // If t is a [*T] then its Config field is inherited. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func CmpJSONPointer(t TestingT, got any, ptr string, expectedValue any, args ...any) bool { t.Helper() return Cmp(t, got, JSONPointer(ptr, expectedValue), args...) } // CmpKeys is a shortcut for: // // td.Cmp(t, got, td.Keys(val), args...) // // See [Keys] for details. // // Returns true if the test is OK, false if it fails. // // If t is a [*T] then its Config field is inherited. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func CmpKeys(t TestingT, got, val any, args ...any) bool { t.Helper() return Cmp(t, got, Keys(val), args...) } // CmpLast is a shortcut for: // // td.Cmp(t, got, td.Last(filter, expectedValue), args...) // // See [Last] for details. // // Returns true if the test is OK, false if it fails. // // If t is a [*T] then its Config field is inherited. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func CmpLast(t TestingT, got, filter, expectedValue any, args ...any) bool { t.Helper() return Cmp(t, got, Last(filter, expectedValue), args...) } // CmpLax is a shortcut for: // // td.Cmp(t, got, td.Lax(expectedValue), args...) // // See [Lax] for details. // // Returns true if the test is OK, false if it fails. // // If t is a [*T] then its Config field is inherited. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func CmpLax(t TestingT, got, expectedValue any, args ...any) bool { t.Helper() return Cmp(t, got, Lax(expectedValue), args...) } // CmpLen is a shortcut for: // // td.Cmp(t, got, td.Len(expectedLen), args...) // // See [Len] for details. // // Returns true if the test is OK, false if it fails. // // If t is a [*T] then its Config field is inherited. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func CmpLen(t TestingT, got, expectedLen any, args ...any) bool { t.Helper() return Cmp(t, got, Len(expectedLen), args...) } // CmpLt is a shortcut for: // // td.Cmp(t, got, td.Lt(maxExpectedValue), args...) // // See [Lt] for details. // // Returns true if the test is OK, false if it fails. // // If t is a [*T] then its Config field is inherited. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func CmpLt(t TestingT, got, maxExpectedValue any, args ...any) bool { t.Helper() return Cmp(t, got, Lt(maxExpectedValue), args...) } // CmpLte is a shortcut for: // // td.Cmp(t, got, td.Lte(maxExpectedValue), args...) // // See [Lte] for details. // // Returns true if the test is OK, false if it fails. // // If t is a [*T] then its Config field is inherited. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func CmpLte(t TestingT, got, maxExpectedValue any, args ...any) bool { t.Helper() return Cmp(t, got, Lte(maxExpectedValue), args...) } // CmpMap is a shortcut for: // // td.Cmp(t, got, td.Map(model, expectedEntries), args...) // // See [Map] for details. // // Returns true if the test is OK, false if it fails. // // If t is a [*T] then its Config field is inherited. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func CmpMap(t TestingT, got, model any, expectedEntries MapEntries, args ...any) bool { t.Helper() return Cmp(t, got, Map(model, expectedEntries), args...) } // CmpMapEach is a shortcut for: // // td.Cmp(t, got, td.MapEach(expectedValue), args...) // // See [MapEach] for details. // // Returns true if the test is OK, false if it fails. // // If t is a [*T] then its Config field is inherited. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func CmpMapEach(t TestingT, got, expectedValue any, args ...any) bool { t.Helper() return Cmp(t, got, MapEach(expectedValue), args...) } // CmpN is a shortcut for: // // td.Cmp(t, got, td.N(num, tolerance), args...) // // See [N] for details. // // [N] optional parameter tolerance is here mandatory. // 0 value should be passed to mimic its absence in // original [N] call. // // Returns true if the test is OK, false if it fails. // // If t is a [*T] then its Config field is inherited. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func CmpN(t TestingT, got, num, tolerance any, args ...any) bool { t.Helper() return Cmp(t, got, N(num, tolerance), args...) } // CmpNaN is a shortcut for: // // td.Cmp(t, got, td.NaN(), args...) // // See [NaN] for details. // // Returns true if the test is OK, false if it fails. // // If t is a [*T] then its Config field is inherited. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func CmpNaN(t TestingT, got any, args ...any) bool { t.Helper() return Cmp(t, got, NaN(), args...) } // CmpNil is a shortcut for: // // td.Cmp(t, got, td.Nil(), args...) // // See [Nil] for details. // // Returns true if the test is OK, false if it fails. // // If t is a [*T] then its Config field is inherited. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func CmpNil(t TestingT, got any, args ...any) bool { t.Helper() return Cmp(t, got, Nil(), args...) } // CmpNone is a shortcut for: // // td.Cmp(t, got, td.None(notExpectedValues...), args...) // // See [None] for details. // // Returns true if the test is OK, false if it fails. // // If t is a [*T] then its Config field is inherited. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func CmpNone(t TestingT, got any, notExpectedValues []any, args ...any) bool { t.Helper() return Cmp(t, got, None(notExpectedValues...), args...) } // CmpNot is a shortcut for: // // td.Cmp(t, got, td.Not(notExpected), args...) // // See [Not] for details. // // Returns true if the test is OK, false if it fails. // // If t is a [*T] then its Config field is inherited. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func CmpNot(t TestingT, got, notExpected any, args ...any) bool { t.Helper() return Cmp(t, got, Not(notExpected), args...) } // CmpNotAny is a shortcut for: // // td.Cmp(t, got, td.NotAny(notExpectedItems...), args...) // // See [NotAny] for details. // // Returns true if the test is OK, false if it fails. // // If t is a [*T] then its Config field is inherited. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func CmpNotAny(t TestingT, got any, notExpectedItems []any, args ...any) bool { t.Helper() return Cmp(t, got, NotAny(notExpectedItems...), args...) } // CmpNotEmpty is a shortcut for: // // td.Cmp(t, got, td.NotEmpty(), args...) // // See [NotEmpty] for details. // // Returns true if the test is OK, false if it fails. // // If t is a [*T] then its Config field is inherited. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func CmpNotEmpty(t TestingT, got any, args ...any) bool { t.Helper() return Cmp(t, got, NotEmpty(), args...) } // CmpNotNaN is a shortcut for: // // td.Cmp(t, got, td.NotNaN(), args...) // // See [NotNaN] for details. // // Returns true if the test is OK, false if it fails. // // If t is a [*T] then its Config field is inherited. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func CmpNotNaN(t TestingT, got any, args ...any) bool { t.Helper() return Cmp(t, got, NotNaN(), args...) } // CmpNotNil is a shortcut for: // // td.Cmp(t, got, td.NotNil(), args...) // // See [NotNil] for details. // // Returns true if the test is OK, false if it fails. // // If t is a [*T] then its Config field is inherited. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func CmpNotNil(t TestingT, got any, args ...any) bool { t.Helper() return Cmp(t, got, NotNil(), args...) } // CmpNotZero is a shortcut for: // // td.Cmp(t, got, td.NotZero(), args...) // // See [NotZero] for details. // // Returns true if the test is OK, false if it fails. // // If t is a [*T] then its Config field is inherited. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func CmpNotZero(t TestingT, got any, args ...any) bool { t.Helper() return Cmp(t, got, NotZero(), args...) } // CmpPPtr is a shortcut for: // // td.Cmp(t, got, td.PPtr(val), args...) // // See [PPtr] for details. // // Returns true if the test is OK, false if it fails. // // If t is a [*T] then its Config field is inherited. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func CmpPPtr(t TestingT, got, val any, args ...any) bool { t.Helper() return Cmp(t, got, PPtr(val), args...) } // CmpPtr is a shortcut for: // // td.Cmp(t, got, td.Ptr(val), args...) // // See [Ptr] for details. // // Returns true if the test is OK, false if it fails. // // If t is a [*T] then its Config field is inherited. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func CmpPtr(t TestingT, got, val any, args ...any) bool { t.Helper() return Cmp(t, got, Ptr(val), args...) } // CmpRe is a shortcut for: // // td.Cmp(t, got, td.Re(reg, capture), args...) // // See [Re] for details. // // [Re] optional parameter capture is here mandatory. // nil value should be passed to mimic its absence in // original [Re] call. // // Returns true if the test is OK, false if it fails. // // If t is a [*T] then its Config field is inherited. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func CmpRe(t TestingT, got, reg, capture any, args ...any) bool { t.Helper() return Cmp(t, got, Re(reg, capture), args...) } // CmpReAll is a shortcut for: // // td.Cmp(t, got, td.ReAll(reg, capture), args...) // // See [ReAll] for details. // // Returns true if the test is OK, false if it fails. // // If t is a [*T] then its Config field is inherited. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func CmpReAll(t TestingT, got, reg, capture any, args ...any) bool { t.Helper() return Cmp(t, got, ReAll(reg, capture), args...) } // CmpRecv is a shortcut for: // // td.Cmp(t, got, td.Recv(expectedValue, timeout), args...) // // See [Recv] for details. // // [Recv] optional parameter timeout is here mandatory. // 0 value should be passed to mimic its absence in // original [Recv] call. // // Returns true if the test is OK, false if it fails. // // If t is a [*T] then its Config field is inherited. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func CmpRecv(t TestingT, got, expectedValue any, timeout time.Duration, args ...any) bool { t.Helper() return Cmp(t, got, Recv(expectedValue, timeout), args...) } // CmpSet is a shortcut for: // // td.Cmp(t, got, td.Set(expectedItems...), args...) // // See [Set] for details. // // Returns true if the test is OK, false if it fails. // // If t is a [*T] then its Config field is inherited. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func CmpSet(t TestingT, got any, expectedItems []any, args ...any) bool { t.Helper() return Cmp(t, got, Set(expectedItems...), args...) } // CmpShallow is a shortcut for: // // td.Cmp(t, got, td.Shallow(expectedPtr), args...) // // See [Shallow] for details. // // Returns true if the test is OK, false if it fails. // // If t is a [*T] then its Config field is inherited. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func CmpShallow(t TestingT, got, expectedPtr any, args ...any) bool { t.Helper() return Cmp(t, got, Shallow(expectedPtr), args...) } // CmpSlice is a shortcut for: // // td.Cmp(t, got, td.Slice(model, expectedEntries), args...) // // See [Slice] for details. // // Returns true if the test is OK, false if it fails. // // If t is a [*T] then its Config field is inherited. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func CmpSlice(t TestingT, got, model any, expectedEntries ArrayEntries, args ...any) bool { t.Helper() return Cmp(t, got, Slice(model, expectedEntries), args...) } // CmpSmuggle is a shortcut for: // // td.Cmp(t, got, td.Smuggle(fn, expectedValue), args...) // // See [Smuggle] for details. // // Returns true if the test is OK, false if it fails. // // If t is a [*T] then its Config field is inherited. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func CmpSmuggle(t TestingT, got, fn, expectedValue any, args ...any) bool { t.Helper() return Cmp(t, got, Smuggle(fn, expectedValue), args...) } // CmpSStruct is a shortcut for: // // td.Cmp(t, got, td.SStruct(model, expectedFields), args...) // // See [SStruct] for details. // // [SStruct] optional parameter expectedFields is here mandatory. // nil value should be passed to mimic its absence in // original [SStruct] call. // // Returns true if the test is OK, false if it fails. // // If t is a [*T] then its Config field is inherited. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func CmpSStruct(t TestingT, got, model any, expectedFields StructFields, args ...any) bool { t.Helper() return Cmp(t, got, SStruct(model, expectedFields), args...) } // CmpString is a shortcut for: // // td.Cmp(t, got, td.String(expected), args...) // // See [String] for details. // // Returns true if the test is OK, false if it fails. // // If t is a [*T] then its Config field is inherited. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func CmpString(t TestingT, got any, expected string, args ...any) bool { t.Helper() return Cmp(t, got, String(expected), args...) } // CmpStruct is a shortcut for: // // td.Cmp(t, got, td.Struct(model, expectedFields), args...) // // See [Struct] for details. // // [Struct] optional parameter expectedFields is here mandatory. // nil value should be passed to mimic its absence in // original [Struct] call. // // Returns true if the test is OK, false if it fails. // // If t is a [*T] then its Config field is inherited. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func CmpStruct(t TestingT, got, model any, expectedFields StructFields, args ...any) bool { t.Helper() return Cmp(t, got, Struct(model, expectedFields), args...) } // CmpSubBagOf is a shortcut for: // // td.Cmp(t, got, td.SubBagOf(expectedItems...), args...) // // See [SubBagOf] for details. // // Returns true if the test is OK, false if it fails. // // If t is a [*T] then its Config field is inherited. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func CmpSubBagOf(t TestingT, got any, expectedItems []any, args ...any) bool { t.Helper() return Cmp(t, got, SubBagOf(expectedItems...), args...) } // CmpSubJSONOf is a shortcut for: // // td.Cmp(t, got, td.SubJSONOf(expectedJSON, params...), args...) // // See [SubJSONOf] for details. // // Returns true if the test is OK, false if it fails. // // If t is a [*T] then its Config field is inherited. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func CmpSubJSONOf(t TestingT, got, expectedJSON any, params []any, args ...any) bool { t.Helper() return Cmp(t, got, SubJSONOf(expectedJSON, params...), args...) } // CmpSubMapOf is a shortcut for: // // td.Cmp(t, got, td.SubMapOf(model, expectedEntries), args...) // // See [SubMapOf] for details. // // Returns true if the test is OK, false if it fails. // // If t is a [*T] then its Config field is inherited. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func CmpSubMapOf(t TestingT, got, model any, expectedEntries MapEntries, args ...any) bool { t.Helper() return Cmp(t, got, SubMapOf(model, expectedEntries), args...) } // CmpSubSetOf is a shortcut for: // // td.Cmp(t, got, td.SubSetOf(expectedItems...), args...) // // See [SubSetOf] for details. // // Returns true if the test is OK, false if it fails. // // If t is a [*T] then its Config field is inherited. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func CmpSubSetOf(t TestingT, got any, expectedItems []any, args ...any) bool { t.Helper() return Cmp(t, got, SubSetOf(expectedItems...), args...) } // CmpSuperBagOf is a shortcut for: // // td.Cmp(t, got, td.SuperBagOf(expectedItems...), args...) // // See [SuperBagOf] for details. // // Returns true if the test is OK, false if it fails. // // If t is a [*T] then its Config field is inherited. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func CmpSuperBagOf(t TestingT, got any, expectedItems []any, args ...any) bool { t.Helper() return Cmp(t, got, SuperBagOf(expectedItems...), args...) } // CmpSuperJSONOf is a shortcut for: // // td.Cmp(t, got, td.SuperJSONOf(expectedJSON, params...), args...) // // See [SuperJSONOf] for details. // // Returns true if the test is OK, false if it fails. // // If t is a [*T] then its Config field is inherited. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func CmpSuperJSONOf(t TestingT, got, expectedJSON any, params []any, args ...any) bool { t.Helper() return Cmp(t, got, SuperJSONOf(expectedJSON, params...), args...) } // CmpSuperMapOf is a shortcut for: // // td.Cmp(t, got, td.SuperMapOf(model, expectedEntries), args...) // // See [SuperMapOf] for details. // // Returns true if the test is OK, false if it fails. // // If t is a [*T] then its Config field is inherited. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func CmpSuperMapOf(t TestingT, got, model any, expectedEntries MapEntries, args ...any) bool { t.Helper() return Cmp(t, got, SuperMapOf(model, expectedEntries), args...) } // CmpSuperSetOf is a shortcut for: // // td.Cmp(t, got, td.SuperSetOf(expectedItems...), args...) // // See [SuperSetOf] for details. // // Returns true if the test is OK, false if it fails. // // If t is a [*T] then its Config field is inherited. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func CmpSuperSetOf(t TestingT, got any, expectedItems []any, args ...any) bool { t.Helper() return Cmp(t, got, SuperSetOf(expectedItems...), args...) } // CmpSuperSliceOf is a shortcut for: // // td.Cmp(t, got, td.SuperSliceOf(model, expectedEntries), args...) // // See [SuperSliceOf] for details. // // Returns true if the test is OK, false if it fails. // // If t is a [*T] then its Config field is inherited. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func CmpSuperSliceOf(t TestingT, got, model any, expectedEntries ArrayEntries, args ...any) bool { t.Helper() return Cmp(t, got, SuperSliceOf(model, expectedEntries), args...) } // CmpTruncTime is a shortcut for: // // td.Cmp(t, got, td.TruncTime(expectedTime, trunc), args...) // // See [TruncTime] for details. // // [TruncTime] optional parameter trunc is here mandatory. // 0 value should be passed to mimic its absence in // original [TruncTime] call. // // Returns true if the test is OK, false if it fails. // // If t is a [*T] then its Config field is inherited. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func CmpTruncTime(t TestingT, got, expectedTime any, trunc time.Duration, args ...any) bool { t.Helper() return Cmp(t, got, TruncTime(expectedTime, trunc), args...) } // CmpValues is a shortcut for: // // td.Cmp(t, got, td.Values(val), args...) // // See [Values] for details. // // Returns true if the test is OK, false if it fails. // // If t is a [*T] then its Config field is inherited. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func CmpValues(t TestingT, got, val any, args ...any) bool { t.Helper() return Cmp(t, got, Values(val), args...) } // CmpZero is a shortcut for: // // td.Cmp(t, got, td.Zero(), args...) // // See [Zero] for details. // // Returns true if the test is OK, false if it fails. // // If t is a [*T] then its Config field is inherited. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func CmpZero(t TestingT, got any, args ...any) bool { t.Helper() return Cmp(t, got, Zero(), args...) } golang-github-maxatome-go-testdeep-1.14.0/td/cmp_funcs_misc.go000066400000000000000000000200261454313311600243020ustar00rootroot00000000000000// Copyright (c) 2018, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td import ( "runtime" "github.com/maxatome/go-testdeep/internal/ctxerr" "github.com/maxatome/go-testdeep/internal/types" "github.com/maxatome/go-testdeep/internal/util" ) // CmpTrue is a shortcut for: // // td.Cmp(t, got, true, args...) // // Returns true if the test is OK, false if it fails. // // td.CmpTrue(t, IsAvailable(x), "x should be available") // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. // // See also [CmpFalse]. func CmpTrue(t TestingT, got bool, args ...any) bool { t.Helper() return Cmp(t, got, true, args...) } // CmpFalse is a shortcut for: // // td.Cmp(t, got, false, args...) // // Returns true if the test is OK, false if it fails. // // td.CmpFalse(t, IsAvailable(x), "x should not be available") // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. // // See also [CmpTrue]. func CmpFalse(t TestingT, got bool, args ...any) bool { t.Helper() return Cmp(t, got, false, args...) } func cmpError(ctx ctxerr.Context, t TestingT, got error, args ...any) bool { if got != nil { return true } t.Helper() formatError(t, ctx.FailureIsFatal, &ctxerr.Error{ Context: ctx, Message: "should be an error", Got: types.RawString("nil"), Expected: types.RawString("non-nil error"), }, args...) return false } func cmpNoError(ctx ctxerr.Context, t TestingT, got error, args ...any) bool { if got == nil { return true } t.Helper() formatError(t, ctx.FailureIsFatal, &ctxerr.Error{ Context: ctx, Message: "should NOT be an error", Got: got, Expected: types.RawString("nil"), }, args...) return false } // CmpError checks that got is non-nil error. // // _, err := MyFunction(1, 2, 3) // td.CmpError(t, err, "MyFunction(1, 2, 3) should return an error") // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. // // See also [CmpNoError]. func CmpError(t TestingT, got error, args ...any) bool { t.Helper() return cmpError(newContext(t), t, got, args...) } // CmpNoError checks that got is nil error. // // value, err := MyFunction(1, 2, 3) // if td.CmpNoError(t, err) { // // one can now check value... // } // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. // // See also [CmpError]. func CmpNoError(t TestingT, got error, args ...any) bool { t.Helper() return cmpNoError(newContext(t), t, got, args...) } func cmpPanic(ctx ctxerr.Context, t TestingT, fn func(), expected any, args ...any) bool { t.Helper() if ctx.Path.Len() == 1 && ctx.Path.String() == contextDefaultRootName { ctx.Path = ctxerr.NewPath(contextPanicRootName) } var ( panicked bool panicParam any ) func() { defer func() { panicParam = recover() }() panicked = true fn() panicked = false }() if !panicked { formatError(t, ctx.FailureIsFatal, &ctxerr.Error{ Context: ctx, Message: "should have panicked", Summary: ctxerr.NewSummary("did not panic"), }, args...) return false } return cmpDeeply(ctx.AddCustomLevel("→panic()"), t, panicParam, expected, args...) } func cmpNotPanic(ctx ctxerr.Context, t TestingT, fn func(), args ...any) bool { var ( panicked bool stackTrace types.RawString ) func() { defer func() { panicParam := recover() if panicked { buf := make([]byte, 8192) n := runtime.Stack(buf, false) for ; n > 0; n-- { if buf[n-1] != '\n' { break } } stackTrace = types.RawString("panic: " + util.ToString(panicParam) + "\n\n" + string(buf[:n])) } }() panicked = true fn() panicked = false }() if !panicked { return true } t.Helper() if ctx.Path.Len() == 1 && ctx.Path.String() == contextDefaultRootName { ctx.Path = ctxerr.NewPath(contextPanicRootName) } formatError(t, ctx.FailureIsFatal, &ctxerr.Error{ Context: ctx, Message: "should NOT have panicked", Got: stackTrace, Expected: types.RawString("not panicking at all"), }, args...) return false } // CmpPanic calls fn and checks a panic() occurred with the // expectedPanic parameter. It returns true only if both conditions // are fulfilled. // // Note that calling panic(nil) in fn body is always detected as a // panic. [runtime] package says: before Go 1.21, programs that called // panic(nil) observed recover returning nil. Starting in Go 1.21, // programs that call panic(nil) observe recover returning a // [*runtime.PanicNilError]. Programs can change back to the old // behavior by setting GODEBUG=panicnil=1. // // td.CmpPanic(t, // func() { panic("I am panicking!") }, // "I am panicking!", // "The function should panic with the right string") // succeeds // // td.CmpPanic(t, // func() { panic("I am panicking!") }, // Contains("panicking!"), // "The function should panic with a string containing `panicking!`") // succeeds // // td.CmpPanic(t, func() { panic(nil) }, nil, "Checks for panic(nil)") // succeeds // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. // // See also [CmpNotPanic]. func CmpPanic(t TestingT, fn func(), expectedPanic any, args ...any) bool { t.Helper() return cmpPanic(newContext(t), t, fn, expectedPanic, args...) } // CmpNotPanic calls fn and checks no panic() occurred. If a panic() // occurred false is returned then the panic() parameter and the stack // trace appear in the test report. // // Note that calling panic(nil) in fn body is always detected as a // panic. [runtime] package says: before Go 1.21, programs that called // panic(nil) observed recover returning nil. Starting in Go 1.21, // programs that call panic(nil) observe recover returning a // [*runtime.PanicNilError]. Programs can change back to the old // behavior by setting GODEBUG=panicnil=1. // // td.CmpNotPanic(t, func() {}) // succeeds as function does not panic // // td.CmpNotPanic(t, func() { panic("I am panicking!") }) // fails // td.CmpNotPanic(t, func() { panic(nil) }) // fails too // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. // // See also [CmpPanic]. func CmpNotPanic(t TestingT, fn func(), args ...any) bool { t.Helper() return cmpNotPanic(newContext(t), t, fn, args...) } golang-github-maxatome-go-testdeep-1.14.0/td/cmp_funcs_misc_test.go000066400000000000000000000071651454313311600253520ustar00rootroot00000000000000// Copyright (c) 2018, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td_test import ( "fmt" "testing" "github.com/maxatome/go-testdeep/td" ) func ExampleCmpTrue() { t := &testing.T{} got := true ok := td.CmpTrue(t, got, "check that got is true!") fmt.Println(ok) got = false ok = td.CmpTrue(t, got, "check that got is true!") fmt.Println(ok) // Output: // true // false } func ExampleCmpFalse() { t := &testing.T{} got := false ok := td.CmpFalse(t, got, "check that got is false!") fmt.Println(ok) got = true ok = td.CmpFalse(t, got, "check that got is false!") fmt.Println(ok) // Output: // true // false } func ExampleCmpError() { t := &testing.T{} got := fmt.Errorf("Error #%d", 42) ok := td.CmpError(t, got, "An error occurred") fmt.Println(ok) got = nil ok = td.CmpError(t, got, "An error occurred") // fails fmt.Println(ok) // Output: // true // false } func ExampleCmpNoError() { t := &testing.T{} got := fmt.Errorf("Error #%d", 42) ok := td.CmpNoError(t, got, "An error occurred") // fails fmt.Println(ok) got = nil ok = td.CmpNoError(t, got, "An error occurred") fmt.Println(ok) // Output: // false // true } func ExampleCmpPanic() { t := &testing.T{} ok := td.CmpPanic(t, func() { panic("I am panicking!") }, "I am panicking!", "Checks for panic") fmt.Println("checks exact panic() string:", ok) // Can use TestDeep operator too ok = td.CmpPanic(t, func() { panic("I am panicking!") }, td.Contains("panicking!"), "Checks for panic") fmt.Println("checks panic() sub-string:", ok) // Can detect panic(nil) // Before Go 1.21, programs that called panic(nil) observed recover // returning nil. Starting in Go 1.21, programs that call panic(nil) // observe recover returning a *PanicNilError. Programs can change // back to the old behavior by setting GODEBUG=panicnil=1. // See https://pkg.go.dev/runtime#PanicNilError ok = td.CmpPanic(t, func() { panic(nil) }, nil, "Checks for panic(nil)") fmt.Println("checks for panic(nil):", ok) // As well as structured data panic type PanicStruct struct { Error string Code int } ok = td.CmpPanic(t, func() { panic(PanicStruct{Error: "Memory violation", Code: 11}) }, PanicStruct{ Error: "Memory violation", Code: 11, }) fmt.Println("checks exact panic() struct:", ok) // or combined with TestDeep operators too ok = td.CmpPanic(t, func() { panic(PanicStruct{Error: "Memory violation", Code: 11}) }, td.Struct(PanicStruct{}, td.StructFields{ "Code": td.Between(10, 20), })) fmt.Println("checks panic() struct against TestDeep operators:", ok) // Of course, do not panic = test failure, even for expected nil // panic parameter ok = td.CmpPanic(t, func() {}, nil) fmt.Println("checks a panic occurred:", ok) // Output: // checks exact panic() string: true // checks panic() sub-string: true // checks for panic(nil): true // checks exact panic() struct: true // checks panic() struct against TestDeep operators: true // checks a panic occurred: false } func ExampleCmpNotPanic() { t := &testing.T{} ok := td.CmpNotPanic(t, func() {}) fmt.Println("checks a panic DID NOT occur:", ok) // Classic panic ok = td.CmpNotPanic(t, func() { panic("I am panicking!") }, "Hope it does not panic!") fmt.Println("still no panic?", ok) // Can detect panic(nil) ok = td.CmpNotPanic(t, func() { panic(nil) }, "Checks for panic(nil)") fmt.Println("last no panic?", ok) // Output: // checks a panic DID NOT occur: true // still no panic? false // last no panic? false } golang-github-maxatome-go-testdeep-1.14.0/td/config.go000066400000000000000000000141701454313311600225620ustar00rootroot00000000000000// Copyright (c) 2018, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td import ( "os" "strconv" "testing" "github.com/maxatome/go-testdeep/internal/anchors" "github.com/maxatome/go-testdeep/internal/ctxerr" "github.com/maxatome/go-testdeep/internal/hooks" "github.com/maxatome/go-testdeep/internal/visited" ) // ContextConfig allows to configure finely how tests failures are rendered. // // See [NewT] function to use it. type ContextConfig struct { // RootName is the string used to represent the root of got data. It // defaults to "DATA". For an HTTP response body, it could be "BODY" // for example. RootName string forkedFromCtx *ctxerr.Context // MaxErrors is the maximal number of errors to dump in case of Cmp* // failure. // // It defaults to 10 except if the environment variable // TESTDEEP_MAX_ERRORS is set. In this latter case, the // TESTDEEP_MAX_ERRORS value is converted to an int and used as is. // // Setting it to 0 has the same effect as 1: only the first error // will be dumped without the "Too many errors" error. // // Setting it to a negative number means no limit: all errors // will be dumped. MaxErrors int anchors *anchors.Info hooks *hooks.Info // FailureIsFatal allows to Fatal() (instead of Error()) when a test // fails. Using *testing.T or *testing.B instance as t.TB value, FailNow() // is called behind the scenes when Fatal() is called. See testing // documentation for details. FailureIsFatal bool // UseEqual allows to use the Equal method on got (if it exists) or // on any of its component to compare got and expected values. // // The signature of the Equal method should be: // (A) Equal(B) bool // with B assignable to A. // // See time.Time as an example of accepted Equal() method. // // See (*T).UseEqual method to only apply this property to some // specific types. UseEqual bool // BeLax allows to compare different but convertible types. If set // to false (default), got and expected types must be the same. If // set to true and expected type is convertible to got one, expected // is first converted to go type before its comparison. See CmpLax // function/method and Lax operator to set this flag without // providing a specific configuration. BeLax bool // IgnoreUnexported allows to ignore unexported struct fields. Be // careful about structs entirely composed of unexported fields // (like time.Time for example). With this flag set to true, they // are all equal. In such case it is advised to set UseEqual flag, // to use (*T).UseEqual method or to add a Cmp hook using // (*T).WithCmpHooks method. // // See (*T).IgnoreUnexported method to only apply this property to some // specific types. IgnoreUnexported bool // TestDeepInGotOK allows to accept TestDeep operator in got Cmp* // parameter. By default it is forbidden and a panic occurs, because // most of the time it is a mistake to compare (expected, got) // instead of official (got, expected). TestDeepInGotOK bool } // Equal returns true if both c and o are equal. Only public fields // are taken into account to check equality. func (c ContextConfig) Equal(o ContextConfig) bool { return c.RootName == o.RootName && c.MaxErrors == o.MaxErrors && c.FailureIsFatal == o.FailureIsFatal && c.UseEqual == o.UseEqual && c.BeLax == o.BeLax && c.IgnoreUnexported == o.IgnoreUnexported && c.TestDeepInGotOK == o.TestDeepInGotOK } // OriginalPath returns the current path when the [ContextConfig] has // been built. It always returns ContextConfig.RootName except if c // has been built by [Code] operator. See [Code] documentation for an // example of use. func (c ContextConfig) OriginalPath() string { if c.forkedFromCtx == nil { return c.RootName } return c.forkedFromCtx.Path.String() } const ( contextDefaultRootName = "DATA" contextPanicRootName = "FUNCTION" envMaxErrors = "TESTDEEP_MAX_ERRORS" ) func getMaxErrorsFromEnv() int { env := os.Getenv(envMaxErrors) if env != "" { n, err := strconv.Atoi(env) if err == nil { return n } } return 10 } // DefaultContextConfig is the default configuration used to render // tests failures. If overridden, new settings will impact all Cmp* // functions and [*T] methods (if not specifically configured.) var DefaultContextConfig = ContextConfig{ RootName: contextDefaultRootName, MaxErrors: getMaxErrorsFromEnv(), FailureIsFatal: false, UseEqual: false, BeLax: false, IgnoreUnexported: false, TestDeepInGotOK: false, } func (c *ContextConfig) sanitize() { if c.RootName == "" { c.RootName = DefaultContextConfig.RootName } if c.MaxErrors == 0 { c.MaxErrors = DefaultContextConfig.MaxErrors } } // newContext creates a new ctxerr.Context using DefaultContextConfig // configuration. func newContext(t TestingT) ctxerr.Context { if tt, ok := t.(*T); ok { return newContextWithConfig(tt, tt.Config) } tb, _ := t.(testing.TB) return newContextWithConfig(tb, DefaultContextConfig) } // newContextWithConfig creates a new ctxerr.Context using a specific // configuration. func newContextWithConfig(tb testing.TB, config ContextConfig) (ctx ctxerr.Context) { config.sanitize() ctx = ctxerr.Context{ Path: ctxerr.NewPath(config.RootName), Visited: visited.NewVisited(), MaxErrors: config.MaxErrors, Anchors: config.anchors, Hooks: config.hooks, OriginalTB: tb, FailureIsFatal: config.FailureIsFatal, UseEqual: config.UseEqual, BeLax: config.BeLax, IgnoreUnexported: config.IgnoreUnexported, TestDeepInGotOK: config.TestDeepInGotOK, } ctx.InitErrors() return } // newBooleanContext creates a new boolean ctxerr.Context. func newBooleanContext() ctxerr.Context { return ctxerr.Context{ Visited: visited.NewVisited(), BooleanError: true, UseEqual: DefaultContextConfig.UseEqual, BeLax: DefaultContextConfig.BeLax, IgnoreUnexported: DefaultContextConfig.IgnoreUnexported, TestDeepInGotOK: DefaultContextConfig.TestDeepInGotOK, } } golang-github-maxatome-go-testdeep-1.14.0/td/config_test.go000066400000000000000000000042721454313311600236230ustar00rootroot00000000000000// Copyright (c) 2018, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td import ( "os" "testing" "github.com/maxatome/go-testdeep/internal/ctxerr" "github.com/maxatome/go-testdeep/internal/test" ) func TestContext(t *testing.T) { nctx := newContext(nil) test.EqualStr(t, nctx.Path.String(), "DATA") if nctx.OriginalTB != nil { t.Error("OriginalTB should be nil") } nctx = newContext(t) test.EqualStr(t, nctx.Path.String(), "DATA") if nctxt, ok := nctx.OriginalTB.(*testing.T); test.IsTrue(t, ok, "%T", nctx.OriginalTB) { if nctxt != t { t.Errorf("OriginalTB, got=%p expected=%p", nctxt, t) } } nctx = newContext(Require(t).UseEqual().TestDeepInGotOK()) _, ok := nctx.OriginalTB.(*T) test.IsTrue(t, ok) test.IsTrue(t, nctx.FailureIsFatal) test.IsTrue(t, nctx.UseEqual) test.IsTrue(t, nctx.TestDeepInGotOK) test.EqualStr(t, nctx.Path.String(), "DATA") nctx = newBooleanContext() test.EqualStr(t, nctx.Path.String(), "") if nctx.OriginalTB != nil { t.Error("OriginalTB should be nil") } if newContextWithConfig(nil, ContextConfig{MaxErrors: -1}).CollectError(nil) != nil { t.Errorf("ctx.CollectError(nil) should return nil") } ctx := ContextConfig{} if ctx.Equal(DefaultContextConfig) { t.Errorf("Empty ContextConfig should be ≠ from DefaultContextConfig") } ctx.sanitize() if !ctx.Equal(DefaultContextConfig) { t.Errorf("Sanitized empty ContextConfig should be = to DefaultContextConfig") } ctx.RootName = "PIPO" test.EqualStr(t, ctx.OriginalPath(), "PIPO") nctx = newContext(t) nctx.Path = ctxerr.NewPath("BINGO[0].Zip") ctx.forkedFromCtx = &nctx test.EqualStr(t, ctx.OriginalPath(), "BINGO[0].Zip") } func TestGetMaxErrorsFromEnv(t *testing.T) { oldEnv, set := os.LookupEnv(envMaxErrors) defer func() { if set { os.Setenv(envMaxErrors, oldEnv) } else { os.Unsetenv(envMaxErrors) } }() os.Setenv(envMaxErrors, "") test.EqualInt(t, getMaxErrorsFromEnv(), 10) os.Setenv(envMaxErrors, "aaa") test.EqualInt(t, getMaxErrorsFromEnv(), 10) os.Setenv(envMaxErrors, "-8") test.EqualInt(t, getMaxErrorsFromEnv(), -8) } golang-github-maxatome-go-testdeep-1.14.0/td/doc.go000066400000000000000000000200531454313311600220570ustar00rootroot00000000000000// Copyright (c) 2018, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. // Package td (from [go-testdeep]) allows extremely flexible deep // comparison, it is built for testing. // // It is a go rewrite and adaptation of wonderful [Test::Deep] perl // module. // // In golang, comparing data structure is usually done using // [reflect.DeepEqual] or using a package that uses this function // behind the scene. // // This function works very well, but it is not flexible. Both // compared structures must match exactly. // // The purpose of td package is to do its best to introduce this // missing flexibility using ["operators"] when the expected value (or // one of its component) cannot be matched exactly. // // See [go-testdeep] for details. // // For easy HTTP API testing, see the [tdhttp] helper. // // For tests suites also just as easy, see [tdsuite] helper. // // # Example of use // // Imagine a function returning a struct containing a newly created // database record. The Id and the CreatedAt fields are set by the // database layer: // // type Record struct { // Id uint64 // Name string // Age int // CreatedAt time.Time // } // // func CreateRecord(name string, age int) (*Record, error) { // // Do INSERT INTO … and return newly created record or error if it failed // } // // # Using standard testing package // // To check the freshly created record contents using standard testing // package, we have to do something like that: // // import ( // "testing" // "time" // ) // // func TestCreateRecord(t *testing.T) { // before := time.Now().Truncate(time.Second) // record, err := CreateRecord() // // if err != nil { // t.Errorf("An error occurred: %s", err) // } else { // expected := Record{Name: "Bob", Age: 23} // // if record.Id == 0 { // t.Error("Id probably not initialized") // } // if before.After(record.CreatedAt) || // time.Now().Before(record.CreatedAt) { // t.Errorf("CreatedAt field not expected: %s", record.CreatedAt) // } // if record.Name != expected.Name { // t.Errorf("Name field differs, got=%s, expected=%s", // record.Name, expected.Name) // } // if record.Age != expected.Age { // t.Errorf("Age field differs, got=%s, expected=%s", // record.Age, expected.Age) // } // } // } // // # Using basic go-testdeep approach // // td package, via its Cmp* functions, handles the tests and all the // error message boiler plate. Let's do it: // // import ( // "testing" // "time" // // "github.com/maxatome/go-testdeep/td" // ) // // func TestCreateRecord(t *testing.T) { // before := time.Now().Truncate(time.Second) // record, err := CreateRecord() // // if td.CmpNoError(t, err) { // td.Cmp(t, record.Id, td.NotZero(), "Id initialized") // td.Cmp(t, record.Name, "Bob") // td.Cmp(t, record.Age, 23) // td.Cmp(t, record.CreatedAt, td.Between(before, time.Now())) // } // } // // As we cannot guess the Id field value before its creation, we use // the [NotZero] operator to check it is set by CreateRecord() // call. The same it true for the creation date field // CreatedAt. Thanks to the [Between] operator we can check it is set // with a value included between the date before CreateRecord() call // and the date just after. // // Note that if Id and CreateAt could be known in advance, we could // simply do: // // import ( // "testing" // "time" // // "github.com/maxatome/go-testdeep/td" // ) // // func TestCreateRecord(t *testing.T) { // before := time.Now().Truncate(time.Second) // record, err := CreateRecord() // // if td.CmpNoError(t, err) { // td.Cmp(t, record, &Record{ // Id: 1234, // Name: "Bob", // Age: 23, // CreatedAt: time.Date(2019, time.May, 1, 12, 13, 14, 0, time.UTC), // }) // } // } // // But unfortunately, it is common to not know exactly the value of some // fields… // // # Using advanced go-testdeep technique // // Of course we can test struct fields one by one, but with go-testdeep, // the whole struct can be compared with one [Cmp] call. // // import ( // "testing" // "time" // // "github.com/maxatome/go-testdeep/td" // ) // // func TestCreateRecord(t *testing.T) { // before := time.Now().Truncate(time.Second) // record, err := CreateRecord() // // if td.CmpNoError(t, err) { // td.Cmp(t, record, // td.Struct( // &Record{ // Name: "Bob", // Age: 23, // }, // td.StructFields{ // "Id": td.NotZero(), // "CreatedAt": td.Between(before, time.Now()), // }), // "Newly created record") // } // } // // See the use of the [Struct] operator. It is needed here to overcome // the go static typing system and so use other go-testdeep operators // for some fields, here [NotZero] and [Between]. // // Not only structs can be compared. A lot of ["operators"] can be found // below to cover most (all?) needed tests. See [TestDeep]. // // # Using go-testdeep Cmp shortcuts // // The [Cmp] function is the keystone of this package, but to make // the writing of tests even easier, the family of Cmp* functions are // provided and act as shortcuts. Using [CmpStruct] function, the // previous example can be written as: // // import ( // "testing" // "time" // // "github.com/maxatome/go-testdeep/td" // ) // // func TestCreateRecord(t *testing.T) { // before := time.Now().Truncate(time.Second) // record, err := CreateRecord() // // if td.CmpNoError(t, err) { // td.CmpStruct(t, record, // &Record{ // Name: "Bob", // Age: 23, // }, // td.StructFields{ // "Id": td.NotZero(), // "CreatedAt": td.Between(before, time.Now()), // }, // "Newly created record") // } // } // // # Using T type // // [testing.T] can be encapsulated in [T] type, simplifying again the // test: // // import ( // "testing" // "time" // // "github.com/maxatome/go-testdeep/td" // ) // // func TestCreateRecord(tt *testing.T) { // t := td.NewT(tt) // // before := time.Now().Truncate(time.Second) // record, err := CreateRecord() // // if t.CmpNoError(err) { // t.RootName("RECORD").Struct(record, // &Record{ // Name: "Bob", // Age: 23, // }, // td.StructFields{ // "Id": td.NotZero(), // "CreatedAt": td.Between(before, time.Now()), // }, // "Newly created record") // } // } // // Note the use of [T.RootName] method, it allows to name what we are // going to test, instead of the default "DATA". // // # A step further with operator anchoring // // Overcome the go static typing system using the [Struct] operator is // sometimes heavy. Especially when structs are nested, as the [Struct] // operator needs to be used for each level surrounding the level in // which an operator is involved. Operator anchoring feature has been // designed to avoid this heaviness: // // import ( // "testing" // "time" // // "github.com/maxatome/go-testdeep/td" // ) // // func TestCreateRecord(tt *testing.T) { // before := time.Now().Truncate(time.Second) // record, err := CreateRecord() // // t := td.NewT(tt) // operator anchoring needs a *td.T instance // // if t.CmpNoError(err) { // t.Cmp(record, // &Record{ // Name: "Bob", // Age: 23, // ID: t.A(td.NotZero(), uint64(0)).(uint64), // CreatedAt: t.A(td.Between(before, time.Now())).(time.Time), // }, // "Newly created record") // } // } // // See the [T.A] method (or its full name alias [T.Anchor]) // documentation for details. // // [go-testdeep]: https://go-testdeep.zetta.rocks/ // [Test::Deep]: https://metacpan.org/pod/Test::Deep // ["operators"]: https://go-testdeep.zetta.rocks/operators/ // [tdhttp]: https://pkg.go.dev/github.com/maxatome/go-testdeep/helpers/tdhttp // [tdsuite]: https://pkg.go.dev/github.com/maxatome/go-testdeep/helpers/tdsuite package td // import "github.com/maxatome/go-testdeep/td" golang-github-maxatome-go-testdeep-1.14.0/td/equal.go000066400000000000000000000305151454313311600224250ustar00rootroot00000000000000// Copyright (c) 2018, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. // // deepValueEqual function is heavily based on reflect.deepValueEqual function // licensed under the BSD-style license found in the LICENSE file in the // golang repository: https://github.com/golang/go/blob/master/LICENSE package td import ( "fmt" "reflect" "github.com/maxatome/go-testdeep/helpers/tdutil" "github.com/maxatome/go-testdeep/internal/color" "github.com/maxatome/go-testdeep/internal/ctxerr" "github.com/maxatome/go-testdeep/internal/dark" "github.com/maxatome/go-testdeep/internal/types" ) func isNilStr(isNil bool) types.RawString { if isNil { return "nil" } return "not nil" } func deepValueEqualFinal(ctx ctxerr.Context, got, expected reflect.Value) (err *ctxerr.Error) { err = deepValueEqual(ctx, got, expected) if err == nil { // Try to merge pending errors errMerge := ctx.MergeErrors() if errMerge != nil { return errMerge } } return } func deepValueEqualFinalOK(ctx ctxerr.Context, got, expected reflect.Value) bool { ctx = ctx.ResetErrors() ctx.BooleanError = true return deepValueEqualFinal(ctx, got, expected) == nil } // nilHandler is called when one of got or expected is nil (but never // both, it is caller responsibility). func nilHandler(ctx ctxerr.Context, got, expected reflect.Value) *ctxerr.Error { err := ctxerr.Error{} if expected.IsValid() { // here: !got.IsValid() if expected.Type().Implements(testDeeper) { curOperator := dark.MustGetInterface(expected).(TestDeep) if curOperator.GetLocation().IsInitialized() { ctx.CurOperator = curOperator } if curOperator.HandleInvalid() { return curOperator.Match(ctx, got) } if ctx.BooleanError { return ctxerr.BooleanError } // Special case if expected is a TestDeep operator which does // not handle invalid values: the operator is not called, but // for the user the error comes from it } else if ctx.BooleanError { return ctxerr.BooleanError } err.Expected = expected } else { // here: !expected.IsValid() && got.IsValid() switch got.Kind() { // Special case: got is a nil interface, so consider as equal // to expected nil. case reflect.Interface: if got.IsNil() { return nil } case reflect.Chan, reflect.Func, reflect.Map, reflect.Ptr, reflect.Slice: // If BeLax, it is OK: we consider typed nil is equal to (untyped) nil if ctx.BeLax && got.IsNil() { return nil } } if ctx.BooleanError { return ctxerr.BooleanError } err.Got = got } err.Message = "values differ" return ctx.CollectError(&err) } func isCustomEqual(a, b reflect.Value) (bool, bool) { aType, bType := a.Type(), b.Type() equal, ok := aType.MethodByName("Equal") if ok { ft := equal.Type if !ft.IsVariadic() && ft.NumIn() == 2 && ft.NumOut() == 1 && ft.In(0).AssignableTo(ft.In(1)) && ft.Out(0) == types.Bool && bType.AssignableTo(ft.In(1)) { return true, equal.Func.Call([]reflect.Value{a, b})[0].Bool() } } return false, false } // resolveAnchor does the same as ctx.Anchors.ResolveAnchor but checks // whether v is valid and not already a TestDeep operator first. func resolveAnchor(ctx ctxerr.Context, v reflect.Value) (reflect.Value, bool) { if !v.IsValid() || v.Type().Implements(testDeeper) { return v, false } return ctx.Anchors.ResolveAnchor(v) } func deepValueEqual(ctx ctxerr.Context, got, expected reflect.Value) (err *ctxerr.Error) { if !ctx.TestDeepInGotOK { // got must not implement testDeeper if got.IsValid() && got.Type().Implements(testDeeper) { panic(color.Bad("Found a TestDeep operator in got param, " + "can only use it in expected one!")) } } // Try to see if a TestDeep operator is anchored in expected if op, ok := resolveAnchor(ctx, expected); ok { expected = op } if !got.IsValid() || !expected.IsValid() { if got.IsValid() == expected.IsValid() { return } return nilHandler(ctx, got, expected) } // Check if a Smuggle hook matches got type if handled, e := ctx.Hooks.Smuggle(&got); handled { if e != nil { // ctx.BooleanError is always false here as hooks cannot be set globally return ctx.CollectError(&ctxerr.Error{ Message: e.Error(), Got: got, Expected: expected, }) } } // Check if a Cmp hook matches got & expected types if handled, e := ctx.Hooks.Cmp(got, expected); handled { if e == nil { return } // ctx.BooleanError is always false here as hooks cannot be set globally return ctx.CollectError(&ctxerr.Error{ Message: e.Error(), Got: got, Expected: expected, }) } // Look for an Equal() method if ctx.UseEqual || ctx.Hooks.UseEqual(got.Type()) { hasEqual, isEqual := isCustomEqual(got, expected) if hasEqual { if isEqual { return } if ctx.BooleanError { return ctxerr.BooleanError } return ctx.CollectError(&ctxerr.Error{ Message: "got.Equal(expected) failed", Got: got, Expected: expected, }) } } if got.Type() != expected.Type() { if expected.Type().Implements(testDeeper) { curOperator := dark.MustGetInterface(expected).(TestDeep) // Resolve interface if got.Kind() == reflect.Interface { got = got.Elem() if !got.IsValid() { return nilHandler(ctx, got, expected) } } if curOperator.GetLocation().IsInitialized() { ctx.CurOperator = curOperator } return curOperator.Match(ctx, got) } // expected is not a TestDeep operator if got.Type() == recvKindType || expected.Type() == recvKindType { if ctx.BooleanError { return ctxerr.BooleanError } return ctx.CollectError(&ctxerr.Error{ Message: "values differ", Got: got, Expected: expected, }) } if ctx.BeLax && types.IsConvertible(expected, got.Type()) { return deepValueEqual(ctx, got, expected.Convert(got.Type())) } // If got is an interface, try to see what is behind before failing // Used by Set/Bag Match method in such cases: // []any{123, "foo"} → Bag("foo", 123) // Interface kind -^-----^ but String-^ and ^- Int kinds if got.Kind() == reflect.Interface { return deepValueEqual(ctx, got.Elem(), expected) } if ctx.BooleanError { return ctxerr.BooleanError } return ctx.CollectError(ctxerr.TypeMismatch(got.Type(), expected.Type())) } // if ctx.Depth > 10 { panic("deepValueEqual") } // for debugging // Avoid looping forever on cyclic references if ctx.Visited.Record(got, expected) { return } switch got.Kind() { case reflect.Array: for i, l := 0, got.Len(); i < l; i++ { err = deepValueEqual(ctx.AddArrayIndex(i), got.Index(i), expected.Index(i)) if err != nil { return } } return case reflect.Slice: if got.IsNil() != expected.IsNil() { if ctx.BooleanError { return ctxerr.BooleanError } return ctx.CollectError(&ctxerr.Error{ Message: "nil slice", Got: isNilStr(got.IsNil()), Expected: isNilStr(expected.IsNil()), }) } var ( gotLen = got.Len() expectedLen = expected.Len() ) if gotLen != expectedLen { // Shortcut in boolean context if ctx.BooleanError { return ctxerr.BooleanError } } else { if got.Pointer() == expected.Pointer() { return } } var maxLen int if gotLen >= expectedLen { maxLen = expectedLen } else { maxLen = gotLen } // Special case for internal tuple type: it is clearer to read // TUPLE instead of DATA when an error occurs when using this type if got.Type() == tupleType && ctx.Path.Len() == 1 && ctx.Path.String() == contextDefaultRootName { ctx = ctx.ResetPath("TUPLE") } for i := 0; i < maxLen; i++ { err = deepValueEqual(ctx.AddArrayIndex(i), got.Index(i), expected.Index(i)) if err != nil { return } } if gotLen != expectedLen { res := tdSetResult{ Kind: itemsSetResult, // do not sort Extra/Mising here } if gotLen > expectedLen { res.Extra = make([]reflect.Value, gotLen-expectedLen) for i := expectedLen; i < gotLen; i++ { res.Extra[i-expectedLen] = got.Index(i) } } else { res.Missing = make([]reflect.Value, expectedLen-gotLen) for i := gotLen; i < expectedLen; i++ { res.Missing[i-gotLen] = expected.Index(i) } } return ctx.CollectError(&ctxerr.Error{ Message: fmt.Sprintf("comparing slices, from index #%d", maxLen), Summary: res.Summary(), }) } return case reflect.Interface: return deepValueEqual(ctx, got.Elem(), expected.Elem()) case reflect.Ptr: if got.Pointer() == expected.Pointer() { return } return deepValueEqual(ctx.AddPtr(1), got.Elem(), expected.Elem()) case reflect.Struct: sType := got.Type() ignoreUnexported := ctx.IgnoreUnexported || ctx.Hooks.IgnoreUnexported(sType) for i, n := 0, got.NumField(); i < n; i++ { field := sType.Field(i) if ignoreUnexported && field.PkgPath != "" { continue } err = deepValueEqual(ctx.AddField(field.Name), got.Field(i), expected.Field(i)) if err != nil { return } } return case reflect.Map: if got.IsNil() != expected.IsNil() { if ctx.BooleanError { return ctxerr.BooleanError } return ctx.CollectError(&ctxerr.Error{ Message: "nil map", Got: isNilStr(got.IsNil()), Expected: isNilStr(expected.IsNil()), }) } // Shortcut in boolean context if ctx.BooleanError && got.Len() != expected.Len() { return ctxerr.BooleanError } if got.Pointer() == expected.Pointer() { return } var notFoundKeys []reflect.Value foundKeys := map[any]bool{} for _, vkey := range tdutil.MapSortedKeys(expected) { gotValue := got.MapIndex(vkey) if !gotValue.IsValid() { notFoundKeys = append(notFoundKeys, vkey) continue } err = deepValueEqual(ctx.AddMapKey(vkey), gotValue, expected.MapIndex(vkey)) if err != nil { return } foundKeys[dark.MustGetInterface(vkey)] = true } if got.Len() == len(foundKeys) { if len(notFoundKeys) == 0 { return } return ctx.CollectError(&ctxerr.Error{ Message: "comparing map", Summary: (tdSetResult{ Kind: keysSetResult, Missing: notFoundKeys, Sort: true, }).Summary(), }) } if ctx.BooleanError { return ctxerr.BooleanError } // Retrieve extra keys res := tdSetResult{ Kind: keysSetResult, Missing: notFoundKeys, Extra: make([]reflect.Value, 0, got.Len()-len(foundKeys)), Sort: true, } for _, vkey := range tdutil.MapSortedKeys(got) { if !foundKeys[dark.MustGetInterface(vkey)] { res.Extra = append(res.Extra, vkey) } } return ctx.CollectError(&ctxerr.Error{ Message: "comparing map", Summary: res.Summary(), }) case reflect.Func: if got.IsNil() && expected.IsNil() { return } if ctx.BooleanError { return ctxerr.BooleanError } // Can't do better than this: return ctx.CollectError(&ctxerr.Error{ Message: "functions mismatch", Summary: ctxerr.NewSummary(""), }) default: // Normal equality suffices if dark.MustGetInterface(got) == dark.MustGetInterface(expected) { return } if ctx.BooleanError { return ctxerr.BooleanError } return ctx.CollectError(&ctxerr.Error{ Message: "values differ", Got: got, Expected: expected, }) } } func deepValueEqualOK(got, expected reflect.Value) bool { return deepValueEqualFinal(newBooleanContext(), got, expected) == nil } // EqDeeply returns true if got matches expected. expected can // be the same type as got is, or contains some [TestDeep] operators. // // got := "foobar" // td.EqDeeply(got, "foobar") // returns true // td.EqDeeply(got, td.HasPrefix("foo")) // returns true func EqDeeply(got, expected any) bool { return deepValueEqualOK(reflect.ValueOf(got), reflect.ValueOf(expected)) } // EqDeeplyError returns nil if got matches expected. expected can be // the same type as got is, or contains some [TestDeep] operators. If // got does not match expected, the returned [*ctxerr.Error] contains // the reason of the first mismatch detected. // // got := "foobar" // if err := td.EqDeeplyError(got, "foobar"); err != nil { // // … // } // if err := td.EqDeeplyError(got, td.HasPrefix("foo")); err != nil { // // … // } func EqDeeplyError(got, expected any) error { err := deepValueEqualFinal(newContext(nil), reflect.ValueOf(got), reflect.ValueOf(expected)) if err == nil { return nil } return err } golang-github-maxatome-go-testdeep-1.14.0/td/equal_examples_test.go000066400000000000000000000024331454313311600253600ustar00rootroot00000000000000// Copyright (c) 2018-2021, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td_test import ( "fmt" "github.com/maxatome/go-testdeep/td" ) func ExampleEqDeeply() { type MyStruct struct { Name string Num int Items []int } got := &MyStruct{ Name: "Foobar", Num: 12, Items: []int{4, 5, 9, 3, 8}, } if td.EqDeeply(got, td.Struct(&MyStruct{}, td.StructFields{ "Name": td.Re("^Foo"), "Num": td.Between(10, 20), "Items": td.ArrayEach(td.Between(3, 9)), })) { fmt.Println("Match!") } else { fmt.Println("NO!") } // Output: // Match! } func ExampleEqDeeplyError() { //line /testdeep/example.go:1 type MyStruct struct { Name string Num int Items []int } got := &MyStruct{ Name: "Foobar", Num: 12, Items: []int{4, 5, 9, 3, 8}, } err := td.EqDeeplyError(got, td.Struct(&MyStruct{}, td.StructFields{ "Name": td.Re("^Foo"), "Num": td.Between(10, 20), "Items": td.ArrayEach(td.Between(3, 8)), })) if err != nil { fmt.Println(err) } // Output: // DATA.Items[2]: values differ // got: 9 // expected: 3 ≤ got ≤ 8 // [under operator Between at example.go:18] } golang-github-maxatome-go-testdeep-1.14.0/td/equal_test.go000066400000000000000000000513351454313311600234670ustar00rootroot00000000000000// Copyright (c) 2018-2022, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td_test import ( "testing" "time" "github.com/maxatome/go-testdeep/internal/ctxerr" "github.com/maxatome/go-testdeep/internal/test" "github.com/maxatome/go-testdeep/td" ) type ItemPropertyKind uint8 type ItemProperty struct { name string kind ItemPropertyKind value any } // Array. func TestEqualArray(t *testing.T) { checkOK(t, [8]int{1, 2}, [8]int{1, 2}) checkError(t, [8]int{1, 2}, [8]int{1, 3}, expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA[1]"), Got: mustBe("2"), Expected: mustBe("3"), }) oldMaxErrors := td.DefaultContextConfig.MaxErrors defer func() { td.DefaultContextConfig.MaxErrors = oldMaxErrors }() t.Run("DefaultContextConfig.MaxErrors = 2", func(t *testing.T) { td.DefaultContextConfig.MaxErrors = 2 err := td.EqDeeplyError([8]int{1, 2, 3, 4}, [8]int{1, 42, 43, 44}) // First error ok := t.Run("First error", func(t *testing.T) { if err == nil { t.Errorf("An Error should have occurred") return } if !matchError(t, err.(*ctxerr.Error), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA[1]"), Got: mustBe("2"), Expected: mustBe("42"), }, false) { return } }) if !ok { return } // Second error eErr := err.(*ctxerr.Error).Next t.Run("Second error", func(t *testing.T) { if eErr == nil { t.Errorf("A second Error should have occurred") return } if !matchError(t, eErr, expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA[2]"), Got: mustBe("3"), Expected: mustBe("43"), }, false) { return } if eErr.Next != ctxerr.ErrTooManyErrors { if eErr.Next == nil { t.Error("ErrTooManyErrors should follow the 2 errors") } else { t.Errorf("Only 2 Errors should have occurred. Found 3rd: %s", eErr.Next) } return } }) }) t.Run("DefaultContextConfig.MaxErrors = -1 (aka all errors)", func(t *testing.T) { td.DefaultContextConfig.MaxErrors = -1 err := td.EqDeeplyError([8]int{1, 2, 3, 4}, [8]int{1, 42, 43, 44}) // First error ok := t.Run("First error", func(t *testing.T) { if err == nil { t.Errorf("An Error should have occurred") return } if !matchError(t, err.(*ctxerr.Error), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA[1]"), Got: mustBe("2"), Expected: mustBe("42"), }, false) { return } }) if !ok { return } // Second error eErr := err.(*ctxerr.Error).Next ok = t.Run("Second error", func(t *testing.T) { if eErr == nil { t.Errorf("A second Error should have occurred") return } if !matchError(t, eErr, expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA[2]"), Got: mustBe("3"), Expected: mustBe("43"), }, false) { return } }) if !ok { return } // Third error eErr = eErr.Next t.Run("Third error", func(t *testing.T) { if eErr == nil { t.Errorf("A third Error should have occurred") return } if !matchError(t, eErr, expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA[3]"), Got: mustBe("4"), Expected: mustBe("44"), }, false) { return } if eErr.Next != nil { t.Errorf("Only 3 Errors should have occurred") return } }) }) } // Slice. func TestEqualSlice(t *testing.T) { checkOK(t, []int{1, 2}, []int{1, 2}) // Same pointer array := [...]int{2, 1, 4, 3} checkOK(t, array[:], array[:]) checkOK(t, ([]int)(nil), ([]int)(nil)) // Same pointer, but not same len checkError(t, array[:2], array[:], expectedError{ Message: mustBe("comparing slices, from index #2"), Path: mustBe("DATA"), // Missing items are not sorted Summary: mustBe(`Missing 2 items: (4, 3)`), }) checkError(t, []int{1, 2}, []int{1, 2, 3}, expectedError{ Message: mustBe("comparing slices, from index #2"), Path: mustBe("DATA"), Summary: mustBe(`Missing item: (3)`), }) checkError(t, []int{1, 2, 3}, []int{1, 2}, expectedError{ Message: mustBe("comparing slices, from index #2"), Path: mustBe("DATA"), Summary: mustBe(`Extra item: (3)`), }) checkError(t, []int{1, 2}, ([]int)(nil), expectedError{ Message: mustBe("nil slice"), Path: mustBe("DATA"), Got: mustBe("not nil"), Expected: mustBe("nil"), }) checkError(t, ([]int)(nil), []int{1, 2}, expectedError{ Message: mustBe("nil slice"), Path: mustBe("DATA"), Got: mustBe("nil"), Expected: mustBe("not nil"), }) checkError(t, []int{1, 2}, []int{1, 3}, expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA[1]"), Got: mustBe("2"), Expected: mustBe("3"), }) } // Interface. func TestEqualInterface(t *testing.T) { checkOK(t, []any{1, "foo"}, []any{1, "foo"}) checkOK(t, []any{1, nil}, []any{1, nil}) checkError(t, []any{1, nil}, []any{1, "foo"}, expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA[1]"), Got: mustBe("nil"), Expected: mustBe(`"foo"`), }) checkError(t, []any{1, "foo"}, []any{1, nil}, expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA[1]"), Got: mustBe(`"foo"`), Expected: mustBe("nil"), }) checkError(t, []any{1, "foo"}, []any{1, 12}, expectedError{ Message: mustBe("type mismatch"), Path: mustBe("DATA[1]"), Got: mustBe("string"), Expected: mustBe("int"), }) } // Ptr. func TestEqualPtr(t *testing.T) { expected := 12 gotOK := expected gotBad := 13 checkOK(t, &gotOK, &expected) checkOK(t, &expected, &expected) // Same pointer checkError(t, &gotBad, &expected, expectedError{ Message: mustBe("values differ"), Path: mustBe("*DATA"), Got: mustBe("13"), Expected: mustBe("12"), }) } // Struct. func TestEqualStruct(t *testing.T) { checkOK(t, ItemProperty{ // got name: "foo", kind: 12, value: "bar", }, ItemProperty{ // expected name: "foo", kind: 12, value: "bar", }) checkError(t, ItemProperty{ // got name: "foo", kind: 12, value: 12, }, ItemProperty{ // expected name: "foo", kind: 12, value: "bar", }, expectedError{ Message: mustBe("type mismatch"), Path: mustBe("DATA.value"), Got: mustBe("int"), Expected: mustBe("string"), }) type SType struct { Public int private string } checkOK(t, SType{Public: 42, private: "test"}, SType{Public: 42, private: "test"}) checkError(t, SType{Public: 42, private: "test"}, SType{Public: 42}, expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA.private"), Got: mustBe(`"test"`), Expected: mustBe(`""`), }) defer func() { td.DefaultContextConfig.IgnoreUnexported = false }() td.DefaultContextConfig.IgnoreUnexported = true checkOK(t, SType{Public: 42, private: "test"}, SType{Public: 42}) // Be careful with structs containing only private fields checkOK(t, ItemProperty{ name: "foo", kind: 12, value: "bar", }, ItemProperty{}) } // Map. func TestEqualMap(t *testing.T) { checkOK(t, map[string]int{}, map[string]int{}) checkOK(t, (map[string]int)(nil), (map[string]int)(nil)) expected := map[string]int{"foo": 1, "bar": 4} checkOK(t, map[string]int{"foo": 1, "bar": 4}, expected) checkOK(t, expected, expected) // Same pointer checkError(t, map[string]int{"foo": 1, "bar": 4}, (map[string]int)(nil), expectedError{ Message: mustBe("nil map"), Path: mustBe("DATA"), Got: mustBe("not nil"), Expected: mustBe("nil"), }) checkError(t, (map[string]int)(nil), map[string]int{"foo": 1, "bar": 4}, expectedError{ Message: mustBe("nil map"), Path: mustBe("DATA"), Got: mustBe("nil"), Expected: mustBe("not nil"), }) checkError(t, map[string]int{"foo": 1, "bar": 4}, map[string]int{"foo": 1, "bar": 5}, expectedError{ Message: mustBe("values differ"), Path: mustBe(`DATA["bar"]`), Got: mustBe("4"), Expected: mustBe("5"), }) checkError(t, map[string]int{"foo": 1, "bar": 4, "test": 12}, map[string]int{"foo": 1, "bar": 4}, expectedError{ Message: mustBe("comparing map"), Path: mustBe("DATA"), Summary: mustMatch(`Extra key:[^"]+"test"`), }) checkError(t, map[string]int{"foo": 1, "bar": 4}, map[string]int{"foo": 1, "bar": 4, "test": 12}, expectedError{ Message: mustBe("comparing map"), Path: mustBe("DATA"), Summary: mustMatch(`Missing key:[^"]+"test"`), }) // Extra and missing keys are sorted checkError(t, map[string]int{"foo": 1, "bar": 4, "test1+": 12, "test2+": 13}, map[string]int{"foo": 1, "bar": 4, "test1-": 12, "test2-": 13}, expectedError{ Message: mustBe("comparing map"), Path: mustBe("DATA"), Summary: mustBe(`Missing 2 keys: ("test1-", "test2-") Extra 2 keys: ("test1+", "test2+")`), }) } // Func. func TestEqualFunc(t *testing.T) { checkOK(t, (func())(nil), (func())(nil)) checkError(t, func() {}, func() {}, expectedError{ Message: mustBe("functions mismatch"), Path: mustBe("DATA"), Summary: mustBe(""), }) } // Channel. func TestEqualChannel(t *testing.T) { var gotCh, expectedCh chan int checkOK(t, gotCh, expectedCh) // nil channels gotCh = make(chan int, 1) checkOK(t, gotCh, gotCh) // exactly the same checkError(t, gotCh, make(chan int, 1), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA"), Got: mustContain("0x"), // hexadecimal pointer Expected: mustContain("0x"), // hexadecimal pointer }) } // Others. func TestEqualOthers(t *testing.T) { type Private struct { //nolint: maligned num int num8 int8 num16 int16 num32 int32 num64 int64 numu uint numu8 uint8 numu16 uint16 numu32 uint32 numu64 uint64 numf32 float32 numf64 float64 numc64 complex64 numc128 complex128 boolean bool } checkOK(t, Private{ // got num: 1, num8: 8, num16: 16, num32: 32, num64: 64, numu: 1, numu8: 8, numu16: 16, numu32: 32, numu64: 64, numf32: 32, numf64: 64, numc64: complex(64, 1), numc128: complex(128, -1), boolean: true, }, Private{ num: 1, num8: 8, num16: 16, num32: 32, num64: 64, numu: 1, numu8: 8, numu16: 16, numu32: 32, numu64: 64, numf32: 32, numf64: 64, numc64: complex(64, 1), numc128: complex(128, -1), boolean: true, }) checkError(t, Private{num: 1}, Private{num: 2}, expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA.num"), Got: mustBe("1"), Expected: mustBe("2"), }) checkError(t, Private{num8: 1}, Private{num8: 2}, expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA.num8"), Got: mustBe("(int8) 1"), Expected: mustBe("(int8) 2"), }) checkError(t, Private{num16: 1}, Private{num16: 2}, expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA.num16"), Got: mustBe("(int16) 1"), Expected: mustBe("(int16) 2"), }) checkError(t, Private{num32: 1}, Private{num32: 2}, expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA.num32"), Got: mustBe("(int32) 1"), Expected: mustBe("(int32) 2"), }) checkError(t, Private{num64: 1}, Private{num64: 2}, expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA.num64"), Got: mustBe("(int64) 1"), Expected: mustBe("(int64) 2"), }) checkError(t, Private{numu: 1}, Private{numu: 2}, expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA.numu"), Got: mustBe("(uint) 1"), Expected: mustBe("(uint) 2"), }) checkError(t, Private{numu8: 1}, Private{numu8: 2}, expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA.numu8"), Got: mustBe("(uint8) 1"), Expected: mustBe("(uint8) 2"), }) checkError(t, Private{numu16: 1}, Private{numu16: 2}, expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA.numu16"), Got: mustBe("(uint16) 1"), Expected: mustBe("(uint16) 2"), }) checkError(t, Private{numu32: 1}, Private{numu32: 2}, expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA.numu32"), Got: mustBe("(uint32) 1"), Expected: mustBe("(uint32) 2"), }) checkError(t, Private{numu64: 1}, Private{numu64: 2}, expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA.numu64"), Got: mustBe("(uint64) 1"), Expected: mustBe("(uint64) 2"), }) checkError(t, Private{numf32: 1}, Private{numf32: 2}, expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA.numf32"), Got: mustBe("(float32) 1"), Expected: mustBe("(float32) 2"), }) checkError(t, Private{numf64: 1}, Private{numf64: 2}, expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA.numf64"), Got: mustBe("1.0"), Expected: mustBe("2.0"), }) checkError(t, Private{numc64: complex(1, 2)}, Private{numc64: complex(2, 1)}, expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA.numc64"), Got: mustBe("(complex64) (1+2i)"), Expected: mustBe("(complex64) (2+1i)"), }) checkError(t, Private{numc128: complex(1, 2)}, Private{numc128: complex(2, 1)}, expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA.numc128"), Got: mustBe("(complex128) (1+2i)"), Expected: mustBe("(complex128) (2+1i)"), }) checkError(t, Private{boolean: true}, Private{boolean: false}, expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA.boolean"), Got: mustBe("true"), Expected: mustBe("false"), }) } // Private non-copyable fields. func TestEqualReallyPrivate(t *testing.T) { type Private struct { channel chan int } ch := make(chan int, 3) checkOKOrPanicIfUnsafeDisabled(t, Private{channel: ch}, Private{channel: ch}) } func TestEqualRecursPtr(t *testing.T) { type S struct { Next *S OK bool } expected1 := &S{} expected1.Next = expected1 got := &S{} got.Next = got expected2 := &S{} expected2.Next = expected2 checkOK(t, got, expected1) checkOK(t, got, expected2) got.Next = &S{OK: true} expected1.Next = &S{OK: false} checkError(t, got, expected1, expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA.Next.OK"), Got: mustBe("true"), Expected: mustBe("false"), }) } func TestEqualRecursMap(t *testing.T) { // issue #101 gen := func() any { type S struct { Map map[int]S } m := make(map[int]S) m[1] = S{ Map: m, } return m } checkOK(t, gen(), gen()) } func TestEqualPanic(t *testing.T) { test.CheckPanic(t, func() { td.EqDeeply(td.Ignore(), td.Ignore()) }, "Found a TestDeep operator in got param, can only use it in expected one!") type tdInside struct { Operator td.TestDeep } test.CheckPanic(t, func() { td.EqDeeply(&tdInside{}, &tdInside{}) }, "Found a TestDeep operator in got param, can only use it in expected one!") t.Cleanup(func() { td.DefaultContextConfig.TestDeepInGotOK = false }) td.DefaultContextConfig.TestDeepInGotOK = true test.IsTrue(t, td.EqDeeply(td.Ignore(), td.Ignore())) test.IsTrue(t, td.EqDeeply(&tdInside{}, &tdInside{})) } type AssignableType1 struct{ x, Ignore int } func (a AssignableType1) Equal(b AssignableType1) bool { return a.x == b.x } type AssignableType2 struct{ x, Ignore int } func (a AssignableType2) Equal(b struct{ x, Ignore int }) bool { return a.x == b.x } type AssignablePtrType3 struct{ x, Ignore int } func (a *AssignablePtrType3) Equal(b *AssignablePtrType3) bool { if a == nil { return b == nil } return b != nil && a.x == b.x } type BadEqual1 int func (b BadEqual1) Equal(o ...BadEqual1) bool { return true } // IsVariadic type BadEqual2 int func (b BadEqual2) Equal() bool { return true } // NumIn() ≠ 2 type BadEqual3 int func (b BadEqual3) Equal(o BadEqual3) (int, int) { return 1, 2 } // NumOut() ≠ 1 type BadEqual4 int func (b BadEqual4) Equal(o string) int { return 1 } // !AssignableTo type BadEqual5 int func (b BadEqual5) Equal(o BadEqual5) int { return 1 } // Out=bool func TestUseEqualGlobal(t *testing.T) { defer func() { td.DefaultContextConfig.UseEqual = false }() td.DefaultContextConfig.UseEqual = true // Real case with time.Time time1 := time.Now() time2 := time1.Truncate(0) if !time1.Equal(time2) || !time2.Equal(time1) { t.Fatal("time.Equal() does not work as expected") } checkOK(t, time1, time2) checkOK(t, time2, time1) // AssignableType1 a1 := AssignableType1{x: 13, Ignore: 666} b1 := AssignableType1{x: 13, Ignore: 789} checkOK(t, a1, b1) checkOK(t, b1, a1) checkError(t, a1, AssignableType1{x: 14, Ignore: 666}, expectedError{ Message: mustBe("got.Equal(expected) failed"), Path: mustBe("DATA"), Got: mustContain("x: (int) 13,"), Expected: mustContain("x: (int) 14,"), }) bs := struct{ x, Ignore int }{x: 13, Ignore: 789} checkOK(t, a1, bs) // bs type is assignable to AssignableType1 checkError(t, bs, a1, expectedError{ Message: mustBe("type mismatch"), Path: mustBe("DATA"), Got: mustBe("struct { x int; Ignore int }"), Expected: mustBe("td_test.AssignableType1"), }) // AssignableType2 a2 := AssignableType2{x: 13, Ignore: 666} b2 := AssignableType2{x: 13, Ignore: 789} checkOK(t, a2, b2) checkOK(t, b2, a2) checkOK(t, a2, bs) // bs type is assignable to AssignableType2 checkError(t, bs, a2, expectedError{ Message: mustBe("type mismatch"), Path: mustBe("DATA"), Got: mustBe("struct { x int; Ignore int }"), Expected: mustBe("td_test.AssignableType2"), }) // AssignablePtrType3 a3 := &AssignablePtrType3{x: 13, Ignore: 666} b3 := &AssignablePtrType3{x: 13, Ignore: 789} checkOK(t, a3, b3) checkOK(t, b3, a3) checkError(t, a3, &bs, // &bs type not assignable to AssignablePtrType3 expectedError{ Message: mustBe("type mismatch"), Path: mustBe("DATA"), Got: mustBe("*td_test.AssignablePtrType3"), Expected: mustBe("*struct { x int; Ignore int }"), }) checkOK(t, (*AssignablePtrType3)(nil), (*AssignablePtrType3)(nil)) checkError(t, (*AssignablePtrType3)(nil), b3, expectedError{ Message: mustBe("got.Equal(expected) failed"), Path: mustBe("DATA"), Got: mustBe("(*td_test.AssignablePtrType3)()"), Expected: mustContain("x: (int) 13,"), }) checkError(t, b3, (*AssignablePtrType3)(nil), expectedError{ Message: mustBe("got.Equal(expected) failed"), Path: mustBe("DATA"), Got: mustContain("x: (int) 13,"), Expected: mustBe("(*td_test.AssignablePtrType3)()"), }) // (A) Equal(A) method not found checkError(t, BadEqual1(1), BadEqual1(2), expectedError{ Message: mustBe("values differ"), }) checkError(t, BadEqual2(1), BadEqual2(2), expectedError{ Message: mustBe("values differ"), }) checkError(t, BadEqual3(1), BadEqual3(2), expectedError{ Message: mustBe("values differ"), }) checkError(t, BadEqual4(1), BadEqual4(2), expectedError{ Message: mustBe("values differ"), }) checkError(t, BadEqual5(1), BadEqual5(2), expectedError{ Message: mustBe("values differ"), }) } func TestUseEqualGlobalVsAnchor(t *testing.T) { defer func() { td.DefaultContextConfig.UseEqual = false }() td.DefaultContextConfig.UseEqual = true tt := test.NewTestingTB(t.Name()) assert := td.Assert(tt) type timeAnchored struct { Time time.Time } td.CmpTrue(t, assert.Cmp( timeAnchored{Time: timeParse(t, "2022-05-31T06:00:00Z")}, timeAnchored{ Time: assert.A(td.Between( timeParse(t, "2022-05-31T00:00:00Z"), timeParse(t, "2022-05-31T12:00:00Z"), )).(time.Time), })) } func TestBeLaxGlobalt(t *testing.T) { defer func() { td.DefaultContextConfig.BeLax = false }() td.DefaultContextConfig.BeLax = true // expected float64 value first converted to int64 before comparison checkOK(t, int64(123), float64(123.56)) type MyInt int32 checkOK(t, int64(123), MyInt(123)) checkOK(t, MyInt(123), int64(123)) type gotStruct struct { name string age int } type expectedStruct struct { name string age int } checkOK(t, gotStruct{ name: "bob", age: 42, }, expectedStruct{ name: "bob", age: 42, }) checkOK(t, &gotStruct{ name: "bob", age: 42, }, &expectedStruct{ name: "bob", age: 42, }) } golang-github-maxatome-go-testdeep-1.14.0/td/equal_unsafe_test.go000066400000000000000000000015431454313311600250240ustar00rootroot00000000000000// Copyright (c) 2022, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. //go:build !js && !appengine && !safe && !disableunsafe // +build !js,!appengine,!safe,!disableunsafe package td_test import ( "testing" ) // Map, unsafe access is mandatory here. func TestEqualMapUnsafe(t *testing.T) { type key struct{ k string } type A struct{ x map[key]struct{} } checkError(t, A{x: map[key]struct{}{{k: "z"}: {}}}, A{x: map[key]struct{}{{k: "x"}: {}}}, expectedError{ Message: mustBe("comparing map"), Path: mustBe("DATA.x"), Summary: mustBe(`Missing key: ((td_test.key) { k: (string) (len=1) "x" }) Extra key: ((td_test.key) { k: (string) (len=1) "z" })`), }) } golang-github-maxatome-go-testdeep-1.14.0/td/example_cmp_test.go000066400000000000000000003074721454313311600246600ustar00rootroot00000000000000// Copyright (c) 2018-2022, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. // // DO NOT EDIT!!! AUTOMATICALLY GENERATED!!! package td_test import ( "bytes" "encoding/json" "errors" "fmt" "math" "os" "regexp" "strconv" "strings" "testing" "time" "github.com/maxatome/go-testdeep/td" ) func ExampleCmpAll() { t := &testing.T{} got := "foo/bar" // Checks got string against: // "o/b" regexp *AND* "bar" suffix *AND* exact "foo/bar" string ok := td.CmpAll(t, got, []any{td.Re("o/b"), td.HasSuffix("bar"), "foo/bar"}, "checks value %s", got) fmt.Println(ok) // Checks got string against: // "o/b" regexp *AND* "bar" suffix *AND* exact "fooX/Ybar" string ok = td.CmpAll(t, got, []any{td.Re("o/b"), td.HasSuffix("bar"), "fooX/Ybar"}, "checks value %s", got) fmt.Println(ok) // When some operators or values have to be reused and mixed between // several calls, Flatten can be used to avoid boring and // inefficient []any copies: regOps := td.Flatten([]td.TestDeep{td.Re("o/b"), td.Re(`^fo`), td.Re(`ar$`)}) ok = td.CmpAll(t, got, []any{td.HasPrefix("foo"), regOps, td.HasSuffix("bar")}, "checks all operators against value %s", got) fmt.Println(ok) // Output: // true // false // true } func ExampleCmpAny() { t := &testing.T{} got := "foo/bar" // Checks got string against: // "zip" regexp *OR* "bar" suffix ok := td.CmpAny(t, got, []any{td.Re("zip"), td.HasSuffix("bar")}, "checks value %s", got) fmt.Println(ok) // Checks got string against: // "zip" regexp *OR* "foo" suffix ok = td.CmpAny(t, got, []any{td.Re("zip"), td.HasSuffix("foo")}, "checks value %s", got) fmt.Println(ok) // When some operators or values have to be reused and mixed between // several calls, Flatten can be used to avoid boring and // inefficient []any copies: regOps := td.Flatten([]td.TestDeep{td.Re("a/c"), td.Re(`^xx`), td.Re(`ar$`)}) ok = td.CmpAny(t, got, []any{td.HasPrefix("xxx"), regOps, td.HasSuffix("zip")}, "check at least one operator matches value %s", got) fmt.Println(ok) // Output: // true // false // true } func ExampleCmpArray_array() { t := &testing.T{} got := [3]int{42, 58, 26} ok := td.CmpArray(t, got, [3]int{42}, td.ArrayEntries{1: 58, 2: td.Ignore()}, "checks array %v", got) fmt.Println("Simple array:", ok) ok = td.CmpArray(t, &got, &[3]int{42}, td.ArrayEntries{1: 58, 2: td.Ignore()}, "checks array %v", got) fmt.Println("Array pointer:", ok) ok = td.CmpArray(t, &got, (*[3]int)(nil), td.ArrayEntries{0: 42, 1: 58, 2: td.Ignore()}, "checks array %v", got) fmt.Println("Array pointer, nil model:", ok) // Output: // Simple array: true // Array pointer: true // Array pointer, nil model: true } func ExampleCmpArray_typedArray() { t := &testing.T{} type MyArray [3]int got := MyArray{42, 58, 26} ok := td.CmpArray(t, got, MyArray{42}, td.ArrayEntries{1: 58, 2: td.Ignore()}, "checks typed array %v", got) fmt.Println("Typed array:", ok) ok = td.CmpArray(t, &got, &MyArray{42}, td.ArrayEntries{1: 58, 2: td.Ignore()}, "checks pointer on typed array %v", got) fmt.Println("Pointer on a typed array:", ok) ok = td.CmpArray(t, &got, &MyArray{}, td.ArrayEntries{0: 42, 1: 58, 2: td.Ignore()}, "checks pointer on typed array %v", got) fmt.Println("Pointer on a typed array, empty model:", ok) ok = td.CmpArray(t, &got, (*MyArray)(nil), td.ArrayEntries{0: 42, 1: 58, 2: td.Ignore()}, "checks pointer on typed array %v", got) fmt.Println("Pointer on a typed array, nil model:", ok) // Output: // Typed array: true // Pointer on a typed array: true // Pointer on a typed array, empty model: true // Pointer on a typed array, nil model: true } func ExampleCmpArrayEach_array() { t := &testing.T{} got := [3]int{42, 58, 26} ok := td.CmpArrayEach(t, got, td.Between(25, 60), "checks each item of array %v is in [25 .. 60]", got) fmt.Println(ok) // Output: // true } func ExampleCmpArrayEach_typedArray() { t := &testing.T{} type MyArray [3]int got := MyArray{42, 58, 26} ok := td.CmpArrayEach(t, got, td.Between(25, 60), "checks each item of typed array %v is in [25 .. 60]", got) fmt.Println(ok) ok = td.CmpArrayEach(t, &got, td.Between(25, 60), "checks each item of typed array pointer %v is in [25 .. 60]", got) fmt.Println(ok) // Output: // true // true } func ExampleCmpArrayEach_slice() { t := &testing.T{} got := []int{42, 58, 26} ok := td.CmpArrayEach(t, got, td.Between(25, 60), "checks each item of slice %v is in [25 .. 60]", got) fmt.Println(ok) // Output: // true } func ExampleCmpArrayEach_typedSlice() { t := &testing.T{} type MySlice []int got := MySlice{42, 58, 26} ok := td.CmpArrayEach(t, got, td.Between(25, 60), "checks each item of typed slice %v is in [25 .. 60]", got) fmt.Println(ok) ok = td.CmpArrayEach(t, &got, td.Between(25, 60), "checks each item of typed slice pointer %v is in [25 .. 60]", got) fmt.Println(ok) // Output: // true // true } func ExampleCmpBag() { t := &testing.T{} got := []int{1, 3, 5, 8, 8, 1, 2} // Matches as all items are present ok := td.CmpBag(t, got, []any{1, 1, 2, 3, 5, 8, 8}, "checks all items are present, in any order") fmt.Println(ok) // Does not match as got contains 2 times 1 and 8, and these // duplicates are not expected ok = td.CmpBag(t, got, []any{1, 2, 3, 5, 8}, "checks all items are present, in any order") fmt.Println(ok) got = []int{1, 3, 5, 8, 2} // Duplicates of 1 and 8 are expected but not present in got ok = td.CmpBag(t, got, []any{1, 1, 2, 3, 5, 8, 8}, "checks all items are present, in any order") fmt.Println(ok) // Matches as all items are present ok = td.CmpBag(t, got, []any{1, 2, 3, 5, td.Gt(7)}, "checks all items are present, in any order") fmt.Println(ok) // When expected is already a non-[]any slice, it cannot be // flattened directly using expected... without copying it to a new // []any slice, then use td.Flatten! expected := []int{1, 2, 3, 5} ok = td.CmpBag(t, got, []any{td.Flatten(expected), td.Gt(7)}, "checks all expected items are present, in any order") fmt.Println(ok) // Output: // true // false // false // true // true } func ExampleCmpBetween_int() { t := &testing.T{} got := 156 ok := td.CmpBetween(t, got, 154, 156, td.BoundsInIn, "checks %v is in [154 .. 156]", got) fmt.Println(ok) // BoundsInIn is implicit ok = td.CmpBetween(t, got, 154, 156, td.BoundsInIn, "checks %v is in [154 .. 156]", got) fmt.Println(ok) ok = td.CmpBetween(t, got, 154, 156, td.BoundsInOut, "checks %v is in [154 .. 156[", got) fmt.Println(ok) ok = td.CmpBetween(t, got, 154, 156, td.BoundsOutIn, "checks %v is in ]154 .. 156]", got) fmt.Println(ok) ok = td.CmpBetween(t, got, 154, 156, td.BoundsOutOut, "checks %v is in ]154 .. 156[", got) fmt.Println(ok) // Output: // true // true // false // true // false } func ExampleCmpBetween_string() { t := &testing.T{} got := "abc" ok := td.CmpBetween(t, got, "aaa", "abc", td.BoundsInIn, `checks "%v" is in ["aaa" .. "abc"]`, got) fmt.Println(ok) // BoundsInIn is implicit ok = td.CmpBetween(t, got, "aaa", "abc", td.BoundsInIn, `checks "%v" is in ["aaa" .. "abc"]`, got) fmt.Println(ok) ok = td.CmpBetween(t, got, "aaa", "abc", td.BoundsInOut, `checks "%v" is in ["aaa" .. "abc"[`, got) fmt.Println(ok) ok = td.CmpBetween(t, got, "aaa", "abc", td.BoundsOutIn, `checks "%v" is in ]"aaa" .. "abc"]`, got) fmt.Println(ok) ok = td.CmpBetween(t, got, "aaa", "abc", td.BoundsOutOut, `checks "%v" is in ]"aaa" .. "abc"[`, got) fmt.Println(ok) // Output: // true // true // false // true // false } func ExampleCmpBetween_time() { t := &testing.T{} before := time.Now() occurredAt := time.Now() after := time.Now() ok := td.CmpBetween(t, occurredAt, before, after, td.BoundsInIn) fmt.Println("It occurred between before and after:", ok) type MyTime time.Time ok = td.CmpBetween(t, MyTime(occurredAt), MyTime(before), MyTime(after), td.BoundsInIn) fmt.Println("Same for convertible MyTime type:", ok) ok = td.CmpBetween(t, MyTime(occurredAt), before, after, td.BoundsInIn) fmt.Println("MyTime vs time.Time:", ok) ok = td.CmpBetween(t, occurredAt, before, 10*time.Second, td.BoundsInIn) fmt.Println("Using a time.Duration as TO:", ok) ok = td.CmpBetween(t, MyTime(occurredAt), MyTime(before), 10*time.Second, td.BoundsInIn) fmt.Println("Using MyTime as FROM and time.Duration as TO:", ok) // Output: // It occurred between before and after: true // Same for convertible MyTime type: true // MyTime vs time.Time: false // Using a time.Duration as TO: true // Using MyTime as FROM and time.Duration as TO: true } func ExampleCmpCap() { t := &testing.T{} got := make([]int, 0, 12) ok := td.CmpCap(t, got, 12, "checks %v capacity is 12", got) fmt.Println(ok) ok = td.CmpCap(t, got, 0, "checks %v capacity is 0", got) fmt.Println(ok) got = nil ok = td.CmpCap(t, got, 0, "checks %v capacity is 0", got) fmt.Println(ok) // Output: // true // false // true } func ExampleCmpCap_operator() { t := &testing.T{} got := make([]int, 0, 12) ok := td.CmpCap(t, got, td.Between(10, 12), "checks %v capacity is in [10 .. 12]", got) fmt.Println(ok) ok = td.CmpCap(t, got, td.Gt(10), "checks %v capacity is in [10 .. 12]", got) fmt.Println(ok) // Output: // true // true } func ExampleCmpCode() { t := &testing.T{} got := "12" ok := td.CmpCode(t, got, func(num string) bool { n, err := strconv.Atoi(num) return err == nil && n > 10 && n < 100 }, "checks string `%s` contains a number and this number is in ]10 .. 100[", got) fmt.Println(ok) // Same with failure reason ok = td.CmpCode(t, got, func(num string) (bool, string) { n, err := strconv.Atoi(num) if err != nil { return false, "not a number" } if n > 10 && n < 100 { return true, "" } return false, "not in ]10 .. 100[" }, "checks string `%s` contains a number and this number is in ]10 .. 100[", got) fmt.Println(ok) // Same with failure reason thanks to error ok = td.CmpCode(t, got, func(num string) error { n, err := strconv.Atoi(num) if err != nil { return err } if n > 10 && n < 100 { return nil } return fmt.Errorf("%d not in ]10 .. 100[", n) }, "checks string `%s` contains a number and this number is in ]10 .. 100[", got) fmt.Println(ok) // Output: // true // true // true } func ExampleCmpCode_custom() { t := &testing.T{} got := 123 ok := td.CmpCode(t, got, func(t *td.T, num int) { t.Cmp(num, 123) }) fmt.Println("with one *td.T:", ok) ok = td.CmpCode(t, got, func(assert, require *td.T, num int) { assert.Cmp(num, 123) require.Cmp(num, 123) }) fmt.Println("with assert & require *td.T:", ok) // Output: // with one *td.T: true // with assert & require *td.T: true } func ExampleCmpContains_arraySlice() { t := &testing.T{} ok := td.CmpContains(t, [...]int{11, 22, 33, 44}, 22) fmt.Println("array contains 22:", ok) ok = td.CmpContains(t, [...]int{11, 22, 33, 44}, td.Between(20, 25)) fmt.Println("array contains at least one item in [20 .. 25]:", ok) ok = td.CmpContains(t, []int{11, 22, 33, 44}, 22) fmt.Println("slice contains 22:", ok) ok = td.CmpContains(t, []int{11, 22, 33, 44}, td.Between(20, 25)) fmt.Println("slice contains at least one item in [20 .. 25]:", ok) ok = td.CmpContains(t, []int{11, 22, 33, 44}, []int{22, 33}) fmt.Println("slice contains the sub-slice [22, 33]:", ok) // Output: // array contains 22: true // array contains at least one item in [20 .. 25]: true // slice contains 22: true // slice contains at least one item in [20 .. 25]: true // slice contains the sub-slice [22, 33]: true } func ExampleCmpContains_nil() { t := &testing.T{} num := 123 got := [...]*int{&num, nil} ok := td.CmpContains(t, got, nil) fmt.Println("array contains untyped nil:", ok) ok = td.CmpContains(t, got, (*int)(nil)) fmt.Println("array contains *int nil:", ok) ok = td.CmpContains(t, got, td.Nil()) fmt.Println("array contains Nil():", ok) ok = td.CmpContains(t, got, (*byte)(nil)) fmt.Println("array contains *byte nil:", ok) // types differ: *byte ≠ *int // Output: // array contains untyped nil: true // array contains *int nil: true // array contains Nil(): true // array contains *byte nil: false } func ExampleCmpContains_map() { t := &testing.T{} ok := td.CmpContains(t, map[string]int{"foo": 11, "bar": 22, "zip": 33}, 22) fmt.Println("map contains value 22:", ok) ok = td.CmpContains(t, map[string]int{"foo": 11, "bar": 22, "zip": 33}, td.Between(20, 25)) fmt.Println("map contains at least one value in [20 .. 25]:", ok) // Output: // map contains value 22: true // map contains at least one value in [20 .. 25]: true } func ExampleCmpContains_string() { t := &testing.T{} got := "foobar" ok := td.CmpContains(t, got, "oob", "checks %s", got) fmt.Println("contains `oob` string:", ok) ok = td.CmpContains(t, got, []byte("oob"), "checks %s", got) fmt.Println("contains `oob` []byte:", ok) ok = td.CmpContains(t, got, 'b', "checks %s", got) fmt.Println("contains 'b' rune:", ok) ok = td.CmpContains(t, got, byte('a'), "checks %s", got) fmt.Println("contains 'a' byte:", ok) ok = td.CmpContains(t, got, td.Between('n', 'p'), "checks %s", got) fmt.Println("contains at least one character ['n' .. 'p']:", ok) // Output: // contains `oob` string: true // contains `oob` []byte: true // contains 'b' rune: true // contains 'a' byte: true // contains at least one character ['n' .. 'p']: true } func ExampleCmpContains_stringer() { t := &testing.T{} // bytes.Buffer implements fmt.Stringer got := bytes.NewBufferString("foobar") ok := td.CmpContains(t, got, "oob", "checks %s", got) fmt.Println("contains `oob` string:", ok) ok = td.CmpContains(t, got, 'b', "checks %s", got) fmt.Println("contains 'b' rune:", ok) ok = td.CmpContains(t, got, byte('a'), "checks %s", got) fmt.Println("contains 'a' byte:", ok) ok = td.CmpContains(t, got, td.Between('n', 'p'), "checks %s", got) fmt.Println("contains at least one character ['n' .. 'p']:", ok) // Output: // contains `oob` string: true // contains 'b' rune: true // contains 'a' byte: true // contains at least one character ['n' .. 'p']: true } func ExampleCmpContains_error() { t := &testing.T{} got := errors.New("foobar") ok := td.CmpContains(t, got, "oob", "checks %s", got) fmt.Println("contains `oob` string:", ok) ok = td.CmpContains(t, got, 'b', "checks %s", got) fmt.Println("contains 'b' rune:", ok) ok = td.CmpContains(t, got, byte('a'), "checks %s", got) fmt.Println("contains 'a' byte:", ok) ok = td.CmpContains(t, got, td.Between('n', 'p'), "checks %s", got) fmt.Println("contains at least one character ['n' .. 'p']:", ok) // Output: // contains `oob` string: true // contains 'b' rune: true // contains 'a' byte: true // contains at least one character ['n' .. 'p']: true } func ExampleCmpContainsKey() { t := &testing.T{} ok := td.CmpContainsKey(t, map[string]int{"foo": 11, "bar": 22, "zip": 33}, "foo") fmt.Println(`map contains key "foo":`, ok) ok = td.CmpContainsKey(t, map[int]bool{12: true, 24: false, 42: true, 51: false}, td.Between(40, 50)) fmt.Println("map contains at least a key in [40 .. 50]:", ok) ok = td.CmpContainsKey(t, map[string]int{"FOO": 11, "bar": 22, "zip": 33}, td.Smuggle(strings.ToLower, "foo")) fmt.Println(`map contains key "foo" without taking case into account:`, ok) // Output: // map contains key "foo": true // map contains at least a key in [40 .. 50]: true // map contains key "foo" without taking case into account: true } func ExampleCmpContainsKey_nil() { t := &testing.T{} num := 1234 got := map[*int]bool{&num: false, nil: true} ok := td.CmpContainsKey(t, got, nil) fmt.Println("map contains untyped nil key:", ok) ok = td.CmpContainsKey(t, got, (*int)(nil)) fmt.Println("map contains *int nil key:", ok) ok = td.CmpContainsKey(t, got, td.Nil()) fmt.Println("map contains Nil() key:", ok) ok = td.CmpContainsKey(t, got, (*byte)(nil)) fmt.Println("map contains *byte nil key:", ok) // types differ: *byte ≠ *int // Output: // map contains untyped nil key: true // map contains *int nil key: true // map contains Nil() key: true // map contains *byte nil key: false } func ExampleCmpEmpty() { t := &testing.T{} ok := td.CmpEmpty(t, nil) // special case: nil is considered empty fmt.Println(ok) // fails, typed nil is not empty (expect for channel, map, slice or // pointers on array, channel, map slice and strings) ok = td.CmpEmpty(t, (*int)(nil)) fmt.Println(ok) ok = td.CmpEmpty(t, "") fmt.Println(ok) // Fails as 0 is a number, so not empty. Use Zero() instead ok = td.CmpEmpty(t, 0) fmt.Println(ok) ok = td.CmpEmpty(t, (map[string]int)(nil)) fmt.Println(ok) ok = td.CmpEmpty(t, map[string]int{}) fmt.Println(ok) ok = td.CmpEmpty(t, ([]int)(nil)) fmt.Println(ok) ok = td.CmpEmpty(t, []int{}) fmt.Println(ok) ok = td.CmpEmpty(t, []int{3}) // fails, as not empty fmt.Println(ok) ok = td.CmpEmpty(t, [3]int{}) // fails, Empty() is not Zero()! fmt.Println(ok) // Output: // true // false // true // false // true // true // true // true // false // false } func ExampleCmpEmpty_pointers() { t := &testing.T{} type MySlice []int ok := td.CmpEmpty(t, MySlice{}) // Ptr() not needed fmt.Println(ok) ok = td.CmpEmpty(t, &MySlice{}) fmt.Println(ok) l1 := &MySlice{} l2 := &l1 l3 := &l2 ok = td.CmpEmpty(t, &l3) fmt.Println(ok) // Works the same for array, map, channel and string // But not for others types as: type MyStruct struct { Value int } ok = td.CmpEmpty(t, &MyStruct{}) // fails, use Zero() instead fmt.Println(ok) // Output: // true // true // true // false } func ExampleCmpErrorIs() { t := &testing.T{} err1 := fmt.Errorf("failure1") err2 := fmt.Errorf("failure2: %w", err1) err3 := fmt.Errorf("failure3: %w", err2) err := fmt.Errorf("failure4: %w", err3) ok := td.CmpErrorIs(t, err, err) fmt.Println("error is itself:", ok) ok = td.CmpErrorIs(t, err, err1) fmt.Println("error is also err1:", ok) ok = td.CmpErrorIs(t, err1, err) fmt.Println("err1 is err:", ok) // Output: // error is itself: true // error is also err1: true // err1 is err: false } func ExampleCmpFirst_classic() { t := &testing.T{} got := []int{-3, -2, -1, 0, 1, 2, 3} ok := td.CmpFirst(t, got, td.Gt(0), 1) fmt.Println("first positive number is 1:", ok) isEven := func(x int) bool { return x%2 == 0 } ok = td.CmpFirst(t, got, isEven, -2) fmt.Println("first even number is -2:", ok) ok = td.CmpFirst(t, got, isEven, td.Lt(0)) fmt.Println("first even number is < 0:", ok) ok = td.CmpFirst(t, got, isEven, td.Code(isEven)) fmt.Println("first even number is well even:", ok) // Output: // first positive number is 1: true // first even number is -2: true // first even number is < 0: true // first even number is well even: true } func ExampleCmpFirst_empty() { t := &testing.T{} ok := td.CmpFirst(t, ([]int)(nil), td.Gt(0), td.Gt(0)) fmt.Println("first in nil slice:", ok) ok = td.CmpFirst(t, []int{}, td.Gt(0), td.Gt(0)) fmt.Println("first in empty slice:", ok) ok = td.CmpFirst(t, &[]int{}, td.Gt(0), td.Gt(0)) fmt.Println("first in empty pointed slice:", ok) ok = td.CmpFirst(t, [0]int{}, td.Gt(0), td.Gt(0)) fmt.Println("first in empty array:", ok) // Output: // first in nil slice: false // first in empty slice: false // first in empty pointed slice: false // first in empty array: false } func ExampleCmpFirst_struct() { t := &testing.T{} type Person struct { Fullname string `json:"fullname"` Age int `json:"age"` } got := []*Person{ { Fullname: "Bob Foobar", Age: 42, }, { Fullname: "Alice Bingo", Age: 37, }, } ok := td.CmpFirst(t, got, td.Smuggle("Age", td.Gt(30)), td.Smuggle("Fullname", "Bob Foobar")) fmt.Println("first person.Age > 30 → Bob:", ok) ok = td.CmpFirst(t, got, td.JSONPointer("/age", td.Gt(30)), td.SuperJSONOf(`{"fullname":"Bob Foobar"}`)) fmt.Println("first person.Age > 30 → Bob, using JSON:", ok) ok = td.CmpFirst(t, got, td.JSONPointer("/age", td.Gt(30)), td.JSONPointer("/fullname", td.HasPrefix("Bob"))) fmt.Println("first person.Age > 30 → Bob, using JSONPointer:", ok) // Output: // first person.Age > 30 → Bob: true // first person.Age > 30 → Bob, using JSON: true // first person.Age > 30 → Bob, using JSONPointer: true } func ExampleCmpGrep_classic() { t := &testing.T{} got := []int{-3, -2, -1, 0, 1, 2, 3} ok := td.CmpGrep(t, got, td.Gt(0), []int{1, 2, 3}) fmt.Println("check positive numbers:", ok) isEven := func(x int) bool { return x%2 == 0 } ok = td.CmpGrep(t, got, isEven, []int{-2, 0, 2}) fmt.Println("even numbers are -2, 0 and 2:", ok) ok = td.CmpGrep(t, got, isEven, td.Set(0, 2, -2)) fmt.Println("even numbers are also 0, 2 and -2:", ok) ok = td.CmpGrep(t, got, isEven, td.ArrayEach(td.Code(isEven))) fmt.Println("even numbers are each even:", ok) // Output: // check positive numbers: true // even numbers are -2, 0 and 2: true // even numbers are also 0, 2 and -2: true // even numbers are each even: true } func ExampleCmpGrep_nil() { t := &testing.T{} var got []int ok := td.CmpGrep(t, got, td.Gt(0), ([]int)(nil)) fmt.Println("typed []int nil:", ok) ok = td.CmpGrep(t, got, td.Gt(0), ([]string)(nil)) fmt.Println("typed []string nil:", ok) ok = td.CmpGrep(t, got, td.Gt(0), td.Nil()) fmt.Println("td.Nil:", ok) ok = td.CmpGrep(t, got, td.Gt(0), []int{}) fmt.Println("empty non-nil slice:", ok) // Output: // typed []int nil: true // typed []string nil: false // td.Nil: true // empty non-nil slice: false } func ExampleCmpGrep_struct() { t := &testing.T{} type Person struct { Fullname string `json:"fullname"` Age int `json:"age"` } got := []*Person{ { Fullname: "Bob Foobar", Age: 42, }, { Fullname: "Alice Bingo", Age: 27, }, } ok := td.CmpGrep(t, got, td.Smuggle("Age", td.Gt(30)), td.All( td.Len(1), td.ArrayEach(td.Smuggle("Fullname", "Bob Foobar")), )) fmt.Println("person.Age > 30 → only Bob:", ok) ok = td.CmpGrep(t, got, td.JSONPointer("/age", td.Gt(30)), td.JSON(`[ SuperMapOf({"fullname":"Bob Foobar"}) ]`)) fmt.Println("person.Age > 30 → only Bob, using JSON:", ok) // Output: // person.Age > 30 → only Bob: true // person.Age > 30 → only Bob, using JSON: true } func ExampleCmpGt_int() { t := &testing.T{} got := 156 ok := td.CmpGt(t, got, 155, "checks %v is > 155", got) fmt.Println(ok) ok = td.CmpGt(t, got, 156, "checks %v is > 156", got) fmt.Println(ok) // Output: // true // false } func ExampleCmpGt_string() { t := &testing.T{} got := "abc" ok := td.CmpGt(t, got, "abb", `checks "%v" is > "abb"`, got) fmt.Println(ok) ok = td.CmpGt(t, got, "abc", `checks "%v" is > "abc"`, got) fmt.Println(ok) // Output: // true // false } func ExampleCmpGte_int() { t := &testing.T{} got := 156 ok := td.CmpGte(t, got, 156, "checks %v is ≥ 156", got) fmt.Println(ok) ok = td.CmpGte(t, got, 155, "checks %v is ≥ 155", got) fmt.Println(ok) ok = td.CmpGte(t, got, 157, "checks %v is ≥ 157", got) fmt.Println(ok) // Output: // true // true // false } func ExampleCmpGte_string() { t := &testing.T{} got := "abc" ok := td.CmpGte(t, got, "abc", `checks "%v" is ≥ "abc"`, got) fmt.Println(ok) ok = td.CmpGte(t, got, "abb", `checks "%v" is ≥ "abb"`, got) fmt.Println(ok) ok = td.CmpGte(t, got, "abd", `checks "%v" is ≥ "abd"`, got) fmt.Println(ok) // Output: // true // true // false } func ExampleCmpHasPrefix() { t := &testing.T{} got := "foobar" ok := td.CmpHasPrefix(t, got, "foo", "checks %s", got) fmt.Println("using string:", ok) ok = td.Cmp(t, []byte(got), td.HasPrefix("foo"), "checks %s", got) fmt.Println("using []byte:", ok) // Output: // using string: true // using []byte: true } func ExampleCmpHasPrefix_stringer() { t := &testing.T{} // bytes.Buffer implements fmt.Stringer got := bytes.NewBufferString("foobar") ok := td.CmpHasPrefix(t, got, "foo", "checks %s", got) fmt.Println(ok) // Output: // true } func ExampleCmpHasPrefix_error() { t := &testing.T{} got := errors.New("foobar") ok := td.CmpHasPrefix(t, got, "foo", "checks %s", got) fmt.Println(ok) // Output: // true } func ExampleCmpHasSuffix() { t := &testing.T{} got := "foobar" ok := td.CmpHasSuffix(t, got, "bar", "checks %s", got) fmt.Println("using string:", ok) ok = td.Cmp(t, []byte(got), td.HasSuffix("bar"), "checks %s", got) fmt.Println("using []byte:", ok) // Output: // using string: true // using []byte: true } func ExampleCmpHasSuffix_stringer() { t := &testing.T{} // bytes.Buffer implements fmt.Stringer got := bytes.NewBufferString("foobar") ok := td.CmpHasSuffix(t, got, "bar", "checks %s", got) fmt.Println(ok) // Output: // true } func ExampleCmpHasSuffix_error() { t := &testing.T{} got := errors.New("foobar") ok := td.CmpHasSuffix(t, got, "bar", "checks %s", got) fmt.Println(ok) // Output: // true } func ExampleCmpIsa() { t := &testing.T{} type TstStruct struct { Field int } got := TstStruct{Field: 1} ok := td.CmpIsa(t, got, TstStruct{}, "checks got is a TstStruct") fmt.Println(ok) ok = td.CmpIsa(t, got, &TstStruct{}, "checks got is a pointer on a TstStruct") fmt.Println(ok) ok = td.CmpIsa(t, &got, &TstStruct{}, "checks &got is a pointer on a TstStruct") fmt.Println(ok) // Output: // true // false // true } func ExampleCmpIsa_interface() { t := &testing.T{} got := bytes.NewBufferString("foobar") ok := td.CmpIsa(t, got, (*fmt.Stringer)(nil), "checks got implements fmt.Stringer interface") fmt.Println(ok) errGot := fmt.Errorf("An error #%d occurred", 123) ok = td.CmpIsa(t, errGot, (*error)(nil), "checks errGot is a *error or implements error interface") fmt.Println(ok) // As nil, is passed below, it is not an interface but nil… So it // does not match errGot = nil ok = td.CmpIsa(t, errGot, (*error)(nil), "checks errGot is a *error or implements error interface") fmt.Println(ok) // BUT if its address is passed, now it is OK as the types match ok = td.CmpIsa(t, &errGot, (*error)(nil), "checks &errGot is a *error or implements error interface") fmt.Println(ok) // Output: // true // true // false // true } func ExampleCmpJSON_basic() { t := &testing.T{} got := &struct { Fullname string `json:"fullname"` Age int `json:"age"` }{ Fullname: "Bob", Age: 42, } ok := td.CmpJSON(t, got, `{"age":42,"fullname":"Bob"}`, nil) fmt.Println("check got with age then fullname:", ok) ok = td.CmpJSON(t, got, `{"fullname":"Bob","age":42}`, nil) fmt.Println("check got with fullname then age:", ok) ok = td.CmpJSON(t, got, ` // This should be the JSON representation of a struct { // A person: "fullname": "Bob", // The name of this person "age": 42 /* The age of this person: - 42 of course - to demonstrate a multi-lines comment */ }`, nil) fmt.Println("check got with nicely formatted and commented JSON:", ok) ok = td.CmpJSON(t, got, `{"fullname":"Bob","age":42,"gender":"male"}`, nil) fmt.Println("check got with gender field:", ok) ok = td.CmpJSON(t, got, `{"fullname":"Bob"}`, nil) fmt.Println("check got with fullname only:", ok) ok = td.CmpJSON(t, true, `true`, nil) fmt.Println("check boolean got is true:", ok) ok = td.CmpJSON(t, 42, `42`, nil) fmt.Println("check numeric got is 42:", ok) got = nil ok = td.CmpJSON(t, got, `null`, nil) fmt.Println("check nil got is null:", ok) // Output: // check got with age then fullname: true // check got with fullname then age: true // check got with nicely formatted and commented JSON: true // check got with gender field: false // check got with fullname only: false // check boolean got is true: true // check numeric got is 42: true // check nil got is null: true } func ExampleCmpJSON_placeholders() { t := &testing.T{} type Person struct { Fullname string `json:"fullname"` Age int `json:"age"` Children []*Person `json:"children,omitempty"` } got := &Person{ Fullname: "Bob Foobar", Age: 42, } ok := td.CmpJSON(t, got, `{"age": $1, "fullname": $2}`, []any{42, "Bob Foobar"}) fmt.Println("check got with numeric placeholders without operators:", ok) ok = td.CmpJSON(t, got, `{"age": $1, "fullname": $2}`, []any{td.Between(40, 45), td.HasSuffix("Foobar")}) fmt.Println("check got with numeric placeholders:", ok) ok = td.CmpJSON(t, got, `{"age": "$1", "fullname": "$2"}`, []any{td.Between(40, 45), td.HasSuffix("Foobar")}) fmt.Println("check got with double-quoted numeric placeholders:", ok) ok = td.CmpJSON(t, got, `{"age": $age, "fullname": $name}`, []any{td.Tag("age", td.Between(40, 45)), td.Tag("name", td.HasSuffix("Foobar"))}) fmt.Println("check got with named placeholders:", ok) got.Children = []*Person{ {Fullname: "Alice", Age: 28}, {Fullname: "Brian", Age: 22}, } ok = td.CmpJSON(t, got, `{"age": $age, "fullname": $name, "children": $children}`, []any{td.Tag("age", td.Between(40, 45)), td.Tag("name", td.HasSuffix("Foobar")), td.Tag("children", td.Bag( &Person{Fullname: "Brian", Age: 22}, &Person{Fullname: "Alice", Age: 28}, ))}) fmt.Println("check got w/named placeholders, and children w/go structs:", ok) ok = td.CmpJSON(t, got, `{"age": Between($1, $2), "fullname": HasSuffix($suffix), "children": Len(2)}`, []any{40, 45, td.Tag("suffix", "Foobar")}) fmt.Println("check got w/num & named placeholders:", ok) // Output: // check got with numeric placeholders without operators: true // check got with numeric placeholders: true // check got with double-quoted numeric placeholders: true // check got with named placeholders: true // check got w/named placeholders, and children w/go structs: true // check got w/num & named placeholders: true } func ExampleCmpJSON_embedding() { t := &testing.T{} got := &struct { Fullname string `json:"fullname"` Age int `json:"age"` }{ Fullname: "Bob Foobar", Age: 42, } ok := td.CmpJSON(t, got, `{"age": NotZero(), "fullname": NotEmpty()}`, nil) fmt.Println("check got with simple operators:", ok) ok = td.CmpJSON(t, got, `{"age": $^NotZero, "fullname": $^NotEmpty}`, nil) fmt.Println("check got with operator shortcuts:", ok) ok = td.CmpJSON(t, got, ` { "age": Between(40, 42, "]]"), // in ]40; 42] "fullname": All( HasPrefix("Bob"), HasSuffix("bar") // ← comma is optional here ) }`, nil) fmt.Println("check got with complex operators:", ok) ok = td.CmpJSON(t, got, ` { "age": Between(40, 42, "]["), // in ]40; 42[ → 42 excluded "fullname": All( HasPrefix("Bob"), HasSuffix("bar"), ) }`, nil) fmt.Println("check got with complex operators:", ok) ok = td.CmpJSON(t, got, ` { "age": Between($1, $2, $3), // in ]40; 42] "fullname": All( HasPrefix($4), HasSuffix("bar") // ← comma is optional here ) }`, []any{40, 42, td.BoundsOutIn, "Bob"}) fmt.Println("check got with complex operators, w/placeholder args:", ok) // Output: // check got with simple operators: true // check got with operator shortcuts: true // check got with complex operators: true // check got with complex operators: false // check got with complex operators, w/placeholder args: true } func ExampleCmpJSON_rawStrings() { t := &testing.T{} type details struct { Address string `json:"address"` Car string `json:"car"` } got := &struct { Fullname string `json:"fullname"` Age int `json:"age"` Details details `json:"details"` }{ Fullname: "Foo Bar", Age: 42, Details: details{ Address: "something", Car: "Peugeot", }, } ok := td.CmpJSON(t, got, ` { "fullname": HasPrefix("Foo"), "age": Between(41, 43), "details": SuperMapOf({ "address": NotEmpty, // () are optional when no parameters "car": Any("Peugeot", "Tesla", "Jeep") // any of these }) }`, nil) fmt.Println("Original:", ok) ok = td.CmpJSON(t, got, ` { "fullname": "$^HasPrefix(\"Foo\")", "age": "$^Between(41, 43)", "details": "$^SuperMapOf({\n\"address\": NotEmpty,\n\"car\": Any(\"Peugeot\", \"Tesla\", \"Jeep\")\n})" }`, nil) fmt.Println("JSON compliant:", ok) ok = td.CmpJSON(t, got, ` { "fullname": "$^HasPrefix(\"Foo\")", "age": "$^Between(41, 43)", "details": "$^SuperMapOf({ \"address\": NotEmpty, // () are optional when no parameters \"car\": Any(\"Peugeot\", \"Tesla\", \"Jeep\") // any of these })" }`, nil) fmt.Println("JSON multilines strings:", ok) ok = td.CmpJSON(t, got, ` { "fullname": "$^HasPrefix(r)", "age": "$^Between(41, 43)", "details": "$^SuperMapOf({ r
: NotEmpty, // () are optional when no parameters r: Any(r, r, r) // any of these })" }`, nil) fmt.Println("Raw strings:", ok) // Output: // Original: true // JSON compliant: true // JSON multilines strings: true // Raw strings: true } func ExampleCmpJSON_file() { t := &testing.T{} got := &struct { Fullname string `json:"fullname"` Age int `json:"age"` Gender string `json:"gender"` }{ Fullname: "Bob Foobar", Age: 42, Gender: "male", } tmpDir, err := os.MkdirTemp("", "") if err != nil { t.Fatal(err) } defer os.RemoveAll(tmpDir) // clean up filename := tmpDir + "/test.json" if err = os.WriteFile(filename, []byte(` { "fullname": "$name", "age": "$age", "gender": "$gender" }`), 0644); err != nil { t.Fatal(err) } // OK let's test with this file ok := td.CmpJSON(t, got, filename, []any{td.Tag("name", td.HasPrefix("Bob")), td.Tag("age", td.Between(40, 45)), td.Tag("gender", td.Re(`^(male|female)\z`))}) fmt.Println("Full match from file name:", ok) // When the file is already open file, err := os.Open(filename) if err != nil { t.Fatal(err) } ok = td.CmpJSON(t, got, file, []any{td.Tag("name", td.HasPrefix("Bob")), td.Tag("age", td.Between(40, 45)), td.Tag("gender", td.Re(`^(male|female)\z`))}) fmt.Println("Full match from io.Reader:", ok) // Output: // Full match from file name: true // Full match from io.Reader: true } func ExampleCmpJSONPointer_rfc6901() { t := &testing.T{} got := json.RawMessage(` { "foo": ["bar", "baz"], "": 0, "a/b": 1, "c%d": 2, "e^f": 3, "g|h": 4, "i\\j": 5, "k\"l": 6, " ": 7, "m~n": 8 }`) expected := map[string]any{ "foo": []any{"bar", "baz"}, "": 0, "a/b": 1, "c%d": 2, "e^f": 3, "g|h": 4, `i\j`: 5, `k"l`: 6, " ": 7, "m~n": 8, } ok := td.CmpJSONPointer(t, got, "", expected) fmt.Println("Empty JSON pointer means all:", ok) ok = td.CmpJSONPointer(t, got, `/foo`, []any{"bar", "baz"}) fmt.Println("Extract `foo` key:", ok) ok = td.CmpJSONPointer(t, got, `/foo/0`, "bar") fmt.Println("First item of `foo` key slice:", ok) ok = td.CmpJSONPointer(t, got, `/`, 0) fmt.Println("Empty key:", ok) ok = td.CmpJSONPointer(t, got, `/a~1b`, 1) fmt.Println("Slash has to be escaped using `~1`:", ok) ok = td.CmpJSONPointer(t, got, `/c%d`, 2) fmt.Println("% in key:", ok) ok = td.CmpJSONPointer(t, got, `/e^f`, 3) fmt.Println("^ in key:", ok) ok = td.CmpJSONPointer(t, got, `/g|h`, 4) fmt.Println("| in key:", ok) ok = td.CmpJSONPointer(t, got, `/i\j`, 5) fmt.Println("Backslash in key:", ok) ok = td.CmpJSONPointer(t, got, `/k"l`, 6) fmt.Println("Double-quote in key:", ok) ok = td.CmpJSONPointer(t, got, `/ `, 7) fmt.Println("Space key:", ok) ok = td.CmpJSONPointer(t, got, `/m~0n`, 8) fmt.Println("Tilde has to be escaped using `~0`:", ok) // Output: // Empty JSON pointer means all: true // Extract `foo` key: true // First item of `foo` key slice: true // Empty key: true // Slash has to be escaped using `~1`: true // % in key: true // ^ in key: true // | in key: true // Backslash in key: true // Double-quote in key: true // Space key: true // Tilde has to be escaped using `~0`: true } func ExampleCmpJSONPointer_struct() { t := &testing.T{} // Without json tags, encoding/json uses public fields name type Item struct { Name string Value int64 Next *Item } got := Item{ Name: "first", Value: 1, Next: &Item{ Name: "second", Value: 2, Next: &Item{ Name: "third", Value: 3, }, }, } ok := td.CmpJSONPointer(t, got, "/Next/Next/Name", "third") fmt.Println("3rd item name is `third`:", ok) ok = td.CmpJSONPointer(t, got, "/Next/Next/Value", td.Gte(int64(3))) fmt.Println("3rd item value is greater or equal than 3:", ok) ok = td.CmpJSONPointer(t, got, "/Next", td.JSONPointer("/Next", td.JSONPointer("/Value", td.Gte(int64(3))))) fmt.Println("3rd item value is still greater or equal than 3:", ok) ok = td.CmpJSONPointer(t, got, "/Next/Next/Next/Name", td.Ignore()) fmt.Println("4th item exists and has a name:", ok) // Struct comparison work with or without pointer: &Item{…} works too ok = td.CmpJSONPointer(t, got, "/Next/Next", Item{ Name: "third", Value: 3, }) fmt.Println("3rd item full comparison:", ok) // Output: // 3rd item name is `third`: true // 3rd item value is greater or equal than 3: true // 3rd item value is still greater or equal than 3: true // 4th item exists and has a name: false // 3rd item full comparison: true } func ExampleCmpJSONPointer_has_hasnt() { t := &testing.T{} got := json.RawMessage(` { "name": "Bob", "age": 42, "children": [ { "name": "Alice", "age": 16 }, { "name": "Britt", "age": 21, "children": [ { "name": "John", "age": 1 } ] } ] }`) // Has Bob some children? ok := td.CmpJSONPointer(t, got, "/children", td.Len(td.Gt(0))) fmt.Println("Bob has at least one child:", ok) // But checking "children" exists is enough here ok = td.CmpJSONPointer(t, got, "/children/0/children", td.Ignore()) fmt.Println("Alice has children:", ok) ok = td.CmpJSONPointer(t, got, "/children/1/children", td.Ignore()) fmt.Println("Britt has children:", ok) // The reverse can be checked too ok = td.Cmp(t, got, td.Not(td.JSONPointer("/children/0/children", td.Ignore()))) fmt.Println("Alice hasn't children:", ok) ok = td.Cmp(t, got, td.Not(td.JSONPointer("/children/1/children", td.Ignore()))) fmt.Println("Britt hasn't children:", ok) // Output: // Bob has at least one child: true // Alice has children: false // Britt has children: true // Alice hasn't children: true // Britt hasn't children: false } func ExampleCmpKeys() { t := &testing.T{} got := map[string]int{"foo": 1, "bar": 2, "zip": 3} // Keys tests keys in an ordered manner ok := td.CmpKeys(t, got, []string{"bar", "foo", "zip"}) fmt.Println("All sorted keys are found:", ok) // If the expected keys are not ordered, it fails ok = td.CmpKeys(t, got, []string{"zip", "bar", "foo"}) fmt.Println("All unsorted keys are found:", ok) // To circumvent that, one can use Bag operator ok = td.CmpKeys(t, got, td.Bag("zip", "bar", "foo")) fmt.Println("All unsorted keys are found, with the help of Bag operator:", ok) // Check that each key is 3 bytes long ok = td.CmpKeys(t, got, td.ArrayEach(td.Len(3))) fmt.Println("Each key is 3 bytes long:", ok) // Output: // All sorted keys are found: true // All unsorted keys are found: false // All unsorted keys are found, with the help of Bag operator: true // Each key is 3 bytes long: true } func ExampleCmpLast_classic() { t := &testing.T{} got := []int{-3, -2, -1, 0, 1, 2, 3} ok := td.CmpLast(t, got, td.Lt(0), -1) fmt.Println("last negative number is -1:", ok) isEven := func(x int) bool { return x%2 == 0 } ok = td.CmpLast(t, got, isEven, 2) fmt.Println("last even number is 2:", ok) ok = td.CmpLast(t, got, isEven, td.Gt(0)) fmt.Println("last even number is > 0:", ok) ok = td.CmpLast(t, got, isEven, td.Code(isEven)) fmt.Println("last even number is well even:", ok) // Output: // last negative number is -1: true // last even number is 2: true // last even number is > 0: true // last even number is well even: true } func ExampleCmpLast_empty() { t := &testing.T{} ok := td.CmpLast(t, ([]int)(nil), td.Gt(0), td.Gt(0)) fmt.Println("last in nil slice:", ok) ok = td.CmpLast(t, []int{}, td.Gt(0), td.Gt(0)) fmt.Println("last in empty slice:", ok) ok = td.CmpLast(t, &[]int{}, td.Gt(0), td.Gt(0)) fmt.Println("last in empty pointed slice:", ok) ok = td.CmpLast(t, [0]int{}, td.Gt(0), td.Gt(0)) fmt.Println("last in empty array:", ok) // Output: // last in nil slice: false // last in empty slice: false // last in empty pointed slice: false // last in empty array: false } func ExampleCmpLast_struct() { t := &testing.T{} type Person struct { Fullname string `json:"fullname"` Age int `json:"age"` } got := []*Person{ { Fullname: "Bob Foobar", Age: 42, }, { Fullname: "Alice Bingo", Age: 37, }, } ok := td.CmpLast(t, got, td.Smuggle("Age", td.Gt(30)), td.Smuggle("Fullname", "Alice Bingo")) fmt.Println("last person.Age > 30 → Alice:", ok) ok = td.CmpLast(t, got, td.JSONPointer("/age", td.Gt(30)), td.SuperJSONOf(`{"fullname":"Alice Bingo"}`)) fmt.Println("last person.Age > 30 → Alice, using JSON:", ok) ok = td.CmpLast(t, got, td.JSONPointer("/age", td.Gt(30)), td.JSONPointer("/fullname", td.HasPrefix("Alice"))) fmt.Println("first person.Age > 30 → Alice, using JSONPointer:", ok) // Output: // last person.Age > 30 → Alice: true // last person.Age > 30 → Alice, using JSON: true // first person.Age > 30 → Alice, using JSONPointer: true } func ExampleCmpLax() { t := &testing.T{} gotInt64 := int64(1234) gotInt32 := int32(1235) type myInt uint16 gotMyInt := myInt(1236) expected := td.Between(1230, 1240) // int type here ok := td.CmpLax(t, gotInt64, expected) fmt.Println("int64 got between ints [1230 .. 1240]:", ok) ok = td.CmpLax(t, gotInt32, expected) fmt.Println("int32 got between ints [1230 .. 1240]:", ok) ok = td.CmpLax(t, gotMyInt, expected) fmt.Println("myInt got between ints [1230 .. 1240]:", ok) // Output: // int64 got between ints [1230 .. 1240]: true // int32 got between ints [1230 .. 1240]: true // myInt got between ints [1230 .. 1240]: true } func ExampleCmpLen_slice() { t := &testing.T{} got := []int{11, 22, 33} ok := td.CmpLen(t, got, 3, "checks %v len is 3", got) fmt.Println(ok) ok = td.CmpLen(t, got, 0, "checks %v len is 0", got) fmt.Println(ok) got = nil ok = td.CmpLen(t, got, 0, "checks %v len is 0", got) fmt.Println(ok) // Output: // true // false // true } func ExampleCmpLen_map() { t := &testing.T{} got := map[int]bool{11: true, 22: false, 33: false} ok := td.CmpLen(t, got, 3, "checks %v len is 3", got) fmt.Println(ok) ok = td.CmpLen(t, got, 0, "checks %v len is 0", got) fmt.Println(ok) got = nil ok = td.CmpLen(t, got, 0, "checks %v len is 0", got) fmt.Println(ok) // Output: // true // false // true } func ExampleCmpLen_operatorSlice() { t := &testing.T{} got := []int{11, 22, 33} ok := td.CmpLen(t, got, td.Between(3, 8), "checks %v len is in [3 .. 8]", got) fmt.Println(ok) ok = td.CmpLen(t, got, td.Lt(5), "checks %v len is < 5", got) fmt.Println(ok) // Output: // true // true } func ExampleCmpLen_operatorMap() { t := &testing.T{} got := map[int]bool{11: true, 22: false, 33: false} ok := td.CmpLen(t, got, td.Between(3, 8), "checks %v len is in [3 .. 8]", got) fmt.Println(ok) ok = td.CmpLen(t, got, td.Gte(3), "checks %v len is ≥ 3", got) fmt.Println(ok) // Output: // true // true } func ExampleCmpLt_int() { t := &testing.T{} got := 156 ok := td.CmpLt(t, got, 157, "checks %v is < 157", got) fmt.Println(ok) ok = td.CmpLt(t, got, 156, "checks %v is < 156", got) fmt.Println(ok) // Output: // true // false } func ExampleCmpLt_string() { t := &testing.T{} got := "abc" ok := td.CmpLt(t, got, "abd", `checks "%v" is < "abd"`, got) fmt.Println(ok) ok = td.CmpLt(t, got, "abc", `checks "%v" is < "abc"`, got) fmt.Println(ok) // Output: // true // false } func ExampleCmpLte_int() { t := &testing.T{} got := 156 ok := td.CmpLte(t, got, 156, "checks %v is ≤ 156", got) fmt.Println(ok) ok = td.CmpLte(t, got, 157, "checks %v is ≤ 157", got) fmt.Println(ok) ok = td.CmpLte(t, got, 155, "checks %v is ≤ 155", got) fmt.Println(ok) // Output: // true // true // false } func ExampleCmpLte_string() { t := &testing.T{} got := "abc" ok := td.CmpLte(t, got, "abc", `checks "%v" is ≤ "abc"`, got) fmt.Println(ok) ok = td.CmpLte(t, got, "abd", `checks "%v" is ≤ "abd"`, got) fmt.Println(ok) ok = td.CmpLte(t, got, "abb", `checks "%v" is ≤ "abb"`, got) fmt.Println(ok) // Output: // true // true // false } func ExampleCmpMap_map() { t := &testing.T{} got := map[string]int{"foo": 12, "bar": 42, "zip": 89} ok := td.CmpMap(t, got, map[string]int{"bar": 42}, td.MapEntries{"foo": td.Lt(15), "zip": td.Ignore()}, "checks map %v", got) fmt.Println(ok) ok = td.CmpMap(t, got, map[string]int{}, td.MapEntries{"bar": 42, "foo": td.Lt(15), "zip": td.Ignore()}, "checks map %v", got) fmt.Println(ok) ok = td.CmpMap(t, got, (map[string]int)(nil), td.MapEntries{"bar": 42, "foo": td.Lt(15), "zip": td.Ignore()}, "checks map %v", got) fmt.Println(ok) // Output: // true // true // true } func ExampleCmpMap_typedMap() { t := &testing.T{} type MyMap map[string]int got := MyMap{"foo": 12, "bar": 42, "zip": 89} ok := td.CmpMap(t, got, MyMap{"bar": 42}, td.MapEntries{"foo": td.Lt(15), "zip": td.Ignore()}, "checks typed map %v", got) fmt.Println(ok) ok = td.CmpMap(t, &got, &MyMap{"bar": 42}, td.MapEntries{"foo": td.Lt(15), "zip": td.Ignore()}, "checks pointer on typed map %v", got) fmt.Println(ok) ok = td.CmpMap(t, &got, &MyMap{}, td.MapEntries{"bar": 42, "foo": td.Lt(15), "zip": td.Ignore()}, "checks pointer on typed map %v", got) fmt.Println(ok) ok = td.CmpMap(t, &got, (*MyMap)(nil), td.MapEntries{"bar": 42, "foo": td.Lt(15), "zip": td.Ignore()}, "checks pointer on typed map %v", got) fmt.Println(ok) // Output: // true // true // true // true } func ExampleCmpMapEach_map() { t := &testing.T{} got := map[string]int{"foo": 12, "bar": 42, "zip": 89} ok := td.CmpMapEach(t, got, td.Between(10, 90), "checks each value of map %v is in [10 .. 90]", got) fmt.Println(ok) // Output: // true } func ExampleCmpMapEach_typedMap() { t := &testing.T{} type MyMap map[string]int got := MyMap{"foo": 12, "bar": 42, "zip": 89} ok := td.CmpMapEach(t, got, td.Between(10, 90), "checks each value of typed map %v is in [10 .. 90]", got) fmt.Println(ok) ok = td.CmpMapEach(t, &got, td.Between(10, 90), "checks each value of typed map pointer %v is in [10 .. 90]", got) fmt.Println(ok) // Output: // true // true } func ExampleCmpN() { t := &testing.T{} got := 1.12345 ok := td.CmpN(t, got, 1.1234, 0.00006, "checks %v = 1.1234 ± 0.00006", got) fmt.Println(ok) // Output: // true } func ExampleCmpNaN_float32() { t := &testing.T{} got := float32(math.NaN()) ok := td.CmpNaN(t, got, "checks %v is not-a-number", got) fmt.Println("float32(math.NaN()) is float32 not-a-number:", ok) got = 12 ok = td.CmpNaN(t, got, "checks %v is not-a-number", got) fmt.Println("float32(12) is float32 not-a-number:", ok) // Output: // float32(math.NaN()) is float32 not-a-number: true // float32(12) is float32 not-a-number: false } func ExampleCmpNaN_float64() { t := &testing.T{} got := math.NaN() ok := td.CmpNaN(t, got, "checks %v is not-a-number", got) fmt.Println("math.NaN() is not-a-number:", ok) got = 12 ok = td.CmpNaN(t, got, "checks %v is not-a-number", got) fmt.Println("float64(12) is not-a-number:", ok) // math.NaN() is not-a-number: true // float64(12) is not-a-number: false } func ExampleCmpNil() { t := &testing.T{} var got fmt.Stringer // interface // nil value can be compared directly with nil, no need of Nil() here ok := td.Cmp(t, got, nil) fmt.Println(ok) // But it works with Nil() anyway ok = td.CmpNil(t, got) fmt.Println(ok) got = (*bytes.Buffer)(nil) // In the case of an interface containing a nil pointer, comparing // with nil fails, as the interface is not nil ok = td.Cmp(t, got, nil) fmt.Println(ok) // In this case Nil() succeed ok = td.CmpNil(t, got) fmt.Println(ok) // Output: // true // true // false // true } func ExampleCmpNone() { t := &testing.T{} got := 18 ok := td.CmpNone(t, got, []any{0, 10, 20, 30, td.Between(100, 199)}, "checks %v is non-null, and ≠ 10, 20 & 30, and not in [100-199]", got) fmt.Println(ok) got = 20 ok = td.CmpNone(t, got, []any{0, 10, 20, 30, td.Between(100, 199)}, "checks %v is non-null, and ≠ 10, 20 & 30, and not in [100-199]", got) fmt.Println(ok) got = 142 ok = td.CmpNone(t, got, []any{0, 10, 20, 30, td.Between(100, 199)}, "checks %v is non-null, and ≠ 10, 20 & 30, and not in [100-199]", got) fmt.Println(ok) prime := td.Flatten([]int{1, 2, 3, 5, 7, 11, 13}) even := td.Flatten([]int{2, 4, 6, 8, 10, 12, 14}) for _, got := range [...]int{9, 3, 8, 15} { ok = td.CmpNone(t, got, []any{prime, even, td.Gt(14)}, "checks %v is not prime number, nor an even number and not > 14") fmt.Printf("%d → %t\n", got, ok) } // Output: // true // false // false // 9 → true // 3 → false // 8 → false // 15 → false } func ExampleCmpNot() { t := &testing.T{} got := 42 ok := td.CmpNot(t, got, 0, "checks %v is non-null", got) fmt.Println(ok) ok = td.CmpNot(t, got, td.Between(10, 30), "checks %v is not in [10 .. 30]", got) fmt.Println(ok) got = 0 ok = td.CmpNot(t, got, 0, "checks %v is non-null", got) fmt.Println(ok) // Output: // true // true // false } func ExampleCmpNotAny() { t := &testing.T{} got := []int{4, 5, 9, 42} ok := td.CmpNotAny(t, got, []any{3, 6, 8, 41, 43}, "checks %v contains no item listed in NotAny()", got) fmt.Println(ok) ok = td.CmpNotAny(t, got, []any{3, 6, 8, 42, 43}, "checks %v contains no item listed in NotAny()", got) fmt.Println(ok) // When expected is already a non-[]any slice, it cannot be // flattened directly using notExpected... without copying it to a new // []any slice, then use td.Flatten! notExpected := []int{3, 6, 8, 41, 43} ok = td.CmpNotAny(t, got, []any{td.Flatten(notExpected)}, "checks %v contains no item listed in notExpected", got) fmt.Println(ok) // Output: // true // false // true } func ExampleCmpNotEmpty() { t := &testing.T{} ok := td.CmpNotEmpty(t, nil) // fails, as nil is considered empty fmt.Println(ok) ok = td.CmpNotEmpty(t, "foobar") fmt.Println(ok) // Fails as 0 is a number, so not empty. Use NotZero() instead ok = td.CmpNotEmpty(t, 0) fmt.Println(ok) ok = td.CmpNotEmpty(t, map[string]int{"foobar": 42}) fmt.Println(ok) ok = td.CmpNotEmpty(t, []int{1}) fmt.Println(ok) ok = td.CmpNotEmpty(t, [3]int{}) // succeeds, NotEmpty() is not NotZero()! fmt.Println(ok) // Output: // false // true // false // true // true // true } func ExampleCmpNotEmpty_pointers() { t := &testing.T{} type MySlice []int ok := td.CmpNotEmpty(t, MySlice{12}) fmt.Println(ok) ok = td.CmpNotEmpty(t, &MySlice{12}) // Ptr() not needed fmt.Println(ok) l1 := &MySlice{12} l2 := &l1 l3 := &l2 ok = td.CmpNotEmpty(t, &l3) fmt.Println(ok) // Works the same for array, map, channel and string // But not for others types as: type MyStruct struct { Value int } ok = td.CmpNotEmpty(t, &MyStruct{}) // fails, use NotZero() instead fmt.Println(ok) // Output: // true // true // true // false } func ExampleCmpNotNaN_float32() { t := &testing.T{} got := float32(math.NaN()) ok := td.CmpNotNaN(t, got, "checks %v is not-a-number", got) fmt.Println("float32(math.NaN()) is NOT float32 not-a-number:", ok) got = 12 ok = td.CmpNotNaN(t, got, "checks %v is not-a-number", got) fmt.Println("float32(12) is NOT float32 not-a-number:", ok) // Output: // float32(math.NaN()) is NOT float32 not-a-number: false // float32(12) is NOT float32 not-a-number: true } func ExampleCmpNotNaN_float64() { t := &testing.T{} got := math.NaN() ok := td.CmpNotNaN(t, got, "checks %v is not-a-number", got) fmt.Println("math.NaN() is not-a-number:", ok) got = 12 ok = td.CmpNotNaN(t, got, "checks %v is not-a-number", got) fmt.Println("float64(12) is not-a-number:", ok) // math.NaN() is NOT not-a-number: false // float64(12) is NOT not-a-number: true } func ExampleCmpNotNil() { t := &testing.T{} var got fmt.Stringer = &bytes.Buffer{} // nil value can be compared directly with Not(nil), no need of NotNil() here ok := td.Cmp(t, got, td.Not(nil)) fmt.Println(ok) // But it works with NotNil() anyway ok = td.CmpNotNil(t, got) fmt.Println(ok) got = (*bytes.Buffer)(nil) // In the case of an interface containing a nil pointer, comparing // with Not(nil) succeeds, as the interface is not nil ok = td.Cmp(t, got, td.Not(nil)) fmt.Println(ok) // In this case NotNil() fails ok = td.CmpNotNil(t, got) fmt.Println(ok) // Output: // true // true // true // false } func ExampleCmpNotZero() { t := &testing.T{} ok := td.CmpNotZero(t, 0) // fails fmt.Println(ok) ok = td.CmpNotZero(t, float64(0)) // fails fmt.Println(ok) ok = td.CmpNotZero(t, 12) fmt.Println(ok) ok = td.CmpNotZero(t, (map[string]int)(nil)) // fails, as nil fmt.Println(ok) ok = td.CmpNotZero(t, map[string]int{}) // succeeds, as not nil fmt.Println(ok) ok = td.CmpNotZero(t, ([]int)(nil)) // fails, as nil fmt.Println(ok) ok = td.CmpNotZero(t, []int{}) // succeeds, as not nil fmt.Println(ok) ok = td.CmpNotZero(t, [3]int{}) // fails fmt.Println(ok) ok = td.CmpNotZero(t, [3]int{0, 1}) // succeeds, DATA[1] is not 0 fmt.Println(ok) ok = td.CmpNotZero(t, bytes.Buffer{}) // fails fmt.Println(ok) ok = td.CmpNotZero(t, &bytes.Buffer{}) // succeeds, as pointer not nil fmt.Println(ok) ok = td.Cmp(t, &bytes.Buffer{}, td.Ptr(td.NotZero())) // fails as deref by Ptr() fmt.Println(ok) // Output: // false // false // true // false // true // false // true // false // true // false // true // false } func ExampleCmpPPtr() { t := &testing.T{} num := 12 got := &num ok := td.CmpPPtr(t, &got, 12) fmt.Println(ok) ok = td.CmpPPtr(t, &got, td.Between(4, 15)) fmt.Println(ok) // Output: // true // true } func ExampleCmpPtr() { t := &testing.T{} got := 12 ok := td.CmpPtr(t, &got, 12) fmt.Println(ok) ok = td.CmpPtr(t, &got, td.Between(4, 15)) fmt.Println(ok) // Output: // true // true } func ExampleCmpRe() { t := &testing.T{} got := "foo bar" ok := td.CmpRe(t, got, "(zip|bar)$", nil, "checks value %s", got) fmt.Println(ok) got = "bar foo" ok = td.CmpRe(t, got, "(zip|bar)$", nil, "checks value %s", got) fmt.Println(ok) // Output: // true // false } func ExampleCmpRe_stringer() { t := &testing.T{} // bytes.Buffer implements fmt.Stringer got := bytes.NewBufferString("foo bar") ok := td.CmpRe(t, got, "(zip|bar)$", nil, "checks value %s", got) fmt.Println(ok) // Output: // true } func ExampleCmpRe_error() { t := &testing.T{} got := errors.New("foo bar") ok := td.CmpRe(t, got, "(zip|bar)$", nil, "checks value %s", got) fmt.Println(ok) // Output: // true } func ExampleCmpRe_capture() { t := &testing.T{} got := "foo bar biz" ok := td.CmpRe(t, got, `^(\w+) (\w+) (\w+)$`, td.Set("biz", "foo", "bar"), "checks value %s", got) fmt.Println(ok) got = "foo bar! biz" ok = td.CmpRe(t, got, `^(\w+) (\w+) (\w+)$`, td.Set("biz", "foo", "bar"), "checks value %s", got) fmt.Println(ok) // Output: // true // false } func ExampleCmpRe_compiled() { t := &testing.T{} expected := regexp.MustCompile("(zip|bar)$") got := "foo bar" ok := td.CmpRe(t, got, expected, nil, "checks value %s", got) fmt.Println(ok) got = "bar foo" ok = td.CmpRe(t, got, expected, nil, "checks value %s", got) fmt.Println(ok) // Output: // true // false } func ExampleCmpRe_compiledStringer() { t := &testing.T{} expected := regexp.MustCompile("(zip|bar)$") // bytes.Buffer implements fmt.Stringer got := bytes.NewBufferString("foo bar") ok := td.CmpRe(t, got, expected, nil, "checks value %s", got) fmt.Println(ok) // Output: // true } func ExampleCmpRe_compiledError() { t := &testing.T{} expected := regexp.MustCompile("(zip|bar)$") got := errors.New("foo bar") ok := td.CmpRe(t, got, expected, nil, "checks value %s", got) fmt.Println(ok) // Output: // true } func ExampleCmpRe_compiledCapture() { t := &testing.T{} expected := regexp.MustCompile(`^(\w+) (\w+) (\w+)$`) got := "foo bar biz" ok := td.CmpRe(t, got, expected, td.Set("biz", "foo", "bar"), "checks value %s", got) fmt.Println(ok) got = "foo bar! biz" ok = td.CmpRe(t, got, expected, td.Set("biz", "foo", "bar"), "checks value %s", got) fmt.Println(ok) // Output: // true // false } func ExampleCmpReAll_capture() { t := &testing.T{} got := "foo bar biz" ok := td.CmpReAll(t, got, `(\w+)`, td.Set("biz", "foo", "bar"), "checks value %s", got) fmt.Println(ok) // Matches, but all catured groups do not match Set got = "foo BAR biz" ok = td.CmpReAll(t, got, `(\w+)`, td.Set("biz", "foo", "bar"), "checks value %s", got) fmt.Println(ok) // Output: // true // false } func ExampleCmpReAll_captureComplex() { t := &testing.T{} got := "11 45 23 56 85 96" ok := td.CmpReAll(t, got, `(\d+)`, td.ArrayEach(td.Code(func(num string) bool { n, err := strconv.Atoi(num) return err == nil && n > 10 && n < 100 })), "checks value %s", got) fmt.Println(ok) // Matches, but 11 is not greater than 20 ok = td.CmpReAll(t, got, `(\d+)`, td.ArrayEach(td.Code(func(num string) bool { n, err := strconv.Atoi(num) return err == nil && n > 20 && n < 100 })), "checks value %s", got) fmt.Println(ok) // Output: // true // false } func ExampleCmpReAll_compiledCapture() { t := &testing.T{} expected := regexp.MustCompile(`(\w+)`) got := "foo bar biz" ok := td.CmpReAll(t, got, expected, td.Set("biz", "foo", "bar"), "checks value %s", got) fmt.Println(ok) // Matches, but all catured groups do not match Set got = "foo BAR biz" ok = td.CmpReAll(t, got, expected, td.Set("biz", "foo", "bar"), "checks value %s", got) fmt.Println(ok) // Output: // true // false } func ExampleCmpReAll_compiledCaptureComplex() { t := &testing.T{} expected := regexp.MustCompile(`(\d+)`) got := "11 45 23 56 85 96" ok := td.CmpReAll(t, got, expected, td.ArrayEach(td.Code(func(num string) bool { n, err := strconv.Atoi(num) return err == nil && n > 10 && n < 100 })), "checks value %s", got) fmt.Println(ok) // Matches, but 11 is not greater than 20 ok = td.CmpReAll(t, got, expected, td.ArrayEach(td.Code(func(num string) bool { n, err := strconv.Atoi(num) return err == nil && n > 20 && n < 100 })), "checks value %s", got) fmt.Println(ok) // Output: // true // false } func ExampleCmpRecv_basic() { t := &testing.T{} got := make(chan int, 3) ok := td.CmpRecv(t, got, td.RecvNothing, 0) fmt.Println("nothing to receive:", ok) got <- 1 got <- 2 got <- 3 close(got) ok = td.CmpRecv(t, got, 1, 0) fmt.Println("1st receive is 1:", ok) ok = td.Cmp(t, got, td.All( td.Recv(2), td.Recv(td.Between(3, 4)), td.Recv(td.RecvClosed), )) fmt.Println("next receives are 2, 3 then closed:", ok) ok = td.CmpRecv(t, got, td.RecvNothing, 0) fmt.Println("nothing to receive:", ok) // Output: // nothing to receive: true // 1st receive is 1: true // next receives are 2, 3 then closed: true // nothing to receive: false } func ExampleCmpRecv_channelPointer() { t := &testing.T{} got := make(chan int, 3) ok := td.CmpRecv(t, got, td.RecvNothing, 0) fmt.Println("nothing to receive:", ok) got <- 1 got <- 2 got <- 3 close(got) ok = td.CmpRecv(t, &got, 1, 0) fmt.Println("1st receive is 1:", ok) ok = td.Cmp(t, &got, td.All( td.Recv(2), td.Recv(td.Between(3, 4)), td.Recv(td.RecvClosed), )) fmt.Println("next receives are 2, 3 then closed:", ok) ok = td.CmpRecv(t, got, td.RecvNothing, 0) fmt.Println("nothing to receive:", ok) // Output: // nothing to receive: true // 1st receive is 1: true // next receives are 2, 3 then closed: true // nothing to receive: false } func ExampleCmpRecv_withTimeout() { t := &testing.T{} got := make(chan int, 1) tick := make(chan struct{}) go func() { // ① <-tick time.Sleep(100 * time.Millisecond) got <- 0 // ② <-tick time.Sleep(100 * time.Millisecond) got <- 1 // ③ <-tick time.Sleep(100 * time.Millisecond) close(got) }() td.CmpRecv(t, got, td.RecvNothing, 0) // ① tick <- struct{}{} ok := td.CmpRecv(t, got, td.RecvNothing, 0) fmt.Println("① RecvNothing:", ok) ok = td.CmpRecv(t, got, 0, 150*time.Millisecond) fmt.Println("① receive 0 w/150ms timeout:", ok) ok = td.CmpRecv(t, got, td.RecvNothing, 0) fmt.Println("① RecvNothing:", ok) // ② tick <- struct{}{} ok = td.CmpRecv(t, got, td.RecvNothing, 0) fmt.Println("② RecvNothing:", ok) ok = td.CmpRecv(t, got, 1, 150*time.Millisecond) fmt.Println("② receive 1 w/150ms timeout:", ok) ok = td.CmpRecv(t, got, td.RecvNothing, 0) fmt.Println("② RecvNothing:", ok) // ③ tick <- struct{}{} ok = td.CmpRecv(t, got, td.RecvNothing, 0) fmt.Println("③ RecvNothing:", ok) ok = td.CmpRecv(t, got, td.RecvClosed, 150*time.Millisecond) fmt.Println("③ check closed w/150ms timeout:", ok) // Output: // ① RecvNothing: true // ① receive 0 w/150ms timeout: true // ① RecvNothing: true // ② RecvNothing: true // ② receive 1 w/150ms timeout: true // ② RecvNothing: true // ③ RecvNothing: true // ③ check closed w/150ms timeout: true } func ExampleCmpRecv_nilChannel() { t := &testing.T{} var ch chan int ok := td.CmpRecv(t, ch, td.RecvNothing, 0) fmt.Println("nothing to receive from nil channel:", ok) ok = td.CmpRecv(t, ch, 42, 0) fmt.Println("something to receive from nil channel:", ok) ok = td.CmpRecv(t, ch, td.RecvClosed, 0) fmt.Println("is a nil channel closed:", ok) // Output: // nothing to receive from nil channel: true // something to receive from nil channel: false // is a nil channel closed: false } func ExampleCmpSet() { t := &testing.T{} got := []int{1, 3, 5, 8, 8, 1, 2} // Matches as all items are present, ignoring duplicates ok := td.CmpSet(t, got, []any{1, 2, 3, 5, 8}, "checks all items are present, in any order") fmt.Println(ok) // Duplicates are ignored in a Set ok = td.CmpSet(t, got, []any{1, 2, 2, 2, 2, 2, 3, 5, 8}, "checks all items are present, in any order") fmt.Println(ok) // Tries its best to not raise an error when a value can be matched // by several Set entries ok = td.CmpSet(t, got, []any{td.Between(1, 4), 3, td.Between(2, 10)}, "checks all items are present, in any order") fmt.Println(ok) // When expected is already a non-[]any slice, it cannot be // flattened directly using expected... without copying it to a new // []any slice, then use td.Flatten! expected := []int{1, 2, 3, 5, 8} ok = td.CmpSet(t, got, []any{td.Flatten(expected)}, "checks all expected items are present, in any order") fmt.Println(ok) // Output: // true // true // true // true } func ExampleCmpShallow() { t := &testing.T{} type MyStruct struct { Value int } data := MyStruct{Value: 12} got := &data ok := td.CmpShallow(t, got, &data, "checks pointers only, not contents") fmt.Println(ok) // Same contents, but not same pointer ok = td.CmpShallow(t, got, &MyStruct{Value: 12}, "checks pointers only, not contents") fmt.Println(ok) // Output: // true // false } func ExampleCmpShallow_slice() { t := &testing.T{} back := []int{1, 2, 3, 1, 2, 3} a := back[:3] b := back[3:] ok := td.CmpShallow(t, a, back) fmt.Println("are ≠ but share the same area:", ok) ok = td.CmpShallow(t, b, back) fmt.Println("are = but do not point to same area:", ok) // Output: // are ≠ but share the same area: true // are = but do not point to same area: false } func ExampleCmpShallow_string() { t := &testing.T{} back := "foobarfoobar" a := back[:6] b := back[6:] ok := td.CmpShallow(t, a, back) fmt.Println("are ≠ but share the same area:", ok) ok = td.CmpShallow(t, b, a) fmt.Println("are = but do not point to same area:", ok) // Output: // are ≠ but share the same area: true // are = but do not point to same area: false } func ExampleCmpSlice_slice() { t := &testing.T{} got := []int{42, 58, 26} ok := td.CmpSlice(t, got, []int{42}, td.ArrayEntries{1: 58, 2: td.Ignore()}, "checks slice %v", got) fmt.Println(ok) ok = td.CmpSlice(t, got, []int{}, td.ArrayEntries{0: 42, 1: 58, 2: td.Ignore()}, "checks slice %v", got) fmt.Println(ok) ok = td.CmpSlice(t, got, ([]int)(nil), td.ArrayEntries{0: 42, 1: 58, 2: td.Ignore()}, "checks slice %v", got) fmt.Println(ok) // Output: // true // true // true } func ExampleCmpSlice_typedSlice() { t := &testing.T{} type MySlice []int got := MySlice{42, 58, 26} ok := td.CmpSlice(t, got, MySlice{42}, td.ArrayEntries{1: 58, 2: td.Ignore()}, "checks typed slice %v", got) fmt.Println(ok) ok = td.CmpSlice(t, &got, &MySlice{42}, td.ArrayEntries{1: 58, 2: td.Ignore()}, "checks pointer on typed slice %v", got) fmt.Println(ok) ok = td.CmpSlice(t, &got, &MySlice{}, td.ArrayEntries{0: 42, 1: 58, 2: td.Ignore()}, "checks pointer on typed slice %v", got) fmt.Println(ok) ok = td.CmpSlice(t, &got, (*MySlice)(nil), td.ArrayEntries{0: 42, 1: 58, 2: td.Ignore()}, "checks pointer on typed slice %v", got) fmt.Println(ok) // Output: // true // true // true // true } func ExampleCmpSmuggle_convert() { t := &testing.T{} got := int64(123) ok := td.CmpSmuggle(t, got, func(n int64) int { return int(n) }, 123, "checks int64 got against an int value") fmt.Println(ok) ok = td.CmpSmuggle(t, "123", func(numStr string) (int, bool) { n, err := strconv.Atoi(numStr) return n, err == nil }, td.Between(120, 130), "checks that number in %#v is in [120 .. 130]") fmt.Println(ok) ok = td.CmpSmuggle(t, "123", func(numStr string) (int, bool, string) { n, err := strconv.Atoi(numStr) if err != nil { return 0, false, "string must contain a number" } return n, true, "" }, td.Between(120, 130), "checks that number in %#v is in [120 .. 130]") fmt.Println(ok) ok = td.CmpSmuggle(t, "123", func(numStr string) (int, error) { //nolint: gocritic return strconv.Atoi(numStr) }, td.Between(120, 130), "checks that number in %#v is in [120 .. 130]") fmt.Println(ok) // Short version :) ok = td.CmpSmuggle(t, "123", strconv.Atoi, td.Between(120, 130), "checks that number in %#v is in [120 .. 130]") fmt.Println(ok) // Output: // true // true // true // true // true } func ExampleCmpSmuggle_lax() { t := &testing.T{} // got is an int16 and Smuggle func input is an int64: it is OK got := int(123) ok := td.CmpSmuggle(t, got, func(n int64) uint32 { return uint32(n) }, uint32(123)) fmt.Println("got int16(123) → smuggle via int64 → uint32(123):", ok) // Output: // got int16(123) → smuggle via int64 → uint32(123): true } func ExampleCmpSmuggle_auto_unmarshal() { t := &testing.T{} // Automatically json.Unmarshal to compare got := []byte(`{"a":1,"b":2}`) ok := td.CmpSmuggle(t, got, func(b json.RawMessage) (r map[string]int, err error) { err = json.Unmarshal(b, &r) return }, map[string]int{ "a": 1, "b": 2, }) fmt.Println("JSON contents is OK:", ok) // Output: // JSON contents is OK: true } func ExampleCmpSmuggle_cast() { t := &testing.T{} // A string containing JSON got := `{ "foo": 123 }` // Automatically cast a string to a json.RawMessage so td.JSON can operate ok := td.CmpSmuggle(t, got, json.RawMessage{}, td.JSON(`{"foo":123}`)) fmt.Println("JSON contents in string is OK:", ok) // Automatically read from io.Reader to a json.RawMessage ok = td.CmpSmuggle(t, bytes.NewReader([]byte(got)), json.RawMessage{}, td.JSON(`{"foo":123}`)) fmt.Println("JSON contents just read is OK:", ok) // Output: // JSON contents in string is OK: true // JSON contents just read is OK: true } func ExampleCmpSmuggle_complex() { t := &testing.T{} // No end date but a start date and a duration type StartDuration struct { StartDate time.Time Duration time.Duration } // Checks that end date is between 17th and 19th February both at 0h // for each of these durations in hours for _, duration := range []time.Duration{48 * time.Hour, 72 * time.Hour, 96 * time.Hour} { got := StartDuration{ StartDate: time.Date(2018, time.February, 14, 12, 13, 14, 0, time.UTC), Duration: duration, } // Simplest way, but in case of Between() failure, error will be bound // to DATA, not very clear... ok := td.CmpSmuggle(t, got, func(sd StartDuration) time.Time { return sd.StartDate.Add(sd.Duration) }, td.Between( time.Date(2018, time.February, 17, 0, 0, 0, 0, time.UTC), time.Date(2018, time.February, 19, 0, 0, 0, 0, time.UTC))) fmt.Println(ok) // Name the computed value "ComputedEndDate" to render a Between() failure // more understandable, so error will be bound to DATA.ComputedEndDate ok = td.CmpSmuggle(t, got, func(sd StartDuration) td.SmuggledGot { return td.SmuggledGot{ Name: "ComputedEndDate", Got: sd.StartDate.Add(sd.Duration), } }, td.Between( time.Date(2018, time.February, 17, 0, 0, 0, 0, time.UTC), time.Date(2018, time.February, 19, 0, 0, 0, 0, time.UTC))) fmt.Println(ok) } // Output: // false // false // true // true // true // true } func ExampleCmpSmuggle_interface() { t := &testing.T{} gotTime, err := time.Parse(time.RFC3339, "2018-05-23T12:13:14Z") if err != nil { t.Fatal(err) } // Do not check the struct itself, but its stringified form ok := td.CmpSmuggle(t, gotTime, func(s fmt.Stringer) string { return s.String() }, "2018-05-23 12:13:14 +0000 UTC") fmt.Println("stringified time.Time OK:", ok) // If got does not implement the fmt.Stringer interface, it fails // without calling the Smuggle func type MyTime time.Time ok = td.CmpSmuggle(t, MyTime(gotTime), func(s fmt.Stringer) string { fmt.Println("Smuggle func called!") return s.String() }, "2018-05-23 12:13:14 +0000 UTC") fmt.Println("stringified MyTime OK:", ok) // Output: // stringified time.Time OK: true // stringified MyTime OK: false } func ExampleCmpSmuggle_field_path() { t := &testing.T{} type Body struct { Name string Value any } type Request struct { Body *Body } type Transaction struct { Request } type ValueNum struct { Num int } got := &Transaction{ Request: Request{ Body: &Body{ Name: "test", Value: &ValueNum{Num: 123}, }, }, } // Want to check whether Num is between 100 and 200? ok := td.CmpSmuggle(t, got, func(t *Transaction) (int, error) { if t.Request.Body == nil || t.Request.Body.Value == nil { return 0, errors.New("Request.Body or Request.Body.Value is nil") } if v, ok := t.Request.Body.Value.(*ValueNum); ok && v != nil { return v.Num, nil } return 0, errors.New("Request.Body.Value isn't *ValueNum or nil") }, td.Between(100, 200)) fmt.Println("check Num by hand:", ok) // Same, but automagically generated... ok = td.CmpSmuggle(t, got, "Request.Body.Value.Num", td.Between(100, 200)) fmt.Println("check Num using a fields-path:", ok) // And as Request is an anonymous field, can be simplified further // as it can be omitted ok = td.CmpSmuggle(t, got, "Body.Value.Num", td.Between(100, 200)) fmt.Println("check Num using an other fields-path:", ok) // Note that maps and array/slices are supported got.Request.Body.Value = map[string]any{ "foo": []any{ 3: map[int]string{666: "bar"}, }, } ok = td.CmpSmuggle(t, got, "Body.Value[foo][3][666]", "bar") fmt.Println("check fields-path including maps/slices:", ok) // Output: // check Num by hand: true // check Num using a fields-path: true // check Num using an other fields-path: true // check fields-path including maps/slices: true } func ExampleCmpSStruct() { t := &testing.T{} type Person struct { Name string Age int NumChildren int } got := Person{ Name: "Foobar", Age: 42, NumChildren: 0, } // NumChildren is not listed in expected fields so it must be zero ok := td.CmpSStruct(t, got, Person{Name: "Foobar"}, td.StructFields{ "Age": td.Between(40, 50), }, "checks %v is the right Person") fmt.Println("Foobar is between 40 & 50:", ok) // Model can be empty got.NumChildren = 3 ok = td.CmpSStruct(t, got, Person{}, td.StructFields{ "Name": "Foobar", "Age": td.Between(40, 50), "NumChildren": td.Not(0), }, "checks %v is the right Person") fmt.Println("Foobar has some children:", ok) // Works with pointers too ok = td.CmpSStruct(t, &got, &Person{}, td.StructFields{ "Name": "Foobar", "Age": td.Between(40, 50), "NumChildren": td.Not(0), }, "checks %v is the right Person") fmt.Println("Foobar has some children (using pointer):", ok) // Model does not need to be instanciated ok = td.CmpSStruct(t, &got, (*Person)(nil), td.StructFields{ "Name": "Foobar", "Age": td.Between(40, 50), "NumChildren": td.Not(0), }, "checks %v is the right Person") fmt.Println("Foobar has some children (using nil model):", ok) // Output: // Foobar is between 40 & 50: true // Foobar has some children: true // Foobar has some children (using pointer): true // Foobar has some children (using nil model): true } func ExampleCmpSStruct_overwrite_model() { t := &testing.T{} type Person struct { Name string Age int NumChildren int } got := Person{ Name: "Foobar", Age: 42, NumChildren: 3, } ok := td.CmpSStruct(t, got, Person{ Name: "Foobar", Age: 53, }, td.StructFields{ ">Age": td.Between(40, 50), // ">" to overwrite Age:53 in model "NumChildren": td.Gt(2), }, "checks %v is the right Person") fmt.Println("Foobar is between 40 & 50:", ok) ok = td.CmpSStruct(t, got, Person{ Name: "Foobar", Age: 53, }, td.StructFields{ "> Age": td.Between(40, 50), // same, ">" can be followed by spaces "NumChildren": td.Gt(2), }, "checks %v is the right Person") fmt.Println("Foobar is between 40 & 50:", ok) // Output: // Foobar is between 40 & 50: true // Foobar is between 40 & 50: true } func ExampleCmpSStruct_patterns() { t := &testing.T{} type Person struct { Firstname string Lastname string Surname string Nickname string CreatedAt time.Time UpdatedAt time.Time DeletedAt *time.Time id int64 secret string } now := time.Now() got := Person{ Firstname: "Maxime", Lastname: "Foo", Surname: "Max", Nickname: "max", CreatedAt: now, UpdatedAt: now, DeletedAt: nil, // not deleted yet id: 2345, secret: "5ecr3T", } ok := td.CmpSStruct(t, got, Person{Lastname: "Foo"}, td.StructFields{ `DeletedAt`: nil, `= *name`: td.Re(`^(?i)max`), // shell pattern, matches all names except Lastname as in model `=~ At\z`: td.Lte(time.Now()), // regexp, matches CreatedAt & UpdatedAt `! [A-Z]*`: td.Ignore(), // private fields }, "mix shell & regexp patterns") fmt.Println("Patterns match only remaining fields:", ok) ok = td.CmpSStruct(t, got, Person{Lastname: "Foo"}, td.StructFields{ `DeletedAt`: nil, `1 = *name`: td.Re(`^(?i)max`), // shell pattern, matches all names except Lastname as in model `2 =~ At\z`: td.Lte(time.Now()), // regexp, matches CreatedAt & UpdatedAt `3 !~ ^[A-Z]`: td.Ignore(), // private fields }, "ordered patterns") fmt.Println("Ordered patterns match only remaining fields:", ok) // Output: // Patterns match only remaining fields: true // Ordered patterns match only remaining fields: true } func ExampleCmpSStruct_lazy_model() { t := &testing.T{} got := struct { name string age int }{ name: "Foobar", age: 42, } ok := td.CmpSStruct(t, got, nil, td.StructFields{ "name": "Foobar", "age": td.Between(40, 45), }) fmt.Println("Lazy model:", ok) ok = td.CmpSStruct(t, got, nil, td.StructFields{ "name": "Foobar", "zip": 666, }) fmt.Println("Lazy model with unknown field:", ok) // Output: // Lazy model: true // Lazy model with unknown field: false } func ExampleCmpString() { t := &testing.T{} got := "foobar" ok := td.CmpString(t, got, "foobar", "checks %s", got) fmt.Println("using string:", ok) ok = td.Cmp(t, []byte(got), td.String("foobar"), "checks %s", got) fmt.Println("using []byte:", ok) // Output: // using string: true // using []byte: true } func ExampleCmpString_stringer() { t := &testing.T{} // bytes.Buffer implements fmt.Stringer got := bytes.NewBufferString("foobar") ok := td.CmpString(t, got, "foobar", "checks %s", got) fmt.Println(ok) // Output: // true } func ExampleCmpString_error() { t := &testing.T{} got := errors.New("foobar") ok := td.CmpString(t, got, "foobar", "checks %s", got) fmt.Println(ok) // Output: // true } func ExampleCmpStruct() { t := &testing.T{} type Person struct { Name string Age int NumChildren int } got := Person{ Name: "Foobar", Age: 42, NumChildren: 3, } // As NumChildren is zero in Struct() call, it is not checked ok := td.CmpStruct(t, got, Person{Name: "Foobar"}, td.StructFields{ "Age": td.Between(40, 50), }, "checks %v is the right Person") fmt.Println("Foobar is between 40 & 50:", ok) // Model can be empty ok = td.CmpStruct(t, got, Person{}, td.StructFields{ "Name": "Foobar", "Age": td.Between(40, 50), "NumChildren": td.Not(0), }, "checks %v is the right Person") fmt.Println("Foobar has some children:", ok) // Works with pointers too ok = td.CmpStruct(t, &got, &Person{}, td.StructFields{ "Name": "Foobar", "Age": td.Between(40, 50), "NumChildren": td.Not(0), }, "checks %v is the right Person") fmt.Println("Foobar has some children (using pointer):", ok) // Model does not need to be instanciated ok = td.CmpStruct(t, &got, (*Person)(nil), td.StructFields{ "Name": "Foobar", "Age": td.Between(40, 50), "NumChildren": td.Not(0), }, "checks %v is the right Person") fmt.Println("Foobar has some children (using nil model):", ok) // Output: // Foobar is between 40 & 50: true // Foobar has some children: true // Foobar has some children (using pointer): true // Foobar has some children (using nil model): true } func ExampleCmpStruct_overwrite_model() { t := &testing.T{} type Person struct { Name string Age int NumChildren int } got := Person{ Name: "Foobar", Age: 42, NumChildren: 3, } ok := td.CmpStruct(t, got, Person{ Name: "Foobar", Age: 53, }, td.StructFields{ ">Age": td.Between(40, 50), // ">" to overwrite Age:53 in model "NumChildren": td.Gt(2), }, "checks %v is the right Person") fmt.Println("Foobar is between 40 & 50:", ok) ok = td.CmpStruct(t, got, Person{ Name: "Foobar", Age: 53, }, td.StructFields{ "> Age": td.Between(40, 50), // same, ">" can be followed by spaces "NumChildren": td.Gt(2), }, "checks %v is the right Person") fmt.Println("Foobar is between 40 & 50:", ok) // Output: // Foobar is between 40 & 50: true // Foobar is between 40 & 50: true } func ExampleCmpStruct_patterns() { t := &testing.T{} type Person struct { Firstname string Lastname string Surname string Nickname string CreatedAt time.Time UpdatedAt time.Time DeletedAt *time.Time } now := time.Now() got := Person{ Firstname: "Maxime", Lastname: "Foo", Surname: "Max", Nickname: "max", CreatedAt: now, UpdatedAt: now, DeletedAt: nil, // not deleted yet } ok := td.CmpStruct(t, got, Person{Lastname: "Foo"}, td.StructFields{ `DeletedAt`: nil, `= *name`: td.Re(`^(?i)max`), // shell pattern, matches all names except Lastname as in model `=~ At\z`: td.Lte(time.Now()), // regexp, matches CreatedAt & UpdatedAt }, "mix shell & regexp patterns") fmt.Println("Patterns match only remaining fields:", ok) ok = td.CmpStruct(t, got, Person{Lastname: "Foo"}, td.StructFields{ `DeletedAt`: nil, `1 = *name`: td.Re(`^(?i)max`), // shell pattern, matches all names except Lastname as in model `2 =~ At\z`: td.Lte(time.Now()), // regexp, matches CreatedAt & UpdatedAt }, "ordered patterns") fmt.Println("Ordered patterns match only remaining fields:", ok) // Output: // Patterns match only remaining fields: true // Ordered patterns match only remaining fields: true } func ExampleCmpStruct_lazy_model() { t := &testing.T{} got := struct { name string age int }{ name: "Foobar", age: 42, } ok := td.CmpStruct(t, got, nil, td.StructFields{ "name": "Foobar", "age": td.Between(40, 45), }) fmt.Println("Lazy model:", ok) ok = td.CmpStruct(t, got, nil, td.StructFields{ "name": "Foobar", "zip": 666, }) fmt.Println("Lazy model with unknown field:", ok) // Output: // Lazy model: true // Lazy model with unknown field: false } func ExampleCmpSubBagOf() { t := &testing.T{} got := []int{1, 3, 5, 8, 8, 1, 2} ok := td.CmpSubBagOf(t, got, []any{0, 0, 1, 1, 2, 2, 3, 3, 5, 5, 8, 8, 9, 9}, "checks at least all items are present, in any order") fmt.Println(ok) // got contains one 8 too many ok = td.CmpSubBagOf(t, got, []any{0, 0, 1, 1, 2, 2, 3, 3, 5, 5, 8, 9, 9}, "checks at least all items are present, in any order") fmt.Println(ok) got = []int{1, 3, 5, 2} ok = td.CmpSubBagOf(t, got, []any{td.Between(0, 3), td.Between(0, 3), td.Between(0, 3), td.Between(0, 3), td.Gt(4), td.Gt(4)}, "checks at least all items match, in any order with TestDeep operators") fmt.Println(ok) // When expected is already a non-[]any slice, it cannot be // flattened directly using expected... without copying it to a new // []any slice, then use td.Flatten! expected := []int{1, 2, 3, 5, 9, 8} ok = td.CmpSubBagOf(t, got, []any{td.Flatten(expected)}, "checks at least all expected items are present, in any order") fmt.Println(ok) // Output: // true // false // true // true } func ExampleCmpSubJSONOf_basic() { t := &testing.T{} got := &struct { Fullname string `json:"fullname"` Age int `json:"age"` }{ Fullname: "Bob", Age: 42, } ok := td.CmpSubJSONOf(t, got, `{"age":42,"fullname":"Bob","gender":"male"}`, nil) fmt.Println("check got with age then fullname:", ok) ok = td.CmpSubJSONOf(t, got, `{"fullname":"Bob","age":42,"gender":"male"}`, nil) fmt.Println("check got with fullname then age:", ok) ok = td.CmpSubJSONOf(t, got, ` // This should be the JSON representation of a struct { // A person: "fullname": "Bob", // The name of this person "age": 42, /* The age of this person: - 42 of course - to demonstrate a multi-lines comment */ "gender": "male" // This field is ignored as SubJSONOf }`, nil) fmt.Println("check got with nicely formatted and commented JSON:", ok) ok = td.CmpSubJSONOf(t, got, `{"fullname":"Bob","gender":"male"}`, nil) fmt.Println("check got without age field:", ok) // Output: // check got with age then fullname: true // check got with fullname then age: true // check got with nicely formatted and commented JSON: true // check got without age field: false } func ExampleCmpSubJSONOf_placeholders() { t := &testing.T{} got := &struct { Fullname string `json:"fullname"` Age int `json:"age"` }{ Fullname: "Bob Foobar", Age: 42, } ok := td.CmpSubJSONOf(t, got, `{"age": $1, "fullname": $2, "gender": $3}`, []any{42, "Bob Foobar", "male"}) fmt.Println("check got with numeric placeholders without operators:", ok) ok = td.CmpSubJSONOf(t, got, `{"age": $1, "fullname": $2, "gender": $3}`, []any{td.Between(40, 45), td.HasSuffix("Foobar"), td.NotEmpty()}) fmt.Println("check got with numeric placeholders:", ok) ok = td.CmpSubJSONOf(t, got, `{"age": "$1", "fullname": "$2", "gender": "$3"}`, []any{td.Between(40, 45), td.HasSuffix("Foobar"), td.NotEmpty()}) fmt.Println("check got with double-quoted numeric placeholders:", ok) ok = td.CmpSubJSONOf(t, got, `{"age": $age, "fullname": $name, "gender": $gender}`, []any{td.Tag("age", td.Between(40, 45)), td.Tag("name", td.HasSuffix("Foobar")), td.Tag("gender", td.NotEmpty())}) fmt.Println("check got with named placeholders:", ok) ok = td.CmpSubJSONOf(t, got, `{"age": $^NotZero, "fullname": $^NotEmpty, "gender": $^NotEmpty}`, nil) fmt.Println("check got with operator shortcuts:", ok) // Output: // check got with numeric placeholders without operators: true // check got with numeric placeholders: true // check got with double-quoted numeric placeholders: true // check got with named placeholders: true // check got with operator shortcuts: true } func ExampleCmpSubJSONOf_file() { t := &testing.T{} got := &struct { Fullname string `json:"fullname"` Age int `json:"age"` Gender string `json:"gender"` }{ Fullname: "Bob Foobar", Age: 42, Gender: "male", } tmpDir, err := os.MkdirTemp("", "") if err != nil { t.Fatal(err) } defer os.RemoveAll(tmpDir) // clean up filename := tmpDir + "/test.json" if err = os.WriteFile(filename, []byte(` { "fullname": "$name", "age": "$age", "gender": "$gender", "details": { "city": "TestCity", "zip": 666 } }`), 0644); err != nil { t.Fatal(err) } // OK let's test with this file ok := td.CmpSubJSONOf(t, got, filename, []any{td.Tag("name", td.HasPrefix("Bob")), td.Tag("age", td.Between(40, 45)), td.Tag("gender", td.Re(`^(male|female)\z`))}) fmt.Println("Full match from file name:", ok) // When the file is already open file, err := os.Open(filename) if err != nil { t.Fatal(err) } ok = td.CmpSubJSONOf(t, got, file, []any{td.Tag("name", td.HasPrefix("Bob")), td.Tag("age", td.Between(40, 45)), td.Tag("gender", td.Re(`^(male|female)\z`))}) fmt.Println("Full match from io.Reader:", ok) // Output: // Full match from file name: true // Full match from io.Reader: true } func ExampleCmpSubMapOf_map() { t := &testing.T{} got := map[string]int{"foo": 12, "bar": 42} ok := td.CmpSubMapOf(t, got, map[string]int{"bar": 42}, td.MapEntries{"foo": td.Lt(15), "zip": 666}, "checks map %v is included in expected keys/values", got) fmt.Println(ok) // Output: // true } func ExampleCmpSubMapOf_typedMap() { t := &testing.T{} type MyMap map[string]int got := MyMap{"foo": 12, "bar": 42} ok := td.CmpSubMapOf(t, got, MyMap{"bar": 42}, td.MapEntries{"foo": td.Lt(15), "zip": 666}, "checks typed map %v is included in expected keys/values", got) fmt.Println(ok) ok = td.CmpSubMapOf(t, &got, &MyMap{"bar": 42}, td.MapEntries{"foo": td.Lt(15), "zip": 666}, "checks pointed typed map %v is included in expected keys/values", got) fmt.Println(ok) // Output: // true // true } func ExampleCmpSubSetOf() { t := &testing.T{} got := []int{1, 3, 5, 8, 8, 1, 2} // Matches as all items are expected, ignoring duplicates ok := td.CmpSubSetOf(t, got, []any{1, 2, 3, 4, 5, 6, 7, 8}, "checks at least all items are present, in any order, ignoring duplicates") fmt.Println(ok) // Tries its best to not raise an error when a value can be matched // by several SubSetOf entries ok = td.CmpSubSetOf(t, got, []any{td.Between(1, 4), 3, td.Between(2, 10), td.Gt(100)}, "checks at least all items are present, in any order, ignoring duplicates") fmt.Println(ok) // When expected is already a non-[]any slice, it cannot be // flattened directly using expected... without copying it to a new // []any slice, then use td.Flatten! expected := []int{1, 2, 3, 4, 5, 6, 7, 8} ok = td.CmpSubSetOf(t, got, []any{td.Flatten(expected)}, "checks at least all expected items are present, in any order, ignoring duplicates") fmt.Println(ok) // Output: // true // true // true } func ExampleCmpSuperBagOf() { t := &testing.T{} got := []int{1, 3, 5, 8, 8, 1, 2} ok := td.CmpSuperBagOf(t, got, []any{8, 5, 8}, "checks the items are present, in any order") fmt.Println(ok) ok = td.CmpSuperBagOf(t, got, []any{td.Gt(5), td.Lte(2)}, "checks at least 2 items of %v match", got) fmt.Println(ok) // When expected is already a non-[]any slice, it cannot be // flattened directly using expected... without copying it to a new // []any slice, then use td.Flatten! expected := []int{8, 5, 8} ok = td.CmpSuperBagOf(t, got, []any{td.Flatten(expected)}, "checks the expected items are present, in any order") fmt.Println(ok) // Output: // true // true // true } func ExampleCmpSuperJSONOf_basic() { t := &testing.T{} got := &struct { Fullname string `json:"fullname"` Age int `json:"age"` Gender string `json:"gender"` City string `json:"city"` Zip int `json:"zip"` }{ Fullname: "Bob", Age: 42, Gender: "male", City: "TestCity", Zip: 666, } ok := td.CmpSuperJSONOf(t, got, `{"age":42,"fullname":"Bob","gender":"male"}`, nil) fmt.Println("check got with age then fullname:", ok) ok = td.CmpSuperJSONOf(t, got, `{"fullname":"Bob","age":42,"gender":"male"}`, nil) fmt.Println("check got with fullname then age:", ok) ok = td.CmpSuperJSONOf(t, got, ` // This should be the JSON representation of a struct { // A person: "fullname": "Bob", // The name of this person "age": 42, /* The age of this person: - 42 of course - to demonstrate a multi-lines comment */ "gender": "male" // The gender! }`, nil) fmt.Println("check got with nicely formatted and commented JSON:", ok) ok = td.CmpSuperJSONOf(t, got, `{"fullname":"Bob","gender":"male","details":{}}`, nil) fmt.Println("check got with details field:", ok) // Output: // check got with age then fullname: true // check got with fullname then age: true // check got with nicely formatted and commented JSON: true // check got with details field: false } func ExampleCmpSuperJSONOf_placeholders() { t := &testing.T{} got := &struct { Fullname string `json:"fullname"` Age int `json:"age"` Gender string `json:"gender"` City string `json:"city"` Zip int `json:"zip"` }{ Fullname: "Bob Foobar", Age: 42, Gender: "male", City: "TestCity", Zip: 666, } ok := td.CmpSuperJSONOf(t, got, `{"age": $1, "fullname": $2, "gender": $3}`, []any{42, "Bob Foobar", "male"}) fmt.Println("check got with numeric placeholders without operators:", ok) ok = td.CmpSuperJSONOf(t, got, `{"age": $1, "fullname": $2, "gender": $3}`, []any{td.Between(40, 45), td.HasSuffix("Foobar"), td.NotEmpty()}) fmt.Println("check got with numeric placeholders:", ok) ok = td.CmpSuperJSONOf(t, got, `{"age": "$1", "fullname": "$2", "gender": "$3"}`, []any{td.Between(40, 45), td.HasSuffix("Foobar"), td.NotEmpty()}) fmt.Println("check got with double-quoted numeric placeholders:", ok) ok = td.CmpSuperJSONOf(t, got, `{"age": $age, "fullname": $name, "gender": $gender}`, []any{td.Tag("age", td.Between(40, 45)), td.Tag("name", td.HasSuffix("Foobar")), td.Tag("gender", td.NotEmpty())}) fmt.Println("check got with named placeholders:", ok) ok = td.CmpSuperJSONOf(t, got, `{"age": $^NotZero, "fullname": $^NotEmpty, "gender": $^NotEmpty}`, nil) fmt.Println("check got with operator shortcuts:", ok) // Output: // check got with numeric placeholders without operators: true // check got with numeric placeholders: true // check got with double-quoted numeric placeholders: true // check got with named placeholders: true // check got with operator shortcuts: true } func ExampleCmpSuperJSONOf_file() { t := &testing.T{} got := &struct { Fullname string `json:"fullname"` Age int `json:"age"` Gender string `json:"gender"` City string `json:"city"` Zip int `json:"zip"` }{ Fullname: "Bob Foobar", Age: 42, Gender: "male", City: "TestCity", Zip: 666, } tmpDir, err := os.MkdirTemp("", "") if err != nil { t.Fatal(err) } defer os.RemoveAll(tmpDir) // clean up filename := tmpDir + "/test.json" if err = os.WriteFile(filename, []byte(` { "fullname": "$name", "age": "$age", "gender": "$gender" }`), 0644); err != nil { t.Fatal(err) } // OK let's test with this file ok := td.CmpSuperJSONOf(t, got, filename, []any{td.Tag("name", td.HasPrefix("Bob")), td.Tag("age", td.Between(40, 45)), td.Tag("gender", td.Re(`^(male|female)\z`))}) fmt.Println("Full match from file name:", ok) // When the file is already open file, err := os.Open(filename) if err != nil { t.Fatal(err) } ok = td.CmpSuperJSONOf(t, got, file, []any{td.Tag("name", td.HasPrefix("Bob")), td.Tag("age", td.Between(40, 45)), td.Tag("gender", td.Re(`^(male|female)\z`))}) fmt.Println("Full match from io.Reader:", ok) // Output: // Full match from file name: true // Full match from io.Reader: true } func ExampleCmpSuperMapOf_map() { t := &testing.T{} got := map[string]int{"foo": 12, "bar": 42, "zip": 89} ok := td.CmpSuperMapOf(t, got, map[string]int{"bar": 42}, td.MapEntries{"foo": td.Lt(15)}, "checks map %v contains at least all expected keys/values", got) fmt.Println(ok) // Output: // true } func ExampleCmpSuperMapOf_typedMap() { t := &testing.T{} type MyMap map[string]int got := MyMap{"foo": 12, "bar": 42, "zip": 89} ok := td.CmpSuperMapOf(t, got, MyMap{"bar": 42}, td.MapEntries{"foo": td.Lt(15)}, "checks typed map %v contains at least all expected keys/values", got) fmt.Println(ok) ok = td.CmpSuperMapOf(t, &got, &MyMap{"bar": 42}, td.MapEntries{"foo": td.Lt(15)}, "checks pointed typed map %v contains at least all expected keys/values", got) fmt.Println(ok) // Output: // true // true } func ExampleCmpSuperSetOf() { t := &testing.T{} got := []int{1, 3, 5, 8, 8, 1, 2} ok := td.CmpSuperSetOf(t, got, []any{1, 2, 3}, "checks the items are present, in any order and ignoring duplicates") fmt.Println(ok) ok = td.CmpSuperSetOf(t, got, []any{td.Gt(5), td.Lte(2)}, "checks at least 2 items of %v match ignoring duplicates", got) fmt.Println(ok) // When expected is already a non-[]any slice, it cannot be // flattened directly using expected... without copying it to a new // []any slice, then use td.Flatten! expected := []int{1, 2, 3} ok = td.CmpSuperSetOf(t, got, []any{td.Flatten(expected)}, "checks the expected items are present, in any order and ignoring duplicates") fmt.Println(ok) // Output: // true // true // true } func ExampleCmpSuperSliceOf_array() { t := &testing.T{} got := [4]int{42, 58, 26, 666} ok := td.CmpSuperSliceOf(t, got, [4]int{1: 58}, td.ArrayEntries{3: td.Gt(660)}, "checks array %v", got) fmt.Println("Only check items #1 & #3:", ok) ok = td.CmpSuperSliceOf(t, got, [4]int{}, td.ArrayEntries{0: 42, 3: td.Between(660, 670)}, "checks array %v", got) fmt.Println("Only check items #0 & #3:", ok) ok = td.CmpSuperSliceOf(t, &got, &[4]int{}, td.ArrayEntries{0: 42, 3: td.Between(660, 670)}, "checks array %v", got) fmt.Println("Only check items #0 & #3 of an array pointer:", ok) ok = td.CmpSuperSliceOf(t, &got, (*[4]int)(nil), td.ArrayEntries{0: 42, 3: td.Between(660, 670)}, "checks array %v", got) fmt.Println("Only check items #0 & #3 of an array pointer, using nil model:", ok) // Output: // Only check items #1 & #3: true // Only check items #0 & #3: true // Only check items #0 & #3 of an array pointer: true // Only check items #0 & #3 of an array pointer, using nil model: true } func ExampleCmpSuperSliceOf_typedArray() { t := &testing.T{} type MyArray [4]int got := MyArray{42, 58, 26, 666} ok := td.CmpSuperSliceOf(t, got, MyArray{1: 58}, td.ArrayEntries{3: td.Gt(660)}, "checks typed array %v", got) fmt.Println("Only check items #1 & #3:", ok) ok = td.CmpSuperSliceOf(t, got, MyArray{}, td.ArrayEntries{0: 42, 3: td.Between(660, 670)}, "checks array %v", got) fmt.Println("Only check items #0 & #3:", ok) ok = td.CmpSuperSliceOf(t, &got, &MyArray{}, td.ArrayEntries{0: 42, 3: td.Between(660, 670)}, "checks array %v", got) fmt.Println("Only check items #0 & #3 of an array pointer:", ok) ok = td.CmpSuperSliceOf(t, &got, (*MyArray)(nil), td.ArrayEntries{0: 42, 3: td.Between(660, 670)}, "checks array %v", got) fmt.Println("Only check items #0 & #3 of an array pointer, using nil model:", ok) // Output: // Only check items #1 & #3: true // Only check items #0 & #3: true // Only check items #0 & #3 of an array pointer: true // Only check items #0 & #3 of an array pointer, using nil model: true } func ExampleCmpSuperSliceOf_slice() { t := &testing.T{} got := []int{42, 58, 26, 666} ok := td.CmpSuperSliceOf(t, got, []int{1: 58}, td.ArrayEntries{3: td.Gt(660)}, "checks array %v", got) fmt.Println("Only check items #1 & #3:", ok) ok = td.CmpSuperSliceOf(t, got, []int{}, td.ArrayEntries{0: 42, 3: td.Between(660, 670)}, "checks array %v", got) fmt.Println("Only check items #0 & #3:", ok) ok = td.CmpSuperSliceOf(t, &got, &[]int{}, td.ArrayEntries{0: 42, 3: td.Between(660, 670)}, "checks array %v", got) fmt.Println("Only check items #0 & #3 of a slice pointer:", ok) ok = td.CmpSuperSliceOf(t, &got, (*[]int)(nil), td.ArrayEntries{0: 42, 3: td.Between(660, 670)}, "checks array %v", got) fmt.Println("Only check items #0 & #3 of a slice pointer, using nil model:", ok) // Output: // Only check items #1 & #3: true // Only check items #0 & #3: true // Only check items #0 & #3 of a slice pointer: true // Only check items #0 & #3 of a slice pointer, using nil model: true } func ExampleCmpSuperSliceOf_typedSlice() { t := &testing.T{} type MySlice []int got := MySlice{42, 58, 26, 666} ok := td.CmpSuperSliceOf(t, got, MySlice{1: 58}, td.ArrayEntries{3: td.Gt(660)}, "checks typed array %v", got) fmt.Println("Only check items #1 & #3:", ok) ok = td.CmpSuperSliceOf(t, got, MySlice{}, td.ArrayEntries{0: 42, 3: td.Between(660, 670)}, "checks array %v", got) fmt.Println("Only check items #0 & #3:", ok) ok = td.CmpSuperSliceOf(t, &got, &MySlice{}, td.ArrayEntries{0: 42, 3: td.Between(660, 670)}, "checks array %v", got) fmt.Println("Only check items #0 & #3 of a slice pointer:", ok) ok = td.CmpSuperSliceOf(t, &got, (*MySlice)(nil), td.ArrayEntries{0: 42, 3: td.Between(660, 670)}, "checks array %v", got) fmt.Println("Only check items #0 & #3 of a slice pointer, using nil model:", ok) // Output: // Only check items #1 & #3: true // Only check items #0 & #3: true // Only check items #0 & #3 of a slice pointer: true // Only check items #0 & #3 of a slice pointer, using nil model: true } func ExampleCmpTruncTime() { t := &testing.T{} dateToTime := func(str string) time.Time { t, err := time.Parse(time.RFC3339Nano, str) if err != nil { panic(err) } return t } got := dateToTime("2018-05-01T12:45:53.123456789Z") // Compare dates ignoring nanoseconds and monotonic parts expected := dateToTime("2018-05-01T12:45:53Z") ok := td.CmpTruncTime(t, got, expected, time.Second, "checks date %v, truncated to the second", got) fmt.Println(ok) // Compare dates ignoring time and so monotonic parts expected = dateToTime("2018-05-01T11:22:33.444444444Z") ok = td.CmpTruncTime(t, got, expected, 24*time.Hour, "checks date %v, truncated to the day", got) fmt.Println(ok) // Compare dates exactly but ignoring monotonic part expected = dateToTime("2018-05-01T12:45:53.123456789Z") ok = td.CmpTruncTime(t, got, expected, 0, "checks date %v ignoring monotonic part", got) fmt.Println(ok) // Output: // true // true // true } func ExampleCmpValues() { t := &testing.T{} got := map[string]int{"foo": 1, "bar": 2, "zip": 3} // Values tests values in an ordered manner ok := td.CmpValues(t, got, []int{1, 2, 3}) fmt.Println("All sorted values are found:", ok) // If the expected values are not ordered, it fails ok = td.CmpValues(t, got, []int{3, 1, 2}) fmt.Println("All unsorted values are found:", ok) // To circumvent that, one can use Bag operator ok = td.CmpValues(t, got, td.Bag(3, 1, 2)) fmt.Println("All unsorted values are found, with the help of Bag operator:", ok) // Check that each value is between 1 and 3 ok = td.CmpValues(t, got, td.ArrayEach(td.Between(1, 3))) fmt.Println("Each value is between 1 and 3:", ok) // Output: // All sorted values are found: true // All unsorted values are found: false // All unsorted values are found, with the help of Bag operator: true // Each value is between 1 and 3: true } func ExampleCmpZero() { t := &testing.T{} ok := td.CmpZero(t, 0) fmt.Println(ok) ok = td.CmpZero(t, float64(0)) fmt.Println(ok) ok = td.CmpZero(t, 12) // fails, as 12 is not 0 :) fmt.Println(ok) ok = td.CmpZero(t, (map[string]int)(nil)) fmt.Println(ok) ok = td.CmpZero(t, map[string]int{}) // fails, as not nil fmt.Println(ok) ok = td.CmpZero(t, ([]int)(nil)) fmt.Println(ok) ok = td.CmpZero(t, []int{}) // fails, as not nil fmt.Println(ok) ok = td.CmpZero(t, [3]int{}) fmt.Println(ok) ok = td.CmpZero(t, [3]int{0, 1}) // fails, DATA[1] is not 0 fmt.Println(ok) ok = td.CmpZero(t, bytes.Buffer{}) fmt.Println(ok) ok = td.CmpZero(t, &bytes.Buffer{}) // fails, as pointer not nil fmt.Println(ok) ok = td.Cmp(t, &bytes.Buffer{}, td.Ptr(td.Zero())) // OK with the help of Ptr() fmt.Println(ok) // Output: // true // true // false // true // false // true // false // true // false // true // false // true } golang-github-maxatome-go-testdeep-1.14.0/td/example_t_test.go000066400000000000000000003035221454313311600243340ustar00rootroot00000000000000// Copyright (c) 2018-2022, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. // // DO NOT EDIT!!! AUTOMATICALLY GENERATED!!! package td_test import ( "bytes" "encoding/json" "errors" "fmt" "math" "os" "regexp" "strconv" "strings" "testing" "time" "github.com/maxatome/go-testdeep/td" ) func ExampleT_All() { t := td.NewT(&testing.T{}) got := "foo/bar" // Checks got string against: // "o/b" regexp *AND* "bar" suffix *AND* exact "foo/bar" string ok := t.All(got, []any{td.Re("o/b"), td.HasSuffix("bar"), "foo/bar"}, "checks value %s", got) fmt.Println(ok) // Checks got string against: // "o/b" regexp *AND* "bar" suffix *AND* exact "fooX/Ybar" string ok = t.All(got, []any{td.Re("o/b"), td.HasSuffix("bar"), "fooX/Ybar"}, "checks value %s", got) fmt.Println(ok) // When some operators or values have to be reused and mixed between // several calls, Flatten can be used to avoid boring and // inefficient []any copies: regOps := td.Flatten([]td.TestDeep{td.Re("o/b"), td.Re(`^fo`), td.Re(`ar$`)}) ok = t.All(got, []any{td.HasPrefix("foo"), regOps, td.HasSuffix("bar")}, "checks all operators against value %s", got) fmt.Println(ok) // Output: // true // false // true } func ExampleT_Any() { t := td.NewT(&testing.T{}) got := "foo/bar" // Checks got string against: // "zip" regexp *OR* "bar" suffix ok := t.Any(got, []any{td.Re("zip"), td.HasSuffix("bar")}, "checks value %s", got) fmt.Println(ok) // Checks got string against: // "zip" regexp *OR* "foo" suffix ok = t.Any(got, []any{td.Re("zip"), td.HasSuffix("foo")}, "checks value %s", got) fmt.Println(ok) // When some operators or values have to be reused and mixed between // several calls, Flatten can be used to avoid boring and // inefficient []any copies: regOps := td.Flatten([]td.TestDeep{td.Re("a/c"), td.Re(`^xx`), td.Re(`ar$`)}) ok = t.Any(got, []any{td.HasPrefix("xxx"), regOps, td.HasSuffix("zip")}, "check at least one operator matches value %s", got) fmt.Println(ok) // Output: // true // false // true } func ExampleT_Array_array() { t := td.NewT(&testing.T{}) got := [3]int{42, 58, 26} ok := t.Array(got, [3]int{42}, td.ArrayEntries{1: 58, 2: td.Ignore()}, "checks array %v", got) fmt.Println("Simple array:", ok) ok = t.Array(&got, &[3]int{42}, td.ArrayEntries{1: 58, 2: td.Ignore()}, "checks array %v", got) fmt.Println("Array pointer:", ok) ok = t.Array(&got, (*[3]int)(nil), td.ArrayEntries{0: 42, 1: 58, 2: td.Ignore()}, "checks array %v", got) fmt.Println("Array pointer, nil model:", ok) // Output: // Simple array: true // Array pointer: true // Array pointer, nil model: true } func ExampleT_Array_typedArray() { t := td.NewT(&testing.T{}) type MyArray [3]int got := MyArray{42, 58, 26} ok := t.Array(got, MyArray{42}, td.ArrayEntries{1: 58, 2: td.Ignore()}, "checks typed array %v", got) fmt.Println("Typed array:", ok) ok = t.Array(&got, &MyArray{42}, td.ArrayEntries{1: 58, 2: td.Ignore()}, "checks pointer on typed array %v", got) fmt.Println("Pointer on a typed array:", ok) ok = t.Array(&got, &MyArray{}, td.ArrayEntries{0: 42, 1: 58, 2: td.Ignore()}, "checks pointer on typed array %v", got) fmt.Println("Pointer on a typed array, empty model:", ok) ok = t.Array(&got, (*MyArray)(nil), td.ArrayEntries{0: 42, 1: 58, 2: td.Ignore()}, "checks pointer on typed array %v", got) fmt.Println("Pointer on a typed array, nil model:", ok) // Output: // Typed array: true // Pointer on a typed array: true // Pointer on a typed array, empty model: true // Pointer on a typed array, nil model: true } func ExampleT_ArrayEach_array() { t := td.NewT(&testing.T{}) got := [3]int{42, 58, 26} ok := t.ArrayEach(got, td.Between(25, 60), "checks each item of array %v is in [25 .. 60]", got) fmt.Println(ok) // Output: // true } func ExampleT_ArrayEach_typedArray() { t := td.NewT(&testing.T{}) type MyArray [3]int got := MyArray{42, 58, 26} ok := t.ArrayEach(got, td.Between(25, 60), "checks each item of typed array %v is in [25 .. 60]", got) fmt.Println(ok) ok = t.ArrayEach(&got, td.Between(25, 60), "checks each item of typed array pointer %v is in [25 .. 60]", got) fmt.Println(ok) // Output: // true // true } func ExampleT_ArrayEach_slice() { t := td.NewT(&testing.T{}) got := []int{42, 58, 26} ok := t.ArrayEach(got, td.Between(25, 60), "checks each item of slice %v is in [25 .. 60]", got) fmt.Println(ok) // Output: // true } func ExampleT_ArrayEach_typedSlice() { t := td.NewT(&testing.T{}) type MySlice []int got := MySlice{42, 58, 26} ok := t.ArrayEach(got, td.Between(25, 60), "checks each item of typed slice %v is in [25 .. 60]", got) fmt.Println(ok) ok = t.ArrayEach(&got, td.Between(25, 60), "checks each item of typed slice pointer %v is in [25 .. 60]", got) fmt.Println(ok) // Output: // true // true } func ExampleT_Bag() { t := td.NewT(&testing.T{}) got := []int{1, 3, 5, 8, 8, 1, 2} // Matches as all items are present ok := t.Bag(got, []any{1, 1, 2, 3, 5, 8, 8}, "checks all items are present, in any order") fmt.Println(ok) // Does not match as got contains 2 times 1 and 8, and these // duplicates are not expected ok = t.Bag(got, []any{1, 2, 3, 5, 8}, "checks all items are present, in any order") fmt.Println(ok) got = []int{1, 3, 5, 8, 2} // Duplicates of 1 and 8 are expected but not present in got ok = t.Bag(got, []any{1, 1, 2, 3, 5, 8, 8}, "checks all items are present, in any order") fmt.Println(ok) // Matches as all items are present ok = t.Bag(got, []any{1, 2, 3, 5, td.Gt(7)}, "checks all items are present, in any order") fmt.Println(ok) // When expected is already a non-[]any slice, it cannot be // flattened directly using expected... without copying it to a new // []any slice, then use td.Flatten! expected := []int{1, 2, 3, 5} ok = t.Bag(got, []any{td.Flatten(expected), td.Gt(7)}, "checks all expected items are present, in any order") fmt.Println(ok) // Output: // true // false // false // true // true } func ExampleT_Between_int() { t := td.NewT(&testing.T{}) got := 156 ok := t.Between(got, 154, 156, td.BoundsInIn, "checks %v is in [154 .. 156]", got) fmt.Println(ok) // BoundsInIn is implicit ok = t.Between(got, 154, 156, td.BoundsInIn, "checks %v is in [154 .. 156]", got) fmt.Println(ok) ok = t.Between(got, 154, 156, td.BoundsInOut, "checks %v is in [154 .. 156[", got) fmt.Println(ok) ok = t.Between(got, 154, 156, td.BoundsOutIn, "checks %v is in ]154 .. 156]", got) fmt.Println(ok) ok = t.Between(got, 154, 156, td.BoundsOutOut, "checks %v is in ]154 .. 156[", got) fmt.Println(ok) // Output: // true // true // false // true // false } func ExampleT_Between_string() { t := td.NewT(&testing.T{}) got := "abc" ok := t.Between(got, "aaa", "abc", td.BoundsInIn, `checks "%v" is in ["aaa" .. "abc"]`, got) fmt.Println(ok) // BoundsInIn is implicit ok = t.Between(got, "aaa", "abc", td.BoundsInIn, `checks "%v" is in ["aaa" .. "abc"]`, got) fmt.Println(ok) ok = t.Between(got, "aaa", "abc", td.BoundsInOut, `checks "%v" is in ["aaa" .. "abc"[`, got) fmt.Println(ok) ok = t.Between(got, "aaa", "abc", td.BoundsOutIn, `checks "%v" is in ]"aaa" .. "abc"]`, got) fmt.Println(ok) ok = t.Between(got, "aaa", "abc", td.BoundsOutOut, `checks "%v" is in ]"aaa" .. "abc"[`, got) fmt.Println(ok) // Output: // true // true // false // true // false } func ExampleT_Between_time() { t := td.NewT(&testing.T{}) before := time.Now() occurredAt := time.Now() after := time.Now() ok := t.Between(occurredAt, before, after, td.BoundsInIn) fmt.Println("It occurred between before and after:", ok) type MyTime time.Time ok = t.Between(MyTime(occurredAt), MyTime(before), MyTime(after), td.BoundsInIn) fmt.Println("Same for convertible MyTime type:", ok) ok = t.Between(MyTime(occurredAt), before, after, td.BoundsInIn) fmt.Println("MyTime vs time.Time:", ok) ok = t.Between(occurredAt, before, 10*time.Second, td.BoundsInIn) fmt.Println("Using a time.Duration as TO:", ok) ok = t.Between(MyTime(occurredAt), MyTime(before), 10*time.Second, td.BoundsInIn) fmt.Println("Using MyTime as FROM and time.Duration as TO:", ok) // Output: // It occurred between before and after: true // Same for convertible MyTime type: true // MyTime vs time.Time: false // Using a time.Duration as TO: true // Using MyTime as FROM and time.Duration as TO: true } func ExampleT_Cap() { t := td.NewT(&testing.T{}) got := make([]int, 0, 12) ok := t.Cap(got, 12, "checks %v capacity is 12", got) fmt.Println(ok) ok = t.Cap(got, 0, "checks %v capacity is 0", got) fmt.Println(ok) got = nil ok = t.Cap(got, 0, "checks %v capacity is 0", got) fmt.Println(ok) // Output: // true // false // true } func ExampleT_Cap_operator() { t := td.NewT(&testing.T{}) got := make([]int, 0, 12) ok := t.Cap(got, td.Between(10, 12), "checks %v capacity is in [10 .. 12]", got) fmt.Println(ok) ok = t.Cap(got, td.Gt(10), "checks %v capacity is in [10 .. 12]", got) fmt.Println(ok) // Output: // true // true } func ExampleT_Code() { t := td.NewT(&testing.T{}) got := "12" ok := t.Code(got, func(num string) bool { n, err := strconv.Atoi(num) return err == nil && n > 10 && n < 100 }, "checks string `%s` contains a number and this number is in ]10 .. 100[", got) fmt.Println(ok) // Same with failure reason ok = t.Code(got, func(num string) (bool, string) { n, err := strconv.Atoi(num) if err != nil { return false, "not a number" } if n > 10 && n < 100 { return true, "" } return false, "not in ]10 .. 100[" }, "checks string `%s` contains a number and this number is in ]10 .. 100[", got) fmt.Println(ok) // Same with failure reason thanks to error ok = t.Code(got, func(num string) error { n, err := strconv.Atoi(num) if err != nil { return err } if n > 10 && n < 100 { return nil } return fmt.Errorf("%d not in ]10 .. 100[", n) }, "checks string `%s` contains a number and this number is in ]10 .. 100[", got) fmt.Println(ok) // Output: // true // true // true } func ExampleT_Code_custom() { t := td.NewT(&testing.T{}) got := 123 ok := t.Code(got, func(t *td.T, num int) { t.Cmp(num, 123) }) fmt.Println("with one *td.T:", ok) ok = t.Code(got, func(assert, require *td.T, num int) { assert.Cmp(num, 123) require.Cmp(num, 123) }) fmt.Println("with assert & require *td.T:", ok) // Output: // with one *td.T: true // with assert & require *td.T: true } func ExampleT_Contains_arraySlice() { t := td.NewT(&testing.T{}) ok := t.Contains([...]int{11, 22, 33, 44}, 22) fmt.Println("array contains 22:", ok) ok = t.Contains([...]int{11, 22, 33, 44}, td.Between(20, 25)) fmt.Println("array contains at least one item in [20 .. 25]:", ok) ok = t.Contains([]int{11, 22, 33, 44}, 22) fmt.Println("slice contains 22:", ok) ok = t.Contains([]int{11, 22, 33, 44}, td.Between(20, 25)) fmt.Println("slice contains at least one item in [20 .. 25]:", ok) ok = t.Contains([]int{11, 22, 33, 44}, []int{22, 33}) fmt.Println("slice contains the sub-slice [22, 33]:", ok) // Output: // array contains 22: true // array contains at least one item in [20 .. 25]: true // slice contains 22: true // slice contains at least one item in [20 .. 25]: true // slice contains the sub-slice [22, 33]: true } func ExampleT_Contains_nil() { t := td.NewT(&testing.T{}) num := 123 got := [...]*int{&num, nil} ok := t.Contains(got, nil) fmt.Println("array contains untyped nil:", ok) ok = t.Contains(got, (*int)(nil)) fmt.Println("array contains *int nil:", ok) ok = t.Contains(got, td.Nil()) fmt.Println("array contains Nil():", ok) ok = t.Contains(got, (*byte)(nil)) fmt.Println("array contains *byte nil:", ok) // types differ: *byte ≠ *int // Output: // array contains untyped nil: true // array contains *int nil: true // array contains Nil(): true // array contains *byte nil: false } func ExampleT_Contains_map() { t := td.NewT(&testing.T{}) ok := t.Contains(map[string]int{"foo": 11, "bar": 22, "zip": 33}, 22) fmt.Println("map contains value 22:", ok) ok = t.Contains(map[string]int{"foo": 11, "bar": 22, "zip": 33}, td.Between(20, 25)) fmt.Println("map contains at least one value in [20 .. 25]:", ok) // Output: // map contains value 22: true // map contains at least one value in [20 .. 25]: true } func ExampleT_Contains_string() { t := td.NewT(&testing.T{}) got := "foobar" ok := t.Contains(got, "oob", "checks %s", got) fmt.Println("contains `oob` string:", ok) ok = t.Contains(got, []byte("oob"), "checks %s", got) fmt.Println("contains `oob` []byte:", ok) ok = t.Contains(got, 'b', "checks %s", got) fmt.Println("contains 'b' rune:", ok) ok = t.Contains(got, byte('a'), "checks %s", got) fmt.Println("contains 'a' byte:", ok) ok = t.Contains(got, td.Between('n', 'p'), "checks %s", got) fmt.Println("contains at least one character ['n' .. 'p']:", ok) // Output: // contains `oob` string: true // contains `oob` []byte: true // contains 'b' rune: true // contains 'a' byte: true // contains at least one character ['n' .. 'p']: true } func ExampleT_Contains_stringer() { t := td.NewT(&testing.T{}) // bytes.Buffer implements fmt.Stringer got := bytes.NewBufferString("foobar") ok := t.Contains(got, "oob", "checks %s", got) fmt.Println("contains `oob` string:", ok) ok = t.Contains(got, 'b', "checks %s", got) fmt.Println("contains 'b' rune:", ok) ok = t.Contains(got, byte('a'), "checks %s", got) fmt.Println("contains 'a' byte:", ok) ok = t.Contains(got, td.Between('n', 'p'), "checks %s", got) fmt.Println("contains at least one character ['n' .. 'p']:", ok) // Output: // contains `oob` string: true // contains 'b' rune: true // contains 'a' byte: true // contains at least one character ['n' .. 'p']: true } func ExampleT_Contains_error() { t := td.NewT(&testing.T{}) got := errors.New("foobar") ok := t.Contains(got, "oob", "checks %s", got) fmt.Println("contains `oob` string:", ok) ok = t.Contains(got, 'b', "checks %s", got) fmt.Println("contains 'b' rune:", ok) ok = t.Contains(got, byte('a'), "checks %s", got) fmt.Println("contains 'a' byte:", ok) ok = t.Contains(got, td.Between('n', 'p'), "checks %s", got) fmt.Println("contains at least one character ['n' .. 'p']:", ok) // Output: // contains `oob` string: true // contains 'b' rune: true // contains 'a' byte: true // contains at least one character ['n' .. 'p']: true } func ExampleT_ContainsKey() { t := td.NewT(&testing.T{}) ok := t.ContainsKey(map[string]int{"foo": 11, "bar": 22, "zip": 33}, "foo") fmt.Println(`map contains key "foo":`, ok) ok = t.ContainsKey(map[int]bool{12: true, 24: false, 42: true, 51: false}, td.Between(40, 50)) fmt.Println("map contains at least a key in [40 .. 50]:", ok) ok = t.ContainsKey(map[string]int{"FOO": 11, "bar": 22, "zip": 33}, td.Smuggle(strings.ToLower, "foo")) fmt.Println(`map contains key "foo" without taking case into account:`, ok) // Output: // map contains key "foo": true // map contains at least a key in [40 .. 50]: true // map contains key "foo" without taking case into account: true } func ExampleT_ContainsKey_nil() { t := td.NewT(&testing.T{}) num := 1234 got := map[*int]bool{&num: false, nil: true} ok := t.ContainsKey(got, nil) fmt.Println("map contains untyped nil key:", ok) ok = t.ContainsKey(got, (*int)(nil)) fmt.Println("map contains *int nil key:", ok) ok = t.ContainsKey(got, td.Nil()) fmt.Println("map contains Nil() key:", ok) ok = t.ContainsKey(got, (*byte)(nil)) fmt.Println("map contains *byte nil key:", ok) // types differ: *byte ≠ *int // Output: // map contains untyped nil key: true // map contains *int nil key: true // map contains Nil() key: true // map contains *byte nil key: false } func ExampleT_Empty() { t := td.NewT(&testing.T{}) ok := t.Empty(nil) // special case: nil is considered empty fmt.Println(ok) // fails, typed nil is not empty (expect for channel, map, slice or // pointers on array, channel, map slice and strings) ok = t.Empty((*int)(nil)) fmt.Println(ok) ok = t.Empty("") fmt.Println(ok) // Fails as 0 is a number, so not empty. Use Zero() instead ok = t.Empty(0) fmt.Println(ok) ok = t.Empty((map[string]int)(nil)) fmt.Println(ok) ok = t.Empty(map[string]int{}) fmt.Println(ok) ok = t.Empty(([]int)(nil)) fmt.Println(ok) ok = t.Empty([]int{}) fmt.Println(ok) ok = t.Empty([]int{3}) // fails, as not empty fmt.Println(ok) ok = t.Empty([3]int{}) // fails, Empty() is not Zero()! fmt.Println(ok) // Output: // true // false // true // false // true // true // true // true // false // false } func ExampleT_Empty_pointers() { t := td.NewT(&testing.T{}) type MySlice []int ok := t.Empty(MySlice{}) // Ptr() not needed fmt.Println(ok) ok = t.Empty(&MySlice{}) fmt.Println(ok) l1 := &MySlice{} l2 := &l1 l3 := &l2 ok = t.Empty(&l3) fmt.Println(ok) // Works the same for array, map, channel and string // But not for others types as: type MyStruct struct { Value int } ok = t.Empty(&MyStruct{}) // fails, use Zero() instead fmt.Println(ok) // Output: // true // true // true // false } func ExampleT_CmpErrorIs() { t := td.NewT(&testing.T{}) err1 := fmt.Errorf("failure1") err2 := fmt.Errorf("failure2: %w", err1) err3 := fmt.Errorf("failure3: %w", err2) err := fmt.Errorf("failure4: %w", err3) ok := t.CmpErrorIs(err, err) fmt.Println("error is itself:", ok) ok = t.CmpErrorIs(err, err1) fmt.Println("error is also err1:", ok) ok = t.CmpErrorIs(err1, err) fmt.Println("err1 is err:", ok) // Output: // error is itself: true // error is also err1: true // err1 is err: false } func ExampleT_First_classic() { t := td.NewT(&testing.T{}) got := []int{-3, -2, -1, 0, 1, 2, 3} ok := t.First(got, td.Gt(0), 1) fmt.Println("first positive number is 1:", ok) isEven := func(x int) bool { return x%2 == 0 } ok = t.First(got, isEven, -2) fmt.Println("first even number is -2:", ok) ok = t.First(got, isEven, td.Lt(0)) fmt.Println("first even number is < 0:", ok) ok = t.First(got, isEven, td.Code(isEven)) fmt.Println("first even number is well even:", ok) // Output: // first positive number is 1: true // first even number is -2: true // first even number is < 0: true // first even number is well even: true } func ExampleT_First_empty() { t := td.NewT(&testing.T{}) ok := t.First(([]int)(nil), td.Gt(0), td.Gt(0)) fmt.Println("first in nil slice:", ok) ok = t.First([]int{}, td.Gt(0), td.Gt(0)) fmt.Println("first in empty slice:", ok) ok = t.First(&[]int{}, td.Gt(0), td.Gt(0)) fmt.Println("first in empty pointed slice:", ok) ok = t.First([0]int{}, td.Gt(0), td.Gt(0)) fmt.Println("first in empty array:", ok) // Output: // first in nil slice: false // first in empty slice: false // first in empty pointed slice: false // first in empty array: false } func ExampleT_First_struct() { t := td.NewT(&testing.T{}) type Person struct { Fullname string `json:"fullname"` Age int `json:"age"` } got := []*Person{ { Fullname: "Bob Foobar", Age: 42, }, { Fullname: "Alice Bingo", Age: 37, }, } ok := t.First(got, td.Smuggle("Age", td.Gt(30)), td.Smuggle("Fullname", "Bob Foobar")) fmt.Println("first person.Age > 30 → Bob:", ok) ok = t.First(got, td.JSONPointer("/age", td.Gt(30)), td.SuperJSONOf(`{"fullname":"Bob Foobar"}`)) fmt.Println("first person.Age > 30 → Bob, using JSON:", ok) ok = t.First(got, td.JSONPointer("/age", td.Gt(30)), td.JSONPointer("/fullname", td.HasPrefix("Bob"))) fmt.Println("first person.Age > 30 → Bob, using JSONPointer:", ok) // Output: // first person.Age > 30 → Bob: true // first person.Age > 30 → Bob, using JSON: true // first person.Age > 30 → Bob, using JSONPointer: true } func ExampleT_Grep_classic() { t := td.NewT(&testing.T{}) got := []int{-3, -2, -1, 0, 1, 2, 3} ok := t.Grep(got, td.Gt(0), []int{1, 2, 3}) fmt.Println("check positive numbers:", ok) isEven := func(x int) bool { return x%2 == 0 } ok = t.Grep(got, isEven, []int{-2, 0, 2}) fmt.Println("even numbers are -2, 0 and 2:", ok) ok = t.Grep(got, isEven, td.Set(0, 2, -2)) fmt.Println("even numbers are also 0, 2 and -2:", ok) ok = t.Grep(got, isEven, td.ArrayEach(td.Code(isEven))) fmt.Println("even numbers are each even:", ok) // Output: // check positive numbers: true // even numbers are -2, 0 and 2: true // even numbers are also 0, 2 and -2: true // even numbers are each even: true } func ExampleT_Grep_nil() { t := td.NewT(&testing.T{}) var got []int ok := t.Grep(got, td.Gt(0), ([]int)(nil)) fmt.Println("typed []int nil:", ok) ok = t.Grep(got, td.Gt(0), ([]string)(nil)) fmt.Println("typed []string nil:", ok) ok = t.Grep(got, td.Gt(0), td.Nil()) fmt.Println("td.Nil:", ok) ok = t.Grep(got, td.Gt(0), []int{}) fmt.Println("empty non-nil slice:", ok) // Output: // typed []int nil: true // typed []string nil: false // td.Nil: true // empty non-nil slice: false } func ExampleT_Grep_struct() { t := td.NewT(&testing.T{}) type Person struct { Fullname string `json:"fullname"` Age int `json:"age"` } got := []*Person{ { Fullname: "Bob Foobar", Age: 42, }, { Fullname: "Alice Bingo", Age: 27, }, } ok := t.Grep(got, td.Smuggle("Age", td.Gt(30)), td.All( td.Len(1), td.ArrayEach(td.Smuggle("Fullname", "Bob Foobar")), )) fmt.Println("person.Age > 30 → only Bob:", ok) ok = t.Grep(got, td.JSONPointer("/age", td.Gt(30)), td.JSON(`[ SuperMapOf({"fullname":"Bob Foobar"}) ]`)) fmt.Println("person.Age > 30 → only Bob, using JSON:", ok) // Output: // person.Age > 30 → only Bob: true // person.Age > 30 → only Bob, using JSON: true } func ExampleT_Gt_int() { t := td.NewT(&testing.T{}) got := 156 ok := t.Gt(got, 155, "checks %v is > 155", got) fmt.Println(ok) ok = t.Gt(got, 156, "checks %v is > 156", got) fmt.Println(ok) // Output: // true // false } func ExampleT_Gt_string() { t := td.NewT(&testing.T{}) got := "abc" ok := t.Gt(got, "abb", `checks "%v" is > "abb"`, got) fmt.Println(ok) ok = t.Gt(got, "abc", `checks "%v" is > "abc"`, got) fmt.Println(ok) // Output: // true // false } func ExampleT_Gte_int() { t := td.NewT(&testing.T{}) got := 156 ok := t.Gte(got, 156, "checks %v is ≥ 156", got) fmt.Println(ok) ok = t.Gte(got, 155, "checks %v is ≥ 155", got) fmt.Println(ok) ok = t.Gte(got, 157, "checks %v is ≥ 157", got) fmt.Println(ok) // Output: // true // true // false } func ExampleT_Gte_string() { t := td.NewT(&testing.T{}) got := "abc" ok := t.Gte(got, "abc", `checks "%v" is ≥ "abc"`, got) fmt.Println(ok) ok = t.Gte(got, "abb", `checks "%v" is ≥ "abb"`, got) fmt.Println(ok) ok = t.Gte(got, "abd", `checks "%v" is ≥ "abd"`, got) fmt.Println(ok) // Output: // true // true // false } func ExampleT_HasPrefix() { t := td.NewT(&testing.T{}) got := "foobar" ok := t.HasPrefix(got, "foo", "checks %s", got) fmt.Println("using string:", ok) ok = t.Cmp([]byte(got), td.HasPrefix("foo"), "checks %s", got) fmt.Println("using []byte:", ok) // Output: // using string: true // using []byte: true } func ExampleT_HasPrefix_stringer() { t := td.NewT(&testing.T{}) // bytes.Buffer implements fmt.Stringer got := bytes.NewBufferString("foobar") ok := t.HasPrefix(got, "foo", "checks %s", got) fmt.Println(ok) // Output: // true } func ExampleT_HasPrefix_error() { t := td.NewT(&testing.T{}) got := errors.New("foobar") ok := t.HasPrefix(got, "foo", "checks %s", got) fmt.Println(ok) // Output: // true } func ExampleT_HasSuffix() { t := td.NewT(&testing.T{}) got := "foobar" ok := t.HasSuffix(got, "bar", "checks %s", got) fmt.Println("using string:", ok) ok = t.Cmp([]byte(got), td.HasSuffix("bar"), "checks %s", got) fmt.Println("using []byte:", ok) // Output: // using string: true // using []byte: true } func ExampleT_HasSuffix_stringer() { t := td.NewT(&testing.T{}) // bytes.Buffer implements fmt.Stringer got := bytes.NewBufferString("foobar") ok := t.HasSuffix(got, "bar", "checks %s", got) fmt.Println(ok) // Output: // true } func ExampleT_HasSuffix_error() { t := td.NewT(&testing.T{}) got := errors.New("foobar") ok := t.HasSuffix(got, "bar", "checks %s", got) fmt.Println(ok) // Output: // true } func ExampleT_Isa() { t := td.NewT(&testing.T{}) type TstStruct struct { Field int } got := TstStruct{Field: 1} ok := t.Isa(got, TstStruct{}, "checks got is a TstStruct") fmt.Println(ok) ok = t.Isa(got, &TstStruct{}, "checks got is a pointer on a TstStruct") fmt.Println(ok) ok = t.Isa(&got, &TstStruct{}, "checks &got is a pointer on a TstStruct") fmt.Println(ok) // Output: // true // false // true } func ExampleT_Isa_interface() { t := td.NewT(&testing.T{}) got := bytes.NewBufferString("foobar") ok := t.Isa(got, (*fmt.Stringer)(nil), "checks got implements fmt.Stringer interface") fmt.Println(ok) errGot := fmt.Errorf("An error #%d occurred", 123) ok = t.Isa(errGot, (*error)(nil), "checks errGot is a *error or implements error interface") fmt.Println(ok) // As nil, is passed below, it is not an interface but nil… So it // does not match errGot = nil ok = t.Isa(errGot, (*error)(nil), "checks errGot is a *error or implements error interface") fmt.Println(ok) // BUT if its address is passed, now it is OK as the types match ok = t.Isa(&errGot, (*error)(nil), "checks &errGot is a *error or implements error interface") fmt.Println(ok) // Output: // true // true // false // true } func ExampleT_JSON_basic() { t := td.NewT(&testing.T{}) got := &struct { Fullname string `json:"fullname"` Age int `json:"age"` }{ Fullname: "Bob", Age: 42, } ok := t.JSON(got, `{"age":42,"fullname":"Bob"}`, nil) fmt.Println("check got with age then fullname:", ok) ok = t.JSON(got, `{"fullname":"Bob","age":42}`, nil) fmt.Println("check got with fullname then age:", ok) ok = t.JSON(got, ` // This should be the JSON representation of a struct { // A person: "fullname": "Bob", // The name of this person "age": 42 /* The age of this person: - 42 of course - to demonstrate a multi-lines comment */ }`, nil) fmt.Println("check got with nicely formatted and commented JSON:", ok) ok = t.JSON(got, `{"fullname":"Bob","age":42,"gender":"male"}`, nil) fmt.Println("check got with gender field:", ok) ok = t.JSON(got, `{"fullname":"Bob"}`, nil) fmt.Println("check got with fullname only:", ok) ok = t.JSON(true, `true`, nil) fmt.Println("check boolean got is true:", ok) ok = t.JSON(42, `42`, nil) fmt.Println("check numeric got is 42:", ok) got = nil ok = t.JSON(got, `null`, nil) fmt.Println("check nil got is null:", ok) // Output: // check got with age then fullname: true // check got with fullname then age: true // check got with nicely formatted and commented JSON: true // check got with gender field: false // check got with fullname only: false // check boolean got is true: true // check numeric got is 42: true // check nil got is null: true } func ExampleT_JSON_placeholders() { t := td.NewT(&testing.T{}) type Person struct { Fullname string `json:"fullname"` Age int `json:"age"` Children []*Person `json:"children,omitempty"` } got := &Person{ Fullname: "Bob Foobar", Age: 42, } ok := t.JSON(got, `{"age": $1, "fullname": $2}`, []any{42, "Bob Foobar"}) fmt.Println("check got with numeric placeholders without operators:", ok) ok = t.JSON(got, `{"age": $1, "fullname": $2}`, []any{td.Between(40, 45), td.HasSuffix("Foobar")}) fmt.Println("check got with numeric placeholders:", ok) ok = t.JSON(got, `{"age": "$1", "fullname": "$2"}`, []any{td.Between(40, 45), td.HasSuffix("Foobar")}) fmt.Println("check got with double-quoted numeric placeholders:", ok) ok = t.JSON(got, `{"age": $age, "fullname": $name}`, []any{td.Tag("age", td.Between(40, 45)), td.Tag("name", td.HasSuffix("Foobar"))}) fmt.Println("check got with named placeholders:", ok) got.Children = []*Person{ {Fullname: "Alice", Age: 28}, {Fullname: "Brian", Age: 22}, } ok = t.JSON(got, `{"age": $age, "fullname": $name, "children": $children}`, []any{td.Tag("age", td.Between(40, 45)), td.Tag("name", td.HasSuffix("Foobar")), td.Tag("children", td.Bag( &Person{Fullname: "Brian", Age: 22}, &Person{Fullname: "Alice", Age: 28}, ))}) fmt.Println("check got w/named placeholders, and children w/go structs:", ok) ok = t.JSON(got, `{"age": Between($1, $2), "fullname": HasSuffix($suffix), "children": Len(2)}`, []any{40, 45, td.Tag("suffix", "Foobar")}) fmt.Println("check got w/num & named placeholders:", ok) // Output: // check got with numeric placeholders without operators: true // check got with numeric placeholders: true // check got with double-quoted numeric placeholders: true // check got with named placeholders: true // check got w/named placeholders, and children w/go structs: true // check got w/num & named placeholders: true } func ExampleT_JSON_embedding() { t := td.NewT(&testing.T{}) got := &struct { Fullname string `json:"fullname"` Age int `json:"age"` }{ Fullname: "Bob Foobar", Age: 42, } ok := t.JSON(got, `{"age": NotZero(), "fullname": NotEmpty()}`, nil) fmt.Println("check got with simple operators:", ok) ok = t.JSON(got, `{"age": $^NotZero, "fullname": $^NotEmpty}`, nil) fmt.Println("check got with operator shortcuts:", ok) ok = t.JSON(got, ` { "age": Between(40, 42, "]]"), // in ]40; 42] "fullname": All( HasPrefix("Bob"), HasSuffix("bar") // ← comma is optional here ) }`, nil) fmt.Println("check got with complex operators:", ok) ok = t.JSON(got, ` { "age": Between(40, 42, "]["), // in ]40; 42[ → 42 excluded "fullname": All( HasPrefix("Bob"), HasSuffix("bar"), ) }`, nil) fmt.Println("check got with complex operators:", ok) ok = t.JSON(got, ` { "age": Between($1, $2, $3), // in ]40; 42] "fullname": All( HasPrefix($4), HasSuffix("bar") // ← comma is optional here ) }`, []any{40, 42, td.BoundsOutIn, "Bob"}) fmt.Println("check got with complex operators, w/placeholder args:", ok) // Output: // check got with simple operators: true // check got with operator shortcuts: true // check got with complex operators: true // check got with complex operators: false // check got with complex operators, w/placeholder args: true } func ExampleT_JSON_rawStrings() { t := td.NewT(&testing.T{}) type details struct { Address string `json:"address"` Car string `json:"car"` } got := &struct { Fullname string `json:"fullname"` Age int `json:"age"` Details details `json:"details"` }{ Fullname: "Foo Bar", Age: 42, Details: details{ Address: "something", Car: "Peugeot", }, } ok := t.JSON(got, ` { "fullname": HasPrefix("Foo"), "age": Between(41, 43), "details": SuperMapOf({ "address": NotEmpty, // () are optional when no parameters "car": Any("Peugeot", "Tesla", "Jeep") // any of these }) }`, nil) fmt.Println("Original:", ok) ok = t.JSON(got, ` { "fullname": "$^HasPrefix(\"Foo\")", "age": "$^Between(41, 43)", "details": "$^SuperMapOf({\n\"address\": NotEmpty,\n\"car\": Any(\"Peugeot\", \"Tesla\", \"Jeep\")\n})" }`, nil) fmt.Println("JSON compliant:", ok) ok = t.JSON(got, ` { "fullname": "$^HasPrefix(\"Foo\")", "age": "$^Between(41, 43)", "details": "$^SuperMapOf({ \"address\": NotEmpty, // () are optional when no parameters \"car\": Any(\"Peugeot\", \"Tesla\", \"Jeep\") // any of these })" }`, nil) fmt.Println("JSON multilines strings:", ok) ok = t.JSON(got, ` { "fullname": "$^HasPrefix(r)", "age": "$^Between(41, 43)", "details": "$^SuperMapOf({ r
: NotEmpty, // () are optional when no parameters r: Any(r, r, r) // any of these })" }`, nil) fmt.Println("Raw strings:", ok) // Output: // Original: true // JSON compliant: true // JSON multilines strings: true // Raw strings: true } func ExampleT_JSON_file() { t := td.NewT(&testing.T{}) got := &struct { Fullname string `json:"fullname"` Age int `json:"age"` Gender string `json:"gender"` }{ Fullname: "Bob Foobar", Age: 42, Gender: "male", } tmpDir, err := os.MkdirTemp("", "") if err != nil { t.Fatal(err) } defer os.RemoveAll(tmpDir) // clean up filename := tmpDir + "/test.json" if err = os.WriteFile(filename, []byte(` { "fullname": "$name", "age": "$age", "gender": "$gender" }`), 0644); err != nil { t.Fatal(err) } // OK let's test with this file ok := t.JSON(got, filename, []any{td.Tag("name", td.HasPrefix("Bob")), td.Tag("age", td.Between(40, 45)), td.Tag("gender", td.Re(`^(male|female)\z`))}) fmt.Println("Full match from file name:", ok) // When the file is already open file, err := os.Open(filename) if err != nil { t.Fatal(err) } ok = t.JSON(got, file, []any{td.Tag("name", td.HasPrefix("Bob")), td.Tag("age", td.Between(40, 45)), td.Tag("gender", td.Re(`^(male|female)\z`))}) fmt.Println("Full match from io.Reader:", ok) // Output: // Full match from file name: true // Full match from io.Reader: true } func ExampleT_JSONPointer_rfc6901() { t := td.NewT(&testing.T{}) got := json.RawMessage(` { "foo": ["bar", "baz"], "": 0, "a/b": 1, "c%d": 2, "e^f": 3, "g|h": 4, "i\\j": 5, "k\"l": 6, " ": 7, "m~n": 8 }`) expected := map[string]any{ "foo": []any{"bar", "baz"}, "": 0, "a/b": 1, "c%d": 2, "e^f": 3, "g|h": 4, `i\j`: 5, `k"l`: 6, " ": 7, "m~n": 8, } ok := t.JSONPointer(got, "", expected) fmt.Println("Empty JSON pointer means all:", ok) ok = t.JSONPointer(got, `/foo`, []any{"bar", "baz"}) fmt.Println("Extract `foo` key:", ok) ok = t.JSONPointer(got, `/foo/0`, "bar") fmt.Println("First item of `foo` key slice:", ok) ok = t.JSONPointer(got, `/`, 0) fmt.Println("Empty key:", ok) ok = t.JSONPointer(got, `/a~1b`, 1) fmt.Println("Slash has to be escaped using `~1`:", ok) ok = t.JSONPointer(got, `/c%d`, 2) fmt.Println("% in key:", ok) ok = t.JSONPointer(got, `/e^f`, 3) fmt.Println("^ in key:", ok) ok = t.JSONPointer(got, `/g|h`, 4) fmt.Println("| in key:", ok) ok = t.JSONPointer(got, `/i\j`, 5) fmt.Println("Backslash in key:", ok) ok = t.JSONPointer(got, `/k"l`, 6) fmt.Println("Double-quote in key:", ok) ok = t.JSONPointer(got, `/ `, 7) fmt.Println("Space key:", ok) ok = t.JSONPointer(got, `/m~0n`, 8) fmt.Println("Tilde has to be escaped using `~0`:", ok) // Output: // Empty JSON pointer means all: true // Extract `foo` key: true // First item of `foo` key slice: true // Empty key: true // Slash has to be escaped using `~1`: true // % in key: true // ^ in key: true // | in key: true // Backslash in key: true // Double-quote in key: true // Space key: true // Tilde has to be escaped using `~0`: true } func ExampleT_JSONPointer_struct() { t := td.NewT(&testing.T{}) // Without json tags, encoding/json uses public fields name type Item struct { Name string Value int64 Next *Item } got := Item{ Name: "first", Value: 1, Next: &Item{ Name: "second", Value: 2, Next: &Item{ Name: "third", Value: 3, }, }, } ok := t.JSONPointer(got, "/Next/Next/Name", "third") fmt.Println("3rd item name is `third`:", ok) ok = t.JSONPointer(got, "/Next/Next/Value", td.Gte(int64(3))) fmt.Println("3rd item value is greater or equal than 3:", ok) ok = t.JSONPointer(got, "/Next", td.JSONPointer("/Next", td.JSONPointer("/Value", td.Gte(int64(3))))) fmt.Println("3rd item value is still greater or equal than 3:", ok) ok = t.JSONPointer(got, "/Next/Next/Next/Name", td.Ignore()) fmt.Println("4th item exists and has a name:", ok) // Struct comparison work with or without pointer: &Item{…} works too ok = t.JSONPointer(got, "/Next/Next", Item{ Name: "third", Value: 3, }) fmt.Println("3rd item full comparison:", ok) // Output: // 3rd item name is `third`: true // 3rd item value is greater or equal than 3: true // 3rd item value is still greater or equal than 3: true // 4th item exists and has a name: false // 3rd item full comparison: true } func ExampleT_JSONPointer_has_hasnt() { t := td.NewT(&testing.T{}) got := json.RawMessage(` { "name": "Bob", "age": 42, "children": [ { "name": "Alice", "age": 16 }, { "name": "Britt", "age": 21, "children": [ { "name": "John", "age": 1 } ] } ] }`) // Has Bob some children? ok := t.JSONPointer(got, "/children", td.Len(td.Gt(0))) fmt.Println("Bob has at least one child:", ok) // But checking "children" exists is enough here ok = t.JSONPointer(got, "/children/0/children", td.Ignore()) fmt.Println("Alice has children:", ok) ok = t.JSONPointer(got, "/children/1/children", td.Ignore()) fmt.Println("Britt has children:", ok) // The reverse can be checked too ok = t.Cmp(got, td.Not(td.JSONPointer("/children/0/children", td.Ignore()))) fmt.Println("Alice hasn't children:", ok) ok = t.Cmp(got, td.Not(td.JSONPointer("/children/1/children", td.Ignore()))) fmt.Println("Britt hasn't children:", ok) // Output: // Bob has at least one child: true // Alice has children: false // Britt has children: true // Alice hasn't children: true // Britt hasn't children: false } func ExampleT_Keys() { t := td.NewT(&testing.T{}) got := map[string]int{"foo": 1, "bar": 2, "zip": 3} // Keys tests keys in an ordered manner ok := t.Keys(got, []string{"bar", "foo", "zip"}) fmt.Println("All sorted keys are found:", ok) // If the expected keys are not ordered, it fails ok = t.Keys(got, []string{"zip", "bar", "foo"}) fmt.Println("All unsorted keys are found:", ok) // To circumvent that, one can use Bag operator ok = t.Keys(got, td.Bag("zip", "bar", "foo")) fmt.Println("All unsorted keys are found, with the help of Bag operator:", ok) // Check that each key is 3 bytes long ok = t.Keys(got, td.ArrayEach(td.Len(3))) fmt.Println("Each key is 3 bytes long:", ok) // Output: // All sorted keys are found: true // All unsorted keys are found: false // All unsorted keys are found, with the help of Bag operator: true // Each key is 3 bytes long: true } func ExampleT_Last_classic() { t := td.NewT(&testing.T{}) got := []int{-3, -2, -1, 0, 1, 2, 3} ok := t.Last(got, td.Lt(0), -1) fmt.Println("last negative number is -1:", ok) isEven := func(x int) bool { return x%2 == 0 } ok = t.Last(got, isEven, 2) fmt.Println("last even number is 2:", ok) ok = t.Last(got, isEven, td.Gt(0)) fmt.Println("last even number is > 0:", ok) ok = t.Last(got, isEven, td.Code(isEven)) fmt.Println("last even number is well even:", ok) // Output: // last negative number is -1: true // last even number is 2: true // last even number is > 0: true // last even number is well even: true } func ExampleT_Last_empty() { t := td.NewT(&testing.T{}) ok := t.Last(([]int)(nil), td.Gt(0), td.Gt(0)) fmt.Println("last in nil slice:", ok) ok = t.Last([]int{}, td.Gt(0), td.Gt(0)) fmt.Println("last in empty slice:", ok) ok = t.Last(&[]int{}, td.Gt(0), td.Gt(0)) fmt.Println("last in empty pointed slice:", ok) ok = t.Last([0]int{}, td.Gt(0), td.Gt(0)) fmt.Println("last in empty array:", ok) // Output: // last in nil slice: false // last in empty slice: false // last in empty pointed slice: false // last in empty array: false } func ExampleT_Last_struct() { t := td.NewT(&testing.T{}) type Person struct { Fullname string `json:"fullname"` Age int `json:"age"` } got := []*Person{ { Fullname: "Bob Foobar", Age: 42, }, { Fullname: "Alice Bingo", Age: 37, }, } ok := t.Last(got, td.Smuggle("Age", td.Gt(30)), td.Smuggle("Fullname", "Alice Bingo")) fmt.Println("last person.Age > 30 → Alice:", ok) ok = t.Last(got, td.JSONPointer("/age", td.Gt(30)), td.SuperJSONOf(`{"fullname":"Alice Bingo"}`)) fmt.Println("last person.Age > 30 → Alice, using JSON:", ok) ok = t.Last(got, td.JSONPointer("/age", td.Gt(30)), td.JSONPointer("/fullname", td.HasPrefix("Alice"))) fmt.Println("first person.Age > 30 → Alice, using JSONPointer:", ok) // Output: // last person.Age > 30 → Alice: true // last person.Age > 30 → Alice, using JSON: true // first person.Age > 30 → Alice, using JSONPointer: true } func ExampleT_CmpLax() { t := td.NewT(&testing.T{}) gotInt64 := int64(1234) gotInt32 := int32(1235) type myInt uint16 gotMyInt := myInt(1236) expected := td.Between(1230, 1240) // int type here ok := t.CmpLax(gotInt64, expected) fmt.Println("int64 got between ints [1230 .. 1240]:", ok) ok = t.CmpLax(gotInt32, expected) fmt.Println("int32 got between ints [1230 .. 1240]:", ok) ok = t.CmpLax(gotMyInt, expected) fmt.Println("myInt got between ints [1230 .. 1240]:", ok) // Output: // int64 got between ints [1230 .. 1240]: true // int32 got between ints [1230 .. 1240]: true // myInt got between ints [1230 .. 1240]: true } func ExampleT_Len_slice() { t := td.NewT(&testing.T{}) got := []int{11, 22, 33} ok := t.Len(got, 3, "checks %v len is 3", got) fmt.Println(ok) ok = t.Len(got, 0, "checks %v len is 0", got) fmt.Println(ok) got = nil ok = t.Len(got, 0, "checks %v len is 0", got) fmt.Println(ok) // Output: // true // false // true } func ExampleT_Len_map() { t := td.NewT(&testing.T{}) got := map[int]bool{11: true, 22: false, 33: false} ok := t.Len(got, 3, "checks %v len is 3", got) fmt.Println(ok) ok = t.Len(got, 0, "checks %v len is 0", got) fmt.Println(ok) got = nil ok = t.Len(got, 0, "checks %v len is 0", got) fmt.Println(ok) // Output: // true // false // true } func ExampleT_Len_operatorSlice() { t := td.NewT(&testing.T{}) got := []int{11, 22, 33} ok := t.Len(got, td.Between(3, 8), "checks %v len is in [3 .. 8]", got) fmt.Println(ok) ok = t.Len(got, td.Lt(5), "checks %v len is < 5", got) fmt.Println(ok) // Output: // true // true } func ExampleT_Len_operatorMap() { t := td.NewT(&testing.T{}) got := map[int]bool{11: true, 22: false, 33: false} ok := t.Len(got, td.Between(3, 8), "checks %v len is in [3 .. 8]", got) fmt.Println(ok) ok = t.Len(got, td.Gte(3), "checks %v len is ≥ 3", got) fmt.Println(ok) // Output: // true // true } func ExampleT_Lt_int() { t := td.NewT(&testing.T{}) got := 156 ok := t.Lt(got, 157, "checks %v is < 157", got) fmt.Println(ok) ok = t.Lt(got, 156, "checks %v is < 156", got) fmt.Println(ok) // Output: // true // false } func ExampleT_Lt_string() { t := td.NewT(&testing.T{}) got := "abc" ok := t.Lt(got, "abd", `checks "%v" is < "abd"`, got) fmt.Println(ok) ok = t.Lt(got, "abc", `checks "%v" is < "abc"`, got) fmt.Println(ok) // Output: // true // false } func ExampleT_Lte_int() { t := td.NewT(&testing.T{}) got := 156 ok := t.Lte(got, 156, "checks %v is ≤ 156", got) fmt.Println(ok) ok = t.Lte(got, 157, "checks %v is ≤ 157", got) fmt.Println(ok) ok = t.Lte(got, 155, "checks %v is ≤ 155", got) fmt.Println(ok) // Output: // true // true // false } func ExampleT_Lte_string() { t := td.NewT(&testing.T{}) got := "abc" ok := t.Lte(got, "abc", `checks "%v" is ≤ "abc"`, got) fmt.Println(ok) ok = t.Lte(got, "abd", `checks "%v" is ≤ "abd"`, got) fmt.Println(ok) ok = t.Lte(got, "abb", `checks "%v" is ≤ "abb"`, got) fmt.Println(ok) // Output: // true // true // false } func ExampleT_Map_map() { t := td.NewT(&testing.T{}) got := map[string]int{"foo": 12, "bar": 42, "zip": 89} ok := t.Map(got, map[string]int{"bar": 42}, td.MapEntries{"foo": td.Lt(15), "zip": td.Ignore()}, "checks map %v", got) fmt.Println(ok) ok = t.Map(got, map[string]int{}, td.MapEntries{"bar": 42, "foo": td.Lt(15), "zip": td.Ignore()}, "checks map %v", got) fmt.Println(ok) ok = t.Map(got, (map[string]int)(nil), td.MapEntries{"bar": 42, "foo": td.Lt(15), "zip": td.Ignore()}, "checks map %v", got) fmt.Println(ok) // Output: // true // true // true } func ExampleT_Map_typedMap() { t := td.NewT(&testing.T{}) type MyMap map[string]int got := MyMap{"foo": 12, "bar": 42, "zip": 89} ok := t.Map(got, MyMap{"bar": 42}, td.MapEntries{"foo": td.Lt(15), "zip": td.Ignore()}, "checks typed map %v", got) fmt.Println(ok) ok = t.Map(&got, &MyMap{"bar": 42}, td.MapEntries{"foo": td.Lt(15), "zip": td.Ignore()}, "checks pointer on typed map %v", got) fmt.Println(ok) ok = t.Map(&got, &MyMap{}, td.MapEntries{"bar": 42, "foo": td.Lt(15), "zip": td.Ignore()}, "checks pointer on typed map %v", got) fmt.Println(ok) ok = t.Map(&got, (*MyMap)(nil), td.MapEntries{"bar": 42, "foo": td.Lt(15), "zip": td.Ignore()}, "checks pointer on typed map %v", got) fmt.Println(ok) // Output: // true // true // true // true } func ExampleT_MapEach_map() { t := td.NewT(&testing.T{}) got := map[string]int{"foo": 12, "bar": 42, "zip": 89} ok := t.MapEach(got, td.Between(10, 90), "checks each value of map %v is in [10 .. 90]", got) fmt.Println(ok) // Output: // true } func ExampleT_MapEach_typedMap() { t := td.NewT(&testing.T{}) type MyMap map[string]int got := MyMap{"foo": 12, "bar": 42, "zip": 89} ok := t.MapEach(got, td.Between(10, 90), "checks each value of typed map %v is in [10 .. 90]", got) fmt.Println(ok) ok = t.MapEach(&got, td.Between(10, 90), "checks each value of typed map pointer %v is in [10 .. 90]", got) fmt.Println(ok) // Output: // true // true } func ExampleT_N() { t := td.NewT(&testing.T{}) got := 1.12345 ok := t.N(got, 1.1234, 0.00006, "checks %v = 1.1234 ± 0.00006", got) fmt.Println(ok) // Output: // true } func ExampleT_NaN_float32() { t := td.NewT(&testing.T{}) got := float32(math.NaN()) ok := t.NaN(got, "checks %v is not-a-number", got) fmt.Println("float32(math.NaN()) is float32 not-a-number:", ok) got = 12 ok = t.NaN(got, "checks %v is not-a-number", got) fmt.Println("float32(12) is float32 not-a-number:", ok) // Output: // float32(math.NaN()) is float32 not-a-number: true // float32(12) is float32 not-a-number: false } func ExampleT_NaN_float64() { t := td.NewT(&testing.T{}) got := math.NaN() ok := t.NaN(got, "checks %v is not-a-number", got) fmt.Println("math.NaN() is not-a-number:", ok) got = 12 ok = t.NaN(got, "checks %v is not-a-number", got) fmt.Println("float64(12) is not-a-number:", ok) // math.NaN() is not-a-number: true // float64(12) is not-a-number: false } func ExampleT_Nil() { t := td.NewT(&testing.T{}) var got fmt.Stringer // interface // nil value can be compared directly with nil, no need of Nil() here ok := t.Cmp(got, nil) fmt.Println(ok) // But it works with Nil() anyway ok = t.Nil(got) fmt.Println(ok) got = (*bytes.Buffer)(nil) // In the case of an interface containing a nil pointer, comparing // with nil fails, as the interface is not nil ok = t.Cmp(got, nil) fmt.Println(ok) // In this case Nil() succeed ok = t.Nil(got) fmt.Println(ok) // Output: // true // true // false // true } func ExampleT_None() { t := td.NewT(&testing.T{}) got := 18 ok := t.None(got, []any{0, 10, 20, 30, td.Between(100, 199)}, "checks %v is non-null, and ≠ 10, 20 & 30, and not in [100-199]", got) fmt.Println(ok) got = 20 ok = t.None(got, []any{0, 10, 20, 30, td.Between(100, 199)}, "checks %v is non-null, and ≠ 10, 20 & 30, and not in [100-199]", got) fmt.Println(ok) got = 142 ok = t.None(got, []any{0, 10, 20, 30, td.Between(100, 199)}, "checks %v is non-null, and ≠ 10, 20 & 30, and not in [100-199]", got) fmt.Println(ok) prime := td.Flatten([]int{1, 2, 3, 5, 7, 11, 13}) even := td.Flatten([]int{2, 4, 6, 8, 10, 12, 14}) for _, got := range [...]int{9, 3, 8, 15} { ok = t.None(got, []any{prime, even, td.Gt(14)}, "checks %v is not prime number, nor an even number and not > 14") fmt.Printf("%d → %t\n", got, ok) } // Output: // true // false // false // 9 → true // 3 → false // 8 → false // 15 → false } func ExampleT_Not() { t := td.NewT(&testing.T{}) got := 42 ok := t.Not(got, 0, "checks %v is non-null", got) fmt.Println(ok) ok = t.Not(got, td.Between(10, 30), "checks %v is not in [10 .. 30]", got) fmt.Println(ok) got = 0 ok = t.Not(got, 0, "checks %v is non-null", got) fmt.Println(ok) // Output: // true // true // false } func ExampleT_NotAny() { t := td.NewT(&testing.T{}) got := []int{4, 5, 9, 42} ok := t.NotAny(got, []any{3, 6, 8, 41, 43}, "checks %v contains no item listed in NotAny()", got) fmt.Println(ok) ok = t.NotAny(got, []any{3, 6, 8, 42, 43}, "checks %v contains no item listed in NotAny()", got) fmt.Println(ok) // When expected is already a non-[]any slice, it cannot be // flattened directly using notExpected... without copying it to a new // []any slice, then use td.Flatten! notExpected := []int{3, 6, 8, 41, 43} ok = t.NotAny(got, []any{td.Flatten(notExpected)}, "checks %v contains no item listed in notExpected", got) fmt.Println(ok) // Output: // true // false // true } func ExampleT_NotEmpty() { t := td.NewT(&testing.T{}) ok := t.NotEmpty(nil) // fails, as nil is considered empty fmt.Println(ok) ok = t.NotEmpty("foobar") fmt.Println(ok) // Fails as 0 is a number, so not empty. Use NotZero() instead ok = t.NotEmpty(0) fmt.Println(ok) ok = t.NotEmpty(map[string]int{"foobar": 42}) fmt.Println(ok) ok = t.NotEmpty([]int{1}) fmt.Println(ok) ok = t.NotEmpty([3]int{}) // succeeds, NotEmpty() is not NotZero()! fmt.Println(ok) // Output: // false // true // false // true // true // true } func ExampleT_NotEmpty_pointers() { t := td.NewT(&testing.T{}) type MySlice []int ok := t.NotEmpty(MySlice{12}) fmt.Println(ok) ok = t.NotEmpty(&MySlice{12}) // Ptr() not needed fmt.Println(ok) l1 := &MySlice{12} l2 := &l1 l3 := &l2 ok = t.NotEmpty(&l3) fmt.Println(ok) // Works the same for array, map, channel and string // But not for others types as: type MyStruct struct { Value int } ok = t.NotEmpty(&MyStruct{}) // fails, use NotZero() instead fmt.Println(ok) // Output: // true // true // true // false } func ExampleT_NotNaN_float32() { t := td.NewT(&testing.T{}) got := float32(math.NaN()) ok := t.NotNaN(got, "checks %v is not-a-number", got) fmt.Println("float32(math.NaN()) is NOT float32 not-a-number:", ok) got = 12 ok = t.NotNaN(got, "checks %v is not-a-number", got) fmt.Println("float32(12) is NOT float32 not-a-number:", ok) // Output: // float32(math.NaN()) is NOT float32 not-a-number: false // float32(12) is NOT float32 not-a-number: true } func ExampleT_NotNaN_float64() { t := td.NewT(&testing.T{}) got := math.NaN() ok := t.NotNaN(got, "checks %v is not-a-number", got) fmt.Println("math.NaN() is not-a-number:", ok) got = 12 ok = t.NotNaN(got, "checks %v is not-a-number", got) fmt.Println("float64(12) is not-a-number:", ok) // math.NaN() is NOT not-a-number: false // float64(12) is NOT not-a-number: true } func ExampleT_NotNil() { t := td.NewT(&testing.T{}) var got fmt.Stringer = &bytes.Buffer{} // nil value can be compared directly with Not(nil), no need of NotNil() here ok := t.Cmp(got, td.Not(nil)) fmt.Println(ok) // But it works with NotNil() anyway ok = t.NotNil(got) fmt.Println(ok) got = (*bytes.Buffer)(nil) // In the case of an interface containing a nil pointer, comparing // with Not(nil) succeeds, as the interface is not nil ok = t.Cmp(got, td.Not(nil)) fmt.Println(ok) // In this case NotNil() fails ok = t.NotNil(got) fmt.Println(ok) // Output: // true // true // true // false } func ExampleT_NotZero() { t := td.NewT(&testing.T{}) ok := t.NotZero(0) // fails fmt.Println(ok) ok = t.NotZero(float64(0)) // fails fmt.Println(ok) ok = t.NotZero(12) fmt.Println(ok) ok = t.NotZero((map[string]int)(nil)) // fails, as nil fmt.Println(ok) ok = t.NotZero(map[string]int{}) // succeeds, as not nil fmt.Println(ok) ok = t.NotZero(([]int)(nil)) // fails, as nil fmt.Println(ok) ok = t.NotZero([]int{}) // succeeds, as not nil fmt.Println(ok) ok = t.NotZero([3]int{}) // fails fmt.Println(ok) ok = t.NotZero([3]int{0, 1}) // succeeds, DATA[1] is not 0 fmt.Println(ok) ok = t.NotZero(bytes.Buffer{}) // fails fmt.Println(ok) ok = t.NotZero(&bytes.Buffer{}) // succeeds, as pointer not nil fmt.Println(ok) ok = t.Cmp(&bytes.Buffer{}, td.Ptr(td.NotZero())) // fails as deref by Ptr() fmt.Println(ok) // Output: // false // false // true // false // true // false // true // false // true // false // true // false } func ExampleT_PPtr() { t := td.NewT(&testing.T{}) num := 12 got := &num ok := t.PPtr(&got, 12) fmt.Println(ok) ok = t.PPtr(&got, td.Between(4, 15)) fmt.Println(ok) // Output: // true // true } func ExampleT_Ptr() { t := td.NewT(&testing.T{}) got := 12 ok := t.Ptr(&got, 12) fmt.Println(ok) ok = t.Ptr(&got, td.Between(4, 15)) fmt.Println(ok) // Output: // true // true } func ExampleT_Re() { t := td.NewT(&testing.T{}) got := "foo bar" ok := t.Re(got, "(zip|bar)$", nil, "checks value %s", got) fmt.Println(ok) got = "bar foo" ok = t.Re(got, "(zip|bar)$", nil, "checks value %s", got) fmt.Println(ok) // Output: // true // false } func ExampleT_Re_stringer() { t := td.NewT(&testing.T{}) // bytes.Buffer implements fmt.Stringer got := bytes.NewBufferString("foo bar") ok := t.Re(got, "(zip|bar)$", nil, "checks value %s", got) fmt.Println(ok) // Output: // true } func ExampleT_Re_error() { t := td.NewT(&testing.T{}) got := errors.New("foo bar") ok := t.Re(got, "(zip|bar)$", nil, "checks value %s", got) fmt.Println(ok) // Output: // true } func ExampleT_Re_capture() { t := td.NewT(&testing.T{}) got := "foo bar biz" ok := t.Re(got, `^(\w+) (\w+) (\w+)$`, td.Set("biz", "foo", "bar"), "checks value %s", got) fmt.Println(ok) got = "foo bar! biz" ok = t.Re(got, `^(\w+) (\w+) (\w+)$`, td.Set("biz", "foo", "bar"), "checks value %s", got) fmt.Println(ok) // Output: // true // false } func ExampleT_Re_compiled() { t := td.NewT(&testing.T{}) expected := regexp.MustCompile("(zip|bar)$") got := "foo bar" ok := t.Re(got, expected, nil, "checks value %s", got) fmt.Println(ok) got = "bar foo" ok = t.Re(got, expected, nil, "checks value %s", got) fmt.Println(ok) // Output: // true // false } func ExampleT_Re_compiledStringer() { t := td.NewT(&testing.T{}) expected := regexp.MustCompile("(zip|bar)$") // bytes.Buffer implements fmt.Stringer got := bytes.NewBufferString("foo bar") ok := t.Re(got, expected, nil, "checks value %s", got) fmt.Println(ok) // Output: // true } func ExampleT_Re_compiledError() { t := td.NewT(&testing.T{}) expected := regexp.MustCompile("(zip|bar)$") got := errors.New("foo bar") ok := t.Re(got, expected, nil, "checks value %s", got) fmt.Println(ok) // Output: // true } func ExampleT_Re_compiledCapture() { t := td.NewT(&testing.T{}) expected := regexp.MustCompile(`^(\w+) (\w+) (\w+)$`) got := "foo bar biz" ok := t.Re(got, expected, td.Set("biz", "foo", "bar"), "checks value %s", got) fmt.Println(ok) got = "foo bar! biz" ok = t.Re(got, expected, td.Set("biz", "foo", "bar"), "checks value %s", got) fmt.Println(ok) // Output: // true // false } func ExampleT_ReAll_capture() { t := td.NewT(&testing.T{}) got := "foo bar biz" ok := t.ReAll(got, `(\w+)`, td.Set("biz", "foo", "bar"), "checks value %s", got) fmt.Println(ok) // Matches, but all catured groups do not match Set got = "foo BAR biz" ok = t.ReAll(got, `(\w+)`, td.Set("biz", "foo", "bar"), "checks value %s", got) fmt.Println(ok) // Output: // true // false } func ExampleT_ReAll_captureComplex() { t := td.NewT(&testing.T{}) got := "11 45 23 56 85 96" ok := t.ReAll(got, `(\d+)`, td.ArrayEach(td.Code(func(num string) bool { n, err := strconv.Atoi(num) return err == nil && n > 10 && n < 100 })), "checks value %s", got) fmt.Println(ok) // Matches, but 11 is not greater than 20 ok = t.ReAll(got, `(\d+)`, td.ArrayEach(td.Code(func(num string) bool { n, err := strconv.Atoi(num) return err == nil && n > 20 && n < 100 })), "checks value %s", got) fmt.Println(ok) // Output: // true // false } func ExampleT_ReAll_compiledCapture() { t := td.NewT(&testing.T{}) expected := regexp.MustCompile(`(\w+)`) got := "foo bar biz" ok := t.ReAll(got, expected, td.Set("biz", "foo", "bar"), "checks value %s", got) fmt.Println(ok) // Matches, but all catured groups do not match Set got = "foo BAR biz" ok = t.ReAll(got, expected, td.Set("biz", "foo", "bar"), "checks value %s", got) fmt.Println(ok) // Output: // true // false } func ExampleT_ReAll_compiledCaptureComplex() { t := td.NewT(&testing.T{}) expected := regexp.MustCompile(`(\d+)`) got := "11 45 23 56 85 96" ok := t.ReAll(got, expected, td.ArrayEach(td.Code(func(num string) bool { n, err := strconv.Atoi(num) return err == nil && n > 10 && n < 100 })), "checks value %s", got) fmt.Println(ok) // Matches, but 11 is not greater than 20 ok = t.ReAll(got, expected, td.ArrayEach(td.Code(func(num string) bool { n, err := strconv.Atoi(num) return err == nil && n > 20 && n < 100 })), "checks value %s", got) fmt.Println(ok) // Output: // true // false } func ExampleT_Recv_basic() { t := td.NewT(&testing.T{}) got := make(chan int, 3) ok := t.Recv(got, td.RecvNothing, 0) fmt.Println("nothing to receive:", ok) got <- 1 got <- 2 got <- 3 close(got) ok = t.Recv(got, 1, 0) fmt.Println("1st receive is 1:", ok) ok = t.Cmp(got, td.All( td.Recv(2), td.Recv(td.Between(3, 4)), td.Recv(td.RecvClosed), )) fmt.Println("next receives are 2, 3 then closed:", ok) ok = t.Recv(got, td.RecvNothing, 0) fmt.Println("nothing to receive:", ok) // Output: // nothing to receive: true // 1st receive is 1: true // next receives are 2, 3 then closed: true // nothing to receive: false } func ExampleT_Recv_channelPointer() { t := td.NewT(&testing.T{}) got := make(chan int, 3) ok := t.Recv(got, td.RecvNothing, 0) fmt.Println("nothing to receive:", ok) got <- 1 got <- 2 got <- 3 close(got) ok = t.Recv(&got, 1, 0) fmt.Println("1st receive is 1:", ok) ok = t.Cmp(&got, td.All( td.Recv(2), td.Recv(td.Between(3, 4)), td.Recv(td.RecvClosed), )) fmt.Println("next receives are 2, 3 then closed:", ok) ok = t.Recv(got, td.RecvNothing, 0) fmt.Println("nothing to receive:", ok) // Output: // nothing to receive: true // 1st receive is 1: true // next receives are 2, 3 then closed: true // nothing to receive: false } func ExampleT_Recv_withTimeout() { t := td.NewT(&testing.T{}) got := make(chan int, 1) tick := make(chan struct{}) go func() { // ① <-tick time.Sleep(100 * time.Millisecond) got <- 0 // ② <-tick time.Sleep(100 * time.Millisecond) got <- 1 // ③ <-tick time.Sleep(100 * time.Millisecond) close(got) }() t.Recv(got, td.RecvNothing, 0) // ① tick <- struct{}{} ok := t.Recv(got, td.RecvNothing, 0) fmt.Println("① RecvNothing:", ok) ok = t.Recv(got, 0, 150*time.Millisecond) fmt.Println("① receive 0 w/150ms timeout:", ok) ok = t.Recv(got, td.RecvNothing, 0) fmt.Println("① RecvNothing:", ok) // ② tick <- struct{}{} ok = t.Recv(got, td.RecvNothing, 0) fmt.Println("② RecvNothing:", ok) ok = t.Recv(got, 1, 150*time.Millisecond) fmt.Println("② receive 1 w/150ms timeout:", ok) ok = t.Recv(got, td.RecvNothing, 0) fmt.Println("② RecvNothing:", ok) // ③ tick <- struct{}{} ok = t.Recv(got, td.RecvNothing, 0) fmt.Println("③ RecvNothing:", ok) ok = t.Recv(got, td.RecvClosed, 150*time.Millisecond) fmt.Println("③ check closed w/150ms timeout:", ok) // Output: // ① RecvNothing: true // ① receive 0 w/150ms timeout: true // ① RecvNothing: true // ② RecvNothing: true // ② receive 1 w/150ms timeout: true // ② RecvNothing: true // ③ RecvNothing: true // ③ check closed w/150ms timeout: true } func ExampleT_Recv_nilChannel() { t := td.NewT(&testing.T{}) var ch chan int ok := t.Recv(ch, td.RecvNothing, 0) fmt.Println("nothing to receive from nil channel:", ok) ok = t.Recv(ch, 42, 0) fmt.Println("something to receive from nil channel:", ok) ok = t.Recv(ch, td.RecvClosed, 0) fmt.Println("is a nil channel closed:", ok) // Output: // nothing to receive from nil channel: true // something to receive from nil channel: false // is a nil channel closed: false } func ExampleT_Set() { t := td.NewT(&testing.T{}) got := []int{1, 3, 5, 8, 8, 1, 2} // Matches as all items are present, ignoring duplicates ok := t.Set(got, []any{1, 2, 3, 5, 8}, "checks all items are present, in any order") fmt.Println(ok) // Duplicates are ignored in a Set ok = t.Set(got, []any{1, 2, 2, 2, 2, 2, 3, 5, 8}, "checks all items are present, in any order") fmt.Println(ok) // Tries its best to not raise an error when a value can be matched // by several Set entries ok = t.Set(got, []any{td.Between(1, 4), 3, td.Between(2, 10)}, "checks all items are present, in any order") fmt.Println(ok) // When expected is already a non-[]any slice, it cannot be // flattened directly using expected... without copying it to a new // []any slice, then use td.Flatten! expected := []int{1, 2, 3, 5, 8} ok = t.Set(got, []any{td.Flatten(expected)}, "checks all expected items are present, in any order") fmt.Println(ok) // Output: // true // true // true // true } func ExampleT_Shallow() { t := td.NewT(&testing.T{}) type MyStruct struct { Value int } data := MyStruct{Value: 12} got := &data ok := t.Shallow(got, &data, "checks pointers only, not contents") fmt.Println(ok) // Same contents, but not same pointer ok = t.Shallow(got, &MyStruct{Value: 12}, "checks pointers only, not contents") fmt.Println(ok) // Output: // true // false } func ExampleT_Shallow_slice() { t := td.NewT(&testing.T{}) back := []int{1, 2, 3, 1, 2, 3} a := back[:3] b := back[3:] ok := t.Shallow(a, back) fmt.Println("are ≠ but share the same area:", ok) ok = t.Shallow(b, back) fmt.Println("are = but do not point to same area:", ok) // Output: // are ≠ but share the same area: true // are = but do not point to same area: false } func ExampleT_Shallow_string() { t := td.NewT(&testing.T{}) back := "foobarfoobar" a := back[:6] b := back[6:] ok := t.Shallow(a, back) fmt.Println("are ≠ but share the same area:", ok) ok = t.Shallow(b, a) fmt.Println("are = but do not point to same area:", ok) // Output: // are ≠ but share the same area: true // are = but do not point to same area: false } func ExampleT_Slice_slice() { t := td.NewT(&testing.T{}) got := []int{42, 58, 26} ok := t.Slice(got, []int{42}, td.ArrayEntries{1: 58, 2: td.Ignore()}, "checks slice %v", got) fmt.Println(ok) ok = t.Slice(got, []int{}, td.ArrayEntries{0: 42, 1: 58, 2: td.Ignore()}, "checks slice %v", got) fmt.Println(ok) ok = t.Slice(got, ([]int)(nil), td.ArrayEntries{0: 42, 1: 58, 2: td.Ignore()}, "checks slice %v", got) fmt.Println(ok) // Output: // true // true // true } func ExampleT_Slice_typedSlice() { t := td.NewT(&testing.T{}) type MySlice []int got := MySlice{42, 58, 26} ok := t.Slice(got, MySlice{42}, td.ArrayEntries{1: 58, 2: td.Ignore()}, "checks typed slice %v", got) fmt.Println(ok) ok = t.Slice(&got, &MySlice{42}, td.ArrayEntries{1: 58, 2: td.Ignore()}, "checks pointer on typed slice %v", got) fmt.Println(ok) ok = t.Slice(&got, &MySlice{}, td.ArrayEntries{0: 42, 1: 58, 2: td.Ignore()}, "checks pointer on typed slice %v", got) fmt.Println(ok) ok = t.Slice(&got, (*MySlice)(nil), td.ArrayEntries{0: 42, 1: 58, 2: td.Ignore()}, "checks pointer on typed slice %v", got) fmt.Println(ok) // Output: // true // true // true // true } func ExampleT_Smuggle_convert() { t := td.NewT(&testing.T{}) got := int64(123) ok := t.Smuggle(got, func(n int64) int { return int(n) }, 123, "checks int64 got against an int value") fmt.Println(ok) ok = t.Smuggle("123", func(numStr string) (int, bool) { n, err := strconv.Atoi(numStr) return n, err == nil }, td.Between(120, 130), "checks that number in %#v is in [120 .. 130]") fmt.Println(ok) ok = t.Smuggle("123", func(numStr string) (int, bool, string) { n, err := strconv.Atoi(numStr) if err != nil { return 0, false, "string must contain a number" } return n, true, "" }, td.Between(120, 130), "checks that number in %#v is in [120 .. 130]") fmt.Println(ok) ok = t.Smuggle("123", func(numStr string) (int, error) { //nolint: gocritic return strconv.Atoi(numStr) }, td.Between(120, 130), "checks that number in %#v is in [120 .. 130]") fmt.Println(ok) // Short version :) ok = t.Smuggle("123", strconv.Atoi, td.Between(120, 130), "checks that number in %#v is in [120 .. 130]") fmt.Println(ok) // Output: // true // true // true // true // true } func ExampleT_Smuggle_lax() { t := td.NewT(&testing.T{}) // got is an int16 and Smuggle func input is an int64: it is OK got := int(123) ok := t.Smuggle(got, func(n int64) uint32 { return uint32(n) }, uint32(123)) fmt.Println("got int16(123) → smuggle via int64 → uint32(123):", ok) // Output: // got int16(123) → smuggle via int64 → uint32(123): true } func ExampleT_Smuggle_auto_unmarshal() { t := td.NewT(&testing.T{}) // Automatically json.Unmarshal to compare got := []byte(`{"a":1,"b":2}`) ok := t.Smuggle(got, func(b json.RawMessage) (r map[string]int, err error) { err = json.Unmarshal(b, &r) return }, map[string]int{ "a": 1, "b": 2, }) fmt.Println("JSON contents is OK:", ok) // Output: // JSON contents is OK: true } func ExampleT_Smuggle_cast() { t := td.NewT(&testing.T{}) // A string containing JSON got := `{ "foo": 123 }` // Automatically cast a string to a json.RawMessage so td.JSON can operate ok := t.Smuggle(got, json.RawMessage{}, td.JSON(`{"foo":123}`)) fmt.Println("JSON contents in string is OK:", ok) // Automatically read from io.Reader to a json.RawMessage ok = t.Smuggle(bytes.NewReader([]byte(got)), json.RawMessage{}, td.JSON(`{"foo":123}`)) fmt.Println("JSON contents just read is OK:", ok) // Output: // JSON contents in string is OK: true // JSON contents just read is OK: true } func ExampleT_Smuggle_complex() { t := td.NewT(&testing.T{}) // No end date but a start date and a duration type StartDuration struct { StartDate time.Time Duration time.Duration } // Checks that end date is between 17th and 19th February both at 0h // for each of these durations in hours for _, duration := range []time.Duration{48 * time.Hour, 72 * time.Hour, 96 * time.Hour} { got := StartDuration{ StartDate: time.Date(2018, time.February, 14, 12, 13, 14, 0, time.UTC), Duration: duration, } // Simplest way, but in case of Between() failure, error will be bound // to DATA, not very clear... ok := t.Smuggle(got, func(sd StartDuration) time.Time { return sd.StartDate.Add(sd.Duration) }, td.Between( time.Date(2018, time.February, 17, 0, 0, 0, 0, time.UTC), time.Date(2018, time.February, 19, 0, 0, 0, 0, time.UTC))) fmt.Println(ok) // Name the computed value "ComputedEndDate" to render a Between() failure // more understandable, so error will be bound to DATA.ComputedEndDate ok = t.Smuggle(got, func(sd StartDuration) td.SmuggledGot { return td.SmuggledGot{ Name: "ComputedEndDate", Got: sd.StartDate.Add(sd.Duration), } }, td.Between( time.Date(2018, time.February, 17, 0, 0, 0, 0, time.UTC), time.Date(2018, time.February, 19, 0, 0, 0, 0, time.UTC))) fmt.Println(ok) } // Output: // false // false // true // true // true // true } func ExampleT_Smuggle_interface() { t := td.NewT(&testing.T{}) gotTime, err := time.Parse(time.RFC3339, "2018-05-23T12:13:14Z") if err != nil { t.Fatal(err) } // Do not check the struct itself, but its stringified form ok := t.Smuggle(gotTime, func(s fmt.Stringer) string { return s.String() }, "2018-05-23 12:13:14 +0000 UTC") fmt.Println("stringified time.Time OK:", ok) // If got does not implement the fmt.Stringer interface, it fails // without calling the Smuggle func type MyTime time.Time ok = t.Smuggle(MyTime(gotTime), func(s fmt.Stringer) string { fmt.Println("Smuggle func called!") return s.String() }, "2018-05-23 12:13:14 +0000 UTC") fmt.Println("stringified MyTime OK:", ok) // Output: // stringified time.Time OK: true // stringified MyTime OK: false } func ExampleT_Smuggle_field_path() { t := td.NewT(&testing.T{}) type Body struct { Name string Value any } type Request struct { Body *Body } type Transaction struct { Request } type ValueNum struct { Num int } got := &Transaction{ Request: Request{ Body: &Body{ Name: "test", Value: &ValueNum{Num: 123}, }, }, } // Want to check whether Num is between 100 and 200? ok := t.Smuggle(got, func(t *Transaction) (int, error) { if t.Request.Body == nil || t.Request.Body.Value == nil { return 0, errors.New("Request.Body or Request.Body.Value is nil") } if v, ok := t.Request.Body.Value.(*ValueNum); ok && v != nil { return v.Num, nil } return 0, errors.New("Request.Body.Value isn't *ValueNum or nil") }, td.Between(100, 200)) fmt.Println("check Num by hand:", ok) // Same, but automagically generated... ok = t.Smuggle(got, "Request.Body.Value.Num", td.Between(100, 200)) fmt.Println("check Num using a fields-path:", ok) // And as Request is an anonymous field, can be simplified further // as it can be omitted ok = t.Smuggle(got, "Body.Value.Num", td.Between(100, 200)) fmt.Println("check Num using an other fields-path:", ok) // Note that maps and array/slices are supported got.Request.Body.Value = map[string]any{ "foo": []any{ 3: map[int]string{666: "bar"}, }, } ok = t.Smuggle(got, "Body.Value[foo][3][666]", "bar") fmt.Println("check fields-path including maps/slices:", ok) // Output: // check Num by hand: true // check Num using a fields-path: true // check Num using an other fields-path: true // check fields-path including maps/slices: true } func ExampleT_SStruct() { t := td.NewT(&testing.T{}) type Person struct { Name string Age int NumChildren int } got := Person{ Name: "Foobar", Age: 42, NumChildren: 0, } // NumChildren is not listed in expected fields so it must be zero ok := t.SStruct(got, Person{Name: "Foobar"}, td.StructFields{ "Age": td.Between(40, 50), }, "checks %v is the right Person") fmt.Println("Foobar is between 40 & 50:", ok) // Model can be empty got.NumChildren = 3 ok = t.SStruct(got, Person{}, td.StructFields{ "Name": "Foobar", "Age": td.Between(40, 50), "NumChildren": td.Not(0), }, "checks %v is the right Person") fmt.Println("Foobar has some children:", ok) // Works with pointers too ok = t.SStruct(&got, &Person{}, td.StructFields{ "Name": "Foobar", "Age": td.Between(40, 50), "NumChildren": td.Not(0), }, "checks %v is the right Person") fmt.Println("Foobar has some children (using pointer):", ok) // Model does not need to be instanciated ok = t.SStruct(&got, (*Person)(nil), td.StructFields{ "Name": "Foobar", "Age": td.Between(40, 50), "NumChildren": td.Not(0), }, "checks %v is the right Person") fmt.Println("Foobar has some children (using nil model):", ok) // Output: // Foobar is between 40 & 50: true // Foobar has some children: true // Foobar has some children (using pointer): true // Foobar has some children (using nil model): true } func ExampleT_SStruct_overwrite_model() { t := td.NewT(&testing.T{}) type Person struct { Name string Age int NumChildren int } got := Person{ Name: "Foobar", Age: 42, NumChildren: 3, } ok := t.SStruct(got, Person{ Name: "Foobar", Age: 53, }, td.StructFields{ ">Age": td.Between(40, 50), // ">" to overwrite Age:53 in model "NumChildren": td.Gt(2), }, "checks %v is the right Person") fmt.Println("Foobar is between 40 & 50:", ok) ok = t.SStruct(got, Person{ Name: "Foobar", Age: 53, }, td.StructFields{ "> Age": td.Between(40, 50), // same, ">" can be followed by spaces "NumChildren": td.Gt(2), }, "checks %v is the right Person") fmt.Println("Foobar is between 40 & 50:", ok) // Output: // Foobar is between 40 & 50: true // Foobar is between 40 & 50: true } func ExampleT_SStruct_patterns() { t := td.NewT(&testing.T{}) type Person struct { Firstname string Lastname string Surname string Nickname string CreatedAt time.Time UpdatedAt time.Time DeletedAt *time.Time id int64 secret string } now := time.Now() got := Person{ Firstname: "Maxime", Lastname: "Foo", Surname: "Max", Nickname: "max", CreatedAt: now, UpdatedAt: now, DeletedAt: nil, // not deleted yet id: 2345, secret: "5ecr3T", } ok := t.SStruct(got, Person{Lastname: "Foo"}, td.StructFields{ `DeletedAt`: nil, `= *name`: td.Re(`^(?i)max`), // shell pattern, matches all names except Lastname as in model `=~ At\z`: td.Lte(time.Now()), // regexp, matches CreatedAt & UpdatedAt `! [A-Z]*`: td.Ignore(), // private fields }, "mix shell & regexp patterns") fmt.Println("Patterns match only remaining fields:", ok) ok = t.SStruct(got, Person{Lastname: "Foo"}, td.StructFields{ `DeletedAt`: nil, `1 = *name`: td.Re(`^(?i)max`), // shell pattern, matches all names except Lastname as in model `2 =~ At\z`: td.Lte(time.Now()), // regexp, matches CreatedAt & UpdatedAt `3 !~ ^[A-Z]`: td.Ignore(), // private fields }, "ordered patterns") fmt.Println("Ordered patterns match only remaining fields:", ok) // Output: // Patterns match only remaining fields: true // Ordered patterns match only remaining fields: true } func ExampleT_SStruct_lazy_model() { t := td.NewT(&testing.T{}) got := struct { name string age int }{ name: "Foobar", age: 42, } ok := t.SStruct(got, nil, td.StructFields{ "name": "Foobar", "age": td.Between(40, 45), }) fmt.Println("Lazy model:", ok) ok = t.SStruct(got, nil, td.StructFields{ "name": "Foobar", "zip": 666, }) fmt.Println("Lazy model with unknown field:", ok) // Output: // Lazy model: true // Lazy model with unknown field: false } func ExampleT_String() { t := td.NewT(&testing.T{}) got := "foobar" ok := t.String(got, "foobar", "checks %s", got) fmt.Println("using string:", ok) ok = t.Cmp([]byte(got), td.String("foobar"), "checks %s", got) fmt.Println("using []byte:", ok) // Output: // using string: true // using []byte: true } func ExampleT_String_stringer() { t := td.NewT(&testing.T{}) // bytes.Buffer implements fmt.Stringer got := bytes.NewBufferString("foobar") ok := t.String(got, "foobar", "checks %s", got) fmt.Println(ok) // Output: // true } func ExampleT_String_error() { t := td.NewT(&testing.T{}) got := errors.New("foobar") ok := t.String(got, "foobar", "checks %s", got) fmt.Println(ok) // Output: // true } func ExampleT_Struct() { t := td.NewT(&testing.T{}) type Person struct { Name string Age int NumChildren int } got := Person{ Name: "Foobar", Age: 42, NumChildren: 3, } // As NumChildren is zero in Struct() call, it is not checked ok := t.Struct(got, Person{Name: "Foobar"}, td.StructFields{ "Age": td.Between(40, 50), }, "checks %v is the right Person") fmt.Println("Foobar is between 40 & 50:", ok) // Model can be empty ok = t.Struct(got, Person{}, td.StructFields{ "Name": "Foobar", "Age": td.Between(40, 50), "NumChildren": td.Not(0), }, "checks %v is the right Person") fmt.Println("Foobar has some children:", ok) // Works with pointers too ok = t.Struct(&got, &Person{}, td.StructFields{ "Name": "Foobar", "Age": td.Between(40, 50), "NumChildren": td.Not(0), }, "checks %v is the right Person") fmt.Println("Foobar has some children (using pointer):", ok) // Model does not need to be instanciated ok = t.Struct(&got, (*Person)(nil), td.StructFields{ "Name": "Foobar", "Age": td.Between(40, 50), "NumChildren": td.Not(0), }, "checks %v is the right Person") fmt.Println("Foobar has some children (using nil model):", ok) // Output: // Foobar is between 40 & 50: true // Foobar has some children: true // Foobar has some children (using pointer): true // Foobar has some children (using nil model): true } func ExampleT_Struct_overwrite_model() { t := td.NewT(&testing.T{}) type Person struct { Name string Age int NumChildren int } got := Person{ Name: "Foobar", Age: 42, NumChildren: 3, } ok := t.Struct(got, Person{ Name: "Foobar", Age: 53, }, td.StructFields{ ">Age": td.Between(40, 50), // ">" to overwrite Age:53 in model "NumChildren": td.Gt(2), }, "checks %v is the right Person") fmt.Println("Foobar is between 40 & 50:", ok) ok = t.Struct(got, Person{ Name: "Foobar", Age: 53, }, td.StructFields{ "> Age": td.Between(40, 50), // same, ">" can be followed by spaces "NumChildren": td.Gt(2), }, "checks %v is the right Person") fmt.Println("Foobar is between 40 & 50:", ok) // Output: // Foobar is between 40 & 50: true // Foobar is between 40 & 50: true } func ExampleT_Struct_patterns() { t := td.NewT(&testing.T{}) type Person struct { Firstname string Lastname string Surname string Nickname string CreatedAt time.Time UpdatedAt time.Time DeletedAt *time.Time } now := time.Now() got := Person{ Firstname: "Maxime", Lastname: "Foo", Surname: "Max", Nickname: "max", CreatedAt: now, UpdatedAt: now, DeletedAt: nil, // not deleted yet } ok := t.Struct(got, Person{Lastname: "Foo"}, td.StructFields{ `DeletedAt`: nil, `= *name`: td.Re(`^(?i)max`), // shell pattern, matches all names except Lastname as in model `=~ At\z`: td.Lte(time.Now()), // regexp, matches CreatedAt & UpdatedAt }, "mix shell & regexp patterns") fmt.Println("Patterns match only remaining fields:", ok) ok = t.Struct(got, Person{Lastname: "Foo"}, td.StructFields{ `DeletedAt`: nil, `1 = *name`: td.Re(`^(?i)max`), // shell pattern, matches all names except Lastname as in model `2 =~ At\z`: td.Lte(time.Now()), // regexp, matches CreatedAt & UpdatedAt }, "ordered patterns") fmt.Println("Ordered patterns match only remaining fields:", ok) // Output: // Patterns match only remaining fields: true // Ordered patterns match only remaining fields: true } func ExampleT_Struct_lazy_model() { t := td.NewT(&testing.T{}) got := struct { name string age int }{ name: "Foobar", age: 42, } ok := t.Struct(got, nil, td.StructFields{ "name": "Foobar", "age": td.Between(40, 45), }) fmt.Println("Lazy model:", ok) ok = t.Struct(got, nil, td.StructFields{ "name": "Foobar", "zip": 666, }) fmt.Println("Lazy model with unknown field:", ok) // Output: // Lazy model: true // Lazy model with unknown field: false } func ExampleT_SubBagOf() { t := td.NewT(&testing.T{}) got := []int{1, 3, 5, 8, 8, 1, 2} ok := t.SubBagOf(got, []any{0, 0, 1, 1, 2, 2, 3, 3, 5, 5, 8, 8, 9, 9}, "checks at least all items are present, in any order") fmt.Println(ok) // got contains one 8 too many ok = t.SubBagOf(got, []any{0, 0, 1, 1, 2, 2, 3, 3, 5, 5, 8, 9, 9}, "checks at least all items are present, in any order") fmt.Println(ok) got = []int{1, 3, 5, 2} ok = t.SubBagOf(got, []any{td.Between(0, 3), td.Between(0, 3), td.Between(0, 3), td.Between(0, 3), td.Gt(4), td.Gt(4)}, "checks at least all items match, in any order with TestDeep operators") fmt.Println(ok) // When expected is already a non-[]any slice, it cannot be // flattened directly using expected... without copying it to a new // []any slice, then use td.Flatten! expected := []int{1, 2, 3, 5, 9, 8} ok = t.SubBagOf(got, []any{td.Flatten(expected)}, "checks at least all expected items are present, in any order") fmt.Println(ok) // Output: // true // false // true // true } func ExampleT_SubJSONOf_basic() { t := td.NewT(&testing.T{}) got := &struct { Fullname string `json:"fullname"` Age int `json:"age"` }{ Fullname: "Bob", Age: 42, } ok := t.SubJSONOf(got, `{"age":42,"fullname":"Bob","gender":"male"}`, nil) fmt.Println("check got with age then fullname:", ok) ok = t.SubJSONOf(got, `{"fullname":"Bob","age":42,"gender":"male"}`, nil) fmt.Println("check got with fullname then age:", ok) ok = t.SubJSONOf(got, ` // This should be the JSON representation of a struct { // A person: "fullname": "Bob", // The name of this person "age": 42, /* The age of this person: - 42 of course - to demonstrate a multi-lines comment */ "gender": "male" // This field is ignored as SubJSONOf }`, nil) fmt.Println("check got with nicely formatted and commented JSON:", ok) ok = t.SubJSONOf(got, `{"fullname":"Bob","gender":"male"}`, nil) fmt.Println("check got without age field:", ok) // Output: // check got with age then fullname: true // check got with fullname then age: true // check got with nicely formatted and commented JSON: true // check got without age field: false } func ExampleT_SubJSONOf_placeholders() { t := td.NewT(&testing.T{}) got := &struct { Fullname string `json:"fullname"` Age int `json:"age"` }{ Fullname: "Bob Foobar", Age: 42, } ok := t.SubJSONOf(got, `{"age": $1, "fullname": $2, "gender": $3}`, []any{42, "Bob Foobar", "male"}) fmt.Println("check got with numeric placeholders without operators:", ok) ok = t.SubJSONOf(got, `{"age": $1, "fullname": $2, "gender": $3}`, []any{td.Between(40, 45), td.HasSuffix("Foobar"), td.NotEmpty()}) fmt.Println("check got with numeric placeholders:", ok) ok = t.SubJSONOf(got, `{"age": "$1", "fullname": "$2", "gender": "$3"}`, []any{td.Between(40, 45), td.HasSuffix("Foobar"), td.NotEmpty()}) fmt.Println("check got with double-quoted numeric placeholders:", ok) ok = t.SubJSONOf(got, `{"age": $age, "fullname": $name, "gender": $gender}`, []any{td.Tag("age", td.Between(40, 45)), td.Tag("name", td.HasSuffix("Foobar")), td.Tag("gender", td.NotEmpty())}) fmt.Println("check got with named placeholders:", ok) ok = t.SubJSONOf(got, `{"age": $^NotZero, "fullname": $^NotEmpty, "gender": $^NotEmpty}`, nil) fmt.Println("check got with operator shortcuts:", ok) // Output: // check got with numeric placeholders without operators: true // check got with numeric placeholders: true // check got with double-quoted numeric placeholders: true // check got with named placeholders: true // check got with operator shortcuts: true } func ExampleT_SubJSONOf_file() { t := td.NewT(&testing.T{}) got := &struct { Fullname string `json:"fullname"` Age int `json:"age"` Gender string `json:"gender"` }{ Fullname: "Bob Foobar", Age: 42, Gender: "male", } tmpDir, err := os.MkdirTemp("", "") if err != nil { t.Fatal(err) } defer os.RemoveAll(tmpDir) // clean up filename := tmpDir + "/test.json" if err = os.WriteFile(filename, []byte(` { "fullname": "$name", "age": "$age", "gender": "$gender", "details": { "city": "TestCity", "zip": 666 } }`), 0644); err != nil { t.Fatal(err) } // OK let's test with this file ok := t.SubJSONOf(got, filename, []any{td.Tag("name", td.HasPrefix("Bob")), td.Tag("age", td.Between(40, 45)), td.Tag("gender", td.Re(`^(male|female)\z`))}) fmt.Println("Full match from file name:", ok) // When the file is already open file, err := os.Open(filename) if err != nil { t.Fatal(err) } ok = t.SubJSONOf(got, file, []any{td.Tag("name", td.HasPrefix("Bob")), td.Tag("age", td.Between(40, 45)), td.Tag("gender", td.Re(`^(male|female)\z`))}) fmt.Println("Full match from io.Reader:", ok) // Output: // Full match from file name: true // Full match from io.Reader: true } func ExampleT_SubMapOf_map() { t := td.NewT(&testing.T{}) got := map[string]int{"foo": 12, "bar": 42} ok := t.SubMapOf(got, map[string]int{"bar": 42}, td.MapEntries{"foo": td.Lt(15), "zip": 666}, "checks map %v is included in expected keys/values", got) fmt.Println(ok) // Output: // true } func ExampleT_SubMapOf_typedMap() { t := td.NewT(&testing.T{}) type MyMap map[string]int got := MyMap{"foo": 12, "bar": 42} ok := t.SubMapOf(got, MyMap{"bar": 42}, td.MapEntries{"foo": td.Lt(15), "zip": 666}, "checks typed map %v is included in expected keys/values", got) fmt.Println(ok) ok = t.SubMapOf(&got, &MyMap{"bar": 42}, td.MapEntries{"foo": td.Lt(15), "zip": 666}, "checks pointed typed map %v is included in expected keys/values", got) fmt.Println(ok) // Output: // true // true } func ExampleT_SubSetOf() { t := td.NewT(&testing.T{}) got := []int{1, 3, 5, 8, 8, 1, 2} // Matches as all items are expected, ignoring duplicates ok := t.SubSetOf(got, []any{1, 2, 3, 4, 5, 6, 7, 8}, "checks at least all items are present, in any order, ignoring duplicates") fmt.Println(ok) // Tries its best to not raise an error when a value can be matched // by several SubSetOf entries ok = t.SubSetOf(got, []any{td.Between(1, 4), 3, td.Between(2, 10), td.Gt(100)}, "checks at least all items are present, in any order, ignoring duplicates") fmt.Println(ok) // When expected is already a non-[]any slice, it cannot be // flattened directly using expected... without copying it to a new // []any slice, then use td.Flatten! expected := []int{1, 2, 3, 4, 5, 6, 7, 8} ok = t.SubSetOf(got, []any{td.Flatten(expected)}, "checks at least all expected items are present, in any order, ignoring duplicates") fmt.Println(ok) // Output: // true // true // true } func ExampleT_SuperBagOf() { t := td.NewT(&testing.T{}) got := []int{1, 3, 5, 8, 8, 1, 2} ok := t.SuperBagOf(got, []any{8, 5, 8}, "checks the items are present, in any order") fmt.Println(ok) ok = t.SuperBagOf(got, []any{td.Gt(5), td.Lte(2)}, "checks at least 2 items of %v match", got) fmt.Println(ok) // When expected is already a non-[]any slice, it cannot be // flattened directly using expected... without copying it to a new // []any slice, then use td.Flatten! expected := []int{8, 5, 8} ok = t.SuperBagOf(got, []any{td.Flatten(expected)}, "checks the expected items are present, in any order") fmt.Println(ok) // Output: // true // true // true } func ExampleT_SuperJSONOf_basic() { t := td.NewT(&testing.T{}) got := &struct { Fullname string `json:"fullname"` Age int `json:"age"` Gender string `json:"gender"` City string `json:"city"` Zip int `json:"zip"` }{ Fullname: "Bob", Age: 42, Gender: "male", City: "TestCity", Zip: 666, } ok := t.SuperJSONOf(got, `{"age":42,"fullname":"Bob","gender":"male"}`, nil) fmt.Println("check got with age then fullname:", ok) ok = t.SuperJSONOf(got, `{"fullname":"Bob","age":42,"gender":"male"}`, nil) fmt.Println("check got with fullname then age:", ok) ok = t.SuperJSONOf(got, ` // This should be the JSON representation of a struct { // A person: "fullname": "Bob", // The name of this person "age": 42, /* The age of this person: - 42 of course - to demonstrate a multi-lines comment */ "gender": "male" // The gender! }`, nil) fmt.Println("check got with nicely formatted and commented JSON:", ok) ok = t.SuperJSONOf(got, `{"fullname":"Bob","gender":"male","details":{}}`, nil) fmt.Println("check got with details field:", ok) // Output: // check got with age then fullname: true // check got with fullname then age: true // check got with nicely formatted and commented JSON: true // check got with details field: false } func ExampleT_SuperJSONOf_placeholders() { t := td.NewT(&testing.T{}) got := &struct { Fullname string `json:"fullname"` Age int `json:"age"` Gender string `json:"gender"` City string `json:"city"` Zip int `json:"zip"` }{ Fullname: "Bob Foobar", Age: 42, Gender: "male", City: "TestCity", Zip: 666, } ok := t.SuperJSONOf(got, `{"age": $1, "fullname": $2, "gender": $3}`, []any{42, "Bob Foobar", "male"}) fmt.Println("check got with numeric placeholders without operators:", ok) ok = t.SuperJSONOf(got, `{"age": $1, "fullname": $2, "gender": $3}`, []any{td.Between(40, 45), td.HasSuffix("Foobar"), td.NotEmpty()}) fmt.Println("check got with numeric placeholders:", ok) ok = t.SuperJSONOf(got, `{"age": "$1", "fullname": "$2", "gender": "$3"}`, []any{td.Between(40, 45), td.HasSuffix("Foobar"), td.NotEmpty()}) fmt.Println("check got with double-quoted numeric placeholders:", ok) ok = t.SuperJSONOf(got, `{"age": $age, "fullname": $name, "gender": $gender}`, []any{td.Tag("age", td.Between(40, 45)), td.Tag("name", td.HasSuffix("Foobar")), td.Tag("gender", td.NotEmpty())}) fmt.Println("check got with named placeholders:", ok) ok = t.SuperJSONOf(got, `{"age": $^NotZero, "fullname": $^NotEmpty, "gender": $^NotEmpty}`, nil) fmt.Println("check got with operator shortcuts:", ok) // Output: // check got with numeric placeholders without operators: true // check got with numeric placeholders: true // check got with double-quoted numeric placeholders: true // check got with named placeholders: true // check got with operator shortcuts: true } func ExampleT_SuperJSONOf_file() { t := td.NewT(&testing.T{}) got := &struct { Fullname string `json:"fullname"` Age int `json:"age"` Gender string `json:"gender"` City string `json:"city"` Zip int `json:"zip"` }{ Fullname: "Bob Foobar", Age: 42, Gender: "male", City: "TestCity", Zip: 666, } tmpDir, err := os.MkdirTemp("", "") if err != nil { t.Fatal(err) } defer os.RemoveAll(tmpDir) // clean up filename := tmpDir + "/test.json" if err = os.WriteFile(filename, []byte(` { "fullname": "$name", "age": "$age", "gender": "$gender" }`), 0644); err != nil { t.Fatal(err) } // OK let's test with this file ok := t.SuperJSONOf(got, filename, []any{td.Tag("name", td.HasPrefix("Bob")), td.Tag("age", td.Between(40, 45)), td.Tag("gender", td.Re(`^(male|female)\z`))}) fmt.Println("Full match from file name:", ok) // When the file is already open file, err := os.Open(filename) if err != nil { t.Fatal(err) } ok = t.SuperJSONOf(got, file, []any{td.Tag("name", td.HasPrefix("Bob")), td.Tag("age", td.Between(40, 45)), td.Tag("gender", td.Re(`^(male|female)\z`))}) fmt.Println("Full match from io.Reader:", ok) // Output: // Full match from file name: true // Full match from io.Reader: true } func ExampleT_SuperMapOf_map() { t := td.NewT(&testing.T{}) got := map[string]int{"foo": 12, "bar": 42, "zip": 89} ok := t.SuperMapOf(got, map[string]int{"bar": 42}, td.MapEntries{"foo": td.Lt(15)}, "checks map %v contains at least all expected keys/values", got) fmt.Println(ok) // Output: // true } func ExampleT_SuperMapOf_typedMap() { t := td.NewT(&testing.T{}) type MyMap map[string]int got := MyMap{"foo": 12, "bar": 42, "zip": 89} ok := t.SuperMapOf(got, MyMap{"bar": 42}, td.MapEntries{"foo": td.Lt(15)}, "checks typed map %v contains at least all expected keys/values", got) fmt.Println(ok) ok = t.SuperMapOf(&got, &MyMap{"bar": 42}, td.MapEntries{"foo": td.Lt(15)}, "checks pointed typed map %v contains at least all expected keys/values", got) fmt.Println(ok) // Output: // true // true } func ExampleT_SuperSetOf() { t := td.NewT(&testing.T{}) got := []int{1, 3, 5, 8, 8, 1, 2} ok := t.SuperSetOf(got, []any{1, 2, 3}, "checks the items are present, in any order and ignoring duplicates") fmt.Println(ok) ok = t.SuperSetOf(got, []any{td.Gt(5), td.Lte(2)}, "checks at least 2 items of %v match ignoring duplicates", got) fmt.Println(ok) // When expected is already a non-[]any slice, it cannot be // flattened directly using expected... without copying it to a new // []any slice, then use td.Flatten! expected := []int{1, 2, 3} ok = t.SuperSetOf(got, []any{td.Flatten(expected)}, "checks the expected items are present, in any order and ignoring duplicates") fmt.Println(ok) // Output: // true // true // true } func ExampleT_SuperSliceOf_array() { t := td.NewT(&testing.T{}) got := [4]int{42, 58, 26, 666} ok := t.SuperSliceOf(got, [4]int{1: 58}, td.ArrayEntries{3: td.Gt(660)}, "checks array %v", got) fmt.Println("Only check items #1 & #3:", ok) ok = t.SuperSliceOf(got, [4]int{}, td.ArrayEntries{0: 42, 3: td.Between(660, 670)}, "checks array %v", got) fmt.Println("Only check items #0 & #3:", ok) ok = t.SuperSliceOf(&got, &[4]int{}, td.ArrayEntries{0: 42, 3: td.Between(660, 670)}, "checks array %v", got) fmt.Println("Only check items #0 & #3 of an array pointer:", ok) ok = t.SuperSliceOf(&got, (*[4]int)(nil), td.ArrayEntries{0: 42, 3: td.Between(660, 670)}, "checks array %v", got) fmt.Println("Only check items #0 & #3 of an array pointer, using nil model:", ok) // Output: // Only check items #1 & #3: true // Only check items #0 & #3: true // Only check items #0 & #3 of an array pointer: true // Only check items #0 & #3 of an array pointer, using nil model: true } func ExampleT_SuperSliceOf_typedArray() { t := td.NewT(&testing.T{}) type MyArray [4]int got := MyArray{42, 58, 26, 666} ok := t.SuperSliceOf(got, MyArray{1: 58}, td.ArrayEntries{3: td.Gt(660)}, "checks typed array %v", got) fmt.Println("Only check items #1 & #3:", ok) ok = t.SuperSliceOf(got, MyArray{}, td.ArrayEntries{0: 42, 3: td.Between(660, 670)}, "checks array %v", got) fmt.Println("Only check items #0 & #3:", ok) ok = t.SuperSliceOf(&got, &MyArray{}, td.ArrayEntries{0: 42, 3: td.Between(660, 670)}, "checks array %v", got) fmt.Println("Only check items #0 & #3 of an array pointer:", ok) ok = t.SuperSliceOf(&got, (*MyArray)(nil), td.ArrayEntries{0: 42, 3: td.Between(660, 670)}, "checks array %v", got) fmt.Println("Only check items #0 & #3 of an array pointer, using nil model:", ok) // Output: // Only check items #1 & #3: true // Only check items #0 & #3: true // Only check items #0 & #3 of an array pointer: true // Only check items #0 & #3 of an array pointer, using nil model: true } func ExampleT_SuperSliceOf_slice() { t := td.NewT(&testing.T{}) got := []int{42, 58, 26, 666} ok := t.SuperSliceOf(got, []int{1: 58}, td.ArrayEntries{3: td.Gt(660)}, "checks array %v", got) fmt.Println("Only check items #1 & #3:", ok) ok = t.SuperSliceOf(got, []int{}, td.ArrayEntries{0: 42, 3: td.Between(660, 670)}, "checks array %v", got) fmt.Println("Only check items #0 & #3:", ok) ok = t.SuperSliceOf(&got, &[]int{}, td.ArrayEntries{0: 42, 3: td.Between(660, 670)}, "checks array %v", got) fmt.Println("Only check items #0 & #3 of a slice pointer:", ok) ok = t.SuperSliceOf(&got, (*[]int)(nil), td.ArrayEntries{0: 42, 3: td.Between(660, 670)}, "checks array %v", got) fmt.Println("Only check items #0 & #3 of a slice pointer, using nil model:", ok) // Output: // Only check items #1 & #3: true // Only check items #0 & #3: true // Only check items #0 & #3 of a slice pointer: true // Only check items #0 & #3 of a slice pointer, using nil model: true } func ExampleT_SuperSliceOf_typedSlice() { t := td.NewT(&testing.T{}) type MySlice []int got := MySlice{42, 58, 26, 666} ok := t.SuperSliceOf(got, MySlice{1: 58}, td.ArrayEntries{3: td.Gt(660)}, "checks typed array %v", got) fmt.Println("Only check items #1 & #3:", ok) ok = t.SuperSliceOf(got, MySlice{}, td.ArrayEntries{0: 42, 3: td.Between(660, 670)}, "checks array %v", got) fmt.Println("Only check items #0 & #3:", ok) ok = t.SuperSliceOf(&got, &MySlice{}, td.ArrayEntries{0: 42, 3: td.Between(660, 670)}, "checks array %v", got) fmt.Println("Only check items #0 & #3 of a slice pointer:", ok) ok = t.SuperSliceOf(&got, (*MySlice)(nil), td.ArrayEntries{0: 42, 3: td.Between(660, 670)}, "checks array %v", got) fmt.Println("Only check items #0 & #3 of a slice pointer, using nil model:", ok) // Output: // Only check items #1 & #3: true // Only check items #0 & #3: true // Only check items #0 & #3 of a slice pointer: true // Only check items #0 & #3 of a slice pointer, using nil model: true } func ExampleT_TruncTime() { t := td.NewT(&testing.T{}) dateToTime := func(str string) time.Time { t, err := time.Parse(time.RFC3339Nano, str) if err != nil { panic(err) } return t } got := dateToTime("2018-05-01T12:45:53.123456789Z") // Compare dates ignoring nanoseconds and monotonic parts expected := dateToTime("2018-05-01T12:45:53Z") ok := t.TruncTime(got, expected, time.Second, "checks date %v, truncated to the second", got) fmt.Println(ok) // Compare dates ignoring time and so monotonic parts expected = dateToTime("2018-05-01T11:22:33.444444444Z") ok = t.TruncTime(got, expected, 24*time.Hour, "checks date %v, truncated to the day", got) fmt.Println(ok) // Compare dates exactly but ignoring monotonic part expected = dateToTime("2018-05-01T12:45:53.123456789Z") ok = t.TruncTime(got, expected, 0, "checks date %v ignoring monotonic part", got) fmt.Println(ok) // Output: // true // true // true } func ExampleT_Values() { t := td.NewT(&testing.T{}) got := map[string]int{"foo": 1, "bar": 2, "zip": 3} // Values tests values in an ordered manner ok := t.Values(got, []int{1, 2, 3}) fmt.Println("All sorted values are found:", ok) // If the expected values are not ordered, it fails ok = t.Values(got, []int{3, 1, 2}) fmt.Println("All unsorted values are found:", ok) // To circumvent that, one can use Bag operator ok = t.Values(got, td.Bag(3, 1, 2)) fmt.Println("All unsorted values are found, with the help of Bag operator:", ok) // Check that each value is between 1 and 3 ok = t.Values(got, td.ArrayEach(td.Between(1, 3))) fmt.Println("Each value is between 1 and 3:", ok) // Output: // All sorted values are found: true // All unsorted values are found: false // All unsorted values are found, with the help of Bag operator: true // Each value is between 1 and 3: true } func ExampleT_Zero() { t := td.NewT(&testing.T{}) ok := t.Zero(0) fmt.Println(ok) ok = t.Zero(float64(0)) fmt.Println(ok) ok = t.Zero(12) // fails, as 12 is not 0 :) fmt.Println(ok) ok = t.Zero((map[string]int)(nil)) fmt.Println(ok) ok = t.Zero(map[string]int{}) // fails, as not nil fmt.Println(ok) ok = t.Zero(([]int)(nil)) fmt.Println(ok) ok = t.Zero([]int{}) // fails, as not nil fmt.Println(ok) ok = t.Zero([3]int{}) fmt.Println(ok) ok = t.Zero([3]int{0, 1}) // fails, DATA[1] is not 0 fmt.Println(ok) ok = t.Zero(bytes.Buffer{}) fmt.Println(ok) ok = t.Zero(&bytes.Buffer{}) // fails, as pointer not nil fmt.Println(ok) ok = t.Cmp(&bytes.Buffer{}, td.Ptr(td.Zero())) // OK with the help of Ptr() fmt.Println(ok) // Output: // true // true // false // true // false // true // false // true // false // true // false // true } golang-github-maxatome-go-testdeep-1.14.0/td/example_test.go000066400000000000000000003321051454313311600240100ustar00rootroot00000000000000// Copyright (c) 2018-2022, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td_test import ( "bytes" "encoding/json" "errors" "fmt" "math" "os" "regexp" "strconv" "strings" "testing" "time" "github.com/maxatome/go-testdeep/td" ) func Example() { t := &testing.T{} dateToTime := func(str string) time.Time { t, err := time.Parse(time.RFC3339, str) if err != nil { panic(err) } return t } type PetFamily uint8 const ( Canidae PetFamily = 1 Felidae PetFamily = 2 ) type Pet struct { Name string Birthday time.Time Family PetFamily } type Master struct { Name string AnnualIncome int Pets []*Pet } // Imagine a function returning a Master slice... masters := []Master{ { Name: "Bob Smith", AnnualIncome: 25000, Pets: []*Pet{ { Name: "Quizz", Birthday: dateToTime("2010-11-05T10:00:00Z"), Family: Canidae, }, { Name: "Charlie", Birthday: dateToTime("2013-05-11T08:00:00Z"), Family: Canidae, }, }, }, { Name: "John Doe", AnnualIncome: 38000, Pets: []*Pet{ { Name: "Coco", Birthday: dateToTime("2015-08-05T18:00:00Z"), Family: Felidae, }, { Name: "Lucky", Birthday: dateToTime("2014-04-17T07:00:00Z"), Family: Canidae, }, }, }, } // Let's check masters slice contents ok := td.Cmp(t, masters, td.All( td.Len(td.Gt(0)), // len(masters) should be > 0 td.ArrayEach( // For each Master td.Struct(Master{}, td.StructFields{ // Master Name should be composed of 2 words, with 1st letter uppercased "Name": td.Re(`^[A-Z][a-z]+ [A-Z][a-z]+\z`), // Annual income should be greater than $10000 "AnnualIncome": td.Gt(10000), "Pets": td.ArrayEach( // For each Pet td.Struct(&Pet{}, td.StructFields{ // Pet Name should be composed of 1 word, with 1st letter uppercased "Name": td.Re(`^[A-Z][a-z]+\z`), "Birthday": td.All( // Pet should be born after 2010, January 1st, but before now! td.Between(dateToTime("2010-01-01T00:00:00Z"), time.Now()), // AND minutes, seconds and nanoseconds should be 0 td.Code(func(t time.Time) bool { return t.Minute() == 0 && t.Second() == 0 && t.Nanosecond() == 0 }), ), // Only dogs and cats allowed "Family": td.Any(Canidae, Felidae), }), ), }), ), )) fmt.Println(ok) // Output: // true } func ExampleIgnore() { t := &testing.T{} ok := td.Cmp(t, []int{1, 2, 3}, td.Slice([]int{}, td.ArrayEntries{ 0: 1, 1: td.Ignore(), // do not care about this entry 2: 3, })) fmt.Println(ok) // Output: // true } func ExampleAll() { t := &testing.T{} got := "foo/bar" // Checks got string against: // "o/b" regexp *AND* "bar" suffix *AND* exact "foo/bar" string ok := td.Cmp(t, got, td.All(td.Re("o/b"), td.HasSuffix("bar"), "foo/bar"), "checks value %s", got) fmt.Println(ok) // Checks got string against: // "o/b" regexp *AND* "bar" suffix *AND* exact "fooX/Ybar" string ok = td.Cmp(t, got, td.All(td.Re("o/b"), td.HasSuffix("bar"), "fooX/Ybar"), "checks value %s", got) fmt.Println(ok) // When some operators or values have to be reused and mixed between // several calls, Flatten can be used to avoid boring and // inefficient []any copies: regOps := td.Flatten([]td.TestDeep{td.Re("o/b"), td.Re(`^fo`), td.Re(`ar$`)}) ok = td.Cmp(t, got, td.All(td.HasPrefix("foo"), regOps, td.HasSuffix("bar")), "checks all operators against value %s", got) fmt.Println(ok) // Output: // true // false // true } func ExampleAny() { t := &testing.T{} got := "foo/bar" // Checks got string against: // "zip" regexp *OR* "bar" suffix ok := td.Cmp(t, got, td.Any(td.Re("zip"), td.HasSuffix("bar")), "checks value %s", got) fmt.Println(ok) // Checks got string against: // "zip" regexp *OR* "foo" suffix ok = td.Cmp(t, got, td.Any(td.Re("zip"), td.HasSuffix("foo")), "checks value %s", got) fmt.Println(ok) // When some operators or values have to be reused and mixed between // several calls, Flatten can be used to avoid boring and // inefficient []any copies: regOps := td.Flatten([]td.TestDeep{td.Re("a/c"), td.Re(`^xx`), td.Re(`ar$`)}) ok = td.Cmp(t, got, td.Any(td.HasPrefix("xxx"), regOps, td.HasSuffix("zip")), "check at least one operator matches value %s", got) fmt.Println(ok) // Output: // true // false // true } func ExampleArray_array() { t := &testing.T{} got := [3]int{42, 58, 26} ok := td.Cmp(t, got, td.Array([3]int{42}, td.ArrayEntries{1: 58, 2: td.Ignore()}), "checks array %v", got) fmt.Println("Simple array:", ok) ok = td.Cmp(t, &got, td.Array(&[3]int{42}, td.ArrayEntries{1: 58, 2: td.Ignore()}), "checks array %v", got) fmt.Println("Array pointer:", ok) ok = td.Cmp(t, &got, td.Array((*[3]int)(nil), td.ArrayEntries{0: 42, 1: 58, 2: td.Ignore()}), "checks array %v", got) fmt.Println("Array pointer, nil model:", ok) // Output: // Simple array: true // Array pointer: true // Array pointer, nil model: true } func ExampleArray_typedArray() { t := &testing.T{} type MyArray [3]int got := MyArray{42, 58, 26} ok := td.Cmp(t, got, td.Array(MyArray{42}, td.ArrayEntries{1: 58, 2: td.Ignore()}), "checks typed array %v", got) fmt.Println("Typed array:", ok) ok = td.Cmp(t, &got, td.Array(&MyArray{42}, td.ArrayEntries{1: 58, 2: td.Ignore()}), "checks pointer on typed array %v", got) fmt.Println("Pointer on a typed array:", ok) ok = td.Cmp(t, &got, td.Array(&MyArray{}, td.ArrayEntries{0: 42, 1: 58, 2: td.Ignore()}), "checks pointer on typed array %v", got) fmt.Println("Pointer on a typed array, empty model:", ok) ok = td.Cmp(t, &got, td.Array((*MyArray)(nil), td.ArrayEntries{0: 42, 1: 58, 2: td.Ignore()}), "checks pointer on typed array %v", got) fmt.Println("Pointer on a typed array, nil model:", ok) // Output: // Typed array: true // Pointer on a typed array: true // Pointer on a typed array, empty model: true // Pointer on a typed array, nil model: true } func ExampleArrayEach_array() { t := &testing.T{} got := [3]int{42, 58, 26} ok := td.Cmp(t, got, td.ArrayEach(td.Between(25, 60)), "checks each item of array %v is in [25 .. 60]", got) fmt.Println(ok) // Output: // true } func ExampleArrayEach_typedArray() { t := &testing.T{} type MyArray [3]int got := MyArray{42, 58, 26} ok := td.Cmp(t, got, td.ArrayEach(td.Between(25, 60)), "checks each item of typed array %v is in [25 .. 60]", got) fmt.Println(ok) ok = td.Cmp(t, &got, td.ArrayEach(td.Between(25, 60)), "checks each item of typed array pointer %v is in [25 .. 60]", got) fmt.Println(ok) // Output: // true // true } func ExampleArrayEach_slice() { t := &testing.T{} got := []int{42, 58, 26} ok := td.Cmp(t, got, td.ArrayEach(td.Between(25, 60)), "checks each item of slice %v is in [25 .. 60]", got) fmt.Println(ok) // Output: // true } func ExampleArrayEach_typedSlice() { t := &testing.T{} type MySlice []int got := MySlice{42, 58, 26} ok := td.Cmp(t, got, td.ArrayEach(td.Between(25, 60)), "checks each item of typed slice %v is in [25 .. 60]", got) fmt.Println(ok) ok = td.Cmp(t, &got, td.ArrayEach(td.Between(25, 60)), "checks each item of typed slice pointer %v is in [25 .. 60]", got) fmt.Println(ok) // Output: // true // true } func ExampleBag() { t := &testing.T{} got := []int{1, 3, 5, 8, 8, 1, 2} // Matches as all items are present ok := td.Cmp(t, got, td.Bag(1, 1, 2, 3, 5, 8, 8), "checks all items are present, in any order") fmt.Println(ok) // Does not match as got contains 2 times 1 and 8, and these // duplicates are not expected ok = td.Cmp(t, got, td.Bag(1, 2, 3, 5, 8), "checks all items are present, in any order") fmt.Println(ok) got = []int{1, 3, 5, 8, 2} // Duplicates of 1 and 8 are expected but not present in got ok = td.Cmp(t, got, td.Bag(1, 1, 2, 3, 5, 8, 8), "checks all items are present, in any order") fmt.Println(ok) // Matches as all items are present ok = td.Cmp(t, got, td.Bag(1, 2, 3, 5, td.Gt(7)), "checks all items are present, in any order") fmt.Println(ok) // When expected is already a non-[]any slice, it cannot be // flattened directly using expected... without copying it to a new // []any slice, then use td.Flatten! expected := []int{1, 2, 3, 5} ok = td.Cmp(t, got, td.Bag(td.Flatten(expected), td.Gt(7)), "checks all expected items are present, in any order") fmt.Println(ok) // Output: // true // false // false // true // true } func ExampleBetween_int() { t := &testing.T{} got := 156 ok := td.Cmp(t, got, td.Between(154, 156), "checks %v is in [154 .. 156]", got) fmt.Println(ok) // BoundsInIn is implicit ok = td.Cmp(t, got, td.Between(154, 156, td.BoundsInIn), "checks %v is in [154 .. 156]", got) fmt.Println(ok) ok = td.Cmp(t, got, td.Between(154, 156, td.BoundsInOut), "checks %v is in [154 .. 156[", got) fmt.Println(ok) ok = td.Cmp(t, got, td.Between(154, 156, td.BoundsOutIn), "checks %v is in ]154 .. 156]", got) fmt.Println(ok) ok = td.Cmp(t, got, td.Between(154, 156, td.BoundsOutOut), "checks %v is in ]154 .. 156[", got) fmt.Println(ok) // Output: // true // true // false // true // false } func ExampleBetween_string() { t := &testing.T{} got := "abc" ok := td.Cmp(t, got, td.Between("aaa", "abc"), `checks "%v" is in ["aaa" .. "abc"]`, got) fmt.Println(ok) // BoundsInIn is implicit ok = td.Cmp(t, got, td.Between("aaa", "abc", td.BoundsInIn), `checks "%v" is in ["aaa" .. "abc"]`, got) fmt.Println(ok) ok = td.Cmp(t, got, td.Between("aaa", "abc", td.BoundsInOut), `checks "%v" is in ["aaa" .. "abc"[`, got) fmt.Println(ok) ok = td.Cmp(t, got, td.Between("aaa", "abc", td.BoundsOutIn), `checks "%v" is in ]"aaa" .. "abc"]`, got) fmt.Println(ok) ok = td.Cmp(t, got, td.Between("aaa", "abc", td.BoundsOutOut), `checks "%v" is in ]"aaa" .. "abc"[`, got) fmt.Println(ok) // Output: // true // true // false // true // false } func ExampleBetween_time() { t := &testing.T{} before := time.Now() occurredAt := time.Now() after := time.Now() ok := td.Cmp(t, occurredAt, td.Between(before, after)) fmt.Println("It occurred between before and after:", ok) type MyTime time.Time ok = td.Cmp(t, MyTime(occurredAt), td.Between(MyTime(before), MyTime(after))) fmt.Println("Same for convertible MyTime type:", ok) ok = td.Cmp(t, MyTime(occurredAt), td.Between(before, after)) fmt.Println("MyTime vs time.Time:", ok) ok = td.Cmp(t, occurredAt, td.Between(before, 10*time.Second)) fmt.Println("Using a time.Duration as TO:", ok) ok = td.Cmp(t, MyTime(occurredAt), td.Between(MyTime(before), 10*time.Second)) fmt.Println("Using MyTime as FROM and time.Duration as TO:", ok) // Output: // It occurred between before and after: true // Same for convertible MyTime type: true // MyTime vs time.Time: false // Using a time.Duration as TO: true // Using MyTime as FROM and time.Duration as TO: true } func ExampleCap() { t := &testing.T{} got := make([]int, 0, 12) ok := td.Cmp(t, got, td.Cap(12), "checks %v capacity is 12", got) fmt.Println(ok) ok = td.Cmp(t, got, td.Cap(0), "checks %v capacity is 0", got) fmt.Println(ok) got = nil ok = td.Cmp(t, got, td.Cap(0), "checks %v capacity is 0", got) fmt.Println(ok) // Output: // true // false // true } func ExampleCap_operator() { t := &testing.T{} got := make([]int, 0, 12) ok := td.Cmp(t, got, td.Cap(td.Between(10, 12)), "checks %v capacity is in [10 .. 12]", got) fmt.Println(ok) ok = td.Cmp(t, got, td.Cap(td.Gt(10)), "checks %v capacity is in [10 .. 12]", got) fmt.Println(ok) // Output: // true // true } func ExampleCatch() { t := &testing.T{} got := &struct { Fullname string `json:"fullname"` Age int `json:"age"` }{ Fullname: "Bob", Age: 42, } var age int ok := td.Cmp(t, got, td.JSON(`{"age":$1,"fullname":"Bob"}`, td.Catch(&age, td.Between(40, 45)))) fmt.Println("check got age+fullname:", ok) fmt.Println("caught age:", age) // Output: // check got age+fullname: true // caught age: 42 } func ExampleCode() { t := &testing.T{} got := "12" ok := td.Cmp(t, got, td.Code(func(num string) bool { n, err := strconv.Atoi(num) return err == nil && n > 10 && n < 100 }), "checks string `%s` contains a number and this number is in ]10 .. 100[", got) fmt.Println(ok) // Same with failure reason ok = td.Cmp(t, got, td.Code(func(num string) (bool, string) { n, err := strconv.Atoi(num) if err != nil { return false, "not a number" } if n > 10 && n < 100 { return true, "" } return false, "not in ]10 .. 100[" }), "checks string `%s` contains a number and this number is in ]10 .. 100[", got) fmt.Println(ok) // Same with failure reason thanks to error ok = td.Cmp(t, got, td.Code(func(num string) error { n, err := strconv.Atoi(num) if err != nil { return err } if n > 10 && n < 100 { return nil } return fmt.Errorf("%d not in ]10 .. 100[", n) }), "checks string `%s` contains a number and this number is in ]10 .. 100[", got) fmt.Println(ok) // Output: // true // true // true } func ExampleCode_custom() { t := &testing.T{} got := 123 ok := td.Cmp(t, got, td.Code(func(t *td.T, num int) { t.Cmp(num, 123) })) fmt.Println("with one *td.T:", ok) ok = td.Cmp(t, got, td.Code(func(assert, require *td.T, num int) { assert.Cmp(num, 123) require.Cmp(num, 123) })) fmt.Println("with assert & require *td.T:", ok) // Output: // with one *td.T: true // with assert & require *td.T: true } func ExampleContains_arraySlice() { t := &testing.T{} ok := td.Cmp(t, [...]int{11, 22, 33, 44}, td.Contains(22)) fmt.Println("array contains 22:", ok) ok = td.Cmp(t, [...]int{11, 22, 33, 44}, td.Contains(td.Between(20, 25))) fmt.Println("array contains at least one item in [20 .. 25]:", ok) ok = td.Cmp(t, []int{11, 22, 33, 44}, td.Contains(22)) fmt.Println("slice contains 22:", ok) ok = td.Cmp(t, []int{11, 22, 33, 44}, td.Contains(td.Between(20, 25))) fmt.Println("slice contains at least one item in [20 .. 25]:", ok) ok = td.Cmp(t, []int{11, 22, 33, 44}, td.Contains([]int{22, 33})) fmt.Println("slice contains the sub-slice [22, 33]:", ok) // Output: // array contains 22: true // array contains at least one item in [20 .. 25]: true // slice contains 22: true // slice contains at least one item in [20 .. 25]: true // slice contains the sub-slice [22, 33]: true } func ExampleContains_nil() { t := &testing.T{} num := 123 got := [...]*int{&num, nil} ok := td.Cmp(t, got, td.Contains(nil)) fmt.Println("array contains untyped nil:", ok) ok = td.Cmp(t, got, td.Contains((*int)(nil))) fmt.Println("array contains *int nil:", ok) ok = td.Cmp(t, got, td.Contains(td.Nil())) fmt.Println("array contains Nil():", ok) ok = td.Cmp(t, got, td.Contains((*byte)(nil))) fmt.Println("array contains *byte nil:", ok) // types differ: *byte ≠ *int // Output: // array contains untyped nil: true // array contains *int nil: true // array contains Nil(): true // array contains *byte nil: false } func ExampleContains_map() { t := &testing.T{} ok := td.Cmp(t, map[string]int{"foo": 11, "bar": 22, "zip": 33}, td.Contains(22)) fmt.Println("map contains value 22:", ok) ok = td.Cmp(t, map[string]int{"foo": 11, "bar": 22, "zip": 33}, td.Contains(td.Between(20, 25))) fmt.Println("map contains at least one value in [20 .. 25]:", ok) // Output: // map contains value 22: true // map contains at least one value in [20 .. 25]: true } func ExampleContains_string() { t := &testing.T{} got := "foobar" ok := td.Cmp(t, got, td.Contains("oob"), "checks %s", got) fmt.Println("contains `oob` string:", ok) ok = td.Cmp(t, got, td.Contains([]byte("oob")), "checks %s", got) fmt.Println("contains `oob` []byte:", ok) ok = td.Cmp(t, got, td.Contains('b'), "checks %s", got) fmt.Println("contains 'b' rune:", ok) ok = td.Cmp(t, got, td.Contains(byte('a')), "checks %s", got) fmt.Println("contains 'a' byte:", ok) ok = td.Cmp(t, got, td.Contains(td.Between('n', 'p')), "checks %s", got) fmt.Println("contains at least one character ['n' .. 'p']:", ok) // Output: // contains `oob` string: true // contains `oob` []byte: true // contains 'b' rune: true // contains 'a' byte: true // contains at least one character ['n' .. 'p']: true } func ExampleContains_stringer() { t := &testing.T{} // bytes.Buffer implements fmt.Stringer got := bytes.NewBufferString("foobar") ok := td.Cmp(t, got, td.Contains("oob"), "checks %s", got) fmt.Println("contains `oob` string:", ok) ok = td.Cmp(t, got, td.Contains('b'), "checks %s", got) fmt.Println("contains 'b' rune:", ok) ok = td.Cmp(t, got, td.Contains(byte('a')), "checks %s", got) fmt.Println("contains 'a' byte:", ok) ok = td.Cmp(t, got, td.Contains(td.Between('n', 'p')), "checks %s", got) fmt.Println("contains at least one character ['n' .. 'p']:", ok) // Output: // contains `oob` string: true // contains 'b' rune: true // contains 'a' byte: true // contains at least one character ['n' .. 'p']: true } func ExampleContains_error() { t := &testing.T{} got := errors.New("foobar") ok := td.Cmp(t, got, td.Contains("oob"), "checks %s", got) fmt.Println("contains `oob` string:", ok) ok = td.Cmp(t, got, td.Contains('b'), "checks %s", got) fmt.Println("contains 'b' rune:", ok) ok = td.Cmp(t, got, td.Contains(byte('a')), "checks %s", got) fmt.Println("contains 'a' byte:", ok) ok = td.Cmp(t, got, td.Contains(td.Between('n', 'p')), "checks %s", got) fmt.Println("contains at least one character ['n' .. 'p']:", ok) // Output: // contains `oob` string: true // contains 'b' rune: true // contains 'a' byte: true // contains at least one character ['n' .. 'p']: true } func ExampleContainsKey() { t := &testing.T{} ok := td.Cmp(t, map[string]int{"foo": 11, "bar": 22, "zip": 33}, td.ContainsKey("foo")) fmt.Println(`map contains key "foo":`, ok) ok = td.Cmp(t, map[int]bool{12: true, 24: false, 42: true, 51: false}, td.ContainsKey(td.Between(40, 50))) fmt.Println("map contains at least a key in [40 .. 50]:", ok) ok = td.Cmp(t, map[string]int{"FOO": 11, "bar": 22, "zip": 33}, td.ContainsKey(td.Smuggle(strings.ToLower, "foo"))) fmt.Println(`map contains key "foo" without taking case into account:`, ok) // Output: // map contains key "foo": true // map contains at least a key in [40 .. 50]: true // map contains key "foo" without taking case into account: true } func ExampleContainsKey_nil() { t := &testing.T{} num := 1234 got := map[*int]bool{&num: false, nil: true} ok := td.Cmp(t, got, td.ContainsKey(nil)) fmt.Println("map contains untyped nil key:", ok) ok = td.Cmp(t, got, td.ContainsKey((*int)(nil))) fmt.Println("map contains *int nil key:", ok) ok = td.Cmp(t, got, td.ContainsKey(td.Nil())) fmt.Println("map contains Nil() key:", ok) ok = td.Cmp(t, got, td.ContainsKey((*byte)(nil))) fmt.Println("map contains *byte nil key:", ok) // types differ: *byte ≠ *int // Output: // map contains untyped nil key: true // map contains *int nil key: true // map contains Nil() key: true // map contains *byte nil key: false } func ExampleDelay() { t := &testing.T{} cmpNow := func(expected td.TestDeep) bool { time.Sleep(time.Microsecond) // imagine a DB insert returning a CreatedAt return td.Cmp(t, time.Now(), expected) } before := time.Now() ok := cmpNow(td.Between(before, time.Now())) fmt.Println("Between called before compare:", ok) ok = cmpNow(td.Delay(func() td.TestDeep { return td.Between(before, time.Now()) })) fmt.Println("Between delayed until compare:", ok) // Output: // Between called before compare: false // Between delayed until compare: true } func ExampleEmpty() { t := &testing.T{} ok := td.Cmp(t, nil, td.Empty()) // special case: nil is considered empty fmt.Println(ok) // fails, typed nil is not empty (expect for channel, map, slice or // pointers on array, channel, map slice and strings) ok = td.Cmp(t, (*int)(nil), td.Empty()) fmt.Println(ok) ok = td.Cmp(t, "", td.Empty()) fmt.Println(ok) // Fails as 0 is a number, so not empty. Use Zero() instead ok = td.Cmp(t, 0, td.Empty()) fmt.Println(ok) ok = td.Cmp(t, (map[string]int)(nil), td.Empty()) fmt.Println(ok) ok = td.Cmp(t, map[string]int{}, td.Empty()) fmt.Println(ok) ok = td.Cmp(t, ([]int)(nil), td.Empty()) fmt.Println(ok) ok = td.Cmp(t, []int{}, td.Empty()) fmt.Println(ok) ok = td.Cmp(t, []int{3}, td.Empty()) // fails, as not empty fmt.Println(ok) ok = td.Cmp(t, [3]int{}, td.Empty()) // fails, Empty() is not Zero()! fmt.Println(ok) // Output: // true // false // true // false // true // true // true // true // false // false } func ExampleEmpty_pointers() { t := &testing.T{} type MySlice []int ok := td.Cmp(t, MySlice{}, td.Empty()) // Ptr() not needed fmt.Println(ok) ok = td.Cmp(t, &MySlice{}, td.Empty()) fmt.Println(ok) l1 := &MySlice{} l2 := &l1 l3 := &l2 ok = td.Cmp(t, &l3, td.Empty()) fmt.Println(ok) // Works the same for array, map, channel and string // But not for others types as: type MyStruct struct { Value int } ok = td.Cmp(t, &MyStruct{}, td.Empty()) // fails, use Zero() instead fmt.Println(ok) // Output: // true // true // true // false } func ExampleErrorIs() { t := &testing.T{} err1 := fmt.Errorf("failure1") err2 := fmt.Errorf("failure2: %w", err1) err3 := fmt.Errorf("failure3: %w", err2) err := fmt.Errorf("failure4: %w", err3) ok := td.Cmp(t, err, td.ErrorIs(err)) fmt.Println("error is itself:", ok) ok = td.Cmp(t, err, td.ErrorIs(err1)) fmt.Println("error is also err1:", ok) ok = td.Cmp(t, err1, td.ErrorIs(err)) fmt.Println("err1 is err:", ok) // Output: // error is itself: true // error is also err1: true // err1 is err: false } func ExampleFirst_classic() { t := &testing.T{} got := []int{-3, -2, -1, 0, 1, 2, 3} ok := td.Cmp(t, got, td.First(td.Gt(0), 1)) fmt.Println("first positive number is 1:", ok) isEven := func(x int) bool { return x%2 == 0 } ok = td.Cmp(t, got, td.First(isEven, -2)) fmt.Println("first even number is -2:", ok) ok = td.Cmp(t, got, td.First(isEven, td.Lt(0))) fmt.Println("first even number is < 0:", ok) ok = td.Cmp(t, got, td.First(isEven, td.Code(isEven))) fmt.Println("first even number is well even:", ok) // Output: // first positive number is 1: true // first even number is -2: true // first even number is < 0: true // first even number is well even: true } func ExampleFirst_empty() { t := &testing.T{} ok := td.Cmp(t, ([]int)(nil), td.First(td.Gt(0), td.Gt(0))) fmt.Println("first in nil slice:", ok) ok = td.Cmp(t, []int{}, td.First(td.Gt(0), td.Gt(0))) fmt.Println("first in empty slice:", ok) ok = td.Cmp(t, &[]int{}, td.First(td.Gt(0), td.Gt(0))) fmt.Println("first in empty pointed slice:", ok) ok = td.Cmp(t, [0]int{}, td.First(td.Gt(0), td.Gt(0))) fmt.Println("first in empty array:", ok) // Output: // first in nil slice: false // first in empty slice: false // first in empty pointed slice: false // first in empty array: false } func ExampleFirst_struct() { t := &testing.T{} type Person struct { Fullname string `json:"fullname"` Age int `json:"age"` } got := []*Person{ { Fullname: "Bob Foobar", Age: 42, }, { Fullname: "Alice Bingo", Age: 37, }, } ok := td.Cmp(t, got, td.First( td.Smuggle("Age", td.Gt(30)), td.Smuggle("Fullname", "Bob Foobar"))) fmt.Println("first person.Age > 30 → Bob:", ok) ok = td.Cmp(t, got, td.First( td.JSONPointer("/age", td.Gt(30)), td.SuperJSONOf(`{"fullname":"Bob Foobar"}`))) fmt.Println("first person.Age > 30 → Bob, using JSON:", ok) ok = td.Cmp(t, got, td.First( td.JSONPointer("/age", td.Gt(30)), td.JSONPointer("/fullname", td.HasPrefix("Bob")))) fmt.Println("first person.Age > 30 → Bob, using JSONPointer:", ok) // Output: // first person.Age > 30 → Bob: true // first person.Age > 30 → Bob, using JSON: true // first person.Age > 30 → Bob, using JSONPointer: true } func ExampleFirst_json() { t := &testing.T{} got := map[string]any{ "values": []int{1, 2, 3, 4}, } ok := td.Cmp(t, got, td.JSON(`{"values": First(Gt(2), 3)}`)) fmt.Println("first number > 2:", ok) got = map[string]any{ "persons": []map[string]any{ {"id": 1, "name": "Joe"}, {"id": 2, "name": "Bob"}, {"id": 3, "name": "Alice"}, {"id": 4, "name": "Brian"}, {"id": 5, "name": "Britt"}, }, } ok = td.Cmp(t, got, td.JSON(` { "persons": First(JSONPointer("/name", "Brian"), {"id": 4, "name": "Brian"}) }`)) fmt.Println(`is "Brian" content OK:`, ok) ok = td.Cmp(t, got, td.JSON(` { "persons": First(JSONPointer("/name", "Brian"), JSONPointer("/id", 4)) }`)) fmt.Println(`ID of "Brian" is 4:`, ok) // Output: // first number > 2: true // is "Brian" content OK: true // ID of "Brian" is 4: true } func ExampleGrep_classic() { t := &testing.T{} got := []int{-3, -2, -1, 0, 1, 2, 3} ok := td.Cmp(t, got, td.Grep(td.Gt(0), []int{1, 2, 3})) fmt.Println("check positive numbers:", ok) isEven := func(x int) bool { return x%2 == 0 } ok = td.Cmp(t, got, td.Grep(isEven, []int{-2, 0, 2})) fmt.Println("even numbers are -2, 0 and 2:", ok) ok = td.Cmp(t, got, td.Grep(isEven, td.Set(0, 2, -2))) fmt.Println("even numbers are also 0, 2 and -2:", ok) ok = td.Cmp(t, got, td.Grep(isEven, td.ArrayEach(td.Code(isEven)))) fmt.Println("even numbers are each even:", ok) // Output: // check positive numbers: true // even numbers are -2, 0 and 2: true // even numbers are also 0, 2 and -2: true // even numbers are each even: true } func ExampleGrep_nil() { t := &testing.T{} var got []int ok := td.Cmp(t, got, td.Grep(td.Gt(0), ([]int)(nil))) fmt.Println("typed []int nil:", ok) ok = td.Cmp(t, got, td.Grep(td.Gt(0), ([]string)(nil))) fmt.Println("typed []string nil:", ok) ok = td.Cmp(t, got, td.Grep(td.Gt(0), td.Nil())) fmt.Println("td.Nil:", ok) ok = td.Cmp(t, got, td.Grep(td.Gt(0), []int{})) fmt.Println("empty non-nil slice:", ok) // Output: // typed []int nil: true // typed []string nil: false // td.Nil: true // empty non-nil slice: false } func ExampleGrep_struct() { t := &testing.T{} type Person struct { Fullname string `json:"fullname"` Age int `json:"age"` } got := []*Person{ { Fullname: "Bob Foobar", Age: 42, }, { Fullname: "Alice Bingo", Age: 27, }, } ok := td.Cmp(t, got, td.Grep( td.Smuggle("Age", td.Gt(30)), td.All( td.Len(1), td.ArrayEach(td.Smuggle("Fullname", "Bob Foobar")), ))) fmt.Println("person.Age > 30 → only Bob:", ok) ok = td.Cmp(t, got, td.Grep( td.JSONPointer("/age", td.Gt(30)), td.JSON(`[ SuperMapOf({"fullname":"Bob Foobar"}) ]`))) fmt.Println("person.Age > 30 → only Bob, using JSON:", ok) // Output: // person.Age > 30 → only Bob: true // person.Age > 30 → only Bob, using JSON: true } func ExampleGrep_json() { t := &testing.T{} got := map[string]any{ "values": []int{1, 2, 3, 4}, } ok := td.Cmp(t, got, td.JSON(`{"values": Grep(Gt(2), [3, 4])}`)) fmt.Println("grep a number > 2:", ok) got = map[string]any{ "persons": []map[string]any{ {"id": 1, "name": "Joe"}, {"id": 2, "name": "Bob"}, {"id": 3, "name": "Alice"}, {"id": 4, "name": "Brian"}, {"id": 5, "name": "Britt"}, }, } ok = td.Cmp(t, got, td.JSON(` { "persons": Grep(JSONPointer("/name", HasPrefix("Br")), [ {"id": 4, "name": "Brian"}, {"id": 5, "name": "Britt"}, ]) }`)) fmt.Println(`grep "Br" prefix:`, ok) // Output: // grep a number > 2: true // grep "Br" prefix: true } func ExampleGt_int() { t := &testing.T{} got := 156 ok := td.Cmp(t, got, td.Gt(155), "checks %v is > 155", got) fmt.Println(ok) ok = td.Cmp(t, got, td.Gt(156), "checks %v is > 156", got) fmt.Println(ok) // Output: // true // false } func ExampleGt_string() { t := &testing.T{} got := "abc" ok := td.Cmp(t, got, td.Gt("abb"), `checks "%v" is > "abb"`, got) fmt.Println(ok) ok = td.Cmp(t, got, td.Gt("abc"), `checks "%v" is > "abc"`, got) fmt.Println(ok) // Output: // true // false } func ExampleGte_int() { t := &testing.T{} got := 156 ok := td.Cmp(t, got, td.Gte(156), "checks %v is ≥ 156", got) fmt.Println(ok) ok = td.Cmp(t, got, td.Gte(155), "checks %v is ≥ 155", got) fmt.Println(ok) ok = td.Cmp(t, got, td.Gte(157), "checks %v is ≥ 157", got) fmt.Println(ok) // Output: // true // true // false } func ExampleGte_string() { t := &testing.T{} got := "abc" ok := td.Cmp(t, got, td.Gte("abc"), `checks "%v" is ≥ "abc"`, got) fmt.Println(ok) ok = td.Cmp(t, got, td.Gte("abb"), `checks "%v" is ≥ "abb"`, got) fmt.Println(ok) ok = td.Cmp(t, got, td.Gte("abd"), `checks "%v" is ≥ "abd"`, got) fmt.Println(ok) // Output: // true // true // false } func ExampleIsa() { t := &testing.T{} type TstStruct struct { Field int } got := TstStruct{Field: 1} ok := td.Cmp(t, got, td.Isa(TstStruct{}), "checks got is a TstStruct") fmt.Println(ok) ok = td.Cmp(t, got, td.Isa(&TstStruct{}), "checks got is a pointer on a TstStruct") fmt.Println(ok) ok = td.Cmp(t, &got, td.Isa(&TstStruct{}), "checks &got is a pointer on a TstStruct") fmt.Println(ok) // Output: // true // false // true } func ExampleIsa_interface() { t := &testing.T{} got := bytes.NewBufferString("foobar") ok := td.Cmp(t, got, td.Isa((*fmt.Stringer)(nil)), "checks got implements fmt.Stringer interface") fmt.Println(ok) errGot := fmt.Errorf("An error #%d occurred", 123) ok = td.Cmp(t, errGot, td.Isa((*error)(nil)), "checks errGot is a *error or implements error interface") fmt.Println(ok) // As nil, is passed below, it is not an interface but nil… So it // does not match errGot = nil ok = td.Cmp(t, errGot, td.Isa((*error)(nil)), "checks errGot is a *error or implements error interface") fmt.Println(ok) // BUT if its address is passed, now it is OK as the types match ok = td.Cmp(t, &errGot, td.Isa((*error)(nil)), "checks &errGot is a *error or implements error interface") fmt.Println(ok) // Output: // true // true // false // true } func ExampleJSON_basic() { t := &testing.T{} got := &struct { Fullname string `json:"fullname"` Age int `json:"age"` }{ Fullname: "Bob", Age: 42, } ok := td.Cmp(t, got, td.JSON(`{"age":42,"fullname":"Bob"}`)) fmt.Println("check got with age then fullname:", ok) ok = td.Cmp(t, got, td.JSON(`{"fullname":"Bob","age":42}`)) fmt.Println("check got with fullname then age:", ok) ok = td.Cmp(t, got, td.JSON(` // This should be the JSON representation of a struct { // A person: "fullname": "Bob", // The name of this person "age": 42 /* The age of this person: - 42 of course - to demonstrate a multi-lines comment */ }`)) fmt.Println("check got with nicely formatted and commented JSON:", ok) ok = td.Cmp(t, got, td.JSON(`{"fullname":"Bob","age":42,"gender":"male"}`)) fmt.Println("check got with gender field:", ok) ok = td.Cmp(t, got, td.JSON(`{"fullname":"Bob"}`)) fmt.Println("check got with fullname only:", ok) ok = td.Cmp(t, true, td.JSON(`true`)) fmt.Println("check boolean got is true:", ok) ok = td.Cmp(t, 42, td.JSON(`42`)) fmt.Println("check numeric got is 42:", ok) got = nil ok = td.Cmp(t, got, td.JSON(`null`)) fmt.Println("check nil got is null:", ok) // Output: // check got with age then fullname: true // check got with fullname then age: true // check got with nicely formatted and commented JSON: true // check got with gender field: false // check got with fullname only: false // check boolean got is true: true // check numeric got is 42: true // check nil got is null: true } func ExampleJSON_placeholders() { t := &testing.T{} type Person struct { Fullname string `json:"fullname"` Age int `json:"age"` Children []*Person `json:"children,omitempty"` } got := &Person{ Fullname: "Bob Foobar", Age: 42, } ok := td.Cmp(t, got, td.JSON(`{"age": $1, "fullname": $2}`, 42, "Bob Foobar")) fmt.Println("check got with numeric placeholders without operators:", ok) ok = td.Cmp(t, got, td.JSON(`{"age": $1, "fullname": $2}`, td.Between(40, 45), td.HasSuffix("Foobar"))) fmt.Println("check got with numeric placeholders:", ok) ok = td.Cmp(t, got, td.JSON(`{"age": "$1", "fullname": "$2"}`, td.Between(40, 45), td.HasSuffix("Foobar"))) fmt.Println("check got with double-quoted numeric placeholders:", ok) ok = td.Cmp(t, got, td.JSON(`{"age": $age, "fullname": $name}`, td.Tag("age", td.Between(40, 45)), td.Tag("name", td.HasSuffix("Foobar")))) fmt.Println("check got with named placeholders:", ok) got.Children = []*Person{ {Fullname: "Alice", Age: 28}, {Fullname: "Brian", Age: 22}, } ok = td.Cmp(t, got, td.JSON(`{"age": $age, "fullname": $name, "children": $children}`, td.Tag("age", td.Between(40, 45)), td.Tag("name", td.HasSuffix("Foobar")), td.Tag("children", td.Bag( &Person{Fullname: "Brian", Age: 22}, &Person{Fullname: "Alice", Age: 28}, )))) fmt.Println("check got w/named placeholders, and children w/go structs:", ok) ok = td.Cmp(t, got, td.JSON(`{"age": Between($1, $2), "fullname": HasSuffix($suffix), "children": Len(2)}`, 40, 45, td.Tag("suffix", "Foobar"))) fmt.Println("check got w/num & named placeholders:", ok) // Output: // check got with numeric placeholders without operators: true // check got with numeric placeholders: true // check got with double-quoted numeric placeholders: true // check got with named placeholders: true // check got w/named placeholders, and children w/go structs: true // check got w/num & named placeholders: true } func ExampleJSON_embedding() { t := &testing.T{} got := &struct { Fullname string `json:"fullname"` Age int `json:"age"` }{ Fullname: "Bob Foobar", Age: 42, } ok := td.Cmp(t, got, td.JSON(`{"age": NotZero(), "fullname": NotEmpty()}`)) fmt.Println("check got with simple operators:", ok) ok = td.Cmp(t, got, td.JSON(`{"age": $^NotZero, "fullname": $^NotEmpty}`)) fmt.Println("check got with operator shortcuts:", ok) ok = td.Cmp(t, got, td.JSON(` { "age": Between(40, 42, "]]"), // in ]40; 42] "fullname": All( HasPrefix("Bob"), HasSuffix("bar") // ← comma is optional here ) }`)) fmt.Println("check got with complex operators:", ok) ok = td.Cmp(t, got, td.JSON(` { "age": Between(40, 42, "]["), // in ]40; 42[ → 42 excluded "fullname": All( HasPrefix("Bob"), HasSuffix("bar"), ) }`)) fmt.Println("check got with complex operators:", ok) ok = td.Cmp(t, got, td.JSON(` { "age": Between($1, $2, $3), // in ]40; 42] "fullname": All( HasPrefix($4), HasSuffix("bar") // ← comma is optional here ) }`, 40, 42, td.BoundsOutIn, "Bob")) fmt.Println("check got with complex operators, w/placeholder args:", ok) // Output: // check got with simple operators: true // check got with operator shortcuts: true // check got with complex operators: true // check got with complex operators: false // check got with complex operators, w/placeholder args: true } func ExampleJSON_rawStrings() { t := &testing.T{} type details struct { Address string `json:"address"` Car string `json:"car"` } got := &struct { Fullname string `json:"fullname"` Age int `json:"age"` Details details `json:"details"` }{ Fullname: "Foo Bar", Age: 42, Details: details{ Address: "something", Car: "Peugeot", }, } ok := td.Cmp(t, got, td.JSON(` { "fullname": HasPrefix("Foo"), "age": Between(41, 43), "details": SuperMapOf({ "address": NotEmpty, // () are optional when no parameters "car": Any("Peugeot", "Tesla", "Jeep") // any of these }) }`)) fmt.Println("Original:", ok) ok = td.Cmp(t, got, td.JSON(` { "fullname": "$^HasPrefix(\"Foo\")", "age": "$^Between(41, 43)", "details": "$^SuperMapOf({\n\"address\": NotEmpty,\n\"car\": Any(\"Peugeot\", \"Tesla\", \"Jeep\")\n})" }`)) fmt.Println("JSON compliant:", ok) ok = td.Cmp(t, got, td.JSON(` { "fullname": "$^HasPrefix(\"Foo\")", "age": "$^Between(41, 43)", "details": "$^SuperMapOf({ \"address\": NotEmpty, // () are optional when no parameters \"car\": Any(\"Peugeot\", \"Tesla\", \"Jeep\") // any of these })" }`)) fmt.Println("JSON multilines strings:", ok) ok = td.Cmp(t, got, td.JSON(` { "fullname": "$^HasPrefix(r)", "age": "$^Between(41, 43)", "details": "$^SuperMapOf({ r
: NotEmpty, // () are optional when no parameters r: Any(r, r, r) // any of these })" }`)) fmt.Println("Raw strings:", ok) // Output: // Original: true // JSON compliant: true // JSON multilines strings: true // Raw strings: true } func ExampleJSON_file() { t := &testing.T{} got := &struct { Fullname string `json:"fullname"` Age int `json:"age"` Gender string `json:"gender"` }{ Fullname: "Bob Foobar", Age: 42, Gender: "male", } tmpDir, err := os.MkdirTemp("", "") if err != nil { t.Fatal(err) } defer os.RemoveAll(tmpDir) // clean up filename := tmpDir + "/test.json" if err = os.WriteFile(filename, []byte(` { "fullname": "$name", "age": "$age", "gender": "$gender" }`), 0644); err != nil { t.Fatal(err) } // OK let's test with this file ok := td.Cmp(t, got, td.JSON(filename, td.Tag("name", td.HasPrefix("Bob")), td.Tag("age", td.Between(40, 45)), td.Tag("gender", td.Re(`^(male|female)\z`)))) fmt.Println("Full match from file name:", ok) // When the file is already open file, err := os.Open(filename) if err != nil { t.Fatal(err) } ok = td.Cmp(t, got, td.JSON(file, td.Tag("name", td.HasPrefix("Bob")), td.Tag("age", td.Between(40, 45)), td.Tag("gender", td.Re(`^(male|female)\z`)))) fmt.Println("Full match from io.Reader:", ok) // Output: // Full match from file name: true // Full match from io.Reader: true } func ExampleJSONPointer_rfc6901() { t := &testing.T{} got := json.RawMessage(` { "foo": ["bar", "baz"], "": 0, "a/b": 1, "c%d": 2, "e^f": 3, "g|h": 4, "i\\j": 5, "k\"l": 6, " ": 7, "m~n": 8 }`) expected := map[string]any{ "foo": []any{"bar", "baz"}, "": 0, "a/b": 1, "c%d": 2, "e^f": 3, "g|h": 4, `i\j`: 5, `k"l`: 6, " ": 7, "m~n": 8, } ok := td.Cmp(t, got, td.JSONPointer("", expected)) fmt.Println("Empty JSON pointer means all:", ok) ok = td.Cmp(t, got, td.JSONPointer(`/foo`, []any{"bar", "baz"})) fmt.Println("Extract `foo` key:", ok) ok = td.Cmp(t, got, td.JSONPointer(`/foo/0`, "bar")) fmt.Println("First item of `foo` key slice:", ok) ok = td.Cmp(t, got, td.JSONPointer(`/`, 0)) fmt.Println("Empty key:", ok) ok = td.Cmp(t, got, td.JSONPointer(`/a~1b`, 1)) fmt.Println("Slash has to be escaped using `~1`:", ok) ok = td.Cmp(t, got, td.JSONPointer(`/c%d`, 2)) fmt.Println("% in key:", ok) ok = td.Cmp(t, got, td.JSONPointer(`/e^f`, 3)) fmt.Println("^ in key:", ok) ok = td.Cmp(t, got, td.JSONPointer(`/g|h`, 4)) fmt.Println("| in key:", ok) ok = td.Cmp(t, got, td.JSONPointer(`/i\j`, 5)) fmt.Println("Backslash in key:", ok) ok = td.Cmp(t, got, td.JSONPointer(`/k"l`, 6)) fmt.Println("Double-quote in key:", ok) ok = td.Cmp(t, got, td.JSONPointer(`/ `, 7)) fmt.Println("Space key:", ok) ok = td.Cmp(t, got, td.JSONPointer(`/m~0n`, 8)) fmt.Println("Tilde has to be escaped using `~0`:", ok) // Output: // Empty JSON pointer means all: true // Extract `foo` key: true // First item of `foo` key slice: true // Empty key: true // Slash has to be escaped using `~1`: true // % in key: true // ^ in key: true // | in key: true // Backslash in key: true // Double-quote in key: true // Space key: true // Tilde has to be escaped using `~0`: true } func ExampleJSONPointer_struct() { t := &testing.T{} // Without json tags, encoding/json uses public fields name type Item struct { Name string Value int64 Next *Item } got := Item{ Name: "first", Value: 1, Next: &Item{ Name: "second", Value: 2, Next: &Item{ Name: "third", Value: 3, }, }, } ok := td.Cmp(t, got, td.JSONPointer("/Next/Next/Name", "third")) fmt.Println("3rd item name is `third`:", ok) ok = td.Cmp(t, got, td.JSONPointer("/Next/Next/Value", td.Gte(int64(3)))) fmt.Println("3rd item value is greater or equal than 3:", ok) ok = td.Cmp(t, got, td.JSONPointer("/Next", td.JSONPointer("/Next", td.JSONPointer("/Value", td.Gte(int64(3)))))) fmt.Println("3rd item value is still greater or equal than 3:", ok) ok = td.Cmp(t, got, td.JSONPointer("/Next/Next/Next/Name", td.Ignore())) fmt.Println("4th item exists and has a name:", ok) // Struct comparison work with or without pointer: &Item{…} works too ok = td.Cmp(t, got, td.JSONPointer("/Next/Next", Item{ Name: "third", Value: 3, })) fmt.Println("3rd item full comparison:", ok) // Output: // 3rd item name is `third`: true // 3rd item value is greater or equal than 3: true // 3rd item value is still greater or equal than 3: true // 4th item exists and has a name: false // 3rd item full comparison: true } func ExampleJSONPointer_has_hasnt() { t := &testing.T{} got := json.RawMessage(` { "name": "Bob", "age": 42, "children": [ { "name": "Alice", "age": 16 }, { "name": "Britt", "age": 21, "children": [ { "name": "John", "age": 1 } ] } ] }`) // Has Bob some children? ok := td.Cmp(t, got, td.JSONPointer("/children", td.Len(td.Gt(0)))) fmt.Println("Bob has at least one child:", ok) // But checking "children" exists is enough here ok = td.Cmp(t, got, td.JSONPointer("/children/0/children", td.Ignore())) fmt.Println("Alice has children:", ok) ok = td.Cmp(t, got, td.JSONPointer("/children/1/children", td.Ignore())) fmt.Println("Britt has children:", ok) // The reverse can be checked too ok = td.Cmp(t, got, td.Not(td.JSONPointer("/children/0/children", td.Ignore()))) fmt.Println("Alice hasn't children:", ok) ok = td.Cmp(t, got, td.Not(td.JSONPointer("/children/1/children", td.Ignore()))) fmt.Println("Britt hasn't children:", ok) // Output: // Bob has at least one child: true // Alice has children: false // Britt has children: true // Alice hasn't children: true // Britt hasn't children: false } func ExampleKeys() { t := &testing.T{} got := map[string]int{"foo": 1, "bar": 2, "zip": 3} // Keys tests keys in an ordered manner ok := td.Cmp(t, got, td.Keys([]string{"bar", "foo", "zip"})) fmt.Println("All sorted keys are found:", ok) // If the expected keys are not ordered, it fails ok = td.Cmp(t, got, td.Keys([]string{"zip", "bar", "foo"})) fmt.Println("All unsorted keys are found:", ok) // To circumvent that, one can use Bag operator ok = td.Cmp(t, got, td.Keys(td.Bag("zip", "bar", "foo"))) fmt.Println("All unsorted keys are found, with the help of Bag operator:", ok) // Check that each key is 3 bytes long ok = td.Cmp(t, got, td.Keys(td.ArrayEach(td.Len(3)))) fmt.Println("Each key is 3 bytes long:", ok) // Output: // All sorted keys are found: true // All unsorted keys are found: false // All unsorted keys are found, with the help of Bag operator: true // Each key is 3 bytes long: true } func ExampleLast_classic() { t := &testing.T{} got := []int{-3, -2, -1, 0, 1, 2, 3} ok := td.Cmp(t, got, td.Last(td.Lt(0), -1)) fmt.Println("last negative number is -1:", ok) isEven := func(x int) bool { return x%2 == 0 } ok = td.Cmp(t, got, td.Last(isEven, 2)) fmt.Println("last even number is 2:", ok) ok = td.Cmp(t, got, td.Last(isEven, td.Gt(0))) fmt.Println("last even number is > 0:", ok) ok = td.Cmp(t, got, td.Last(isEven, td.Code(isEven))) fmt.Println("last even number is well even:", ok) // Output: // last negative number is -1: true // last even number is 2: true // last even number is > 0: true // last even number is well even: true } func ExampleLast_empty() { t := &testing.T{} ok := td.Cmp(t, ([]int)(nil), td.Last(td.Gt(0), td.Gt(0))) fmt.Println("last in nil slice:", ok) ok = td.Cmp(t, []int{}, td.Last(td.Gt(0), td.Gt(0))) fmt.Println("last in empty slice:", ok) ok = td.Cmp(t, &[]int{}, td.Last(td.Gt(0), td.Gt(0))) fmt.Println("last in empty pointed slice:", ok) ok = td.Cmp(t, [0]int{}, td.Last(td.Gt(0), td.Gt(0))) fmt.Println("last in empty array:", ok) // Output: // last in nil slice: false // last in empty slice: false // last in empty pointed slice: false // last in empty array: false } func ExampleLast_struct() { t := &testing.T{} type Person struct { Fullname string `json:"fullname"` Age int `json:"age"` } got := []*Person{ { Fullname: "Bob Foobar", Age: 42, }, { Fullname: "Alice Bingo", Age: 37, }, } ok := td.Cmp(t, got, td.Last( td.Smuggle("Age", td.Gt(30)), td.Smuggle("Fullname", "Alice Bingo"))) fmt.Println("last person.Age > 30 → Alice:", ok) ok = td.Cmp(t, got, td.Last( td.JSONPointer("/age", td.Gt(30)), td.SuperJSONOf(`{"fullname":"Alice Bingo"}`))) fmt.Println("last person.Age > 30 → Alice, using JSON:", ok) ok = td.Cmp(t, got, td.Last( td.JSONPointer("/age", td.Gt(30)), td.JSONPointer("/fullname", td.HasPrefix("Alice")))) fmt.Println("first person.Age > 30 → Alice, using JSONPointer:", ok) // Output: // last person.Age > 30 → Alice: true // last person.Age > 30 → Alice, using JSON: true // first person.Age > 30 → Alice, using JSONPointer: true } func ExampleLast_json() { t := &testing.T{} got := map[string]any{ "values": []int{1, 2, 3, 4}, } ok := td.Cmp(t, got, td.JSON(`{"values": Last(Lt(3), 2)}`)) fmt.Println("last number < 3:", ok) got = map[string]any{ "persons": []map[string]any{ {"id": 1, "name": "Joe"}, {"id": 2, "name": "Bob"}, {"id": 3, "name": "Alice"}, {"id": 4, "name": "Brian"}, {"id": 5, "name": "Britt"}, }, } ok = td.Cmp(t, got, td.JSON(` { "persons": Last(JSONPointer("/name", "Brian"), {"id": 4, "name": "Brian"}) }`)) fmt.Println(`is "Brian" content OK:`, ok) ok = td.Cmp(t, got, td.JSON(` { "persons": Last(JSONPointer("/name", "Brian"), JSONPointer("/id", 4)) }`)) fmt.Println(`ID of "Brian" is 4:`, ok) // Output: // last number < 3: true // is "Brian" content OK: true // ID of "Brian" is 4: true } func ExampleLax() { t := &testing.T{} gotInt64 := int64(1234) gotInt32 := int32(1235) type myInt uint16 gotMyInt := myInt(1236) expected := td.Between(1230, 1240) // int type here ok := td.Cmp(t, gotInt64, td.Lax(expected)) fmt.Println("int64 got between ints [1230 .. 1240]:", ok) ok = td.Cmp(t, gotInt32, td.Lax(expected)) fmt.Println("int32 got between ints [1230 .. 1240]:", ok) ok = td.Cmp(t, gotMyInt, td.Lax(expected)) fmt.Println("myInt got between ints [1230 .. 1240]:", ok) // Output: // int64 got between ints [1230 .. 1240]: true // int32 got between ints [1230 .. 1240]: true // myInt got between ints [1230 .. 1240]: true } func ExampleLen_slice() { t := &testing.T{} got := []int{11, 22, 33} ok := td.Cmp(t, got, td.Len(3), "checks %v len is 3", got) fmt.Println(ok) ok = td.Cmp(t, got, td.Len(0), "checks %v len is 0", got) fmt.Println(ok) got = nil ok = td.Cmp(t, got, td.Len(0), "checks %v len is 0", got) fmt.Println(ok) // Output: // true // false // true } func ExampleLen_map() { t := &testing.T{} got := map[int]bool{11: true, 22: false, 33: false} ok := td.Cmp(t, got, td.Len(3), "checks %v len is 3", got) fmt.Println(ok) ok = td.Cmp(t, got, td.Len(0), "checks %v len is 0", got) fmt.Println(ok) got = nil ok = td.Cmp(t, got, td.Len(0), "checks %v len is 0", got) fmt.Println(ok) // Output: // true // false // true } func ExampleLen_operatorSlice() { t := &testing.T{} got := []int{11, 22, 33} ok := td.Cmp(t, got, td.Len(td.Between(3, 8)), "checks %v len is in [3 .. 8]", got) fmt.Println(ok) ok = td.Cmp(t, got, td.Len(td.Lt(5)), "checks %v len is < 5", got) fmt.Println(ok) // Output: // true // true } func ExampleLen_operatorMap() { t := &testing.T{} got := map[int]bool{11: true, 22: false, 33: false} ok := td.Cmp(t, got, td.Len(td.Between(3, 8)), "checks %v len is in [3 .. 8]", got) fmt.Println(ok) ok = td.Cmp(t, got, td.Len(td.Gte(3)), "checks %v len is ≥ 3", got) fmt.Println(ok) // Output: // true // true } func ExampleLt_int() { t := &testing.T{} got := 156 ok := td.Cmp(t, got, td.Lt(157), "checks %v is < 157", got) fmt.Println(ok) ok = td.Cmp(t, got, td.Lt(156), "checks %v is < 156", got) fmt.Println(ok) // Output: // true // false } func ExampleLt_string() { t := &testing.T{} got := "abc" ok := td.Cmp(t, got, td.Lt("abd"), `checks "%v" is < "abd"`, got) fmt.Println(ok) ok = td.Cmp(t, got, td.Lt("abc"), `checks "%v" is < "abc"`, got) fmt.Println(ok) // Output: // true // false } func ExampleLte_int() { t := &testing.T{} got := 156 ok := td.Cmp(t, got, td.Lte(156), "checks %v is ≤ 156", got) fmt.Println(ok) ok = td.Cmp(t, got, td.Lte(157), "checks %v is ≤ 157", got) fmt.Println(ok) ok = td.Cmp(t, got, td.Lte(155), "checks %v is ≤ 155", got) fmt.Println(ok) // Output: // true // true // false } func ExampleLte_string() { t := &testing.T{} got := "abc" ok := td.Cmp(t, got, td.Lte("abc"), `checks "%v" is ≤ "abc"`, got) fmt.Println(ok) ok = td.Cmp(t, got, td.Lte("abd"), `checks "%v" is ≤ "abd"`, got) fmt.Println(ok) ok = td.Cmp(t, got, td.Lte("abb"), `checks "%v" is ≤ "abb"`, got) fmt.Println(ok) // Output: // true // true // false } func ExampleMap_map() { t := &testing.T{} got := map[string]int{"foo": 12, "bar": 42, "zip": 89} ok := td.Cmp(t, got, td.Map(map[string]int{"bar": 42}, td.MapEntries{"foo": td.Lt(15), "zip": td.Ignore()}), "checks map %v", got) fmt.Println(ok) ok = td.Cmp(t, got, td.Map(map[string]int{}, td.MapEntries{"bar": 42, "foo": td.Lt(15), "zip": td.Ignore()}), "checks map %v", got) fmt.Println(ok) ok = td.Cmp(t, got, td.Map((map[string]int)(nil), td.MapEntries{"bar": 42, "foo": td.Lt(15), "zip": td.Ignore()}), "checks map %v", got) fmt.Println(ok) // Output: // true // true // true } func ExampleMap_typedMap() { t := &testing.T{} type MyMap map[string]int got := MyMap{"foo": 12, "bar": 42, "zip": 89} ok := td.Cmp(t, got, td.Map(MyMap{"bar": 42}, td.MapEntries{"foo": td.Lt(15), "zip": td.Ignore()}), "checks typed map %v", got) fmt.Println(ok) ok = td.Cmp(t, &got, td.Map(&MyMap{"bar": 42}, td.MapEntries{"foo": td.Lt(15), "zip": td.Ignore()}), "checks pointer on typed map %v", got) fmt.Println(ok) ok = td.Cmp(t, &got, td.Map(&MyMap{}, td.MapEntries{"bar": 42, "foo": td.Lt(15), "zip": td.Ignore()}), "checks pointer on typed map %v", got) fmt.Println(ok) ok = td.Cmp(t, &got, td.Map((*MyMap)(nil), td.MapEntries{"bar": 42, "foo": td.Lt(15), "zip": td.Ignore()}), "checks pointer on typed map %v", got) fmt.Println(ok) // Output: // true // true // true // true } func ExampleMapEach_map() { t := &testing.T{} got := map[string]int{"foo": 12, "bar": 42, "zip": 89} ok := td.Cmp(t, got, td.MapEach(td.Between(10, 90)), "checks each value of map %v is in [10 .. 90]", got) fmt.Println(ok) // Output: // true } func ExampleMapEach_typedMap() { t := &testing.T{} type MyMap map[string]int got := MyMap{"foo": 12, "bar": 42, "zip": 89} ok := td.Cmp(t, got, td.MapEach(td.Between(10, 90)), "checks each value of typed map %v is in [10 .. 90]", got) fmt.Println(ok) ok = td.Cmp(t, &got, td.MapEach(td.Between(10, 90)), "checks each value of typed map pointer %v is in [10 .. 90]", got) fmt.Println(ok) // Output: // true // true } func ExampleN() { t := &testing.T{} got := 1.12345 ok := td.Cmp(t, got, td.N(1.1234, 0.00006), "checks %v = 1.1234 ± 0.00006", got) fmt.Println(ok) // Output: // true } func ExampleNaN_float32() { t := &testing.T{} got := float32(math.NaN()) ok := td.Cmp(t, got, td.NaN(), "checks %v is not-a-number", got) fmt.Println("float32(math.NaN()) is float32 not-a-number:", ok) got = 12 ok = td.Cmp(t, got, td.NaN(), "checks %v is not-a-number", got) fmt.Println("float32(12) is float32 not-a-number:", ok) // Output: // float32(math.NaN()) is float32 not-a-number: true // float32(12) is float32 not-a-number: false } func ExampleNaN_float64() { t := &testing.T{} got := math.NaN() ok := td.Cmp(t, got, td.NaN(), "checks %v is not-a-number", got) fmt.Println("math.NaN() is not-a-number:", ok) got = 12 ok = td.Cmp(t, got, td.NaN(), "checks %v is not-a-number", got) fmt.Println("float64(12) is not-a-number:", ok) // math.NaN() is not-a-number: true // float64(12) is not-a-number: false } func ExampleNil() { t := &testing.T{} var got fmt.Stringer // interface // nil value can be compared directly with nil, no need of Nil() here ok := td.Cmp(t, got, nil) fmt.Println(ok) // But it works with Nil() anyway ok = td.Cmp(t, got, td.Nil()) fmt.Println(ok) got = (*bytes.Buffer)(nil) // In the case of an interface containing a nil pointer, comparing // with nil fails, as the interface is not nil ok = td.Cmp(t, got, nil) fmt.Println(ok) // In this case Nil() succeed ok = td.Cmp(t, got, td.Nil()) fmt.Println(ok) // Output: // true // true // false // true } func ExampleNone() { t := &testing.T{} got := 18 ok := td.Cmp(t, got, td.None(0, 10, 20, 30, td.Between(100, 199)), "checks %v is non-null, and ≠ 10, 20 & 30, and not in [100-199]", got) fmt.Println(ok) got = 20 ok = td.Cmp(t, got, td.None(0, 10, 20, 30, td.Between(100, 199)), "checks %v is non-null, and ≠ 10, 20 & 30, and not in [100-199]", got) fmt.Println(ok) got = 142 ok = td.Cmp(t, got, td.None(0, 10, 20, 30, td.Between(100, 199)), "checks %v is non-null, and ≠ 10, 20 & 30, and not in [100-199]", got) fmt.Println(ok) prime := td.Flatten([]int{1, 2, 3, 5, 7, 11, 13}) even := td.Flatten([]int{2, 4, 6, 8, 10, 12, 14}) for _, got := range [...]int{9, 3, 8, 15} { ok = td.Cmp(t, got, td.None(prime, even, td.Gt(14)), "checks %v is not prime number, nor an even number and not > 14") fmt.Printf("%d → %t\n", got, ok) } // Output: // true // false // false // 9 → true // 3 → false // 8 → false // 15 → false } func ExampleNotAny() { t := &testing.T{} got := []int{4, 5, 9, 42} ok := td.Cmp(t, got, td.NotAny(3, 6, 8, 41, 43), "checks %v contains no item listed in NotAny()", got) fmt.Println(ok) ok = td.Cmp(t, got, td.NotAny(3, 6, 8, 42, 43), "checks %v contains no item listed in NotAny()", got) fmt.Println(ok) // When expected is already a non-[]any slice, it cannot be // flattened directly using notExpected... without copying it to a new // []any slice, then use td.Flatten! notExpected := []int{3, 6, 8, 41, 43} ok = td.Cmp(t, got, td.NotAny(td.Flatten(notExpected)), "checks %v contains no item listed in notExpected", got) fmt.Println(ok) // Output: // true // false // true } func ExampleNot() { t := &testing.T{} got := 42 ok := td.Cmp(t, got, td.Not(0), "checks %v is non-null", got) fmt.Println(ok) ok = td.Cmp(t, got, td.Not(td.Between(10, 30)), "checks %v is not in [10 .. 30]", got) fmt.Println(ok) got = 0 ok = td.Cmp(t, got, td.Not(0), "checks %v is non-null", got) fmt.Println(ok) // Output: // true // true // false } func ExampleNotEmpty() { t := &testing.T{} ok := td.Cmp(t, nil, td.NotEmpty()) // fails, as nil is considered empty fmt.Println(ok) ok = td.Cmp(t, "foobar", td.NotEmpty()) fmt.Println(ok) // Fails as 0 is a number, so not empty. Use NotZero() instead ok = td.Cmp(t, 0, td.NotEmpty()) fmt.Println(ok) ok = td.Cmp(t, map[string]int{"foobar": 42}, td.NotEmpty()) fmt.Println(ok) ok = td.Cmp(t, []int{1}, td.NotEmpty()) fmt.Println(ok) ok = td.Cmp(t, [3]int{}, td.NotEmpty()) // succeeds, NotEmpty() is not NotZero()! fmt.Println(ok) // Output: // false // true // false // true // true // true } func ExampleNotEmpty_pointers() { t := &testing.T{} type MySlice []int ok := td.Cmp(t, MySlice{12}, td.NotEmpty()) fmt.Println(ok) ok = td.Cmp(t, &MySlice{12}, td.NotEmpty()) // Ptr() not needed fmt.Println(ok) l1 := &MySlice{12} l2 := &l1 l3 := &l2 ok = td.Cmp(t, &l3, td.NotEmpty()) fmt.Println(ok) // Works the same for array, map, channel and string // But not for others types as: type MyStruct struct { Value int } ok = td.Cmp(t, &MyStruct{}, td.NotEmpty()) // fails, use NotZero() instead fmt.Println(ok) // Output: // true // true // true // false } func ExampleNotNaN_float32() { t := &testing.T{} got := float32(math.NaN()) ok := td.Cmp(t, got, td.NotNaN(), "checks %v is not-a-number", got) fmt.Println("float32(math.NaN()) is NOT float32 not-a-number:", ok) got = 12 ok = td.Cmp(t, got, td.NotNaN(), "checks %v is not-a-number", got) fmt.Println("float32(12) is NOT float32 not-a-number:", ok) // Output: // float32(math.NaN()) is NOT float32 not-a-number: false // float32(12) is NOT float32 not-a-number: true } func ExampleNotNaN_float64() { t := &testing.T{} got := math.NaN() ok := td.Cmp(t, got, td.NotNaN(), "checks %v is not-a-number", got) fmt.Println("math.NaN() is not-a-number:", ok) got = 12 ok = td.Cmp(t, got, td.NotNaN(), "checks %v is not-a-number", got) fmt.Println("float64(12) is not-a-number:", ok) // math.NaN() is NOT not-a-number: false // float64(12) is NOT not-a-number: true } func ExampleNotNil() { t := &testing.T{} var got fmt.Stringer = &bytes.Buffer{} // nil value can be compared directly with Not(nil), no need of NotNil() here ok := td.Cmp(t, got, td.Not(nil)) fmt.Println(ok) // But it works with NotNil() anyway ok = td.Cmp(t, got, td.NotNil()) fmt.Println(ok) got = (*bytes.Buffer)(nil) // In the case of an interface containing a nil pointer, comparing // with Not(nil) succeeds, as the interface is not nil ok = td.Cmp(t, got, td.Not(nil)) fmt.Println(ok) // In this case NotNil() fails ok = td.Cmp(t, got, td.NotNil()) fmt.Println(ok) // Output: // true // true // true // false } func ExampleNotZero() { t := &testing.T{} ok := td.Cmp(t, 0, td.NotZero()) // fails fmt.Println(ok) ok = td.Cmp(t, float64(0), td.NotZero()) // fails fmt.Println(ok) ok = td.Cmp(t, 12, td.NotZero()) fmt.Println(ok) ok = td.Cmp(t, (map[string]int)(nil), td.NotZero()) // fails, as nil fmt.Println(ok) ok = td.Cmp(t, map[string]int{}, td.NotZero()) // succeeds, as not nil fmt.Println(ok) ok = td.Cmp(t, ([]int)(nil), td.NotZero()) // fails, as nil fmt.Println(ok) ok = td.Cmp(t, []int{}, td.NotZero()) // succeeds, as not nil fmt.Println(ok) ok = td.Cmp(t, [3]int{}, td.NotZero()) // fails fmt.Println(ok) ok = td.Cmp(t, [3]int{0, 1}, td.NotZero()) // succeeds, DATA[1] is not 0 fmt.Println(ok) ok = td.Cmp(t, bytes.Buffer{}, td.NotZero()) // fails fmt.Println(ok) ok = td.Cmp(t, &bytes.Buffer{}, td.NotZero()) // succeeds, as pointer not nil fmt.Println(ok) ok = td.Cmp(t, &bytes.Buffer{}, td.Ptr(td.NotZero())) // fails as deref by Ptr() fmt.Println(ok) // Output: // false // false // true // false // true // false // true // false // true // false // true // false } func ExamplePPtr() { t := &testing.T{} num := 12 got := &num ok := td.Cmp(t, &got, td.PPtr(12)) fmt.Println(ok) ok = td.Cmp(t, &got, td.PPtr(td.Between(4, 15))) fmt.Println(ok) // Output: // true // true } func ExamplePtr() { t := &testing.T{} got := 12 ok := td.Cmp(t, &got, td.Ptr(12)) fmt.Println(ok) ok = td.Cmp(t, &got, td.Ptr(td.Between(4, 15))) fmt.Println(ok) // Output: // true // true } func ExampleRe() { t := &testing.T{} got := "foo bar" ok := td.Cmp(t, got, td.Re("(zip|bar)$"), "checks value %s", got) fmt.Println(ok) got = "bar foo" ok = td.Cmp(t, got, td.Re("(zip|bar)$"), "checks value %s", got) fmt.Println(ok) // Output: // true // false } func ExampleRe_stringer() { t := &testing.T{} // bytes.Buffer implements fmt.Stringer got := bytes.NewBufferString("foo bar") ok := td.Cmp(t, got, td.Re("(zip|bar)$"), "checks value %s", got) fmt.Println(ok) // Output: // true } func ExampleRe_error() { t := &testing.T{} got := errors.New("foo bar") ok := td.Cmp(t, got, td.Re("(zip|bar)$"), "checks value %s", got) fmt.Println(ok) // Output: // true } func ExampleRe_capture() { t := &testing.T{} got := "foo bar biz" ok := td.Cmp(t, got, td.Re(`^(\w+) (\w+) (\w+)$`, td.Set("biz", "foo", "bar")), "checks value %s", got) fmt.Println(ok) got = "foo bar! biz" ok = td.Cmp(t, got, td.Re(`^(\w+) (\w+) (\w+)$`, td.Set("biz", "foo", "bar")), "checks value %s", got) fmt.Println(ok) // Output: // true // false } func ExampleReAll_capture() { t := &testing.T{} got := "foo bar biz" ok := td.Cmp(t, got, td.ReAll(`(\w+)`, td.Set("biz", "foo", "bar")), "checks value %s", got) fmt.Println(ok) // Matches, but all catured groups do not match Set got = "foo BAR biz" ok = td.Cmp(t, got, td.ReAll(`(\w+)`, td.Set("biz", "foo", "bar")), "checks value %s", got) fmt.Println(ok) // Output: // true // false } func ExampleReAll_captureComplex() { t := &testing.T{} got := "11 45 23 56 85 96" ok := td.Cmp(t, got, td.ReAll(`(\d+)`, td.ArrayEach(td.Code(func(num string) bool { n, err := strconv.Atoi(num) return err == nil && n > 10 && n < 100 }))), "checks value %s", got) fmt.Println(ok) // Matches, but 11 is not greater than 20 ok = td.Cmp(t, got, td.ReAll(`(\d+)`, td.ArrayEach(td.Code(func(num string) bool { n, err := strconv.Atoi(num) return err == nil && n > 20 && n < 100 }))), "checks value %s", got) fmt.Println(ok) // Output: // true // false } func ExampleRe_compiled() { t := &testing.T{} expected := regexp.MustCompile("(zip|bar)$") got := "foo bar" ok := td.Cmp(t, got, td.Re(expected), "checks value %s", got) fmt.Println(ok) got = "bar foo" ok = td.Cmp(t, got, td.Re(expected), "checks value %s", got) fmt.Println(ok) // Output: // true // false } func ExampleRe_compiledStringer() { t := &testing.T{} expected := regexp.MustCompile("(zip|bar)$") // bytes.Buffer implements fmt.Stringer got := bytes.NewBufferString("foo bar") ok := td.Cmp(t, got, td.Re(expected), "checks value %s", got) fmt.Println(ok) // Output: // true } func ExampleRe_compiledError() { t := &testing.T{} expected := regexp.MustCompile("(zip|bar)$") got := errors.New("foo bar") ok := td.Cmp(t, got, td.Re(expected), "checks value %s", got) fmt.Println(ok) // Output: // true } func ExampleRe_compiledCapture() { t := &testing.T{} expected := regexp.MustCompile(`^(\w+) (\w+) (\w+)$`) got := "foo bar biz" ok := td.Cmp(t, got, td.Re(expected, td.Set("biz", "foo", "bar")), "checks value %s", got) fmt.Println(ok) got = "foo bar! biz" ok = td.Cmp(t, got, td.Re(expected, td.Set("biz", "foo", "bar")), "checks value %s", got) fmt.Println(ok) // Output: // true // false } func ExampleReAll_compiledCapture() { t := &testing.T{} expected := regexp.MustCompile(`(\w+)`) got := "foo bar biz" ok := td.Cmp(t, got, td.ReAll(expected, td.Set("biz", "foo", "bar")), "checks value %s", got) fmt.Println(ok) // Matches, but all catured groups do not match Set got = "foo BAR biz" ok = td.Cmp(t, got, td.ReAll(expected, td.Set("biz", "foo", "bar")), "checks value %s", got) fmt.Println(ok) // Output: // true // false } func ExampleReAll_compiledCaptureComplex() { t := &testing.T{} expected := regexp.MustCompile(`(\d+)`) got := "11 45 23 56 85 96" ok := td.Cmp(t, got, td.ReAll(expected, td.ArrayEach(td.Code(func(num string) bool { n, err := strconv.Atoi(num) return err == nil && n > 10 && n < 100 }))), "checks value %s", got) fmt.Println(ok) // Matches, but 11 is not greater than 20 ok = td.Cmp(t, got, td.ReAll(expected, td.ArrayEach(td.Code(func(num string) bool { n, err := strconv.Atoi(num) return err == nil && n > 20 && n < 100 }))), "checks value %s", got) fmt.Println(ok) // Output: // true // false } func ExampleRecv_basic() { t := &testing.T{} got := make(chan int, 3) ok := td.Cmp(t, got, td.Recv(td.RecvNothing)) fmt.Println("nothing to receive:", ok) got <- 1 got <- 2 got <- 3 close(got) ok = td.Cmp(t, got, td.Recv(1)) fmt.Println("1st receive is 1:", ok) ok = td.Cmp(t, got, td.All( td.Recv(2), td.Recv(td.Between(3, 4)), td.Recv(td.RecvClosed), )) fmt.Println("next receives are 2, 3 then closed:", ok) ok = td.Cmp(t, got, td.Recv(td.RecvNothing)) fmt.Println("nothing to receive:", ok) // Output: // nothing to receive: true // 1st receive is 1: true // next receives are 2, 3 then closed: true // nothing to receive: false } func ExampleRecv_channelPointer() { t := &testing.T{} got := make(chan int, 3) ok := td.Cmp(t, got, td.Recv(td.RecvNothing)) fmt.Println("nothing to receive:", ok) got <- 1 got <- 2 got <- 3 close(got) ok = td.Cmp(t, &got, td.Recv(1)) fmt.Println("1st receive is 1:", ok) ok = td.Cmp(t, &got, td.All( td.Recv(2), td.Recv(td.Between(3, 4)), td.Recv(td.RecvClosed), )) fmt.Println("next receives are 2, 3 then closed:", ok) ok = td.Cmp(t, got, td.Recv(td.RecvNothing)) fmt.Println("nothing to receive:", ok) // Output: // nothing to receive: true // 1st receive is 1: true // next receives are 2, 3 then closed: true // nothing to receive: false } func ExampleRecv_withTimeout() { t := &testing.T{} got := make(chan int, 1) tick := make(chan struct{}) go func() { // ① <-tick time.Sleep(100 * time.Millisecond) got <- 0 // ② <-tick time.Sleep(100 * time.Millisecond) got <- 1 // ③ <-tick time.Sleep(100 * time.Millisecond) close(got) }() td.Cmp(t, got, td.Recv(td.RecvNothing)) // ① tick <- struct{}{} ok := td.Cmp(t, got, td.Recv(td.RecvNothing)) fmt.Println("① RecvNothing:", ok) ok = td.Cmp(t, got, td.Recv(0, 150*time.Millisecond)) fmt.Println("① receive 0 w/150ms timeout:", ok) ok = td.Cmp(t, got, td.Recv(td.RecvNothing)) fmt.Println("① RecvNothing:", ok) // ② tick <- struct{}{} ok = td.Cmp(t, got, td.Recv(td.RecvNothing)) fmt.Println("② RecvNothing:", ok) ok = td.Cmp(t, got, td.Recv(1, 150*time.Millisecond)) fmt.Println("② receive 1 w/150ms timeout:", ok) ok = td.Cmp(t, got, td.Recv(td.RecvNothing)) fmt.Println("② RecvNothing:", ok) // ③ tick <- struct{}{} ok = td.Cmp(t, got, td.Recv(td.RecvNothing)) fmt.Println("③ RecvNothing:", ok) ok = td.Cmp(t, got, td.Recv(td.RecvClosed, 150*time.Millisecond)) fmt.Println("③ check closed w/150ms timeout:", ok) // Output: // ① RecvNothing: true // ① receive 0 w/150ms timeout: true // ① RecvNothing: true // ② RecvNothing: true // ② receive 1 w/150ms timeout: true // ② RecvNothing: true // ③ RecvNothing: true // ③ check closed w/150ms timeout: true } func ExampleRecv_nilChannel() { t := &testing.T{} var ch chan int ok := td.Cmp(t, ch, td.Recv(td.RecvNothing)) fmt.Println("nothing to receive from nil channel:", ok) ok = td.Cmp(t, ch, td.Recv(42)) fmt.Println("something to receive from nil channel:", ok) ok = td.Cmp(t, ch, td.Recv(td.RecvClosed)) fmt.Println("is a nil channel closed:", ok) // Output: // nothing to receive from nil channel: true // something to receive from nil channel: false // is a nil channel closed: false } func ExampleSet() { t := &testing.T{} got := []int{1, 3, 5, 8, 8, 1, 2} // Matches as all items are present, ignoring duplicates ok := td.Cmp(t, got, td.Set(1, 2, 3, 5, 8), "checks all items are present, in any order") fmt.Println(ok) // Duplicates are ignored in a Set ok = td.Cmp(t, got, td.Set(1, 2, 2, 2, 2, 2, 3, 5, 8), "checks all items are present, in any order") fmt.Println(ok) // Tries its best to not raise an error when a value can be matched // by several Set entries ok = td.Cmp(t, got, td.Set(td.Between(1, 4), 3, td.Between(2, 10)), "checks all items are present, in any order") fmt.Println(ok) // When expected is already a non-[]any slice, it cannot be // flattened directly using expected... without copying it to a new // []any slice, then use td.Flatten! expected := []int{1, 2, 3, 5, 8} ok = td.Cmp(t, got, td.Set(td.Flatten(expected)), "checks all expected items are present, in any order") fmt.Println(ok) // Output: // true // true // true // true } func ExampleShallow() { t := &testing.T{} type MyStruct struct { Value int } data := MyStruct{Value: 12} got := &data ok := td.Cmp(t, got, td.Shallow(&data), "checks pointers only, not contents") fmt.Println(ok) // Same contents, but not same pointer ok = td.Cmp(t, got, td.Shallow(&MyStruct{Value: 12}), "checks pointers only, not contents") fmt.Println(ok) // Output: // true // false } func ExampleShallow_slice() { t := &testing.T{} back := []int{1, 2, 3, 1, 2, 3} a := back[:3] b := back[3:] ok := td.Cmp(t, a, td.Shallow(back)) fmt.Println("are ≠ but share the same area:", ok) ok = td.Cmp(t, b, td.Shallow(back)) fmt.Println("are = but do not point to same area:", ok) // Output: // are ≠ but share the same area: true // are = but do not point to same area: false } func ExampleShallow_string() { t := &testing.T{} back := "foobarfoobar" a := back[:6] b := back[6:] ok := td.Cmp(t, a, td.Shallow(back)) fmt.Println("are ≠ but share the same area:", ok) ok = td.Cmp(t, b, td.Shallow(a)) fmt.Println("are = but do not point to same area:", ok) // Output: // are ≠ but share the same area: true // are = but do not point to same area: false } func ExampleSlice_slice() { t := &testing.T{} got := []int{42, 58, 26} ok := td.Cmp(t, got, td.Slice([]int{42}, td.ArrayEntries{1: 58, 2: td.Ignore()}), "checks slice %v", got) fmt.Println(ok) ok = td.Cmp(t, got, td.Slice([]int{}, td.ArrayEntries{0: 42, 1: 58, 2: td.Ignore()}), "checks slice %v", got) fmt.Println(ok) ok = td.Cmp(t, got, td.Slice(([]int)(nil), td.ArrayEntries{0: 42, 1: 58, 2: td.Ignore()}), "checks slice %v", got) fmt.Println(ok) // Output: // true // true // true } func ExampleSlice_typedSlice() { t := &testing.T{} type MySlice []int got := MySlice{42, 58, 26} ok := td.Cmp(t, got, td.Slice(MySlice{42}, td.ArrayEntries{1: 58, 2: td.Ignore()}), "checks typed slice %v", got) fmt.Println(ok) ok = td.Cmp(t, &got, td.Slice(&MySlice{42}, td.ArrayEntries{1: 58, 2: td.Ignore()}), "checks pointer on typed slice %v", got) fmt.Println(ok) ok = td.Cmp(t, &got, td.Slice(&MySlice{}, td.ArrayEntries{0: 42, 1: 58, 2: td.Ignore()}), "checks pointer on typed slice %v", got) fmt.Println(ok) ok = td.Cmp(t, &got, td.Slice((*MySlice)(nil), td.ArrayEntries{0: 42, 1: 58, 2: td.Ignore()}), "checks pointer on typed slice %v", got) fmt.Println(ok) // Output: // true // true // true // true } func ExampleSuperSliceOf_array() { t := &testing.T{} got := [4]int{42, 58, 26, 666} ok := td.Cmp(t, got, td.SuperSliceOf([4]int{1: 58}, td.ArrayEntries{3: td.Gt(660)}), "checks array %v", got) fmt.Println("Only check items #1 & #3:", ok) ok = td.Cmp(t, got, td.SuperSliceOf([4]int{}, td.ArrayEntries{0: 42, 3: td.Between(660, 670)}), "checks array %v", got) fmt.Println("Only check items #0 & #3:", ok) ok = td.Cmp(t, &got, td.SuperSliceOf(&[4]int{}, td.ArrayEntries{0: 42, 3: td.Between(660, 670)}), "checks array %v", got) fmt.Println("Only check items #0 & #3 of an array pointer:", ok) ok = td.Cmp(t, &got, td.SuperSliceOf((*[4]int)(nil), td.ArrayEntries{0: 42, 3: td.Between(660, 670)}), "checks array %v", got) fmt.Println("Only check items #0 & #3 of an array pointer, using nil model:", ok) // Output: // Only check items #1 & #3: true // Only check items #0 & #3: true // Only check items #0 & #3 of an array pointer: true // Only check items #0 & #3 of an array pointer, using nil model: true } func ExampleSuperSliceOf_typedArray() { t := &testing.T{} type MyArray [4]int got := MyArray{42, 58, 26, 666} ok := td.Cmp(t, got, td.SuperSliceOf(MyArray{1: 58}, td.ArrayEntries{3: td.Gt(660)}), "checks typed array %v", got) fmt.Println("Only check items #1 & #3:", ok) ok = td.Cmp(t, got, td.SuperSliceOf(MyArray{}, td.ArrayEntries{0: 42, 3: td.Between(660, 670)}), "checks array %v", got) fmt.Println("Only check items #0 & #3:", ok) ok = td.Cmp(t, &got, td.SuperSliceOf(&MyArray{}, td.ArrayEntries{0: 42, 3: td.Between(660, 670)}), "checks array %v", got) fmt.Println("Only check items #0 & #3 of an array pointer:", ok) ok = td.Cmp(t, &got, td.SuperSliceOf((*MyArray)(nil), td.ArrayEntries{0: 42, 3: td.Between(660, 670)}), "checks array %v", got) fmt.Println("Only check items #0 & #3 of an array pointer, using nil model:", ok) // Output: // Only check items #1 & #3: true // Only check items #0 & #3: true // Only check items #0 & #3 of an array pointer: true // Only check items #0 & #3 of an array pointer, using nil model: true } func ExampleSuperSliceOf_slice() { t := &testing.T{} got := []int{42, 58, 26, 666} ok := td.Cmp(t, got, td.SuperSliceOf([]int{1: 58}, td.ArrayEntries{3: td.Gt(660)}), "checks array %v", got) fmt.Println("Only check items #1 & #3:", ok) ok = td.Cmp(t, got, td.SuperSliceOf([]int{}, td.ArrayEntries{0: 42, 3: td.Between(660, 670)}), "checks array %v", got) fmt.Println("Only check items #0 & #3:", ok) ok = td.Cmp(t, &got, td.SuperSliceOf(&[]int{}, td.ArrayEntries{0: 42, 3: td.Between(660, 670)}), "checks array %v", got) fmt.Println("Only check items #0 & #3 of a slice pointer:", ok) ok = td.Cmp(t, &got, td.SuperSliceOf((*[]int)(nil), td.ArrayEntries{0: 42, 3: td.Between(660, 670)}), "checks array %v", got) fmt.Println("Only check items #0 & #3 of a slice pointer, using nil model:", ok) // Output: // Only check items #1 & #3: true // Only check items #0 & #3: true // Only check items #0 & #3 of a slice pointer: true // Only check items #0 & #3 of a slice pointer, using nil model: true } func ExampleSuperSliceOf_typedSlice() { t := &testing.T{} type MySlice []int got := MySlice{42, 58, 26, 666} ok := td.Cmp(t, got, td.SuperSliceOf(MySlice{1: 58}, td.ArrayEntries{3: td.Gt(660)}), "checks typed array %v", got) fmt.Println("Only check items #1 & #3:", ok) ok = td.Cmp(t, got, td.SuperSliceOf(MySlice{}, td.ArrayEntries{0: 42, 3: td.Between(660, 670)}), "checks array %v", got) fmt.Println("Only check items #0 & #3:", ok) ok = td.Cmp(t, &got, td.SuperSliceOf(&MySlice{}, td.ArrayEntries{0: 42, 3: td.Between(660, 670)}), "checks array %v", got) fmt.Println("Only check items #0 & #3 of a slice pointer:", ok) ok = td.Cmp(t, &got, td.SuperSliceOf((*MySlice)(nil), td.ArrayEntries{0: 42, 3: td.Between(660, 670)}), "checks array %v", got) fmt.Println("Only check items #0 & #3 of a slice pointer, using nil model:", ok) // Output: // Only check items #1 & #3: true // Only check items #0 & #3: true // Only check items #0 & #3 of a slice pointer: true // Only check items #0 & #3 of a slice pointer, using nil model: true } func ExampleSmuggle_convert() { t := &testing.T{} got := int64(123) ok := td.Cmp(t, got, td.Smuggle(func(n int64) int { return int(n) }, 123), "checks int64 got against an int value") fmt.Println(ok) ok = td.Cmp(t, "123", td.Smuggle( func(numStr string) (int, bool) { n, err := strconv.Atoi(numStr) return n, err == nil }, td.Between(120, 130)), "checks that number in %#v is in [120 .. 130]") fmt.Println(ok) ok = td.Cmp(t, "123", td.Smuggle( func(numStr string) (int, bool, string) { n, err := strconv.Atoi(numStr) if err != nil { return 0, false, "string must contain a number" } return n, true, "" }, td.Between(120, 130)), "checks that number in %#v is in [120 .. 130]") fmt.Println(ok) ok = td.Cmp(t, "123", td.Smuggle( func(numStr string) (int, error) { //nolint: gocritic return strconv.Atoi(numStr) }, td.Between(120, 130)), "checks that number in %#v is in [120 .. 130]") fmt.Println(ok) // Short version :) ok = td.Cmp(t, "123", td.Smuggle(strconv.Atoi, td.Between(120, 130)), "checks that number in %#v is in [120 .. 130]") fmt.Println(ok) // Output: // true // true // true // true // true } func ExampleSmuggle_lax() { t := &testing.T{} // got is an int16 and Smuggle func input is an int64: it is OK got := int(123) ok := td.Cmp(t, got, td.Smuggle(func(n int64) uint32 { return uint32(n) }, uint32(123))) fmt.Println("got int16(123) → smuggle via int64 → uint32(123):", ok) // Output: // got int16(123) → smuggle via int64 → uint32(123): true } func ExampleSmuggle_auto_unmarshal() { t := &testing.T{} // Automatically json.Unmarshal to compare got := []byte(`{"a":1,"b":2}`) ok := td.Cmp(t, got, td.Smuggle( func(b json.RawMessage) (r map[string]int, err error) { err = json.Unmarshal(b, &r) return }, map[string]int{ "a": 1, "b": 2, })) fmt.Println("JSON contents is OK:", ok) // Output: // JSON contents is OK: true } func ExampleSmuggle_cast() { t := &testing.T{} // A string containing JSON got := `{ "foo": 123 }` // Automatically cast a string to a json.RawMessage so td.JSON can operate ok := td.Cmp(t, got, td.Smuggle(json.RawMessage{}, td.JSON(`{"foo":123}`))) fmt.Println("JSON contents in string is OK:", ok) // Automatically read from io.Reader to a json.RawMessage ok = td.Cmp(t, bytes.NewReader([]byte(got)), td.Smuggle(json.RawMessage{}, td.JSON(`{"foo":123}`))) fmt.Println("JSON contents just read is OK:", ok) // Output: // JSON contents in string is OK: true // JSON contents just read is OK: true } func ExampleSmuggle_complex() { t := &testing.T{} // No end date but a start date and a duration type StartDuration struct { StartDate time.Time Duration time.Duration } // Checks that end date is between 17th and 19th February both at 0h // for each of these durations in hours for _, duration := range []time.Duration{48 * time.Hour, 72 * time.Hour, 96 * time.Hour} { got := StartDuration{ StartDate: time.Date(2018, time.February, 14, 12, 13, 14, 0, time.UTC), Duration: duration, } // Simplest way, but in case of Between() failure, error will be bound // to DATA, not very clear... ok := td.Cmp(t, got, td.Smuggle( func(sd StartDuration) time.Time { return sd.StartDate.Add(sd.Duration) }, td.Between( time.Date(2018, time.February, 17, 0, 0, 0, 0, time.UTC), time.Date(2018, time.February, 19, 0, 0, 0, 0, time.UTC)))) fmt.Println(ok) // Name the computed value "ComputedEndDate" to render a Between() failure // more understandable, so error will be bound to DATA.ComputedEndDate ok = td.Cmp(t, got, td.Smuggle( func(sd StartDuration) td.SmuggledGot { return td.SmuggledGot{ Name: "ComputedEndDate", Got: sd.StartDate.Add(sd.Duration), } }, td.Between( time.Date(2018, time.February, 17, 0, 0, 0, 0, time.UTC), time.Date(2018, time.February, 19, 0, 0, 0, 0, time.UTC)))) fmt.Println(ok) } // Output: // false // false // true // true // true // true } func ExampleSmuggle_interface() { t := &testing.T{} gotTime, err := time.Parse(time.RFC3339, "2018-05-23T12:13:14Z") if err != nil { t.Fatal(err) } // Do not check the struct itself, but its stringified form ok := td.Cmp(t, gotTime, td.Smuggle(func(s fmt.Stringer) string { return s.String() }, "2018-05-23 12:13:14 +0000 UTC")) fmt.Println("stringified time.Time OK:", ok) // If got does not implement the fmt.Stringer interface, it fails // without calling the Smuggle func type MyTime time.Time ok = td.Cmp(t, MyTime(gotTime), td.Smuggle(func(s fmt.Stringer) string { fmt.Println("Smuggle func called!") return s.String() }, "2018-05-23 12:13:14 +0000 UTC")) fmt.Println("stringified MyTime OK:", ok) // Output: // stringified time.Time OK: true // stringified MyTime OK: false } func ExampleSmuggle_field_path() { t := &testing.T{} type Body struct { Name string Value any } type Request struct { Body *Body } type Transaction struct { Request } type ValueNum struct { Num int } got := &Transaction{ Request: Request{ Body: &Body{ Name: "test", Value: &ValueNum{Num: 123}, }, }, } // Want to check whether Num is between 100 and 200? ok := td.Cmp(t, got, td.Smuggle( func(t *Transaction) (int, error) { if t.Request.Body == nil || t.Request.Body.Value == nil { return 0, errors.New("Request.Body or Request.Body.Value is nil") } if v, ok := t.Request.Body.Value.(*ValueNum); ok && v != nil { return v.Num, nil } return 0, errors.New("Request.Body.Value isn't *ValueNum or nil") }, td.Between(100, 200))) fmt.Println("check Num by hand:", ok) // Same, but automagically generated... ok = td.Cmp(t, got, td.Smuggle("Request.Body.Value.Num", td.Between(100, 200))) fmt.Println("check Num using a fields-path:", ok) // And as Request is an anonymous field, can be simplified further // as it can be omitted ok = td.Cmp(t, got, td.Smuggle("Body.Value.Num", td.Between(100, 200))) fmt.Println("check Num using an other fields-path:", ok) // Note that maps and array/slices are supported got.Request.Body.Value = map[string]any{ "foo": []any{ 3: map[int]string{666: "bar"}, }, } ok = td.Cmp(t, got, td.Smuggle("Body.Value[foo][3][666]", "bar")) fmt.Println("check fields-path including maps/slices:", ok) // Output: // check Num by hand: true // check Num using a fields-path: true // check Num using an other fields-path: true // check fields-path including maps/slices: true } func ExampleString() { t := &testing.T{} got := "foobar" ok := td.Cmp(t, got, td.String("foobar"), "checks %s", got) fmt.Println("using string:", ok) ok = td.Cmp(t, []byte(got), td.String("foobar"), "checks %s", got) fmt.Println("using []byte:", ok) // Output: // using string: true // using []byte: true } func ExampleString_stringer() { t := &testing.T{} // bytes.Buffer implements fmt.Stringer got := bytes.NewBufferString("foobar") ok := td.Cmp(t, got, td.String("foobar"), "checks %s", got) fmt.Println(ok) // Output: // true } func ExampleString_error() { t := &testing.T{} got := errors.New("foobar") ok := td.Cmp(t, got, td.String("foobar"), "checks %s", got) fmt.Println(ok) // Output: // true } func ExampleHasPrefix() { t := &testing.T{} got := "foobar" ok := td.Cmp(t, got, td.HasPrefix("foo"), "checks %s", got) fmt.Println("using string:", ok) ok = td.Cmp(t, []byte(got), td.HasPrefix("foo"), "checks %s", got) fmt.Println("using []byte:", ok) // Output: // using string: true // using []byte: true } func ExampleHasPrefix_stringer() { t := &testing.T{} // bytes.Buffer implements fmt.Stringer got := bytes.NewBufferString("foobar") ok := td.Cmp(t, got, td.HasPrefix("foo"), "checks %s", got) fmt.Println(ok) // Output: // true } func ExampleHasPrefix_error() { t := &testing.T{} got := errors.New("foobar") ok := td.Cmp(t, got, td.HasPrefix("foo"), "checks %s", got) fmt.Println(ok) // Output: // true } func ExampleHasSuffix() { t := &testing.T{} got := "foobar" ok := td.Cmp(t, got, td.HasSuffix("bar"), "checks %s", got) fmt.Println("using string:", ok) ok = td.Cmp(t, []byte(got), td.HasSuffix("bar"), "checks %s", got) fmt.Println("using []byte:", ok) // Output: // using string: true // using []byte: true } func ExampleHasSuffix_stringer() { t := &testing.T{} // bytes.Buffer implements fmt.Stringer got := bytes.NewBufferString("foobar") ok := td.Cmp(t, got, td.HasSuffix("bar"), "checks %s", got) fmt.Println(ok) // Output: // true } func ExampleHasSuffix_error() { t := &testing.T{} got := errors.New("foobar") ok := td.Cmp(t, got, td.HasSuffix("bar"), "checks %s", got) fmt.Println(ok) // Output: // true } func ExampleStruct() { t := &testing.T{} type Person struct { Name string Age int NumChildren int } got := Person{ Name: "Foobar", Age: 42, NumChildren: 3, } // As NumChildren is zero in Struct() call, it is not checked ok := td.Cmp(t, got, td.Struct(Person{Name: "Foobar"}, td.StructFields{ "Age": td.Between(40, 50), }), "checks %v is the right Person") fmt.Println("Foobar is between 40 & 50:", ok) // Model can be empty ok = td.Cmp(t, got, td.Struct(Person{}, td.StructFields{ "Name": "Foobar", "Age": td.Between(40, 50), "NumChildren": td.Not(0), }), "checks %v is the right Person") fmt.Println("Foobar has some children:", ok) // Works with pointers too ok = td.Cmp(t, &got, td.Struct(&Person{}, td.StructFields{ "Name": "Foobar", "Age": td.Between(40, 50), "NumChildren": td.Not(0), }), "checks %v is the right Person") fmt.Println("Foobar has some children (using pointer):", ok) // Model does not need to be instanciated ok = td.Cmp(t, &got, td.Struct((*Person)(nil), td.StructFields{ "Name": "Foobar", "Age": td.Between(40, 50), "NumChildren": td.Not(0), }), "checks %v is the right Person") fmt.Println("Foobar has some children (using nil model):", ok) // Output: // Foobar is between 40 & 50: true // Foobar has some children: true // Foobar has some children (using pointer): true // Foobar has some children (using nil model): true } func ExampleStruct_overwrite_model() { t := &testing.T{} type Person struct { Name string Age int NumChildren int } got := Person{ Name: "Foobar", Age: 42, NumChildren: 3, } ok := td.Cmp(t, got, td.Struct( Person{ Name: "Foobar", Age: 53, }, td.StructFields{ ">Age": td.Between(40, 50), // ">" to overwrite Age:53 in model "NumChildren": td.Gt(2), }), "checks %v is the right Person") fmt.Println("Foobar is between 40 & 50:", ok) ok = td.Cmp(t, got, td.Struct( Person{ Name: "Foobar", Age: 53, }, td.StructFields{ "> Age": td.Between(40, 50), // same, ">" can be followed by spaces "NumChildren": td.Gt(2), }), "checks %v is the right Person") fmt.Println("Foobar is between 40 & 50:", ok) // Output: // Foobar is between 40 & 50: true // Foobar is between 40 & 50: true } func ExampleStruct_patterns() { t := &testing.T{} type Person struct { Firstname string Lastname string Surname string Nickname string CreatedAt time.Time UpdatedAt time.Time DeletedAt *time.Time } now := time.Now() got := Person{ Firstname: "Maxime", Lastname: "Foo", Surname: "Max", Nickname: "max", CreatedAt: now, UpdatedAt: now, DeletedAt: nil, // not deleted yet } ok := td.Cmp(t, got, td.Struct(Person{Lastname: "Foo"}, td.StructFields{ `DeletedAt`: nil, `= *name`: td.Re(`^(?i)max`), // shell pattern, matches all names except Lastname as in model `=~ At\z`: td.Lte(time.Now()), // regexp, matches CreatedAt & UpdatedAt }), "mix shell & regexp patterns") fmt.Println("Patterns match only remaining fields:", ok) ok = td.Cmp(t, got, td.Struct(Person{Lastname: "Foo"}, td.StructFields{ `DeletedAt`: nil, `1 = *name`: td.Re(`^(?i)max`), // shell pattern, matches all names except Lastname as in model `2 =~ At\z`: td.Lte(time.Now()), // regexp, matches CreatedAt & UpdatedAt }), "ordered patterns") fmt.Println("Ordered patterns match only remaining fields:", ok) // Output: // Patterns match only remaining fields: true // Ordered patterns match only remaining fields: true } func ExampleStruct_struct_fields() { // only operator t := &testing.T{} type Person struct { Name string Age int NumChildren int } got := Person{ Name: "Foobar", Age: 42, NumChildren: 3, } ok := td.Cmp(t, got, td.Struct(Person{Name: "Foobar"}), "no StructFields") fmt.Println("Without any StructFields:", ok) ok = td.Cmp(t, got, td.Struct(Person{Name: "Bingo"}, td.StructFields{ "> Name": "pipo", "Age": 42, }, td.StructFields{ "> Name": "bingo", "NumChildren": 10, }, td.StructFields{ ">Name": "Foobar", "NumChildren": 3, }), "merge several StructFields") fmt.Println("Merge several StructFields:", ok) // Output: // Without any StructFields: true // Merge several StructFields: true } func ExampleStruct_lazy_model() { t := &testing.T{} got := struct { name string age int }{ name: "Foobar", age: 42, } ok := td.Cmp(t, got, td.Struct(nil, td.StructFields{ "name": "Foobar", "age": td.Between(40, 45), })) fmt.Println("Lazy model:", ok) ok = td.Cmp(t, got, td.Struct(nil, td.StructFields{ "name": "Foobar", "zip": 666, })) fmt.Println("Lazy model with unknown field:", ok) // Output: // Lazy model: true // Lazy model with unknown field: false } func ExampleSStruct() { t := &testing.T{} type Person struct { Name string Age int NumChildren int } got := Person{ Name: "Foobar", Age: 42, NumChildren: 0, } // NumChildren is not listed in expected fields so it must be zero ok := td.Cmp(t, got, td.SStruct(Person{Name: "Foobar"}, td.StructFields{ "Age": td.Between(40, 50), }), "checks %v is the right Person") fmt.Println("Foobar is between 40 & 50:", ok) // Model can be empty got.NumChildren = 3 ok = td.Cmp(t, got, td.SStruct(Person{}, td.StructFields{ "Name": "Foobar", "Age": td.Between(40, 50), "NumChildren": td.Not(0), }), "checks %v is the right Person") fmt.Println("Foobar has some children:", ok) // Works with pointers too ok = td.Cmp(t, &got, td.SStruct(&Person{}, td.StructFields{ "Name": "Foobar", "Age": td.Between(40, 50), "NumChildren": td.Not(0), }), "checks %v is the right Person") fmt.Println("Foobar has some children (using pointer):", ok) // Model does not need to be instanciated ok = td.Cmp(t, &got, td.SStruct((*Person)(nil), td.StructFields{ "Name": "Foobar", "Age": td.Between(40, 50), "NumChildren": td.Not(0), }), "checks %v is the right Person") fmt.Println("Foobar has some children (using nil model):", ok) // Output: // Foobar is between 40 & 50: true // Foobar has some children: true // Foobar has some children (using pointer): true // Foobar has some children (using nil model): true } func ExampleSStruct_overwrite_model() { t := &testing.T{} type Person struct { Name string Age int NumChildren int } got := Person{ Name: "Foobar", Age: 42, NumChildren: 3, } ok := td.Cmp(t, got, td.SStruct( Person{ Name: "Foobar", Age: 53, }, td.StructFields{ ">Age": td.Between(40, 50), // ">" to overwrite Age:53 in model "NumChildren": td.Gt(2), }), "checks %v is the right Person") fmt.Println("Foobar is between 40 & 50:", ok) ok = td.Cmp(t, got, td.SStruct( Person{ Name: "Foobar", Age: 53, }, td.StructFields{ "> Age": td.Between(40, 50), // same, ">" can be followed by spaces "NumChildren": td.Gt(2), }), "checks %v is the right Person") fmt.Println("Foobar is between 40 & 50:", ok) // Output: // Foobar is between 40 & 50: true // Foobar is between 40 & 50: true } func ExampleSStruct_patterns() { t := &testing.T{} type Person struct { Firstname string Lastname string Surname string Nickname string CreatedAt time.Time UpdatedAt time.Time DeletedAt *time.Time id int64 secret string } now := time.Now() got := Person{ Firstname: "Maxime", Lastname: "Foo", Surname: "Max", Nickname: "max", CreatedAt: now, UpdatedAt: now, DeletedAt: nil, // not deleted yet id: 2345, secret: "5ecr3T", } ok := td.Cmp(t, got, td.SStruct(Person{Lastname: "Foo"}, td.StructFields{ `DeletedAt`: nil, `= *name`: td.Re(`^(?i)max`), // shell pattern, matches all names except Lastname as in model `=~ At\z`: td.Lte(time.Now()), // regexp, matches CreatedAt & UpdatedAt `! [A-Z]*`: td.Ignore(), // private fields }), "mix shell & regexp patterns") fmt.Println("Patterns match only remaining fields:", ok) ok = td.Cmp(t, got, td.SStruct(Person{Lastname: "Foo"}, td.StructFields{ `DeletedAt`: nil, `1 = *name`: td.Re(`^(?i)max`), // shell pattern, matches all names except Lastname as in model `2 =~ At\z`: td.Lte(time.Now()), // regexp, matches CreatedAt & UpdatedAt `3 !~ ^[A-Z]`: td.Ignore(), // private fields }), "ordered patterns") fmt.Println("Ordered patterns match only remaining fields:", ok) // Output: // Patterns match only remaining fields: true // Ordered patterns match only remaining fields: true } func ExampleSStruct_struct_fields() { // only operator t := &testing.T{} type Person struct { Name string Age int NumChildren int } got := Person{ Name: "Foobar", Age: 42, NumChildren: 3, } // No added value here, but it works ok := td.Cmp(t, got, td.SStruct(Person{ Name: "Foobar", Age: 42, NumChildren: 3, }), "no StructFields") fmt.Println("Without any StructFields:", ok) ok = td.Cmp(t, got, td.SStruct(Person{Name: "Bingo"}, td.StructFields{ "> Name": "pipo", "Age": 42, }, td.StructFields{ "> Name": "bingo", "NumChildren": 10, }, td.StructFields{ ">Name": "Foobar", "NumChildren": 3, }), "merge several StructFields") fmt.Println("Merge several StructFields:", ok) // Output: // Without any StructFields: true // Merge several StructFields: true } func ExampleSStruct_lazy_model() { t := &testing.T{} got := struct { name string age int }{ name: "Foobar", age: 42, } ok := td.Cmp(t, got, td.SStruct(nil, td.StructFields{ "name": "Foobar", "age": td.Between(40, 45), })) fmt.Println("Lazy model:", ok) ok = td.Cmp(t, got, td.SStruct(nil, td.StructFields{ "name": "Foobar", "zip": 666, })) fmt.Println("Lazy model with unknown field:", ok) // Output: // Lazy model: true // Lazy model with unknown field: false } func ExampleSubBagOf() { t := &testing.T{} got := []int{1, 3, 5, 8, 8, 1, 2} ok := td.Cmp(t, got, td.SubBagOf(0, 0, 1, 1, 2, 2, 3, 3, 5, 5, 8, 8, 9, 9), "checks at least all items are present, in any order") fmt.Println(ok) // got contains one 8 too many ok = td.Cmp(t, got, td.SubBagOf(0, 0, 1, 1, 2, 2, 3, 3, 5, 5, 8, 9, 9), "checks at least all items are present, in any order") fmt.Println(ok) got = []int{1, 3, 5, 2} ok = td.Cmp(t, got, td.SubBagOf( td.Between(0, 3), td.Between(0, 3), td.Between(0, 3), td.Between(0, 3), td.Gt(4), td.Gt(4)), "checks at least all items match, in any order with TestDeep operators") fmt.Println(ok) // When expected is already a non-[]any slice, it cannot be // flattened directly using expected... without copying it to a new // []any slice, then use td.Flatten! expected := []int{1, 2, 3, 5, 9, 8} ok = td.Cmp(t, got, td.SubBagOf(td.Flatten(expected)), "checks at least all expected items are present, in any order") fmt.Println(ok) // Output: // true // false // true // true } func ExampleSubJSONOf_basic() { t := &testing.T{} got := &struct { Fullname string `json:"fullname"` Age int `json:"age"` }{ Fullname: "Bob", Age: 42, } ok := td.Cmp(t, got, td.SubJSONOf(`{"age":42,"fullname":"Bob","gender":"male"}`)) fmt.Println("check got with age then fullname:", ok) ok = td.Cmp(t, got, td.SubJSONOf(`{"fullname":"Bob","age":42,"gender":"male"}`)) fmt.Println("check got with fullname then age:", ok) ok = td.Cmp(t, got, td.SubJSONOf(` // This should be the JSON representation of a struct { // A person: "fullname": "Bob", // The name of this person "age": 42, /* The age of this person: - 42 of course - to demonstrate a multi-lines comment */ "gender": "male" // This field is ignored as SubJSONOf }`)) fmt.Println("check got with nicely formatted and commented JSON:", ok) ok = td.Cmp(t, got, td.SubJSONOf(`{"fullname":"Bob","gender":"male"}`)) fmt.Println("check got without age field:", ok) // Output: // check got with age then fullname: true // check got with fullname then age: true // check got with nicely formatted and commented JSON: true // check got without age field: false } func ExampleSubJSONOf_placeholders() { t := &testing.T{} got := &struct { Fullname string `json:"fullname"` Age int `json:"age"` }{ Fullname: "Bob Foobar", Age: 42, } ok := td.Cmp(t, got, td.SubJSONOf(`{"age": $1, "fullname": $2, "gender": $3}`, 42, "Bob Foobar", "male")) fmt.Println("check got with numeric placeholders without operators:", ok) ok = td.Cmp(t, got, td.SubJSONOf(`{"age": $1, "fullname": $2, "gender": $3}`, td.Between(40, 45), td.HasSuffix("Foobar"), td.NotEmpty())) fmt.Println("check got with numeric placeholders:", ok) ok = td.Cmp(t, got, td.SubJSONOf(`{"age": "$1", "fullname": "$2", "gender": "$3"}`, td.Between(40, 45), td.HasSuffix("Foobar"), td.NotEmpty())) fmt.Println("check got with double-quoted numeric placeholders:", ok) ok = td.Cmp(t, got, td.SubJSONOf(`{"age": $age, "fullname": $name, "gender": $gender}`, td.Tag("age", td.Between(40, 45)), td.Tag("name", td.HasSuffix("Foobar")), td.Tag("gender", td.NotEmpty()))) fmt.Println("check got with named placeholders:", ok) ok = td.Cmp(t, got, td.SubJSONOf(`{"age": $^NotZero, "fullname": $^NotEmpty, "gender": $^NotEmpty}`)) fmt.Println("check got with operator shortcuts:", ok) // Output: // check got with numeric placeholders without operators: true // check got with numeric placeholders: true // check got with double-quoted numeric placeholders: true // check got with named placeholders: true // check got with operator shortcuts: true } func ExampleSubJSONOf_file() { t := &testing.T{} got := &struct { Fullname string `json:"fullname"` Age int `json:"age"` Gender string `json:"gender"` }{ Fullname: "Bob Foobar", Age: 42, Gender: "male", } tmpDir, err := os.MkdirTemp("", "") if err != nil { t.Fatal(err) } defer os.RemoveAll(tmpDir) // clean up filename := tmpDir + "/test.json" if err = os.WriteFile(filename, []byte(` { "fullname": "$name", "age": "$age", "gender": "$gender", "details": { "city": "TestCity", "zip": 666 } }`), 0644); err != nil { t.Fatal(err) } // OK let's test with this file ok := td.Cmp(t, got, td.SubJSONOf(filename, td.Tag("name", td.HasPrefix("Bob")), td.Tag("age", td.Between(40, 45)), td.Tag("gender", td.Re(`^(male|female)\z`)))) fmt.Println("Full match from file name:", ok) // When the file is already open file, err := os.Open(filename) if err != nil { t.Fatal(err) } ok = td.Cmp(t, got, td.SubJSONOf(file, td.Tag("name", td.HasPrefix("Bob")), td.Tag("age", td.Between(40, 45)), td.Tag("gender", td.Re(`^(male|female)\z`)))) fmt.Println("Full match from io.Reader:", ok) // Output: // Full match from file name: true // Full match from io.Reader: true } func ExampleSubMapOf_map() { t := &testing.T{} got := map[string]int{"foo": 12, "bar": 42} ok := td.Cmp(t, got, td.SubMapOf(map[string]int{"bar": 42}, td.MapEntries{"foo": td.Lt(15), "zip": 666}), "checks map %v is included in expected keys/values", got) fmt.Println(ok) // Output: // true } func ExampleSubMapOf_typedMap() { t := &testing.T{} type MyMap map[string]int got := MyMap{"foo": 12, "bar": 42} ok := td.Cmp(t, got, td.SubMapOf(MyMap{"bar": 42}, td.MapEntries{"foo": td.Lt(15), "zip": 666}), "checks typed map %v is included in expected keys/values", got) fmt.Println(ok) ok = td.Cmp(t, &got, td.SubMapOf(&MyMap{"bar": 42}, td.MapEntries{"foo": td.Lt(15), "zip": 666}), "checks pointed typed map %v is included in expected keys/values", got) fmt.Println(ok) // Output: // true // true } func ExampleSubSetOf() { t := &testing.T{} got := []int{1, 3, 5, 8, 8, 1, 2} // Matches as all items are expected, ignoring duplicates ok := td.Cmp(t, got, td.SubSetOf(1, 2, 3, 4, 5, 6, 7, 8), "checks at least all items are present, in any order, ignoring duplicates") fmt.Println(ok) // Tries its best to not raise an error when a value can be matched // by several SubSetOf entries ok = td.Cmp(t, got, td.SubSetOf(td.Between(1, 4), 3, td.Between(2, 10), td.Gt(100)), "checks at least all items are present, in any order, ignoring duplicates") fmt.Println(ok) // When expected is already a non-[]any slice, it cannot be // flattened directly using expected... without copying it to a new // []any slice, then use td.Flatten! expected := []int{1, 2, 3, 4, 5, 6, 7, 8} ok = td.Cmp(t, got, td.SubSetOf(td.Flatten(expected)), "checks at least all expected items are present, in any order, ignoring duplicates") fmt.Println(ok) // Output: // true // true // true } func ExampleSuperBagOf() { t := &testing.T{} got := []int{1, 3, 5, 8, 8, 1, 2} ok := td.Cmp(t, got, td.SuperBagOf(8, 5, 8), "checks the items are present, in any order") fmt.Println(ok) ok = td.Cmp(t, got, td.SuperBagOf(td.Gt(5), td.Lte(2)), "checks at least 2 items of %v match", got) fmt.Println(ok) // When expected is already a non-[]any slice, it cannot be // flattened directly using expected... without copying it to a new // []any slice, then use td.Flatten! expected := []int{8, 5, 8} ok = td.Cmp(t, got, td.SuperBagOf(td.Flatten(expected)), "checks the expected items are present, in any order") fmt.Println(ok) // Output: // true // true // true } func ExampleSuperJSONOf_basic() { t := &testing.T{} got := &struct { Fullname string `json:"fullname"` Age int `json:"age"` Gender string `json:"gender"` City string `json:"city"` Zip int `json:"zip"` }{ Fullname: "Bob", Age: 42, Gender: "male", City: "TestCity", Zip: 666, } ok := td.Cmp(t, got, td.SuperJSONOf(`{"age":42,"fullname":"Bob","gender":"male"}`)) fmt.Println("check got with age then fullname:", ok) ok = td.Cmp(t, got, td.SuperJSONOf(`{"fullname":"Bob","age":42,"gender":"male"}`)) fmt.Println("check got with fullname then age:", ok) ok = td.Cmp(t, got, td.SuperJSONOf(` // This should be the JSON representation of a struct { // A person: "fullname": "Bob", // The name of this person "age": 42, /* The age of this person: - 42 of course - to demonstrate a multi-lines comment */ "gender": "male" // The gender! }`)) fmt.Println("check got with nicely formatted and commented JSON:", ok) ok = td.Cmp(t, got, td.SuperJSONOf(`{"fullname":"Bob","gender":"male","details":{}}`)) fmt.Println("check got with details field:", ok) // Output: // check got with age then fullname: true // check got with fullname then age: true // check got with nicely formatted and commented JSON: true // check got with details field: false } func ExampleSuperJSONOf_placeholders() { t := &testing.T{} got := &struct { Fullname string `json:"fullname"` Age int `json:"age"` Gender string `json:"gender"` City string `json:"city"` Zip int `json:"zip"` }{ Fullname: "Bob Foobar", Age: 42, Gender: "male", City: "TestCity", Zip: 666, } ok := td.Cmp(t, got, td.SuperJSONOf(`{"age": $1, "fullname": $2, "gender": $3}`, 42, "Bob Foobar", "male")) fmt.Println("check got with numeric placeholders without operators:", ok) ok = td.Cmp(t, got, td.SuperJSONOf(`{"age": $1, "fullname": $2, "gender": $3}`, td.Between(40, 45), td.HasSuffix("Foobar"), td.NotEmpty())) fmt.Println("check got with numeric placeholders:", ok) ok = td.Cmp(t, got, td.SuperJSONOf(`{"age": "$1", "fullname": "$2", "gender": "$3"}`, td.Between(40, 45), td.HasSuffix("Foobar"), td.NotEmpty())) fmt.Println("check got with double-quoted numeric placeholders:", ok) ok = td.Cmp(t, got, td.SuperJSONOf(`{"age": $age, "fullname": $name, "gender": $gender}`, td.Tag("age", td.Between(40, 45)), td.Tag("name", td.HasSuffix("Foobar")), td.Tag("gender", td.NotEmpty()))) fmt.Println("check got with named placeholders:", ok) ok = td.Cmp(t, got, td.SuperJSONOf(`{"age": $^NotZero, "fullname": $^NotEmpty, "gender": $^NotEmpty}`)) fmt.Println("check got with operator shortcuts:", ok) // Output: // check got with numeric placeholders without operators: true // check got with numeric placeholders: true // check got with double-quoted numeric placeholders: true // check got with named placeholders: true // check got with operator shortcuts: true } func ExampleSuperJSONOf_file() { t := &testing.T{} got := &struct { Fullname string `json:"fullname"` Age int `json:"age"` Gender string `json:"gender"` City string `json:"city"` Zip int `json:"zip"` }{ Fullname: "Bob Foobar", Age: 42, Gender: "male", City: "TestCity", Zip: 666, } tmpDir, err := os.MkdirTemp("", "") if err != nil { t.Fatal(err) } defer os.RemoveAll(tmpDir) // clean up filename := tmpDir + "/test.json" if err = os.WriteFile(filename, []byte(` { "fullname": "$name", "age": "$age", "gender": "$gender" }`), 0644); err != nil { t.Fatal(err) } // OK let's test with this file ok := td.Cmp(t, got, td.SuperJSONOf(filename, td.Tag("name", td.HasPrefix("Bob")), td.Tag("age", td.Between(40, 45)), td.Tag("gender", td.Re(`^(male|female)\z`)))) fmt.Println("Full match from file name:", ok) // When the file is already open file, err := os.Open(filename) if err != nil { t.Fatal(err) } ok = td.Cmp(t, got, td.SuperJSONOf(file, td.Tag("name", td.HasPrefix("Bob")), td.Tag("age", td.Between(40, 45)), td.Tag("gender", td.Re(`^(male|female)\z`)))) fmt.Println("Full match from io.Reader:", ok) // Output: // Full match from file name: true // Full match from io.Reader: true } func ExampleSuperMapOf_map() { t := &testing.T{} got := map[string]int{"foo": 12, "bar": 42, "zip": 89} ok := td.Cmp(t, got, td.SuperMapOf(map[string]int{"bar": 42}, td.MapEntries{"foo": td.Lt(15)}), "checks map %v contains at least all expected keys/values", got) fmt.Println(ok) // Output: // true } func ExampleSuperMapOf_typedMap() { t := &testing.T{} type MyMap map[string]int got := MyMap{"foo": 12, "bar": 42, "zip": 89} ok := td.Cmp(t, got, td.SuperMapOf(MyMap{"bar": 42}, td.MapEntries{"foo": td.Lt(15)}), "checks typed map %v contains at least all expected keys/values", got) fmt.Println(ok) ok = td.Cmp(t, &got, td.SuperMapOf(&MyMap{"bar": 42}, td.MapEntries{"foo": td.Lt(15)}), "checks pointed typed map %v contains at least all expected keys/values", got) fmt.Println(ok) // Output: // true // true } func ExampleSuperSetOf() { t := &testing.T{} got := []int{1, 3, 5, 8, 8, 1, 2} ok := td.Cmp(t, got, td.SuperSetOf(1, 2, 3), "checks the items are present, in any order and ignoring duplicates") fmt.Println(ok) ok = td.Cmp(t, got, td.SuperSetOf(td.Gt(5), td.Lte(2)), "checks at least 2 items of %v match ignoring duplicates", got) fmt.Println(ok) // When expected is already a non-[]any slice, it cannot be // flattened directly using expected... without copying it to a new // []any slice, then use td.Flatten! expected := []int{1, 2, 3} ok = td.Cmp(t, got, td.SuperSetOf(td.Flatten(expected)), "checks the expected items are present, in any order and ignoring duplicates") fmt.Println(ok) // Output: // true // true // true } func ExampleTruncTime() { t := &testing.T{} dateToTime := func(str string) time.Time { t, err := time.Parse(time.RFC3339Nano, str) if err != nil { panic(err) } return t } got := dateToTime("2018-05-01T12:45:53.123456789Z") // Compare dates ignoring nanoseconds and monotonic parts expected := dateToTime("2018-05-01T12:45:53Z") ok := td.Cmp(t, got, td.TruncTime(expected, time.Second), "checks date %v, truncated to the second", got) fmt.Println(ok) // Compare dates ignoring time and so monotonic parts expected = dateToTime("2018-05-01T11:22:33.444444444Z") ok = td.Cmp(t, got, td.TruncTime(expected, 24*time.Hour), "checks date %v, truncated to the day", got) fmt.Println(ok) // Compare dates exactly but ignoring monotonic part expected = dateToTime("2018-05-01T12:45:53.123456789Z") ok = td.Cmp(t, got, td.TruncTime(expected), "checks date %v ignoring monotonic part", got) fmt.Println(ok) // Output: // true // true // true } func ExampleValues() { t := &testing.T{} got := map[string]int{"foo": 1, "bar": 2, "zip": 3} // Values tests values in an ordered manner ok := td.Cmp(t, got, td.Values([]int{1, 2, 3})) fmt.Println("All sorted values are found:", ok) // If the expected values are not ordered, it fails ok = td.Cmp(t, got, td.Values([]int{3, 1, 2})) fmt.Println("All unsorted values are found:", ok) // To circumvent that, one can use Bag operator ok = td.Cmp(t, got, td.Values(td.Bag(3, 1, 2))) fmt.Println("All unsorted values are found, with the help of Bag operator:", ok) // Check that each value is between 1 and 3 ok = td.Cmp(t, got, td.Values(td.ArrayEach(td.Between(1, 3)))) fmt.Println("Each value is between 1 and 3:", ok) // Output: // All sorted values are found: true // All unsorted values are found: false // All unsorted values are found, with the help of Bag operator: true // Each value is between 1 and 3: true } func ExampleZero() { t := &testing.T{} ok := td.Cmp(t, 0, td.Zero()) fmt.Println(ok) ok = td.Cmp(t, float64(0), td.Zero()) fmt.Println(ok) ok = td.Cmp(t, 12, td.Zero()) // fails, as 12 is not 0 :) fmt.Println(ok) ok = td.Cmp(t, (map[string]int)(nil), td.Zero()) fmt.Println(ok) ok = td.Cmp(t, map[string]int{}, td.Zero()) // fails, as not nil fmt.Println(ok) ok = td.Cmp(t, ([]int)(nil), td.Zero()) fmt.Println(ok) ok = td.Cmp(t, []int{}, td.Zero()) // fails, as not nil fmt.Println(ok) ok = td.Cmp(t, [3]int{}, td.Zero()) fmt.Println(ok) ok = td.Cmp(t, [3]int{0, 1}, td.Zero()) // fails, DATA[1] is not 0 fmt.Println(ok) ok = td.Cmp(t, bytes.Buffer{}, td.Zero()) fmt.Println(ok) ok = td.Cmp(t, &bytes.Buffer{}, td.Zero()) // fails, as pointer not nil fmt.Println(ok) ok = td.Cmp(t, &bytes.Buffer{}, td.Ptr(td.Zero())) // OK with the help of Ptr() fmt.Println(ok) // Output: // true // true // false // true // false // true // false // true // false // true // false // true } golang-github-maxatome-go-testdeep-1.14.0/td/flatten.go000066400000000000000000000231071454313311600227520ustar00rootroot00000000000000// Copyright (c) 2020-2023, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td import ( "reflect" "strings" "github.com/maxatome/go-testdeep/internal/color" "github.com/maxatome/go-testdeep/internal/flat" "github.com/maxatome/go-testdeep/internal/types" ) // Flatten allows to flatten any slice, array or map in parameters of // operators expecting ...any. fn parameter allows to filter and/or // transform items before flattening and is described below. // // For example the [Set] operator is defined as: // // func Set(expectedItems ...any) TestDeep // // so when comparing to a []int slice, we usually do: // // got := []int{42, 66, 22} // td.Cmp(t, got, td.Set(22, 42, 66)) // // it works but if the expected items are already in a []int, we have // to copy them in a []any as it can not be flattened directly // in [Set] parameters: // // expected := []int{22, 42, 66} // expectedIf := make([]any, len(expected)) // for i, item := range expected { // expectedIf[i] = item // } // td.Cmp(t, got, td.Set(expectedIf...)) // // but it is a bit boring and less efficient, as [Set] does not keep // the []any behind the scene. // // The same with Flatten follows: // // expected := []int{22, 42, 66} // td.Cmp(t, got, td.Set(td.Flatten(expected))) // // Several Flatten calls can be passed, and even combined with normal // parameters: // // expectedPart1 := []int{11, 22, 33} // expectedPart2 := []int{55, 66, 77} // expectedPart3 := []int{99} // td.Cmp(t, got, // td.Set( // td.Flatten(expectedPart1), // 44, // td.Flatten(expectedPart2), // 88, // td.Flatten(expectedPart3), // )) // // is exactly the same as: // // td.Cmp(t, got, td.Set(11, 22, 33, 44, 55, 66, 77, 88, 99)) // // Note that Flatten calls can even be nested: // // td.Cmp(t, got, // td.Set( // td.Flatten([]any{ // 11, // td.Flatten([]int{22, 33}), // td.Flatten([]int{44, 55, 66}), // }), // 77, // )) // // is exactly the same as: // // td.Cmp(t, got, td.Set(11, 22, 33, 44, 55, 66, 77)) // // Maps can be flattened too, keeping in mind there is no particular order: // // td.Flatten(map[int]int{1: 2, 3: 4}) // // is flattened as 1, 2, 3, 4 or 3, 4, 1, 2. // // Optional fn parameter can be used to filter and/or transform items // before flattening. If passed, it has to be one element length and // this single element can be: // // - untyped nil: it is a no-op, as if it was not passed // - a function // - a string shortcut // // If it is a function, it must be a non-nil function with a signature like: // // func(T) V // func(T) (V, bool) // // T can be the same as V, but it is not mandatory. The (V, bool) // returned case allows to exclude some items when returning false. // // If the function signature does not match these cases, Flatten panics. // // If the type of an item of sliceOrMap is not convertible to T, the // item is dropped silently, as if fn returned false. // // This single element can also be a string among: // // "Smuggle:FIELD" // "JSONPointer:/PATH" // // that are shortcuts for respectively: // // func(in any) any { return td.Smuggle("FIELD", in) } // func(in any) any { return td.JSONPointer("/PATH", in) } // // See [Smuggle] and [JSONPointer] for a description of what "FIELD" // and "/PATH" can really be. // // Flatten with an fn can be useful when testing some fields of // structs in a slice with [Set] or [Bag] operators families. As an // example, here we test only "Name" field for each item of a person // slice: // // type person struct { // Name string `json:"name"` // Age int `json:"age"` // } // got := []person{{"alice", 22}, {"bob", 18}, {"brian", 34}, {"britt", 32}} // // td.Cmp(t, got, // td.Bag(td.Flatten( // []string{"alice", "britt", "brian", "bob"}, // func(name string) any { return td.Smuggle("Name", name) }))) // // distributes td.Smuggle for each Name, so is equivalent of: // td.Cmp(t, got, td.Bag( // td.Smuggle("Name", "alice"), // td.Smuggle("Name", "britt"), // td.Smuggle("Name", "brian"), // td.Smuggle("Name", "bob"), // )) // // // Same here using Smuggle string shortcut // td.Cmp(t, got, // td.Bag(td.Flatten( // []string{"alice", "britt", "brian", "bob"}, "Smuggle:Name"))) // // // Same here, but using JSONPointer operator // td.Cmp(t, got, // td.Bag(td.Flatten( // []string{"alice", "britt", "brian", "bob"}, // func(name string) any { return td.JSONPointer("/name", name) }))) // // // Same here using JSONPointer string shortcut // td.Cmp(t, got, // td.Bag(td.Flatten( // []string{"alice", "britt", "brian", "bob"}, "JSONPointer:/name"))) // // // Same here, but using SuperJSONOf operator // td.Cmp(t, got, // td.Bag(td.Flatten( // []string{"alice", "britt", "brian", "bob"}, // func(name string) any { return td.SuperJSONOf(`{"name":$1}`, name) }))) // // // Same here, but using Struct operator // td.Cmp(t, got, // td.Bag(td.Flatten( // []string{"alice", "britt", "brian", "bob"}, // func(name string) any { return td.Struct(person{Name: name}) }))) // // See also [Grep]. func Flatten(sliceOrMap any, fn ...any) flat.Slice { const ( smugglePrefix = "Smuggle:" jsonPointerPrefix = "JSONPointer:" usage = "Flatten(SLICE|ARRAY|MAP[, FUNC])" usageFunc = usage + `, FUNC should be non-nil func(T) V or func(T) (V, bool) or a string "` + smugglePrefix + `…" or "` + jsonPointerPrefix + `…"` ) switch reflect.ValueOf(sliceOrMap).Kind() { case reflect.Slice, reflect.Array, reflect.Map: default: panic(color.BadUsage(usage, sliceOrMap, 1, true)) } switch len(fn) { case 1: if fn[0] != nil { break } fallthrough case 0: return flat.Slice{Slice: sliceOrMap} default: panic(color.TooManyParams(usage)) } f := fn[0] // Smuggle & JSONPointer specific shortcuts if s, ok := f.(string); ok { switch { case strings.HasPrefix(s, smugglePrefix): f = func(in any) any { return Smuggle(s[len(smugglePrefix):], in) } case strings.HasPrefix(s, jsonPointerPrefix): f = func(in any) any { return JSONPointer(s[len(jsonPointerPrefix):], in) } default: panic(color.Bad("usage: "+usageFunc+", but received %q as 2nd parameter", s)) } } fnType := reflect.TypeOf(f) vfn := reflect.ValueOf(f) if fnType.Kind() != reflect.Func || fnType.NumIn() != 1 || fnType.IsVariadic() || (fnType.NumOut() != 1 && (fnType.NumOut() != 2 || fnType.Out(1) != types.Bool)) { panic(color.BadUsage(usageFunc, f, 2, false)) } if vfn.IsNil() { panic(color.Bad("usage: " + usageFunc)) } inType := fnType.In(0) var final []any for _, v := range flat.Values([]any{flat.Slice{Slice: sliceOrMap}}) { if v.Type() != inType { if !v.Type().ConvertibleTo(inType) { continue } v = v.Convert(inType) } ret := vfn.Call([]reflect.Value{v}) if len(ret) == 1 || ret[1].Bool() { final = append(final, ret[0].Interface()) } } return flat.Slice{Slice: final} } // Flatten allows to flatten any slice, array or map in // parameters of operators expecting ...any after applying a function // on each item to exclude or transform it. // // fn must be a non-nil function with a signature like: // // func(T) V // func(T) (V, bool) // // T can be the same as V but it is not mandatory. The (V, bool) // returned case allows to exclude some items when returning false. // // If fn signature does not match these cases, Flatten panics. // // If the type of an item of sliceOrMap is not convertible to T, the // item is dropped silently, as if fn returned false. // // fn can also be a string among: // // "Smuggle:FIELD" // "JSONPointer:/PATH" // // that are shortcuts for respectively: // // func(in any) any { return td.Smuggle("FIELD", in) } // func(in any) any { return td.JSONPointer("/PATH", in) } // // See [Smuggle] and [JSONPointer] for a description of what "FIELD" // and "/PATH" can really be. // // Flatten can be useful when testing some fields of structs in // a slice with [Set] or [Bag] operators families. As an example, here // we test only "Name" field for each item of a person slice: // // type person struct { // Name string `json:"name"` // Age int `json:"age"` // } // got := []person{{"alice", 22}, {"bob", 18}, {"brian", 34}, {"britt", 32}} // // td.Cmp(t, got, // td.Bag(td.Flatten( // func(name string) any { return td.Smuggle("Name", name) }, // []string{"alice", "britt", "brian", "bob"}))) // // distributes td.Smuggle for each Name, so is equivalent of: // td.Cmp(t, got, td.Bag( // td.Smuggle("Name", "alice"), // td.Smuggle("Name", "britt"), // td.Smuggle("Name", "brian"), // td.Smuggle("Name", "bob"))) // // // Same here using Smuggle string shortcut // td.Cmp(t, got, // td.Bag(td.Flatten( // "Smuggle:Name", []string{"alice", "britt", "brian", "bob"}))) // // // Same here, but using JSONPointer operator // td.Cmp(t, got, // td.Bag(td.Flatten( // func(name string) any { return td.JSONPointer("/name", name) }, // []string{"alice", "britt", "brian", "bob"}))) // // // Same here using JSONPointer string shortcut // td.Cmp(t, got, // td.Bag(td.Flatten( // "JSONPointer:/name", []string{"alice", "britt", "brian", "bob"}))) // // // Same here, but using SuperJSONOf operator // td.Cmp(t, got, // td.Bag(td.Flatten( // func(name string) any { return td.SuperJSONOf(`{"name":$1}`, name) }, // []string{"alice", "britt", "brian", "bob"}))) // // // Same here, but using Struct operator // td.Cmp(t, got, // td.Bag(td.Flatten( // func(name string) any { return td.Struct(person{Name: name}) }, // []string{"alice", "britt", "brian", "bob"}))) // // See also [Flatten] and [Grep]. golang-github-maxatome-go-testdeep-1.14.0/td/flatten_test.go000066400000000000000000000153361454313311600240160ustar00rootroot00000000000000// Copyright (c) 2020-2023, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td_test import ( "reflect" "strconv" "testing" "github.com/maxatome/go-testdeep/internal/test" "github.com/maxatome/go-testdeep/td" ) func TestFlatten(t *testing.T) { t.Run("ok", func(t *testing.T) { testCases := []struct { name string sliceOrMap any fn []any expectedType reflect.Type expectedLen int }{ { name: "slice", sliceOrMap: []int{1, 2, 3}, expectedType: reflect.TypeOf([]int{}), expectedLen: 3, }, { name: "array", sliceOrMap: [3]int{1, 2, 3}, expectedType: reflect.TypeOf([3]int{}), expectedLen: 3, }, { name: "map", sliceOrMap: map[int]int{1: 2, 3: 4}, expectedType: reflect.TypeOf(map[int]int{}), expectedLen: 2, }, { name: "slice+untyped nil fn", sliceOrMap: []int{1, 2, 3}, fn: []any{nil}, expectedType: reflect.TypeOf([]int{}), expectedLen: 3, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { s := td.Flatten(tc.sliceOrMap, tc.fn...) if reflect.TypeOf(s.Slice) != tc.expectedType { t.Errorf("types differ: got=%s, expected=%s", reflect.TypeOf(s.Slice), tc.expectedType) return } test.EqualInt(t, reflect.ValueOf(s.Slice).Len(), tc.expectedLen) }) } }) t.Run("ok+func", func(t *testing.T) { cmp := func(t *testing.T, got, expected []any) { t.Helper() if (got == nil) != (expected == nil) { t.Errorf("nil mismatch: got=%#v, expected=%#v", got, expected) return } lg, le := len(got), len(expected) l := lg if l > le { l = le } i := 0 for ; i < l; i++ { if got[i] != expected[i] { t.Errorf("#%d item differ, got=%v, expected=%v", i, got[i], expected[i]) } } for ; i < lg; i++ { t.Errorf("#%d item is extra, got=%v", i, got[i]) } for ; i < le; i++ { t.Errorf("#%d item is missing, expected=%v", i, expected[i]) } } testCases := []struct { name string fn any expected []any }{ { name: "func never called", fn: func(s bool) bool { return true }, expected: nil, }, { name: "double", fn: func(a int) int { return a * 2 }, expected: []any{0, 2, 4, 6, 8, 10, 12, 14, 16, 18}, }, { name: "even", fn: func(a int) (int, bool) { return a, a%2 == 0 }, expected: []any{0, 2, 4, 6, 8}, }, { name: "transform", fn: func(a int) (string, bool) { return strconv.Itoa(a), a%2 == 0 }, expected: []any{"0", "2", "4", "6", "8"}, }, { name: "nil", fn: func(a int) any { return nil }, expected: []any{nil, nil, nil, nil, nil, nil, nil, nil, nil, nil}, }, { name: "convertible", fn: func(a int8) int8 { return a * 3 }, expected: []any{ int8(0), int8(3), int8(6), int8(9), int8(12), int8(15), int8(18), int8(21), int8(24), int8(27), }, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { s := td.Flatten([]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, tc.fn) if sa, ok := s.Slice.([]any); test.IsTrue(t, ok) { cmp(t, sa, tc.expected) } }) } }) t.Run("complex", func(t *testing.T) { type person struct { Name string `json:"name"` Age int `json:"age"` } got := []person{{"alice", 22}, {"bob", 18}, {"brian", 34}, {"britt", 32}} td.Cmp(t, got, td.Bag(td.Flatten( []string{"alice", "britt", "brian", "bob"}, func(name string) any { return td.Smuggle("Name", name) }))) td.Cmp(t, got, td.Bag(td.Flatten( []string{"alice", "britt", "brian", "bob"}, "Smuggle:Name"))) td.Cmp(t, got, td.Bag(td.Flatten( []string{"alice", "britt", "brian", "bob"}, func(name string) any { return td.JSONPointer("/name", name) }))) td.Cmp(t, got, td.Bag(td.Flatten( []string{"alice", "britt", "brian", "bob"}, "JSONPointer:/name"))) td.Cmp(t, got, td.Bag(td.Flatten( []string{"alice", "britt", "brian", "bob"}, func(name string) any { return td.SuperJSONOf(`{"name":$1}`, name) }))) td.Cmp(t, got, td.Bag(td.Flatten( []string{"alice", "britt", "brian", "bob"}, func(name string) any { return td.Struct(person{Name: name}) }))) }) t.Run("errors", func(t *testing.T) { const ( usage = `usage: Flatten(SLICE|ARRAY|MAP[, FUNC])` usageFunc = usage + `, FUNC should be non-nil func(T) V or func(T) (V, bool) or a string "Smuggle:…" or "JSONPointer:…"` ) testCases := []struct { name string fn []any sliceOrMap any expected string }{ { name: "too many params", sliceOrMap: []int{}, fn: []any{1, 2}, expected: usage + ", too many parameters", }, { name: "nil sliceOrMap", expected: usage + ", but received nil as 1st parameter", }, { name: "bad sliceOrMap type", sliceOrMap: 42, expected: usage + ", but received int as 1st parameter", }, { name: "not func", sliceOrMap: []int{}, fn: []any{42}, expected: usageFunc + ", but received int as 2nd parameter", }, { name: "func w/0 inputs", sliceOrMap: []int{}, fn: []any{func() int { return 0 }}, expected: usageFunc + ", but received func() int as 2nd parameter", }, { name: "func w/2 inputs", sliceOrMap: []int{}, fn: []any{func(a, b int) int { return 0 }}, expected: usageFunc + ", but received func(int, int) int as 2nd parameter", }, { name: "variadic func", sliceOrMap: []int{}, fn: []any{func(a ...int) int { return 0 }}, expected: usageFunc + ", but received func(...int) int as 2nd parameter", }, { name: "func w/0 output", sliceOrMap: []int{}, fn: []any{func(a int) {}}, expected: usageFunc + ", but received func(int) as 2nd parameter", }, { name: "func w/2 out without bool", sliceOrMap: []int{}, fn: []any{func(a int) (int, int) { return 0, 0 }}, expected: usageFunc + ", but received func(int) (int, int) as 2nd parameter", }, { name: "bad shortcut", sliceOrMap: []int{}, fn: []any{"Pipo"}, expected: usageFunc + `, but received "Pipo" as 2nd parameter`, }, { name: "typed nil func", sliceOrMap: []int{}, fn: []any{(func(a int) int)(nil)}, expected: usageFunc, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { test.CheckPanic(t, func() { td.Flatten(tc.sliceOrMap, tc.fn...) }, tc.expected) }) } }) } golang-github-maxatome-go-testdeep-1.14.0/td/private_test.go000066400000000000000000000026571454313311600240350ustar00rootroot00000000000000// Copyright (c) 2018-2021, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td import ( "testing" "github.com/maxatome/go-testdeep/internal/test" ) // Edge cases not tested elsewhere... func TestBase(t *testing.T) { td := base{} td.setLocation(200) if td.location.File != "???" && td.location.Line != 0 { t.Errorf("Location found! => %s", td.location) } } func TestTdSetResult(t *testing.T) { if tdSetResultKind(199).String() != "?" { t.Errorf("tdSetResultKind stringification failed => %s", tdSetResultKind(199)) } } func TestPkgFunc(t *testing.T) { pkg, fn := pkgFunc("package.Foo") test.EqualStr(t, pkg, "package") test.EqualStr(t, fn, "Foo") pkg, fn = pkgFunc("the/package.Foo") test.EqualStr(t, pkg, "the/package") test.EqualStr(t, fn, "Foo") pkg, fn = pkgFunc("the/package.(*T).Foo") test.EqualStr(t, pkg, "the/package") test.EqualStr(t, fn, "(*T).Foo") pkg, fn = pkgFunc("the/package.glob..func1") test.EqualStr(t, pkg, "the/package") test.EqualStr(t, fn, "glob..func1") // Theorically not possible, but... pkg, fn = pkgFunc(".Foo") test.EqualStr(t, pkg, "") test.EqualStr(t, fn, "Foo") pkg, fn = pkgFunc("no/func") test.EqualStr(t, pkg, "no/func") test.EqualStr(t, fn, "") pkg, fn = pkgFunc("no/func.") test.EqualStr(t, pkg, "no/func") test.EqualStr(t, fn, "") } golang-github-maxatome-go-testdeep-1.14.0/td/t.go000066400000000000000000001257601454313311600215700ustar00rootroot00000000000000// Copyright (c) 2018-2022, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. // // DO NOT EDIT!!! AUTOMATICALLY GENERATED!!! package td import ( "time" ) // All is a shortcut for: // // t.Cmp(got, td.All(expectedValues...), args...) // // See [All] for details. // // Returns true if the test is OK, false if it fails. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func (t *T) All(got any, expectedValues []any, args ...any) bool { t.Helper() return t.Cmp(got, All(expectedValues...), args...) } // Any is a shortcut for: // // t.Cmp(got, td.Any(expectedValues...), args...) // // See [Any] for details. // // Returns true if the test is OK, false if it fails. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func (t *T) Any(got any, expectedValues []any, args ...any) bool { t.Helper() return t.Cmp(got, Any(expectedValues...), args...) } // Array is a shortcut for: // // t.Cmp(got, td.Array(model, expectedEntries), args...) // // See [Array] for details. // // Returns true if the test is OK, false if it fails. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func (t *T) Array(got, model any, expectedEntries ArrayEntries, args ...any) bool { t.Helper() return t.Cmp(got, Array(model, expectedEntries), args...) } // ArrayEach is a shortcut for: // // t.Cmp(got, td.ArrayEach(expectedValue), args...) // // See [ArrayEach] for details. // // Returns true if the test is OK, false if it fails. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func (t *T) ArrayEach(got, expectedValue any, args ...any) bool { t.Helper() return t.Cmp(got, ArrayEach(expectedValue), args...) } // Bag is a shortcut for: // // t.Cmp(got, td.Bag(expectedItems...), args...) // // See [Bag] for details. // // Returns true if the test is OK, false if it fails. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func (t *T) Bag(got any, expectedItems []any, args ...any) bool { t.Helper() return t.Cmp(got, Bag(expectedItems...), args...) } // Between is a shortcut for: // // t.Cmp(got, td.Between(from, to, bounds), args...) // // See [Between] for details. // // [Between] optional parameter bounds is here mandatory. // [BoundsInIn] value should be passed to mimic its absence in // original [Between] call. // // Returns true if the test is OK, false if it fails. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func (t *T) Between(got, from, to any, bounds BoundsKind, args ...any) bool { t.Helper() return t.Cmp(got, Between(from, to, bounds), args...) } // Cap is a shortcut for: // // t.Cmp(got, td.Cap(expectedCap), args...) // // See [Cap] for details. // // Returns true if the test is OK, false if it fails. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func (t *T) Cap(got, expectedCap any, args ...any) bool { t.Helper() return t.Cmp(got, Cap(expectedCap), args...) } // Code is a shortcut for: // // t.Cmp(got, td.Code(fn), args...) // // See [Code] for details. // // Returns true if the test is OK, false if it fails. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func (t *T) Code(got, fn any, args ...any) bool { t.Helper() return t.Cmp(got, Code(fn), args...) } // Contains is a shortcut for: // // t.Cmp(got, td.Contains(expectedValue), args...) // // See [Contains] for details. // // Returns true if the test is OK, false if it fails. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func (t *T) Contains(got, expectedValue any, args ...any) bool { t.Helper() return t.Cmp(got, Contains(expectedValue), args...) } // ContainsKey is a shortcut for: // // t.Cmp(got, td.ContainsKey(expectedValue), args...) // // See [ContainsKey] for details. // // Returns true if the test is OK, false if it fails. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func (t *T) ContainsKey(got, expectedValue any, args ...any) bool { t.Helper() return t.Cmp(got, ContainsKey(expectedValue), args...) } // Empty is a shortcut for: // // t.Cmp(got, td.Empty(), args...) // // See [Empty] for details. // // Returns true if the test is OK, false if it fails. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func (t *T) Empty(got any, args ...any) bool { t.Helper() return t.Cmp(got, Empty(), args...) } // CmpErrorIs is a shortcut for: // // t.Cmp(got, td.ErrorIs(expectedError), args...) // // See [ErrorIs] for details. // // Returns true if the test is OK, false if it fails. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func (t *T) CmpErrorIs(got, expectedError any, args ...any) bool { t.Helper() return t.Cmp(got, ErrorIs(expectedError), args...) } // First is a shortcut for: // // t.Cmp(got, td.First(filter, expectedValue), args...) // // See [First] for details. // // Returns true if the test is OK, false if it fails. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func (t *T) First(got, filter, expectedValue any, args ...any) bool { t.Helper() return t.Cmp(got, First(filter, expectedValue), args...) } // Grep is a shortcut for: // // t.Cmp(got, td.Grep(filter, expectedValue), args...) // // See [Grep] for details. // // Returns true if the test is OK, false if it fails. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func (t *T) Grep(got, filter, expectedValue any, args ...any) bool { t.Helper() return t.Cmp(got, Grep(filter, expectedValue), args...) } // Gt is a shortcut for: // // t.Cmp(got, td.Gt(minExpectedValue), args...) // // See [Gt] for details. // // Returns true if the test is OK, false if it fails. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func (t *T) Gt(got, minExpectedValue any, args ...any) bool { t.Helper() return t.Cmp(got, Gt(minExpectedValue), args...) } // Gte is a shortcut for: // // t.Cmp(got, td.Gte(minExpectedValue), args...) // // See [Gte] for details. // // Returns true if the test is OK, false if it fails. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func (t *T) Gte(got, minExpectedValue any, args ...any) bool { t.Helper() return t.Cmp(got, Gte(minExpectedValue), args...) } // HasPrefix is a shortcut for: // // t.Cmp(got, td.HasPrefix(expected), args...) // // See [HasPrefix] for details. // // Returns true if the test is OK, false if it fails. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func (t *T) HasPrefix(got any, expected string, args ...any) bool { t.Helper() return t.Cmp(got, HasPrefix(expected), args...) } // HasSuffix is a shortcut for: // // t.Cmp(got, td.HasSuffix(expected), args...) // // See [HasSuffix] for details. // // Returns true if the test is OK, false if it fails. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func (t *T) HasSuffix(got any, expected string, args ...any) bool { t.Helper() return t.Cmp(got, HasSuffix(expected), args...) } // Isa is a shortcut for: // // t.Cmp(got, td.Isa(model), args...) // // See [Isa] for details. // // Returns true if the test is OK, false if it fails. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func (t *T) Isa(got, model any, args ...any) bool { t.Helper() return t.Cmp(got, Isa(model), args...) } // JSON is a shortcut for: // // t.Cmp(got, td.JSON(expectedJSON, params...), args...) // // See [JSON] for details. // // Returns true if the test is OK, false if it fails. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func (t *T) JSON(got, expectedJSON any, params []any, args ...any) bool { t.Helper() return t.Cmp(got, JSON(expectedJSON, params...), args...) } // JSONPointer is a shortcut for: // // t.Cmp(got, td.JSONPointer(ptr, expectedValue), args...) // // See [JSONPointer] for details. // // Returns true if the test is OK, false if it fails. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func (t *T) JSONPointer(got any, ptr string, expectedValue any, args ...any) bool { t.Helper() return t.Cmp(got, JSONPointer(ptr, expectedValue), args...) } // Keys is a shortcut for: // // t.Cmp(got, td.Keys(val), args...) // // See [Keys] for details. // // Returns true if the test is OK, false if it fails. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func (t *T) Keys(got, val any, args ...any) bool { t.Helper() return t.Cmp(got, Keys(val), args...) } // Last is a shortcut for: // // t.Cmp(got, td.Last(filter, expectedValue), args...) // // See [Last] for details. // // Returns true if the test is OK, false if it fails. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func (t *T) Last(got, filter, expectedValue any, args ...any) bool { t.Helper() return t.Cmp(got, Last(filter, expectedValue), args...) } // CmpLax is a shortcut for: // // t.Cmp(got, td.Lax(expectedValue), args...) // // See [Lax] for details. // // Returns true if the test is OK, false if it fails. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func (t *T) CmpLax(got, expectedValue any, args ...any) bool { t.Helper() return t.Cmp(got, Lax(expectedValue), args...) } // Len is a shortcut for: // // t.Cmp(got, td.Len(expectedLen), args...) // // See [Len] for details. // // Returns true if the test is OK, false if it fails. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func (t *T) Len(got, expectedLen any, args ...any) bool { t.Helper() return t.Cmp(got, Len(expectedLen), args...) } // Lt is a shortcut for: // // t.Cmp(got, td.Lt(maxExpectedValue), args...) // // See [Lt] for details. // // Returns true if the test is OK, false if it fails. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func (t *T) Lt(got, maxExpectedValue any, args ...any) bool { t.Helper() return t.Cmp(got, Lt(maxExpectedValue), args...) } // Lte is a shortcut for: // // t.Cmp(got, td.Lte(maxExpectedValue), args...) // // See [Lte] for details. // // Returns true if the test is OK, false if it fails. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func (t *T) Lte(got, maxExpectedValue any, args ...any) bool { t.Helper() return t.Cmp(got, Lte(maxExpectedValue), args...) } // Map is a shortcut for: // // t.Cmp(got, td.Map(model, expectedEntries), args...) // // See [Map] for details. // // Returns true if the test is OK, false if it fails. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func (t *T) Map(got, model any, expectedEntries MapEntries, args ...any) bool { t.Helper() return t.Cmp(got, Map(model, expectedEntries), args...) } // MapEach is a shortcut for: // // t.Cmp(got, td.MapEach(expectedValue), args...) // // See [MapEach] for details. // // Returns true if the test is OK, false if it fails. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func (t *T) MapEach(got, expectedValue any, args ...any) bool { t.Helper() return t.Cmp(got, MapEach(expectedValue), args...) } // N is a shortcut for: // // t.Cmp(got, td.N(num, tolerance), args...) // // See [N] for details. // // [N] optional parameter tolerance is here mandatory. // 0 value should be passed to mimic its absence in // original [N] call. // // Returns true if the test is OK, false if it fails. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func (t *T) N(got, num, tolerance any, args ...any) bool { t.Helper() return t.Cmp(got, N(num, tolerance), args...) } // NaN is a shortcut for: // // t.Cmp(got, td.NaN(), args...) // // See [NaN] for details. // // Returns true if the test is OK, false if it fails. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func (t *T) NaN(got any, args ...any) bool { t.Helper() return t.Cmp(got, NaN(), args...) } // Nil is a shortcut for: // // t.Cmp(got, td.Nil(), args...) // // See [Nil] for details. // // Returns true if the test is OK, false if it fails. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func (t *T) Nil(got any, args ...any) bool { t.Helper() return t.Cmp(got, Nil(), args...) } // None is a shortcut for: // // t.Cmp(got, td.None(notExpectedValues...), args...) // // See [None] for details. // // Returns true if the test is OK, false if it fails. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func (t *T) None(got any, notExpectedValues []any, args ...any) bool { t.Helper() return t.Cmp(got, None(notExpectedValues...), args...) } // Not is a shortcut for: // // t.Cmp(got, td.Not(notExpected), args...) // // See [Not] for details. // // Returns true if the test is OK, false if it fails. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func (t *T) Not(got, notExpected any, args ...any) bool { t.Helper() return t.Cmp(got, Not(notExpected), args...) } // NotAny is a shortcut for: // // t.Cmp(got, td.NotAny(notExpectedItems...), args...) // // See [NotAny] for details. // // Returns true if the test is OK, false if it fails. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func (t *T) NotAny(got any, notExpectedItems []any, args ...any) bool { t.Helper() return t.Cmp(got, NotAny(notExpectedItems...), args...) } // NotEmpty is a shortcut for: // // t.Cmp(got, td.NotEmpty(), args...) // // See [NotEmpty] for details. // // Returns true if the test is OK, false if it fails. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func (t *T) NotEmpty(got any, args ...any) bool { t.Helper() return t.Cmp(got, NotEmpty(), args...) } // NotNaN is a shortcut for: // // t.Cmp(got, td.NotNaN(), args...) // // See [NotNaN] for details. // // Returns true if the test is OK, false if it fails. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func (t *T) NotNaN(got any, args ...any) bool { t.Helper() return t.Cmp(got, NotNaN(), args...) } // NotNil is a shortcut for: // // t.Cmp(got, td.NotNil(), args...) // // See [NotNil] for details. // // Returns true if the test is OK, false if it fails. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func (t *T) NotNil(got any, args ...any) bool { t.Helper() return t.Cmp(got, NotNil(), args...) } // NotZero is a shortcut for: // // t.Cmp(got, td.NotZero(), args...) // // See [NotZero] for details. // // Returns true if the test is OK, false if it fails. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func (t *T) NotZero(got any, args ...any) bool { t.Helper() return t.Cmp(got, NotZero(), args...) } // PPtr is a shortcut for: // // t.Cmp(got, td.PPtr(val), args...) // // See [PPtr] for details. // // Returns true if the test is OK, false if it fails. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func (t *T) PPtr(got, val any, args ...any) bool { t.Helper() return t.Cmp(got, PPtr(val), args...) } // Ptr is a shortcut for: // // t.Cmp(got, td.Ptr(val), args...) // // See [Ptr] for details. // // Returns true if the test is OK, false if it fails. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func (t *T) Ptr(got, val any, args ...any) bool { t.Helper() return t.Cmp(got, Ptr(val), args...) } // Re is a shortcut for: // // t.Cmp(got, td.Re(reg, capture), args...) // // See [Re] for details. // // [Re] optional parameter capture is here mandatory. // nil value should be passed to mimic its absence in // original [Re] call. // // Returns true if the test is OK, false if it fails. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func (t *T) Re(got, reg, capture any, args ...any) bool { t.Helper() return t.Cmp(got, Re(reg, capture), args...) } // ReAll is a shortcut for: // // t.Cmp(got, td.ReAll(reg, capture), args...) // // See [ReAll] for details. // // Returns true if the test is OK, false if it fails. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func (t *T) ReAll(got, reg, capture any, args ...any) bool { t.Helper() return t.Cmp(got, ReAll(reg, capture), args...) } // Recv is a shortcut for: // // t.Cmp(got, td.Recv(expectedValue, timeout), args...) // // See [Recv] for details. // // [Recv] optional parameter timeout is here mandatory. // 0 value should be passed to mimic its absence in // original [Recv] call. // // Returns true if the test is OK, false if it fails. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func (t *T) Recv(got, expectedValue any, timeout time.Duration, args ...any) bool { t.Helper() return t.Cmp(got, Recv(expectedValue, timeout), args...) } // Set is a shortcut for: // // t.Cmp(got, td.Set(expectedItems...), args...) // // See [Set] for details. // // Returns true if the test is OK, false if it fails. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func (t *T) Set(got any, expectedItems []any, args ...any) bool { t.Helper() return t.Cmp(got, Set(expectedItems...), args...) } // Shallow is a shortcut for: // // t.Cmp(got, td.Shallow(expectedPtr), args...) // // See [Shallow] for details. // // Returns true if the test is OK, false if it fails. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func (t *T) Shallow(got, expectedPtr any, args ...any) bool { t.Helper() return t.Cmp(got, Shallow(expectedPtr), args...) } // Slice is a shortcut for: // // t.Cmp(got, td.Slice(model, expectedEntries), args...) // // See [Slice] for details. // // Returns true if the test is OK, false if it fails. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func (t *T) Slice(got, model any, expectedEntries ArrayEntries, args ...any) bool { t.Helper() return t.Cmp(got, Slice(model, expectedEntries), args...) } // Smuggle is a shortcut for: // // t.Cmp(got, td.Smuggle(fn, expectedValue), args...) // // See [Smuggle] for details. // // Returns true if the test is OK, false if it fails. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func (t *T) Smuggle(got, fn, expectedValue any, args ...any) bool { t.Helper() return t.Cmp(got, Smuggle(fn, expectedValue), args...) } // SStruct is a shortcut for: // // t.Cmp(got, td.SStruct(model, expectedFields), args...) // // See [SStruct] for details. // // [SStruct] optional parameter expectedFields is here mandatory. // nil value should be passed to mimic its absence in // original [SStruct] call. // // Returns true if the test is OK, false if it fails. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func (t *T) SStruct(got, model any, expectedFields StructFields, args ...any) bool { t.Helper() return t.Cmp(got, SStruct(model, expectedFields), args...) } // String is a shortcut for: // // t.Cmp(got, td.String(expected), args...) // // See [String] for details. // // Returns true if the test is OK, false if it fails. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func (t *T) String(got any, expected string, args ...any) bool { t.Helper() return t.Cmp(got, String(expected), args...) } // Struct is a shortcut for: // // t.Cmp(got, td.Struct(model, expectedFields), args...) // // See [Struct] for details. // // [Struct] optional parameter expectedFields is here mandatory. // nil value should be passed to mimic its absence in // original [Struct] call. // // Returns true if the test is OK, false if it fails. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func (t *T) Struct(got, model any, expectedFields StructFields, args ...any) bool { t.Helper() return t.Cmp(got, Struct(model, expectedFields), args...) } // SubBagOf is a shortcut for: // // t.Cmp(got, td.SubBagOf(expectedItems...), args...) // // See [SubBagOf] for details. // // Returns true if the test is OK, false if it fails. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func (t *T) SubBagOf(got any, expectedItems []any, args ...any) bool { t.Helper() return t.Cmp(got, SubBagOf(expectedItems...), args...) } // SubJSONOf is a shortcut for: // // t.Cmp(got, td.SubJSONOf(expectedJSON, params...), args...) // // See [SubJSONOf] for details. // // Returns true if the test is OK, false if it fails. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func (t *T) SubJSONOf(got, expectedJSON any, params []any, args ...any) bool { t.Helper() return t.Cmp(got, SubJSONOf(expectedJSON, params...), args...) } // SubMapOf is a shortcut for: // // t.Cmp(got, td.SubMapOf(model, expectedEntries), args...) // // See [SubMapOf] for details. // // Returns true if the test is OK, false if it fails. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func (t *T) SubMapOf(got, model any, expectedEntries MapEntries, args ...any) bool { t.Helper() return t.Cmp(got, SubMapOf(model, expectedEntries), args...) } // SubSetOf is a shortcut for: // // t.Cmp(got, td.SubSetOf(expectedItems...), args...) // // See [SubSetOf] for details. // // Returns true if the test is OK, false if it fails. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func (t *T) SubSetOf(got any, expectedItems []any, args ...any) bool { t.Helper() return t.Cmp(got, SubSetOf(expectedItems...), args...) } // SuperBagOf is a shortcut for: // // t.Cmp(got, td.SuperBagOf(expectedItems...), args...) // // See [SuperBagOf] for details. // // Returns true if the test is OK, false if it fails. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func (t *T) SuperBagOf(got any, expectedItems []any, args ...any) bool { t.Helper() return t.Cmp(got, SuperBagOf(expectedItems...), args...) } // SuperJSONOf is a shortcut for: // // t.Cmp(got, td.SuperJSONOf(expectedJSON, params...), args...) // // See [SuperJSONOf] for details. // // Returns true if the test is OK, false if it fails. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func (t *T) SuperJSONOf(got, expectedJSON any, params []any, args ...any) bool { t.Helper() return t.Cmp(got, SuperJSONOf(expectedJSON, params...), args...) } // SuperMapOf is a shortcut for: // // t.Cmp(got, td.SuperMapOf(model, expectedEntries), args...) // // See [SuperMapOf] for details. // // Returns true if the test is OK, false if it fails. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func (t *T) SuperMapOf(got, model any, expectedEntries MapEntries, args ...any) bool { t.Helper() return t.Cmp(got, SuperMapOf(model, expectedEntries), args...) } // SuperSetOf is a shortcut for: // // t.Cmp(got, td.SuperSetOf(expectedItems...), args...) // // See [SuperSetOf] for details. // // Returns true if the test is OK, false if it fails. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func (t *T) SuperSetOf(got any, expectedItems []any, args ...any) bool { t.Helper() return t.Cmp(got, SuperSetOf(expectedItems...), args...) } // SuperSliceOf is a shortcut for: // // t.Cmp(got, td.SuperSliceOf(model, expectedEntries), args...) // // See [SuperSliceOf] for details. // // Returns true if the test is OK, false if it fails. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func (t *T) SuperSliceOf(got, model any, expectedEntries ArrayEntries, args ...any) bool { t.Helper() return t.Cmp(got, SuperSliceOf(model, expectedEntries), args...) } // TruncTime is a shortcut for: // // t.Cmp(got, td.TruncTime(expectedTime, trunc), args...) // // See [TruncTime] for details. // // [TruncTime] optional parameter trunc is here mandatory. // 0 value should be passed to mimic its absence in // original [TruncTime] call. // // Returns true if the test is OK, false if it fails. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func (t *T) TruncTime(got, expectedTime any, trunc time.Duration, args ...any) bool { t.Helper() return t.Cmp(got, TruncTime(expectedTime, trunc), args...) } // Values is a shortcut for: // // t.Cmp(got, td.Values(val), args...) // // See [Values] for details. // // Returns true if the test is OK, false if it fails. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func (t *T) Values(got, val any, args ...any) bool { t.Helper() return t.Cmp(got, Values(val), args...) } // Zero is a shortcut for: // // t.Cmp(got, td.Zero(), args...) // // See [Zero] for details. // // Returns true if the test is OK, false if it fails. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func (t *T) Zero(got any, args ...any) bool { t.Helper() return t.Cmp(got, Zero(), args...) } golang-github-maxatome-go-testdeep-1.14.0/td/t_anchor.go000066400000000000000000000213261454313311600231130ustar00rootroot00000000000000// Copyright (c) 2019, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td import ( "reflect" "sync" "github.com/maxatome/go-testdeep/internal/anchors" "github.com/maxatome/go-testdeep/internal/color" ) // Anchors are stored globally by testing.TB.Name(). var allAnchors = map[string]*anchors.Info{} var allAnchorsMu sync.Mutex // AddAnchorableStructType declares a struct type as anchorable. fn // is a function allowing to return a unique and identifiable instance // of the struct type. // // fn has to have the following signature: // // func (nextAnchor int) TYPE // // TYPE is the struct type to make anchorable and nextAnchor is an // index to allow to differentiate several instances of the same type. // // For example, the [time.Time] type which is anchorable by default, // could be declared as: // // AddAnchorableStructType(func (nextAnchor int) time.Time { // return time.Unix(int64(math.MaxInt64-1000424443-nextAnchor), 42) // }) // // Just as a note, the 1000424443 constant allows to avoid to flirt // with the math.MaxInt64 extreme limit and so avoid possible // collision with real world values. // // It panics if the provided fn is not a function or if it has not the // expected signature (see above). // // See also [T.Anchor], [T.AnchorsPersistTemporarily], // [T.DoAnchorsPersist], [T.ResetAnchors] and [T.SetAnchorsPersist]. func AddAnchorableStructType(fn any) { err := anchors.AddAnchorableStructType(fn) if err != nil { panic(color.Bad(err.Error())) } } // Anchor returns a typed value allowing to anchor the TestDeep // operator operator in a go classic literal like a struct, slice, // array or map value. // // If the TypeBehind method of operator returns non-nil, model can be // omitted (like with [Between] operator in the example // below). Otherwise, model should contain only one value // corresponding to the returning type. It can be: // - a go value: returning type is the type of the value, // whatever the value is; // - a [reflect.Type]. // // It returns a typed value ready to be embed in a go data structure to // be compared using [T.Cmp] or [T.CmpLax]: // // import ( // "testing" // // "github.com/maxatome/go-testdeep/td" // ) // // func TestFunc(tt *testing.T) { // got := Func() // // t := td.NewT(tt) // t.Cmp(got, &MyStruct{ // Name: "Bob", // Details: &MyDetails{ // Nick: t.Anchor(td.HasPrefix("Bobby"), "").(string), // Age: t.Anchor(td.Between(40, 50)).(int), // }, // }) // } // // In this example: // // - [HasPrefix] operates on several input types (string, // [fmt.Stringer], error, …), so its TypeBehind method returns always // nil as it can not guess in advance on which type it operates. In // this case, we must pass "" as model parameter in order to tell it // to return the string type. Note that the .(string) type assertion // is then mandatory to conform to the strict type checking. // - [Between], on its side, knows the type on which it operates, as // it is the same as the one of its parameters. So its TypeBehind // method returns the right type, and so no need to pass it as model // parameter. Note that the .(int) type assertion is still mandatory // to conform to the strict type checking. // // Without operator anchoring feature, the previous example would have // been: // // import ( // "testing" // // "github.com/maxatome/go-testdeep/td" // ) // // func TestFunc(tt *testing.T) { // got := Func() // // t := td.NewT(tt) // t.Cmp(got, td.Struct(&MyStruct{Name: "Bob"}, // td.StructFields{ // "Details": td.Struct(&MyDetails{}, // td.StructFields{ // "Nick": td.HasPrefix("Bobby"), // "Age": td.Between(40, 50), // }), // })) // } // // using two times the [Struct] operator to work around the strict type // checking of golang. // // By default, the value returned by Anchor can only be used in the // next [T.Cmp] or [T.CmpLax] call. To make it persistent across calls, // see [T.SetAnchorsPersist] and [T.AnchorsPersistTemporarily] methods. // // See [T.A] method for a shorter synonym of Anchor. // // See also [T.AnchorsPersistTemporarily], [T.DoAnchorsPersist], // [T.ResetAnchors], [T.SetAnchorsPersist] and [AddAnchorableStructType]. func (t *T) Anchor(operator TestDeep, model ...any) any { if operator == nil { t.Helper() t.Fatal(color.Bad("Cannot anchor a nil TestDeep operator")) } var typ reflect.Type if len(model) > 0 { if len(model) != 1 { t.Helper() t.Fatal(color.TooManyParams("Anchor(OPERATOR[, MODEL])")) } var ok bool typ, ok = model[0].(reflect.Type) if !ok { typ = reflect.TypeOf(model[0]) if typ == nil { t.Helper() t.Fatal(color.Bad("Untyped nil value is not valid as model for an anchor")) } } typeBehind := operator.TypeBehind() if typeBehind != nil && typeBehind != typ { t.Helper() t.Fatal(color.Bad("Operator %s TypeBehind() returned %s which differs from model type %s. Omit model or ensure its type is %[2]s", operator.GetLocation().Func, typeBehind, typ)) } } else { typ = operator.TypeBehind() if typ == nil { t.Helper() t.Fatal(color.Bad("Cannot anchor operator %s as TypeBehind() returned nil. Use model parameter to specify the type to return", operator.GetLocation().Func)) } } nvm, err := t.Config.anchors.AddAnchor(typ, reflect.ValueOf(operator)) if err != nil { t.Helper() t.Fatal(color.Bad(err.Error())) } return nvm.Interface() } // A is a synonym for [T.Anchor]. // // import ( // "testing" // // "github.com/maxatome/go-testdeep/td" // ) // // func TestFunc(tt *testing.T) { // got := Func() // // t := td.NewT(tt) // t.Cmp(got, &MyStruct{ // Name: "Bob", // Details: &MyDetails{ // Nick: t.A(td.HasPrefix("Bobby"), "").(string), // Age: t.A(td.Between(40, 50)).(int), // }, // }) // } // // See also [T.AnchorsPersistTemporarily], [T.DoAnchorsPersist], // [T.ResetAnchors], [T.SetAnchorsPersist] and [AddAnchorableStructType]. func (t *T) A(operator TestDeep, model ...any) any { t.Helper() return t.Anchor(operator, model...) } func (t *T) resetNonPersistentAnchors() { t.Config.anchors.ResetAnchors(false) } // ResetAnchors frees all operators anchored with [T.Anchor] // method. Unless operators anchoring persistence has been enabled // with [T.SetAnchorsPersist], there is no need to call this // method. Anchored operators are automatically freed after each [Cmp], // [CmpDeeply] and [CmpPanic] call (or others methods calling them behind // the scene). // // See also [T.Anchor], [T.AnchorsPersistTemporarily], // [T.DoAnchorsPersist], [T.SetAnchorsPersist] and [AddAnchorableStructType]. func (t *T) ResetAnchors() { t.Config.anchors.ResetAnchors(true) } // AnchorsPersistTemporarily is used by helpers to temporarily enable // anchors persistence. See [tdhttp] package for an example of use. It // returns a function to be deferred, to restore the normal behavior // (clear anchored operators if persistence was false, do nothing // otherwise). // // Typically used as: // // defer t.AnchorsPersistTemporarily()() // // or // t.Cleanup(t.AnchorsPersistTemporarily()) // // See also [T.Anchor], [T.DoAnchorsPersist], [T.ResetAnchors], // [T.SetAnchorsPersist] and [AddAnchorableStructType]. // // [tdhttp]: https://pkg.go.dev/github.com/maxatome/go-testdeep/helpers/tdhttp func (t *T) AnchorsPersistTemporarily() func() { // If already persistent, do nothing on defer if t.DoAnchorsPersist() { return func() {} } t.SetAnchorsPersist(true) return func() { t.SetAnchorsPersist(false) t.Config.anchors.ResetAnchors(true) } } // DoAnchorsPersist returns true if anchors persistence is enabled, // false otherwise. // // See also [T.Anchor], [T.AnchorsPersistTemporarily], // [T.ResetAnchors], [T.SetAnchorsPersist] and [AddAnchorableStructType]. func (t *T) DoAnchorsPersist() bool { return t.Config.anchors.DoAnchorsPersist() } // SetAnchorsPersist allows to enable or disable anchors persistence. // // See also [T.Anchor], [T.AnchorsPersistTemporarily], // [T.DoAnchorsPersist], [T.ResetAnchors] and [AddAnchorableStructType]. func (t *T) SetAnchorsPersist(persist bool) { t.Config.anchors.SetAnchorsPersist(persist) } func (t *T) initAnchors() { if t.Config.anchors != nil { return } name := t.Name() allAnchorsMu.Lock() defer allAnchorsMu.Unlock() t.Config.anchors = allAnchors[name] if t.Config.anchors == nil { t.Config.anchors = anchors.NewInfo() allAnchors[name] = t.Config.anchors // Do not record a finalizer if no name (should not happen // except perhaps in tests) if name != "" { t.Cleanup(func() { allAnchorsMu.Lock() defer allAnchorsMu.Unlock() delete(allAnchors, name) }) } } } golang-github-maxatome-go-testdeep-1.14.0/td/t_anchor_118.go000066400000000000000000000010121454313311600234720ustar00rootroot00000000000000// Copyright (c) 2023, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. //go:build go1.18 // +build go1.18 package td // Anchor is a generic shortcut to [T.Anchor]. func Anchor[X any](t *T, operator TestDeep) X { var model X return t.Anchor(operator, model).(X) } // A is a generic shortcut to [T.A]. func A[X any](t *T, operator TestDeep) X { var model X return t.A(operator, model).(X) } golang-github-maxatome-go-testdeep-1.14.0/td/t_anchor_118_test.go000066400000000000000000000024741454313311600245460ustar00rootroot00000000000000// Copyright (c) 2023, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. //go:build go1.18 // +build go1.18 package td_test import ( "testing" "time" "github.com/maxatome/go-testdeep/internal/test" "github.com/maxatome/go-testdeep/td" ) func TestAnchor(tt *testing.T) { ttt := test.NewTestingTB(tt.Name()) t := td.NewT(ttt) type MyStruct struct { PNum *int Num int64 Str string Slice []int Map map[string]bool Time time.Time } n := 42 got := MyStruct{ PNum: &n, Num: 136, Str: "Pipo bingo", Time: timeParse(tt, "2019-01-02T11:22:33.123456Z"), } td.CmpTrue(tt, t.Cmp(got, MyStruct{ PNum: td.Anchor[*int](t, td.Ptr(td.Between(40, 45))), Num: td.Anchor[int64](t, td.Between(int64(135), int64(137))), Str: td.Anchor[string](t, td.HasPrefix("Pipo")), Time: td.Anchor[time.Time](t, td.TruncTime(timeParse(tt, "2019-01-02T11:22:00Z"), time.Minute)), })) td.CmpTrue(tt, t.Cmp(got, MyStruct{ PNum: td.A[*int](t, td.Ptr(td.Between(40, 45))), Num: td.A[int64](t, td.Between(int64(135), int64(137))), Str: td.A[string](t, td.HasPrefix("Pipo")), Time: td.A[time.Time](t, td.TruncTime(timeParse(tt, "2019-01-02T11:22:00Z"), time.Minute)), })) } golang-github-maxatome-go-testdeep-1.14.0/td/t_anchor_test.go000066400000000000000000000120421454313311600241450ustar00rootroot00000000000000// Copyright (c) 2019, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td_test import ( "testing" "time" "github.com/maxatome/go-testdeep/internal/test" "github.com/maxatome/go-testdeep/td" ) func timeParse(t *testing.T, s string) time.Time { dt, err := time.Parse(time.RFC3339Nano, s) if err != nil { t.Helper() t.Fatalf("Cannot parse `%s`: %s", s, err) } return dt } func TestT_Anchor(tt *testing.T) { ttt := test.NewTestingTB(tt.Name()) t := td.NewT(ttt) type MyStruct struct { PNum *int Num int64 Str string Slice []int Map map[string]bool Time time.Time } n := 42 got := MyStruct{ PNum: &n, Num: 136, Str: "Pipo bingo", Time: timeParse(tt, "2019-01-02T11:22:33.123456Z"), } // Using T.Anchor() td.CmpTrue(tt, t.Cmp(got, MyStruct{ PNum: t.Anchor(td.Ptr(td.Between(40, 45))).(*int), Num: t.Anchor(td.Between(int64(135), int64(137))).(int64), Str: t.Anchor(td.HasPrefix("Pipo"), "").(string), Time: t.Anchor(td.TruncTime(timeParse(tt, "2019-01-02T11:22:00Z"), time.Minute)).(time.Time), })) // Using T.A() td.CmpTrue(tt, t.Cmp(got, MyStruct{ PNum: t.A(td.Ptr(td.Between(40, 45))).(*int), Num: t.A(td.Between(int64(135), int64(137))).(int64), Str: t.A(td.HasPrefix("Pipo"), "").(string), Time: t.A(td.TruncTime(timeParse(tt, "2019-01-02T11:22:00Z"), time.Minute)).(time.Time), })) // Testing persistence got = MyStruct{Num: 136} tt.Run("without persistence", func(tt *testing.T) { numOp := t.Anchor(td.Between(int64(135), int64(137))).(int64) td.CmpTrue(tt, t.Cmp(got, MyStruct{Num: numOp})) td.CmpFalse(tt, t.Cmp(got, MyStruct{Num: numOp})) }) tt.Run("with persistence", func(tt *testing.T) { numOp := t.Anchor(td.Between(int64(135), int64(137))).(int64) defer t.AnchorsPersistTemporarily()() td.CmpTrue(tt, t.Cmp(got, MyStruct{Num: numOp})) td.CmpTrue(tt, t.Cmp(got, MyStruct{Num: numOp})) t.ResetAnchors() // force reset anchored operators td.CmpFalse(tt, t.Cmp(got, MyStruct{Num: numOp})) }) // Errors tt.Run("errors", func(tt *testing.T) { td.Cmp(tt, ttt.CatchFatal(func() { t.Anchor(nil) }), "Cannot anchor a nil TestDeep operator") td.Cmp(tt, ttt.CatchFatal(func() { t.Anchor(td.Ignore(), 1, 2) }), "usage: Anchor(OPERATOR[, MODEL]), too many parameters") td.Cmp(tt, ttt.CatchFatal(func() { t.Anchor(td.Ignore(), nil) }), "Untyped nil value is not valid as model for an anchor") td.Cmp(tt, ttt.CatchFatal(func() { t.Anchor(td.Between(1, 2), 12.3) }), "Operator Between TypeBehind() returned int which differs from model type float64. Omit model or ensure its type is int") td.Cmp(tt, ttt.CatchFatal(func() { t.Anchor(td.Ignore()) }), "Cannot anchor operator Ignore as TypeBehind() returned nil. Use model parameter to specify the type to return") }) } type privStruct struct { num int64 } func (p privStruct) Num() int64 { return p.num } func TestAddAnchorableStructType(tt *testing.T) { type MyStruct struct { Priv privStruct } ttt := test.NewTestingTB(tt.Name()) t := td.NewT(ttt) // We want to anchor this operator op := td.Smuggle((privStruct).Num, int64(42)) // Without making privStruct anchorable, it does not work td.Cmp(tt, ttt.CatchFatal(func() { t.A(op, privStruct{}) }), "td_test.privStruct struct type is not supported as an anchor. Try AddAnchorableStructType") // Make privStruct anchorable td.AddAnchorableStructType(func(nextAnchor int) privStruct { return privStruct{num: int64(2e9 - nextAnchor)} }) td.CmpTrue(tt, t.Cmp(MyStruct{Priv: privStruct{num: 42}}, MyStruct{ Priv: t.A(op, privStruct{}).(privStruct), // ← now it works })) // Error test.CheckPanic(tt, func() { td.AddAnchorableStructType(123) }, "usage: AddAnchorableStructType(func (nextAnchor int) STRUCT_TYPE)") } func TestT_AnchorsPersist(tt *testing.T) { ttt := test.NewTestingTB(tt.Name()) t1 := td.NewT(ttt) t2 := td.NewT(ttt) t3 := td.NewT(t1) tt.Run("without anchors persistence", func(tt *testing.T) { // Anchors persistence is shared for a same testing.TB td.CmpFalse(tt, t1.DoAnchorsPersist()) td.CmpFalse(tt, t2.DoAnchorsPersist()) td.CmpFalse(tt, t3.DoAnchorsPersist()) func() { defer t1.AnchorsPersistTemporarily()() td.CmpTrue(tt, t1.DoAnchorsPersist()) td.CmpTrue(tt, t2.DoAnchorsPersist()) td.CmpTrue(tt, t3.DoAnchorsPersist()) }() td.CmpFalse(tt, t1.DoAnchorsPersist()) td.CmpFalse(tt, t2.DoAnchorsPersist()) td.CmpFalse(tt, t3.DoAnchorsPersist()) }) tt.Run("with anchors persistence", func(tt *testing.T) { t3.SetAnchorsPersist(true) td.CmpTrue(tt, t1.DoAnchorsPersist()) td.CmpTrue(tt, t2.DoAnchorsPersist()) td.CmpTrue(tt, t3.DoAnchorsPersist()) func() { defer t1.AnchorsPersistTemporarily()() td.CmpTrue(tt, t1.DoAnchorsPersist()) td.CmpTrue(tt, t2.DoAnchorsPersist()) td.CmpTrue(tt, t3.DoAnchorsPersist()) }() td.CmpTrue(tt, t1.DoAnchorsPersist()) td.CmpTrue(tt, t2.DoAnchorsPersist()) td.CmpTrue(tt, t3.DoAnchorsPersist()) }) } golang-github-maxatome-go-testdeep-1.14.0/td/t_hooks.go000066400000000000000000000117371454313311600227710ustar00rootroot00000000000000// Copyright (c) 2020, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td import ( "github.com/maxatome/go-testdeep/internal/color" ) // WithCmpHooks returns a new [*T] instance with new Cmp hooks recorded // using functions passed in fns. // // Each function in fns has to be a function with the following // possible signatures: // // func (got A, expected A) bool // func (got A, expected A) error // // First arg is always got, and second is always expected. // // A cannot be an interface. This restriction can be removed in the // future, if really needed. // // This function is called as soon as possible each time the type A is // encountered for got while expected type is assignable to A. // // When it returns a bool, false means A is not equal to B. // // When it returns a non-nil error (meaning got ≠ expected), its // content is used to tell the reason of the failure. // // Cmp hooks are checked before [UseEqual] feature. // // Cmp hooks are run just after [Smuggle] hooks. // // func TestCmpHook(tt *testing.T) { // t := td.NewT(tt) // // // Test reflect.Value contents instead of default field/field // t = t.WithCmpHooks(func (got, expected reflect.Value) bool { // return td.EqDeeply(got.Interface(), expected.Interface()) // }) // a, b := 1, 1 // t.Cmp(reflect.ValueOf(&a), reflect.ValueOf(&b)) // succeeds // // // Test reflect.Type correctly instead of default field/field // t = t.WithCmpHooks(func (got, expected reflect.Type) bool { // return got == expected // }) // // // Test time.Time via its Equal() method instead of default // // field/field (note it bypasses the UseEqual flag) // t = t.WithCmpHooks((time.Time).Equal) // date, _ := time.Parse(time.RFC3339, "2020-09-08T22:13:54+02:00") // t.Cmp(date, date.UTC()) // succeeds // // // Several hooks can be declared at once // t = t.WithCmpHooks( // func (got, expected reflect.Value) bool { // return td.EqDeeply(got.Interface(), expected.Interface()) // }, // func (got, expected reflect.Type) bool { // return got == expected // }, // (time.Time).Equal, // ) // } // // There is no way to add or remove hooks of an existing [*T] // instance, only to create a new [*T] instance with this method or // [T.WithSmuggleHooks] to add some. // // WithCmpHooks calls t.Fatal if an item of fns is not a function or // if its signature does not match the expected ones. // // See also [T.WithSmuggleHooks]. // // [UseEqual]: https://pkg.go.dev/github.com/maxatome/go-testdeep/td#ContextConfig.UseEqual func (t *T) WithCmpHooks(fns ...any) *T { t = t.copyWithHooks() err := t.Config.hooks.AddCmpHooks(fns) if err != nil { t.Helper() t.Fatal(color.Bad("WithCmpHooks " + err.Error())) } return t } // WithSmuggleHooks returns a new [*T] instance with new Smuggle hooks // recorded using functions passed in fns. // // Each function in fns has to be a function with the following // possible signatures: // // func (got A) B // func (got A) (B, error) // // A cannot be an interface. This restriction can be removed in the // future, if really needed. // // B cannot be an interface. If you have a use case, we can talk about it. // // This function is called as soon as possible each time the type A is // encountered for got. // // The B value returned replaces the got value for subsequent tests. // Smuggle hooks are NOT run again for this returned value to avoid // easy infinite loop recursion. // // When it returns non-nil error (meaning something wrong happened // during the conversion of A to B), it raises a global error and its // content is used to tell the reason of the failure. // // Smuggle hooks are run just before Cmp hooks. // // func TestSmuggleHook(tt *testing.T) { // t := td.NewT(tt) // // // Each encountered int is changed to a bool // t = t.WithSmuggleHooks(func (got int) bool { // return got != 0 // }) // t.Cmp(map[string]int{"ok": 1, "no": 0}, // map[string]bool{"ok", true, "no", false}) // succeeds // // // Each encountered string is converted to int // t = t.WithSmuggleHooks(strconv.Atoi) // t.Cmp("123", 123) // succeeds // // // Several hooks can be declared at once // t = t.WithSmuggleHooks( // func (got int) bool { return got != 0 }, // strconv.Atoi, // ) // } // // There is no way to add or remove hooks of an existing [*T] // instance, only create a new [*T] instance with this method or // [T.WithCmpHooks] to add some. // // WithSmuggleHooks calls t.Fatal if an item of fns is not a // function or if its signature does not match the expected ones. // // See also [T.WithCmpHooks]. func (t *T) WithSmuggleHooks(fns ...any) *T { t = t.copyWithHooks() err := t.Config.hooks.AddSmuggleHooks(fns) if err != nil { t.Helper() t.Fatal(color.Bad("WithSmuggleHooks " + err.Error())) } return t } func (t *T) copyWithHooks() *T { nt := NewT(t) nt.Config.hooks = t.Config.hooks.Copy() return nt } golang-github-maxatome-go-testdeep-1.14.0/td/t_hooks_test.go000066400000000000000000000114671454313311600240300ustar00rootroot00000000000000// Copyright (c) 2020, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td_test import ( "errors" "fmt" "reflect" "strconv" "strings" "testing" "time" "github.com/maxatome/go-testdeep/internal/test" "github.com/maxatome/go-testdeep/td" ) func TestWithCmpHooks(tt *testing.T) { na, nb := 1234, 1234 date, _ := time.Parse(time.RFC3339, "2020-09-08T22:13:54+02:00") for _, tst := range []struct { name string cmp any got, expected any }{ { name: "reflect.Value", cmp: func(got, expected reflect.Value) bool { return td.EqDeeply(got.Interface(), expected.Interface()) }, got: reflect.ValueOf(&na), expected: reflect.ValueOf(&nb), }, { name: "time.Time", cmp: (time.Time).Equal, got: date, expected: date.UTC(), }, { name: "numify", cmp: func(got, expected string) error { ngot, err := strconv.Atoi(got) if err != nil { return fmt.Errorf("strconv.Atoi(got) failed: %s", err) } nexpected, err := strconv.Atoi(expected) if err != nil { return fmt.Errorf("strconv.Atoi(expected) failed: %s", err) } if ngot != nexpected { return errors.New("values differ") } return nil }, got: "0000001234", expected: "1234", }, { name: "false test :)", cmp: func(got, expected int) bool { return got == -expected }, got: 1, expected: -1, }, } { tt.Run(tst.name, func(tt *testing.T) { ttt := test.NewTestingTB(tt.Name()) t := td.NewT(ttt) td.CmpFalse(tt, func() bool { // A panic can occur when -tags safe: // dark.GetInterface() does not handle private unsafe.Pointer kind defer func() { recover() }() //nolint: errcheck return t.Cmp(tst.got, tst.expected) }()) t = t.WithCmpHooks(tst.cmp) td.CmpTrue(tt, t.Cmp(tst.got, tst.expected)) }) } tt.Run("Error", func(tt *testing.T) { ttt := test.NewTestingTB(tt.Name()) t := td.NewT(ttt). WithCmpHooks(func(got, expected int) error { return errors.New("never equal") }) td.CmpFalse(tt, t.Cmp(1, 1)) if !strings.Contains(ttt.LastMessage(), "DATA: never equal\n") { tt.Errorf(`<%s> does not contain "DATA: never equal\n"`, ttt.LastMessage()) } }) for _, tst := range []struct { name string cmp any fatal string }{ { name: "not a function", cmp: "Booh", fatal: "WithCmpHooks expects a function, not a string", }, { name: "wrong signature", cmp: func(a []int, b ...int) bool { return false }, fatal: "WithCmpHooks expects: func (T, T) bool|error not ", }, } { tt.Run("panic: "+tst.name, func(tt *testing.T) { ttt := test.NewTestingTB(tt.Name()) t := td.NewT(ttt) fatalMesg := ttt.CatchFatal(func() { t.WithCmpHooks(tst.cmp) }) test.IsTrue(tt, ttt.IsFatal) if !strings.Contains(fatalMesg, tst.fatal) { tt.Errorf(`<%s> does not contain %q`, fatalMesg, tst.fatal) } }) } } func TestWithSmuggleHooks(tt *testing.T) { for _, tst := range []struct { name string cmp any got, expected any }{ { name: "abs", cmp: func(got int) int { if got < 0 { return -got } return got }, got: -1234, expected: 1234, }, { name: "int2bool", cmp: func(got int) bool { return got != 0 }, got: 1, expected: true, }, { name: "Atoi", cmp: strconv.Atoi, got: "1234", expected: 1234, }, } { tt.Run(tst.name, func(tt *testing.T) { ttt := test.NewTestingTB(tt.Name()) t := td.NewT(ttt) td.CmpFalse(tt, t.Cmp(tst.got, tst.expected)) t = t.WithSmuggleHooks(tst.cmp) td.CmpTrue(tt, t.Cmp(tst.got, tst.expected)) }) } tt.Run("Error", func(tt *testing.T) { ttt := test.NewTestingTB(tt.Name()) t := td.NewT(ttt).WithSmuggleHooks(func(got int) (int, error) { return 0, errors.New("never equal") }) td.CmpFalse(tt, t.Cmp(1, 1)) if !strings.Contains(ttt.LastMessage(), "DATA: never equal\n") { tt.Errorf(`<%s> does not contain "DATA: never equal\n"`, ttt.LastMessage()) } }) for _, tst := range []struct { name string cmp any fatal string }{ { name: "not a function", cmp: "Booh", fatal: "WithSmuggleHooks expects a function, not a string", }, { name: "wrong signature", cmp: func(a []int, b ...int) bool { return false }, fatal: "WithSmuggleHooks expects: func (A) (B[, error]) not ", }, } { tt.Run("panic: "+tst.name, func(tt *testing.T) { ttt := test.NewTestingTB(tt.Name()) t := td.NewT(ttt) fatalMesg := ttt.CatchFatal(func() { t.WithSmuggleHooks(tst.cmp) }) test.IsTrue(tt, ttt.IsFatal) if !strings.Contains(fatalMesg, tst.fatal) { tt.Errorf(`<%s> does not contain %q`, fatalMesg, tst.fatal) } }) } } golang-github-maxatome-go-testdeep-1.14.0/td/t_struct.go000066400000000000000000000633501454313311600231700ustar00rootroot00000000000000// Copyright (c) 2018, 2019, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td import ( "reflect" "strings" "sync" "testing" "github.com/maxatome/go-testdeep/helpers/tdutil" "github.com/maxatome/go-testdeep/internal/color" "github.com/maxatome/go-testdeep/internal/trace" "github.com/maxatome/go-testdeep/internal/types" ) // T is a type that encapsulates [testing.TB] interface (which is // implemented by [*testing.T] and [*testing.B]) allowing to easily use // [*testing.T] methods as well as T ones. type T struct { testing.TB Config ContextConfig // defaults to DefaultContextConfig } var _ testing.TB = T{} // NewT returns a new [*T] instance. Typically used as: // // import ( // "testing" // // "github.com/maxatome/go-testdeep/td" // ) // // type Record struct { // Id uint64 // Name string // Age int // CreatedAt time.Time // } // // func TestCreateRecord(tt *testing.T) { // t := NewT(tt, ContextConfig{ // MaxErrors: 3, // in case of failure, will dump up to 3 errors // }) // // before := time.Now() // record, err := CreateRecord() // // if t.CmpNoError(err) { // t.Log("No error, can now check struct contents") // // ok := t.Struct(record, // &Record{ // Name: "Bob", // Age: 23, // }, // td.StructFields{ // "Id": td.NotZero(), // "CreatedAt": td.Between(before, time.Now()), // }, // "Newly created record") // if ok { // t.Log(Record created successfully!") // } // } // } // // config is an optional parameter and, if passed, must be unique. It // allows to configure how failures will be rendered during the // lifetime of the returned instance. // // t := NewT(tt) // t.Cmp( // Record{Age: 12, Name: "Bob", Id: 12}, // got // Record{Age: 21, Name: "John", Id: 28}) // expected // // will produce: // // === RUN TestFoobar // --- FAIL: TestFoobar (0.00s) // foobar_test.go:88: Failed test // DATA.Id: values differ // got: (uint64) 12 // expected: (uint64) 28 // DATA.Name: values differ // got: "Bob" // expected: "John" // DATA.Age: values differ // got: 12 // expected: 28 // FAIL // // Now with a special configuration: // // t := NewT(tt, ContextConfig{ // RootName: "RECORD", // got data named "RECORD" instead of "DATA" // MaxErrors: 2, // stops after 2 errors instead of default 10 // }) // t.Cmp( // Record{Age: 12, Name: "Bob", Id: 12}, // got // Record{Age: 21, Name: "John", Id: 28}, // expected // ) // // will produce: // // === RUN TestFoobar // --- FAIL: TestFoobar (0.00s) // foobar_test.go:96: Failed test // RECORD.Id: values differ // got: (uint64) 12 // expected: (uint64) 28 // RECORD.Name: values differ // got: "Bob" // expected: "John" // Too many errors (use TESTDEEP_MAX_ERRORS=-1 to see all) // FAIL // // See [T.RootName] method to configure RootName in a more specific fashion. // // Note that setting MaxErrors to a negative value produces a dump // with all errors. // // If MaxErrors is not set (or set to 0), it is set to // DefaultContextConfig.MaxErrors which is potentially dependent from // the TESTDEEP_MAX_ERRORS environment variable (else defaults to 10.) // See [ContextConfig] documentation for details. // // Of course t can already be a [*T], in this special case if config // is omitted, the Config of the new instance is a copy of the t // Config, including hooks. func NewT(t testing.TB, config ...ContextConfig) *T { var newT T const usage = "NewT(testing.TB[, ContextConfig])" if t == nil { panic(color.BadUsage(usage, nil, 1, false)) } if len(config) > 1 { t.Helper() t.Fatal(color.TooManyParams(usage)) } // Already a *T, so steal its testing.TB and its Config if needed if tdT, ok := t.(*T); ok { newT.TB = tdT.TB if len(config) == 0 { newT.Config = tdT.Config } else { newT.Config = config[0] } } else { newT.TB = t if len(config) == 0 { newT.Config = DefaultContextConfig } else { newT.Config = config[0] } } newT.Config.sanitize() newT.initAnchors() return &newT } // Assert returns a new [*T] instance with FailureIsFatal flag set to // false. // // assert := Assert(t) // // is roughly equivalent to: // // assert := NewT(t).FailureIsFatal(false) // // See [NewT] documentation for usefulness of config optional parameter. // // See also [Require], [AssertRequire] and [T.Assert]. func Assert(t testing.TB, config ...ContextConfig) *T { return NewT(t, config...).FailureIsFatal(false) } // Require returns a new [*T] instance with FailureIsFatal flag set to // true. // // require := Require(t) // // is roughly equivalent to: // // require := NewT(t).FailureIsFatal(true) // // See [NewT] documentation for usefulness of config optional parameter. // // See also [Assert], [AssertRequire] and [T.Require]. func Require(t testing.TB, config ...ContextConfig) *T { return NewT(t, config...).FailureIsFatal() } // AssertRequire returns 2 instances of [*T]. assert with // FailureIsFatal flag set to false, and require with FailureIsFatal // flag set to true. // // assert, require := AssertRequire(t) // // is roughly equivalent to: // // assert, require := Assert(t), Require(t) // // See [NewT] documentation for usefulness of config optional parameter. // // See also [Assert] and [Require]. func AssertRequire(t testing.TB, config ...ContextConfig) (assert, require *T) { assert = Assert(t, config...) require = assert.FailureIsFatal() return } // RootName changes the name of the got data. By default it is // "DATA". For an HTTP response body, it could be "BODY" for example. // // It returns a new instance of [*T] so does not alter the original t // and is used as follows: // // t.RootName("RECORD"). // Struct(record, // &Record{ // Name: "Bob", // Age: 23, // }, // td.StructFields{ // "Id": td.NotZero(), // "CreatedAt": td.Between(before, time.Now()), // }, // "Newly created record") // // In case of error for the field Age, the failure message will contain: // // RECORD.Age: values differ // // Which is more readable than the generic: // // DATA.Age: values differ // // If "" is passed the name is set to "DATA", the default value. func (t *T) RootName(rootName string) *T { nt := *t if rootName == "" { rootName = contextDefaultRootName } nt.Config.RootName = rootName return &nt } // FailureIsFatal allows to choose whether t.TB.Fatal() or // t.TB.Error() will be used to print the next failure reports. When // enable is true (or missing) testing.Fatal() will be called, else // testing.Error(). Using [*testing.T] or [*testing.B] instance as // t.TB value, FailNow() method is called behind the scenes when // Fatal() is called. See [testing] documentation for details. // // It returns a new instance of [*T] so does not alter the original t // and used as follows: // // // Following t.Cmp() will call Fatal() if failure // t = t.FailureIsFatal() // t.Cmp(...) // t.Cmp(...) // // Following t.Cmp() won't call Fatal() if failure // t = t.FailureIsFatal(false) // t.Cmp(...) // // or, if only one call is critic: // // // This Cmp() call will call Fatal() if failure // t.FailureIsFatal().Cmp(...) // // Following t.Cmp() won't call Fatal() if failure // t.Cmp(...) // t.Cmp(...) // // Note that t.FailureIsFatal() acts as t.FailureIsFatal(true). // // See also [T.Assert] and [T.Require]. func (t *T) FailureIsFatal(enable ...bool) *T { nt := *t nt.Config.FailureIsFatal = len(enable) == 0 || enable[0] return &nt } // Assert returns a new [*T] instance inheriting the t config but with // FailureIsFatal flag set to false. // // It returns a new instance of [*T] so does not alter the original t // // It is a shortcut for: // // t.FailureIsFatal(false) // // See also [T.FailureIsFatal] and [T.Require]. func (t *T) Assert() *T { return t.FailureIsFatal(false) } // Require returns a new [*T] instance inheriting the t config but // with FailureIsFatal flag set to true. // // It returns a new instance of [*T] so does not alter the original t // // It is a shortcut for: // // t.FailureIsFatal(true) // // See also [T.FailureIsFatal] and [T.Assert]. func (t *T) Require() *T { return t.FailureIsFatal(true) } // UseEqual tells go-testdeep to delegate the comparison of items // whose type is one of types to their Equal() method. // // The signature this method should be: // // (A) Equal(B) bool // // with B assignable to A. // // See [time.Time.Equal] as an example of accepted Equal() method. // // It always returns a new instance of [*T] so does not alter the // original t. // // t = t.UseEqual(time.Time{}, net.IP{}) // // types items can also be [reflect.Type] items. In this case, the // target type is the one reflected by the [reflect.Type]. // // t = t.UseEqual(reflect.TypeOf(time.Time{}), reflect.typeOf(net.IP{})) // // As a special case, calling t.UseEqual() or t.UseEqual(true) returns // an instance using the Equal() method globally, for all types owning // an Equal() method. Other types fall back to the default comparison // mechanism. t.UseEqual(false) returns an instance not using Equal() // method anymore, except for types already recorded using a previous // UseEqual call. func (t *T) UseEqual(types ...any) *T { // special case: UseEqual() if len(types) == 0 { nt := *t nt.Config.UseEqual = true return &nt } // special cases: UseEqual(true) or UseEqual(false) if len(types) == 1 { if ignore, ok := types[0].(bool); ok { nt := *t nt.Config.UseEqual = ignore return &nt } } // Enable UseEqual only for types types t = t.copyWithHooks() err := t.Config.hooks.AddUseEqual(types) if err != nil { t.Helper() t.Fatal(color.Bad("UseEqual " + err.Error())) } return t } // BeLax allows to compare different but convertible types. If set to // false, got and expected types must be the same. If set to true and // expected type is convertible to got one, expected is first // converted to go type before its comparison. See [CmpLax] or // [T.CmpLax] and [Lax] operator to set this flag without providing a // specific configuration. // // It returns a new instance of [*T] so does not alter the original t. // // Note that t.BeLax() acts as t.BeLax(true). func (t *T) BeLax(enable ...bool) *T { nt := *t nt.Config.BeLax = len(enable) == 0 || enable[0] return &nt } // IgnoreUnexported tells go-testdeep to ignore unexported fields of // structs whose type is one of types. // // It always returns a new instance of [*T] so does not alter the original t. // // t = t.IgnoreUnexported(MyStruct1{}, MyStruct2{}) // // types items can also be [reflect.Type] items. In this case, the // target type is the one reflected by the [reflect.Type]. // // t = t.IgnoreUnexported(reflect.TypeOf(MyStruct1{})) // // As a special case, calling t.IgnoreUnexported() or // t.IgnoreUnexported(true) returns an instance ignoring unexported // fields globally, for all struct types. t.IgnoreUnexported(false) // returns an instance not ignoring unexported fields anymore, except // for types already recorded using a previous IgnoreUnexported call. func (t *T) IgnoreUnexported(types ...any) *T { // special case: IgnoreUnexported() if len(types) == 0 { nt := *t nt.Config.IgnoreUnexported = true return &nt } // special cases: IgnoreUnexported(true) or IgnoreUnexported(false) if len(types) == 1 { if ignore, ok := types[0].(bool); ok { nt := *t nt.Config.IgnoreUnexported = ignore return &nt } } // Enable IgnoreUnexported only for types types t = t.copyWithHooks() err := t.Config.hooks.AddIgnoreUnexported(types) if err != nil { t.Helper() t.Fatal(color.Bad("IgnoreUnexported " + err.Error())) } return t } // TestDeepInGotOK tells go-testdeep to not panic when a [TestDeep] // operator is found on got side. By default it is forbidden because // most of the time it is a mistake to compare (expected, got) instead // of official (got, expected). // // It returns a new instance of [*T] so does not alter the original t. // // Note that t.TestDeepInGotOK() acts as t.TestDeepInGotOK(true). func (t *T) TestDeepInGotOK(enable ...bool) *T { nt := *t nt.Config.TestDeepInGotOK = len(enable) == 0 || enable[0] return &nt } // Cmp is mostly a shortcut for: // // Cmp(t.TB, got, expected, args...) // // with the exception that t.Config is used to configure the test // [ContextConfig]. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. func (t *T) Cmp(got, expected any, args ...any) bool { t.Helper() defer t.resetNonPersistentAnchors() return cmpDeeply(newContext(t), t.TB, got, expected, args...) } // CmpDeeply works the same as [Cmp] and is still available for // compatibility purpose. Use shorter [Cmp] in new code. func (t *T) CmpDeeply(got, expected any, args ...any) bool { t.Helper() defer t.resetNonPersistentAnchors() return cmpDeeply(newContext(t), t.TB, got, expected, args...) } // True is shortcut for: // // t.Cmp(got, true, args...) // // Returns true if the test is OK, false if it fails. // // t.True(IsAvailable(x), "x should be available") // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. // // See also [T.False]. func (t *T) True(got any, args ...any) bool { t.Helper() return t.Cmp(got, true, args...) } // False is shortcut for: // // t.Cmp(got, false, args...) // // Returns true if the test is OK, false if it fails. // // t.False(IsAvailable(x), "x should not be available") // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. // // See also [T.True]. func (t *T) False(got any, args ...any) bool { t.Helper() return t.Cmp(got, false, args...) } // CmpError checks that got is non-nil error. // // _, err := MyFunction(1, 2, 3) // t.CmpError(err, "MyFunction(1, 2, 3) should return an error") // // CmpError and not Error to avoid collision with t.TB.Error method. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. // // See also [T.CmpNoError]. func (t *T) CmpError(got error, args ...any) bool { t.Helper() return cmpError(newContext(t), t.TB, got, args...) } // CmpNoError checks that got is nil error. // // value, err := MyFunction(1, 2, 3) // if t.CmpNoError(err) { // // one can now check value... // } // // CmpNoError and not NoError to be consistent with [T.CmpError] method. // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. // // See also [T.CmpError]. func (t *T) CmpNoError(got error, args ...any) bool { t.Helper() return cmpNoError(newContext(t), t.TB, got, args...) } // CmpPanic calls fn and checks a panic() occurred with the // expectedPanic parameter. It returns true only if both conditions // are fulfilled. // // Note that calling panic(nil) in fn body is always detected as a // panic. [runtime] package says: before Go 1.21, programs that called // panic(nil) observed recover returning nil. Starting in Go 1.21, // programs that call panic(nil) observe recover returning a // [*runtime.PanicNilError]. Programs can change back to the old // behavior by setting GODEBUG=panicnil=1. // // t.CmpPanic(func() { panic("I am panicking!") }, // "I am panicking!", // "The function should panic with the right string") // // t.CmpPanic(func() { panic("I am panicking!") }, // Contains("panicking!"), // "The function should panic with a string containing `panicking!`") // // t.CmpPanic(t, func() { panic(nil) }, nil, "Checks for panic(nil)") // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. // // See also [T.CmpNotPanic]. func (t *T) CmpPanic(fn func(), expected any, args ...any) bool { t.Helper() defer t.resetNonPersistentAnchors() return cmpPanic(newContext(t), t, fn, expected, args...) } // CmpNotPanic calls fn and checks no panic() occurred. If a panic() // occurred false is returned then the panic() parameter and the stack // trace appear in the test report. // // Note that calling panic(nil) in fn body is always detected as a // panic. [runtime] package says: before Go 1.21, programs that called // panic(nil) observed recover returning nil. Starting in Go 1.21, // programs that call panic(nil) observe recover returning a // [*runtime.PanicNilError]. Programs can change back to the old // behavior by setting GODEBUG=panicnil=1. // // t.CmpNotPanic(func() {}) // succeeds as function does not panic // // t.CmpNotPanic(func() { panic("I am panicking!") }) // fails // t.CmpNotPanic(func() { panic(nil) }) // fails too // // args... are optional and allow to name the test. This name is // used in case of failure to qualify the test. If len(args) > 1 and // the first item of args is a string and contains a '%' rune then // [fmt.Fprintf] is used to compose the name, else args are passed to // [fmt.Fprint]. Do not forget it is the name of the test, not the // reason of a potential failure. // // See also [T.CmpPanic]. func (t *T) CmpNotPanic(fn func(), args ...any) bool { t.Helper() return cmpNotPanic(newContext(t), t, fn, args...) } // Parallel marks this test as runnable in parallel with other // parallel tests. If t.TB implements Parallel(), as [*testing.T] // does, it is usually used to mark top-level tests and/or subtests as // safe for parallel execution: // // func TestCreateRecord(tt *testing.T) { // t := td.NewT(tt) // t.Parallel() // // t.Run("no error", func(t *td.T) { // t.Parallel() // // // ... // }) // // If t.TB does not implement Parallel(), this method is a no-op. func (t *T) Parallel() { p, ok := t.TB.(interface{ Parallel() }) if ok { p.Parallel() } } type runtFuncs struct { run reflect.Value fnt reflect.Type } var ( runtMu sync.Mutex runt = map[reflect.Type]runtFuncs{} ) func (t *T) getRunFunc() (runtFuncs, bool) { ttb := reflect.TypeOf(t.TB) runtMu.Lock() defer runtMu.Unlock() vfuncs, ok := runt[ttb] if !ok { run, ok := ttb.MethodByName("Run") if ok { mt := run.Type if mt.NumIn() == 3 && mt.NumOut() == 1 && !mt.IsVariadic() && mt.In(1) == types.String && mt.Out(0) == types.Bool { fnt := mt.In(2) if fnt.Kind() == reflect.Func && fnt.NumIn() == 1 && fnt.NumOut() == 0 && fnt.In(0) == mt.In(0) { vfuncs = runtFuncs{ run: run.Func, fnt: fnt, } runt[ttb] = vfuncs ok = true } } } if !ok { runt[ttb] = vfuncs } } return vfuncs, vfuncs != (runtFuncs{}) } // Run runs f as a subtest of t called name. // // If t.TB implement a method with the following signature: // // (X) Run(string, func(X)) bool // // it calls it with a function of its own in which it creates a new // instance of [*T] on the fly before calling f with it. // // So if t.TB is a [*testing.T] or a [*testing.B] (which is in normal // cases), let's quote the [testing.T.Run] & [testing.B.Run] // documentation: f is called in a separate goroutine and blocks // until f returns or calls t.Parallel to become a parallel // test. Run reports whether f succeeded (or at least did not fail // before calling t.Parallel). Run may be called simultaneously from // multiple goroutines, but all such calls must return before the // outer test function for t returns. // // If this Run() method is not found, it simply logs name then // executes f using a new [*T] instance in the current goroutine. Note // that it is only done for convenience. // // The t param of f inherits the configuration of the self-reference. // // See also [T.RunAssertRequire]. func (t *T) Run(name string, f func(t *T)) bool { t.Helper() vfuncs, ok := t.getRunFunc() if !ok { t = NewT(t) t.Logf("++++ %s", name) f(t) return !t.Failed() } conf := t.Config ret := vfuncs.run.Call([]reflect.Value{ reflect.ValueOf(t.TB), reflect.ValueOf(name), reflect.MakeFunc(vfuncs.fnt, func(args []reflect.Value) (results []reflect.Value) { f(NewT(args[0].Interface().(testing.TB), conf)) return nil }), }) return ret[0].Bool() } // RunAssertRequire runs f as a subtest of t called name. // // If t.TB implement a method with the following signature: // // (X) Run(string, func(X)) bool // // it calls it with a function of its own in which it creates two new // instances of [*T] using [AssertRequire] on the fly before calling f // with them. // // So if t.TB is a [*testing.T] or a [*testing.B] (which is in normal // cases), let's quote the [testing.T.Run] & [testing.B.Run] // documentation: f is called in a separate goroutine and blocks // until f returns or calls t.Parallel to become a parallel // test. Run reports whether f succeeded (or at least did not fail // before calling t.Parallel). Run may be called simultaneously from // multiple goroutines, but all such calls must return before the // outer test function for t returns. // // If this Run() method is not found, it simply logs name then // executes f using two new instances of [*T] (built with // [AssertRequire]) in the current goroutine. Note that it is only // done for convenience. // // The assert and require params of f inherit the configuration // of the self-reference, except that a failure is never fatal using // assert and always fatal using require. // // See also [T.Run]. func (t *T) RunAssertRequire(name string, f func(assert, require *T)) bool { t.Helper() vfuncs, ok := t.getRunFunc() if !ok { assert, require := AssertRequire(t) t.Logf("++++ %s", name) f(assert, require) return !t.Failed() } conf := t.Config ret := vfuncs.run.Call([]reflect.Value{ reflect.ValueOf(t.TB), reflect.ValueOf(name), reflect.MakeFunc(vfuncs.fnt, func(args []reflect.Value) (results []reflect.Value) { f(AssertRequire(NewT(args[0].Interface().(testing.TB), conf))) return nil }), }) return ret[0].Bool() } // RunT runs f as a subtest of t called name. // // Deprecated: RunT has been superseded by [T.Run] method. It is kept // for compatibility. func (t *T) RunT(name string, f func(t *T)) bool { t.Helper() return t.Run(name, f) } func getTrace(args ...any) string { var b strings.Builder tdutil.FbuildTestName(&b, args...) if b.Len() == 0 { b.WriteString("Stack trace:\n") } else if !strings.HasSuffix(b.String(), "\n") { b.WriteByte('\n') } s := stripTrace(trace.Retrieve(1, "testing.tRunner")) if len(s) == 0 { b.WriteString("\tEmpty stack trace") return b.String() } s.Dump(&b) return b.String() } // LogTrace uses t.TB.Log() to log a stack trace. // // args... are optional and allow to prefix the trace by a // message. If empty, this message defaults to "Stack trace:\n". If // this message does not end with a "\n", one is automatically // added. If len(args) > 1 and the first item of args is a string // and contains a '%' rune then [fmt.Fprintf] is used to compose the // name, else args are passed to [fmt.Fprint]. // // See also [T.ErrorTrace] and [T.FatalTrace]. func (t *T) LogTrace(args ...any) { t.Helper() t.Log(getTrace(args...)) } // ErrorTrace uses t.TB.Error() to log a stack trace. // // args... are optional and allow to prefix the trace by a // message. If empty, this message defaults to "Stack trace:\n". If // this message does not end with a "\n", one is automatically // added. If len(args) > 1 and the first item of args is a string // and contains a '%' rune then [fmt.Fprintf] is used to compose the // name, else args are passed to [fmt.Fprint]. // // See also [T.LogTrace] and [T.FatalTrace]. func (t *T) ErrorTrace(args ...any) { t.Helper() t.Error(getTrace(args...)) } // FatalTrace uses t.TB.Fatal() to log a stack trace. // // args... are optional and allow to prefix the trace by a // message. If empty, this message defaults to "Stack trace:\n". If // this message does not end with a "\n", one is automatically // added. If len(args) > 1 and the first item of args is a string // and contains a '%' rune then [fmt.Fprintf] is used to compose the // name, else args are passed to [fmt.Fprint]. // // See also [T.LogTrace] and [T.ErrorTrace]. func (t *T) FatalTrace(args ...any) { t.Helper() t.Fatal(getTrace(args...)) } golang-github-maxatome-go-testdeep-1.14.0/td/t_struct_examples_test.go000066400000000000000000000064551454313311600261300ustar00rootroot00000000000000// Copyright (c) 2018, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td_test import ( "fmt" "testing" "github.com/maxatome/go-testdeep/td" ) func ExampleT_True() { t := td.NewT(&testing.T{}) got := true ok := t.True(got, "check that got is true!") fmt.Println(ok) got = false ok = t.True(got, "check that got is true!") fmt.Println(ok) // Output: // true // false } func ExampleT_False() { t := td.NewT(&testing.T{}) got := false ok := t.False(got, "check that got is false!") fmt.Println(ok) got = true ok = t.False(got, "check that got is false!") fmt.Println(ok) // Output: // true // false } func ExampleT_CmpError() { t := td.NewT(&testing.T{}) got := fmt.Errorf("Error #%d", 42) ok := t.CmpError(got, "An error occurred") fmt.Println(ok) got = nil ok = t.CmpError(got, "An error occurred") // fails fmt.Println(ok) // Output: // true // false } func ExampleT_CmpNoError() { t := td.NewT(&testing.T{}) got := fmt.Errorf("Error #%d", 42) ok := t.CmpNoError(got, "An error occurred") // fails fmt.Println(ok) got = nil ok = t.CmpNoError(got, "An error occurred") fmt.Println(ok) // Output: // false // true } func ExampleT_CmpPanic() { t := td.NewT(&testing.T{}) ok := t.CmpPanic(func() { panic("I am panicking!") }, "I am panicking!", "Checks for panic") fmt.Println("checks exact panic() string:", ok) // Can use TestDeep operator too ok = t.CmpPanic( func() { panic("I am panicking!") }, td.Contains("panicking!"), "Checks for panic") fmt.Println("checks panic() sub-string:", ok) // Can detect panic(nil) ok = t.CmpPanic(func() { panic(nil) }, nil, "Checks for panic(nil)") fmt.Println("checks for panic(nil):", ok) // As well as structured data panic type PanicStruct struct { Error string Code int } ok = t.CmpPanic( func() { panic(PanicStruct{Error: "Memory violation", Code: 11}) }, PanicStruct{ Error: "Memory violation", Code: 11, }) fmt.Println("checks exact panic() struct:", ok) // or combined with TestDeep operators too ok = t.CmpPanic( func() { panic(PanicStruct{Error: "Memory violation", Code: 11}) }, td.Struct(PanicStruct{}, td.StructFields{ "Code": td.Between(10, 20), })) fmt.Println("checks panic() struct against TestDeep operators:", ok) // Of course, do not panic = test failure, even for expected nil // panic parameter ok = t.CmpPanic(func() {}, nil) fmt.Println("checks a panic occurred:", ok) // Output: // checks exact panic() string: true // checks panic() sub-string: true // checks for panic(nil): true // checks exact panic() struct: true // checks panic() struct against TestDeep operators: true // checks a panic occurred: false } func ExampleT_CmpNotPanic() { t := td.NewT(&testing.T{}) ok := t.CmpNotPanic(func() {}, nil) fmt.Println("checks a panic DID NOT occur:", ok) // Classic panic ok = t.CmpNotPanic(func() { panic("I am panicking!") }, "Hope it does not panic!") fmt.Println("still no panic?", ok) // Can detect panic(nil) ok = t.CmpNotPanic(func() { panic(nil) }, "Checks for panic(nil)") fmt.Println("last no panic?", ok) // Output: // checks a panic DID NOT occur: true // still no panic? false // last no panic? false } golang-github-maxatome-go-testdeep-1.14.0/td/t_struct_test.go000066400000000000000000000452101454313311600242220ustar00rootroot00000000000000// Copyright (c) 2018-2021, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td_test import ( "regexp" "strings" "sync" "testing" "time" "github.com/maxatome/go-testdeep/internal/test" "github.com/maxatome/go-testdeep/internal/trace" "github.com/maxatome/go-testdeep/td" ) func TestT(tt *testing.T) { // We don't want to include "anchors" field in comparison cmp := func(tt *testing.T, got, expected td.ContextConfig) { tt.Helper() td.Cmp(tt, got, td.SStruct(expected, td.StructFields{ "anchors": td.Ignore(), "hooks": td.Ignore(), }), ) } tt.Run("without config", func(tt *testing.T) { t := td.NewT(tt) cmp(tt, t.Config, td.DefaultContextConfig) tDup := td.NewT(t) cmp(tt, tDup.Config, td.DefaultContextConfig) }) tt.Run("explicit default config", func(tt *testing.T) { t := td.NewT(tt, td.ContextConfig{}) cmp(tt, t.Config, td.DefaultContextConfig) tDup := td.NewT(t) cmp(tt, tDup.Config, td.DefaultContextConfig) }) tt.Run("specific config", func(tt *testing.T) { conf := td.ContextConfig{ RootName: "TEST", MaxErrors: 33, } t := td.NewT(tt, conf) cmp(tt, t.Config, conf) tDup := td.NewT(t) cmp(tt, tDup.Config, conf) newConf := conf newConf.MaxErrors = 34 tDup = td.NewT(t, newConf) cmp(tt, tDup.Config, newConf) t2 := t.RootName("T2") cmp(tt, t.Config, conf) cmp(tt, t2.Config, td.ContextConfig{ RootName: "T2", MaxErrors: 33, }) t3 := t.RootName("") cmp(tt, t3.Config, td.ContextConfig{ RootName: "DATA", MaxErrors: 33, }) }) // // Bad usages ttb := test.NewTestingTB("usage params") ttb.CatchFatal(func() { td.NewT(ttb, td.ContextConfig{}, td.ContextConfig{}) }) test.IsTrue(tt, ttb.IsFatal) test.IsTrue(tt, strings.Contains(ttb.Messages[0], "usage: NewT(")) test.CheckPanic(tt, func() { td.NewT(nil) }, "usage: NewT") } func TestTCmp(tt *testing.T) { ttt := test.NewTestingTB(tt.Name()) t := td.NewT(ttt) test.IsTrue(tt, t.Cmp(1, 1)) test.IsFalse(tt, ttt.Failed()) ttt = test.NewTestingTB(tt.Name()) t = td.NewT(ttt) test.IsFalse(tt, t.Cmp(1, 2)) test.IsTrue(tt, ttt.Failed()) } func TestTCmpDeeply(tt *testing.T) { ttt := test.NewTestingTB(tt.Name()) t := td.NewT(ttt) test.IsTrue(tt, t.CmpDeeply(1, 1)) test.IsFalse(tt, ttt.Failed()) ttt = test.NewTestingTB(tt.Name()) t = td.NewT(ttt) test.IsFalse(tt, t.CmpDeeply(1, 2)) test.IsTrue(tt, ttt.Failed()) } func TestParallel(t *testing.T) { t.Run("without Parallel", func(tt *testing.T) { ttt := test.NewTestingTB(tt.Name()) t := td.NewT(ttt) t.Parallel() // has no effect }) t.Run("with Parallel", func(tt *testing.T) { ttt := test.NewParallelTestingTB(tt.Name()) t := td.NewT(ttt) t.Parallel() test.IsTrue(tt, ttt.IsParallel) }) t.Run("Run with Parallel", func(tt *testing.T) { // This test verifies that subtests with t.Parallel() are run // in parallel. We use a WaitGroup to make both subtests block // until they're both ready. This test will block forever if // the tests are not run together. var ready sync.WaitGroup ready.Add(2) t := td.NewT(tt) t.Run("level 1", func(t *td.T) { t.Parallel() ready.Done() // I'm ready. ready.Wait() // Are you? }) t.Run("level 2", func(t *td.T) { t.Parallel() ready.Done() // I'm ready. ready.Wait() // Are you? }) }) } func TestRun(t *testing.T) { t.Run("test.TB with Run", func(tt *testing.T) { t := td.NewT(tt) runPassed := false nestedFailureIsFatal := false ok := t.Run("Test level1", func(t *td.T) { ok := t.FailureIsFatal().Run("Test level2", func(t *td.T) { runPassed = t.True(true) // test succeeds! // Check we inherit config from caller nestedFailureIsFatal = t.Config.FailureIsFatal }) t.True(ok) }) test.IsTrue(tt, ok) test.IsTrue(tt, runPassed) test.IsTrue(tt, nestedFailureIsFatal) }) t.Run("test.TB without Run", func(tt *testing.T) { t := td.NewT(test.NewTestingTB("gg")) runPassed := false ok := t.Run("Test level1", func(t *td.T) { ok := t.Run("Test level2", func(t *td.T) { runPassed = t.True(true) // test succeeds! }) t.True(ok) }) t.True(ok) t.True(runPassed) }) } func TestRunAssertRequire(t *testing.T) { t.Run("test.TB with Run", func(tt *testing.T) { t := td.NewT(tt) runPassed := false assertIsFatal := true requireIsFatal := false ok := t.RunAssertRequire("Test level1", func(assert, require *td.T) { assertIsFatal = assert.Config.FailureIsFatal requireIsFatal = require.Config.FailureIsFatal ok := assert.RunAssertRequire("Test level2", func(assert, require *td.T) { runPassed = assert.True(true) // test succeeds! runPassed = runPassed && require.True(true) // test succeeds! assertIsFatal = assertIsFatal || assert.Config.FailureIsFatal requireIsFatal = requireIsFatal && require.Config.FailureIsFatal }) assert.True(ok) require.True(ok) ok = require.RunAssertRequire("Test level2", func(assert, require *td.T) { runPassed = runPassed && assert.True(true) // test succeeds! runPassed = runPassed && require.True(true) // test succeeds! assertIsFatal = assertIsFatal || assert.Config.FailureIsFatal requireIsFatal = requireIsFatal && require.Config.FailureIsFatal }) assert.True(ok) require.True(ok) }) test.IsTrue(tt, ok) test.IsTrue(tt, runPassed) test.IsFalse(tt, assertIsFatal) test.IsTrue(tt, requireIsFatal) }) t.Run("test.TB without Run", func(tt *testing.T) { t := td.NewT(test.NewTestingTB("gg")) runPassed := false assertIsFatal := true requireIsFatal := false ok := t.RunAssertRequire("Test level1", func(assert, require *td.T) { assertIsFatal = assert.Config.FailureIsFatal requireIsFatal = require.Config.FailureIsFatal ok := assert.RunAssertRequire("Test level2", func(assert, require *td.T) { runPassed = assert.True(true) // test succeeds! runPassed = runPassed && require.True(true) // test succeeds! assertIsFatal = assertIsFatal || assert.Config.FailureIsFatal requireIsFatal = requireIsFatal && require.Config.FailureIsFatal }) assert.True(ok) require.True(ok) ok = require.RunAssertRequire("Test level2", func(assert, require *td.T) { runPassed = runPassed && assert.True(true) // test succeeds! runPassed = runPassed && require.True(true) // test succeeds! assertIsFatal = assertIsFatal || assert.Config.FailureIsFatal requireIsFatal = requireIsFatal && require.Config.FailureIsFatal }) assert.True(ok) require.True(ok) }) test.IsTrue(tt, ok) test.IsTrue(tt, runPassed) test.IsFalse(tt, assertIsFatal) test.IsTrue(tt, requireIsFatal) }) } // Deprecated RunT. func TestRunT(t *testing.T) { t.Run("test.TB with Run", func(tt *testing.T) { t := td.NewT(tt) runPassed := false ok := t.RunT("Test level1", //nolint: staticcheck func(t *td.T) { ok := t.RunT("Test level2", //nolint: staticcheck func(t *td.T) { runPassed = t.True(true) // test succeeds! }) t.True(ok) }) test.IsTrue(tt, ok) test.IsTrue(tt, runPassed) }) t.Run("test.TB without Run", func(tt *testing.T) { t := td.NewT(test.NewTestingTB("gg")) runPassed := false ok := t.RunT("Test level1", //nolint: staticcheck func(t *td.T) { ok := t.RunT("Test level2", //nolint: staticcheck func(t *td.T) { runPassed = t.True(true) // test succeeds! }) t.True(ok) }) test.IsTrue(tt, ok) test.IsTrue(tt, runPassed) }) } func TestFailureIsFatal(tt *testing.T) { // All t.True(false) tests of course fail // Using default config ttt := test.NewTestingTB(tt.Name()) t := td.NewT(ttt) t.True(false) // failure test.IsTrue(tt, ttt.LastMessage() != "") test.IsFalse(tt, ttt.IsFatal, "by default it is not fatal") // Using specific config ttt = test.NewTestingTB(tt.Name()) t = td.NewT(ttt, td.ContextConfig{FailureIsFatal: true}) ttt.CatchFatal(func() { t.True(false) }) // failure test.IsTrue(tt, ttt.LastMessage() != "") test.IsTrue(tt, ttt.IsFatal, "it must be fatal") // Using FailureIsFatal() ttt = test.NewTestingTB(tt.Name()) t = td.NewT(ttt).FailureIsFatal() ttt.CatchFatal(func() { t.True(false) }) // failure test.IsTrue(tt, ttt.LastMessage() != "") test.IsTrue(tt, ttt.IsFatal, "it must be fatal") // Using FailureIsFatal(true) ttt = test.NewTestingTB(tt.Name()) t = td.NewT(ttt).FailureIsFatal(true) ttt.CatchFatal(func() { t.True(false) }) // failure test.IsTrue(tt, ttt.LastMessage() != "") test.IsTrue(tt, ttt.IsFatal, "it must be fatal") // Using T.Assert() ttt = test.NewTestingTB(tt.Name()) t = td.NewT(ttt, td.ContextConfig{FailureIsFatal: true}).Assert() t.True(false) // failure test.IsTrue(tt, ttt.LastMessage() != "") test.IsFalse(tt, ttt.IsFatal, "by default it is not fatal") // Using T.Require() ttt = test.NewTestingTB(tt.Name()) t = td.NewT(ttt).Require() ttt.CatchFatal(func() { t.True(false) }) // failure test.IsTrue(tt, ttt.LastMessage() != "") test.IsTrue(tt, ttt.IsFatal, "it must be fatal") // Using Require() ttt = test.NewTestingTB(tt.Name()) t = td.Require(ttt) ttt.CatchFatal(func() { t.True(false) }) // failure test.IsTrue(tt, ttt.LastMessage() != "") test.IsTrue(tt, ttt.IsFatal, "it must be fatal") // Using Require() with specific config (cannot override FailureIsFatal) ttt = test.NewTestingTB(tt.Name()) t = td.Require(ttt, td.ContextConfig{FailureIsFatal: false}) ttt.CatchFatal(func() { t.True(false) }) // failure test.IsTrue(tt, ttt.LastMessage() != "") test.IsTrue(tt, ttt.IsFatal, "it must be fatal") // Canceling specific config ttt = test.NewTestingTB(tt.Name()) t = td.NewT(ttt, td.ContextConfig{FailureIsFatal: false}). FailureIsFatal(false) t.True(false) // failure test.IsTrue(tt, ttt.LastMessage() != "") test.IsFalse(tt, ttt.IsFatal, "it must be not fatal") // Using Assert() ttt = test.NewTestingTB(tt.Name()) t = td.Assert(ttt) t.True(false) // failure test.IsTrue(tt, ttt.LastMessage() != "") test.IsFalse(tt, ttt.IsFatal, "it must be not fatal") // Using Assert() with specific config (cannot override FailureIsFatal) ttt = test.NewTestingTB(tt.Name()) t = td.Assert(ttt, td.ContextConfig{FailureIsFatal: true}) t.True(false) // failure test.IsTrue(tt, ttt.LastMessage() != "") test.IsFalse(tt, ttt.IsFatal, "it must be not fatal") // AssertRequire() / assert ttt = test.NewTestingTB(tt.Name()) t, _ = td.AssertRequire(ttt) t.True(false) // failure test.IsTrue(tt, ttt.LastMessage() != "") test.IsFalse(tt, ttt.IsFatal, "it must be not fatal") // Using AssertRequire() / assert with specific config (cannot // override FailureIsFatal) ttt = test.NewTestingTB(tt.Name()) t, _ = td.AssertRequire(ttt, td.ContextConfig{FailureIsFatal: true}) t.True(false) // failure test.IsTrue(tt, ttt.LastMessage() != "") test.IsFalse(tt, ttt.IsFatal, "it must be not fatal") // AssertRequire() / require ttt = test.NewTestingTB(tt.Name()) _, t = td.AssertRequire(ttt) ttt.CatchFatal(func() { t.True(false) }) // failure test.IsTrue(tt, ttt.LastMessage() != "") test.IsTrue(tt, ttt.IsFatal, "it must be fatal") // Using AssertRequire() / require with specific config (cannot // override FailureIsFatal) ttt = test.NewTestingTB(tt.Name()) _, t = td.AssertRequire(ttt, td.ContextConfig{FailureIsFatal: true}) ttt.CatchFatal(func() { t.True(false) }) // failure test.IsTrue(tt, ttt.LastMessage() != "") test.IsTrue(tt, ttt.IsFatal, "it must be fatal") } func TestUseEqual(tt *testing.T) { ttt := test.NewTestingTB(tt.Name()) var time1, time2 time.Time for { time1 = time.Now() time2 = time1.Truncate(0) if !time1.Equal(time2) { tt.Fatal("time.Equal() does not work as expected") } if time1 != time2 { // to avoid the bad luck case where time1.wall=0 break } } // Using default config t := td.NewT(ttt) test.IsFalse(tt, t.Cmp(time1, time2)) // UseEqual t = td.NewT(ttt).UseEqual() // enable globally test.IsTrue(tt, t.Cmp(time1, time2)) t = td.NewT(ttt).UseEqual(true) // enable globally test.IsTrue(tt, t.Cmp(time1, time2)) t = td.NewT(ttt).UseEqual(false) // disable globally test.IsFalse(tt, t.Cmp(time1, time2)) t = td.NewT(ttt).UseEqual(time.Time{}) // enable only for time.Time test.IsTrue(tt, t.Cmp(time1, time2)) t = t.UseEqual().UseEqual(false) // enable then disable globally test.IsTrue(tt, t.Cmp(time1, time2)) // Equal() still used test.EqualStr(tt, ttt.CatchFatal(func() { td.NewT(ttt).UseEqual(42) }), "UseEqual expects type int owns an Equal method (@0)") } func TestBeLax(tt *testing.T) { ttt := test.NewTestingTB(tt.Name()) // Using default config t := td.NewT(ttt) test.IsFalse(tt, t.Cmp(int64(123), 123)) // BeLax t = td.NewT(ttt).BeLax() test.IsTrue(tt, t.Cmp(int64(123), 123)) t = td.NewT(ttt).BeLax(true) test.IsTrue(tt, t.Cmp(int64(123), 123)) t = td.NewT(ttt).BeLax(false) test.IsFalse(tt, t.Cmp(int64(123), 123)) } func TestIgnoreUnexported(tt *testing.T) { ttt := test.NewTestingTB(tt.Name()) type SType1 struct { Public int private string } a1, b1 := SType1{Public: 42, private: "test"}, SType1{Public: 42} type SType2 struct { Public int private string } a2, b2 := SType2{Public: 42, private: "test"}, SType2{Public: 42} // Using default config t := td.NewT(ttt) test.IsFalse(tt, t.Cmp(a1, b1)) // IgnoreUnexported t = td.NewT(ttt).IgnoreUnexported() // ignore unexported globally test.IsTrue(tt, t.Cmp(a1, b1)) test.IsTrue(tt, t.Cmp(a2, b2)) t = td.NewT(ttt).IgnoreUnexported(true) // ignore unexported globally test.IsTrue(tt, t.Cmp(a1, b1)) test.IsTrue(tt, t.Cmp(a2, b2)) t = td.NewT(ttt).IgnoreUnexported(false) // handle unexported globally test.IsFalse(tt, t.Cmp(a1, b1)) test.IsFalse(tt, t.Cmp(a2, b2)) t = td.NewT(ttt).IgnoreUnexported(SType1{}) // ignore only for SType1 test.IsTrue(tt, t.Cmp(a1, b1)) test.IsFalse(tt, t.Cmp(a2, b2)) t = t.UseEqual().UseEqual(false) // enable then disable globally test.IsTrue(tt, t.Cmp(a1, b1)) test.IsFalse(tt, t.Cmp(a2, b2)) t = td.NewT(ttt).IgnoreUnexported(SType1{}, SType2{}) // enable for both test.IsTrue(tt, t.Cmp(a1, b1)) test.IsTrue(tt, t.Cmp(a2, b2)) test.EqualStr(tt, ttt.CatchFatal(func() { td.NewT(ttt).IgnoreUnexported(42) }), "IgnoreUnexported expects type int be a struct, not a int (@0)") } func TestTestDeepInGotOK(tt *testing.T) { ttt := test.NewTestingTB(tt.Name()) var t *td.T cmp := func() bool { return t.Cmp(td.Ignore(), td.Ignore()) } // Using default config t = td.NewT(ttt) test.CheckPanic(tt, func() { cmp() }, "Found a TestDeep operator in got param, can only use it in expected one!") t = td.NewT(ttt).TestDeepInGotOK() test.IsTrue(tt, cmp()) t = t.TestDeepInGotOK(false) test.CheckPanic(tt, func() { cmp() }, "Found a TestDeep operator in got param, can only use it in expected one!") t = t.TestDeepInGotOK(true) test.IsTrue(tt, cmp()) } func TestLogTrace(tt *testing.T) { ttt := test.NewTestingTB(tt.Name()) t := td.NewT(ttt) //line /t_struct_test.go:100 t.LogTrace() test.EqualStr(tt, ttt.LastMessage(), `Stack trace: TestLogTrace() /t_struct_test.go:100`) test.IsFalse(tt, ttt.HasFailed) test.IsFalse(tt, ttt.IsFatal) ttt.ResetMessages() //line /t_struct_test.go:110 t.LogTrace("This is the %s:", "stack") test.EqualStr(tt, ttt.LastMessage(), `This is the stack: TestLogTrace() /t_struct_test.go:110`) ttt.ResetMessages() //line /t_struct_test.go:120 t.LogTrace("This is the %s:\n", "stack") test.EqualStr(tt, ttt.LastMessage(), `This is the stack: TestLogTrace() /t_struct_test.go:120`) ttt.ResetMessages() //line /t_struct_test.go:130 t.LogTrace("This is the ", "stack") test.EqualStr(tt, ttt.LastMessage(), `This is the stack TestLogTrace() /t_struct_test.go:130`) ttt.ResetMessages() trace.IgnorePackage() defer trace.UnignorePackage() //line /t_struct_test.go:140 t.LogTrace("Stack:\n") test.EqualStr(tt, ttt.LastMessage(), `Stack: Empty stack trace`) } func TestErrorTrace(tt *testing.T) { ttt := test.NewTestingTB(tt.Name()) t := td.NewT(ttt) //line /t_struct_test.go:200 t.ErrorTrace() test.EqualStr(tt, ttt.LastMessage(), `Stack trace: TestErrorTrace() /t_struct_test.go:200`) test.IsTrue(tt, ttt.HasFailed) test.IsFalse(tt, ttt.IsFatal) ttt.ResetMessages() //line /t_struct_test.go:210 t.ErrorTrace("This is the %s:", "stack") test.EqualStr(tt, ttt.LastMessage(), `This is the stack: TestErrorTrace() /t_struct_test.go:210`) ttt.ResetMessages() //line /t_struct_test.go:220 t.ErrorTrace("This is the %s:\n", "stack") test.EqualStr(tt, ttt.LastMessage(), `This is the stack: TestErrorTrace() /t_struct_test.go:220`) ttt.ResetMessages() //line /t_struct_test.go:230 t.ErrorTrace("This is the ", "stack") test.EqualStr(tt, ttt.LastMessage(), `This is the stack TestErrorTrace() /t_struct_test.go:230`) ttt.ResetMessages() trace.IgnorePackage() defer trace.UnignorePackage() //line /t_struct_test.go:240 t.ErrorTrace("Stack:\n") test.EqualStr(tt, ttt.LastMessage(), `Stack: Empty stack trace`) } func TestFatalTrace(tt *testing.T) { ttt := test.NewTestingTB(tt.Name()) t := td.NewT(ttt) match := func(got, expectedRe string) { tt.Helper() re := regexp.MustCompile(expectedRe) if !re.MatchString(got) { test.EqualErrorMessage(tt, got, expectedRe) } } //line /t_struct_test.go:300 match(ttt.CatchFatal(func() { t.FatalTrace() }), `Stack trace: TestFatalTrace\.func\d\(\) /t_struct_test\.go:300 \(\*TestingT\)\.CatchFatal\(\) internal/test/types\.go:\d+ TestFatalTrace\(\) /t_struct_test\.go:300`) test.IsTrue(tt, ttt.HasFailed) test.IsTrue(tt, ttt.IsFatal) ttt.ResetMessages() //line /t_struct_test.go:310 match(ttt.CatchFatal(func() { t.FatalTrace("This is the %s:", "stack") }), `This is the stack: TestFatalTrace\.func\d\(\) /t_struct_test\.go:310 \(\*TestingT\)\.CatchFatal\(\) internal/test/types\.go:\d+ TestFatalTrace\(\) /t_struct_test\.go:310`) ttt.ResetMessages() //line /t_struct_test.go:320 match(ttt.CatchFatal(func() { t.FatalTrace("This is the %s:\n", "stack") }), `This is the stack: TestFatalTrace\.func\d\(\) /t_struct_test\.go:320 \(\*TestingT\)\.CatchFatal\(\) internal/test/types\.go:\d+ TestFatalTrace\(\) /t_struct_test\.go:320`) ttt.ResetMessages() //line /t_struct_test.go:330 match(ttt.CatchFatal(func() { t.FatalTrace("This is the ", "stack") }), `This is the stack TestFatalTrace\.func\d\(\) /t_struct_test\.go:330 \(\*TestingT\)\.CatchFatal\(\) internal/test/types\.go:\d+ TestFatalTrace\(\) /t_struct_test\.go:330`) ttt.ResetMessages() trace.IgnorePackage() defer trace.UnignorePackage() //line /t_struct_test.go:340 test.EqualStr(tt, ttt.CatchFatal(func() { t.FatalTrace("Stack:\n") }), `Stack: Empty stack trace`) } golang-github-maxatome-go-testdeep-1.14.0/td/td_all.go000066400000000000000000000051271454313311600225560ustar00rootroot00000000000000// Copyright (c) 2018, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td import ( "fmt" "reflect" "github.com/maxatome/go-testdeep/internal/ctxerr" ) type tdAll struct { tdList } var _ TestDeep = &tdAll{} // summary(All): all expected values have to match // input(All): all // All operator compares data against several expected values. During // a match, all of them have to match to succeed. Consider it // as a "AND" logical operator. // // td.Cmp(t, "foobar", td.All( // td.Len(6), // td.HasPrefix("fo"), // td.HasSuffix("ar"), // )) // succeeds // // Note [Flatten] function can be used to group or reuse some values or // operators and so avoid boring and inefficient copies: // // stringOps := td.Flatten([]td.TestDeep{td.HasPrefix("fo"), td.HasSuffix("ar")}) // td.Cmp(t, "foobar", td.All( // td.Len(6), // stringOps, // )) // succeeds // // One can do the same with All operator itself: // // stringOps := td.All(td.HasPrefix("fo"), td.HasSuffix("ar")) // td.Cmp(t, "foobar", td.All( // td.Len(6), // stringOps, // )) // succeeds // // but if an error occurs in the nested All, the report is a bit more // complex to read due to the nested level. [Flatten] does not create // a new level, its slice is just flattened in the All parameters. // // TypeBehind method can return a non-nil [reflect.Type] if all items // known non-interface types are equal, or if only interface types // are found (mostly issued from [Isa]) and they are equal. // // See also [Any] and [None]. func All(expectedValues ...any) TestDeep { return &tdAll{ tdList: newList(expectedValues...), } } func (a *tdAll) Match(ctx ctxerr.Context, got reflect.Value) (err *ctxerr.Error) { var origErr *ctxerr.Error for idx, item := range a.items { // Use deepValueEqualFinal here instead of deepValueEqual as we // want to know whether an error occurred or not, we do not want // to accumulate it silently origErr = deepValueEqualFinal( ctx.ResetErrors(). AddCustomLevel(fmt.Sprintf("", idx+1, len(a.items))), got, item) if origErr != nil { if ctx.BooleanError { return ctxerr.BooleanError } err := &ctxerr.Error{ Message: fmt.Sprintf("compared (part %d of %d)", idx+1, len(a.items)), Got: got, Expected: item, } if item.IsValid() && item.Type().Implements(testDeeper) { err.Origin = origErr } return ctx.CollectError(err) } } return nil } func (a *tdAll) TypeBehind() reflect.Type { return uniqTypeBehindSlice(a.items) } golang-github-maxatome-go-testdeep-1.14.0/td/td_all_test.go000066400000000000000000000047531454313311600236210ustar00rootroot00000000000000// Copyright (c) 2018, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td_test import ( "fmt" "testing" "github.com/maxatome/go-testdeep/internal/test" "github.com/maxatome/go-testdeep/td" ) func TestAll(t *testing.T) { checkOK(t, 6, td.All(6, 6, 6)) checkOK(t, nil, td.All(nil, nil, nil)) checkError(t, 6, td.All(6, 5, 6), expectedError{ Message: mustBe("compared (part 2 of 3)"), Path: mustBe("DATA"), Got: mustBe("6"), Expected: mustBe("5"), }) checkError(t, 6, td.All(6, nil, 6), expectedError{ Message: mustBe("compared (part 2 of 3)"), Path: mustBe("DATA"), Got: mustBe("6"), Expected: mustBe("nil"), }) checkError(t, nil, td.All(nil, 5, nil), expectedError{ Message: mustBe("compared (part 2 of 3)"), Path: mustBe("DATA"), Got: mustBe("nil"), Expected: mustBe("5"), }) checkError(t, 6, td.All( 6, td.All(td.Between(3, 8), td.Between(4, 5)), 6), expectedError{ Message: mustBe("compared (part 2 of 3)"), Path: mustBe("DATA"), Got: mustBe("6"), Expected: mustBe("All(3 ≤ got ≤ 8,\n 4 ≤ got ≤ 5)"), Origin: &expectedError{ Message: mustBe("compared (part 2 of 2)"), Path: mustBe("DATA"), Got: mustBe("6"), Expected: mustBe("4 ≤ got ≤ 5"), Origin: &expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA"), Got: mustBe("6"), Expected: mustBe("4 ≤ got ≤ 5"), }, }, }) // // String test.EqualStr(t, td.All(6).String(), "All(6)") test.EqualStr(t, td.All(6, 7).String(), "All(6,\n 7)") } func TestAllTypeBehind(t *testing.T) { equalTypes(t, td.All(6, nil), nil) equalTypes(t, td.All(6, "toto"), nil) equalTypes(t, td.All(6, td.Zero(), 7, 8), 26) // Always the same non-interface type (even if we encounter several // interface types) equalTypes(t, td.All( td.Empty(), 5, td.Isa((*error)(nil)), // interface type (in fact pointer to ...) td.All(6, 7), td.Isa((*fmt.Stringer)(nil)), // interface type 8), 42) // Only one interface type equalTypes(t, td.All( td.Isa((*error)(nil)), td.Isa((*error)(nil)), td.Isa((*error)(nil)), ), (*error)(nil)) // Several interface types, cannot be sure equalTypes(t, td.All( td.Isa((*error)(nil)), td.Isa((*fmt.Stringer)(nil)), ), nil) } golang-github-maxatome-go-testdeep-1.14.0/td/td_any.go000066400000000000000000000034771454313311600226030ustar00rootroot00000000000000// Copyright (c) 2018, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td import ( "reflect" "github.com/maxatome/go-testdeep/internal/ctxerr" ) type tdAny struct { tdList } var _ TestDeep = &tdAny{} // summary(Any): at least one expected value have to match // input(Any): all // Any operator compares data against several expected values. During // a match, at least one of them has to match to succeed. Consider it // as a "OR" logical operator. // // td.Cmp(t, "foo", td.Any("bar", "foo", "zip")) // succeeds // td.Cmp(t, "foo", td.Any( // td.Len(4), // td.HasPrefix("f"), // td.HasSuffix("z"), // )) // succeeds coz "f" prefix // // Note [Flatten] function can be used to group or reuse some values or // operators and so avoid boring and inefficient copies: // // stringOps := td.Flatten([]td.TestDeep{td.HasPrefix("f"), td.HasSuffix("z")}) // td.Cmp(t, "foobar", td.All( // td.Len(4), // stringOps, // )) // succeeds coz "f" prefix // // TypeBehind method can return a non-nil [reflect.Type] if all items // known non-interface types are equal, or if only interface types // are found (mostly issued from Isa()) and they are equal. // // See also [All] and [None]. func Any(expectedValues ...any) TestDeep { return &tdAny{ tdList: newList(expectedValues...), } } func (a *tdAny) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error { for _, item := range a.items { if deepValueEqualFinalOK(ctx, got, item) { return nil } } if ctx.BooleanError { return ctxerr.BooleanError } return ctx.CollectError(&ctxerr.Error{ Message: "comparing with Any", Got: got, Expected: a, }) } func (a *tdAny) TypeBehind() reflect.Type { return uniqTypeBehindSlice(a.items) } golang-github-maxatome-go-testdeep-1.14.0/td/td_any_test.go000066400000000000000000000040071454313311600236300ustar00rootroot00000000000000// Copyright (c) 2018, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td_test import ( "fmt" "testing" "github.com/maxatome/go-testdeep/internal/test" "github.com/maxatome/go-testdeep/td" ) func TestAny(t *testing.T) { checkOK(t, 6, td.Any(nil, 5, 6, 7)) checkOK(t, nil, td.Any(5, 6, 7, nil)) checkError(t, 6, td.Any(5), expectedError{ Message: mustBe("comparing with Any"), Path: mustBe("DATA"), Got: mustBe("6"), Expected: mustBe("Any(5)"), }) checkError(t, 6, td.Any(nil), expectedError{ Message: mustBe("comparing with Any"), Path: mustBe("DATA"), Got: mustBe("6"), Expected: mustBe("Any(nil)"), }) checkError(t, nil, td.Any(6), expectedError{ Message: mustBe("comparing with Any"), Path: mustBe("DATA"), Got: mustBe("nil"), Expected: mustBe("Any(6)"), }) // Lax checkOK(t, float64(123), td.Lax(td.Any(122, 123, 124))) // // String test.EqualStr(t, td.Any(6).String(), "Any(6)") test.EqualStr(t, td.Any(6, 7).String(), "Any(6,\n 7)") } func TestAnyTypeBehind(t *testing.T) { equalTypes(t, td.Any(6, nil), nil) equalTypes(t, td.Any(6, "toto"), nil) equalTypes(t, td.Any(6, td.Zero(), 7, 8), 26) // Always the same non-interface type (even if we encounter several // interface types) equalTypes(t, td.Any( td.Empty(), 5, td.Isa((*error)(nil)), // interface type (in fact pointer to ...) td.Any(6, 7), td.Isa((*fmt.Stringer)(nil)), // interface type 8), 42) // Only one interface type equalTypes(t, td.Any( td.Isa((*error)(nil)), td.Isa((*error)(nil)), td.Isa((*error)(nil)), ), (*error)(nil)) // Several interface types, cannot be sure equalTypes(t, td.Any( td.Isa((*error)(nil)), td.Isa((*fmt.Stringer)(nil)), ), nil) equalTypes(t, td.Any( td.Code(func(x any) bool { return true }), td.Code(func(y int) bool { return true }), ), 12) } golang-github-maxatome-go-testdeep-1.14.0/td/td_array.go000066400000000000000000000300721454313311600231210ustar00rootroot00000000000000// Copyright (c) 2018-2021, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td import ( "bytes" "fmt" "reflect" "sort" "github.com/maxatome/go-testdeep/internal/ctxerr" "github.com/maxatome/go-testdeep/internal/types" "github.com/maxatome/go-testdeep/internal/util" ) type tdArray struct { tdExpectedType expectedEntries []reflect.Value onlyIndexes []int // only used by SuperSliceOf, nil otherwise } var _ TestDeep = &tdArray{} // ArrayEntries allows to pass array or slice entries to check in // functions [Array], [Slice] and [SuperSliceOf]. It is a map whose // each key is the item index and the corresponding value the expected // item value (which can be a [TestDeep] operator as well as a zero // value). type ArrayEntries map[int]any const ( arrayArray uint = iota arraySlice arraySuper ) func newArray(kind uint, model any, expectedEntries ArrayEntries) *tdArray { vmodel := reflect.ValueOf(model) a := tdArray{ tdExpectedType: tdExpectedType{ base: newBase(4), }, } if kind == arraySuper { a.onlyIndexes = make([]int, 0, len(expectedEntries)) } kindIsOK := func(k reflect.Kind) bool { switch kind { case arrayArray: return k == reflect.Array case arraySlice: return k == reflect.Slice default: // arraySuper return k == reflect.Slice || k == reflect.Array } } switch vk := vmodel.Kind(); { case vk == reflect.Ptr: if !kindIsOK(vmodel.Type().Elem().Kind()) { break } a.isPtr = true if vmodel.IsNil() { a.expectedType = vmodel.Type().Elem() a.populateExpectedEntries(expectedEntries, reflect.Value{}) return &a } vmodel = vmodel.Elem() fallthrough case kindIsOK(vk): a.expectedType = vmodel.Type() a.populateExpectedEntries(expectedEntries, vmodel) return &a } switch kind { case arrayArray: a.err = ctxerr.OpBadUsage("Array", "(ARRAY|&ARRAY, EXPECTED_ENTRIES)", model, 1, true) case arraySlice: a.err = ctxerr.OpBadUsage("Slice", "(SLICE|&SLICE, EXPECTED_ENTRIES)", model, 1, true) default: // arraySuper a.err = ctxerr.OpBadUsage("SuperSliceOf", "(ARRAY|&ARRAY|SLICE|&SLICE, EXPECTED_ENTRIES)", model, 1, true) } return &a } // summary(Array): compares the contents of an array or a pointer on an array // input(Array): array,ptr(ptr on array) // Array operator compares the contents of an array or a pointer on an // array against the values of model and the values of // expectedEntries. Entries with zero values of model are ignored // if the same entry is present in expectedEntries, otherwise they // are taken into account. An entry cannot be present in both model // and expectedEntries, except if it is a zero-value in model. At // the end, all entries are checked. To check only some entries of an // array, see [SuperSliceOf] operator. // // model must be the same type as compared data. // // expectedEntries can be nil, if no zero entries are expected and // no [TestDeep] operators are involved. // // got := [3]int{12, 14, 17} // td.Cmp(t, got, td.Array([3]int{0, 14}, td.ArrayEntries{0: 12, 2: 17})) // succeeds // td.Cmp(t, &got, // td.Array(&[3]int{0, 14}, td.ArrayEntries{0: td.Gt(10), 2: td.Gt(15)})) // succeeds // // TypeBehind method returns the [reflect.Type] of model. // // See also [Slice] and [SuperSliceOf]. func Array(model any, expectedEntries ArrayEntries) TestDeep { return newArray(arrayArray, model, expectedEntries) } // summary(Slice): compares the contents of a slice or a pointer on a slice // input(Slice): slice,ptr(ptr on slice) // Slice operator compares the contents of a slice or a pointer on a // slice against the values of model and the values of // expectedEntries. Entries with zero values of model are ignored // if the same entry is present in expectedEntries, otherwise they // are taken into account. An entry cannot be present in both model // and expectedEntries, except if it is a zero-value in model. At // the end, all entries are checked. To check only some entries of a // slice, see [SuperSliceOf] operator. // // model must be the same type as compared data. // // expectedEntries can be nil, if no zero entries are expected and // no [TestDeep] operators are involved. // // got := []int{12, 14, 17} // td.Cmp(t, got, td.Slice([]int{0, 14}, td.ArrayEntries{0: 12, 2: 17})) // succeeds // td.Cmp(t, &got, // td.Slice(&[]int{0, 14}, td.ArrayEntries{0: td.Gt(10), 2: td.Gt(15)})) // succeeds // // TypeBehind method returns the [reflect.Type] of model. // // See also [Array] and [SuperSliceOf]. func Slice(model any, expectedEntries ArrayEntries) TestDeep { return newArray(arraySlice, model, expectedEntries) } // summary(SuperSliceOf): compares the contents of a slice, a pointer // on a slice, an array or a pointer on an array but with potentially // some extra entries // input(SuperSliceOf): array,slice,ptr(ptr on array/slice) // SuperSliceOf operator compares the contents of an array, a pointer // on an array, a slice or a pointer on a slice against the non-zero // values of model (if any) and the values of expectedEntries. So // entries with zero value of model are always ignored. If a zero // value check is needed, this zero value has to be set in // expectedEntries. An entry cannot be present in both model and // expectedEntries, except if it is a zero-value in model. At the // end, only entries present in expectedEntries and non-zero ones // present in model are checked. To check all entries of an array // see [Array] operator. To check all entries of a slice see [Slice] // operator. // // model must be the same type as compared data. // // expectedEntries can be nil, if no zero entries are expected and // no [TestDeep] operators are involved. // // Works with slices: // // got := []int{12, 14, 17} // td.Cmp(t, got, td.SuperSliceOf([]int{12}, nil)) // succeeds // td.Cmp(t, got, td.SuperSliceOf([]int{12}, td.ArrayEntries{2: 17})) // succeeds // td.Cmp(t, &got, td.SuperSliceOf(&[]int{0, 14}, td.ArrayEntries{2: td.Gt(16)})) // succeeds // // and arrays: // // got := [5]int{12, 14, 17, 26, 56} // td.Cmp(t, got, td.SuperSliceOf([5]int{12}, nil)) // succeeds // td.Cmp(t, got, td.SuperSliceOf([5]int{12}, td.ArrayEntries{2: 17})) // succeeds // td.Cmp(t, &got, td.SuperSliceOf(&[5]int{0, 14}, td.ArrayEntries{2: td.Gt(16)})) // succeeds // // See also [Array] and [Slice]. func SuperSliceOf(model any, expectedEntries ArrayEntries) TestDeep { return newArray(arraySuper, model, expectedEntries) } func (a *tdArray) populateExpectedEntries(expectedEntries ArrayEntries, expectedModel reflect.Value) { // Compute highest expected index maxExpectedIdx := -1 for index := range expectedEntries { if index > maxExpectedIdx { maxExpectedIdx = index } } var numEntries int array := a.expectedType.Kind() == reflect.Array if array { numEntries = a.expectedType.Len() if numEntries <= maxExpectedIdx { a.err = ctxerr.OpBad( a.GetLocation().Func, "array length is %d, so cannot have #%d expected index", numEntries, maxExpectedIdx) return } } else { numEntries = maxExpectedIdx + 1 // If slice is non-nil if expectedModel.IsValid() { if numEntries < expectedModel.Len() { numEntries = expectedModel.Len() } } } a.expectedEntries = make([]reflect.Value, numEntries) elemType := a.expectedType.Elem() var vexpectedValue reflect.Value for index, expectedValue := range expectedEntries { if expectedValue == nil { switch elemType.Kind() { case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice: vexpectedValue = reflect.Zero(elemType) // change to a typed nil default: a.err = ctxerr.OpBad( a.GetLocation().Func, "expected value of #%d cannot be nil as items type is %s", index, elemType) return } } else { vexpectedValue = reflect.ValueOf(expectedValue) if _, ok := expectedValue.(TestDeep); !ok { if !vexpectedValue.Type().AssignableTo(elemType) { a.err = ctxerr.OpBad( a.GetLocation().Func, "type %s of #%d expected value differs from %s contents (%s)", vexpectedValue.Type(), index, util.TernStr(array, "array", "slice"), elemType) return } } } a.expectedEntries[index] = vexpectedValue // SuperSliceOf if a.onlyIndexes != nil { a.onlyIndexes = append(a.onlyIndexes, index) } } vzero := reflect.Zero(elemType) // Check initialized entries in model if expectedModel.IsValid() { zero := vzero.Interface() for index := expectedModel.Len() - 1; index >= 0; index-- { ventry := expectedModel.Index(index) modelIsZero := reflect.DeepEqual(zero, ventry.Interface()) // Entry already expected if _, ok := expectedEntries[index]; ok { // If non-zero entry, consider it as an error (= 2 expected // values for the same item) if !modelIsZero { a.err = ctxerr.OpBad( a.GetLocation().Func, "non zero #%d entry in model already exists in expectedEntries", index) return } continue } // Expect this entry except if not SuperSliceOf || not zero entry if a.onlyIndexes == nil || !modelIsZero { a.expectedEntries[index] = ventry // SuperSliceOf if a.onlyIndexes != nil { a.onlyIndexes = append(a.onlyIndexes, index) } } } } else if a.expectedType.Kind() == reflect.Slice { sort.Ints(a.onlyIndexes) // nil slice return } // For SuperSliceOf, we don't want to initialize missing entries if a.onlyIndexes != nil { sort.Ints(a.onlyIndexes) return } var index int // Array case, all is OK if array { // Non-nil array => a.expectedEntries already fully initialized if expectedModel.IsValid() { return } // nil array => a.expectedEntries must be initialized from index=0 // to numEntries - 1 below } else { // Non-nil slice => a.expectedEntries must be initialized from // index=len(slice) to last entry index of expectedEntries index = expectedModel.Len() } // Slice case, initialize missing expected items to zero for ; index < numEntries; index++ { if _, ok := expectedEntries[index]; !ok { a.expectedEntries[index] = vzero } } } func (a *tdArray) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error { if a.err != nil { return ctx.CollectError(a.err) } err := a.checkPtr(ctx, &got, true) if err != nil { return ctx.CollectError(err) } err = a.checkType(ctx, got) if err != nil { return ctx.CollectError(err) } gotLen := got.Len() check := func(index int, expectedValue reflect.Value) *ctxerr.Error { curCtx := ctx.AddArrayIndex(index) if index >= gotLen { if curCtx.BooleanError { return ctxerr.BooleanError } return curCtx.CollectError(&ctxerr.Error{ Message: "expected value out of range", Got: types.RawString(""), Expected: expectedValue, }) } return deepValueEqual(curCtx, got.Index(index), expectedValue) } // SuperSliceOf, only check some indexes if a.onlyIndexes != nil { for _, index := range a.onlyIndexes { err = check(index, a.expectedEntries[index]) if err != nil { return err } } return nil } // Array or Slice for index, expectedValue := range a.expectedEntries { err = check(index, expectedValue) if err != nil { return err } } if gotLen > len(a.expectedEntries) { if ctx.BooleanError { return ctxerr.BooleanError } return ctx.AddArrayIndex(len(a.expectedEntries)). CollectError(&ctxerr.Error{ Message: "got value out of range", Got: got.Index(len(a.expectedEntries)), Expected: types.RawString(""), }) } return nil } func (a *tdArray) String() string { if a.err != nil { return a.stringError() } buf := bytes.NewBufferString(a.GetLocation().Func) buf.WriteByte('(') buf.WriteString(a.expectedTypeStr()) if len(a.expectedEntries) == 0 { buf.WriteString("{})") } else { buf.WriteString("{\n") for index, expectedValue := range a.expectedEntries { fmt.Fprintf(buf, " %d: %s\n", //nolint: errcheck index, util.ToString(expectedValue)) } buf.WriteString("})") } return buf.String() } golang-github-maxatome-go-testdeep-1.14.0/td/td_array_each.go000066400000000000000000000053511454313311600241030ustar00rootroot00000000000000// Copyright (c) 2018, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td import ( "reflect" "strings" "github.com/maxatome/go-testdeep/internal/ctxerr" "github.com/maxatome/go-testdeep/internal/types" "github.com/maxatome/go-testdeep/internal/util" ) type tdArrayEach struct { baseOKNil expected reflect.Value } var _ TestDeep = &tdArrayEach{} // summary(ArrayEach): compares each array or slice item // input(ArrayEach): array,slice,ptr(ptr on array/slice) // ArrayEach operator has to be applied on arrays or slices or on // pointers on array/slice. It compares each item of data array/slice // against expectedValue. During a match, all items have to match to // succeed. // // got := [3]string{"foo", "bar", "biz"} // td.Cmp(t, got, td.ArrayEach(td.Len(3))) // succeeds // td.Cmp(t, got, td.ArrayEach(td.HasPrefix("b"))) // fails coz "foo" // // Works on slices as well: // // got := []Person{ // {Name: "Bob", Age: 42}, // {Name: "Alice", Age: 24}, // } // td.Cmp(t, got, td.ArrayEach( // td.Struct(Person{}, td.StructFields{ // Age: td.Between(20, 45), // })), // ) // succeeds, each Person has Age field between 20 and 45 func ArrayEach(expectedValue any) TestDeep { return &tdArrayEach{ baseOKNil: newBaseOKNil(3), expected: reflect.ValueOf(expectedValue), } } func (a *tdArrayEach) Match(ctx ctxerr.Context, got reflect.Value) (err *ctxerr.Error) { if !got.IsValid() { if ctx.BooleanError { return ctxerr.BooleanError } return ctx.CollectError(&ctxerr.Error{ Message: "nil value", Got: types.RawString("nil"), Expected: types.RawString("slice OR array OR *slice OR *array"), }) } switch got.Kind() { case reflect.Ptr: gotElem := got.Elem() if !gotElem.IsValid() { if ctx.BooleanError { return ctxerr.BooleanError } return ctx.CollectError(ctxerr.NilPointer(got, "non-nil *slice OR *array")) } if gotElem.Kind() != reflect.Array && gotElem.Kind() != reflect.Slice { break } got = gotElem fallthrough case reflect.Array, reflect.Slice: gotLen := got.Len() var err *ctxerr.Error for idx := 0; idx < gotLen; idx++ { err = deepValueEqual(ctx.AddArrayIndex(idx), got.Index(idx), a.expected) if err != nil { return err } } return nil } if ctx.BooleanError { return ctxerr.BooleanError } return ctx.CollectError(ctxerr.BadKind(got, "slice OR array OR *slice OR *array")) } func (a *tdArrayEach) String() string { const prefix = "ArrayEach(" content := util.ToString(a.expected) if strings.Contains(content, "\n") { return prefix + util.IndentString(content, " ") + ")" } return prefix + content + ")" } golang-github-maxatome-go-testdeep-1.14.0/td/td_array_each_test.go000066400000000000000000000060161454313311600251410ustar00rootroot00000000000000// Copyright (c) 2018, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td_test import ( "testing" "github.com/maxatome/go-testdeep/internal/test" "github.com/maxatome/go-testdeep/td" ) func TestArrayEach(t *testing.T) { type MyArray [3]int type MyEmptyArray [0]int type MySlice []int checkOKForEach(t, []any{ [...]int{4, 4, 4}, []int{4, 4, 4}, &[...]int{4, 4, 4}, &[]int{4, 4, 4}, MyArray{4, 4, 4}, MySlice{4, 4, 4}, &MyArray{4, 4, 4}, &MySlice{4, 4, 4}, }, td.ArrayEach(4)) // Empty slice/array checkOKForEach(t, []any{ [0]int{}, []int{}, &[0]int{}, &[]int{}, MyEmptyArray{}, MySlice{}, &MyEmptyArray{}, &MySlice{}, // nil cases ([]int)(nil), MySlice(nil), }, td.ArrayEach(4)) checkError(t, (*MyArray)(nil), td.ArrayEach(4), expectedError{ Message: mustBe("nil pointer"), Path: mustBe("DATA"), Got: mustBe("nil *array (*td_test.MyArray type)"), Expected: mustBe("non-nil *slice OR *array"), }) checkError(t, (*MySlice)(nil), td.ArrayEach(4), expectedError{ Message: mustBe("nil pointer"), Path: mustBe("DATA"), Got: mustBe("nil *slice (*td_test.MySlice type)"), Expected: mustBe("non-nil *slice OR *array"), }) checkOKForEach(t, []any{ [...]int{20, 22, 29}, []int{20, 22, 29}, MyArray{20, 22, 29}, MySlice{20, 22, 29}, &MyArray{20, 22, 29}, &MySlice{20, 22, 29}, }, td.ArrayEach(td.Between(20, 30))) checkError(t, nil, td.ArrayEach(4), expectedError{ Message: mustBe("nil value"), Path: mustBe("DATA"), Got: mustBe("nil"), Expected: mustBe("slice OR array OR *slice OR *array"), }) checkErrorForEach(t, []any{ [...]int{4, 5, 4}, []int{4, 5, 4}, MyArray{4, 5, 4}, MySlice{4, 5, 4}, &MyArray{4, 5, 4}, &MySlice{4, 5, 4}, }, td.ArrayEach(4), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA[1]"), Got: mustBe("5"), Expected: mustBe("4"), }) checkError(t, 666, td.ArrayEach(4), expectedError{ Message: mustBe("bad kind"), Path: mustBe("DATA"), Got: mustBe("int"), Expected: mustBe("slice OR array OR *slice OR *array"), }) num := 666 checkError(t, &num, td.ArrayEach(4), expectedError{ Message: mustBe("bad kind"), Path: mustBe("DATA"), Got: mustBe("*int"), Expected: mustBe("slice OR array OR *slice OR *array"), }) checkOK(t, []any{nil, nil, nil}, td.ArrayEach(nil)) checkError(t, []any{nil, nil, nil, 66}, td.ArrayEach(nil), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA[3]"), Got: mustBe("66"), Expected: mustBe("nil"), }) // // String test.EqualStr(t, td.ArrayEach(4).String(), "ArrayEach(4)") test.EqualStr(t, td.ArrayEach(td.All(1, 2)).String(), `ArrayEach(All(1, 2))`) } func TestArrayEachTypeBehind(t *testing.T) { equalTypes(t, td.ArrayEach(6), nil) } golang-github-maxatome-go-testdeep-1.14.0/td/td_array_test.go000066400000000000000000000532211454313311600241610ustar00rootroot00000000000000// Copyright (c) 2018, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td_test import ( "testing" "github.com/maxatome/go-testdeep/internal/test" "github.com/maxatome/go-testdeep/td" ) func TestArray(t *testing.T) { type MyArray [5]int // // Simple array checkOK(t, [5]int{}, td.Array([5]int{}, nil)) checkOK(t, [5]int{0, 0, 0, 4}, td.Array([5]int{0, 0, 0, 4}, nil)) checkOK(t, [5]int{1, 0, 3}, td.Array([5]int{}, td.ArrayEntries{2: 3, 0: 1})) checkOK(t, [5]int{1, 2, 3}, td.Array([5]int{0, 2}, td.ArrayEntries{2: 3, 0: 1})) checkOK(t, [5]any{1, 2, nil, 4, nil}, td.Array([5]any{nil, 2, nil, 4}, td.ArrayEntries{0: 1, 2: nil})) zero, one, two := 0, 1, 2 checkOK(t, [5]*int{nil, &zero, &one, &two}, td.Array( [5]*int{}, td.ArrayEntries{1: &zero, 2: &one, 3: &two, 4: nil})) gotArray := [...]int{1, 2, 3, 4, 5} checkError(t, gotArray, td.Array(MyArray{}, nil), expectedError{ Message: mustBe("type mismatch"), Path: mustBe("DATA"), Got: mustBe("[5]int"), Expected: mustBe("td_test.MyArray"), }) checkError(t, gotArray, td.Array([5]int{1, 2, 3, 4, 6}, nil), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA[4]"), Got: mustBe("5"), Expected: mustBe("6"), }) checkError(t, gotArray, td.Array([5]int{1, 2, 3, 4}, td.ArrayEntries{4: 6}), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA[4]"), Got: mustBe("5"), Expected: mustBe("6"), }) checkError(t, nil, td.Array([1]int{42}, nil), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA"), Got: mustBe("nil"), Expected: mustContain("Array("), }) // // Array type checkOK(t, MyArray{}, td.Array(MyArray{}, nil)) checkOK(t, MyArray{0, 0, 0, 4}, td.Array(MyArray{0, 0, 0, 4}, nil)) checkOK(t, MyArray{1, 0, 3}, td.Array(MyArray{}, td.ArrayEntries{2: 3, 0: 1})) checkOK(t, MyArray{1, 2, 3}, td.Array(MyArray{0, 2}, td.ArrayEntries{2: 3, 0: 1})) checkOK(t, &MyArray{}, td.Array(&MyArray{}, nil)) checkOK(t, &MyArray{0, 0, 0, 4}, td.Array(&MyArray{0, 0, 0, 4}, nil)) checkOK(t, &MyArray{1, 0, 3}, td.Array(&MyArray{}, td.ArrayEntries{2: 3, 0: 1})) checkOK(t, &MyArray{1, 0, 3}, td.Array((*MyArray)(nil), td.ArrayEntries{2: 3, 0: 1})) checkOK(t, &MyArray{1, 2, 3}, td.Array(&MyArray{0, 2}, td.ArrayEntries{2: 3, 0: 1})) gotTypedArray := MyArray{1, 2, 3, 4, 5} checkError(t, 123, td.Array(&MyArray{}, td.ArrayEntries{}), expectedError{ Message: mustBe("type mismatch"), Path: mustBe("DATA"), Got: mustBe("int"), Expected: mustBe("*td_test.MyArray"), }) checkError(t, &MyStruct{}, td.Array(&MyArray{}, td.ArrayEntries{}), expectedError{ Message: mustBe("type mismatch"), Path: mustBe("DATA"), Got: mustBe("*td_test.MyStruct"), Expected: mustBe("*td_test.MyArray"), }) checkError(t, gotTypedArray, td.Array([5]int{}, nil), expectedError{ Message: mustBe("type mismatch"), Path: mustBe("DATA"), Got: mustBe("td_test.MyArray"), Expected: mustBe("[5]int"), }) checkError(t, gotTypedArray, td.Array(MyArray{1, 2, 3, 4, 6}, nil), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA[4]"), Got: mustBe("5"), Expected: mustBe("6"), }) checkError(t, gotTypedArray, td.Array(MyArray{1, 2, 3, 4}, td.ArrayEntries{4: 6}), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA[4]"), Got: mustBe("5"), Expected: mustBe("6"), }) checkError(t, &gotTypedArray, td.Array([5]int{}, nil), expectedError{ Message: mustBe("type mismatch"), Path: mustBe("DATA"), Got: mustBe("*td_test.MyArray"), Expected: mustBe("[5]int"), }) checkError(t, &gotTypedArray, td.Array(&MyArray{1, 2, 3, 4, 6}, nil), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA[4]"), Got: mustBe("5"), Expected: mustBe("6"), }) checkError(t, &gotTypedArray, td.Array(&MyArray{1, 2, 3, 4}, td.ArrayEntries{4: 6}), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA[4]"), Got: mustBe("5"), Expected: mustBe("6"), }) // Be lax... // Without Lax → error checkError(t, MyArray{}, td.Array([5]int{}, nil), expectedError{ Message: mustBe("type mismatch"), }) checkError(t, [5]int{}, td.Array(MyArray{}, nil), expectedError{ Message: mustBe("type mismatch"), }) checkOK(t, MyArray{}, td.Lax(td.Array([5]int{}, nil))) checkOK(t, [5]int{}, td.Lax(td.Array(MyArray{}, nil))) // // Bad usage checkError(t, "never tested", td.Array("test", nil), expectedError{ Message: mustBe("bad usage of Array operator"), Path: mustBe("DATA"), Summary: mustBe("usage: Array(ARRAY|&ARRAY, EXPECTED_ENTRIES), but received string as 1st parameter"), }) checkError(t, "never tested", td.Array(&MyStruct{}, nil), expectedError{ Message: mustBe("bad usage of Array operator"), Path: mustBe("DATA"), Summary: mustBe("usage: Array(ARRAY|&ARRAY, EXPECTED_ENTRIES), but received *td_test.MyStruct (ptr) as 1st parameter"), }) checkError(t, "never tested", td.Array([]int{}, nil), expectedError{ Message: mustBe("bad usage of Array operator"), Path: mustBe("DATA"), Summary: mustBe("usage: Array(ARRAY|&ARRAY, EXPECTED_ENTRIES), but received []int (slice) as 1st parameter"), }) checkError(t, "never tested", td.Array([1]int{}, td.ArrayEntries{1: 34}), expectedError{ Message: mustBe("bad usage of Array operator"), Path: mustBe("DATA"), Summary: mustBe("array length is 1, so cannot have #1 expected index"), }) checkError(t, "never tested", td.Array([3]int{}, td.ArrayEntries{1: nil}), expectedError{ Message: mustBe("bad usage of Array operator"), Path: mustBe("DATA"), Summary: mustBe("expected value of #1 cannot be nil as items type is int"), }) checkError(t, "never tested", td.Array([3]int{}, td.ArrayEntries{1: "bad"}), expectedError{ Message: mustBe("bad usage of Array operator"), Path: mustBe("DATA"), Summary: mustBe("type string of #1 expected value differs from array contents (int)"), }) checkError(t, "never tested", td.Array([1]int{12}, td.ArrayEntries{0: 21}), expectedError{ Message: mustBe("bad usage of Array operator"), Path: mustBe("DATA"), Summary: mustBe("non zero #0 entry in model already exists in expectedEntries"), }) // // String test.EqualStr(t, td.Array(MyArray{0, 0, 4}, td.ArrayEntries{1: 3, 0: 2}).String(), `Array(td_test.MyArray{ 0: 2 1: 3 2: 4 3: 0 4: 0 })`) test.EqualStr(t, td.Array(&MyArray{0, 0, 4}, td.ArrayEntries{1: 3, 0: 2}).String(), `Array(*td_test.MyArray{ 0: 2 1: 3 2: 4 3: 0 4: 0 })`) test.EqualStr(t, td.Array([0]int{}, td.ArrayEntries{}).String(), `Array([0]int{})`) // Erroneous op test.EqualStr(t, td.Array([3]int{}, td.ArrayEntries{1: "bad"}).String(), "Array()") } func TestArrayTypeBehind(t *testing.T) { type MyArray [12]int equalTypes(t, td.Array([12]int{}, nil), [12]int{}) equalTypes(t, td.Array(MyArray{}, nil), MyArray{}) equalTypes(t, td.Array(&MyArray{}, nil), &MyArray{}) // Erroneous op equalTypes(t, td.Array([3]int{}, td.ArrayEntries{1: "bad"}), nil) } func TestSlice(t *testing.T) { type MySlice []int // // Simple slice checkOK(t, []int{}, td.Slice([]int{}, nil)) checkOK(t, []int{0, 3}, td.Slice([]int{0, 3}, nil)) checkOK(t, []int{2, 3}, td.Slice([]int{}, td.ArrayEntries{1: 3, 0: 2})) checkOK(t, []int{2, 3}, td.Slice(([]int)(nil), td.ArrayEntries{1: 3, 0: 2})) checkOK(t, []int{2, 3, 4}, td.Slice([]int{0, 0, 4}, td.ArrayEntries{1: 3, 0: 2})) checkOK(t, []int{2, 3, 4}, td.Slice([]int{2, 3}, td.ArrayEntries{2: 4})) checkOK(t, []int{2, 3, 4, 0, 6}, td.Slice([]int{2, 3}, td.ArrayEntries{2: 4, 4: 6})) gotSlice := []int{2, 3, 4} checkError(t, gotSlice, td.Slice(MySlice{}, nil), expectedError{ Message: mustBe("type mismatch"), Path: mustBe("DATA"), Got: mustBe("[]int"), Expected: mustBe("td_test.MySlice"), }) checkError(t, gotSlice, td.Slice([]int{2, 3, 5}, nil), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA[2]"), Got: mustBe("4"), Expected: mustBe("5"), }) checkError(t, gotSlice, td.Slice([]int{2, 3}, td.ArrayEntries{2: 5}), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA[2]"), Got: mustBe("4"), Expected: mustBe("5"), }) checkError(t, nil, td.Slice([]int{2, 3}, nil), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA"), Got: mustBe("nil"), Expected: mustContain("Slice("), }) // // Slice type checkOK(t, MySlice{}, td.Slice(MySlice{}, nil)) checkOK(t, MySlice{0, 3}, td.Slice(MySlice{0, 3}, nil)) checkOK(t, MySlice{2, 3}, td.Slice(MySlice{}, td.ArrayEntries{1: 3, 0: 2})) checkOK(t, MySlice{2, 3}, td.Slice((MySlice)(nil), td.ArrayEntries{1: 3, 0: 2})) checkOK(t, MySlice{2, 3, 4}, td.Slice(MySlice{0, 0, 4}, td.ArrayEntries{1: 3, 0: 2})) checkOK(t, MySlice{2, 3, 4, 0, 6}, td.Slice(MySlice{2, 3}, td.ArrayEntries{2: 4, 4: 6})) checkOK(t, &MySlice{}, td.Slice(&MySlice{}, nil)) checkOK(t, &MySlice{0, 3}, td.Slice(&MySlice{0, 3}, nil)) checkOK(t, &MySlice{2, 3}, td.Slice(&MySlice{}, td.ArrayEntries{1: 3, 0: 2})) checkOK(t, &MySlice{2, 3}, td.Slice((*MySlice)(nil), td.ArrayEntries{1: 3, 0: 2})) checkOK(t, &MySlice{2, 3, 4}, td.Slice(&MySlice{0, 0, 4}, td.ArrayEntries{1: 3, 0: 2})) checkOK(t, &MySlice{2, 3, 4, 0, 6}, td.Slice(&MySlice{2, 3}, td.ArrayEntries{2: 4, 4: 6})) gotTypedSlice := MySlice{2, 3, 4} checkError(t, 123, td.Slice(&MySlice{}, td.ArrayEntries{}), expectedError{ Message: mustBe("type mismatch"), Path: mustBe("DATA"), Got: mustBe("int"), Expected: mustBe("*td_test.MySlice"), }) checkError(t, &MyStruct{}, td.Slice(&MySlice{}, td.ArrayEntries{}), expectedError{ Message: mustBe("type mismatch"), Path: mustBe("DATA"), Got: mustBe("*td_test.MyStruct"), Expected: mustBe("*td_test.MySlice"), }) checkError(t, gotTypedSlice, td.Slice([]int{}, nil), expectedError{ Message: mustBe("type mismatch"), Path: mustBe("DATA"), Got: mustBe("td_test.MySlice"), Expected: mustBe("[]int"), }) checkError(t, gotTypedSlice, td.Slice(MySlice{2, 3, 5}, nil), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA[2]"), Got: mustBe("4"), Expected: mustBe("5"), }) checkError(t, gotTypedSlice, td.Slice(MySlice{2, 3}, td.ArrayEntries{2: 5}), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA[2]"), Got: mustBe("4"), Expected: mustBe("5"), }) checkError(t, gotTypedSlice, td.Slice(MySlice{2, 3, 4}, td.ArrayEntries{3: 5}), expectedError{ Message: mustBe("expected value out of range"), Path: mustBe("DATA[3]"), Got: mustBe(""), Expected: mustBe("5"), }) checkError(t, gotTypedSlice, td.Slice(MySlice{2, 3}, nil), expectedError{ Message: mustBe("got value out of range"), Path: mustBe("DATA[2]"), Got: mustBe("4"), Expected: mustBe(""), }) checkError(t, &gotTypedSlice, td.Slice([]int{}, nil), expectedError{ Message: mustBe("type mismatch"), Path: mustBe("DATA"), Got: mustBe("*td_test.MySlice"), Expected: mustBe("[]int"), }) checkError(t, &gotTypedSlice, td.Slice(&MySlice{2, 3, 5}, nil), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA[2]"), Got: mustBe("4"), Expected: mustBe("5"), }) checkError(t, &gotTypedSlice, td.Slice(&MySlice{2, 3}, td.ArrayEntries{2: 5}), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA[2]"), Got: mustBe("4"), Expected: mustBe("5"), }) checkError(t, &gotTypedSlice, td.Slice(&MySlice{2, 3}, nil), expectedError{ Message: mustBe("got value out of range"), Path: mustBe("DATA[2]"), Got: mustBe("4"), Expected: mustBe(""), }) // // nil cases var ( gotNilSlice []int gotNilTypedSlice MySlice ) checkOK(t, gotNilSlice, td.Slice([]int{}, nil)) checkOK(t, gotNilTypedSlice, td.Slice(MySlice{}, nil)) checkOK(t, &gotNilTypedSlice, td.Slice(&MySlice{}, nil)) // Be lax... // Without Lax → error checkError(t, MySlice{}, td.Slice([]int{}, nil), expectedError{ Message: mustBe("type mismatch"), }) checkError(t, []int{}, td.Slice(MySlice{}, nil), expectedError{ Message: mustBe("type mismatch"), }) checkOK(t, MySlice{}, td.Lax(td.Slice([]int{}, nil))) checkOK(t, []int{}, td.Lax(td.Slice(MySlice{}, nil))) // // Bad usage checkError(t, "never tested", td.Slice("test", nil), expectedError{ Message: mustBe("bad usage of Slice operator"), Path: mustBe("DATA"), Summary: mustBe("usage: Slice(SLICE|&SLICE, EXPECTED_ENTRIES), but received string as 1st parameter"), }) checkError(t, "never tested", td.Slice(&MyStruct{}, nil), expectedError{ Message: mustBe("bad usage of Slice operator"), Path: mustBe("DATA"), Summary: mustBe("usage: Slice(SLICE|&SLICE, EXPECTED_ENTRIES), but received *td_test.MyStruct (ptr) as 1st parameter"), }) checkError(t, "never tested", td.Slice([0]int{}, nil), expectedError{ Message: mustBe("bad usage of Slice operator"), Path: mustBe("DATA"), Summary: mustBe("usage: Slice(SLICE|&SLICE, EXPECTED_ENTRIES), but received [0]int (array) as 1st parameter"), }) checkError(t, "never tested", td.Slice([]int{}, td.ArrayEntries{1: "bad"}), expectedError{ Message: mustBe("bad usage of Slice operator"), Path: mustBe("DATA"), Summary: mustBe("type string of #1 expected value differs from slice contents (int)"), }) checkError(t, "never tested", td.Slice([]int{12}, td.ArrayEntries{0: 21}), expectedError{ Message: mustBe("bad usage of Slice operator"), Path: mustBe("DATA"), Summary: mustBe("non zero #0 entry in model already exists in expectedEntries"), }) // // String test.EqualStr(t, td.Slice(MySlice{0, 0, 4}, td.ArrayEntries{1: 3, 0: 2}).String(), `Slice(td_test.MySlice{ 0: 2 1: 3 2: 4 })`) test.EqualStr(t, td.Slice(&MySlice{0, 0, 4}, td.ArrayEntries{1: 3, 0: 2}).String(), `Slice(*td_test.MySlice{ 0: 2 1: 3 2: 4 })`) test.EqualStr(t, td.Slice(&MySlice{}, td.ArrayEntries{}).String(), `Slice(*td_test.MySlice{})`) // Erroneous op test.EqualStr(t, td.Slice([]int{}, td.ArrayEntries{1: "bad"}).String(), "Slice()") } func TestSliceTypeBehind(t *testing.T) { type MySlice []int equalTypes(t, td.Slice([]int{}, nil), []int{}) equalTypes(t, td.Slice(MySlice{}, nil), MySlice{}) equalTypes(t, td.Slice(&MySlice{}, nil), &MySlice{}) // Erroneous op equalTypes(t, td.Slice([]int{}, td.ArrayEntries{1: "bad"}), nil) } func TestSuperSliceOf(t *testing.T) { t.Run("interface array", func(t *testing.T) { got := [5]any{"foo", "bar", nil, 666, 777} checkOK(t, got, td.SuperSliceOf([5]any{1: "bar"}, td.ArrayEntries{2: td.Nil()})) checkOK(t, got, td.SuperSliceOf([5]any{1: "bar"}, td.ArrayEntries{2: nil})) checkOK(t, got, td.SuperSliceOf([5]any{1: "bar"}, td.ArrayEntries{3: 666})) checkOK(t, got, td.SuperSliceOf([5]any{1: "bar"}, td.ArrayEntries{3: td.Between(665, 667)})) checkOK(t, &got, td.SuperSliceOf(&[5]any{1: "bar"}, td.ArrayEntries{3: td.Between(665, 667)})) checkError(t, got, td.SuperSliceOf([5]any{1: "foo"}, td.ArrayEntries{2: td.Nil()}), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA[1]"), Got: mustBe(`"bar"`), Expected: mustBe(`"foo"`), }) checkError(t, got, td.SuperSliceOf([5]any{1: 666}, td.ArrayEntries{2: td.Nil()}), expectedError{ Message: mustBe("type mismatch"), Path: mustBe("DATA[1]"), Got: mustBe("string"), Expected: mustBe("int"), }) checkError(t, &got, td.SuperSliceOf([5]any{1: 666}, td.ArrayEntries{2: td.Nil()}), expectedError{ Message: mustBe("type mismatch"), Path: mustBe("DATA"), Got: mustBe("*[5]interface {}"), Expected: mustBe("[5]interface {}"), }) checkError(t, got, td.SuperSliceOf(&[5]any{1: 666}, td.ArrayEntries{2: td.Nil()}), expectedError{ Message: mustBe("type mismatch"), Path: mustBe("DATA"), Got: mustBe("[5]interface {}"), Expected: mustBe("*[5]interface {}"), }) }) t.Run("ints array", func(t *testing.T) { type MyArray [5]int checkOK(t, MyArray{}, td.SuperSliceOf(MyArray{}, nil)) got := MyArray{3: 4} checkOK(t, got, td.SuperSliceOf(MyArray{}, nil)) checkOK(t, got, td.SuperSliceOf(MyArray{3: 4}, nil)) checkOK(t, got, td.SuperSliceOf(MyArray{}, td.ArrayEntries{3: 4})) checkError(t, got, td.SuperSliceOf(MyArray{}, td.ArrayEntries{1: 666}), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA[1]"), Got: mustBe(`0`), Expected: mustBe(`666`), }) // Be lax... // Without Lax → error checkError(t, got, td.SuperSliceOf([5]int{}, td.ArrayEntries{3: 4}), expectedError{ Message: mustBe("type mismatch"), Path: mustBe("DATA"), Got: mustBe(`td_test.MyArray`), Expected: mustBe(`[5]int`), }) checkOK(t, got, td.Lax(td.SuperSliceOf([5]int{}, td.ArrayEntries{3: 4}))) checkError(t, [5]int{3: 4}, td.SuperSliceOf(MyArray{}, td.ArrayEntries{3: 4}), expectedError{ Message: mustBe("type mismatch"), Path: mustBe("DATA"), Got: mustBe(`[5]int`), Expected: mustBe(`td_test.MyArray`), }) checkOK(t, [5]int{3: 4}, td.Lax(td.SuperSliceOf(MyArray{}, td.ArrayEntries{3: 4}))) checkError(t, "never tested", td.SuperSliceOf(MyArray{}, td.ArrayEntries{8: 34}), expectedError{ Message: mustBe("bad usage of SuperSliceOf operator"), Path: mustBe("DATA"), Summary: mustBe("array length is 5, so cannot have #8 expected index"), }) }) t.Run("ints slice", func(t *testing.T) { type MySlice []int checkOK(t, MySlice{}, td.SuperSliceOf(MySlice{}, nil)) checkOK(t, MySlice(nil), td.SuperSliceOf(MySlice{}, nil)) got := MySlice{3: 4} checkOK(t, got, td.SuperSliceOf(MySlice{}, td.ArrayEntries{3: td.N(5, 1)})) checkOK(t, got, td.SuperSliceOf(MySlice{3: 4}, td.ArrayEntries{2: 0})) checkError(t, got, td.SuperSliceOf(MySlice{}, td.ArrayEntries{1: 666}), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA[1]"), Got: mustBe(`0`), Expected: mustBe(`666`), }) checkError(t, got, td.SuperSliceOf(MySlice{}, td.ArrayEntries{3: 0}), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA[3]"), Got: mustBe(`4`), Expected: mustBe(`0`), }) checkError(t, got, td.SuperSliceOf(MySlice{}, td.ArrayEntries{28: 666}), expectedError{ Message: mustBe("expected value out of range"), Path: mustBe("DATA[28]"), Got: mustBe(``), Expected: mustBe(`666`), }) checkError(t, got, td.SuperSliceOf(MySlice{28: 666}, nil), expectedError{ Message: mustBe("expected value out of range"), Path: mustBe("DATA[28]"), Got: mustBe(``), Expected: mustBe(`666`), }) // Be lax... // Without Lax → error checkError(t, got, td.SuperSliceOf([]int{}, td.ArrayEntries{3: 4}), expectedError{ Message: mustBe("type mismatch"), Path: mustBe("DATA"), Got: mustBe(`td_test.MySlice`), Expected: mustBe(`[]int`), }) checkOK(t, got, td.Lax(td.SuperSliceOf([]int{}, td.ArrayEntries{3: 4}))) checkError(t, []int{3: 4}, td.SuperSliceOf(MySlice{}, td.ArrayEntries{3: 4}), expectedError{ Message: mustBe("type mismatch"), Path: mustBe("DATA"), Got: mustBe(`[]int`), Expected: mustBe(`td_test.MySlice`), }) checkOK(t, []int{3: 4}, td.Lax(td.SuperSliceOf(MySlice{}, td.ArrayEntries{3: 4}))) }) // // Bad usage checkError(t, "never tested", td.SuperSliceOf("test", nil), expectedError{ Message: mustBe("bad usage of SuperSliceOf operator"), Path: mustBe("DATA"), Summary: mustBe("usage: SuperSliceOf(ARRAY|&ARRAY|SLICE|&SLICE, EXPECTED_ENTRIES), but received string as 1st parameter"), }) checkError(t, "never tested", td.SuperSliceOf(&MyStruct{}, nil), expectedError{ Message: mustBe("bad usage of SuperSliceOf operator"), Path: mustBe("DATA"), Summary: mustBe("usage: SuperSliceOf(ARRAY|&ARRAY|SLICE|&SLICE, EXPECTED_ENTRIES), but received *td_test.MyStruct (ptr) as 1st parameter"), }) checkError(t, "never tested", td.SuperSliceOf([]int{}, td.ArrayEntries{1: "bad"}), expectedError{ Message: mustBe("bad usage of SuperSliceOf operator"), Path: mustBe("DATA"), Summary: mustBe("type string of #1 expected value differs from slice contents (int)"), }) checkError(t, "never tested", td.SuperSliceOf([]int{12}, td.ArrayEntries{0: 21}), expectedError{ Message: mustBe("bad usage of SuperSliceOf operator"), Path: mustBe("DATA"), Summary: mustBe("non zero #0 entry in model already exists in expectedEntries"), }) // Erroneous op test.EqualStr(t, td.SuperSliceOf([]int{}, td.ArrayEntries{1: "bad"}).String(), "SuperSliceOf()") } func TestSuperSliceOfTypeBehind(t *testing.T) { type MySlice []int equalTypes(t, td.SuperSliceOf([]int{}, nil), []int{}) equalTypes(t, td.SuperSliceOf(MySlice{}, nil), MySlice{}) equalTypes(t, td.SuperSliceOf(&MySlice{}, nil), &MySlice{}) type MyArray [12]int equalTypes(t, td.SuperSliceOf([12]int{}, nil), [12]int{}) equalTypes(t, td.SuperSliceOf(MyArray{}, nil), MyArray{}) equalTypes(t, td.SuperSliceOf(&MyArray{}, nil), &MyArray{}) // Erroneous op equalTypes(t, td.SuperSliceOf([]int{}, td.ArrayEntries{1: "bad"}), nil) } golang-github-maxatome-go-testdeep-1.14.0/td/td_bag.go000066400000000000000000000123601454313311600225340ustar00rootroot00000000000000// Copyright (c) 2018, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td // summary(Bag): compares the contents of an array or a slice without taking // care of the order of items // input(Bag): array,slice,ptr(ptr on array/slice) // Bag operator compares the contents of an array or a slice (or a // pointer on array/slice) without taking care of the order of items. // // During a match, each expected item should match in the compared // array/slice, and each array/slice item should be matched by an // expected item to succeed. // // td.Cmp(t, []int{1, 1, 2}, td.Bag(1, 1, 2)) // succeeds // td.Cmp(t, []int{1, 1, 2}, td.Bag(1, 2, 1)) // succeeds // td.Cmp(t, []int{1, 1, 2}, td.Bag(2, 1, 1)) // succeeds // td.Cmp(t, []int{1, 1, 2}, td.Bag(1, 2)) // fails, one 1 is missing // td.Cmp(t, []int{1, 1, 2}, td.Bag(1, 2, 1, 3)) // fails, 3 is missing // // // works with slices/arrays of any type // td.Cmp(t, personSlice, td.Bag( // Person{Name: "Bob", Age: 32}, // Person{Name: "Alice", Age: 26}, // )) // // To flatten a non-[]any slice/array, use [Flatten] function // and so avoid boring and inefficient copies: // // expected := []int{1, 2, 1} // td.Cmp(t, []int{1, 1, 2}, td.Bag(td.Flatten(expected))) // succeeds // // = td.Cmp(t, []int{1, 1, 2}, td.Bag(1, 2, 1)) // // exp1 := []int{5, 1, 1} // exp2 := []int{8, 42, 3} // td.Cmp(t, []int{1, 5, 1, 8, 42, 3, 3}, // td.Bag(td.Flatten(exp1), 3, td.Flatten(exp2))) // succeeds // // = td.Cmp(t, []int{1, 5, 1, 8, 42, 3, 3}, td.Bag(5, 1, 1, 3, 8, 42, 3)) // // TypeBehind method can return a non-nil [reflect.Type] if all items // known non-interface types are equal, or if only interface types // are found (mostly issued from Isa()) and they are equal. // // See also [SubBagOf], [SuperBagOf] and [Set]. func Bag(expectedItems ...any) TestDeep { return newSetBase(allSet, false, expectedItems) } // summary(SubBagOf): compares the contents of an array or a slice // without taking care of the order of items but with potentially some // exclusions // input(SubBagOf): array,slice,ptr(ptr on array/slice) // SubBagOf operator compares the contents of an array or a slice (or a // pointer on array/slice) without taking care of the order of items. // // During a match, each array/slice item should be matched by an // expected item to succeed. But some expected items can be missing // from the compared array/slice. // // td.Cmp(t, []int{1}, td.SubBagOf(1, 1, 2)) // succeeds // td.Cmp(t, []int{1, 1, 1}, td.SubBagOf(1, 1, 2)) // fails, one 1 is an extra item // // // works with slices/arrays of any type // td.Cmp(t, personSlice, td.SubBagOf( // Person{Name: "Bob", Age: 32}, // Person{Name: "Alice", Age: 26}, // )) // // To flatten a non-[]any slice/array, use [Flatten] function // and so avoid boring and inefficient copies: // // expected := []int{1, 2, 1} // td.Cmp(t, []int{1}, td.SubBagOf(td.Flatten(expected))) // succeeds // // = td.Cmp(t, []int{1}, td.SubBagOf(1, 2, 1)) // // exp1 := []int{5, 1, 1} // exp2 := []int{8, 42, 3} // td.Cmp(t, []int{1, 42, 3}, // td.SubBagOf(td.Flatten(exp1), 3, td.Flatten(exp2))) // succeeds // // = td.Cmp(t, []int{1, 42, 3}, td.SubBagOf(5, 1, 1, 3, 8, 42, 3)) // // TypeBehind method can return a non-nil [reflect.Type] if all items // known non-interface types are equal, or if only interface types // are found (mostly issued from Isa()) and they are equal. // // See also [Bag] and [SuperBagOf]. func SubBagOf(expectedItems ...any) TestDeep { return newSetBase(subSet, false, expectedItems) } // summary(SuperBagOf): compares the contents of an array or a slice // without taking care of the order of items but with potentially some // extra items // input(SuperBagOf): array,slice,ptr(ptr on array/slice) // SuperBagOf operator compares the contents of an array or a slice (or a // pointer on array/slice) without taking care of the order of items. // // During a match, each expected item should match in the compared // array/slice. But some items in the compared array/slice may not be // expected. // // td.Cmp(t, []int{1, 1, 2}, td.SuperBagOf(1)) // succeeds // td.Cmp(t, []int{1, 1, 2}, td.SuperBagOf(1, 1, 1)) // fails, one 1 is missing // // // works with slices/arrays of any type // td.Cmp(t, personSlice, td.SuperBagOf( // Person{Name: "Bob", Age: 32}, // Person{Name: "Alice", Age: 26}, // )) // // To flatten a non-[]any slice/array, use [Flatten] function // and so avoid boring and inefficient copies: // // expected := []int{1, 2, 1} // td.Cmp(t, []int{1}, td.SuperBagOf(td.Flatten(expected))) // succeeds // // = td.Cmp(t, []int{1}, td.SuperBagOf(1, 2, 1)) // // exp1 := []int{5, 1, 1} // exp2 := []int{8, 42} // td.Cmp(t, []int{1, 5, 1, 8, 42, 3, 3, 6}, // td.SuperBagOf(td.Flatten(exp1), 3, td.Flatten(exp2))) // succeeds // // = td.Cmp(t, []int{1, 5, 1, 8, 42, 3, 3, 6}, td.SuperBagOf(5, 1, 1, 3, 8, 42)) // // TypeBehind method can return a non-nil [reflect.Type] if all items // known non-interface types are equal, or if only interface types // are found (mostly issued from Isa()) and they are equal. // // See also [Bag] and [SubBagOf]. func SuperBagOf(expectedItems ...any) TestDeep { return newSetBase(superSet, false, expectedItems) } golang-github-maxatome-go-testdeep-1.14.0/td/td_bag_test.go000066400000000000000000000126531454313311600236000ustar00rootroot00000000000000// Copyright (c) 2018, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td_test import ( "fmt" "testing" "github.com/maxatome/go-testdeep/internal/test" "github.com/maxatome/go-testdeep/td" ) func TestBag(t *testing.T) { type MyArray [5]int type MySlice []int for idx, got := range []any{ []int{1, 3, 4, 4, 5}, [...]int{1, 3, 4, 4, 5}, MySlice{1, 3, 4, 4, 5}, MyArray{1, 3, 4, 4, 5}, &MySlice{1, 3, 4, 4, 5}, &MyArray{1, 3, 4, 4, 5}, } { testName := fmt.Sprintf("Test #%d → %v", idx, got) // // Bag checkOK(t, got, td.Bag(5, 4, 1, 4, 3), testName) checkError(t, got, td.Bag(5, 4, 1, 3), expectedError{ Message: mustBe("comparing %% as a Bag"), Path: mustBe("DATA"), Summary: mustBe("Extra item: (4)"), }, testName) checkError(t, got, td.Bag(5, 4, 1, 4, 3, 66, 42), expectedError{ Message: mustBe("comparing %% as a Bag"), Path: mustBe("DATA"), // items are sorted Summary: mustBe(`Missing 2 items: (42, 66)`), }, testName) checkError(t, got, td.Bag(5, 66, 4, 1, 4, 3), expectedError{ Message: mustBe("comparing %% as a Bag"), Path: mustBe("DATA"), Summary: mustBe("Missing item: (66)"), }, testName) checkError(t, got, td.Bag(5, 66, 4, 1, 4, 3, 66), expectedError{ Message: mustBe("comparing %% as a Bag"), Path: mustBe("DATA"), Summary: mustBe("Missing 2 items: (66,\n 66)"), }, testName) checkError(t, got, td.Bag(5, 66, 4, 1, 3), expectedError{ Message: mustBe("comparing %% as a Bag"), Path: mustBe("DATA"), Summary: mustBe("Missing item: (66)\n Extra item: (4)"), }, testName) // Lax checkOK(t, got, td.Lax(td.Bag(float64(5), 4, 1, 4, 3)), testName) // // SubBagOf checkOK(t, got, td.SubBagOf(5, 4, 1, 4, 3), testName) checkOK(t, got, td.SubBagOf(5, 66, 4, 1, 4, 3), testName) checkError(t, got, td.SubBagOf(5, 66, 4, 1, 3), expectedError{ Message: mustBe("comparing %% as a SubBagOf"), Path: mustBe("DATA"), Summary: mustBe("Extra item: (4)"), }, testName) // Lax checkOK(t, got, td.Lax(td.SubBagOf(float64(5), 4, 1, 4, 3)), testName) // // SuperBagOf checkOK(t, got, td.SuperBagOf(5, 4, 1, 4, 3), testName) checkOK(t, got, td.SuperBagOf(5, 4, 3), testName) checkError(t, got, td.SuperBagOf(5, 66, 4, 1, 3), expectedError{ Message: mustBe("comparing %% as a SuperBagOf"), Path: mustBe("DATA"), Summary: mustBe("Missing item: (66)"), }, testName) // Lax checkOK(t, got, td.Lax(td.SuperBagOf(float64(5), 4, 1, 4, 3)), testName) } checkOK(t, []any{123, "foo", nil, "bar", nil}, td.Bag("foo", "bar", 123, nil, nil)) var nilSlice MySlice for idx, got := range []any{([]int)(nil), &nilSlice} { testName := fmt.Sprintf("Test #%d", idx) checkOK(t, got, td.Bag(), testName) checkOK(t, got, td.SubBagOf(), testName) checkOK(t, got, td.SubBagOf(1, 2), testName) checkOK(t, got, td.SuperBagOf(), testName) } for idx, bag := range []td.TestDeep{ td.Bag(123), td.SubBagOf(123), td.SuperBagOf(123), } { testName := fmt.Sprintf("Test #%d → %s", idx, bag) checkError(t, 123, bag, expectedError{ Message: mustBe("bad kind"), Path: mustBe("DATA"), Got: mustBe("int"), Expected: mustBe("slice OR array OR *slice OR *array"), }, testName) num := 123 checkError(t, &num, bag, expectedError{ Message: mustBe("bad kind"), Path: mustBe("DATA"), Got: mustBe("*int"), Expected: mustBe("slice OR array OR *slice OR *array"), }, testName) var list *MySlice checkError(t, list, bag, expectedError{ Message: mustBe("nil pointer"), Path: mustBe("DATA"), Got: mustBe("nil *slice (*td_test.MySlice type)"), Expected: mustBe("non-nil *slice OR *array"), }, testName) checkError(t, nil, bag, expectedError{ Message: mustBe("bad kind"), Path: mustBe("DATA"), Got: mustBe("nil"), Expected: mustBe("slice OR array OR *slice OR *array"), }, testName) } // // String test.EqualStr(t, td.Bag(1).String(), "Bag(1)") test.EqualStr(t, td.Bag(1, 2).String(), "Bag(1,\n 2)") test.EqualStr(t, td.SubBagOf(1).String(), "SubBagOf(1)") test.EqualStr(t, td.SubBagOf(1, 2).String(), "SubBagOf(1,\n 2)") test.EqualStr(t, td.SuperBagOf(1).String(), "SuperBagOf(1)") test.EqualStr(t, td.SuperBagOf(1, 2).String(), "SuperBagOf(1,\n 2)") } func TestBagTypeBehind(t *testing.T) { equalTypes(t, td.Bag(6, 5), ([]int)(nil)) equalTypes(t, td.Bag(6, "foo"), nil) equalTypes(t, td.SubBagOf(6, 5), ([]int)(nil)) equalTypes(t, td.SubBagOf(6, "foo"), nil) equalTypes(t, td.SuperBagOf(6, 5), ([]int)(nil)) equalTypes(t, td.SuperBagOf(6, "foo"), nil) // Always the same non-interface type (even if we encounter several // interface types) equalTypes(t, td.Bag( td.Empty(), 5, td.Isa((*error)(nil)), // interface type (in fact pointer to ...) td.All(6, 7), td.Isa((*fmt.Stringer)(nil)), // interface type 8), ([]int)(nil)) // Only one interface type equalTypes(t, td.Bag( td.Isa((*error)(nil)), td.Isa((*error)(nil)), td.Isa((*error)(nil)), ), ([]*error)(nil)) // Several interface types, cannot be sure equalTypes(t, td.Bag( td.Isa((*error)(nil)), td.Isa((*fmt.Stringer)(nil)), ), nil) } golang-github-maxatome-go-testdeep-1.14.0/td/td_between.go000066400000000000000000000462411454313311600234410ustar00rootroot00000000000000// Copyright (c) 2018-2022, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td import ( "fmt" "math" "reflect" "time" "github.com/maxatome/go-testdeep/internal/ctxerr" "github.com/maxatome/go-testdeep/internal/types" "github.com/maxatome/go-testdeep/internal/util" ) type boundCmp uint8 const ( boundNone boundCmp = iota boundIn boundOut ) type tdBetween struct { base expectedMin reflect.Value expectedMax reflect.Value minBound boundCmp maxBound boundCmp } var _ TestDeep = &tdBetween{} // BoundsKind type qualifies the [Between] bounds. type BoundsKind uint8 const ( _ BoundsKind = (iota - 1) & 3 BoundsInIn // allows to match between "from" and "to" both included. BoundsInOut // allows to match between "from" included and "to" excluded. BoundsOutIn // allows to match between "from" excluded and "to" included. BoundsOutOut // allows to match between "from" and "to" both excluded. ) type tdBetweenTime struct { tdBetween expectedType reflect.Type mustConvert bool } var _ TestDeep = &tdBetweenTime{} type tdBetweenCmp struct { tdBetween expectedType reflect.Type cmp func(a, b reflect.Value) int } // summary(Between): checks that a number, string or time.Time is // between two bounds // input(Between): str,int,float,cplx(todo),struct(time.Time) // Between operator checks that data is between from and // to. from and to can be any numeric, string, [time.Time] (or // assignable) value or implement at least one of the two following // methods: // // func (a T) Less(b T) bool // returns true if a < b // func (a T) Compare(b T) int // returns -1 if a < b, 1 if a > b, 0 if a == b // // from and to must be the same type as the compared value, except // if BeLax config flag is true. [time.Duration] type is accepted as // to when from is [time.Time] or convertible. bounds allows to // specify whether bounds are included or not: // - [BoundsInIn] (default): between from and to both included // - [BoundsInOut]: between from included and to excluded // - [BoundsOutIn]: between from excluded and to included // - [BoundsOutOut]: between from and to both excluded // // If bounds is missing, it defaults to [BoundsInIn]. // // tc.Cmp(t, 17, td.Between(17, 20)) // succeeds, BoundsInIn by default // tc.Cmp(t, 17, td.Between(10, 17, BoundsInOut)) // fails // tc.Cmp(t, 17, td.Between(10, 17, BoundsOutIn)) // succeeds // tc.Cmp(t, 17, td.Between(17, 20, BoundsOutOut)) // fails // tc.Cmp(t, // succeeds // netip.MustParse("127.0.0.1"), // td.Between(netip.MustParse("127.0.0.0"), netip.MustParse("127.255.255.255"))) // // TypeBehind method returns the [reflect.Type] of from. func Between(from, to any, bounds ...BoundsKind) TestDeep { b := tdBetween{ base: newBase(3), expectedMin: reflect.ValueOf(from), expectedMax: reflect.ValueOf(to), } const usage = "(NUM|STRING|TIME, NUM|STRING|TIME/DURATION[, BOUNDS_KIND])" if len(bounds) > 0 { if len(bounds) > 1 { b.err = ctxerr.OpTooManyParams("Between", usage) return &b } if bounds[0] == BoundsInIn || bounds[0] == BoundsInOut { b.minBound = boundIn } else { b.minBound = boundOut } if bounds[0] == BoundsInIn || bounds[0] == BoundsOutIn { b.maxBound = boundIn } else { b.maxBound = boundOut } } else { b.minBound = boundIn b.maxBound = boundIn } if b.expectedMax.Type() == b.expectedMin.Type() { return b.initBetween(usage) } // Special case for (TIME, DURATION) ok, convertible := types.IsTypeOrConvertible(b.expectedMin, types.Time) if ok { if d, ok := to.(time.Duration); ok { if convertible { b.expectedMax = reflect.ValueOf( b.expectedMin. Convert(types.Time). Interface().(time.Time). Add(d)). Convert(b.expectedMin.Type()) } else { b.expectedMax = reflect.ValueOf(from.(time.Time).Add(d)) } return b.initBetween(usage) } b.err = ctxerr.OpBad("Between", "Between(FROM, TO): when FROM type is %[1]s, TO must have the same type or time.Duration: %[2]s ≠ %[1]s|time.Duration", b.expectedMin.Type(), b.expectedMax.Type(), ) return &b } b.err = ctxerr.OpBad("Between", "Between(FROM, TO): FROM and TO must have the same type: %s ≠ %s", b.expectedMin.Type(), b.expectedMax.Type(), ) return &b } func (b *tdBetween) initBetween(usage string) TestDeep { if !b.expectedMax.IsValid() { b.expectedMax = b.expectedMin } // Is any of: // (T) Compare(T) int // or // (T) Less(T) bool // available? if cmp := types.NewOrder(b.expectedMin.Type()); cmp != nil { if order := cmp(b.expectedMin, b.expectedMax); order > 0 { b.expectedMin, b.expectedMax = b.expectedMax, b.expectedMin } return &tdBetweenCmp{ tdBetween: *b, expectedType: b.expectedMin.Type(), cmp: cmp, } } switch b.expectedMin.Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: if b.expectedMin.Int() > b.expectedMax.Int() { b.expectedMin, b.expectedMax = b.expectedMax, b.expectedMin } return b case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: if b.expectedMin.Uint() > b.expectedMax.Uint() { b.expectedMin, b.expectedMax = b.expectedMax, b.expectedMin } return b case reflect.Float32, reflect.Float64: if b.expectedMin.Float() > b.expectedMax.Float() { b.expectedMin, b.expectedMax = b.expectedMax, b.expectedMin } return b case reflect.String: if b.expectedMin.String() > b.expectedMax.String() { b.expectedMin, b.expectedMax = b.expectedMax, b.expectedMin } return b case reflect.Struct: ok, convertible := types.IsTypeOrConvertible(b.expectedMin, types.Time) if !ok { break } bt := tdBetweenTime{ tdBetween: *b, expectedType: b.expectedMin.Type(), mustConvert: convertible, } if convertible { bt.expectedMin = b.expectedMin.Convert(types.Time) bt.expectedMax = b.expectedMax.Convert(types.Time) } if bt.expectedMin.Interface().(time.Time). After(bt.expectedMax.Interface().(time.Time)) { bt.expectedMin, bt.expectedMax = bt.expectedMax, bt.expectedMin } return &bt } b.err = ctxerr.OpBadUsage(b.GetLocation().Func, usage, b.expectedMin.Interface(), 1, true) return b } func (b *tdBetween) nInt(tolerance reflect.Value) { if diff := tolerance.Int(); diff != 0 { expectedBase := b.expectedMin.Int() max := expectedBase + diff if max < expectedBase { max = math.MaxInt64 } min := expectedBase - diff if min > expectedBase { min = math.MinInt64 } b.expectedMin = reflect.New(tolerance.Type()).Elem() b.expectedMin.SetInt(min) b.expectedMax = reflect.New(tolerance.Type()).Elem() b.expectedMax.SetInt(max) } } func (b *tdBetween) nUint(tolerance reflect.Value) { if diff := tolerance.Uint(); diff != 0 { base := b.expectedMin.Uint() max := base + diff if max < base { max = math.MaxUint64 } min := base - diff if min > base { min = 0 } b.expectedMin = reflect.New(tolerance.Type()).Elem() b.expectedMin.SetUint(min) b.expectedMax = reflect.New(tolerance.Type()).Elem() b.expectedMax.SetUint(max) } } func (b *tdBetween) nFloat(tolerance reflect.Value) { if diff := tolerance.Float(); diff != 0 { base := b.expectedMin.Float() b.expectedMin = reflect.New(tolerance.Type()).Elem() b.expectedMin.SetFloat(base - diff) b.expectedMax = reflect.New(tolerance.Type()).Elem() b.expectedMax.SetFloat(base + diff) } } // summary(N): compares a number with a tolerance value // input(N): int,float,cplx(todo) // N operator compares a numeric data against num ± tolerance. If // tolerance is missing, it defaults to 0. num and tolerance // must be the same type as the compared value, except if BeLax config // flag is true. // // td.Cmp(t, 12.2, td.N(12., 0.3)) // succeeds // td.Cmp(t, 12.2, td.N(12., 0.1)) // fails // // TypeBehind method returns the [reflect.Type] of num. func N(num any, tolerance ...any) TestDeep { n := tdBetween{ base: newBase(3), expectedMin: reflect.ValueOf(num), minBound: boundIn, maxBound: boundIn, } const usage = "({,U}INT{,8,16,32,64}|FLOAT{32,64}[, TOLERANCE])" switch n.expectedMin.Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Float32, reflect.Float64: default: n.err = ctxerr.OpBadUsage("N", usage, num, 1, true) return &n } n.expectedMax = n.expectedMin if len(tolerance) > 0 { if len(tolerance) > 1 { n.err = ctxerr.OpTooManyParams("N", usage) return &n } tol := reflect.ValueOf(tolerance[0]) if tol.Type() != n.expectedMin.Type() { n.err = ctxerr.OpBad("N", "N(NUM, TOLERANCE): NUM and TOLERANCE must have the same type: %s ≠ %s", n.expectedMin.Type(), tol.Type()) return &n } switch tol.Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: n.nInt(tol) case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: n.nUint(tol) default: // case reflect.Float32, reflect.Float64: n.nFloat(tol) } } return &n } // summary(Gt): checks that a number, string or time.Time is // greater than a value // input(Gt): str,int,float,cplx(todo),struct(time.Time) // Gt operator checks that data is greater than // minExpectedValue. minExpectedValue can be any numeric, string, // [time.Time] (or assignable) value or implements at least one of the // two following methods: // // func (a T) Less(b T) bool // returns true if a < b // func (a T) Compare(b T) int // returns -1 if a < b, 1 if a > b, 0 if a == b // // minExpectedValue must be the same type as the compared value, // except if BeLax config flag is true. // // td.Cmp(t, 17, td.Gt(15)) // before := time.Now() // td.Cmp(t, time.Now(), td.Gt(before)) // // TypeBehind method returns the [reflect.Type] of minExpectedValue. func Gt(minExpectedValue any) TestDeep { b := &tdBetween{ base: newBase(3), expectedMin: reflect.ValueOf(minExpectedValue), minBound: boundOut, } return b.initBetween("(NUM|STRING|TIME)") } // summary(Gte): checks that a number, string or time.Time is // greater or equal than a value // input(Gte): str,int,float,cplx(todo),struct(time.Time) // Gte operator checks that data is greater or equal than // minExpectedValue. minExpectedValue can be any numeric, string, // [time.Time] (or assignable) value or implements at least one of the // two following methods: // // func (a T) Less(b T) bool // returns true if a < b // func (a T) Compare(b T) int // returns -1 if a < b, 1 if a > b, 0 if a == b // // minExpectedValue must be the same type as the compared value, // except if BeLax config flag is true. // // td.Cmp(t, 17, td.Gte(17)) // before := time.Now() // td.Cmp(t, time.Now(), td.Gte(before)) // // TypeBehind method returns the [reflect.Type] of minExpectedValue. func Gte(minExpectedValue any) TestDeep { b := &tdBetween{ base: newBase(3), expectedMin: reflect.ValueOf(minExpectedValue), minBound: boundIn, } return b.initBetween("(NUM|STRING|TIME)") } // summary(Lt): checks that a number, string or time.Time is // lesser than a value // input(Lt): str,int,float,cplx(todo),struct(time.Time) // Lt operator checks that data is lesser than // maxExpectedValue. maxExpectedValue can be any numeric, string, // [time.Time] (or assignable) value or implements at least one of the // two following methods: // // func (a T) Less(b T) bool // returns true if a < b // func (a T) Compare(b T) int // returns -1 if a < b, 1 if a > b, 0 if a == b // // maxExpectedValue must be the same type as the compared value, // except if BeLax config flag is true. // // td.Cmp(t, 17, td.Lt(19)) // before := time.Now() // td.Cmp(t, before, td.Lt(time.Now())) // // TypeBehind method returns the [reflect.Type] of maxExpectedValue. func Lt(maxExpectedValue any) TestDeep { b := &tdBetween{ base: newBase(3), expectedMin: reflect.ValueOf(maxExpectedValue), maxBound: boundOut, } return b.initBetween("(NUM|STRING|TIME)") } // summary(Lte): checks that a number, string or time.Time is // lesser or equal than a value // input(Lte): str,int,float,cplx(todo),struct(time.Time) // Lte operator checks that data is lesser or equal than // maxExpectedValue. maxExpectedValue can be any numeric, string, // [time.Time] (or assignable) value or implements at least one of the // two following methods: // // func (a T) Less(b T) bool // returns true if a < b // func (a T) Compare(b T) int // returns -1 if a < b, 1 if a > b, 0 if a == b // // maxExpectedValue must be the same type as the compared value, // except if BeLax config flag is true. // // td.Cmp(t, 17, td.Lte(17)) // before := time.Now() // td.Cmp(t, before, td.Lt(time.Now())) // // TypeBehind method returns the [reflect.Type] of maxExpectedValue. func Lte(maxExpectedValue any) TestDeep { b := &tdBetween{ base: newBase(3), expectedMin: reflect.ValueOf(maxExpectedValue), maxBound: boundIn, } return b.initBetween("(NUM|STRING|TIME)") } func (b *tdBetween) matchInt(got reflect.Value) (ok bool) { switch b.minBound { case boundIn: ok = got.Int() >= b.expectedMin.Int() case boundOut: ok = got.Int() > b.expectedMin.Int() default: ok = true } if ok { switch b.maxBound { case boundIn: ok = got.Int() <= b.expectedMax.Int() case boundOut: ok = got.Int() < b.expectedMax.Int() default: ok = true } } return } func (b *tdBetween) matchUint(got reflect.Value) (ok bool) { switch b.minBound { case boundIn: ok = got.Uint() >= b.expectedMin.Uint() case boundOut: ok = got.Uint() > b.expectedMin.Uint() default: ok = true } if ok { switch b.maxBound { case boundIn: ok = got.Uint() <= b.expectedMax.Uint() case boundOut: ok = got.Uint() < b.expectedMax.Uint() default: ok = true } } return } func (b *tdBetween) matchFloat(got reflect.Value) (ok bool) { switch b.minBound { case boundIn: ok = got.Float() >= b.expectedMin.Float() case boundOut: ok = got.Float() > b.expectedMin.Float() default: ok = true } if ok { switch b.maxBound { case boundIn: ok = got.Float() <= b.expectedMax.Float() case boundOut: ok = got.Float() < b.expectedMax.Float() default: ok = true } } return } func (b *tdBetween) matchString(got reflect.Value) (ok bool) { switch b.minBound { case boundIn: ok = got.String() >= b.expectedMin.String() case boundOut: ok = got.String() > b.expectedMin.String() default: ok = true } if ok { switch b.maxBound { case boundIn: ok = got.String() <= b.expectedMax.String() case boundOut: ok = got.String() < b.expectedMax.String() default: ok = true } } return } func (b *tdBetween) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error { if b.err != nil { return ctx.CollectError(b.err) } if got.Type() != b.expectedMin.Type() { if !ctx.BeLax || !types.IsConvertible(b.expectedMin, got.Type()) { if ctx.BooleanError { return ctxerr.BooleanError } return ctx.CollectError(ctxerr.TypeMismatch(got.Type(), b.expectedMin.Type())) } nb := *b nb.expectedMin = b.expectedMin.Convert(got.Type()) nb.expectedMax = b.expectedMax.Convert(got.Type()) b = &nb } var ok bool switch got.Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: ok = b.matchInt(got) case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: ok = b.matchUint(got) case reflect.Float32, reflect.Float64: ok = b.matchFloat(got) case reflect.String: ok = b.matchString(got) } if ok { return nil } if ctx.BooleanError { return ctxerr.BooleanError } return ctx.CollectError(&ctxerr.Error{ Message: "values differ", Got: got, Expected: types.RawString(b.String()), }) } func (b *tdBetween) String() string { if b.err != nil { return b.stringError() } var ( min, max any minStr, maxStr string ) if b.minBound != boundNone { min = b.expectedMin.Interface() minStr = util.ToString(min) } if b.maxBound != boundNone { max = b.expectedMax.Interface() maxStr = util.ToString(max) } if min != nil { if max != nil { return fmt.Sprintf("%s %c got %c %s", minStr, util.TernRune(b.minBound == boundIn, '≤', '<'), util.TernRune(b.maxBound == boundIn, '≤', '<'), maxStr) } return fmt.Sprintf("%c %s", util.TernRune(b.minBound == boundIn, '≥', '>'), minStr) } return fmt.Sprintf("%c %s", util.TernRune(b.maxBound == boundIn, '≤', '<'), maxStr) } func (b *tdBetween) TypeBehind() reflect.Type { if b.err != nil { return nil } return b.expectedMin.Type() } var _ TestDeep = &tdBetweenTime{} func (b *tdBetweenTime) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error { // b.err != nil is not possible here, as when a *tdBetweenTime is // built, there is never an error if got.Type() != b.expectedType { if !ctx.BeLax || !types.IsConvertible(got, b.expectedType) { if ctx.BooleanError { return ctxerr.BooleanError } return ctx.CollectError(ctxerr.TypeMismatch(got.Type(), b.expectedType)) } got = got.Convert(b.expectedType) } cmpGot, err := getTime(ctx, got, b.mustConvert) if err != nil { return ctx.CollectError(err) } var ok bool if b.minBound != boundNone { min := b.expectedMin.Interface().(time.Time) if b.minBound == boundIn { ok = !min.After(cmpGot) } else { ok = cmpGot.After(min) } } else { ok = true } if ok && b.maxBound != boundNone { max := b.expectedMax.Interface().(time.Time) if b.maxBound == boundIn { ok = !max.Before(cmpGot) } else { ok = cmpGot.Before(max) } } if ok { return nil } if ctx.BooleanError { return ctxerr.BooleanError } return ctx.CollectError(&ctxerr.Error{ Message: "values differ", Got: got, Expected: types.RawString(b.String()), }) } func (b *tdBetweenTime) TypeBehind() reflect.Type { // b.err != nil is not possible here, as when a *tdBetweenTime is // built, there is never an error return b.expectedType } var _ TestDeep = &tdBetweenCmp{} func (b *tdBetweenCmp) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error { // b.err != nil is not possible here, as when a *tdBetweenCmp is // built, there is never an error if got.Type() != b.expectedType { if ctx.BeLax && types.IsConvertible(got, b.expectedType) { got = got.Convert(b.expectedType) } else { if ctx.BooleanError { return ctxerr.BooleanError } return ctx.CollectError(ctxerr.TypeMismatch(got.Type(), b.expectedType)) } } var ok bool if b.minBound != boundNone { order := b.cmp(got, b.expectedMin) if b.minBound == boundIn { ok = order >= 0 } else { ok = order > 0 } } else { ok = true } if ok && b.maxBound != boundNone { order := b.cmp(got, b.expectedMax) if b.maxBound == boundIn { ok = order <= 0 } else { ok = order < 0 } } if ok { return nil } if ctx.BooleanError { return ctxerr.BooleanError } return ctx.CollectError(&ctxerr.Error{ Message: "values differ", Got: got, Expected: types.RawString(b.String()), }) } func (b *tdBetweenCmp) TypeBehind() reflect.Type { // b.err != nil is not possible here, as when a *tdBetweenCmp is // built, there is never an error return b.expectedType } golang-github-maxatome-go-testdeep-1.14.0/td/td_between_test.go000066400000000000000000000600141454313311600244720ustar00rootroot00000000000000// Copyright (c) 2018-2022, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td_test import ( "fmt" "math" "testing" "time" "github.com/maxatome/go-testdeep/internal/test" "github.com/maxatome/go-testdeep/td" ) func TestBetween(t *testing.T) { checkOK(t, 12, td.Between(9, 13)) checkOK(t, 12, td.Between(13, 9)) checkOK(t, 12, td.Between(9, 12, td.BoundsOutIn)) checkOK(t, 12, td.Between(12, 13, td.BoundsInOut)) checkError(t, 10, td.Between(10, 15, td.BoundsOutIn), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA"), Got: mustBe("10"), Expected: mustBe("10 < got ≤ 15"), }) checkError(t, 10, td.Between(10, 15, td.BoundsOutOut), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA"), Got: mustBe("10"), Expected: mustBe("10 < got < 15"), }) checkError(t, 15, td.Between(10, 15, td.BoundsInOut), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA"), Got: mustBe("15"), Expected: mustBe("10 ≤ got < 15"), }) checkError(t, 15, td.Between(10, 15, td.BoundsOutOut), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA"), Got: mustBe("15"), Expected: mustBe("10 < got < 15"), }) checkError(t, 15, td.Between(uint(10), uint(15), td.BoundsOutOut), expectedError{ Message: mustBe("type mismatch"), Path: mustBe("DATA"), Got: mustBe("int"), Expected: mustBe("uint"), }) checkOK(t, uint16(12), td.Between(uint16(9), uint16(13))) checkOK(t, uint16(12), td.Between(uint16(13), uint16(9))) checkOK(t, uint16(12), td.Between(uint16(9), uint16(12), td.BoundsOutIn)) checkOK(t, uint16(12), td.Between(uint16(12), uint16(13), td.BoundsInOut)) checkOK(t, 12.1, td.Between(9.5, 13.1)) checkOK(t, 12.1, td.Between(13.1, 9.5)) checkOK(t, 12.1, td.Between(9.5, 12.1, td.BoundsOutIn)) checkOK(t, 12.1, td.Between(12.1, 13.1, td.BoundsInOut)) checkOK(t, "abc", td.Between("aaa", "bbb")) checkOK(t, "abc", td.Between("bbb", "aaa")) checkOK(t, "abc", td.Between("aaa", "abc", td.BoundsOutIn)) checkOK(t, "abc", td.Between("abc", "bbb", td.BoundsInOut)) checkOK(t, 12*time.Hour, td.Between(60*time.Second, 24*time.Hour)) // // Bad usage checkError(t, "never tested", td.Between([]byte("test"), []byte("test")), expectedError{ Message: mustBe("bad usage of Between operator"), Path: mustBe("DATA"), Summary: mustBe("usage: Between(NUM|STRING|TIME, NUM|STRING|TIME/DURATION[, BOUNDS_KIND]), but received []uint8 (slice) as 1st parameter"), }) checkError(t, "never tested", td.Between(12, "test"), expectedError{ Message: mustBe("bad usage of Between operator"), Path: mustBe("DATA"), Summary: mustBe("Between(FROM, TO): FROM and TO must have the same type: int ≠ string"), }) checkError(t, "never tested", td.Between("test", 12), expectedError{ Message: mustBe("bad usage of Between operator"), Path: mustBe("DATA"), Summary: mustBe("Between(FROM, TO): FROM and TO must have the same type: string ≠ int"), }) checkError(t, "never tested", td.Between(1, 2, td.BoundsInIn, td.BoundsInOut), expectedError{ Message: mustBe("bad usage of Between operator"), Path: mustBe("DATA"), Summary: mustBe("usage: Between(NUM|STRING|TIME, NUM|STRING|TIME/DURATION[, BOUNDS_KIND]), too many parameters"), }) type notTime struct{} checkError(t, "never tested", td.Between(notTime{}, notTime{}), expectedError{ Message: mustBe("bad usage of Between operator"), Path: mustBe("DATA"), Summary: mustBe("usage: Between(NUM|STRING|TIME, NUM|STRING|TIME/DURATION[, BOUNDS_KIND]), but received td_test.notTime (struct) as 1st parameter"), }) // Erroneous op test.EqualStr(t, td.Between("test", 12).String(), "Between()") } func TestN(t *testing.T) { // // Unsigned checkOK(t, uint(12), td.N(uint(12))) checkOK(t, uint(11), td.N(uint(12), uint(1))) checkOK(t, uint(13), td.N(uint(12), uint(1))) checkError(t, 10, td.N(uint(12), uint(1)), expectedError{ Message: mustBe("type mismatch"), Path: mustBe("DATA"), Got: mustBe("int"), Expected: mustBe("uint"), }) checkOK(t, uint8(12), td.N(uint8(12))) checkOK(t, uint8(11), td.N(uint8(12), uint8(1))) checkOK(t, uint8(13), td.N(uint8(12), uint8(1))) checkError(t, 10, td.N(uint8(12), uint8(1)), expectedError{ Message: mustBe("type mismatch"), Path: mustBe("DATA"), Got: mustBe("int"), Expected: mustBe("uint8"), }) checkOK(t, uint16(12), td.N(uint16(12))) checkOK(t, uint16(11), td.N(uint16(12), uint16(1))) checkOK(t, uint16(13), td.N(uint16(12), uint16(1))) checkError(t, 10, td.N(uint16(12), uint16(1)), expectedError{ Message: mustBe("type mismatch"), Path: mustBe("DATA"), Got: mustBe("int"), Expected: mustBe("uint16"), }) checkOK(t, uint32(12), td.N(uint32(12))) checkOK(t, uint32(11), td.N(uint32(12), uint32(1))) checkOK(t, uint32(13), td.N(uint32(12), uint32(1))) checkError(t, 10, td.N(uint32(12), uint32(1)), expectedError{ Message: mustBe("type mismatch"), Path: mustBe("DATA"), Got: mustBe("int"), Expected: mustBe("uint32"), }) checkOK(t, uint64(12), td.N(uint64(12))) checkOK(t, uint64(11), td.N(uint64(12), uint64(1))) checkOK(t, uint64(13), td.N(uint64(12), uint64(1))) checkError(t, 10, td.N(uint64(12), uint64(1)), expectedError{ Message: mustBe("type mismatch"), Path: mustBe("DATA"), Got: mustBe("int"), Expected: mustBe("uint64"), }) checkOK(t, uint64(math.MaxUint64), td.N(uint64(math.MaxUint64), uint64(2))) checkError(t, uint64(0), td.N(uint64(math.MaxUint64), uint64(2)), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA"), Got: mustBe("(uint64) 0"), Expected: mustBe(fmt.Sprintf("(uint64) %v ≤ got ≤ (uint64) %v", uint64(math.MaxUint64)-2, uint64(math.MaxUint64))), }) checkOK(t, uint64(0), td.N(uint64(0), uint64(2))) checkError(t, uint64(math.MaxUint64), td.N(uint64(0), uint64(2)), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA"), Got: mustBe(fmt.Sprintf("(uint64) %v", uint64(math.MaxUint64))), Expected: mustBe("(uint64) 0 ≤ got ≤ (uint64) 2"), }) // // Signed checkOK(t, 12, td.N(12)) checkOK(t, 11, td.N(12, 1)) checkOK(t, 13, td.N(12, 1)) checkError(t, 10, td.N(12, 1), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA"), Got: mustBe("10"), Expected: mustBe("11 ≤ got ≤ 13"), }) checkError(t, 10, td.N(12, 0), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA"), Got: mustBe("10"), Expected: mustBe("12 ≤ got ≤ 12"), }) checkOK(t, int8(12), td.N(int8(12))) checkOK(t, int8(11), td.N(int8(12), int8(1))) checkOK(t, int8(13), td.N(int8(12), int8(1))) checkError(t, 10, td.N(int8(12), int8(1)), expectedError{ Message: mustBe("type mismatch"), Path: mustBe("DATA"), Got: mustBe("int"), Expected: mustBe("int8"), }) checkOK(t, int16(12), td.N(int16(12))) checkOK(t, int16(11), td.N(int16(12), int16(1))) checkOK(t, int16(13), td.N(int16(12), int16(1))) checkError(t, 10, td.N(int16(12), int16(1)), expectedError{ Message: mustBe("type mismatch"), Path: mustBe("DATA"), Got: mustBe("int"), Expected: mustBe("int16"), }) checkOK(t, int32(12), td.N(int32(12))) checkOK(t, int32(11), td.N(int32(12), int32(1))) checkOK(t, int32(13), td.N(int32(12), int32(1))) checkError(t, 10, td.N(int32(12), int32(1)), expectedError{ Message: mustBe("type mismatch"), Path: mustBe("DATA"), Got: mustBe("int"), Expected: mustBe("int32"), }) checkOK(t, int64(12), td.N(int64(12))) checkOK(t, int64(11), td.N(int64(12), int64(1))) checkOK(t, int64(13), td.N(int64(12), int64(1))) checkError(t, 10, td.N(int64(12), int64(1)), expectedError{ Message: mustBe("type mismatch"), Path: mustBe("DATA"), Got: mustBe("int"), Expected: mustBe("int64"), }) checkOK(t, int64(math.MaxInt64), td.N(int64(math.MaxInt64), int64(2))) checkError(t, int64(0), td.N(int64(math.MaxInt64), int64(2)), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA"), Got: mustBe("(int64) 0"), Expected: mustBe(fmt.Sprintf("(int64) %v ≤ got ≤ (int64) %v", int64(math.MaxInt64)-2, int64(math.MaxInt64))), }) checkOK(t, int64(math.MinInt64), td.N(int64(math.MinInt64), int64(2))) checkError(t, int64(0), td.N(int64(math.MinInt64), int64(2)), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA"), Got: mustBe("(int64) 0"), Expected: mustBe(fmt.Sprintf("(int64) %v ≤ got ≤ (int64) %v", int64(math.MinInt64), int64(math.MinInt64)+2)), }) // // Float checkOK(t, 12.1, td.N(12.1)) checkOK(t, 11.9, td.N(12.0, 0.1)) checkOK(t, 12.1, td.N(12.0, 0.1)) checkError(t, 11.8, td.N(12.0, 0.1), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA"), Got: mustBe("11.8"), Expected: mustBe("11.9 ≤ got ≤ 12.1"), }) checkOK(t, float32(12.1), td.N(float32(12.1))) checkOK(t, float32(11.9), td.N(float32(12), float32(0.1))) checkOK(t, float32(12.1), td.N(float32(12), float32(0.1))) checkError(t, 11.8, td.N(float32(12), float32(0.1)), expectedError{ Message: mustBe("type mismatch"), Path: mustBe("DATA"), Got: mustBe("float64"), Expected: mustBe("float32"), }) floatTol := 10e304 checkOK(t, float64(math.MaxFloat64), td.N(float64(math.MaxFloat64), floatTol)) checkError(t, float64(0), td.N(float64(math.MaxFloat64), floatTol), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA"), Got: mustBe("0.0"), Expected: mustBe(fmt.Sprintf("%v ≤ got ≤ +Inf", float64(math.MaxFloat64)-floatTol)), }) checkOK(t, -float64(math.MaxFloat64), td.N(-float64(math.MaxFloat64), float64(2))) checkError(t, float64(0), td.N(-float64(math.MaxFloat64), floatTol), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA"), Got: mustBe("0.0"), Expected: mustBe(fmt.Sprintf("-Inf ≤ got ≤ %v", -float64(math.MaxFloat64)+floatTol)), }) // // Bad usage checkError(t, "never tested", td.N("test"), expectedError{ Message: mustBe("bad usage of N operator"), Path: mustBe("DATA"), Summary: mustBe("usage: N({,U}INT{,8,16,32,64}|FLOAT{32,64}[, TOLERANCE]), but received string as 1st parameter"), }) checkError(t, "never tested", td.N(10, 1, 2), expectedError{ Message: mustBe("bad usage of N operator"), Path: mustBe("DATA"), Summary: mustBe("usage: N({,U}INT{,8,16,32,64}|FLOAT{32,64}[, TOLERANCE]), too many parameters"), }) checkError(t, "never tested", td.N(10, "test"), expectedError{ Message: mustBe("bad usage of N operator"), Path: mustBe("DATA"), Summary: mustBe("N(NUM, TOLERANCE): NUM and TOLERANCE must have the same type: int ≠ string"), }) // Erroneous op test.EqualStr(t, td.N(10, 1, 2).String(), "N()") } func TestLGt(t *testing.T) { type MyTime time.Time checkOK(t, 12, td.Gt(11)) checkOK(t, 12, td.Gte(12)) checkOK(t, 12, td.Lt(13)) checkOK(t, 12, td.Lte(12)) checkOK(t, uint16(12), td.Gt(uint16(11))) checkOK(t, uint16(12), td.Gte(uint16(12))) checkOK(t, uint16(12), td.Lt(uint16(13))) checkOK(t, uint16(12), td.Lte(uint16(12))) checkOK(t, 12.3, td.Gt(12.2)) checkOK(t, 12.3, td.Gte(12.3)) checkOK(t, 12.3, td.Lt(12.4)) checkOK(t, 12.3, td.Lte(12.3)) checkOK(t, "abc", td.Gt("abb")) checkOK(t, "abc", td.Gte("abc")) checkOK(t, "abc", td.Lt("abd")) checkOK(t, "abc", td.Lte("abc")) checkError(t, 12, td.Gt(12), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA"), Got: mustBe("12"), Expected: mustBe("> 12"), }) checkError(t, 12, td.Lt(12), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA"), Got: mustBe("12"), Expected: mustBe("< 12"), }) checkError(t, 12, td.Gte(13), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA"), Got: mustBe("12"), Expected: mustBe("≥ 13"), }) checkError(t, 12, td.Lte(11), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA"), Got: mustBe("12"), Expected: mustBe("≤ 11"), }) checkError(t, "abc", td.Gt("abc"), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA"), Got: mustBe(`"abc"`), Expected: mustBe(`> "abc"`), }) checkError(t, "abc", td.Lt("abc"), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA"), Got: mustBe(`"abc"`), Expected: mustBe(`< "abc"`), }) checkError(t, "abc", td.Gte("abd"), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA"), Got: mustBe(`"abc"`), Expected: mustBe(`≥ "abd"`), }) checkError(t, "abc", td.Lte("abb"), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA"), Got: mustBe(`"abc"`), Expected: mustBe(`≤ "abb"`), }) gotDate := time.Date(2018, time.March, 4, 1, 2, 3, 0, time.UTC) expectedDate := gotDate checkOK(t, gotDate, td.Gte(expectedDate)) checkOK(t, gotDate, td.Lte(expectedDate)) checkOK(t, gotDate, td.Lax(td.Gte(MyTime(expectedDate)))) checkOK(t, gotDate, td.Lax(td.Lte(MyTime(expectedDate)))) checkError(t, gotDate, td.Gt(expectedDate), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA"), Got: mustBe("(time.Time) 2018-03-04 01:02:03 +0000 UTC"), Expected: mustBe("> (time.Time) 2018-03-04 01:02:03 +0000 UTC"), }) checkError(t, gotDate, td.Lt(expectedDate), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA"), Got: mustBe("(time.Time) 2018-03-04 01:02:03 +0000 UTC"), Expected: mustBe("< (time.Time) 2018-03-04 01:02:03 +0000 UTC"), }) // // Bad usage checkError(t, "never tested", td.Gt([]byte("test")), expectedError{ Message: mustBe("bad usage of Gt operator"), Path: mustBe("DATA"), Summary: mustBe("usage: Gt(NUM|STRING|TIME), but received []uint8 (slice) as 1st parameter"), }) checkError(t, "never tested", td.Gte([]byte("test")), expectedError{ Message: mustBe("bad usage of Gte operator"), Path: mustBe("DATA"), Summary: mustBe("usage: Gte(NUM|STRING|TIME), but received []uint8 (slice) as 1st parameter"), }) checkError(t, "never tested", td.Lt([]byte("test")), expectedError{ Message: mustBe("bad usage of Lt operator"), Path: mustBe("DATA"), Summary: mustBe("usage: Lt(NUM|STRING|TIME), but received []uint8 (slice) as 1st parameter"), }) checkError(t, "never tested", td.Lte([]byte("test")), expectedError{ Message: mustBe("bad usage of Lte operator"), Path: mustBe("DATA"), Summary: mustBe("usage: Lte(NUM|STRING|TIME), but received []uint8 (slice) as 1st parameter"), }) // Erroneous op test.EqualStr(t, td.Gt([]byte("test")).String(), "Gt()") test.EqualStr(t, td.Gte([]byte("test")).String(), "Gte()") test.EqualStr(t, td.Lt([]byte("test")).String(), "Lt()") test.EqualStr(t, td.Lte([]byte("test")).String(), "Lte()") } func TestBetweenTime(t *testing.T) { type MyTime time.Time now := time.Now() checkOK(t, now, td.Between(now, now)) checkOK(t, now, td.Between(now.Add(-time.Second), now.Add(time.Second))) checkOK(t, now, td.Between(now.Add(time.Second), now.Add(-time.Second))) // (TIME, DURATION) checkOK(t, now, td.Between(now.Add(-time.Second), 2*time.Second)) checkOK(t, now, td.Between(now.Add(time.Second), -2*time.Second)) checkOK(t, MyTime(now), td.Between( MyTime(now.Add(-time.Second)), MyTime(now.Add(time.Second)))) // (TIME, DURATION) checkOK(t, MyTime(now), td.Between( MyTime(now.Add(-time.Second)), 2*time.Second)) checkOK(t, MyTime(now), td.Between( MyTime(now.Add(time.Second)), -2*time.Second)) // Lax mode checkOK(t, MyTime(now), td.Lax(td.Between( now.Add(time.Second), now.Add(-time.Second)))) checkOK(t, now, td.Lax(td.Between( MyTime(now.Add(time.Second)), MyTime(now.Add(-time.Second))))) checkOK(t, MyTime(now), td.Lax(td.Between( now.Add(-time.Second), 2*time.Second))) checkOK(t, now, td.Lax(td.Between( MyTime(now.Add(-time.Second)), 2*time.Second))) checkOK(t, now, td.Lax(td.Between( MyTime(now.Add(-time.Second)), 2*time.Second, td.BoundsOutOut))) date := time.Date(2018, time.March, 4, 0, 0, 0, 0, time.UTC) checkError(t, date, td.Between(date.Add(-2*time.Second), date.Add(-time.Second)), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA"), Got: mustBe("(time.Time) 2018-03-04 00:00:00 +0000 UTC"), Expected: mustBe("(time.Time) 2018-03-03 23:59:58 +0000 UTC" + " ≤ got ≤ " + "(time.Time) 2018-03-03 23:59:59 +0000 UTC"), }) checkError(t, MyTime(date), td.Between(MyTime(date.Add(-2*time.Second)), MyTime(date.Add(-time.Second))), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA"), Got: mustContain("(td_test.MyTime) "), Expected: mustBe( "(time.Time) 2018-03-03 23:59:58 +0000 UTC" + " ≤ got ≤ " + "(time.Time) 2018-03-03 23:59:59 +0000 UTC"), }) checkError(t, date, td.Between(date.Add(-2*time.Second), date, td.BoundsInOut), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA"), Got: mustBe("(time.Time) 2018-03-04 00:00:00 +0000 UTC"), Expected: mustBe("(time.Time) 2018-03-03 23:59:58 +0000 UTC" + " ≤ got < " + "(time.Time) 2018-03-04 00:00:00 +0000 UTC"), }) checkError(t, date, td.Between(date, date.Add(2*time.Second), td.BoundsOutIn), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA"), Got: mustBe("(time.Time) 2018-03-04 00:00:00 +0000 UTC"), Expected: mustBe("(time.Time) 2018-03-04 00:00:00 +0000 UTC" + " < got ≤ " + "(time.Time) 2018-03-04 00:00:02 +0000 UTC"), }) checkError(t, "string", td.Between(date, date.Add(2*time.Second), td.BoundsOutIn), expectedError{ Message: mustBe("type mismatch"), Path: mustBe("DATA"), Got: mustBe("string"), Expected: mustBe("time.Time"), }) checkError(t, "string", td.Between(MyTime(date), MyTime(date.Add(2*time.Second)), td.BoundsOutIn), expectedError{ Message: mustBe("type mismatch"), Path: mustBe("DATA"), Got: mustBe("string"), Expected: mustBe("td_test.MyTime"), }) checkError(t, "never tested", td.Between(date, 12), // (Time, Time) or (Time, Duration) expectedError{ Message: mustBe("bad usage of Between operator"), Path: mustBe("DATA"), Summary: mustBe("Between(FROM, TO): when FROM type is time.Time, TO must have the same type or time.Duration: int ≠ time.Time|time.Duration"), }) checkError(t, "never tested", td.Between(MyTime(date), 12), // (MyTime, MyTime) or (MyTime, Duration) expectedError{ Message: mustBe("bad usage of Between operator"), Path: mustBe("DATA"), Summary: mustBe("Between(FROM, TO): when FROM type is td_test.MyTime, TO must have the same type or time.Duration: int ≠ td_test.MyTime|time.Duration"), }) checkOK(t, now, td.Gt(now.Add(-time.Second))) checkOK(t, now, td.Lt(now.Add(time.Second))) } type compareType int func (i compareType) Compare(j compareType) int { if i < j { return -1 } if i > j { return 1 } return 0 } type lessType int func (i lessType) Less(j lessType) bool { return i < j } func TestBetweenCmp(t *testing.T) { t.Run("compareType", func(t *testing.T) { checkOK(t, compareType(5), td.Between(compareType(4), compareType(6))) checkOK(t, compareType(5), td.Between(compareType(6), compareType(4))) checkOK(t, compareType(5), td.Between(compareType(5), compareType(6))) checkOK(t, compareType(5), td.Between(compareType(4), compareType(5))) checkOK(t, compareType(5), td.Between(compareType(4), compareType(6), td.BoundsOutOut)) checkError(t, compareType(5), td.Between(compareType(5), compareType(6), td.BoundsOutIn), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA"), Got: mustBe("(td_test.compareType) 5"), Expected: mustBe("(td_test.compareType) 5 < got ≤ (td_test.compareType) 6"), }) checkError(t, compareType(5), td.Between(compareType(4), compareType(5), td.BoundsInOut), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA"), Got: mustBe("(td_test.compareType) 5"), Expected: mustBe("(td_test.compareType) 4 ≤ got < (td_test.compareType) 5"), }) // Other between forms checkOK(t, compareType(5), td.Gt(compareType(4))) checkOK(t, compareType(5), td.Gte(compareType(5))) checkOK(t, compareType(5), td.Lt(compareType(6))) checkOK(t, compareType(5), td.Lte(compareType(5))) // BeLax or not BeLax for i, op := range []td.TestDeep{ td.Between(compareType(4), compareType(6)), td.Gt(compareType(4)), td.Gte(compareType(5)), td.Lt(compareType(6)), td.Lte(compareType(5)), } { // Type mismatch if BeLax not enabled checkError(t, 5, op, expectedError{ Message: mustBe("type mismatch"), Path: mustBe("DATA"), Got: mustBe("int"), Expected: mustBe("td_test.compareType"), }, "Op #%d", i) // BeLax enabled is OK checkOK(t, 5, td.Lax(op), "Op #%d", i) } // In a private field type private struct { num compareType } checkOK(t, private{num: 5}, td.Struct(private{}, td.StructFields{ "num": td.Between(compareType(4), compareType(6)), })) }) t.Run("lessType", func(t *testing.T) { checkOK(t, lessType(5), td.Between(lessType(4), lessType(6))) checkOK(t, lessType(5), td.Between(lessType(6), lessType(4))) checkOK(t, lessType(5), td.Between(lessType(5), lessType(6))) checkOK(t, lessType(5), td.Between(lessType(4), lessType(5))) checkOK(t, lessType(5), td.Between(lessType(4), lessType(6), td.BoundsOutOut)) checkError(t, lessType(5), td.Between(lessType(5), lessType(6), td.BoundsOutIn), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA"), Got: mustBe("(td_test.lessType) 5"), Expected: mustBe("(td_test.lessType) 5 < got ≤ (td_test.lessType) 6"), }) checkError(t, lessType(5), td.Between(lessType(4), lessType(5), td.BoundsInOut), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA"), Got: mustBe("(td_test.lessType) 5"), Expected: mustBe("(td_test.lessType) 4 ≤ got < (td_test.lessType) 5"), }) // Other between forms checkOK(t, lessType(5), td.Gt(lessType(4))) checkOK(t, lessType(5), td.Gte(lessType(5))) checkOK(t, lessType(5), td.Lt(lessType(6))) checkOK(t, lessType(5), td.Lte(lessType(5))) // BeLax or not BeLax for i, op := range []td.TestDeep{ td.Between(lessType(4), lessType(6)), td.Gt(lessType(4)), td.Gte(lessType(5)), td.Lt(lessType(6)), td.Lte(lessType(5)), } { // Type mismatch if BeLax not enabled checkError(t, 5, op, expectedError{ Message: mustBe("type mismatch"), Path: mustBe("DATA"), Got: mustBe("int"), Expected: mustBe("td_test.lessType"), }, "Op #%d", i) // BeLax enabled is OK checkOK(t, 5, td.Lax(op), "Op #%d", i) } // In a private field type private struct { num lessType } checkOK(t, private{num: 5}, td.Struct(private{}, td.StructFields{ "num": td.Between(lessType(4), lessType(6)), })) }) } func TestBetweenTypeBehind(t *testing.T) { type MyTime time.Time for _, typ := range []any{ 10, int64(23), int32(23), time.Time{}, MyTime{}, compareType(0), lessType(0), } { equalTypes(t, td.Between(typ, typ), typ) equalTypes(t, td.Gt(typ), typ) equalTypes(t, td.Gte(typ), typ) equalTypes(t, td.Lt(typ), typ) equalTypes(t, td.Lte(typ), typ) } equalTypes(t, td.N(int64(23), int64(5)), int64(0)) // Erroneous op equalTypes(t, td.Between("test", 12), nil) equalTypes(t, td.N(10, 1, 2), nil) equalTypes(t, td.Gt([]byte("test")), nil) equalTypes(t, td.Gte([]byte("test")), nil) equalTypes(t, td.Lt([]byte("test")), nil) equalTypes(t, td.Lte([]byte("test")), nil) } golang-github-maxatome-go-testdeep-1.14.0/td/td_catch.go000066400000000000000000000071711454313311600230710ustar00rootroot00000000000000// Copyright (c) 2019, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td import ( "reflect" "github.com/maxatome/go-testdeep/internal/ctxerr" "github.com/maxatome/go-testdeep/internal/types" "github.com/maxatome/go-testdeep/internal/util" ) type tdCatch struct { tdSmugglerBase target reflect.Value } var _ TestDeep = &tdCatch{} // summary(Catch): catches data on the fly before comparing it // input(Catch): all // Catch is a smuggler operator. It allows to copy data in target on // the fly before comparing it as usual against expectedValue. // // target must be a non-nil pointer and data should be assignable to // its pointed type. If BeLax config flag is true or called under [Lax] // (and so [JSON]) operator, data should be convertible to its pointer // type. // // var id int64 // if td.Cmp(t, CreateRecord("test"), // td.JSON(`{"id": $1, "name": "test"}`, td.Catch(&id, td.NotZero()))) { // t.Logf("Created record ID is %d", id) // } // // It is really useful when used with [JSON] operator and/or [tdhttp] helper. // // var id int64 // ta := tdhttp.NewTestAPI(t, api.Handler). // PostJSON("/item", `{"name":"foo"}`). // CmpStatus(http.StatusCreated). // CmpJSONBody(td.JSON(`{"id": $1, "name": "foo"}`, td.Catch(&id, td.Gt(0)))) // if !ta.Failed() { // t.Logf("Created record ID is %d", id) // } // // If you need to only catch data without comparing it, use [Ignore] // operator as expectedValue as in: // // var id int64 // if td.Cmp(t, CreateRecord("test"), // td.JSON(`{"id": $1, "name": "test"}`, td.Catch(&id, td.Ignore()))) { // t.Logf("Created record ID is %d", id) // } // // TypeBehind method returns the [reflect.Type] of expectedValue, // except if expectedValue is a [TestDeep] operator. In this case, it // delegates TypeBehind() to the operator, but if nil is returned by // this call, the dereferenced [reflect.Type] of target is returned. // // [tdhttp]: https://pkg.go.dev/github.com/maxatome/go-testdeep/helpers/tdhttp func Catch(target, expectedValue any) TestDeep { vt := reflect.ValueOf(target) c := tdCatch{ tdSmugglerBase: newSmugglerBase(expectedValue), target: vt, } if vt.Kind() != reflect.Ptr || vt.IsNil() || !vt.Elem().CanSet() { c.err = ctxerr.OpBadUsage("Catch", "(NON_NIL_PTR, EXPECTED_VALUE)", target, 1, true) return &c } if !c.isTestDeeper { c.expectedValue = reflect.ValueOf(expectedValue) } return &c } func (c *tdCatch) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error { if c.err != nil { return ctx.CollectError(c.err) } if targetType := c.target.Elem().Type(); !got.Type().AssignableTo(targetType) { if !ctx.BeLax || !types.IsConvertible(got, targetType) { if ctx.BooleanError { return ctxerr.BooleanError } return ctx.CollectError(ctxerr.TypeMismatch(got.Type(), c.target.Elem().Type())) } c.target.Elem().Set(got.Convert(targetType)) } else { c.target.Elem().Set(got) } return deepValueEqual(ctx, got, c.expectedValue) } func (c *tdCatch) String() string { if c.err != nil { return c.stringError() } if c.isTestDeeper { return c.expectedValue.Interface().(TestDeep).String() } return util.ToString(c.expectedValue) } func (c *tdCatch) TypeBehind() reflect.Type { if c.err != nil { return nil } if c.isTestDeeper { if typ := c.expectedValue.Interface().(TestDeep).TypeBehind(); typ != nil { return typ } // Operator unknown type behind, fallback on target dereferenced type return c.target.Type().Elem() } if c.expectedValue.IsValid() { return c.expectedValue.Type() } return nil } golang-github-maxatome-go-testdeep-1.14.0/td/td_catch_test.go000066400000000000000000000043561454313311600241320ustar00rootroot00000000000000// Copyright (c) 2019, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td_test import ( "testing" "github.com/maxatome/go-testdeep/internal/test" "github.com/maxatome/go-testdeep/td" ) func TestCatch(t *testing.T) { var num int checkOK(t, 12, td.Catch(&num, 12)) test.EqualInt(t, num, 12) var num64 int64 checkError(t, 12, td.Catch(&num64, 12), expectedError{ Message: mustBe("type mismatch"), Got: mustBe("int"), Expected: mustBe("int64"), }) checkOK(t, 12, td.Lax(td.Catch(&num64, 12))) test.EqualInt(t, int(num64), 12) // Lax not needed for interfaces var val any if checkOK(t, 12, td.Catch(&val, 12)) { if n, ok := val.(int); ok { test.EqualInt(t, n, 12) } else { t.Errorf("val is not an int but a %T", val) } } // // Bad usages checkError(t, "never tested", td.Catch(12, 28), expectedError{ Message: mustBe("bad usage of Catch operator"), Path: mustBe("DATA"), Summary: mustBe("usage: Catch(NON_NIL_PTR, EXPECTED_VALUE), but received int as 1st parameter"), }) checkError(t, "never tested", td.Catch(nil, 28), expectedError{ Message: mustBe("bad usage of Catch operator"), Path: mustBe("DATA"), Summary: mustBe("usage: Catch(NON_NIL_PTR, EXPECTED_VALUE), but received nil as 1st parameter"), }) checkError(t, "never tested", td.Catch((*int)(nil), 28), expectedError{ Message: mustBe("bad usage of Catch operator"), Path: mustBe("DATA"), Summary: mustBe("usage: Catch(NON_NIL_PTR, EXPECTED_VALUE), but received *int (ptr) as 1st parameter"), }) // // String test.EqualStr(t, td.Catch(&num, 12).String(), "12") test.EqualStr(t, td.Catch(&num, td.Gt(4)).String(), td.Gt(4).String()) test.EqualStr(t, td.Catch(&num, nil).String(), "nil") // Erroneous op test.EqualStr(t, td.Catch(nil, 28).String(), "Catch()") } func TestCatchTypeBehind(t *testing.T) { var num int equalTypes(t, td.Catch(&num, 8), 0) equalTypes(t, td.Catch(&num, td.Gt(4)), 0) equalTypes(t, td.Catch(&num, td.Ignore()), 0) // fallback on *target equalTypes(t, td.Catch(&num, nil), nil) // Erroneous op equalTypes(t, td.Catch(nil, 28), nil) } golang-github-maxatome-go-testdeep-1.14.0/td/td_code.go000066400000000000000000000213531454313311600227170ustar00rootroot00000000000000// Copyright (c) 2018, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td import ( "reflect" "github.com/maxatome/go-testdeep/internal/ctxerr" "github.com/maxatome/go-testdeep/internal/types" ) type tdCode struct { base function reflect.Value argType reflect.Type tParams int } var _ TestDeep = &tdCode{} // summary(Code): checks using a custom function // input(Code): all // Code operator allows to check data using a custom function. So // fn is a function that must take one parameter whose type must be // the same as the type of the compared value. // // fn can return a single bool kind value, telling that yes or no // the custom test is successful: // // td.Cmp(t, gotTime, // td.Code(func(date time.Time) bool { // return date.Year() == 2018 // })) // // or two values (bool, string) kinds. The bool value has the same // meaning as above, and the string value is used to describe the // test when it fails: // // td.Cmp(t, gotTime, // td.Code(func(date time.Time) (bool, string) { // if date.Year() == 2018 { // return true, "" // } // return false, "year must be 2018" // })) // // or a single error value. If the returned error is nil, the test // succeeded, else the error contains the reason of failure: // // td.Cmp(t, gotJsonRawMesg, // td.Code(func(b json.RawMessage) error { // var c map[string]int // err := json.Unmarshal(b, &c) // if err != nil { // return err // } // if c["test"] != 42 { // return fmt.Errorf(`key "test" does not match 42`) // } // return nil // })) // // This operator allows to handle any specific comparison not handled // by standard operators. // // It is not recommended to call [Cmp] (or any other Cmp* // functions or [*T] methods) inside the body of fn, because of // confusion produced by output in case of failure. When the data // needs to be transformed before being compared again, [Smuggle] // operator should be used instead. // // But in some cases it can be better to handle yourself the // comparison than to chain [TestDeep] operators. In this case, fn can // be a function receiving one or two [*T] as first parameters and // returning no values. // // When fn expects one [*T] parameter, it is directly derived from the // [testing.TB] instance passed originally to [Cmp] (or its derivatives) // using [NewT]: // // td.Cmp(t, httpRequest, td.Code(func(t *td.T, r *http.Request) { // token, err := DecodeToken(r.Header.Get("X-Token-1")) // if t.CmpNoError(err) { // t.True(token.OK()) // } // })) // // When fn expects two [*T] parameters, they are directly derived from // the [testing.TB] instance passed originally to [Cmp] (or its derivatives) // using [AssertRequire]: // // td.Cmp(t, httpRequest, td.Code(func(assert, require *td.T, r *http.Request) { // token, err := DecodeToken(r.Header.Get("X-Token-1")) // require.CmpNoError(err) // assert.True(token.OK()) // })) // // Note that these forms do not work when there is no initial // [testing.TB] instance, like when using [EqDeeplyError] or // [EqDeeply] functions, or when the Code operator is called behind // the following operators, as they just check if a match occurs // without raising an error: [Any], [Bag], [Contains], [ContainsKey], // [None], [Not], [NotAny], [Set], [SubBagOf], [SubSetOf], // [SuperBagOf] and [SuperSetOf]. // // RootName is inherited but not the current path, but it can be // recovered if needed: // // got := map[string]int{"foo": 123} // td.NewT(t). // RootName("PIPO"). // Cmp(got, td.Map(map[string]int{}, td.MapEntries{ // "foo": td.Code(func(t *td.T, n int) { // t.Cmp(n, 124) // inherit only RootName // t.RootName(t.Config.OriginalPath()).Cmp(n, 125) // recover current path // t.RootName("").Cmp(n, 126) // undo RootName inheritance // }), // })) // // produces the following errors: // // --- FAIL: TestCodeCustom (0.00s) // td_code_test.go:339: Failed test // PIPO: values differ ← inherit only RootName // got: 123 // expected: 124 // td_code_test.go:338: Failed test // PIPO["foo"]: values differ ← recover current path // got: 123 // expected: 125 // td_code_test.go:342: Failed test // DATA: values differ ← undo RootName inheritance // got: 123 // expected: 126 // // TypeBehind method returns the [reflect.Type] of last parameter of fn. func Code(fn any) TestDeep { vfn := reflect.ValueOf(fn) c := tdCode{ base: newBase(3), function: vfn, } if vfn.Kind() != reflect.Func { c.err = ctxerr.OpBadUsage("Code", "(FUNC)", fn, 1, true) return &c } if vfn.IsNil() { c.err = ctxerr.OpBad("Code", "Code(FUNC): FUNC cannot be a nil function") return &c } fnType := vfn.Type() in := fnType.NumIn() // We accept only: // func (arg) bool // func (arg) error // func (arg) (bool, error) // func (*td.T, arg) // with arg ≠ *td.T, as it is certainly an error // func (assert, require *td.T, arg) if fnType.IsVariadic() || in == 0 || in > 3 || (in > 1 && (fnType.In(0) != tType)) || (in >= 2 && (in == 2) == (fnType.In(1) == tType)) { c.err = ctxerr.OpBad("Code", "Code(FUNC): FUNC must take only one non-variadic argument or (*td.T, arg) or (*td.T, *td.T, arg)") return &c } // func (arg) bool // func (arg) error // func (arg) (bool, error) if in == 1 { switch fnType.NumOut() { case 2: // (bool, *string*) if fnType.Out(1).Kind() != reflect.String { break } fallthrough case 1: // (*bool*) or (*bool*, string) if fnType.Out(0).Kind() == reflect.Bool || // (*error*) (fnType.NumOut() == 1 && fnType.Out(0) == types.Error) { c.argType = fnType.In(0) return &c } } c.err = ctxerr.OpBad("Code", "Code(FUNC): FUNC must return bool or (bool, string) or error") return &c } // in == 2 || in == 3 // func (*td.T, arg) (with arg ≠ *td.T) // func (assert, require *td.T, arg) if fnType.NumOut() != 0 { c.err = ctxerr.OpBad("Code", "Code(FUNC): FUNC must return nothing") return &c } c.tParams = in - 1 c.argType = fnType.In(c.tParams) return &c } func (c *tdCode) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error { if c.err != nil { return ctx.CollectError(c.err) } if !got.Type().AssignableTo(c.argType) { if !ctx.BeLax || !types.IsConvertible(got, c.argType) { if ctx.BooleanError { return ctxerr.BooleanError } return ctx.CollectError(&ctxerr.Error{ Message: "incompatible parameter type", Got: types.RawString(got.Type().String()), Expected: types.RawString(c.argType.String()), }) } got = got.Convert(c.argType) } // Refuse to override unexported fields access in this case. It is a // choice, as we think it is better to use Code() on surrounding // struct instead. if !got.CanInterface() { if ctx.BooleanError { return ctxerr.BooleanError } return ctx.CollectError(&ctxerr.Error{ Message: "cannot compare unexported field", Summary: ctxerr.NewSummary("use Code() on surrounding struct instead"), }) } if c.tParams == 0 { ret := c.function.Call([]reflect.Value{got}) if ret[0].Kind() == reflect.Bool { if ret[0].Bool() { return nil } } else if ret[0].IsNil() { // reflect.Interface return nil } if ctx.BooleanError { return ctxerr.BooleanError } var reason string if len(ret) > 1 { // (bool, string) reason = ret[1].String() } else if ret[0].Kind() == reflect.Interface { // (error) // For internal use only if cErr, ok := ret[0].Interface().(*ctxerr.Error); ok { return ctx.CollectError(cErr) } reason = ret[0].Interface().(error).Error() } // else (bool) so no reason to report return ctx.CollectError(&ctxerr.Error{ Message: "ran code with %% as argument", Summary: ctxerr.NewSummaryReason(got, reason), }) } if ctx.OriginalTB == nil { return ctx.CollectError(&ctxerr.Error{ Message: "cannot build *td.T instance", Summary: ctxerr.NewSummary("original testing.TB instance is missing"), }) } t := NewT(ctx.OriginalTB) t.Config.forkedFromCtx = &ctx // func(*td.T, arg) if c.tParams == 1 { c.function.Call([]reflect.Value{ reflect.ValueOf(t), got, }) return nil } // func(assert, require *td.T, arg) assert, require := AssertRequire(t) c.function.Call([]reflect.Value{ reflect.ValueOf(assert), reflect.ValueOf(require), got, }) return nil } func (c *tdCode) String() string { if c.err != nil { return c.stringError() } return "Code(" + c.function.Type().String() + ")" } func (c *tdCode) TypeBehind() reflect.Type { if c.err != nil { return nil } return c.argType } golang-github-maxatome-go-testdeep-1.14.0/td/td_code_test.go000066400000000000000000000345061454313311600237620ustar00rootroot00000000000000// Copyright (c) 2018, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td_test import ( "encoding/json" "errors" "fmt" "strings" "testing" "time" "github.com/maxatome/go-testdeep/internal/ctxerr" "github.com/maxatome/go-testdeep/internal/test" "github.com/maxatome/go-testdeep/td" ) func TestCode(t *testing.T) { checkOK(t, 12, td.Code(func(n int) bool { return n >= 10 && n < 20 })) checkOK(t, 12, td.Code(func(val any) bool { num, ok := val.(int) return ok && num == 12 })) checkOK(t, errors.New("foobar"), td.Code(func(val error) bool { return val.Error() == "foobar" })) checkOK(t, json.RawMessage(`[42]`), td.Code(func(b json.RawMessage) error { var l []int err := json.Unmarshal(b, &l) if err != nil { return err } if len(l) != 1 || l[0] != 42 { return errors.New("42 not found") } return nil })) // Lax checkOK(t, 123, td.Lax(td.Code(func(n float64) bool { return n == 123 }))) checkError(t, 123, td.Code(func(n float64) bool { return true }), expectedError{ Message: mustBe("incompatible parameter type"), Path: mustBe("DATA"), Got: mustBe("int"), Expected: mustBe("float64"), }) type xInt int checkError(t, xInt(12), td.Code(func(n int) bool { return n >= 10 && n < 20 }), expectedError{ Message: mustBe("incompatible parameter type"), Path: mustBe("DATA"), Got: mustBe("td_test.xInt"), Expected: mustBe("int"), }) checkError(t, 12, td.Code(func(n int) (bool, string) { return false, "custom error" }), expectedError{ Message: mustBe("ran code with %% as argument"), Path: mustBe("DATA"), Summary: mustBe(" value: 12\nit failed coz: custom error"), }) checkError(t, 12, td.Code(func(n int) bool { return false }), expectedError{ Message: mustBe("ran code with %% as argument"), Path: mustBe("DATA"), Summary: mustBe(" value: 12\nit failed but didn't say why"), }) type MyBool bool type MyString string checkError(t, 12, td.Code(func(n int) (MyBool, MyString) { return false, "very custom error" }), expectedError{ Message: mustBe("ran code with %% as argument"), Path: mustBe("DATA"), Summary: mustBe(" value: 12\nit failed coz: very custom error"), }) checkError(t, 12, td.Code(func(i int) error { return errors.New("very custom error") }), expectedError{ Message: mustBe("ran code with %% as argument"), Path: mustBe("DATA"), Summary: mustBe(" value: 12\nit failed coz: very custom error"), }) // Internal use checkError(t, 12, td.Code(func(i int) error { return &ctxerr.Error{ Message: "my message", Summary: ctxerr.NewSummary("my summary"), } }), expectedError{ Message: mustBe("my message"), Path: mustBe("DATA"), Summary: mustBe("my summary"), }) // // Bad usage checkError(t, "never tested", td.Code(nil), expectedError{ Message: mustBe("bad usage of Code operator"), Path: mustBe("DATA"), Summary: mustBe("usage: Code(FUNC), but received nil as 1st parameter"), }) checkError(t, "never tested", td.Code((func(string) bool)(nil)), expectedError{ Message: mustBe("bad usage of Code operator"), Path: mustBe("DATA"), Summary: mustBe("Code(FUNC): FUNC cannot be a nil function"), }) checkError(t, "never tested", td.Code("test"), expectedError{ Message: mustBe("bad usage of Code operator"), Path: mustBe("DATA"), Summary: mustBe("usage: Code(FUNC), but received string as 1st parameter"), }) checkError(t, "never tested", td.Code(func(x ...int) bool { return true }), expectedError{ Message: mustBe("bad usage of Code operator"), Path: mustBe("DATA"), Summary: mustBe("Code(FUNC): FUNC must take only one non-variadic argument or (*td.T, arg) or (*td.T, *td.T, arg)"), }) checkError(t, "never tested", td.Code(func() bool { return true }), expectedError{ Message: mustBe("bad usage of Code operator"), Path: mustBe("DATA"), Summary: mustBe("Code(FUNC): FUNC must take only one non-variadic argument or (*td.T, arg) or (*td.T, *td.T, arg)"), }) checkError(t, "never tested", td.Code(func(a, b, c, d string) bool { return true }), expectedError{ Message: mustBe("bad usage of Code operator"), Path: mustBe("DATA"), Summary: mustBe("Code(FUNC): FUNC must take only one non-variadic argument or (*td.T, arg) or (*td.T, *td.T, arg)"), }) checkError(t, "never tested", td.Code(func(a int, b string) bool { return true }), expectedError{ Message: mustBe("bad usage of Code operator"), Path: mustBe("DATA"), Summary: mustBe("Code(FUNC): FUNC must take only one non-variadic argument or (*td.T, arg) or (*td.T, *td.T, arg)"), }) checkError(t, "never tested", td.Code(func(t *td.T, a int, b string) bool { return true }), expectedError{ Message: mustBe("bad usage of Code operator"), Path: mustBe("DATA"), Summary: mustBe("Code(FUNC): FUNC must take only one non-variadic argument or (*td.T, arg) or (*td.T, *td.T, arg)"), }) checkError(t, "never tested", // because it is certainly an error td.Code(func(assert, require *td.T) bool { return true }), expectedError{ Message: mustBe("bad usage of Code operator"), Path: mustBe("DATA"), Summary: mustBe("Code(FUNC): FUNC must take only one non-variadic argument or (*td.T, arg) or (*td.T, *td.T, arg)"), }) checkError(t, "never tested", td.Code(func(n int) (bool, int) { return true, 0 }), expectedError{ Message: mustBe("bad usage of Code operator"), Path: mustBe("DATA"), Summary: mustBe("Code(FUNC): FUNC must return bool or (bool, string) or error"), }) checkError(t, "never tested", td.Code(func(n int) (error, string) { return nil, "" }), //nolint: staticcheck expectedError{ Message: mustBe("bad usage of Code operator"), Path: mustBe("DATA"), Summary: mustBe("Code(FUNC): FUNC must return bool or (bool, string) or error"), }) checkError(t, "never tested", td.Code(func(n int) (int, string) { return 0, "" }), expectedError{ Message: mustBe("bad usage of Code operator"), Path: mustBe("DATA"), Summary: mustBe("Code(FUNC): FUNC must return bool or (bool, string) or error"), }) checkError(t, "never tested", td.Code(func(n int) (string, bool) { return "", true }), expectedError{ Message: mustBe("bad usage of Code operator"), Path: mustBe("DATA"), Summary: mustBe("Code(FUNC): FUNC must return bool or (bool, string) or error"), }) checkError(t, "never tested", td.Code(func(n int) (bool, string, int) { return true, "", 0 }), expectedError{ Message: mustBe("bad usage of Code operator"), Path: mustBe("DATA"), Summary: mustBe("Code(FUNC): FUNC must return bool or (bool, string) or error"), }) checkError(t, "never tested", td.Code(func(n int) {}), expectedError{ Message: mustBe("bad usage of Code operator"), Path: mustBe("DATA"), Summary: mustBe("Code(FUNC): FUNC must return bool or (bool, string) or error"), }) checkError(t, "never tested", td.Code(func(n int) int { return 0 }), expectedError{ Message: mustBe("bad usage of Code operator"), Path: mustBe("DATA"), Summary: mustBe("Code(FUNC): FUNC must return bool or (bool, string) or error"), }) checkError(t, "never tested", td.Code(func(t *td.T, a int) bool { return true }), expectedError{ Message: mustBe("bad usage of Code operator"), Path: mustBe("DATA"), Summary: mustBe("Code(FUNC): FUNC must return nothing"), }) checkError(t, "never tested", td.Code(func(assert, require *td.T, a int) bool { return true }), expectedError{ Message: mustBe("bad usage of Code operator"), Path: mustBe("DATA"), Summary: mustBe("Code(FUNC): FUNC must return nothing"), }) // // String test.EqualStr(t, td.Code(func(n int) bool { return false }).String(), "Code(func(int) bool)") test.EqualStr(t, td.Code(func(n int) (bool, string) { return false, "" }).String(), "Code(func(int) (bool, string))") test.EqualStr(t, td.Code(func(n int) error { return nil }).String(), "Code(func(int) error)") test.EqualStr(t, td.Code(func(n int) (MyBool, MyString) { return false, "" }).String(), "Code(func(int) (td_test.MyBool, td_test.MyString))") // Erroneous op test.EqualStr(t, td.Code(nil).String(), "Code()") } func TestCodeCustom(t *testing.T) { // Specific _checkOK func as td.Code(FUNC) with FUNC(t,arg) or // FUNC(assert,require,arg) works in non-boolean context but cannot // work in boolean context as there is no initial testing.TB instance _customCheckOK := func(t *testing.T, got, expected any, args ...any) bool { t.Helper() if !td.Cmp(t, got, expected, args...) { return false } // Should always fail in boolean context as no original testing.TB available err := td.EqDeeplyError(got, expected) if err == nil { t.Error(`Boolean context succeeded and it shouldn't`) return false } expErr := expectedError{ Message: mustBe("cannot build *td.T instance"), Path: mustBe("DATA"), Summary: mustBe("original testing.TB instance is missing"), } if !strings.HasPrefix(expected.(fmt.Stringer).String(), "Code") { expErr = ifaceExpectedError(t, expErr) } if !matchError(t, err.(*ctxerr.Error), expErr, true, args...) { return false } if td.EqDeeply(got, expected) { t.Error(`Boolean context succeeded and it shouldn't`) return false } return true } customCheckOK(t, _customCheckOK, 123, td.Code(func(t *td.T, n int) { t.Cmp(t.Config.FailureIsFatal, false) t.Cmp(n, 123) })) customCheckOK(t, _customCheckOK, 123, td.Code(func(assert, require *td.T, n int) { assert.Cmp(assert.Config.FailureIsFatal, false) assert.Cmp(require.Config.FailureIsFatal, true) assert.Cmp(n, 123) require.Cmp(n, 123) })) got := map[string]int{"foo": 123} t.Run("Simple success", func(t *testing.T) { mockT := test.NewTestingTB("TestCodeCustom") td.Cmp(mockT, got, td.Map(map[string]int{}, td.MapEntries{ "foo": td.Code(func(t *td.T, n int) { t.Cmp(n, 123) }), })) test.EqualInt(t, len(mockT.Messages), 0) }) t.Run("Simple failure", func(t *testing.T) { mockT := test.NewTestingTB("TestCodeCustom") td.NewT(mockT). RootName("PIPO"). Cmp(got, td.Map(map[string]int{}, td.MapEntries{ "foo": td.Code(func(t *td.T, n int) { t.Cmp(n, 124) // inherit only RootName t.RootName(t.Config.OriginalPath()).Cmp(n, 125) // recover current path t.RootName("").Cmp(n, 126) // undo RootName inheritance }), })) test.IsTrue(t, mockT.HasFailed) test.IsFalse(t, mockT.IsFatal) missing := mockT.ContainsMessages( `PIPO: values differ`, ` got: 123`, `expected: 124`, `PIPO["foo"]: values differ`, ` got: 123`, `expected: 125`, `DATA: values differ`, ` got: 123`, `expected: 126`, ) if len(missing) != 0 { t.Error("Following expected messages are not found:\n-", strings.Join(missing, "\n- ")) t.Error("================================ in:") t.Error(strings.Join(mockT.Messages, "\n")) t.Error("====================================") } }) t.Run("AssertRequire success", func(t *testing.T) { mockT := test.NewTestingTB("TestCodeCustom") td.Cmp(mockT, got, td.Map(map[string]int{}, td.MapEntries{ "foo": td.Code(func(assert, require *td.T, n int) { assert.Cmp(n, 123) require.Cmp(n, 123) }), })) test.EqualInt(t, len(mockT.Messages), 0) }) t.Run("AssertRequire failure", func(t *testing.T) { mockT := test.NewTestingTB("TestCodeCustom") td.NewT(mockT). RootName("PIPO"). Cmp(got, td.Map(map[string]int{}, td.MapEntries{ "foo": td.Code(func(assert, require *td.T, n int) { assert.Cmp(n, 124) // inherit only RootName assert.RootName(assert.Config.OriginalPath()).Cmp(n, 125) // recover current path assert.RootName(require.Config.OriginalPath()).Cmp(n, 126) // recover current path assert.RootName("").Cmp(n, 127) // undo RootName inheritance }), })) test.IsTrue(t, mockT.HasFailed) test.IsFalse(t, mockT.IsFatal) missing := mockT.ContainsMessages( `PIPO: values differ`, ` got: 123`, `expected: 124`, `PIPO["foo"]: values differ`, ` got: 123`, `expected: 125`, `PIPO["foo"]: values differ`, ` got: 123`, `expected: 126`, `DATA: values differ`, ` got: 123`, `expected: 127`, ) if len(missing) != 0 { t.Error("Following expected messages are not found:\n-", strings.Join(missing, "\n- ")) t.Error("================================ in:") t.Error(strings.Join(mockT.Messages, "\n")) t.Error("====================================") } }) t.Run("AssertRequire fatalfailure", func(t *testing.T) { mockT := test.NewTestingTB("TestCodeCustom") td.NewT(mockT). RootName("PIPO"). Cmp(got, td.Map(map[string]int{}, td.MapEntries{ "foo": td.Code(func(assert, require *td.T, n int) { mockT.CatchFatal(func() { assert.RootName("FIRST").Cmp(n, 124) require.RootName("SECOND").Cmp(n, 125) assert.RootName("THIRD").Cmp(n, 126) }) }), })) test.IsTrue(t, mockT.HasFailed) test.IsTrue(t, mockT.IsFatal) missing := mockT.ContainsMessages( `FIRST: values differ`, ` got: 123`, `expected: 124`, `SECOND: values differ`, ` got: 123`, `expected: 125`, ) mesgs := strings.Join(mockT.Messages, "\n") if len(missing) != 0 { t.Error("Following expected messages are not found:\n-", strings.Join(missing, "\n- ")) t.Error("================================ in:") t.Error(mesgs) t.Error("====================================") } if strings.Contains(mesgs, "THIRD") { t.Error("THIRD test found, but shouldn't, in:") t.Error(mesgs) t.Error("====================================") } }) } func TestCodeTypeBehind(t *testing.T) { // Type behind is the code function parameter one equalTypes(t, td.Code(func(n int) bool { return n != 0 }), 23) equalTypes(t, td.Code(func(_ *td.T, n int) {}), 23) equalTypes(t, td.Code(func(_, _ *td.T, n int) {}), 23) type MyTime time.Time equalTypes(t, td.Code(func(t MyTime) bool { return time.Time(t).IsZero() }), MyTime{}) equalTypes(t, td.Code(func(_ *td.T, t MyTime) {}), MyTime{}) equalTypes(t, td.Code(func(_, _ *td.T, t MyTime) {}), MyTime{}) // Erroneous op equalTypes(t, td.Code(nil), nil) } golang-github-maxatome-go-testdeep-1.14.0/td/td_contains.go000066400000000000000000000235511454313311600236250ustar00rootroot00000000000000// Copyright (c) 2018, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td import ( "bytes" "reflect" "strings" "github.com/maxatome/go-testdeep/helpers/tdutil" "github.com/maxatome/go-testdeep/internal/ctxerr" "github.com/maxatome/go-testdeep/internal/types" "github.com/maxatome/go-testdeep/internal/util" ) type tdContains struct { tdSmugglerBase } var _ TestDeep = &tdContains{} // summary(Contains): checks that a string, []byte, error or // fmt.Stringer interfaces contain a rune, byte or a sub-string; or a // slice contains a single value or a sub-slice; or an array or map // contain a single value // input(Contains): str,array,slice,map,if(✓ + fmt.Stringer/error) // Contains is a smuggler operator to check if something is contained // in another thing. Contains has to be applied on arrays, slices, maps or // strings. It tries to be as smarter as possible. // // If expectedValue is a [TestDeep] operator, each item of data // array/slice/map/string (rune for strings) is compared to it. The // use of a [TestDeep] operator as expectedValue works only in this // way: item per item. // // If data is a slice, and expectedValue has the same type, then // expectedValue is searched as a sub-slice, otherwise // expectedValue is compared to each slice value. // // list := []int{12, 34, 28} // td.Cmp(t, list, td.Contains(34)) // succeeds // td.Cmp(t, list, td.Contains(td.Between(30, 35))) // succeeds too // td.Cmp(t, list, td.Contains(35)) // fails // td.Cmp(t, list, td.Contains([]int{34, 28})) // succeeds // // If data is an array or a map, each value is compared to // expectedValue. Map keys are not checked: see [ContainsKey] to check // map keys existence. // // hash := map[string]int{"foo": 12, "bar": 34, "zip": 28} // td.Cmp(t, hash, td.Contains(34)) // succeeds // td.Cmp(t, hash, td.Contains(td.Between(30, 35))) // succeeds too // td.Cmp(t, hash, td.Contains(35)) // fails // // array := [...]int{12, 34, 28} // td.Cmp(t, array, td.Contains(34)) // succeeds // td.Cmp(t, array, td.Contains(td.Between(30, 35))) // succeeds too // td.Cmp(t, array, td.Contains(35)) // fails // // If data is a string (or convertible), []byte (or convertible), // error or [fmt.Stringer] interface (error interface is tested before // [fmt.Stringer]), expectedValue can be a string, a []byte, a rune or // a byte. In this case, it tests if the got string contains this // expected string, []byte, rune or byte. // // got := "foo bar" // td.Cmp(t, got, td.Contains('o')) // succeeds // td.Cmp(t, got, td.Contains(rune('o'))) // succeeds // td.Cmp(t, got, td.Contains(td.Between('n', 'p'))) // succeeds // td.Cmp(t, got, td.Contains("bar")) // succeeds // td.Cmp(t, got, td.Contains([]byte("bar"))) // succeeds // // td.Cmp(t, []byte("foobar"), td.Contains("ooba")) // succeeds // // type Foobar string // td.Cmp(t, Foobar("foobar"), td.Contains("ooba")) // succeeds // // err := errors.New("error!") // td.Cmp(t, err, td.Contains("ror")) // succeeds // // bstr := bytes.NewBufferString("fmt.Stringer!") // td.Cmp(t, bstr, td.Contains("String")) // succeeds // // Pitfall: if you want to check if 2 words are contained in got, don't do: // // td.Cmp(t, "foobar", td.Contains(td.All("foo", "bar"))) // Bad! // // as [TestDeep] operator [All] in Contains operates on each rune, so it // does not work as expected, but do:: // // td.Cmp(t, "foobar", td.All(td.Contains("foo"), td.Contains("bar"))) // // When Contains(nil) is used, nil is automatically converted to a // typed nil on the fly to avoid confusion (if the array/slice/map // item type allows it of course.) So all following [Cmp] calls // are equivalent (except the (*byte)(nil) one): // // num := 123 // list := []*int{&num, nil} // td.Cmp(t, list, td.Contains(nil)) // succeeds → (*int)(nil) // td.Cmp(t, list, td.Contains((*int)(nil))) // succeeds // td.Cmp(t, list, td.Contains(td.Nil())) // succeeds // // But... // td.Cmp(t, list, td.Contains((*byte)(nil))) // fails: (*byte)(nil) ≠ (*int)(nil) // // As well as these ones: // // hash := map[string]*int{"foo": nil, "bar": &num} // td.Cmp(t, hash, td.Contains(nil)) // succeeds → (*int)(nil) // td.Cmp(t, hash, td.Contains((*int)(nil))) // succeeds // td.Cmp(t, hash, td.Contains(td.Nil())) // succeeds // // See also [ContainsKey]. func Contains(expectedValue any) TestDeep { c := tdContains{ tdSmugglerBase: newSmugglerBase(expectedValue), } if !c.isTestDeeper { c.expectedValue = reflect.ValueOf(expectedValue) } return &c } func (c *tdContains) doesNotContainErr(ctx ctxerr.Context, got any) *ctxerr.Error { if ctx.BooleanError { return ctxerr.BooleanError } return ctx.CollectError(&ctxerr.Error{ Message: "does not contain", Got: got, Expected: c, }) } // getExpectedValue returns the expected value handling the // Contains(nil) case: in this case it returns a typed nil (same type // as the items of got). // got is an array, a slice or a map (it's the caller responsibility to check). func (c *tdContains) getExpectedValue(got reflect.Value) reflect.Value { // If the expectValue is non-typed nil if !c.expectedValue.IsValid() { // AND the kind of items in got is... switch got.Type().Elem().Kind() { case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice: // returns a typed nil return reflect.Zero(got.Type().Elem()) } } return c.expectedValue } func (c *tdContains) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error { switch got.Kind() { case reflect.Slice: if !c.isTestDeeper && c.expectedValue.IsValid() { // Special case for []byte & expected []byte or string if got.Type().Elem() == types.Uint8 { switch c.expectedValue.Kind() { case reflect.String: if bytes.Contains(got.Bytes(), []byte(c.expectedValue.String())) { return nil } return c.doesNotContainErr(ctx, got) case reflect.Slice: if c.expectedValue.Type().Elem() == types.Uint8 { if bytes.Contains(got.Bytes(), c.expectedValue.Bytes()) { return nil } return c.doesNotContainErr(ctx, got) } case reflect.Int32: // rune if bytes.ContainsRune(got.Bytes(), rune(c.expectedValue.Int())) { return nil } return c.doesNotContainErr(ctx, got) case reflect.Uint8: // byte if bytes.ContainsRune(got.Bytes(), rune(c.expectedValue.Uint())) { return nil } return c.doesNotContainErr(ctx, got) } // fall back on string conversion break } // Search slice in slice if got.Type() == c.expectedValue.Type() { gotLen, expectedLen := got.Len(), c.expectedValue.Len() if expectedLen == 0 { return nil } if expectedLen > gotLen { return c.doesNotContainErr(ctx, got) } if expectedLen == gotLen { if deepValueEqualOK(got, c.expectedValue) { return nil } return c.doesNotContainErr(ctx, got) } for i := 0; i <= gotLen-expectedLen; i++ { if deepValueEqualOK(got.Slice(i, i+expectedLen), c.expectedValue) { return nil } } } } fallthrough case reflect.Array: expectedValue := c.getExpectedValue(got) for index := got.Len() - 1; index >= 0; index-- { if deepValueEqualFinalOK(ctx, got.Index(index), expectedValue) { return nil } } return c.doesNotContainErr(ctx, got) case reflect.Map: expectedValue := c.getExpectedValue(got) if !tdutil.MapEachValue(got, func(v reflect.Value) bool { return !deepValueEqualFinalOK(ctx, v, expectedValue) }) { return nil } return c.doesNotContainErr(ctx, got) } str, err := getString(ctx, got) if err != nil { return err } // If a TestDeep operator is expected, applies this operator on // each character of the string if c.isTestDeeper { // If the type behind the operator is known *and* is not rune, // then no need to go further, but return an explicit error to // help our user to fix his probably bogus code op := c.expectedValue.Interface().(TestDeep) if typeBehind := op.TypeBehind(); typeBehind != nil && typeBehind != types.Rune && !ctx.BeLax { if ctx.BooleanError { return ctxerr.BooleanError } return ctx.CollectError(&ctxerr.Error{ Message: op.GetLocation().Func + " operator has to match rune in string, but it does not", Got: types.RawString(typeBehind.String()), Expected: types.RawString("rune"), }) } for _, chr := range str { if deepValueEqualFinalOK(ctx, reflect.ValueOf(chr), c.expectedValue) { return nil } } return c.doesNotContainErr(ctx, got) } // If expectedValue is a []byte, a string, a rune or a byte, we // check whether it is contained in the string or not var contains bool switch expectedKind := c.expectedValue.Kind(); expectedKind { case reflect.String: contains = strings.Contains(str, c.expectedValue.String()) case reflect.Int32: // rune contains = strings.ContainsRune(str, rune(c.expectedValue.Int())) case reflect.Uint8: // byte contains = strings.ContainsRune(str, rune(c.expectedValue.Uint())) case reflect.Slice: // Only []byte if c.expectedValue.Type().Elem() == types.Uint8 { contains = strings.Contains(str, string(c.expectedValue.Bytes())) break } fallthrough default: if ctx.BooleanError { return ctxerr.BooleanError } var expectedType any if c.expectedValue.IsValid() { expectedType = types.RawString(c.expectedValue.Type().String()) } else { expectedType = c } return ctx.CollectError(&ctxerr.Error{ Message: "cannot check contains", Got: types.RawString(got.Type().String()), Expected: expectedType, }) } if contains { return nil } return c.doesNotContainErr(ctx, str) } func (c *tdContains) String() string { return "Contains(" + util.ToString(c.expectedValue) + ")" } golang-github-maxatome-go-testdeep-1.14.0/td/td_contains_key.go000066400000000000000000000100471454313311600244710ustar00rootroot00000000000000// Copyright (c) 2018, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td import ( "reflect" "github.com/maxatome/go-testdeep/helpers/tdutil" "github.com/maxatome/go-testdeep/internal/ctxerr" "github.com/maxatome/go-testdeep/internal/types" "github.com/maxatome/go-testdeep/internal/util" ) type tdContainsKey struct { tdSmugglerBase } var _ TestDeep = &tdContainsKey{} // summary(ContainsKey): checks that a map contains a key // input(ContainsKey): map // ContainsKey is a smuggler operator and works on maps only. It // compares each key of map against expectedValue. // // hash := map[string]int{"foo": 12, "bar": 34, "zip": 28} // td.Cmp(t, hash, td.ContainsKey("foo")) // succeeds // td.Cmp(t, hash, td.ContainsKey(td.HasPrefix("z"))) // succeeds // td.Cmp(t, hash, td.ContainsKey(td.HasPrefix("x"))) // fails // // hnum := map[int]string{1: "foo", 42: "bar"} // td.Cmp(t, hash, td.ContainsKey(42)) // succeeds // td.Cmp(t, hash, td.ContainsKey(td.Between(40, 45))) // succeeds // // When ContainsKey(nil) is used, nil is automatically converted to a // typed nil on the fly to avoid confusion (if the map key type allows // it of course.) So all following [Cmp] calls are equivalent // (except the (*byte)(nil) one): // // num := 123 // hnum := map[*int]bool{&num: true, nil: true} // td.Cmp(t, hnum, td.ContainsKey(nil)) // succeeds → (*int)(nil) // td.Cmp(t, hnum, td.ContainsKey((*int)(nil))) // succeeds // td.Cmp(t, hnum, td.ContainsKey(td.Nil())) // succeeds // // But... // td.Cmp(t, hnum, td.ContainsKey((*byte)(nil))) // fails: (*byte)(nil) ≠ (*int)(nil) // // See also [Contains]. func ContainsKey(expectedValue any) TestDeep { c := tdContainsKey{ tdSmugglerBase: newSmugglerBase(expectedValue), } if !c.isTestDeeper { c.expectedValue = reflect.ValueOf(expectedValue) } return &c } func (c *tdContainsKey) doesNotContainKey(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error { if ctx.BooleanError { return ctxerr.BooleanError } return ctx.CollectError(&ctxerr.Error{ Message: "does not contain key", Summary: ctxerr.ErrorSummaryItems{ { Label: "expected key", Value: util.ToString(c.expectedValue), }, { Label: "not in keys", Value: util.ToString(tdutil.MapSortedKeys(got)), }, }, }) } // getExpectedValue returns the expected value handling the // Contains(nil) case: in this case it returns a typed nil (same type // as the keys of got). // got is a map (it's the caller responsibility to check). func (c *tdContainsKey) getExpectedValue(got reflect.Value) reflect.Value { // If the expectValue is non-typed nil if !c.expectedValue.IsValid() { // AND the kind of items in got is... switch got.Type().Key().Kind() { case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice: // returns a typed nil return reflect.Zero(got.Type().Key()) } } return c.expectedValue } func (c *tdContainsKey) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error { if got.Kind() == reflect.Map { expectedValue := c.getExpectedValue(got) // If expected value is a TestDeep operator OR BeLax, check each key if c.isTestDeeper || ctx.BeLax { for _, k := range got.MapKeys() { if deepValueEqualFinalOK(ctx, k, expectedValue) { return nil } } } else if expectedValue.IsValid() && got.Type().Key() == expectedValue.Type() && got.MapIndex(expectedValue).IsValid() { return nil } return c.doesNotContainKey(ctx, got) } if ctx.BooleanError { return ctxerr.BooleanError } var expectedType any if c.expectedValue.IsValid() { expectedType = types.RawString(c.expectedValue.Type().String()) } else { expectedType = c } return ctx.CollectError(&ctxerr.Error{ Message: "cannot check contains key", Got: types.RawString(got.Type().String()), Expected: expectedType, }) } func (c *tdContainsKey) String() string { return "ContainsKey(" + util.ToString(c.expectedValue) + ")" } golang-github-maxatome-go-testdeep-1.14.0/td/td_contains_key_test.go000066400000000000000000000055271454313311600255370ustar00rootroot00000000000000// Copyright (c) 2018, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td_test import ( "fmt" "testing" "github.com/maxatome/go-testdeep/td" ) func TestContainsKey(t *testing.T) { type MyMap map[int]string for idx, got := range []any{ map[int]string{12: "foo", 34: "bar", 28: "zip"}, MyMap{12: "foo", 34: "bar", 28: "zip"}, } { testName := fmt.Sprintf("#%d: got=%v", idx, got) checkOK(t, got, td.ContainsKey(34), testName) checkOK(t, got, td.ContainsKey(td.Between(30, 35)), testName) checkError(t, got, td.ContainsKey(35), expectedError{ Message: mustBe("does not contain key"), Path: mustBe("DATA"), Summary: mustMatch(`expected key: 35 not in keys: \((12|28|34), (12|28|34), (12|28|34)\)`), }, testName) // Lax checkOK(t, got, td.Lax(td.ContainsKey(float64(34))), testName) } } // nil case. func TestContainsKeyNil(t *testing.T) { type MyPtrMap map[*int]int num := 12345642 for idx, got := range []any{ map[*int]int{&num: 42, nil: 666}, MyPtrMap{&num: 42, nil: 666}, } { testName := fmt.Sprintf("#%d: got=%v", idx, got) checkOK(t, got, td.ContainsKey(nil), testName) checkOK(t, got, td.ContainsKey((*int)(nil)), testName) checkOK(t, got, td.ContainsKey(td.Nil()), testName) checkOK(t, got, td.ContainsKey(td.NotNil()), testName) checkError(t, got, td.ContainsKey((*uint8)(nil)), expectedError{ Message: mustBe("does not contain key"), Path: mustBe("DATA"), Summary: mustMatch(`expected key: \(\*uint8\)\(\) not in keys: \(\(\*int\)\((|.*12345642.*)\), \(\*int\)\((|.*12345642.*)\)\)`), }, testName) } checkError(t, map[string]int{"foo": 12, "bar": 34, "zip": 28}, // got td.ContainsKey(nil), expectedError{ Message: mustBe("does not contain key"), Path: mustBe("DATA"), Summary: mustMatch(`expected key: nil not in keys: \("(foo|bar|zip)", "(foo|bar|zip)", "(foo|bar|zip)"\)`), }) checkError(t, "foobar", td.ContainsKey(nil), expectedError{ Message: mustBe("cannot check contains key"), Path: mustBe("DATA"), Got: mustBe("string"), Expected: mustBe("ContainsKey(nil)"), }) checkError(t, "foobar", td.ContainsKey(123), expectedError{ Message: mustBe("cannot check contains key"), Path: mustBe("DATA"), Got: mustBe("string"), Expected: mustBe("int"), }) // Caught by deepValueEqual, before Match() call checkError(t, nil, td.ContainsKey(nil), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA"), Got: mustBe("nil"), Expected: mustBe("ContainsKey(nil)"), }) } func TestContainsKeyTypeBehind(t *testing.T) { equalTypes(t, td.ContainsKey("x"), nil) } golang-github-maxatome-go-testdeep-1.14.0/td/td_contains_test.go000066400000000000000000000177661454313311600246770ustar00rootroot00000000000000// Copyright (c) 2018-2022, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td_test import ( "errors" "fmt" "reflect" "testing" "github.com/maxatome/go-testdeep/td" ) func TestContains(t *testing.T) { type ( MySlice []int MyArray [3]int MyMap map[string]int MyString string ) for idx, got := range []any{ []int{12, 34, 28}, MySlice{12, 34, 28}, [...]int{12, 34, 28}, MyArray{12, 34, 28}, map[string]int{"foo": 12, "bar": 34, "zip": 28}, MyMap{"foo": 12, "bar": 34, "zip": 28}, } { testName := fmt.Sprintf("#%d: got=%v", idx, got) checkOK(t, got, td.Contains(34), testName) checkOK(t, got, td.Contains(td.Between(30, 35)), testName) checkError(t, got, td.Contains(35), expectedError{ Message: mustBe("does not contain"), Path: mustBe("DATA"), Got: mustContain("34"), // as well as other items in fact... Expected: mustBe("Contains(35)"), }, testName) // Lax checkOK(t, got, td.Lax(td.Contains(float64(34))), testName) } for idx, got := range []any{ "foobar", MyString("foobar"), } { testName := fmt.Sprintf("#%d: got=%v", idx, got) checkOK(t, got, td.Contains(td.Between('n', 'p')), testName) checkError(t, got, td.Contains(td.Between('y', 'z')), expectedError{ Message: mustBe("does not contain"), Path: mustBe("DATA"), Got: mustContain(`"foobar"`), // as well as other items in fact... Expected: mustBe(fmt.Sprintf("Contains((int32) %d ≤ got ≤ (int32) %d)", 'y', 'z')), }, testName) } } // nil case. func TestContainsNil(t *testing.T) { type ( MyPtrSlice []*int MyPtrArray [3]*int MyPtrMap map[string]*int ) num := 12345642 for idx, got := range []any{ []*int{&num, nil}, MyPtrSlice{&num, nil}, [...]*int{&num, nil}, MyPtrArray{&num}, map[string]*int{"foo": &num, "bar": nil}, MyPtrMap{"foo": &num, "bar": nil}, } { testName := fmt.Sprintf("#%d: got=%v", idx, got) checkOK(t, got, td.Contains(nil), testName) checkOK(t, got, td.Contains((*int)(nil)), testName) checkOK(t, got, td.Contains(td.Nil()), testName) checkOK(t, got, td.Contains(td.NotNil()), testName) checkError(t, got, td.Contains((*uint8)(nil)), expectedError{ Message: mustBe("does not contain"), Path: mustBe("DATA"), Got: mustContain("12345642"), Expected: mustBe("Contains((*uint8)())"), }, testName) } for idx, got := range []any{ []any{nil, 12345642}, []func(){nil, func() {}}, [][]int{{}, nil}, [...]any{nil, 12345642}, [...]func(){nil, func() {}}, [...][]int{{}, nil}, map[bool]any{true: nil, false: 12345642}, map[bool]func(){true: nil, false: func() {}}, map[bool][]int{true: {}, false: nil}, } { testName := fmt.Sprintf("#%d: got=%v", idx, got) checkOK(t, got, td.Contains(nil), testName) checkOK(t, got, td.Contains(td.Nil()), testName) checkOK(t, got, td.Contains(td.NotNil()), testName) } for idx, got := range []any{ []int{1, 2, 3}, [...]int{1, 2, 3}, map[string]int{"foo": 12, "bar": 34, "zip": 28}, } { testName := fmt.Sprintf("#%d: got=%v", idx, got) checkError(t, got, td.Contains(nil), expectedError{ Message: mustBe("does not contain"), Path: mustBe("DATA"), // Got Expected: mustBe("Contains(nil)"), }, testName) } checkError(t, "foobar", td.Contains(nil), expectedError{ Message: mustBe("cannot check contains"), Path: mustBe("DATA"), Got: mustBe("string"), Expected: mustBe("Contains(nil)"), }) // Caught by deepValueEqual, before Match() call checkError(t, nil, td.Contains(nil), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA"), Got: mustBe("nil"), Expected: mustBe("Contains(nil)"), }) } func TestContainsString(t *testing.T) { type MyString string for idx, got := range []any{ "pipo bingo", MyString("pipo bingo"), []byte("pipo bingo"), errors.New("pipo bingo"), // error interface MyStringer{}, // fmt.Stringer interface } { testName := fmt.Sprintf("#%d: got=%v", idx, got) checkOK(t, got, td.Contains("pipo"), testName) checkOK(t, got, td.Contains("po bi"), testName) checkOK(t, got, td.Contains("bingo"), testName) checkOK(t, got, td.Contains([]byte("pipo")), testName) checkOK(t, got, td.Contains([]byte("po bi")), testName) checkOK(t, got, td.Contains([]byte("bingo")), testName) checkOK(t, got, td.Contains('o'), testName) checkOK(t, got, td.Contains(byte('o')), testName) checkOK(t, got, td.Contains(""), testName) checkOK(t, got, td.Contains([]byte{}), testName) if _, ok := got.([]byte); ok { checkOK(t, got, td.Contains(td.Code(func(b byte) bool { return b == 'o' })), testName) } else { checkOK(t, got, td.Contains(td.Code(func(r rune) bool { return r == 'o' })), testName) } checkError(t, got, td.Contains("zip"), expectedError{ Message: mustBe("does not contain"), Path: mustBe("DATA"), Got: mustContain(`pipo bingo`), Expected: mustMatch(`^Contains\(.*"zip"`), }) checkError(t, got, td.Contains([]byte("zip")), expectedError{ Message: mustBe("does not contain"), Path: mustBe("DATA"), Got: mustContain(`pipo bingo`), Expected: mustMatch(`^(?s)Contains\(.*zip`), }) checkError(t, got, td.Contains('z'), expectedError{ Message: mustBe("does not contain"), Path: mustBe("DATA"), Got: mustContain(`pipo bingo`), Expected: mustBe(`Contains((int32) 122)`), }) checkError(t, got, td.Contains(byte('z')), expectedError{ Message: mustBe("does not contain"), Path: mustBe("DATA"), Got: mustContain(`pipo bingo`), Expected: mustBe(`Contains((uint8) 122)`), }) checkError(t, got, td.Contains(12), expectedError{ Message: mustBe("cannot check contains"), Path: mustBe("DATA"), Got: mustBe(reflect.TypeOf(got).String()), Expected: mustBe("int"), }) checkError(t, got, td.Contains([]int{1, 2, 3}), expectedError{ Message: mustBe("cannot check contains"), Path: mustBe("DATA"), Got: mustBe(reflect.TypeOf(got).String()), Expected: mustBe("[]int"), }) // Lax checkOK(t, got, td.Lax(td.Contains(td.Code(func(b int) bool { return b == 'o' }))), testName) } checkError(t, 12, td.Contains("bar"), expectedError{ Message: mustBe("bad type"), Path: mustBe("DATA"), Got: mustBe("int"), Expected: mustBe("string (convertible) OR []byte (convertible) OR fmt.Stringer OR error"), }) checkError(t, "pipo", td.Contains(td.Code(func(x int) bool { return true })), expectedError{ Message: mustBe("Code operator has to match rune in string, but it does not"), Path: mustBe("DATA"), Got: mustBe("int"), Expected: mustBe("rune"), }) } func TestContainsSlice(t *testing.T) { got := []int{1, 2, 3, 4, 5, 6} // Empty slice is always OK checkOK(t, got, td.Contains([]int{})) // Expected length > got length checkError(t, got, td.Contains([]int{1, 2, 3, 4, 5, 6, 7}), expectedError{ Message: mustBe("does not contain"), Path: mustBe("DATA"), Got: mustContain(`([]int) (len=6 `), Expected: mustContain(`Contains(([]int) (len=7 `), }) // Same length checkOK(t, got, td.Contains([]int{1, 2, 3, 4, 5, 6})) checkError(t, got, td.Contains([]int{8, 8, 8, 8, 8, 8}), expectedError{ Message: mustBe("does not contain"), Path: mustBe("DATA"), Got: mustContain(`([]int) (len=6 `), Expected: mustContain(`Contains(([]int) (len=6 `), }) checkOK(t, got, td.Contains([]int{1, 2, 3})) checkOK(t, got, td.Contains([]int{3, 4, 5})) checkOK(t, got, td.Contains([]int{4, 5, 6})) checkError(t, got, td.Contains([]int{8, 8, 8}), expectedError{ Message: mustBe("does not contain"), Path: mustBe("DATA"), Got: mustContain(`([]int) (len=6 `), Expected: mustContain(`Contains(([]int) (len=3 `), }) } func TestContainsTypeBehind(t *testing.T) { equalTypes(t, td.Contains("x"), nil) } golang-github-maxatome-go-testdeep-1.14.0/td/td_delay.go000066400000000000000000000031041454313311600230750ustar00rootroot00000000000000// Copyright (c) 2020, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td import ( "reflect" "sync" "github.com/maxatome/go-testdeep/internal/ctxerr" ) type tdDelay struct { base operator TestDeep once sync.Once delayed func() TestDeep } var _ TestDeep = &tdDelay{} // summary(Delay): delays the operator construction till first use // input(Delay): all // Delay operator allows to delay the construction of an operator to // the time it is used for the first time. Most of the time, it is // used with helpers. See the example for a very simple use case. func Delay(delayed func() TestDeep) TestDeep { d := tdDelay{ base: newBase(3), delayed: delayed, } if delayed == nil { d.err = ctxerr.OpBad("Delay", "Delay(DELAYED): DELAYED must be non-nil") } return &d } func (d *tdDelay) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error { if d.err != nil { return ctx.CollectError(d.err) } op := d.getOperator() ctx.CurOperator = op // to have correct location return op.Match(ctx, got) } func (d *tdDelay) String() string { if d.err != nil { return d.stringError() } return d.getOperator().String() } func (d *tdDelay) TypeBehind() reflect.Type { if d.err != nil { return nil } return d.getOperator().TypeBehind() } func (d *tdDelay) HandleInvalid() bool { return d.getOperator().HandleInvalid() } func (d *tdDelay) getOperator() TestDeep { d.once.Do(func() { d.operator = d.delayed() }) return d.operator } golang-github-maxatome-go-testdeep-1.14.0/td/td_delay_test.go000066400000000000000000000026531454313311600241440ustar00rootroot00000000000000// Copyright (c) 2020, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td_test import ( "testing" "github.com/maxatome/go-testdeep/internal/test" "github.com/maxatome/go-testdeep/td" ) func TestDelay(t *testing.T) { called := 0 op := td.Delay(func() td.TestDeep { called++ return td.Lt(13) }) test.EqualInt(t, called, 0) checkOK(t, 12, op) test.EqualInt(t, called, 1) checkOK(t, 12, op) test.EqualInt(t, called, 1) delayNil := td.Delay(td.Nil) checkOK(t, nil, delayNil) test.EqualStr(t, delayNil.String(), "nil") checkError(t, 8, td.Delay( func() td.TestDeep { return td.Gt(13) }, ), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA"), Got: mustBe("8"), Expected: mustBe("> 13"), }) // Bad usage checkError(t, "never tested", td.Delay(nil), expectedError{ Message: mustBe("bad usage of Delay operator"), Path: mustBe("DATA"), Summary: mustBe("Delay(DELAYED): DELAYED must be non-nil"), }) // Erroneous op test.EqualStr(t, td.Delay(nil).String(), "Delay()") } func TestDelayTypeBehind(t *testing.T) { equalTypes(t, td.Delay(func() td.TestDeep { return td.String("x") }), nil) equalTypes(t, td.Delay(func() td.TestDeep { return td.Gt(16) }), 42) // Erroneous op equalTypes(t, td.Delay(nil), nil) } golang-github-maxatome-go-testdeep-1.14.0/td/td_empty.go000066400000000000000000000074451454313311600231510ustar00rootroot00000000000000// Copyright (c) 2018, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td import ( "reflect" "github.com/maxatome/go-testdeep/internal/ctxerr" "github.com/maxatome/go-testdeep/internal/types" ) const emptyBadKind = "array OR chan OR map OR slice OR string OR pointer(s) on them" type tdEmpty struct { baseOKNil } var _ TestDeep = &tdEmpty{} // summary(Empty): checks that an array, a channel, a map, a slice or // a string is empty // input(Empty): str,array,slice,map,ptr(ptr on array/slice/map/string),chan // Empty operator checks that an array, a channel, a map, a slice or a // string is empty. As a special case (non-typed) nil, as well as nil // channel, map or slice are considered empty. // // Note that the compared data can be a pointer (of pointer of pointer // etc.) on an array, a channel, a map, a slice or a string. // // td.Cmp(t, "", td.Empty()) // succeeds // td.Cmp(t, map[string]bool{}, td.Empty()) // succeeds // td.Cmp(t, []string{"foo"}, td.Empty()) // fails func Empty() TestDeep { return &tdEmpty{ baseOKNil: newBaseOKNil(3), } } // isEmpty returns (isEmpty, kindError) boolean values with only 3 // possible cases: // - true, false → "got" is empty // - false, false → "got" is not empty // - false, true → "got" kind is not compatible with emptiness func isEmpty(got reflect.Value) (bool, bool) { switch got.Kind() { case reflect.Array, reflect.Chan, reflect.Map, reflect.Slice, reflect.String: return got.Len() == 0, false case reflect.Ptr: switch got.Type().Elem().Kind() { case reflect.Array, reflect.Chan, reflect.Map, reflect.Slice, reflect.String: if got.IsNil() { return true, false } fallthrough case reflect.Ptr: return isEmpty(got.Elem()) default: return false, true // bad kind } default: // nil case if !got.IsValid() { return true, false } return false, true // bad kind } } func (e *tdEmpty) Match(ctx ctxerr.Context, got reflect.Value) (err *ctxerr.Error) { ok, badKind := isEmpty(got) if ok { return nil } if ctx.BooleanError { return ctxerr.BooleanError } if badKind { return ctx.CollectError(ctxerr.BadKind(got, emptyBadKind)) } return ctx.CollectError(&ctxerr.Error{ Message: "not empty", Got: got, Expected: types.RawString("empty"), }) } func (e *tdEmpty) String() string { return "Empty()" } type tdNotEmpty struct { baseOKNil } var _ TestDeep = &tdNotEmpty{} // summary(NotEmpty): checks that an array, a channel, a map, a slice // or a string is not empty // input(NotEmpty): str,array,slice,map,ptr(ptr on array/slice/map/string),chan // NotEmpty operator checks that an array, a channel, a map, a slice // or a string is not empty. As a special case (non-typed) nil, as // well as nil channel, map or slice are considered empty. // // Note that the compared data can be a pointer (of pointer of pointer // etc.) on an array, a channel, a map, a slice or a string. // // td.Cmp(t, "", td.NotEmpty()) // fails // td.Cmp(t, map[string]bool{}, td.NotEmpty()) // fails // td.Cmp(t, []string{"foo"}, td.NotEmpty()) // succeeds func NotEmpty() TestDeep { return &tdNotEmpty{ baseOKNil: newBaseOKNil(3), } } func (e *tdNotEmpty) Match(ctx ctxerr.Context, got reflect.Value) (err *ctxerr.Error) { ok, badKind := isEmpty(got) if ok { if ctx.BooleanError { return ctxerr.BooleanError } return ctx.CollectError(&ctxerr.Error{ Message: "empty", Got: got, Expected: types.RawString("not empty"), }) } if badKind { if ctx.BooleanError { return ctxerr.BooleanError } return ctx.CollectError(ctxerr.BadKind(got, emptyBadKind)) } return nil } func (e *tdNotEmpty) String() string { return "NotEmpty()" } golang-github-maxatome-go-testdeep-1.14.0/td/td_empty_test.go000066400000000000000000000123651454313311600242050ustar00rootroot00000000000000// Copyright (c) 2018, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td_test import ( "testing" "github.com/maxatome/go-testdeep/internal/test" "github.com/maxatome/go-testdeep/td" ) func TestEmpty(t *testing.T) { checkOK(t, nil, td.Empty()) checkOK(t, "", td.Empty()) checkOK(t, ([]int)(nil), td.Empty()) checkOK(t, []int{}, td.Empty()) checkOK(t, (map[string]bool)(nil), td.Empty()) checkOK(t, map[string]bool{}, td.Empty()) checkOK(t, (chan int)(nil), td.Empty()) checkOK(t, make(chan int), td.Empty()) checkOK(t, [0]int{}, td.Empty()) type MySlice []int checkOK(t, MySlice{}, td.Empty()) checkOK(t, &MySlice{}, td.Empty()) l1 := &MySlice{} l2 := &l1 l3 := &l2 checkOK(t, &l3, td.Empty()) l1 = nil checkOK(t, &l3, td.Empty()) checkError(t, 12, td.Empty(), expectedError{ Message: mustBe("bad kind"), Path: mustBe("DATA"), Got: mustBe("int"), Expected: mustBe("array OR chan OR map OR slice OR string OR pointer(s) on them"), }) num := 12 n1 := &num n2 := &n1 n3 := &n2 checkError(t, &n3, td.Empty(), expectedError{ Message: mustBe("bad kind"), Path: mustBe("DATA"), Got: mustBe("****int"), Expected: mustBe("array OR chan OR map OR slice OR string OR pointer(s) on them"), }) n1 = nil checkError(t, &n3, td.Empty(), expectedError{ Message: mustBe("bad kind"), Path: mustBe("DATA"), Got: mustBe("****int"), Expected: mustBe("array OR chan OR map OR slice OR string OR pointer(s) on them"), }) checkError(t, "foobar", td.Empty(), expectedError{ Message: mustBe("not empty"), Path: mustBe("DATA"), Got: mustContain(`"foobar"`), Expected: mustBe("empty"), }) checkError(t, []int{1}, td.Empty(), expectedError{ Message: mustBe("not empty"), Path: mustBe("DATA"), Got: mustContain("1"), Expected: mustBe("empty"), }) checkError(t, map[string]bool{"foo": true}, td.Empty(), expectedError{ Message: mustBe("not empty"), Path: mustBe("DATA"), Got: mustContain(`"foo": (bool) true`), Expected: mustBe("empty"), }) ch := make(chan int, 1) ch <- 42 checkError(t, ch, td.Empty(), expectedError{ Message: mustBe("not empty"), Path: mustBe("DATA"), Got: mustContain("(chan int)"), Expected: mustBe("empty"), }) checkError(t, [3]int{}, td.Empty(), expectedError{ Message: mustBe("not empty"), Path: mustBe("DATA"), Got: mustContain("0"), Expected: mustBe("empty"), }) // // String test.EqualStr(t, td.Empty().String(), "Empty()") } func TestNotEmpty(t *testing.T) { checkOK(t, "foobar", td.NotEmpty()) checkOK(t, []int{1}, td.NotEmpty()) checkOK(t, map[string]bool{"foo": true}, td.NotEmpty()) checkOK(t, [3]int{}, td.NotEmpty()) ch := make(chan int, 1) ch <- 42 checkOK(t, ch, td.NotEmpty()) type MySlice []int checkOK(t, MySlice{1}, td.NotEmpty()) checkOK(t, &MySlice{1}, td.NotEmpty()) l1 := &MySlice{1} l2 := &l1 l3 := &l2 checkOK(t, &l3, td.NotEmpty()) checkError(t, 12, td.NotEmpty(), expectedError{ Message: mustBe("bad kind"), Path: mustBe("DATA"), Got: mustBe("int"), Expected: mustBe("array OR chan OR map OR slice OR string OR pointer(s) on them"), }) checkError(t, nil, td.NotEmpty(), expectedError{ Message: mustBe("empty"), Path: mustBe("DATA"), Got: mustContain("nil"), Expected: mustBe("not empty"), }) checkError(t, "", td.NotEmpty(), expectedError{ Message: mustBe("empty"), Path: mustBe("DATA"), Got: mustContain(`""`), Expected: mustBe("not empty"), }) checkError(t, ([]int)(nil), td.NotEmpty(), expectedError{ Message: mustBe("empty"), Path: mustBe("DATA"), Got: mustBe("([]int) "), Expected: mustBe("not empty"), }) checkError(t, []int{}, td.NotEmpty(), expectedError{ Message: mustBe("empty"), Path: mustBe("DATA"), Got: mustContain("([]int)"), Expected: mustBe("not empty"), }) checkError(t, (map[string]bool)(nil), td.NotEmpty(), expectedError{ Message: mustBe("empty"), Path: mustBe("DATA"), Got: mustContain("(map[string]bool) "), Expected: mustBe("not empty"), }) checkError(t, map[string]bool{}, td.NotEmpty(), expectedError{ Message: mustBe("empty"), Path: mustBe("DATA"), Got: mustContain("(map[string]bool)"), Expected: mustBe("not empty"), }) checkError(t, (chan int)(nil), td.NotEmpty(), expectedError{ Message: mustBe("empty"), Path: mustBe("DATA"), Got: mustContain("(chan int) "), Expected: mustBe("not empty"), }) checkError(t, make(chan int), td.NotEmpty(), expectedError{ Message: mustBe("empty"), Path: mustBe("DATA"), Got: mustContain("(chan int)"), Expected: mustBe("not empty"), }) checkError(t, [0]int{}, td.NotEmpty(), expectedError{ Message: mustBe("empty"), Path: mustBe("DATA"), Got: mustContain("([0]int)"), Expected: mustBe("not empty"), }) // // String test.EqualStr(t, td.NotEmpty().String(), "NotEmpty()") } func TestEmptyTypeBehind(t *testing.T) { equalTypes(t, td.Empty(), nil) equalTypes(t, td.NotEmpty(), nil) } golang-github-maxatome-go-testdeep-1.14.0/td/td_error_is.go000066400000000000000000000115071454313311600236310ustar00rootroot00000000000000// Copyright (c) 2022, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td import ( "errors" "fmt" "reflect" "github.com/maxatome/go-testdeep/internal/ctxerr" "github.com/maxatome/go-testdeep/internal/dark" "github.com/maxatome/go-testdeep/internal/types" ) type tdErrorIs struct { tdSmugglerBase typeBehind reflect.Type } var _ TestDeep = &tdErrorIs{} func errorToRawString(err error) types.RawString { if err == nil { return "nil" } return types.RawString(fmt.Sprintf("(%[1]T) %[1]q", err)) } // summary(ErrorIs): checks the data is an error and matches a wrapped error // input(ErrorIs): if(error) // ErrorIs is a smuggler operator. It reports whether any error in an // error's chain matches expectedError. // // _, err := os.Open("/unknown/file") // td.Cmp(t, err, os.ErrNotExist) // fails // td.Cmp(t, err, td.ErrorIs(os.ErrNotExist)) // succeeds // // err1 := fmt.Errorf("failure1") // err2 := fmt.Errorf("failure2: %w", err1) // err3 := fmt.Errorf("failure3: %w", err2) // err := fmt.Errorf("failure4: %w", err3) // td.Cmp(t, err, td.ErrorIs(err)) // succeeds // td.Cmp(t, err, td.ErrorIs(err1)) // succeeds // td.Cmp(t, err1, td.ErrorIs(err)) // fails // // var cerr myError // td.Cmp(t, err, td.ErrorIs(td.Catch(&cerr, td.String("my error...")))) // // td.Cmp(t, err, td.ErrorIs(td.All( // td.Isa(myError{}), // td.String("my error..."), // ))) // // Behind the scene it uses [errors.Is] function if expectedError is // an [error] and [errors.As] function if expectedError is a // [TestDeep] operator. // // Note that like [errors.Is], expectedError can be nil: in this case // the comparison succeeds only when got is nil too. // // See also [CmpError] and [CmpNoError]. func ErrorIs(expectedError any) TestDeep { e := tdErrorIs{ tdSmugglerBase: newSmugglerBase(expectedError), } switch expErr := expectedError.(type) { case nil: case error: e.expectedValue = reflect.ValueOf(expectedError) case TestDeep: e.typeBehind = expErr.TypeBehind() if e.typeBehind == nil { e.typeBehind = types.Interface break } if !e.typeBehind.Implements(types.Error) && e.typeBehind.Kind() != reflect.Interface { e.err = ctxerr.OpBad("ErrorIs", "ErrorIs(%[1]s): type %[2]s behind %[1]s operator is not an interface or does not implement error", expErr.GetLocation().Func, e.typeBehind) } default: e.err = ctxerr.OpBadUsage("ErrorIs", "(error|TESTDEEP_OPERATOR)", expectedError, 1, false) } return &e } func (e *tdErrorIs) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error { if e.err != nil { return ctx.CollectError(e.err) } // nil case if !got.IsValid() { // Special case if !e.expectedValue.IsValid() { return nil } if ctx.BooleanError { return ctxerr.BooleanError } return ctx.CollectError(&ctxerr.Error{ Message: "nil value", Got: types.RawString("nil"), Expected: types.RawString("anything implementing error interface"), }) } gotIf, ok := dark.GetInterface(got, true) if !ok { return ctx.CollectError(ctx.CannotCompareError()) } gotErr, ok := gotIf.(error) if !ok { if ctx.BooleanError { return ctxerr.BooleanError } return ctx.CollectError(&ctxerr.Error{ Message: got.Type().String() + " does not implement error interface", Got: gotIf, Expected: types.RawString("anything implementing error interface"), }) } if e.isTestDeeper { target := reflect.New(e.typeBehind) if !errors.As(gotErr, target.Interface()) { if ctx.BooleanError { return ctxerr.BooleanError } return ctx.CollectError(&ctxerr.Error{ Message: "type is not found in err's tree", Got: gotIf, Expected: types.RawString(e.typeBehind.String()), }) } return deepValueEqual(ctx.AddCustomLevel(S(".ErrorIs(%s)", e.typeBehind)), target.Elem(), e.expectedValue) } var expErr error if e.expectedValue.IsValid() { expErr = e.expectedValue.Interface().(error) if errors.Is(gotErr, expErr) { return nil } if ctx.BooleanError { return ctxerr.BooleanError } return ctx.CollectError(&ctxerr.Error{ Message: "is not found in err's tree", Got: errorToRawString(gotErr), Expected: errorToRawString(expErr), }) } if ctx.BooleanError { return ctxerr.BooleanError } return ctx.CollectError(&ctxerr.Error{ Message: "is not nil", Got: errorToRawString(gotErr), Expected: errorToRawString(expErr), }) } func (e *tdErrorIs) String() string { if e.err != nil { return e.stringError() } if e.isTestDeeper { return "ErrorIs(" + e.expectedValue.Interface().(TestDeep).String() + ")" } if !e.expectedValue.IsValid() { return "ErrorIs(nil)" } return "ErrorIs(" + e.expectedValue.Interface().(error).Error() + ")" } func (e *tdErrorIs) HandleInvalid() bool { return true } golang-github-maxatome-go-testdeep-1.14.0/td/td_error_is_test.go000066400000000000000000000127301454313311600246670ustar00rootroot00000000000000// Copyright (c) 2022, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td_test import ( "fmt" "io" "testing" "github.com/maxatome/go-testdeep/internal/dark" "github.com/maxatome/go-testdeep/internal/test" "github.com/maxatome/go-testdeep/td" ) type errorIsSimpleErr string func (e errorIsSimpleErr) Error() string { return string(e) } type errorIsWrappedErr struct { s string err error } func (e errorIsWrappedErr) Error() string { if e.err != nil { return e.s + ": " + e.err.Error() } return e.s + ": nil" } func (e errorIsWrappedErr) Unwrap() error { return e.err } var _ = []error{errorIsSimpleErr(""), errorIsWrappedErr{}} func TestErrorIs(t *testing.T) { insideErr1 := errorIsSimpleErr("failure1") insideErr2 := errorIsWrappedErr{"failure2", insideErr1} insideErr3 := errorIsWrappedErr{"failure3", insideErr2} err := errorIsWrappedErr{"failure4", insideErr3} checkOK(t, err, td.ErrorIs(err)) checkOK(t, err, td.ErrorIs(insideErr3)) checkOK(t, err, td.ErrorIs(insideErr2)) checkOK(t, err, td.ErrorIs(insideErr1)) checkOK(t, nil, td.ErrorIs(nil)) checkOK(t, err, td.ErrorIs(td.All( td.Isa(errorIsSimpleErr("")), td.String("failure1"), ))) // many errorIsWrappedErr in the err's tree, so only the first // encountered matches checkOK(t, err, td.ErrorIs(td.All( td.Isa(errorIsWrappedErr{}), td.HasPrefix("failure4"), ))) // HasPrefix().TypeBehind() always returns nil // so errors.As() is called with &any, so the toplevel error matches checkOK(t, err, td.ErrorIs(td.HasPrefix("failure4"))) var errNil error checkOK(t, &errNil, td.Ptr(td.ErrorIs(nil))) var inside errorIsSimpleErr checkOK(t, err, td.ErrorIs(td.Catch(&inside, td.String("failure1")))) test.EqualStr(t, string(inside), "failure1") checkError(t, nil, td.ErrorIs(insideErr1), expectedError{ Path: mustBe("DATA"), Message: mustBe("nil value"), Got: mustBe("nil"), Expected: mustBe("anything implementing error interface"), }) checkError(t, 45, td.ErrorIs(insideErr1), expectedError{ Path: mustBe("DATA"), Message: mustBe("int does not implement error interface"), Got: mustBe("45"), Expected: mustBe("anything implementing error interface"), }) checkError(t, 45, td.ErrorIs(fmt.Errorf("another")), expectedError{ Path: mustBe("DATA"), Message: mustBe("int does not implement error interface"), Got: mustBe("45"), Expected: mustBe("anything implementing error interface"), }) checkError(t, err, td.ErrorIs(fmt.Errorf("another")), expectedError{ Path: mustBe("DATA"), Message: mustBe("is not found in err's tree"), Got: mustBe(`(td_test.errorIsWrappedErr) "failure4: failure3: failure2: failure1"`), Expected: mustBe(`(*errors.errorString) "another"`), }) checkError(t, err, td.ErrorIs(td.String("nonono")), expectedError{ Path: mustBe("DATA.ErrorIs(interface {})"), Message: mustBe("does not match"), Got: mustBe(`"failure4: failure3: failure2: failure1"`), Expected: mustBe(`"nonono"`), }) checkError(t, err, td.ErrorIs(td.Isa(fmt.Errorf("another"))), expectedError{ Path: mustBe("DATA"), Message: mustBe("type is not found in err's tree"), Got: mustBe(`(td_test.errorIsWrappedErr) failure4: failure3: failure2: failure1`), Expected: mustBe(`*errors.errorString`), }) checkError(t, err, td.ErrorIs(td.Smuggle(io.ReadAll, td.String("xx"))), expectedError{ Path: mustBe("DATA"), Message: mustBe("type is not found in err's tree"), Got: mustBe(`(td_test.errorIsWrappedErr) failure4: failure3: failure2: failure1`), Expected: mustBe(`io.Reader`), }) checkError(t, err, td.ErrorIs(nil), expectedError{ Path: mustBe("DATA"), Message: mustBe("is not nil"), Got: mustBe(`(td_test.errorIsWrappedErr) "failure4: failure3: failure2: failure1"`), Expected: mustBe(`nil`), }) // As errors.Is, it does not match checkError(t, errorIsWrappedErr{"failure", nil}, td.ErrorIs(nil), expectedError{ Path: mustBe("DATA"), Message: mustBe("is not nil"), Got: mustBe(`(td_test.errorIsWrappedErr) "failure: nil"`), Expected: mustBe(`nil`), }) checkError(t, err, td.ErrorIs(td.Gt(0)), expectedError{ Path: mustBe("DATA"), Message: mustBe("bad usage of ErrorIs operator"), Summary: mustBe(`ErrorIs(Gt): type int behind Gt operator is not an interface or does not implement error`), }) type private struct{ err error } got := private{err: err} for _, expErr := range []error{err, insideErr3} { expected := td.Struct(private{}, td.StructFields{"err": td.ErrorIs(expErr)}) if dark.UnsafeDisabled { checkError(t, got, expected, expectedError{ Message: mustBe("cannot compare"), Path: mustBe("DATA.err"), Summary: mustBe("unexported field that cannot be overridden"), }) } else { checkOK(t, got, expected) } } if !dark.UnsafeDisabled { got = private{} checkOK(t, got, td.Struct(private{}, td.StructFields{"err": td.ErrorIs(nil)})) } // // String test.EqualStr(t, td.ErrorIs(insideErr1).String(), "ErrorIs(failure1)") test.EqualStr(t, td.ErrorIs(nil).String(), "ErrorIs(nil)") test.EqualStr(t, td.ErrorIs(td.HasPrefix("pipo")).String(), `ErrorIs(HasPrefix("pipo"))`) test.EqualStr(t, td.ErrorIs(12).String(), "ErrorIs()") } func TestErrorIsTypeBehind(t *testing.T) { equalTypes(t, td.ErrorIs(fmt.Errorf("another")), nil) } golang-github-maxatome-go-testdeep-1.14.0/td/td_expected_type.go000066400000000000000000000035311454313311600246450ustar00rootroot00000000000000// Copyright (c) 2018, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td import ( "reflect" "github.com/maxatome/go-testdeep/internal/ctxerr" "github.com/maxatome/go-testdeep/internal/types" ) type tdExpectedType struct { base expectedType reflect.Type isPtr bool } func (t *tdExpectedType) errorTypeMismatch(gotType reflect.Type) *ctxerr.Error { expectedType := t.expectedType if t.isPtr { expectedType = reflect.PtrTo(expectedType) } return ctxerr.TypeMismatch(gotType, expectedType) } func (t *tdExpectedType) checkPtr(ctx ctxerr.Context, pGot *reflect.Value, nilAllowed bool) *ctxerr.Error { if t.isPtr { got := *pGot if got.Kind() != reflect.Ptr { if ctx.BooleanError { return ctxerr.BooleanError } return t.errorTypeMismatch(got.Type()) } if !nilAllowed && got.IsNil() { if ctx.BooleanError { return ctxerr.BooleanError } return &ctxerr.Error{ Message: "values differ", Got: got, Expected: types.RawString("non-nil"), } } *pGot = got.Elem() } return nil } func (t *tdExpectedType) checkType(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error { if got.Type() != t.expectedType { if ctx.BeLax && t.expectedType.ConvertibleTo(got.Type()) { return nil } if ctx.BooleanError { return ctxerr.BooleanError } gt := got.Type() if t.isPtr { gt = reflect.PtrTo(gt) } return t.errorTypeMismatch(gt) } return nil } func (t *tdExpectedType) TypeBehind() reflect.Type { if t.err != nil { return nil } if t.isPtr { return reflect.New(t.expectedType).Type() } return t.expectedType } func (t *tdExpectedType) expectedTypeStr() string { if t.isPtr { return "*" + t.expectedType.String() } return t.expectedType.String() } golang-github-maxatome-go-testdeep-1.14.0/td/td_grep.go000066400000000000000000000255711454313311600227500ustar00rootroot00000000000000// Copyright (c) 2022-2023, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td import ( "reflect" "github.com/maxatome/go-testdeep/internal/ctxerr" "github.com/maxatome/go-testdeep/internal/types" ) const grepUsage = "(FILTER_FUNC|FILTER_TESTDEEP_OPERATOR, TESTDEEP_OPERATOR|EXPECTED_VALUE)" type tdGrepBase struct { tdSmugglerBase filter reflect.Value // func (argType ≠ nil) OR TestDeep operator argType reflect.Type } func (g *tdGrepBase) initGrepBase(filter, expectedValue any) { g.tdSmugglerBase = newSmugglerBase(expectedValue, 1) if !g.isTestDeeper { g.expectedValue = reflect.ValueOf(expectedValue) } if op, ok := filter.(TestDeep); ok { g.filter = reflect.ValueOf(op) return } vfilter := reflect.ValueOf(filter) if vfilter.Kind() != reflect.Func { g.err = ctxerr.OpBad(g.GetLocation().Func, "usage: %s%s, FILTER_FUNC must be a function or FILTER_TESTDEEP_OPERATOR a TestDeep operator", g.GetLocation().Func, grepUsage) return } filterType := vfilter.Type() if filterType.IsVariadic() || filterType.NumIn() != 1 { g.err = ctxerr.OpBad(g.GetLocation().Func, "usage: %s%s, FILTER_FUNC must take only one non-variadic argument", g.GetLocation().Func, grepUsage) return } if filterType.NumOut() != 1 || filterType.Out(0) != types.Bool { g.err = ctxerr.OpBad(g.GetLocation().Func, "usage: %s%s, FILTER_FUNC must return bool", g.GetLocation().Func, grepUsage) return } g.argType = filterType.In(0) g.filter = vfilter } func (g *tdGrepBase) matchItem(ctx ctxerr.Context, idx int, item reflect.Value) (bool, *ctxerr.Error) { if g.argType == nil { // g.filter is a TestDeep operator return deepValueEqualFinalOK(ctx, item, g.filter), nil } // item is an interface, but the filter function does not expect an // interface, resolve it if item.Kind() == reflect.Interface && g.argType.Kind() != reflect.Interface { item = item.Elem() } if !item.Type().AssignableTo(g.argType) { if !types.IsConvertible(item, g.argType) { if ctx.BooleanError { return false, ctxerr.BooleanError } return false, ctx.AddArrayIndex(idx).CollectError(&ctxerr.Error{ Message: "incompatible parameter type", Got: types.RawString(item.Type().String()), Expected: types.RawString(g.argType.String()), }) } item = item.Convert(g.argType) } return g.filter.Call([]reflect.Value{item})[0].Bool(), nil } func (g *tdGrepBase) HandleInvalid() bool { return true // Knows how to handle untyped nil values (aka invalid values) } func (g *tdGrepBase) String() string { if g.err != nil { return g.stringError() } if g.argType == nil { return S("%s(%s)", g.GetLocation().Func, g.filter.Interface().(TestDeep)) } return S("%s(%s)", g.GetLocation().Func, g.filter.Type()) } func (g *tdGrepBase) TypeBehind() reflect.Type { if g.err != nil { return nil } return g.internalTypeBehind() } // sliceTypeBehind is used by First & Last TypeBehind method. func (g *tdGrepBase) sliceTypeBehind() reflect.Type { typ := g.TypeBehind() if typ == nil { return nil } return reflect.SliceOf(typ) } func (g *tdGrepBase) notFound(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error { if ctx.BooleanError { return ctxerr.BooleanError } return ctx.CollectError(&ctxerr.Error{ Message: "item not found", Got: got, Expected: types.RawString(g.String()), }) } func grepResolvePtr(ctx ctxerr.Context, got *reflect.Value) *ctxerr.Error { if got.Kind() == reflect.Ptr { gotElem := got.Elem() if !gotElem.IsValid() { if ctx.BooleanError { return ctxerr.BooleanError } return ctx.CollectError(ctxerr.NilPointer(*got, "non-nil *slice OR *array")) } switch gotElem.Kind() { case reflect.Slice, reflect.Array: *got = gotElem } } return nil } func grepBadKind(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error { if ctx.BooleanError { return ctxerr.BooleanError } return ctx.CollectError(ctxerr.BadKind(got, "slice OR array OR *slice OR *array")) } type tdGrep struct { tdGrepBase } var _ TestDeep = &tdGrep{} // summary(Grep): reduces a slice or an array before comparing its content // input(Grep): array,slice,ptr(ptr on array/slice) // Grep is a smuggler operator. It takes an array, a slice or a // pointer on array/slice. For each item it applies filter, a // [TestDeep] operator or a function returning a bool, and produces a // slice consisting of those items for which the filter matched and // compares it to expectedValue. The filter matches when it is a: // - [TestDeep] operator and it matches for the item; // - function receiving the item and it returns true. // // expectedValue can be a [TestDeep] operator or a slice (but never an // array nor a pointer on a slice/array nor any other kind). // // got := []int{-3, -2, -1, 0, 1, 2, 3} // td.Cmp(t, got, td.Grep(td.Gt(0), []int{1, 2, 3})) // succeeds // td.Cmp(t, got, td.Grep( // func(x int) bool { return x%2 == 0 }, // []int{-2, 0, 2})) // succeeds // td.Cmp(t, got, td.Grep( // func(x int) bool { return x%2 == 0 }, // td.Set(0, 2, -2))) // succeeds // // If Grep receives a nil slice or a pointer on a nil slice, it always // returns a nil slice: // // var got []int // td.Cmp(t, got, td.Grep(td.Gt(0), ([]int)(nil))) // succeeds // td.Cmp(t, got, td.Grep(td.Gt(0), td.Nil())) // succeeds // td.Cmp(t, got, td.Grep(td.Gt(0), []int{})) // fails // // See also [First], [Last] and [Flatten]. func Grep(filter, expectedValue any) TestDeep { g := tdGrep{} g.initGrepBase(filter, expectedValue) if g.err == nil && !g.isTestDeeper && g.expectedValue.Kind() != reflect.Slice { g.err = ctxerr.OpBad("Grep", "usage: Grep%s, EXPECTED_VALUE must be a slice not a %s", grepUsage, types.KindType(g.expectedValue)) } return &g } func (g *tdGrep) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error { if g.err != nil { return ctx.CollectError(g.err) } if rErr := grepResolvePtr(ctx, &got); rErr != nil { return rErr } switch got.Kind() { case reflect.Slice, reflect.Array: const grepped = "" if got.Kind() == reflect.Slice && got.IsNil() { return deepValueEqual( ctx.AddCustomLevel(grepped), reflect.New(got.Type()).Elem(), g.expectedValue, ) } l := got.Len() out := reflect.MakeSlice(reflect.SliceOf(got.Type().Elem()), 0, l) for idx := 0; idx < l; idx++ { item := got.Index(idx) ok, rErr := g.matchItem(ctx, idx, item) if rErr != nil { return rErr } if ok { out = reflect.Append(out, item) } } return deepValueEqual(ctx.AddCustomLevel(grepped), out, g.expectedValue) } return grepBadKind(ctx, got) } type tdFirst struct { tdGrepBase } var _ TestDeep = &tdFirst{} // summary(First): find the first matching item of a slice or an array // then compare its content // input(First): array,slice,ptr(ptr on array/slice) // First is a smuggler operator. It takes an array, a slice or a // pointer on array/slice. For each item it applies filter, a // [TestDeep] operator or a function returning a bool. It takes the // first item for which the filter matched and compares it to // expectedValue. The filter matches when it is a: // - [TestDeep] operator and it matches for the item; // - function receiving the item and it returns true. // // expectedValue can of course be a [TestDeep] operator. // // got := []int{-3, -2, -1, 0, 1, 2, 3} // td.Cmp(t, got, td.First(td.Gt(0), 1)) // succeeds // td.Cmp(t, got, td.First(func(x int) bool { return x%2 == 0 }, -2)) // succeeds // td.Cmp(t, got, td.First(func(x int) bool { return x%2 == 0 }, td.Lt(0))) // succeeds // // If the input is empty (and/or nil for a slice), an "item not found" // error is raised before comparing to expectedValue. // // var got []int // td.Cmp(t, got, td.First(td.Gt(0), td.Gt(0))) // fails // td.Cmp(t, []int{}, td.First(td.Gt(0), td.Gt(0))) // fails // td.Cmp(t, [0]int{}, td.First(td.Gt(0), td.Gt(0))) // fails // // See also [Last] and [Grep]. func First(filter, expectedValue any) TestDeep { g := tdFirst{} g.initGrepBase(filter, expectedValue) return &g } func (g *tdFirst) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error { if g.err != nil { return ctx.CollectError(g.err) } if rErr := grepResolvePtr(ctx, &got); rErr != nil { return rErr } switch got.Kind() { case reflect.Slice, reflect.Array: for idx, l := 0, got.Len(); idx < l; idx++ { item := got.Index(idx) ok, rErr := g.matchItem(ctx, idx, item) if rErr != nil { return rErr } if ok { return deepValueEqual( ctx.AddCustomLevel(S("", idx)), item, g.expectedValue, ) } } return g.notFound(ctx, got) } return grepBadKind(ctx, got) } func (g *tdFirst) TypeBehind() reflect.Type { return g.sliceTypeBehind() } type tdLast struct { tdGrepBase } var _ TestDeep = &tdLast{} // summary(Last): find the last matching item of a slice or an array // then compare its content // input(Last): array,slice,ptr(ptr on array/slice) // Last is a smuggler operator. It takes an array, a slice or a // pointer on array/slice. For each item it applies filter, a // [TestDeep] operator or a function returning a bool. It takes the // last item for which the filter matched and compares it to // expectedValue. The filter matches when it is a: // - [TestDeep] operator and it matches for the item; // - function receiving the item and it returns true. // // expectedValue can of course be a [TestDeep] operator. // // got := []int{-3, -2, -1, 0, 1, 2, 3} // td.Cmp(t, got, td.Last(td.Lt(0), -1)) // succeeds // td.Cmp(t, got, td.Last(func(x int) bool { return x%2 == 0 }, 2)) // succeeds // td.Cmp(t, got, td.Last(func(x int) bool { return x%2 == 0 }, td.Gt(0))) // succeeds // // If the input is empty (and/or nil for a slice), an "item not found" // error is raised before comparing to expectedValue. // // var got []int // td.Cmp(t, got, td.Last(td.Gt(0), td.Gt(0))) // fails // td.Cmp(t, []int{}, td.Last(td.Gt(0), td.Gt(0))) // fails // td.Cmp(t, [0]int{}, td.Last(td.Gt(0), td.Gt(0))) // fails // // See also [First] and [Grep]. func Last(filter, expectedValue any) TestDeep { g := tdLast{} g.initGrepBase(filter, expectedValue) return &g } func (g *tdLast) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error { if g.err != nil { return ctx.CollectError(g.err) } if rErr := grepResolvePtr(ctx, &got); rErr != nil { return rErr } switch got.Kind() { case reflect.Slice, reflect.Array: for idx := got.Len() - 1; idx >= 0; idx-- { item := got.Index(idx) ok, rErr := g.matchItem(ctx, idx, item) if rErr != nil { return rErr } if ok { return deepValueEqual( ctx.AddCustomLevel(S("", idx)), item, g.expectedValue, ) } } return g.notFound(ctx, got) } return grepBadKind(ctx, got) } func (g *tdLast) TypeBehind() reflect.Type { return g.sliceTypeBehind() } golang-github-maxatome-go-testdeep-1.14.0/td/td_grep_test.go000066400000000000000000000474151454313311600240100ustar00rootroot00000000000000// Copyright (c) 2022, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td_test import ( "testing" "github.com/maxatome/go-testdeep/internal/test" "github.com/maxatome/go-testdeep/td" ) func TestGrep(t *testing.T) { t.Run("basic", func(t *testing.T) { got := [...]int{-3, -2, -1, 0, 1, 2, 3} sgot := got[:] testCases := []struct { name string got any }{ {"slice", sgot}, {"array", got}, {"*slice", &sgot}, {"*array", &got}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { checkOK(t, tc.got, td.Grep(td.Gt(0), []int{1, 2, 3})) checkOK(t, tc.got, td.Grep(td.Not(td.Between(-2, 2)), []int{-3, 3})) checkOK(t, tc.got, td.Grep( func(x int) bool { return (x & 1) != 0 }, []int{-3, -1, 1, 3})) checkOK(t, tc.got, td.Grep( func(x int64) bool { return (x & 1) != 0 }, []int{-3, -1, 1, 3}), "int64 filter vs int items") checkOK(t, tc.got, td.Grep( func(x any) bool { return (x.(int) & 1) != 0 }, []int{-3, -1, 1, 3}), "any filter vs int items") }) } }) t.Run("struct", func(t *testing.T) { type person struct { ID int64 Name string } got := [...]person{ {ID: 1, Name: "Joe"}, {ID: 2, Name: "Bob"}, {ID: 3, Name: "Alice"}, {ID: 4, Name: "Brian"}, {ID: 5, Name: "Britt"}, } sgot := got[:] testCases := []struct { name string got any }{ {"slice", sgot}, {"array", got}, {"*slice", &sgot}, {"*array", &got}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { checkOK(t, tc.got, td.Grep( td.JSONPointer("/Name", td.HasPrefix("Br")), []person{{ID: 4, Name: "Brian"}, {ID: 5, Name: "Britt"}})) checkOK(t, tc.got, td.Grep( func(p person) bool { return p.ID < 3 }, []person{{ID: 1, Name: "Joe"}, {ID: 2, Name: "Bob"}})) }) } }) t.Run("interfaces", func(t *testing.T) { got := [...]any{-3, -2, -1, 0, 1, 2, 3} sgot := got[:] testCases := []struct { name string got any }{ {"slice", sgot}, {"array", got}, {"*slice", &sgot}, {"*array", &got}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { checkOK(t, tc.got, td.Grep(td.Gt(0), []any{1, 2, 3})) checkOK(t, tc.got, td.Grep(td.Not(td.Between(-2, 2)), []any{-3, 3})) checkOK(t, tc.got, td.Grep( func(x int) bool { return (x & 1) != 0 }, []any{-3, -1, 1, 3})) checkOK(t, tc.got, td.Grep( func(x int64) bool { return (x & 1) != 0 }, []any{-3, -1, 1, 3}), "int64 filter vs any/int items") checkOK(t, tc.got, td.Grep( func(x any) bool { return (x.(int) & 1) != 0 }, []any{-3, -1, 1, 3}), "any filter vs any/int items") }) } }) t.Run("interfaces error", func(t *testing.T) { got := [...]any{123, "foo"} sgot := got[:] testCases := []struct { name string got any }{ {"slice", sgot}, {"array", got}, {"*slice", &sgot}, {"*array", &got}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { checkError(t, tc.got, td.Grep(func(x int) bool { return true }, []string{"never reached"}), expectedError{ Message: mustBe("incompatible parameter type"), Path: mustBe("DATA[1]"), Got: mustBe("string"), Expected: mustBe("int"), }) }) } }) t.Run("nil slice", func(t *testing.T) { var got []int testCases := []struct { name string got any }{ {"slice", got}, {"*slice", &got}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { checkOK(t, tc.got, td.Grep(td.Gt(666), ([]int)(nil))) }) } }) t.Run("nil pointer", func(t *testing.T) { checkError(t, (*[]int)(nil), td.Grep(td.Ignore(), []int{33}), expectedError{ Message: mustBe("nil pointer"), Path: mustBe("DATA"), Got: mustBe("nil *slice (*[]int type)"), Expected: mustBe("non-nil *slice OR *array"), }) }) t.Run("JSON", func(t *testing.T) { got := map[string]any{ "values": []int{1, 2, 3, 4}, } checkOK(t, got, td.JSON(`{"values": Grep(Gt(2), [3, 4])}`)) }) t.Run("errors", func(t *testing.T) { for _, filter := range []any{nil, 33} { checkError(t, "never tested", td.Grep(filter, 42), expectedError{ Message: mustBe("bad usage of Grep operator"), Path: mustBe("DATA"), Summary: mustBe("usage: Grep(FILTER_FUNC|FILTER_TESTDEEP_OPERATOR, TESTDEEP_OPERATOR|EXPECTED_VALUE), FILTER_FUNC must be a function or FILTER_TESTDEEP_OPERATOR a TestDeep operator"), }, "filter:", filter) } for _, filter := range []any{ func() bool { return true }, func(a, b int) bool { return true }, func(a ...int) bool { return true }, } { checkError(t, "never tested", td.Grep(filter, 42), expectedError{ Message: mustBe("bad usage of Grep operator"), Path: mustBe("DATA"), Summary: mustBe("usage: Grep(FILTER_FUNC|FILTER_TESTDEEP_OPERATOR, TESTDEEP_OPERATOR|EXPECTED_VALUE), FILTER_FUNC must take only one non-variadic argument"), }, "filter:", filter) } for _, filter := range []any{ func(a int) {}, func(a int) int { return 0 }, func(a int) (bool, bool) { return true, true }, } { checkError(t, "never tested", td.Grep(filter, 42), expectedError{ Message: mustBe("bad usage of Grep operator"), Path: mustBe("DATA"), Summary: mustBe("usage: Grep(FILTER_FUNC|FILTER_TESTDEEP_OPERATOR, TESTDEEP_OPERATOR|EXPECTED_VALUE), FILTER_FUNC must return bool"), }, "filter:", filter) } checkError(t, "never tested", td.Grep(td.Ignore(), 42), expectedError{ Message: mustBe("bad usage of Grep operator"), Path: mustBe("DATA"), Summary: mustBe("usage: Grep(FILTER_FUNC|FILTER_TESTDEEP_OPERATOR, TESTDEEP_OPERATOR|EXPECTED_VALUE), EXPECTED_VALUE must be a slice not a int"), }) checkError(t, &struct{}{}, td.Grep(td.Ignore(), []int{33}), expectedError{ Message: mustBe("bad kind"), Path: mustBe("DATA"), Got: mustBe("*struct (*struct {} type)"), Expected: mustBe("slice OR array OR *slice OR *array"), }) checkError(t, nil, td.Grep(td.Ignore(), []int{33}), expectedError{ Message: mustBe("bad kind"), Path: mustBe("DATA"), Got: mustBe("nil"), Expected: mustBe("slice OR array OR *slice OR *array"), }) }) } func TestGrepTypeBehind(t *testing.T) { equalTypes(t, td.Grep(func(n int) bool { return true }, []int{33}), []int{}) equalTypes(t, td.Grep(td.Gt("0"), []string{"33"}), []string{}) // Erroneous op equalTypes(t, td.Grep(42, 33), nil) } func TestGrepString(t *testing.T) { test.EqualStr(t, td.Grep(func(n int) bool { return true }, []int{}).String(), "Grep(func(int) bool)") test.EqualStr(t, td.Grep(td.Gt(0), []int{}).String(), "Grep(> 0)") // Erroneous op test.EqualStr(t, td.Grep(42, []int{}).String(), "Grep()") } func TestFirst(t *testing.T) { t.Run("basic", func(t *testing.T) { got := [...]int{-3, -2, -1, 0, 1, 2, 3} sgot := got[:] testCases := []struct { name string got any }{ {"slice", sgot}, {"array", got}, {"*slice", &sgot}, {"*array", &got}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { checkOK(t, tc.got, td.First(td.Gt(0), 1)) checkOK(t, tc.got, td.First(td.Not(td.Between(-3, 2)), 3)) checkOK(t, tc.got, td.First( func(x int) bool { return (x & 1) == 0 }, -2)) checkOK(t, tc.got, td.First( func(x int64) bool { return (x & 1) != 0 }, -3), "int64 filter vs int items") checkOK(t, tc.got, td.First( func(x any) bool { return (x.(int) & 1) == 0 }, -2), "any filter vs int items") checkError(t, tc.got, td.First(td.Gt(666), "never reached"), expectedError{ Message: mustBe("item not found"), Path: mustBe("DATA"), Got: mustContain(`]int) (len=7 `), Expected: mustBe("First(> 666)"), }) }) } }) t.Run("struct", func(t *testing.T) { type person struct { ID int64 Name string } got := [...]person{ {ID: 1, Name: "Joe"}, {ID: 2, Name: "Bob"}, {ID: 3, Name: "Alice"}, {ID: 4, Name: "Brian"}, {ID: 5, Name: "Britt"}, } sgot := got[:] testCases := []struct { name string got any }{ {"slice", sgot}, {"array", got}, {"*slice", &sgot}, {"*array", &got}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { checkOK(t, tc.got, td.First( td.JSONPointer("/Name", td.HasPrefix("Br")), person{ID: 4, Name: "Brian"})) checkOK(t, tc.got, td.First( func(p person) bool { return p.ID < 3 }, person{ID: 1, Name: "Joe"})) }) } }) t.Run("interfaces", func(t *testing.T) { got := [...]any{-3, -2, -1, 0, 1, 2, 3} sgot := got[:] testCases := []struct { name string got any }{ {"slice", sgot}, {"array", got}, {"*slice", &sgot}, {"*array", &got}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { checkOK(t, tc.got, td.First(td.Gt(0), 1)) checkOK(t, tc.got, td.First(td.Not(td.Between(-3, 2)), 3)) checkOK(t, tc.got, td.First( func(x int) bool { return (x & 1) == 0 }, -2)) checkOK(t, tc.got, td.First( func(x int64) bool { return (x & 1) != 0 }, -3), "int64 filter vs any/int items") checkOK(t, tc.got, td.First( func(x any) bool { return (x.(int) & 1) == 0 }, -2), "any filter vs any/int items") }) } }) t.Run("interfaces error", func(t *testing.T) { got := [...]any{123, "foo"} sgot := got[:] testCases := []struct { name string got any }{ {"slice", sgot}, {"array", got}, {"*slice", &sgot}, {"*array", &got}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { checkError(t, tc.got, td.First(func(x int) bool { return false }, "never reached"), expectedError{ Message: mustBe("incompatible parameter type"), Path: mustBe("DATA[1]"), Got: mustBe("string"), Expected: mustBe("int"), }) }) } }) t.Run("nil slice", func(t *testing.T) { var got []int testCases := []struct { name string got any }{ {"slice", got}, {"*slice", &got}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { checkError(t, tc.got, td.First(td.Gt(666), "never reached"), expectedError{ Message: mustBe("item not found"), Path: mustBe("DATA"), Got: mustBe("([]int) "), Expected: mustBe("First(> 666)"), }) }) } }) t.Run("nil pointer", func(t *testing.T) { checkError(t, (*[]int)(nil), td.First(td.Ignore(), 33), expectedError{ Message: mustBe("nil pointer"), Path: mustBe("DATA"), Got: mustBe("nil *slice (*[]int type)"), Expected: mustBe("non-nil *slice OR *array"), }) }) t.Run("JSON", func(t *testing.T) { got := map[string]any{ "values": []int{1, 2, 3, 4}, } checkOK(t, got, td.JSON(`{"values": First(Gt(2), 3)}`)) }) t.Run("errors", func(t *testing.T) { for _, filter := range []any{nil, 33} { checkError(t, "never tested", td.First(filter, 42), expectedError{ Message: mustBe("bad usage of First operator"), Path: mustBe("DATA"), Summary: mustBe("usage: First(FILTER_FUNC|FILTER_TESTDEEP_OPERATOR, TESTDEEP_OPERATOR|EXPECTED_VALUE), FILTER_FUNC must be a function or FILTER_TESTDEEP_OPERATOR a TestDeep operator"), }, "filter:", filter) } for _, filter := range []any{ func() bool { return true }, func(a, b int) bool { return true }, func(a ...int) bool { return true }, } { checkError(t, "never tested", td.First(filter, 42), expectedError{ Message: mustBe("bad usage of First operator"), Path: mustBe("DATA"), Summary: mustBe("usage: First(FILTER_FUNC|FILTER_TESTDEEP_OPERATOR, TESTDEEP_OPERATOR|EXPECTED_VALUE), FILTER_FUNC must take only one non-variadic argument"), }, "filter:", filter) } for _, filter := range []any{ func(a int) {}, func(a int) int { return 0 }, func(a int) (bool, bool) { return true, true }, } { checkError(t, "never tested", td.First(filter, 42), expectedError{ Message: mustBe("bad usage of First operator"), Path: mustBe("DATA"), Summary: mustBe("usage: First(FILTER_FUNC|FILTER_TESTDEEP_OPERATOR, TESTDEEP_OPERATOR|EXPECTED_VALUE), FILTER_FUNC must return bool"), }, "filter:", filter) } checkError(t, &struct{}{}, td.First(td.Ignore(), 33), expectedError{ Message: mustBe("bad kind"), Path: mustBe("DATA"), Got: mustBe("*struct (*struct {} type)"), Expected: mustBe("slice OR array OR *slice OR *array"), }) checkError(t, nil, td.First(td.Ignore(), 33), expectedError{ Message: mustBe("bad kind"), Path: mustBe("DATA"), Got: mustBe("nil"), Expected: mustBe("slice OR array OR *slice OR *array"), }) }) } func TestFirstString(t *testing.T) { test.EqualStr(t, td.First(func(n int) bool { return true }, 33).String(), "First(func(int) bool)") test.EqualStr(t, td.First(td.Gt(0), 33).String(), "First(> 0)") // Erroneous op test.EqualStr(t, td.First(42, 33).String(), "First()") } func TestFirstTypeBehind(t *testing.T) { equalTypes(t, td.First(func(n int) bool { return true }, 33), []int{}) equalTypes(t, td.First(td.Gt("x"), "x"), []string{}) // Erroneous op equalTypes(t, td.First(42, 33), nil) } func TestLast(t *testing.T) { t.Run("basic", func(t *testing.T) { got := [...]int{-3, -2, -1, 0, 1, 2, 3} sgot := got[:] testCases := []struct { name string got any }{ {"slice", sgot}, {"array", got}, {"*slice", &sgot}, {"*array", &got}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { checkOK(t, tc.got, td.Last(td.Lt(0), -1)) checkOK(t, tc.got, td.Last(td.Not(td.Between(1, 3)), 0)) checkOK(t, tc.got, td.Last( func(x int) bool { return (x & 1) == 0 }, 2)) checkOK(t, tc.got, td.Last( func(x int64) bool { return (x & 1) != 0 }, 3), "int64 filter vs int items") checkOK(t, tc.got, td.Last( func(x any) bool { return (x.(int) & 1) == 0 }, 2), "any filter vs int items") checkError(t, tc.got, td.Last(td.Gt(666), "never reached"), expectedError{ Message: mustBe("item not found"), Path: mustBe("DATA"), Got: mustContain(`]int) (len=7 `), Expected: mustBe("Last(> 666)"), }) }) } }) t.Run("struct", func(t *testing.T) { type person struct { ID int64 Name string } got := [...]person{ {ID: 1, Name: "Joe"}, {ID: 2, Name: "Bob"}, {ID: 3, Name: "Alice"}, {ID: 4, Name: "Brian"}, {ID: 5, Name: "Britt"}, } sgot := got[:] testCases := []struct { name string got any }{ {"slice", sgot}, {"array", got}, {"*slice", &sgot}, {"*array", &got}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { checkOK(t, tc.got, td.Last( td.JSONPointer("/Name", td.HasPrefix("Br")), person{ID: 5, Name: "Britt"})) checkOK(t, tc.got, td.Last( func(p person) bool { return p.ID < 3 }, person{ID: 2, Name: "Bob"})) }) } }) t.Run("interfaces", func(t *testing.T) { got := [...]any{-3, -2, -1, 0, 1, 2, 3} sgot := got[:] testCases := []struct { name string got any }{ {"slice", sgot}, {"array", got}, {"*slice", &sgot}, {"*array", &got}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { checkOK(t, tc.got, td.Last(td.Lt(0), -1)) checkOK(t, tc.got, td.Last(td.Not(td.Between(1, 3)), 0)) checkOK(t, tc.got, td.Last( func(x int) bool { return (x & 1) == 0 }, 2)) checkOK(t, tc.got, td.Last( func(x int64) bool { return (x & 1) != 0 }, 3), "int64 filter vs any/int items") checkOK(t, tc.got, td.Last( func(x any) bool { return (x.(int) & 1) == 0 }, 2), "any filter vs any/int items") }) } }) t.Run("interfaces error", func(t *testing.T) { got := [...]any{123, "foo", 456} sgot := got[:] testCases := []struct { name string got any }{ {"slice", sgot}, {"array", got}, {"*slice", &sgot}, {"*array", &got}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { checkError(t, tc.got, td.Last(func(x int) bool { return false }, "never reached"), expectedError{ Message: mustBe("incompatible parameter type"), Path: mustBe("DATA[1]"), Got: mustBe("string"), Expected: mustBe("int"), }) }) } }) t.Run("nil slice", func(t *testing.T) { var got []int testCases := []struct { name string got any }{ {"slice", got}, {"*slice", &got}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { checkError(t, tc.got, td.Last(td.Gt(666), "never reached"), expectedError{ Message: mustBe("item not found"), Path: mustBe("DATA"), Got: mustBe("([]int) "), Expected: mustBe("Last(> 666)"), }) }) } }) t.Run("nil pointer", func(t *testing.T) { checkError(t, (*[]int)(nil), td.Last(td.Ignore(), 33), expectedError{ Message: mustBe("nil pointer"), Path: mustBe("DATA"), Got: mustBe("nil *slice (*[]int type)"), Expected: mustBe("non-nil *slice OR *array"), }) }) t.Run("JSON", func(t *testing.T) { got := map[string]any{ "values": []int{1, 2, 3, 4}, } checkOK(t, got, td.JSON(`{"values": Last(Lt(3), 2)}`)) }) t.Run("errors", func(t *testing.T) { for _, filter := range []any{nil, 33} { checkError(t, "never tested", td.Last(filter, 42), expectedError{ Message: mustBe("bad usage of Last operator"), Path: mustBe("DATA"), Summary: mustBe("usage: Last(FILTER_FUNC|FILTER_TESTDEEP_OPERATOR, TESTDEEP_OPERATOR|EXPECTED_VALUE), FILTER_FUNC must be a function or FILTER_TESTDEEP_OPERATOR a TestDeep operator"), }, "filter:", filter) } for _, filter := range []any{ func() bool { return true }, func(a, b int) bool { return true }, func(a ...int) bool { return true }, } { checkError(t, "never tested", td.Last(filter, 42), expectedError{ Message: mustBe("bad usage of Last operator"), Path: mustBe("DATA"), Summary: mustBe("usage: Last(FILTER_FUNC|FILTER_TESTDEEP_OPERATOR, TESTDEEP_OPERATOR|EXPECTED_VALUE), FILTER_FUNC must take only one non-variadic argument"), }, "filter:", filter) } for _, filter := range []any{ func(a int) {}, func(a int) int { return 0 }, func(a int) (bool, bool) { return true, true }, } { checkError(t, "never tested", td.Last(filter, 42), expectedError{ Message: mustBe("bad usage of Last operator"), Path: mustBe("DATA"), Summary: mustBe("usage: Last(FILTER_FUNC|FILTER_TESTDEEP_OPERATOR, TESTDEEP_OPERATOR|EXPECTED_VALUE), FILTER_FUNC must return bool"), }, "filter:", filter) } checkError(t, &struct{}{}, td.Last(td.Ignore(), 33), expectedError{ Message: mustBe("bad kind"), Path: mustBe("DATA"), Got: mustBe("*struct (*struct {} type)"), Expected: mustBe("slice OR array OR *slice OR *array"), }) checkError(t, nil, td.Last(td.Ignore(), 33), expectedError{ Message: mustBe("bad kind"), Path: mustBe("DATA"), Got: mustBe("nil"), Expected: mustBe("slice OR array OR *slice OR *array"), }) }) } func TestLastString(t *testing.T) { test.EqualStr(t, td.Last(func(n int) bool { return true }, 33).String(), "Last(func(int) bool)") test.EqualStr(t, td.Last(td.Gt(0), 33).String(), "Last(> 0)") // Erroneous op test.EqualStr(t, td.Last(42, 33).String(), "Last()") } func TestLastTypeBehind(t *testing.T) { equalTypes(t, td.Last(func(n int) bool { return true }, 33), []int{}) equalTypes(t, td.Last(td.Gt("x"), "x"), []string{}) // Erroneous op equalTypes(t, td.Last(42, 33), nil) } golang-github-maxatome-go-testdeep-1.14.0/td/td_ignore.go000066400000000000000000000020731454313311600232660ustar00rootroot00000000000000// Copyright (c) 2018, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td import ( "reflect" "github.com/maxatome/go-testdeep/internal/ctxerr" ) type tdIgnore struct { baseOKNil } // summary(Ignore): allows to ignore a comparison // input(Ignore): all // Ignore operator is always true, whatever data is. It is useful when // comparing a slice with [Slice] and wanting to ignore some indexes, // for example (if you don't want to use [SuperSliceOf]). Or comparing // a struct with [SStruct] and wanting to ignore some fields: // // td.Cmp(t, got, td.SStruct( // Person{ // Name: "John Doe", // }, // td.StructFields{ // Age: td.Between(40, 45), // Children: td.Ignore(), // }), // ) func Ignore() TestDeep { return &tdIgnore{ baseOKNil: newBaseOKNil(3), } } func (i *tdIgnore) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error { return nil } func (i *tdIgnore) String() string { return "Ignore()" } golang-github-maxatome-go-testdeep-1.14.0/td/td_ignore_test.go000066400000000000000000000011451454313311600243240ustar00rootroot00000000000000// Copyright (c) 2018, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td_test import ( "testing" "github.com/maxatome/go-testdeep/internal/test" "github.com/maxatome/go-testdeep/td" ) func TestIgnore(t *testing.T) { checkOK(t, "any value!", td.Ignore()) checkOK(t, nil, td.Ignore()) checkOK(t, (*int)(nil), td.Ignore()) // // String test.EqualStr(t, td.Ignore().String(), "Ignore()") } func TestIgnoreTypeBehind(t *testing.T) { equalTypes(t, td.Ignore(), nil) } golang-github-maxatome-go-testdeep-1.14.0/td/td_isa.go000066400000000000000000000046001454313311600225550ustar00rootroot00000000000000// Copyright (c) 2018, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td import ( "reflect" "github.com/maxatome/go-testdeep/internal/ctxerr" ) type tdIsa struct { tdExpectedType checkImplement bool } var _ TestDeep = &tdIsa{} // summary(Isa): checks the data type or whether data implements an // interface or not // input(Isa): bool,str,int,float,cplx,array,slice,map,struct,ptr,chan,func // Isa operator checks the data type or whether data implements an // interface or not. // // Typical type checks: // // td.Cmp(t, time.Now(), td.Isa(time.Time{})) // succeeds // td.Cmp(t, time.Now(), td.Isa(&time.Time{})) // fails, as not a *time.Time // td.Cmp(t, got, td.Isa(map[string]time.Time{})) // // For interfaces, it is a bit more complicated, as: // // fmt.Stringer(nil) // // is not an interface, but just nil… To bypass this golang // limitation, Isa accepts pointers on interfaces. So checking that // data implements [fmt.Stringer] interface should be written as: // // td.Cmp(t, bytes.Buffer{}, td.Isa((*fmt.Stringer)(nil))) // succeeds // // Of course, in the latter case, if checked data type is // [*fmt.Stringer], Isa will match too (in fact before checking whether // it implements [fmt.Stringer] or not). // // TypeBehind method returns the [reflect.Type] of model. func Isa(model any) TestDeep { modelType := reflect.TypeOf(model) i := tdIsa{ tdExpectedType: tdExpectedType{ base: newBase(3), expectedType: modelType, }, } if modelType == nil { i.err = ctxerr.OpBad("Isa", "Isa(nil) is not allowed. To check an interface, try Isa((*fmt.Stringer)(nil)), for fmt.Stringer for example") return &i } i.checkImplement = modelType.Kind() == reflect.Ptr && modelType.Elem().Kind() == reflect.Interface return &i } func (i *tdIsa) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error { if i.err != nil { return ctx.CollectError(i.err) } gotType := got.Type() if gotType == i.expectedType { return nil } if i.checkImplement { if gotType.Implements(i.expectedType.Elem()) { return nil } } if ctx.BooleanError { return ctxerr.BooleanError } return ctx.CollectError(i.errorTypeMismatch(gotType)) } func (i *tdIsa) String() string { if i.err != nil { return i.stringError() } return i.expectedType.String() } golang-github-maxatome-go-testdeep-1.14.0/td/td_isa_test.go000066400000000000000000000064241454313311600236220ustar00rootroot00000000000000// Copyright (c) 2018, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td_test import ( "bytes" "fmt" "testing" "github.com/maxatome/go-testdeep/internal/test" "github.com/maxatome/go-testdeep/td" ) func TestIsa(t *testing.T) { gotStruct := MyStruct{ MyStructMid: MyStructMid{ MyStructBase: MyStructBase{ ValBool: true, }, ValStr: "foobar", }, ValInt: 123, } checkOK(t, &gotStruct, td.Isa(&MyStruct{})) checkOK(t, (*MyStruct)(nil), td.Isa(&MyStruct{})) checkOK(t, (*MyStruct)(nil), td.Isa((*MyStruct)(nil))) checkOK(t, gotStruct, td.Isa(MyStruct{})) checkOK(t, bytes.NewBufferString("foobar"), td.Isa((*fmt.Stringer)(nil)), "checks bytes.NewBufferString() implements fmt.Stringer") // does bytes.NewBufferString("foobar") implements fmt.Stringer? checkOK(t, bytes.NewBufferString("foobar"), td.Isa((*fmt.Stringer)(nil))) checkError(t, &gotStruct, td.Isa(&MyStructBase{}), expectedError{ Message: mustBe("type mismatch"), Path: mustBe("DATA"), Got: mustContain("*td_test.MyStruct"), Expected: mustContain("*td_test.MyStructBase"), }) checkError(t, (*MyStruct)(nil), td.Isa(&MyStructBase{}), expectedError{ Message: mustBe("type mismatch"), Path: mustBe("DATA"), Got: mustContain("*td_test.MyStruct"), Expected: mustContain("*td_test.MyStructBase"), }) checkError(t, gotStruct, td.Isa(&MyStruct{}), expectedError{ Message: mustBe("type mismatch"), Path: mustBe("DATA"), Got: mustContain("td_test.MyStruct"), Expected: mustContain("*td_test.MyStruct"), }) checkError(t, &gotStruct, td.Isa(MyStruct{}), expectedError{ Message: mustBe("type mismatch"), Path: mustBe("DATA"), Got: mustContain("*td_test.MyStruct"), Expected: mustContain("td_test.MyStruct"), }) gotSlice := []int{1, 2, 3} checkOK(t, gotSlice, td.Isa([]int{})) checkOK(t, &gotSlice, td.Isa(((*[]int)(nil)))) checkError(t, &gotSlice, td.Isa([]int{}), expectedError{ Message: mustBe("type mismatch"), Path: mustBe("DATA"), Got: mustContain("*[]int"), Expected: mustContain("[]int"), }) checkError(t, gotSlice, td.Isa((*[]int)(nil)), expectedError{ Message: mustBe("type mismatch"), Path: mustBe("DATA"), Got: mustContain("[]int"), Expected: mustContain("*[]int"), }) checkError(t, gotSlice, td.Isa([1]int{2}), expectedError{ Message: mustBe("type mismatch"), Path: mustBe("DATA"), Got: mustContain("[]int"), Expected: mustContain("[1]int"), }) // // Bad usage checkError(t, "never tested", td.Isa(nil), expectedError{ Message: mustBe("bad usage of Isa operator"), Path: mustBe("DATA"), Summary: mustBe("Isa(nil) is not allowed. To check an interface, try Isa((*fmt.Stringer)(nil)), for fmt.Stringer for example"), }) // // String test.EqualStr(t, td.Isa((*MyStruct)(nil)).String(), "*td_test.MyStruct") // Erroneous op test.EqualStr(t, td.Isa(nil).String(), "Isa()") } func TestIsaTypeBehind(t *testing.T) { equalTypes(t, td.Isa(([]int)(nil)), []int{}) equalTypes(t, td.Isa((*fmt.Stringer)(nil)), (*fmt.Stringer)(nil)) // Erroneous op equalTypes(t, td.Isa(nil), nil) } golang-github-maxatome-go-testdeep-1.14.0/td/td_json.go000066400000000000000000001307021454313311600227550ustar00rootroot00000000000000// Copyright (c) 2019-2023, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td import ( "bytes" ejson "encoding/json" "errors" "fmt" "io" "os" "reflect" "strings" "github.com/maxatome/go-testdeep/internal/ctxerr" "github.com/maxatome/go-testdeep/internal/dark" "github.com/maxatome/go-testdeep/internal/flat" "github.com/maxatome/go-testdeep/internal/json" "github.com/maxatome/go-testdeep/internal/location" "github.com/maxatome/go-testdeep/internal/types" "github.com/maxatome/go-testdeep/internal/util" ) // forbiddenOpsInJSON contains operators forbidden inside JSON, // SubJSONOf or SuperJSONOf, optionally with an alternative to help // the user. var forbiddenOpsInJSON = map[string]string{ "Array": "literal []", "Cap": "", "Catch": "", "Code": "", "Delay": "", "ErrorIs": "", "Isa": "", "JSON": "literal JSON", "Lax": "", "Map": "literal {}", "PPtr": "", "Ptr": "", "Recv": "", "SStruct": "", "Shallow": "", "Slice": "literal []", "Smuggle": "", "String": `literal ""`, "SubJSONOf": "SubMapOf operator", "SuperJSONOf": "SuperMapOf operator", "SuperSliceOf": "All and JSONPointer operators", "Struct": "", "Tag": "", "TruncTime": "", } // tdJSONUnmarshaler handles the JSON unmarshaling of JSON, SubJSONOf // and SuperJSONOf first parameter. type tdJSONUnmarshaler struct { location.Location // position of the operator } // newJSONUnmarshaler returns a new instance of tdJSONUnmarshaler. func newJSONUnmarshaler(pos location.Location) tdJSONUnmarshaler { return tdJSONUnmarshaler{ Location: pos, } } // replaceLocation replaces the location of tdOp by the // JSON/SubJSONOf/SuperJSONOf one then add the position of the // operator inside the JSON string. func (u tdJSONUnmarshaler) replaceLocation(tdOp TestDeep, posInJSON json.Position) { // The goal, instead of: // [under operator Len at value.go:476] // having: // [under operator Len at line 12:7 (pos 123) inside operator JSON at file.go:23] // so add ^------------------------------------------^ newPos := u.Location newPos.Inside = fmt.Sprintf("%s inside operator %s ", posInJSON, u.Func) newPos.Func = tdOp.GetLocation().Func tdOp.replaceLocation(newPos) } // unmarshal unmarshals expectedJSON using placeholder parameters params. func (u tdJSONUnmarshaler) unmarshal(expectedJSON any, params []any) (any, *ctxerr.Error) { var ( err error b []byte ) switch data := expectedJSON.(type) { case string: // Try to load this file (if it seems it can be a filename and not // a JSON content) if strings.HasSuffix(data, ".json") { // It could be a file name, try to read from it b, err = os.ReadFile(data) if err != nil { return nil, ctxerr.OpBad(u.Func, "JSON file %s cannot be read: %s", data, err) } break } b = []byte(data) case []byte: b = data case ejson.RawMessage: b = data case io.Reader: b, err = io.ReadAll(data) if err != nil { return nil, ctxerr.OpBad(u.Func, "JSON read error: %s", err) } default: return nil, ctxerr.OpBadUsage( u.Func, "(STRING_JSON|STRING_FILENAME|[]byte|json.RawMessage|io.Reader, ...)", expectedJSON, 1, false) } params = flat.Interfaces(params...) var byTag map[string]any for i, p := range params { if op, ok := p.(*tdTag); ok && op.err == nil { if byTag[op.tag] != nil { return nil, ctxerr.OpBad(u.Func, `2 params have the same tag "%s"`, op.tag) } if byTag == nil { byTag = map[string]any{} } // Don't keep the tag layer p = nil if op.expectedValue.IsValid() { p = op.expectedValue.Interface() } byTag[op.tag] = newJSONNamedPlaceholder(op.tag, p) } params[i] = newJSONNumPlaceholder(uint64(i+1), p) } final, err := json.Parse(b, json.ParseOpts{ Placeholders: params, PlaceholdersByName: byTag, OpFn: u.resolveOp(), }) if err != nil { return nil, ctxerr.OpBad(u.Func, "JSON unmarshal error: %s", err) } return final, nil } // resolveOp returns a closure usable as json.ParseOpts.OpFn. func (u tdJSONUnmarshaler) resolveOp() func(json.Operator, json.Position) (any, error) { return func(jop json.Operator, posInJSON json.Position) (any, error) { op, exists := allOperators[jop.Name] if !exists { return nil, fmt.Errorf("unknown operator %s()", jop.Name) } if hint, exists := forbiddenOpsInJSON[jop.Name]; exists { if hint == "" { return nil, fmt.Errorf("%s() is not usable in JSON()", jop.Name) } return nil, fmt.Errorf("%s() is not usable in JSON(), use %s instead", jop.Name, hint) } vfn := reflect.ValueOf(op) tfn := vfn.Type() // If some parameters contain a placeholder, dereference it for i, p := range jop.Params { if ph, ok := p.(*tdJSONPlaceholder); ok { jop.Params[i] = ph.expectedValue.Interface() } } // Special cases var min, max int addNilParam := false switch jop.Name { case "Between": min, max = 2, 3 if len(jop.Params) == 3 { bad := false switch tp := jop.Params[2].(type) { case BoundsKind: // Special case, accept numeric values of Bounds* // constants, for the case: // td.JSON(`Between(40, 42, $1)`, td.BoundsInOut) case string: switch tp { case "[]", "BoundsInIn": jop.Params[2] = BoundsInIn case "[[", "BoundsInOut": jop.Params[2] = BoundsInOut case "]]", "BoundsOutIn": jop.Params[2] = BoundsOutIn case "][", "BoundsOutOut": jop.Params[2] = BoundsOutOut default: bad = true } default: bad = true } if bad { return nil, errors.New(`Between() bad 3rd parameter, use "[]", "[[", "]]" or "]["`) } } case "N", "Re": min, max = 1, 2 case "SubMapOf", "SuperMapOf": min, max, addNilParam = 1, 1, true default: min = tfn.NumIn() if tfn.IsVariadic() { // for All(expected ...any) → min == 1, as All() is a non-sense max = -1 } else { max = min } } if len(jop.Params) < min || (max >= 0 && len(jop.Params) > max) { switch { case max < 0: return nil, fmt.Errorf("%s() requires at least one parameter", jop.Name) case max == 0: return nil, fmt.Errorf("%s() requires no parameters", jop.Name) case min == max: if min == 1 { return nil, fmt.Errorf("%s() requires only one parameter", jop.Name) } return nil, fmt.Errorf("%s() requires %d parameters", jop.Name, min) default: return nil, fmt.Errorf("%s() requires %d or %d parameters", jop.Name, min, max) } } var in []reflect.Value if len(jop.Params) > 0 { in = make([]reflect.Value, len(jop.Params)) for i, p := range jop.Params { in[i] = reflect.ValueOf(p) } if addNilParam { in = append(in, reflect.ValueOf(MapEntries(nil))) } // If the function is variadic, no need to check each param as all // variadic operators have always a ...any numCheck := len(in) if tfn.IsVariadic() { numCheck = tfn.NumIn() - 1 } for i, p := range in[:numCheck] { fpt := tfn.In(i) if fpt.Kind() != reflect.Interface && p.Type() != fpt { return nil, fmt.Errorf( "%s() bad #%d parameter type: %s required but %s received", jop.Name, i+1, fpt, p.Type(), ) } } } tdOp := vfn.Call(in)[0].Interface().(TestDeep) // let erroneous operators (tdOp.err != nil) pass // replace the location by the JSON/SubJSONOf/SuperJSONOf one u.replaceLocation(tdOp, posInJSON) return newJSONEmbedded(tdOp), nil } } // tdJSONSmuggler is the base type for tdJSONPlaceholder & tdJSONEmbedded. type tdJSONSmuggler struct { tdSmugglerBase // ignored by tools/gen_funcs.pl } func (s *tdJSONSmuggler) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error { vgot, _ := jsonify(ctx, got) // Cannot fail // Here, vgot type is either a bool, float64, string, // []any, a map[string]any or simply nil return s.jsonValueEqual(ctx, vgot) } func (s *tdJSONSmuggler) String() string { return util.ToString(s.expectedValue.Interface()) } func (s *tdJSONSmuggler) HandleInvalid() bool { return true } func (s *tdJSONSmuggler) TypeBehind() reflect.Type { return s.internalTypeBehind() } // tdJSONPlaceholder is an internal smuggler operator. It represents a // JSON placeholder in an unmarshaled JSON expected data structure. As $1 in: // // td.JSON(`{"foo": $1}`, td.Between(12, 34)) // // It takes the JSON representation of data and compares it to // expectedValue. // // It does its best to convert back the JSON pointed data to the type // of expectedValue or to the type behind the expectedValue. type tdJSONPlaceholder struct { tdJSONSmuggler name string num uint64 } func newJSONNamedPlaceholder(name string, expectedValue any) TestDeep { p := tdJSONPlaceholder{ tdJSONSmuggler: tdJSONSmuggler{ tdSmugglerBase: newSmugglerBase(expectedValue, -100), // without location }, name: name, } if !p.isTestDeeper { p.expectedValue = reflect.ValueOf(expectedValue) } return &p } func newJSONNumPlaceholder(num uint64, expectedValue any) TestDeep { p := tdJSONPlaceholder{ tdJSONSmuggler: tdJSONSmuggler{ tdSmugglerBase: newSmugglerBase(expectedValue, -100), // without location }, num: num, } if !p.isTestDeeper { p.expectedValue = reflect.ValueOf(expectedValue) } return &p } func (p *tdJSONPlaceholder) MarshalJSON() ([]byte, error) { if !p.isTestDeeper { var expected any if p.expectedValue.IsValid() { expected = p.expectedValue.Interface() } return ejson.Marshal(expected) } var b bytes.Buffer if p.num == 0 { fmt.Fprintf(&b, `"$%s"`, p.name) } else { fmt.Fprintf(&b, `"$%d"`, p.num) } b.WriteString(` /* `) indent := "\n" + strings.Repeat(" ", b.Len()) b.WriteString(strings.ReplaceAll(p.String(), "\n", indent)) b.WriteString(` */`) return b.Bytes(), nil } // tdJSONEmbedded represents a MarshalJSON'able operator. As Between() in: // // td.JSON(`{"foo": Between(12, 34)}`) // // tdSmugglerBase always contains a TestDeep operator, newJSONEmbedded() // ensures that. // // It does its best to convert back the JSON pointed data to the type // of the type behind the expectedValue (which is always a TestDeep // operator). type tdJSONEmbedded struct { tdJSONSmuggler } func newJSONEmbedded(tdOp TestDeep) TestDeep { e := tdJSONEmbedded{ tdJSONSmuggler: tdJSONSmuggler{ tdSmugglerBase: newSmugglerBase(tdOp, -100), // without location }, } return &e } func (e *tdJSONEmbedded) MarshalJSON() ([]byte, error) { return []byte(e.String()), nil } // tdJSON is the JSON operator. type tdJSON struct { baseOKNil expected reflect.Value } var _ TestDeep = &tdJSON{} func gotViaJSON(ctx ctxerr.Context, pGot *reflect.Value) *ctxerr.Error { got, err := jsonify(ctx, *pGot) if err != nil { return err } *pGot = reflect.ValueOf(got) return nil } func jsonify(ctx ctxerr.Context, got reflect.Value) (any, *ctxerr.Error) { gotIf, ok := dark.GetInterface(got, true) if !ok { return nil, ctx.CannotCompareError() } b, err := ejson.Marshal(gotIf) if err != nil { if ctx.BooleanError { return nil, ctxerr.BooleanError } return nil, &ctxerr.Error{ Message: "json.Marshal failed", Summary: ctxerr.NewSummary(err.Error()), } } // As Marshal succeeded, Unmarshal in an any cannot fail var vgot any ejson.Unmarshal(b, &vgot) //nolint: errcheck return vgot, nil } // summary(JSON): compares against JSON representation // input(JSON): nil,bool,str,int,float,array,slice,map,struct,ptr // JSON operator allows to compare the JSON representation of data // against expectedJSON. expectedJSON can be a: // // - string containing JSON data like `{"fullname":"Bob","age":42}` // - string containing a JSON filename, ending with ".json" (its // content is [os.ReadFile] before unmarshaling) // - []byte containing JSON data // - [encoding/json.RawMessage] containing JSON data // - [io.Reader] stream containing JSON data (is [io.ReadAll] // before unmarshaling) // // expectedJSON JSON value can contain placeholders. The params // are for any placeholder parameters in expectedJSON. params can // contain [TestDeep] operators as well as raw values. A placeholder can // be numeric like $2 or named like $name and always references an // item in params. // // Numeric placeholders reference the n'th "operators" item (starting // at 1). Named placeholders are used with [Tag] operator as follows: // // td.Cmp(t, gotValue, // td.JSON(`{"fullname": $name, "age": $2, "gender": $3}`, // td.Tag("name", td.HasPrefix("Foo")), // matches $1 and $name // td.Between(41, 43), // matches only $2 // "male")) // matches only $3 // // Note that placeholders can be double-quoted as in: // // td.Cmp(t, gotValue, // td.JSON(`{"fullname": "$name", "age": "$2", "gender": "$3"}`, // td.Tag("name", td.HasPrefix("Foo")), // matches $1 and $name // td.Between(41, 43), // matches only $2 // "male")) // matches only $3 // // It makes no difference whatever the underlying type of the replaced // item is (= double quoting a placeholder matching a number is not a // problem). It is just a matter of taste, double-quoting placeholders // can be preferred when the JSON data has to conform to the JSON // specification, like when used in a ".json" file. // // JSON does its best to convert back the JSON corresponding to a // placeholder to the type of the placeholder or, if the placeholder // is an operator, to the type behind the operator. Allowing to do // things like: // // td.Cmp(t, gotValue, td.JSON(`{"foo":$1}`, []int{1, 2, 3, 4})) // td.Cmp(t, gotValue, // td.JSON(`{"foo":$1}`, []any{1, 2, td.Between(2, 4), 4})) // td.Cmp(t, gotValue, td.JSON(`{"foo":$1}`, td.Between(27, 32))) // // Of course, it does this conversion only if the expected type can be // guessed. In the case the conversion cannot occur, data is compared // as is, in its freshly unmarshaled JSON form (so as bool, float64, // string, []any, map[string]any or simply nil). // // Note expectedJSON can be a []byte, an [encoding/json.RawMessage], a // JSON filename or a [io.Reader]: // // td.Cmp(t, gotValue, td.JSON("file.json", td.Between(12, 34))) // td.Cmp(t, gotValue, td.JSON([]byte(`[1, $1, 3]`), td.Between(12, 34))) // td.Cmp(t, gotValue, td.JSON(osFile, td.Between(12, 34))) // // A JSON filename ends with ".json". // // To avoid a legit "$" string prefix causes a bad placeholder error, // just double it to escape it. Note it is only needed when the "$" is // the first character of a string: // // td.Cmp(t, gotValue, // td.JSON(`{"fullname": "$name", "details": "$$info", "age": $2}`, // td.Tag("name", td.HasPrefix("Foo")), // matches $1 and $name // td.Between(41, 43))) // matches only $2 // // For the "details" key, the raw value "$info" is expected, no // placeholders are involved here. // // Note that [Lax] mode is automatically enabled by JSON operator to // simplify numeric tests. // // Comments can be embedded in JSON data: // // td.Cmp(t, gotValue, // td.JSON(` // { // // A guy properties: // "fullname": "$name", // The full name of the guy // "details": "$$info", // Literally "$info", thanks to "$" escape // "age": $2 /* The age of the guy: // - placeholder unquoted, but could be without // any change // - to demonstrate a multi-lines comment */ // }`, // td.Tag("name", td.HasPrefix("Foo")), // matches $1 and $name // td.Between(41, 43))) // matches only $2 // // Comments, like in go, have 2 forms. To quote the Go language specification: // - line comments start with the character sequence // and stop at the // end of the line. // - multi-lines comments start with the character sequence /* and stop // with the first subsequent character sequence */. // // Other JSON divergences: // - ',' can precede a '}' or a ']' (as in go); // - strings can contain non-escaped \n, \r and \t; // - raw strings are accepted (r{raw}, r!raw!, …), see below; // - int_lit & float_lit numbers as defined in go spec are accepted; // - numbers can be prefixed by '+'. // // Most operators can be directly embedded in JSON without requiring // any placeholder. If an operators does not take any parameter, the // parenthesis can be omitted. // // td.Cmp(t, gotValue, // td.JSON(` // { // "fullname": HasPrefix("Foo"), // "age": Between(41, 43), // "details": SuperMapOf({ // "address": NotEmpty, // () are optional when no parameters // "car": Any("Peugeot", "Tesla", "Jeep") // any of these // }) // }`)) // // Placeholders can be used anywhere, even in operators parameters as in: // // td.Cmp(t, gotValue, td.JSON(`{"fullname": HasPrefix($1)}`, "Zip")) // // A few notes about operators embedding: // - [SubMapOf] and [SuperMapOf] take only one parameter, a JSON object; // - the optional 3rd parameter of [Between] has to be specified as a string // and can be: "[]" or "BoundsInIn" (default), "[[" or "BoundsInOut", // "]]" or "BoundsOutIn", "][" or "BoundsOutOut"; // - not all operators are embeddable only the following are: [All], // [Any], [ArrayEach], [Bag], [Between], [Contains], // [ContainsKey], [Empty], [First], [Grep], [Gt], [Gte], // [HasPrefix], [HasSuffix], [Ignore], [JSONPointer], [Keys], // [Last], [Len], [Lt], [Lte], [MapEach], [N], [NaN], [Nil], // [None], [Not], [NotAny], [NotEmpty], [NotNaN], [NotNil], // [NotZero], [Re], [ReAll], [Set], [SubBagOf], [SubMapOf], // [SubSetOf], [SuperBagOf], [SuperMapOf], [SuperSetOf], [Values] // and [Zero]. // // It is also possible to embed operators in JSON strings. This way, // the JSON specification can be fulfilled. To avoid collision with // possible strings, just prefix the first operator name with // "$^". The previous example becomes: // // td.Cmp(t, gotValue, // td.JSON(` // { // "fullname": "$^HasPrefix(\"Foo\")", // "age": "$^Between(41, 43)", // "details": "$^SuperMapOf({ // \"address\": NotEmpty, // () are optional when no parameters // \"car\": Any(\"Peugeot\", \"Tesla\", \"Jeep\") // any of these // })" // }`)) // // As you can see, in this case, strings in strings have to be // escaped. Fortunately, newlines are accepted, but unfortunately they // are forbidden by JSON specification. To avoid too much escaping, // raw strings are accepted. A raw string is a "r" followed by a // delimiter, the corresponding delimiter closes the string. The // following raw strings are all the same as "foo\\bar(\"zip\")!": // - r'foo\bar"zip"!' // - r,foo\bar"zip"!, // - r%foo\bar"zip"!% // - r(foo\bar("zip")!) // - r{foo\bar("zip")!} // - r[foo\bar("zip")!] // - r // // So non-bracketing delimiters use the same character before and // after, but the 4 sorts of ASCII brackets (round, angle, square, // curly) all nest: r[x[y]z] equals "x[y]z". The end delimiter cannot // be escaped. // // With raw strings, the previous example becomes: // // td.Cmp(t, gotValue, // td.JSON(` // { // "fullname": "$^HasPrefix(r)", // "age": "$^Between(41, 43)", // "details": "$^SuperMapOf({ // r
: NotEmpty, // () are optional when no parameters // r: Any(r, r, r) // any of these // })" // }`)) // // Note that raw strings are accepted anywhere, not only in original // JSON strings. // // To be complete, $^ can prefix an operator even outside a // string. This is accepted for compatibility purpose as the first // operator embedding feature used this way to embed some operators. // // So the following calls are all equivalent: // // td.Cmp(t, gotValue, td.JSON(`{"id": $1}`, td.NotZero())) // td.Cmp(t, gotValue, td.JSON(`{"id": NotZero}`)) // td.Cmp(t, gotValue, td.JSON(`{"id": NotZero()}`)) // td.Cmp(t, gotValue, td.JSON(`{"id": $^NotZero}`)) // td.Cmp(t, gotValue, td.JSON(`{"id": $^NotZero()}`)) // td.Cmp(t, gotValue, td.JSON(`{"id": "$^NotZero"}`)) // td.Cmp(t, gotValue, td.JSON(`{"id": "$^NotZero()"}`)) // // As for placeholders, there is no differences between $^NotZero and // "$^NotZero". // // Tip: when an [io.Reader] is expected to contain JSON data, it // cannot be tested directly, but using the [Smuggle] operator simply // solves the problem: // // var body io.Reader // // … // td.Cmp(t, body, td.Smuggle(json.RawMessage{}, td.JSON(`{"foo":1}`))) // // or equally // td.Cmp(t, body, td.Smuggle(json.RawMessage(nil), td.JSON(`{"foo":1}`))) // // [Smuggle] reads from body into an [encoding/json.RawMessage] then // this buffer is unmarshaled by JSON operator before the comparison. // // TypeBehind method returns the [reflect.Type] of the expectedJSON // once JSON unmarshaled. So it can be bool, string, float64, []any, // map[string]any or any in case expectedJSON is "null". // // See also [JSONPointer], [SubJSONOf] and [SuperJSONOf]. func JSON(expectedJSON any, params ...any) TestDeep { j := &tdJSON{ baseOKNil: newBaseOKNil(3), } v, err := newJSONUnmarshaler(j.GetLocation()).unmarshal(expectedJSON, params) if err != nil { j.err = err } else { j.expected = reflect.ValueOf(v) } return j } func (j *tdJSON) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error { if j.err != nil { return ctx.CollectError(j.err) } err := gotViaJSON(ctx, &got) if err != nil { return ctx.CollectError(err) } ctx.BeLax = true return deepValueEqual(ctx, got, j.expected) } func (j *tdJSON) String() string { if j.err != nil { return j.stringError() } return jsonStringify("JSON", j.expected) } func jsonStringify(opName string, v reflect.Value) string { if !v.IsValid() { return "JSON(null)" } var b bytes.Buffer b.WriteString(opName) b.WriteByte('(') json.AppendMarshal(&b, v.Interface(), len(opName)+1) //nolint: errcheck b.WriteByte(')') return b.String() } func (j *tdJSON) TypeBehind() reflect.Type { if j.err != nil { return nil } if j.expected.IsValid() { // In case we have an operator at the root, delegate it the call if tdOp, ok := j.expected.Interface().(TestDeep); ok { return tdOp.TypeBehind() } return j.expected.Type() } return types.Interface } type tdMapJSON struct { tdMap expected reflect.Value } var _ TestDeep = &tdMapJSON{} // summary(SubJSONOf): compares struct or map against JSON // representation but with potentially some exclusions // input(SubJSONOf): map,struct,ptr(ptr on map/struct) // SubJSONOf operator allows to compare the JSON representation of // data against expectedJSON. Unlike [JSON] operator, marshaled data // must be a JSON object/map (aka {…}). expectedJSON can be a: // // - string containing JSON data like `{"fullname":"Bob","age":42}` // - string containing a JSON filename, ending with ".json" (its // content is [os.ReadFile] before unmarshaling) // - []byte containing JSON data // - [encoding/json.RawMessage] containing JSON data // - [io.Reader] stream containing JSON data (is [io.ReadAll] before // unmarshaling) // // JSON data contained in expectedJSON must be a JSON object/map // (aka {…}) too. During a match, each expected entry should match in // the compared map. But some expected entries can be missing from the // compared map. // // type MyStruct struct { // Name string `json:"name"` // Age int `json:"age"` // } // got := MyStruct{ // Name: "Bob", // Age: 42, // } // td.Cmp(t, got, td.SubJSONOf(`{"name": "Bob", "age": 42, "city": "NY"}`)) // succeeds // td.Cmp(t, got, td.SubJSONOf(`{"name": "Bob", "zip": 666}`)) // fails, extra "age" // // expectedJSON JSON value can contain placeholders. The params // are for any placeholder parameters in expectedJSON. params can // contain [TestDeep] operators as well as raw values. A placeholder can // be numeric like $2 or named like $name and always references an // item in params. // // Numeric placeholders reference the n'th "operators" item (starting // at 1). Named placeholders are used with [Tag] operator as follows: // // td.Cmp(t, gotValue, // td.SubJSONOf(`{"fullname": $name, "age": $2, "gender": $3}`, // td.Tag("name", td.HasPrefix("Foo")), // matches $1 and $name // td.Between(41, 43), // matches only $2 // "male")) // matches only $3 // // Note that placeholders can be double-quoted as in: // // td.Cmp(t, gotValue, // td.SubJSONOf(`{"fullname": "$name", "age": "$2", "gender": "$3"}`, // td.Tag("name", td.HasPrefix("Foo")), // matches $1 and $name // td.Between(41, 43), // matches only $2 // "male")) // matches only $3 // // It makes no difference whatever the underlying type of the replaced // item is (= double quoting a placeholder matching a number is not a // problem). It is just a matter of taste, double-quoting placeholders // can be preferred when the JSON data has to conform to the JSON // specification, like when used in a ".json" file. // // SubJSONOf does its best to convert back the JSON corresponding to a // placeholder to the type of the placeholder or, if the placeholder // is an operator, to the type behind the operator. Allowing to do // things like: // // td.Cmp(t, gotValue, // td.SubJSONOf(`{"foo":$1, "bar": 12}`, []int{1, 2, 3, 4})) // td.Cmp(t, gotValue, // td.SubJSONOf(`{"foo":$1, "bar": 12}`, []any{1, 2, td.Between(2, 4), 4})) // td.Cmp(t, gotValue, // td.SubJSONOf(`{"foo":$1, "bar": 12}`, td.Between(27, 32))) // // Of course, it does this conversion only if the expected type can be // guessed. In the case the conversion cannot occur, data is compared // as is, in its freshly unmarshaled JSON form (so as bool, float64, // string, []any, map[string]any or simply nil). // // Note expectedJSON can be a []byte, an [encoding/json.RawMessage], a // JSON filename or a [io.Reader]: // // td.Cmp(t, gotValue, td.SubJSONOf("file.json", td.Between(12, 34))) // td.Cmp(t, gotValue, td.SubJSONOf([]byte(`[1, $1, 3]`), td.Between(12, 34))) // td.Cmp(t, gotValue, td.SubJSONOf(osFile, td.Between(12, 34))) // // A JSON filename ends with ".json". // // To avoid a legit "$" string prefix causes a bad placeholder error, // just double it to escape it. Note it is only needed when the "$" is // the first character of a string: // // td.Cmp(t, gotValue, // td.SubJSONOf(`{"fullname": "$name", "details": "$$info", "age": $2}`, // td.Tag("name", td.HasPrefix("Foo")), // matches $1 and $name // td.Between(41, 43))) // matches only $2 // // For the "details" key, the raw value "$info" is expected, no // placeholders are involved here. // // Note that [Lax] mode is automatically enabled by SubJSONOf operator to // simplify numeric tests. // // Comments can be embedded in JSON data: // // td.Cmp(t, gotValue, // SubJSONOf(` // { // // A guy properties: // "fullname": "$name", // The full name of the guy // "details": "$$info", // Literally "$info", thanks to "$" escape // "age": $2 /* The age of the guy: // - placeholder unquoted, but could be without // any change // - to demonstrate a multi-lines comment */ // }`, // td.Tag("name", td.HasPrefix("Foo")), // matches $1 and $name // td.Between(41, 43))) // matches only $2 // // Comments, like in go, have 2 forms. To quote the Go language specification: // - line comments start with the character sequence // and stop at the // end of the line. // - multi-lines comments start with the character sequence /* and stop // with the first subsequent character sequence */. // // Other JSON divergences: // - ',' can precede a '}' or a ']' (as in go); // - strings can contain non-escaped \n, \r and \t; // - raw strings are accepted (r{raw}, r!raw!, …), see below; // - int_lit & float_lit numbers as defined in go spec are accepted; // - numbers can be prefixed by '+'. // // Most operators can be directly embedded in SubJSONOf without requiring // any placeholder. If an operators does not take any parameter, the // parenthesis can be omitted. // // td.Cmp(t, gotValue, // td.SubJSONOf(` // { // "fullname": HasPrefix("Foo"), // "age": Between(41, 43), // "details": SuperMapOf({ // "address": NotEmpty, // () are optional when no parameters // "car": Any("Peugeot", "Tesla", "Jeep") // any of these // }) // }`)) // // Placeholders can be used anywhere, even in operators parameters as in: // // td.Cmp(t, gotValue, // td.SubJSONOf(`{"fullname": HasPrefix($1), "bar": 42}`, "Zip")) // // A few notes about operators embedding: // - [SubMapOf] and [SuperMapOf] take only one parameter, a JSON object; // - the optional 3rd parameter of [Between] has to be specified as a string // and can be: "[]" or "BoundsInIn" (default), "[[" or "BoundsInOut", // "]]" or "BoundsOutIn", "][" or "BoundsOutOut"; // - not all operators are embeddable only the following are: [All], // [Any], [ArrayEach], [Bag], [Between], [Contains], // [ContainsKey], [Empty], [First], [Grep], [Gt], [Gte], // [HasPrefix], [HasSuffix], [Ignore], [JSONPointer], [Keys], // [Last], [Len], [Lt], [Lte], [MapEach], [N], [NaN], [Nil], // [None], [Not], [NotAny], [NotEmpty], [NotNaN], [NotNil], // [NotZero], [Re], [ReAll], [Set], [SubBagOf], [SubMapOf], // [SubSetOf], [SuperBagOf], [SuperMapOf], [SuperSetOf], [Values] // and [Zero]. // // It is also possible to embed operators in JSON strings. This way, // the JSON specification can be fulfilled. To avoid collision with // possible strings, just prefix the first operator name with // "$^". The previous example becomes: // // td.Cmp(t, gotValue, // td.SubJSONOf(` // { // "fullname": "$^HasPrefix(\"Foo\")", // "age": "$^Between(41, 43)", // "details": "$^SuperMapOf({ // \"address\": NotEmpty, // () are optional when no parameters // \"car\": Any(\"Peugeot\", \"Tesla\", \"Jeep\") // any of these // })" // }`)) // // As you can see, in this case, strings in strings have to be // escaped. Fortunately, newlines are accepted, but unfortunately they // are forbidden by JSON specification. To avoid too much escaping, // raw strings are accepted. A raw string is a "r" followed by a // delimiter, the corresponding delimiter closes the string. The // following raw strings are all the same as "foo\\bar(\"zip\")!": // - r'foo\bar"zip"!' // - r,foo\bar"zip"!, // - r%foo\bar"zip"!% // - r(foo\bar("zip")!) // - r{foo\bar("zip")!} // - r[foo\bar("zip")!] // - r // // So non-bracketing delimiters use the same character before and // after, but the 4 sorts of ASCII brackets (round, angle, square, // curly) all nest: r[x[y]z] equals "x[y]z". The end delimiter cannot // be escaped. // // With raw strings, the previous example becomes: // // td.Cmp(t, gotValue, // td.SubJSONOf(` // { // "fullname": "$^HasPrefix(r)", // "age": "$^Between(41, 43)", // "details": "$^SuperMapOf({ // r
: NotEmpty, // () are optional when no parameters // r: Any(r, r, r) // any of these // })" // }`)) // // Note that raw strings are accepted anywhere, not only in original // JSON strings. // // To be complete, $^ can prefix an operator even outside a // string. This is accepted for compatibility purpose as the first // operator embedding feature used this way to embed some operators. // // So the following calls are all equivalent: // // td.Cmp(t, gotValue, td.SubJSONOf(`{"id": $1}`, td.NotZero())) // td.Cmp(t, gotValue, td.SubJSONOf(`{"id": NotZero}`)) // td.Cmp(t, gotValue, td.SubJSONOf(`{"id": NotZero()}`)) // td.Cmp(t, gotValue, td.SubJSONOf(`{"id": $^NotZero}`)) // td.Cmp(t, gotValue, td.SubJSONOf(`{"id": $^NotZero()}`)) // td.Cmp(t, gotValue, td.SubJSONOf(`{"id": "$^NotZero"}`)) // td.Cmp(t, gotValue, td.SubJSONOf(`{"id": "$^NotZero()"}`)) // // As for placeholders, there is no differences between $^NotZero and // "$^NotZero". // // Tip: when an [io.Reader] is expected to contain JSON data, it // cannot be tested directly, but using the [Smuggle] operator simply // solves the problem: // // var body io.Reader // // … // td.Cmp(t, body, td.Smuggle(json.RawMessage{}, td.SubJSONOf(`{"foo":1,"bar":2}`))) // // or equally // td.Cmp(t, body, td.Smuggle(json.RawMessage(nil), td.SubJSONOf(`{"foo":1,"bar":2}`))) // // [Smuggle] reads from body into an [encoding/json.RawMessage] then // this buffer is unmarshaled by SubJSONOf operator before the comparison. // // TypeBehind method returns the map[string]any type. // // See also [JSON], [JSONPointer] and [SuperJSONOf]. func SubJSONOf(expectedJSON any, params ...any) TestDeep { m := &tdMapJSON{ tdMap: tdMap{ tdExpectedType: tdExpectedType{ base: newBase(3), expectedType: reflect.TypeOf((map[string]any)(nil)), }, kind: subMap, }, } v, err := newJSONUnmarshaler(m.GetLocation()).unmarshal(expectedJSON, params) if err != nil { m.err = err return m } _, ok := v.(map[string]any) if !ok { m.err = ctxerr.OpBad("SubJSONOf", "SubJSONOf() only accepts JSON objects {…}") return m } m.expected = reflect.ValueOf(v) m.populateExpectedEntries(nil, m.expected) return m } // summary(SuperJSONOf): compares struct or map against JSON // representation but with potentially extra entries // input(SuperJSONOf): map,struct,ptr(ptr on map/struct) // SuperJSONOf operator allows to compare the JSON representation of // data against expectedJSON. Unlike JSON operator, marshaled data // must be a JSON object/map (aka {…}). expectedJSON can be a: // // - string containing JSON data like `{"fullname":"Bob","age":42}` // - string containing a JSON filename, ending with ".json" (its // content is [os.ReadFile] before unmarshaling) // - []byte containing JSON data // - [encoding/json.RawMessage] containing JSON data // - [io.Reader] stream containing JSON data (is [io.ReadAll] before // unmarshaling) // // JSON data contained in expectedJSON must be a JSON object/map // (aka {…}) too. During a match, each expected entry should match in // the compared map. But some entries in the compared map may not be // expected. // // type MyStruct struct { // Name string `json:"name"` // Age int `json:"age"` // City string `json:"city"` // } // got := MyStruct{ // Name: "Bob", // Age: 42, // City: "TestCity", // } // td.Cmp(t, got, td.SuperJSONOf(`{"name": "Bob", "age": 42}`)) // succeeds // td.Cmp(t, got, td.SuperJSONOf(`{"name": "Bob", "zip": 666}`)) // fails, miss "zip" // // expectedJSON JSON value can contain placeholders. The params are // for any placeholder parameters in expectedJSON. params can contain // [TestDeep] operators as well as raw values. A placeholder can be // numeric like $2 or named like $name and always references an item // in params. // // Numeric placeholders reference the n'th "operators" item (starting // at 1). Named placeholders are used with [Tag] operator as follows: // // td.Cmp(t, gotValue, // SuperJSONOf(`{"fullname": $name, "age": $2, "gender": $3}`, // td.Tag("name", td.HasPrefix("Foo")), // matches $1 and $name // td.Between(41, 43), // matches only $2 // "male")) // matches only $3 // // Note that placeholders can be double-quoted as in: // // td.Cmp(t, gotValue, // td.SuperJSONOf(`{"fullname": "$name", "age": "$2", "gender": "$3"}`, // td.Tag("name", td.HasPrefix("Foo")), // matches $1 and $name // td.Between(41, 43), // matches only $2 // "male")) // matches only $3 // // It makes no difference whatever the underlying type of the replaced // item is (= double quoting a placeholder matching a number is not a // problem). It is just a matter of taste, double-quoting placeholders // can be preferred when the JSON data has to conform to the JSON // specification, like when used in a ".json" file. // // SuperJSONOf does its best to convert back the JSON corresponding to a // placeholder to the type of the placeholder or, if the placeholder // is an operator, to the type behind the operator. Allowing to do // things like: // // td.Cmp(t, gotValue, // td.SuperJSONOf(`{"foo":$1}`, []int{1, 2, 3, 4})) // td.Cmp(t, gotValue, // td.SuperJSONOf(`{"foo":$1}`, []any{1, 2, td.Between(2, 4), 4})) // td.Cmp(t, gotValue, // td.SuperJSONOf(`{"foo":$1}`, td.Between(27, 32))) // // Of course, it does this conversion only if the expected type can be // guessed. In the case the conversion cannot occur, data is compared // as is, in its freshly unmarshaled JSON form (so as bool, float64, // string, []any, map[string]any or simply nil). // // Note expectedJSON can be a []byte, an [encoding/json.RawMessage], a // JSON filename or a [io.Reader]: // // td.Cmp(t, gotValue, td.SuperJSONOf("file.json", td.Between(12, 34))) // td.Cmp(t, gotValue, td.SuperJSONOf([]byte(`[1, $1, 3]`), td.Between(12, 34))) // td.Cmp(t, gotValue, td.SuperJSONOf(osFile, td.Between(12, 34))) // // A JSON filename ends with ".json". // // To avoid a legit "$" string prefix causes a bad placeholder error, // just double it to escape it. Note it is only needed when the "$" is // the first character of a string: // // td.Cmp(t, gotValue, // td.SuperJSONOf(`{"fullname": "$name", "details": "$$info", "age": $2}`, // td.Tag("name", td.HasPrefix("Foo")), // matches $1 and $name // td.Between(41, 43))) // matches only $2 // // For the "details" key, the raw value "$info" is expected, no // placeholders are involved here. // // Note that [Lax] mode is automatically enabled by SuperJSONOf operator to // simplify numeric tests. // // Comments can be embedded in JSON data: // // td.Cmp(t, gotValue, // td.SuperJSONOf(` // { // // A guy properties: // "fullname": "$name", // The full name of the guy // "details": "$$info", // Literally "$info", thanks to "$" escape // "age": $2 /* The age of the guy: // - placeholder unquoted, but could be without // any change // - to demonstrate a multi-lines comment */ // }`, // td.Tag("name", td.HasPrefix("Foo")), // matches $1 and $name // td.Between(41, 43))) // matches only $2 // // Comments, like in go, have 2 forms. To quote the Go language specification: // - line comments start with the character sequence // and stop at the // end of the line. // - multi-lines comments start with the character sequence /* and stop // with the first subsequent character sequence */. // // Other JSON divergences: // - ',' can precede a '}' or a ']' (as in go); // - strings can contain non-escaped \n, \r and \t; // - raw strings are accepted (r{raw}, r!raw!, …), see below; // - int_lit & float_lit numbers as defined in go spec are accepted; // - numbers can be prefixed by '+'. // // Most operators can be directly embedded in SuperJSONOf without requiring // any placeholder. If an operators does not take any parameter, the // parenthesis can be omitted. // // td.Cmp(t, gotValue, // td.SuperJSONOf(` // { // "fullname": HasPrefix("Foo"), // "age": Between(41, 43), // "details": SuperMapOf({ // "address": NotEmpty, // () are optional when no parameters // "car": Any("Peugeot", "Tesla", "Jeep") // any of these // }) // }`)) // // Placeholders can be used anywhere, even in operators parameters as in: // // td.Cmp(t, gotValue, td.SuperJSONOf(`{"fullname": HasPrefix($1)}`, "Zip")) // // A few notes about operators embedding: // - [SubMapOf] and [SuperMapOf] take only one parameter, a JSON object; // - the optional 3rd parameter of [Between] has to be specified as a string // and can be: "[]" or "BoundsInIn" (default), "[[" or "BoundsInOut", // "]]" or "BoundsOutIn", "][" or "BoundsOutOut"; // - not all operators are embeddable only the following are: [All], // [Any], [ArrayEach], [Bag], [Between], [Contains], // [ContainsKey], [Empty], [First], [Grep], [Gt], [Gte], // [HasPrefix], [HasSuffix], [Ignore], [JSONPointer], [Keys], // [Last], [Len], [Lt], [Lte], [MapEach], [N], [NaN], [Nil], // [None], [Not], [NotAny], [NotEmpty], [NotNaN], [NotNil], // [NotZero], [Re], [ReAll], [Set], [SubBagOf], [SubMapOf], // [SubSetOf], [SuperBagOf], [SuperMapOf], [SuperSetOf], [Values] // and [Zero]. // // It is also possible to embed operators in JSON strings. This way, // the JSON specification can be fulfilled. To avoid collision with // possible strings, just prefix the first operator name with // "$^". The previous example becomes: // // td.Cmp(t, gotValue, // td.SuperJSONOf(` // { // "fullname": "$^HasPrefix(\"Foo\")", // "age": "$^Between(41, 43)", // "details": "$^SuperMapOf({ // \"address\": NotEmpty, // () are optional when no parameters // \"car\": Any(\"Peugeot\", \"Tesla\", \"Jeep\") // any of these // })" // }`)) // // As you can see, in this case, strings in strings have to be // escaped. Fortunately, newlines are accepted, but unfortunately they // are forbidden by JSON specification. To avoid too much escaping, // raw strings are accepted. A raw string is a "r" followed by a // delimiter, the corresponding delimiter closes the string. The // following raw strings are all the same as "foo\\bar(\"zip\")!": // - r'foo\bar"zip"!' // - r,foo\bar"zip"!, // - r%foo\bar"zip"!% // - r(foo\bar("zip")!) // - r{foo\bar("zip")!} // - r[foo\bar("zip")!] // - r // // So non-bracketing delimiters use the same character before and // after, but the 4 sorts of ASCII brackets (round, angle, square, // curly) all nest: r[x[y]z] equals "x[y]z". The end delimiter cannot // be escaped. // // With raw strings, the previous example becomes: // // td.Cmp(t, gotValue, // td.SuperJSONOf(` // { // "fullname": "$^HasPrefix(r)", // "age": "$^Between(41, 43)", // "details": "$^SuperMapOf({ // r
: NotEmpty, // () are optional when no parameters // r: Any(r, r, r) // any of these // })" // }`)) // // Note that raw strings are accepted anywhere, not only in original // JSON strings. // // To be complete, $^ can prefix an operator even outside a // string. This is accepted for compatibility purpose as the first // operator embedding feature used this way to embed some operators. // // So the following calls are all equivalent: // // td.Cmp(t, gotValue, td.SuperJSONOf(`{"id": $1}`, td.NotZero())) // td.Cmp(t, gotValue, td.SuperJSONOf(`{"id": NotZero}`)) // td.Cmp(t, gotValue, td.SuperJSONOf(`{"id": NotZero()}`)) // td.Cmp(t, gotValue, td.SuperJSONOf(`{"id": $^NotZero}`)) // td.Cmp(t, gotValue, td.SuperJSONOf(`{"id": $^NotZero()}`)) // td.Cmp(t, gotValue, td.SuperJSONOf(`{"id": "$^NotZero"}`)) // td.Cmp(t, gotValue, td.SuperJSONOf(`{"id": "$^NotZero()"}`)) // // As for placeholders, there is no differences between $^NotZero and // "$^NotZero". // // Tip: when an [io.Reader] is expected to contain JSON data, it // cannot be tested directly, but using the [Smuggle] operator simply // solves the problem: // // var body io.Reader // // … // td.Cmp(t, body, td.Smuggle(json.RawMessage{}, td.SuperJSONOf(`{"foo":1}`))) // // or equally // td.Cmp(t, body, td.Smuggle(json.RawMessage(nil), td.SuperJSONOf(`{"foo":1}`))) // // [Smuggle] reads from body into an [encoding/json.RawMessage] then // this buffer is unmarshaled by SuperJSONOf operator before the comparison. // // TypeBehind method returns the map[string]any type. // // See also [JSON], [JSONPointer] and [SubJSONOf]. func SuperJSONOf(expectedJSON any, params ...any) TestDeep { m := &tdMapJSON{ tdMap: tdMap{ tdExpectedType: tdExpectedType{ base: newBase(3), expectedType: reflect.TypeOf((map[string]any)(nil)), }, kind: superMap, }, } v, err := newJSONUnmarshaler(m.GetLocation()).unmarshal(expectedJSON, params) if err != nil { m.err = err return m } _, ok := v.(map[string]any) if !ok { m.err = ctxerr.OpBad("SuperJSONOf", "SuperJSONOf() only accepts JSON objects {…}") return m } m.expected = reflect.ValueOf(v) m.populateExpectedEntries(nil, m.expected) return m } func (m *tdMapJSON) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error { if m.err != nil { return ctx.CollectError(m.err) } err := gotViaJSON(ctx, &got) if err != nil { return ctx.CollectError(err) } // nil case if !got.IsValid() { if ctx.BooleanError { return ctxerr.BooleanError } return ctx.CollectError(&ctxerr.Error{ Message: "values differ", Got: types.RawString("null"), Expected: types.RawString("non-null"), }) } ctx.BeLax = true return m.match(ctx, got) } func (m *tdMapJSON) String() string { if m.err != nil { return m.stringError() } return jsonStringify(m.GetLocation().Func, m.expected) } func (m *tdMapJSON) HandleInvalid() bool { return true } golang-github-maxatome-go-testdeep-1.14.0/td/td_json_pointer.go000066400000000000000000000115561454313311600245220ustar00rootroot00000000000000// Copyright (c) 2020-2023, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td import ( "fmt" "reflect" "strings" "github.com/maxatome/go-testdeep/internal/ctxerr" "github.com/maxatome/go-testdeep/internal/util" ) type tdJSONPointer struct { tdSmugglerBase pointer string } var _ TestDeep = &tdJSONPointer{} // summary(JSONPointer): compares against JSON representation using a // JSON pointer // input(JSONPointer): nil,bool,str,int,float,array,slice,map,struct,ptr // JSONPointer is a smuggler operator. It takes the JSON // representation of data, gets the value corresponding to the JSON // pointer ptr (as [RFC 6901] specifies it) and compares it to // expectedValue. // // [Lax] mode is automatically enabled to simplify numeric tests. // // JSONPointer does its best to convert back the JSON pointed data to // the type of expectedValue or to the type behind the // expectedValue operator, if it is an operator. Allowing to do // things like: // // type Item struct { // Val int `json:"val"` // Next *Item `json:"next"` // } // got := Item{Val: 1, Next: &Item{Val: 2, Next: &Item{Val: 3}}} // // td.Cmp(t, got, td.JSONPointer("/next/next", Item{Val: 3})) // td.Cmp(t, got, td.JSONPointer("/next/next", &Item{Val: 3})) // td.Cmp(t, // got, // td.JSONPointer("/next/next", // td.Struct(Item{}, td.StructFields{"Val": td.Gte(3)})), // ) // // got := map[string]int64{"zzz": 42} // 42 is int64 here // td.Cmp(t, got, td.JSONPointer("/zzz", 42)) // td.Cmp(t, got, td.JSONPointer("/zzz", td.Between(40, 45))) // // Of course, it does this conversion only if the expected type can be // guessed. In the case the conversion cannot occur, data is compared // as is, in its freshly unmarshaled JSON form (so as bool, float64, // string, []any, map[string]any or simply nil). // // Note that as any [TestDeep] operator can be used as expectedValue, // [JSON] operator works out of the box: // // got := json.RawMessage(`{"foo":{"bar": {"zip": true}}}`) // td.Cmp(t, got, td.JSONPointer("/foo/bar", td.JSON(`{"zip": true}`))) // // It can be used with structs lacking json tags. In this case, fields // names have to be used in JSON pointer: // // type Item struct { // Val int // Next *Item // } // got := Item{Val: 1, Next: &Item{Val: 2, Next: &Item{Val: 3}}} // // td.Cmp(t, got, td.JSONPointer("/Next/Next", Item{Val: 3})) // // Contrary to [Smuggle] operator and its fields-path feature, only // public fields can be followed, as private ones are never (un)marshaled. // // There is no JSONHas nor JSONHasnt operators to only check a JSON // pointer exists or not, but they can easily be emulated: // // JSONHas := func(pointer string) td.TestDeep { // return td.JSONPointer(pointer, td.Ignore()) // } // // JSONHasnt := func(pointer string) td.TestDeep { // return td.Not(td.JSONPointer(pointer, td.Ignore())) // } // // TypeBehind method always returns nil as the expected type cannot be // guessed from a JSON pointer. // // See also [JSON], [SubJSONOf], [SuperJSONOf], [Smuggle] and [Flatten]. // // [RFC 6901]: https://tools.ietf.org/html/rfc6901 func JSONPointer(ptr string, expectedValue any) TestDeep { p := tdJSONPointer{ tdSmugglerBase: newSmugglerBase(expectedValue), pointer: ptr, } if !strings.HasPrefix(ptr, "/") && ptr != "" { p.err = ctxerr.OpBad("JSONPointer", "bad JSON pointer %q", ptr) return &p } if !p.isTestDeeper { p.expectedValue = reflect.ValueOf(expectedValue) } return &p } func (p *tdJSONPointer) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error { if p.err != nil { return ctx.CollectError(p.err) } vgot, eErr := jsonify(ctx, got) if eErr != nil { return ctx.CollectError(eErr) } vgot, err := util.JSONPointer(vgot, p.pointer) if err != nil { if ctx.BooleanError { return ctxerr.BooleanError } pErr := err.(*util.JSONPointerError) ctx = jsonPointerContext(ctx, pErr.Pointer) return ctx.CollectError(&ctxerr.Error{ Message: "cannot retrieve value via JSON pointer", Summary: ctxerr.NewSummary(pErr.Type), }) } // Here, vgot type is either a bool, float64, string, // []any, a map[string]any or simply nil ctx = jsonPointerContext(ctx, p.pointer) ctx.BeLax = true return p.jsonValueEqual(ctx, vgot) } func (p *tdJSONPointer) String() string { if p.err != nil { return p.stringError() } var expected string switch { case p.isTestDeeper: expected = p.expectedValue.Interface().(TestDeep).String() case p.expectedValue.IsValid(): expected = util.ToString(p.expectedValue.Interface()) default: expected = "nil" } return fmt.Sprintf("JSONPointer(%s, %s)", p.pointer, expected) } func (p *tdJSONPointer) HandleInvalid() bool { return true } func jsonPointerContext(ctx ctxerr.Context, pointer string) ctxerr.Context { return ctx.AddCustomLevel(".JSONPointer<" + pointer + ">") } golang-github-maxatome-go-testdeep-1.14.0/td/td_json_pointer_test.go000066400000000000000000000201701454313311600255510ustar00rootroot00000000000000// Copyright (c) 2020-2022, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td_test import ( "encoding/json" "errors" "testing" "github.com/maxatome/go-testdeep/internal/test" "github.com/maxatome/go-testdeep/td" ) type jsonPtrTest int func (j jsonPtrTest) UnmarshalJSON(b []byte) error { return errors.New("jsonPtrTest unmarshal custom error") } type jsonPtrMap map[string]any func (j *jsonPtrMap) UnmarshalJSON(b []byte) error { return json.Unmarshal(b, (*map[string]any)(j)) } var _ = []json.Unmarshaler{jsonPtrTest(0), &jsonPtrMap{}} func TestJSONPointer(t *testing.T) { // // nil t.Run("nil", func(t *testing.T) { checkOK(t, nil, td.JSONPointer("", nil)) checkOK(t, (*int)(nil), td.JSONPointer("", nil)) // Yes encoding/json succeeds to unmarshal nil into an int checkOK(t, nil, td.JSONPointer("", 0)) checkOK(t, (*int)(nil), td.JSONPointer("", 0)) checkError(t, map[string]int{"foo": 42}, td.JSONPointer("/foo", nil), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA.JSONPointer"), Got: mustBe(`42.0`), Expected: mustBe(`nil`), }) // As encoding/json succeeds to unmarshal nil into an int checkError(t, map[string]any{"foo": nil}, td.JSONPointer("/foo", 1), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA.JSONPointer"), Got: mustBe(`0`), // as an int is expected, nil becomes 0 Expected: mustBe(`1`), }) }) // // Basic types t.Run("basic types", func(t *testing.T) { checkOK(t, 123, td.JSONPointer("", 123)) checkOK(t, 123, td.JSONPointer("", td.Between(120, 130))) checkOK(t, true, td.JSONPointer("", true)) }) // // More complex type with encoding/json tags t.Run("complex type with json tags", func(t *testing.T) { type jpStruct struct { Slice []string `json:"slice,omitempty"` Map map[string]*jpStruct `json:"map,omitempty"` Num int `json:"num"` Bool bool `json:"bool"` Str string `json:"str,omitempty"` } got := jpStruct{ Slice: []string{"bar", "baz"}, Map: map[string]*jpStruct{ "test": { Num: 2, Str: "level2", }, }, Num: 1, Bool: true, Str: "level1", } // No filter, should match got or its map representation checkOK(t, got, td.JSONPointer("", map[string]any{ "slice": []any{"bar", "baz"}, "map": map[string]any{ "test": map[string]any{ "num": 2, "str": "level2", "bool": false, }, }, "num": int64(1), // should be OK as Lax is enabled "bool": true, "str": "level1", })) checkOK(t, got, td.JSONPointer("", got)) checkOK(t, got, td.JSONPointer("", &got)) // A specific field checkOK(t, got, td.JSONPointer("/num", int64(1))) // Lax enabled checkOK(t, got, td.JSONPointer("/slice/1", "baz")) checkOK(t, got, td.JSONPointer("/map/test/num", 2)) checkOK(t, got, td.JSONPointer("/map/test/str", td.Contains("vel2"))) checkOK(t, got, td.JSONPointer("/map", td.JSONPointer("/test", td.JSONPointer("/num", 2)))) checkError(t, got, td.JSONPointer("/zzz/pipo", 666), expectedError{ Message: mustBe("cannot retrieve value via JSON pointer"), Path: mustBe("DATA.JSONPointer"), Summary: mustBe("key not found"), }) checkError(t, got, td.JSONPointer("/num/pipo", 666), expectedError{ Message: mustBe("cannot retrieve value via JSON pointer"), Path: mustBe("DATA.JSONPointer"), Summary: mustBe("not a map nor an array"), }) checkError(t, got, td.JSONPointer("/slice/2", "zip"), expectedError{ Message: mustBe("cannot retrieve value via JSON pointer"), Path: mustBe("DATA.JSONPointer"), Summary: mustBe("out of array range"), }) checkError(t, got, td.JSONPointer("/slice/xxx", "zip"), expectedError{ Message: mustBe("cannot retrieve value via JSON pointer"), Path: mustBe("DATA.JSONPointer"), Summary: mustBe("array but not an index in JSON pointer"), }) checkError(t, got, td.JSONPointer("/slice/1", "zip"), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA.JSONPointer"), Got: mustBe(`"baz"`), Expected: mustBe(`"zip"`), }) // A struct behind a specific field checkOK(t, got, td.JSONPointer("/map/test", map[string]any{ "num": 2, "str": "level2", "bool": false, })) checkOK(t, got, td.JSONPointer("/map/test", jpStruct{ Num: 2, Str: "level2", })) checkOK(t, got, td.JSONPointer("/map/test", &jpStruct{ Num: 2, Str: "level2", })) checkOK(t, got, td.JSONPointer("/map/test", td.Struct(&jpStruct{ Num: 2, Str: "level2", }, nil))) }) // // Complex type without encoding/json tags t.Run("complex type without json tags", func(t *testing.T) { type jpStruct struct { Slice []string Map map[string]*jpStruct Num int Bool bool Str string } got := jpStruct{ Slice: []string{"bar", "baz"}, Map: map[string]*jpStruct{ "test": { Num: 2, Str: "level2", }, }, Num: 1, Bool: true, Str: "level1", } checkOK(t, got, td.JSONPointer("/Num", 1)) checkOK(t, got, td.JSONPointer("/Slice/1", "baz")) checkOK(t, got, td.JSONPointer("/Map/test/Num", 2)) checkOK(t, got, td.JSONPointer("/Map/test/Str", td.Contains("vel2"))) }) // // Chained list t.Run("Chained list", func(t *testing.T) { type Item struct { Val int `json:"val"` Next *Item `json:"next"` } got := Item{Val: 1, Next: &Item{Val: 2, Next: &Item{Val: 3}}} checkOK(t, got, td.JSONPointer("/next/next", Item{Val: 3})) checkOK(t, got, td.JSONPointer("/next/next", &Item{Val: 3})) checkOK(t, got, td.JSONPointer("/next/next", td.Struct(Item{}, td.StructFields{"Val": td.Gte(3)}))) checkOK(t, json.RawMessage(`{"foo":{"bar": {"zip": true}}}`), td.JSONPointer("/foo/bar", td.JSON(`{"zip": true}`))) }) // // Lax cases t.Run("Lax", func(t *testing.T) { t.Run("json.Unmarshaler", func(t *testing.T) { got := jsonPtrMap{"x": 123} checkOK(t, got, td.JSONPointer("", jsonPtrMap{"x": float64(123)})) checkOK(t, got, td.JSONPointer("", &jsonPtrMap{"x": float64(123)})) checkOK(t, got, td.JSONPointer("", got)) checkOK(t, got, td.JSONPointer("", &got)) }) t.Run("struct", func(t *testing.T) { type jpStruct struct { Num any } got := jpStruct{Num: 123} checkOK(t, got, td.JSONPointer("", jpStruct{Num: float64(123)})) checkOK(t, jpStruct{Num: got}, td.JSONPointer("/Num", jpStruct{Num: float64(123)})) checkOK(t, got, td.JSONPointer("", got)) checkOK(t, got, td.JSONPointer("", &got)) expected := int8(123) checkOK(t, got, td.JSONPointer("/Num", expected)) checkOK(t, got, td.JSONPointer("/Num", &expected)) }) }) // // Errors t.Run("errors", func(t *testing.T) { checkError(t, func() {}, td.JSONPointer("", td.NotNil()), expectedError{ Message: mustBe("json.Marshal failed"), Path: mustBe("DATA"), Summary: mustContain("json: unsupported type"), }) checkError(t, map[string]int{"zzz": 42}, td.JSONPointer("/zzz", jsonPtrTest(56)), expectedError{ Message: mustBe("an error occurred while unmarshalling JSON into td_test.jsonPtrTest"), Path: mustBe("DATA.JSONPointer"), Summary: mustBe("jsonPtrTest unmarshal custom error"), }) }) // // Bad usage checkError(t, "never tested", td.JSONPointer("x", 1234), expectedError{ Message: mustBe("bad usage of JSONPointer operator"), Path: mustBe("DATA"), Summary: mustBe(`bad JSON pointer "x"`), }) // // String test.EqualStr(t, td.JSONPointer("/x", td.Gt(2)).String(), "JSONPointer(/x, > 2)") test.EqualStr(t, td.JSONPointer("/x", 2).String(), "JSONPointer(/x, 2)") test.EqualStr(t, td.JSONPointer("/x", nil).String(), "JSONPointer(/x, nil)") // Erroneous op test.EqualStr(t, td.JSONPointer("x", 1234).String(), "JSONPointer()") } func TestJSONPointerTypeBehind(t *testing.T) { equalTypes(t, td.JSONPointer("", 42), nil) // Erroneous op equalTypes(t, td.JSONPointer("x", 1234), nil) } golang-github-maxatome-go-testdeep-1.14.0/td/td_json_test.go000066400000000000000000001164741454313311600240260ustar00rootroot00000000000000// Copyright (c) 2019-2023, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td_test import ( "encoding/json" "errors" "os" "reflect" "testing" "time" "github.com/maxatome/go-testdeep/internal/test" "github.com/maxatome/go-testdeep/td" ) type errReader struct{} // Read implements io.Reader. func (r errReader) Read(p []byte) (int, error) { return 0, errors.New("an error occurred") } const ( insideOpJSON = " inside operator JSON at td_json_test.go:" underOpJSON = "under operator JSON at td_json_test.go:" ) func TestJSON(t *testing.T) { type MyStruct struct { Name string `json:"name"` Age uint `json:"age"` Gender string `json:"gender"` } // // nil checkOK(t, nil, td.JSON(`null`)) checkOK(t, (*int)(nil), td.JSON(`null`)) // // Basic types checkOK(t, 123, td.JSON(` 123 `)) checkOK(t, true, td.JSON(` true `)) checkOK(t, false, td.JSON(` false `)) checkOK(t, "foobar", td.JSON(` "foobar" `)) // // struct // got := MyStruct{Name: "Bob", Age: 42, Gender: "male"} // No placeholder checkOK(t, got, td.JSON(`{"name":"Bob","age":42,"gender":"male"}`)) checkOK(t, got, td.JSON(`$1`, got)) // json.Marshal() got for $1 // Numeric placeholders checkOK(t, got, td.JSON(`{"name":"$1","age":$2,"gender":$3}`, "Bob", 42, "male")) // raw values checkOK(t, got, td.JSON(`{"name":"$1","age":$2,"gender":"$3"}`, td.Re(`^Bob`), td.Between(40, 45), td.NotEmpty())) // Same using Flatten checkOK(t, got, td.JSON(`{"name":"$1","age":$2,"gender":"$3"}`, td.Re(`^Bob`), td.Flatten([]td.TestDeep{td.Between(40, 45), td.NotEmpty()}), )) // Operators are not JSON marshallable checkOK(t, got, td.JSON(`$1`, map[string]any{ "name": td.Re(`^Bob`), "age": 42, "gender": td.NotEmpty(), })) // Placeholder + unmarshal before comparison checkOK(t, json.RawMessage(`[1,2,3]`), td.JSON(`$1`, []int{1, 2, 3})) checkOK(t, json.RawMessage(`{"foo":[1,2,3]}`), td.JSON(`{"foo":$1}`, []int{1, 2, 3})) checkOK(t, json.RawMessage(`[1,2,3]`), td.JSON(`$1`, []any{1, td.Between(1, 3), 3})) // Tag placeholders checkOK(t, got, td.JSON(`{"name":"$name","age":$age,"gender":$gender}`, // raw values td.Tag("name", "Bob"), td.Tag("age", 42), td.Tag("gender", "male"))) checkOK(t, got, td.JSON(`{"name":"$name","age":$age,"gender":"$gender"}`, td.Tag("name", td.Re(`^Bo`)), td.Tag("age", td.Between(40, 45)), td.Tag("gender", td.NotEmpty()))) // Tag placeholders + numeric placeholders checkOK(t, []MyStruct{got, got}, td.JSON(`[ {"name":"$1","age":$age,"gender":"$3"}, {"name":"$1","age":$2,"gender":"$3"} ]`, td.Re(`^Bo`), // $1 td.Tag("age", td.Between(40, 45)), // $2 "male")) // $3 // Tag placeholders + operators are not JSON marshallable checkOK(t, got, td.JSON(`$all`, td.Tag("all", map[string]any{ "name": td.Re(`^Bob`), "age": 42, "gender": td.NotEmpty(), }))) checkError(t, got, td.JSON(`{"name":$1, "age":$1, "gender":$1}`, td.Tag("!!", td.Ignore())), expectedError{ Message: mustBe("bad usage of Tag operator"), Summary: mustBe("Invalid tag, should match (Letter|_)(Letter|_|Number)*"), Under: mustContain("under operator Tag"), }) // Tag placeholders + nil checkOK(t, nil, td.JSON(`$all`, td.Tag("all", nil))) // Mixed placeholders + operator for _, op := range []string{ "NotEmpty", "NotEmpty()", "$^NotEmpty", "$^NotEmpty()", `"$^NotEmpty"`, `"$^NotEmpty()"`, `r<$^NotEmpty>`, `r<$^NotEmpty()>`, } { checkOK(t, got, td.JSON(`{"name":"$name","age":$1,"gender":`+op+`}`, td.Tag("age", td.Between(40, 45)), td.Tag("name", td.Re(`^Bob`))), "using operator %s", op) } checkOK(t, got, td.JSON(`{"name":Re("^Bo\\w"),"age":Between(40,45),"gender":NotEmpty()}`)) checkOK(t, got, td.JSON(` { "name": All(Re("^Bo\\w"), HasPrefix("Bo"), HasSuffix("ob")), "age": Between(40,45), "gender": NotEmpty() }`)) checkOK(t, got, td.JSON(` { "name": All(Re("^Bo\\w"), HasPrefix("Bo"), HasSuffix("ob")), "age": Between(40,45), "gender": NotEmpty }`)) // Same but operators in strings using "$^" checkOK(t, got, td.JSON(`{"name":Re("^Bo\\w"),"age":"$^Between(40,45)","gender":"$^NotEmpty()"}`)) checkOK(t, got, // using classic "" string, so each \ has to be escaped td.JSON(` { "name": "$^All(Re(\"^Bo\\\\w\"), HasPrefix(\"Bo\"), HasSuffix(\"ob\"))", "age": "$^Between(40,45)", "gender": "$^NotEmpty()", }`)) checkOK(t, got, // using raw strings, no escape needed td.JSON(` { "name": "$^All(Re(r(^Bo\\w)), HasPrefix(r{Bo}), HasSuffix(r'ob'))", "age": "$^Between(40,45)", "gender": "$^NotEmpty()", }`)) // …with comments… checkOK(t, got, td.JSON(` // This should be the JSON representation of MyStruct struct { // A person: "name": "$name", // The name of this person "age": $1, /* The age of this person: - placeholder unquoted, but could be without any change - to demonstrate a multi-lines comment */ "gender": $^NotEmpty // Operator NotEmpty }`, td.Tag("age", td.Between(40, 45)), td.Tag("name", td.Re(`^Bob`)))) before := time.Now() timeGot := map[string]time.Time{"created_at": time.Now()} checkOK(t, timeGot, td.JSON(`{"created_at": Between($1, $2)}`, before, time.Now())) checkOK(t, timeGot, td.JSON(`{"created_at": $1}`, td.Between(before, time.Now()))) // Len checkOK(t, []int{1, 2, 3}, td.JSON(`Len(3)`)) // // []byte checkOK(t, got, td.JSON([]byte(`{"name":"$name","age":$1,"gender":"male"}`), td.Tag("age", td.Between(40, 45)), td.Tag("name", td.Re(`^Bob`)))) // // json.RawMessage checkOK(t, got, td.JSON(json.RawMessage(`{"name":"$name","age":$1,"gender":"male"}`), td.Tag("age", td.Between(40, 45)), td.Tag("name", td.Re(`^Bob`)))) // // nil++ checkOK(t, nil, td.JSON(`$1`, nil)) checkOK(t, (*int)(nil), td.JSON(`$1`, td.Nil())) checkOK(t, nil, td.JSON(`$x`, td.Tag("x", nil))) checkOK(t, (*int)(nil), td.JSON(`$x`, td.Tag("x", nil))) checkOK(t, json.RawMessage(`{"foo": null}`), td.JSON(`{"foo": null}`)) checkOK(t, json.RawMessage(`{"foo": null}`), td.JSON(`{"foo": $1}`, nil)) checkOK(t, json.RawMessage(`{"foo": null}`), td.JSON(`{"foo": $1}`, td.Nil())) checkOK(t, json.RawMessage(`{"foo": null}`), td.JSON(`{"foo": $x}`, td.Tag("x", nil))) checkOK(t, json.RawMessage(`{"foo": null}`), td.JSON(`{"foo": $x}`, td.Tag("x", td.Nil()))) // // Loading a file tmpDir := t.TempDir() filename := tmpDir + "/test.json" err := os.WriteFile( filename, []byte(`{"name":$name,"age":$1,"gender":$^NotEmpty}`), 0644) if err != nil { t.Fatal(err) } checkOK(t, got, td.JSON(filename, td.Tag("age", td.Between(40, 45)), td.Tag("name", td.Re(`^Bob`)))) // // Reading (a file) tmpfile, err := os.Open(filename) if err != nil { t.Fatal(err) } checkOK(t, got, td.JSON(tmpfile, td.Tag("age", td.Between(40, 45)), td.Tag("name", td.Re(`^Bob`)))) tmpfile.Close() // // Escaping $ in strings checkOK(t, "$test", td.JSON(`"$$test"`)) // // Errors checkError(t, func() {}, td.JSON(`null`), expectedError{ Message: mustBe("json.Marshal failed"), Summary: mustContain("json: unsupported type"), Under: mustContain(underOpJSON), }) checkError(t, map[string]string{"zip": "pipo"}, td.All(td.JSON(`SuperMapOf({"zip":$1})`, "bingo")), expectedError{ Path: mustBe(`DATA`), Message: mustBe("compared (part 1 of 1)"), Got: mustBe(`(map[string]string) (len=1) { (string) (len=3) "zip": (string) (len=4) "pipo" }`), Expected: mustBe(`JSON(SuperMapOf(map[string]interface {}{ "zip": "bingo", }))`, ), Under: mustContain("under operator All at "), Origin: &expectedError{ Path: mustBe(`DATA["zip"]`), Message: mustBe(`values differ`), Got: mustBe(`"pipo"`), Expected: mustBe(`"bingo"`), Under: mustContain("under operator SuperMapOf at line 1:0 (pos 0)" + insideOpJSON), }, }) checkError(t, map[string]string{"zip": "pipo"}, td.JSON(`SuperMapOf({"zip":$1})`, "bingo"), expectedError{ Path: mustBe(`DATA["zip"]`), Message: mustBe("values differ"), Got: mustBe(`"pipo"`), Expected: mustBe(`"bingo"`), Under: mustContain("under operator SuperMapOf at line 1:0 (pos 0)" + insideOpJSON), }) checkError(t, json.RawMessage(`"pipo:bingo"`), td.JSON(`Re(r;^pipo:(\w+);, ["bad"])`), expectedError{ Path: mustBe(`(DATA =~ ^pipo:(\w+))[0]`), Got: mustBe(`"bingo"`), Expected: mustBe(`"bad"`), Under: mustContain("under operator Re at line 1:0 (pos 0)" + insideOpJSON), }) checkError(t, json.RawMessage(`"pipo:bingo"`), td.JSON(`Re(r;^pipo:(\w+);, [$1])`, "bad"), expectedError{ Path: mustBe(`(DATA =~ ^pipo:(\w+))[0]`), Got: mustBe(`"bingo"`), Expected: mustBe(`"bad"`), Under: mustContain("under operator Re at line 1:0 (pos 0)" + insideOpJSON), }) checkError(t, json.RawMessage(`"pipo:bingo"`), td.JSON(`Re(r;^pipo:(\w+);, [$param])`, td.Tag("param", "bad")), expectedError{ Path: mustBe(`(DATA =~ ^pipo:(\w+))[0]`), Got: mustBe(`"bingo"`), Expected: mustBe(`"bad"`), Under: mustContain("under operator Re at line 1:0 (pos 0)" + insideOpJSON), }) checkError(t, json.RawMessage(`"pipo:bingo"`), td.JSON(`Re(r;^pipo:(\w+);, Bag($1))`, "bad"), expectedError{ Path: mustBe(`(DATA =~ ^pipo:(\w+))`), Summary: mustBe(`Missing item: ("bad")` + "\n" + ` Extra item: ("bingo")`), Under: mustContain("under operator Bag at line 1:19 (pos 19)" + insideOpJSON), }) checkError(t, json.RawMessage(`"pipo:bingo"`), td.JSON(`Re(r;^pipo:(\w+);, Bag($param))`, td.Tag("param", "bad")), expectedError{ Path: mustBe(`(DATA =~ ^pipo:(\w+))`), Summary: mustBe(`Missing item: ("bad")` + "\n" + ` Extra item: ("bingo")`), Under: mustContain("under operator Bag at line 1:19 (pos 19)" + insideOpJSON), }) // // Fatal errors checkError(t, "never tested", td.JSON("uNkNoWnFiLe.json"), expectedError{ Message: mustBe("bad usage of JSON operator"), Path: mustBe("DATA"), Summary: mustContain("JSON file uNkNoWnFiLe.json cannot be read: "), Under: mustContain(underOpJSON), }) checkError(t, "never tested", td.JSON(42), expectedError{ Message: mustBe("bad usage of JSON operator"), Path: mustBe("DATA"), Summary: mustBe("usage: JSON(STRING_JSON|STRING_FILENAME|[]byte|json.RawMessage|io.Reader, ...), but received int as 1st parameter"), Under: mustContain(underOpJSON), }) checkError(t, "never tested", td.JSON(errReader{}), expectedError{ Message: mustBe("bad usage of JSON operator"), Path: mustBe("DATA"), Summary: mustBe("JSON read error: an error occurred"), Under: mustContain(underOpJSON), }) checkError(t, "never tested", td.JSON(`pipo`), expectedError{ Message: mustBe("bad usage of JSON operator"), Path: mustBe("DATA"), Summary: mustContain("JSON unmarshal error: "), Under: mustContain(underOpJSON), }) checkError(t, "never tested", td.JSON(`[$foo]`, td.Tag("foo", td.Ignore()), td.Tag("foo", td.Ignore())), expectedError{ Message: mustBe("bad usage of JSON operator"), Path: mustBe("DATA"), Summary: mustBe(`2 params have the same tag "foo"`), Under: mustContain(underOpJSON), }) checkError(t, []int{42}, td.JSON(`[$1]`, func() {}), expectedError{ Message: mustBe("an error occurred while unmarshalling JSON into func()"), Path: mustBe("DATA[0]"), Summary: mustBe("json: cannot unmarshal number into Go value of type func()"), Under: mustContain(underOpJSON), }) checkError(t, []int{42}, td.JSON(`[$foo]`, td.Tag("foo", func() {})), expectedError{ Message: mustBe("an error occurred while unmarshalling JSON into func()"), Path: mustBe("DATA[0]"), Summary: mustBe("json: cannot unmarshal number into Go value of type func()"), Under: mustContain(underOpJSON), }) // numeric placeholders checkError(t, "never tested", td.JSON(`[1, "$123bad"]`), expectedError{ Message: mustBe("bad usage of JSON operator"), Path: mustBe("DATA"), Summary: mustBe(`JSON unmarshal error: invalid numeric placeholder at line 1:5 (pos 5)`), Under: mustContain(underOpJSON), }) checkError(t, "never tested", td.JSON(`[1, $000]`), expectedError{ Message: mustBe("bad usage of JSON operator"), Path: mustBe("DATA"), Summary: mustBe(`JSON unmarshal error: invalid numeric placeholder "$000", it should start at "$1" at line 1:4 (pos 4)`), Under: mustContain(underOpJSON), }) checkError(t, "never tested", td.JSON(`[1, $1]`), expectedError{ Message: mustBe("bad usage of JSON operator"), Path: mustBe("DATA"), Summary: mustBe(`JSON unmarshal error: numeric placeholder "$1", but no params given at line 1:4 (pos 4)`), Under: mustContain(underOpJSON), }) checkError(t, "never tested", td.JSON(`[1, 2, $3]`, td.Ignore()), expectedError{ Message: mustBe("bad usage of JSON operator"), Path: mustBe("DATA"), Summary: mustBe(`JSON unmarshal error: numeric placeholder "$3", but only one param given at line 1:7 (pos 7)`), Under: mustContain(underOpJSON), }) // $^Operator checkError(t, "never tested", td.JSON(`[1, $^bad%]`), expectedError{ Message: mustBe("bad usage of JSON operator"), Path: mustBe("DATA"), Summary: mustBe(`JSON unmarshal error: $^ must be followed by an operator name at line 1:4 (pos 4)`), Under: mustContain(underOpJSON), }) checkError(t, "never tested", td.JSON(`[1, "$^bad%"]`), expectedError{ Message: mustBe("bad usage of JSON operator"), Path: mustBe("DATA"), Summary: mustBe(`JSON unmarshal error: $^ must be followed by an operator name at line 1:5 (pos 5)`), Under: mustContain(underOpJSON), }) // named placeholders checkError(t, "never tested", td.JSON(`[ 1, "$bad%" ]`), expectedError{ Message: mustBe("bad usage of JSON operator"), Path: mustBe("DATA"), Summary: mustBe(`JSON unmarshal error: bad placeholder "$bad%" at line 3:3 (pos 10)`), Under: mustContain(underOpJSON), }) checkError(t, "never tested", td.JSON(`[1, $unknown]`), expectedError{ Message: mustBe("bad usage of JSON operator"), Path: mustBe("DATA"), Summary: mustBe(`JSON unmarshal error: unknown placeholder "$unknown" at line 1:4 (pos 4)`), Under: mustContain(underOpJSON), }) // // Stringification test.EqualStr(t, td.JSON(`1`).String(), `JSON(1)`) test.EqualStr(t, td.JSON(`[ 1, 2, 3 ]`).String(), ` JSON([ 1, 2, 3 ])`[1:]) test.EqualStr(t, td.JSON(` null `).String(), `JSON(null)`) test.EqualStr(t, td.JSON(`[ $1, $name, $2, Nil(), $nil, 26, Between(5, 6), Len(34), Len(Between(5, 6)), 28 ]`, td.Between(12, 20), "test", td.Tag("name", td.Code(func(s string) bool { return len(s) > 0 })), td.Tag("nil", nil), 14, ).String(), ` JSON([ "$1" /* 12 ≤ got ≤ 20 */, "$name" /* Code(func(string) bool) */, "test", nil, null, 26, 5.0 ≤ got ≤ 6.0, len=34, len: 5.0 ≤ got ≤ 6.0, 28 ])`[1:]) test.EqualStr(t, td.JSON(`[ $1, $name, $2, $^Nil, $nil ]`, td.Between(12, 20), "test", td.Tag("name", td.Code(func(s string) bool { return len(s) > 0 })), td.Tag("nil", nil), ).String(), ` JSON([ "$1" /* 12 ≤ got ≤ 20 */, "$name" /* Code(func(string) bool) */, "test", nil, null ])`[1:]) test.EqualStr(t, td.JSON(`{"label": $value, "zip": $^NotZero}`, td.Tag("value", td.Bag( td.JSON(`{"name": $1,"age":$2,"surname":$3}`, td.HasPrefix("Bob"), td.Between(12, 24), "captain", ), td.JSON(`{"name": $1}`, td.HasPrefix("Alice")), )), ).String(), ` JSON({ "label": "$value" /* Bag(JSON({ "age": "$2" /* 12 ≤ got ≤ 24 */, "name": "$1" /* HasPrefix("Bob") */, "surname": "captain" }), JSON({ "name": "$1" /* HasPrefix("Alice") */ })) */, "zip": NotZero() })`[1:]) test.EqualStr(t, td.JSON(` { "label": {"name": HasPrefix("Bob"), "age": Between(12,24)}, "zip": NotZero() }`).String(), ` JSON({ "label": { "age": 12.0 ≤ got ≤ 24.0, "name": HasPrefix("Bob") }, "zip": NotZero() })`[1:]) // Erroneous op test.EqualStr(t, td.JSON(`[`).String(), "JSON()") } func TestJSONInside(t *testing.T) { // Between t.Run("Between", func(t *testing.T) { got := map[string]int{"val1": 1, "val2": 2} checkOK(t, got, td.JSON(`{"val1": Between(0, 2), "val2": Between(2, 3, "[[")}`)) checkOK(t, got, td.JSON(`{"val1": Between(0, 2), "val2": Between(2, 3, "BoundsInOut")}`)) for _, bounds := range []string{"[[", "BoundsInOut"} { checkError(t, got, td.JSON(`{"val1": Between(0, 2), "val2": Between(1, 2, $1)}`, bounds), expectedError{ Message: mustBe("values differ"), Path: mustBe(`DATA["val2"]`), Got: mustBe("2.0"), Expected: mustBe("1.0 ≤ got < 2.0"), Under: mustContain("under operator Between at line 1:32 (pos 32)" + insideOpJSON), }) } checkError(t, json.RawMessage(`123`), td.JSON(`Between(0, 2)`), expectedError{ Message: mustBe("values differ"), Path: mustBe(`DATA`), Got: mustBe("123.0"), Expected: mustBe("0.0 ≤ got ≤ 2.0"), Under: mustContain("under operator Between at line 1:0 (pos 0)" + insideOpJSON), }) checkOK(t, got, td.JSON(`{"val1": Between(1, 1), "val2": Between(2, 2, "[]")}`)) checkOK(t, got, td.JSON(`{"val1": Between(1, 1), "val2": Between(2, 2, "BoundsInIn")}`)) checkOK(t, got, td.JSON(`{"val1": Between(0, 1, "]]"), "val2": Between(1, 3, "][")}`)) checkOK(t, got, td.JSON(`{"val1": Between(0, 1, "BoundsOutIn"), "val2": Between(1, 3, "BoundsOutOut")}`)) for _, bounds := range []string{"]]", "BoundsOutIn"} { checkError(t, got, td.JSON(`{"val1": 1, "val2": Between(2, 3, $1)}`, bounds), expectedError{ Message: mustBe("values differ"), Path: mustBe(`DATA["val2"]`), Got: mustBe("2.0"), Expected: mustBe("2.0 < got ≤ 3.0"), Under: mustContain("under operator Between at line 1:20 (pos 20)" + insideOpJSON), }) } for _, bounds := range []string{"][", "BoundsOutOut"} { checkError(t, got, td.JSON(`{"val1": 1, "val2": Between(2, 3, $1)}`, bounds), expectedError{ Message: mustBe("values differ"), Path: mustBe(`DATA["val2"]`), Got: mustBe("2.0"), Expected: mustBe("2.0 < got < 3.0"), Under: mustContain("under operator Between at line 1:20 (pos 20)" + insideOpJSON), }, "using bounds %q", bounds) checkError(t, got, td.JSON(`{"val1": 1, "val2": Between(1, 2, $1)}`, bounds), expectedError{ Message: mustBe("values differ"), Path: mustBe(`DATA["val2"]`), Got: mustBe("2.0"), Expected: mustBe("1.0 < got < 2.0"), Under: mustContain("under operator Between at line 1:20 (pos 20)" + insideOpJSON), }, "using bounds %q", bounds) } // Bad 3rd parameter checkError(t, "never tested", td.JSON(`{ "val2": Between(1, 2, "<>") }`), expectedError{ Message: mustBe("bad usage of JSON operator"), Path: mustBe("DATA"), Summary: mustBe(`JSON unmarshal error: Between() bad 3rd parameter, use "[]", "[[", "]]" or "][" at line 2:10 (pos 12)`), Under: mustContain(underOpJSON), }) checkError(t, "never tested", td.JSON(`{ "val2": Between(1, 2, 125) }`), expectedError{ Message: mustBe("bad usage of JSON operator"), Path: mustBe("DATA"), Summary: mustBe(`JSON unmarshal error: Between() bad 3rd parameter, use "[]", "[[", "]]" or "][" at line 2:10 (pos 12)`), Under: mustContain(underOpJSON), }) checkError(t, "never tested", td.JSON(`{"val2": Between(1)}`), expectedError{ Message: mustBe("bad usage of JSON operator"), Path: mustBe("DATA"), Summary: mustBe(`JSON unmarshal error: Between() requires 2 or 3 parameters at line 1:9 (pos 9)`), Under: mustContain(underOpJSON), }) checkError(t, "never tested", td.JSON(`{"val2": Between(1,2,3,4)}`), expectedError{ Message: mustBe("bad usage of JSON operator"), Path: mustBe("DATA"), Summary: mustBe(`JSON unmarshal error: Between() requires 2 or 3 parameters at line 1:9 (pos 9)`), Under: mustContain(underOpJSON), }) }) // N t.Run("N", func(t *testing.T) { got := map[string]float32{"val": 2.1} checkOK(t, got, td.JSON(`{"val": N(2.1)}`)) checkOK(t, got, td.JSON(`{"val": N(2, 0.1)}`)) checkError(t, "never tested", td.JSON(`{"val2": N()}`), expectedError{ Message: mustBe("bad usage of JSON operator"), Path: mustBe("DATA"), Summary: mustBe(`JSON unmarshal error: N() requires 1 or 2 parameters at line 1:9 (pos 9)`), Under: mustContain(underOpJSON), }) checkError(t, "never tested", td.JSON(`{"val2": N(1,2,3)}`), expectedError{ Message: mustBe("bad usage of JSON operator"), Path: mustBe("DATA"), Summary: mustBe(`JSON unmarshal error: N() requires 1 or 2 parameters at line 1:9 (pos 9)`), Under: mustContain(underOpJSON), }) }) // Re t.Run("Re", func(t *testing.T) { got := map[string]string{"val": "Foo bar"} checkOK(t, got, td.JSON(`{"val": Re("^Foo")}`)) checkOK(t, got, td.JSON(`{"val": Re("^(\\w+)", ["Foo"])}`)) checkOK(t, got, td.JSON(`{"val": Re("^(\\w+)", Bag("Foo"))}`)) checkError(t, "never tested", td.JSON(`{"val2": Re()}`), expectedError{ Message: mustBe("bad usage of JSON operator"), Path: mustBe("DATA"), Summary: mustBe(`JSON unmarshal error: Re() requires 1 or 2 parameters at line 1:9 (pos 9)`), Under: mustContain(underOpJSON), }) checkError(t, "never tested", td.JSON(`{"val2": Re(1,2,3)}`), expectedError{ Message: mustBe("bad usage of JSON operator"), Path: mustBe("DATA"), Summary: mustBe(`JSON unmarshal error: Re() requires 1 or 2 parameters at line 1:9 (pos 9)`), Under: mustContain(underOpJSON), }) }) // SubMapOf t.Run("SubMapOf", func(t *testing.T) { got := []map[string]int{{"val1": 1, "val2": 2}} checkOK(t, got, td.JSON(`[ SubMapOf({"val1":1, "val2":2, "xxx": "yyy"}) ]`)) checkError(t, "never tested", td.JSON(`[ SubMapOf() ]`), expectedError{ Message: mustBe("bad usage of JSON operator"), Path: mustBe("DATA"), Summary: mustBe(`JSON unmarshal error: SubMapOf() requires only one parameter at line 1:2 (pos 2)`), Under: mustContain(underOpJSON), }) checkError(t, "never tested", td.JSON(`[ SubMapOf(1, 2) ]`), expectedError{ Message: mustBe("bad usage of JSON operator"), Path: mustBe("DATA"), Summary: mustBe(`JSON unmarshal error: SubMapOf() requires only one parameter at line 1:2 (pos 2)`), Under: mustContain(underOpJSON), }) }) // SuperMapOf t.Run("SuperMapOf", func(t *testing.T) { got := []map[string]int{{"val1": 1, "val2": 2}} checkOK(t, got, td.JSON(`[ SuperMapOf({"val1":1}) ]`)) checkError(t, "never tested", td.JSON(`[ SuperMapOf() ]`), expectedError{ Message: mustBe("bad usage of JSON operator"), Path: mustBe("DATA"), Summary: mustBe(`JSON unmarshal error: SuperMapOf() requires only one parameter at line 1:2 (pos 2)`), Under: mustContain(underOpJSON), }) checkError(t, "never tested", td.JSON(`[ SuperMapOf(1, 2) ]`), expectedError{ Message: mustBe("bad usage of JSON operator"), Path: mustBe("DATA"), Summary: mustBe(`JSON unmarshal error: SuperMapOf() requires only one parameter at line 1:2 (pos 2)`), Under: mustContain(underOpJSON), }) }) // errors t.Run("Errors", func(t *testing.T) { checkError(t, "never tested", td.JSON(`[ UnknownOp() ]`), expectedError{ Message: mustBe("bad usage of JSON operator"), Path: mustBe("DATA"), Summary: mustBe(`JSON unmarshal error: unknown operator UnknownOp() at line 1:2 (pos 2)`), Under: mustContain(underOpJSON), }) checkError(t, "never tested", td.JSON(`[ Catch() ]`), expectedError{ Message: mustBe("bad usage of JSON operator"), Path: mustBe("DATA"), Summary: mustBe(`JSON unmarshal error: Catch() is not usable in JSON() at line 1:2 (pos 2)`), Under: mustContain(underOpJSON), }) checkError(t, "never tested", td.JSON(`[ JSON() ]`), expectedError{ Message: mustBe("bad usage of JSON operator"), Path: mustBe("DATA"), Summary: mustBe(`JSON unmarshal error: JSON() is not usable in JSON(), use literal JSON instead at line 1:2 (pos 2)`), Under: mustContain(underOpJSON), }) checkError(t, "never tested", td.JSON(`[ All() ]`), expectedError{ Message: mustBe("bad usage of JSON operator"), Path: mustBe("DATA"), Summary: mustBe(`JSON unmarshal error: All() requires at least one parameter at line 1:2 (pos 2)`), Under: mustContain(underOpJSON), }) checkError(t, "never tested", td.JSON(`[ Empty(12) ]`), expectedError{ Message: mustBe("bad usage of JSON operator"), Path: mustBe("DATA"), Summary: mustBe(`JSON unmarshal error: Empty() requires no parameters at line 1:2 (pos 2)`), Under: mustContain(underOpJSON), }) checkError(t, "never tested", td.JSON(`[ HasPrefix() ]`), expectedError{ Message: mustBe("bad usage of JSON operator"), Path: mustBe("DATA"), Summary: mustBe(`JSON unmarshal error: HasPrefix() requires only one parameter at line 1:2 (pos 2)`), Under: mustContain(underOpJSON), }) checkError(t, "never tested", td.JSON(`[ JSONPointer(1, 2, 3) ]`), expectedError{ Message: mustBe("bad usage of JSON operator"), Path: mustBe("DATA"), Summary: mustBe(`JSON unmarshal error: JSONPointer() requires 2 parameters at line 1:2 (pos 2)`), Under: mustContain(underOpJSON), }) checkError(t, "never tested", td.JSON(`[ JSONPointer(1, 2) ]`), expectedError{ Message: mustBe("bad usage of JSON operator"), Path: mustBe("DATA"), Summary: mustBe(`JSON unmarshal error: JSONPointer() bad #1 parameter type: string required but float64 received at line 1:2 (pos 2)`), Under: mustContain(underOpJSON), }) // This one is not caught by JSON, but by Re itself, as the number // of parameters is correct checkError(t, json.RawMessage(`"never tested"`), td.JSON(`Re(1)`), expectedError{ Message: mustBe("bad usage of Re operator"), Path: mustBe("DATA"), Summary: mustBe(`usage: Re(STRING|*regexp.Regexp[, NON_NIL_CAPTURE]), but received float64 as 1st parameter`), Under: mustContain("under operator Re at line 1:0 (pos 0)" + insideOpJSON), }) }) } func TestJSONTypeBehind(t *testing.T) { equalTypes(t, td.JSON(`false`), true) equalTypes(t, td.JSON(`"foo"`), "") equalTypes(t, td.JSON(`42`), float64(0)) equalTypes(t, td.JSON(`[1,2,3]`), ([]any)(nil)) equalTypes(t, td.JSON(`{"a":12}`), (map[string]any)(nil)) // operator at the root → delegate it TypeBehind() call equalTypes(t, td.JSON(`$1`, td.SuperMapOf(map[string]any{"x": 1}, nil)), (map[string]any)(nil)) equalTypes(t, td.JSON(`SuperMapOf({"x":1})`), (map[string]any)(nil)) equalTypes(t, td.JSON(`$1`, 123), 42) nullType := td.JSON(`null`).TypeBehind() if nullType != reflect.TypeOf((*any)(nil)).Elem() { t.Errorf("Failed test: got %s intead of interface {}", nullType) } // Erroneous op equalTypes(t, td.JSON(`[`), nil) } func TestSubJSONOf(t *testing.T) { type MyStruct struct { Name string `json:"name"` Age uint `json:"age"` Gender string `json:"gender"` } // // struct // got := MyStruct{Name: "Bob", Age: 42, Gender: "male"} // No placeholder checkOK(t, got, td.SubJSONOf(` { "name": "Bob", "age": 42, "gender": "male", "details": { // ← we don't want to test this field "city": "Test City", "zip": 666 } }`)) // Numeric placeholders checkOK(t, got, td.SubJSONOf(`{"name":"$1","age":$2,"gender":$3,"details":{}}`, "Bob", 42, "male")) // raw values checkOK(t, got, td.SubJSONOf(`{"name":"$1","age":$2,"gender":$3,"details":{}}`, td.Re(`^Bob`), td.Between(40, 45), td.NotEmpty())) // Same using Flatten checkOK(t, got, td.SubJSONOf(`{"name":"$1","age":$2,"gender":$3,"details":{}}`, td.Re(`^Bob`), td.Flatten([]td.TestDeep{td.Between(40, 45), td.NotEmpty()}), )) // Tag placeholders checkOK(t, got, td.SubJSONOf( `{"name":"$name","age":$age,"gender":"$gender","details":{}}`, td.Tag("name", td.Re(`^Bob`)), td.Tag("age", td.Between(40, 45)), td.Tag("gender", td.NotEmpty()))) // Mixed placeholders + operator for _, op := range []string{ "NotEmpty", "NotEmpty()", "$^NotEmpty", "$^NotEmpty()", `"$^NotEmpty"`, `"$^NotEmpty()"`, `r<$^NotEmpty>`, `r<$^NotEmpty()>`, } { checkOK(t, got, td.SubJSONOf( `{"name":"$name","age":$1,"gender":`+op+`,"details":{}}`, td.Tag("age", td.Between(40, 45)), td.Tag("name", td.Re(`^Bob`))), "using operator %s", op) } // // Errors checkError(t, func() {}, td.SubJSONOf(`{}`), expectedError{ Message: mustBe("json.Marshal failed"), Summary: mustContain("json: unsupported type"), }) for i, n := range []any{ nil, (map[string]any)(nil), (map[string]bool)(nil), ([]int)(nil), } { checkError(t, n, td.SubJSONOf(`{}`), expectedError{ Message: mustBe("values differ"), Got: mustBe("null"), Expected: mustBe("non-null"), }, "nil test #%d", i) } // // Fatal errors checkError(t, "never tested", td.SubJSONOf(`[1, "$123bad"]`), expectedError{ Message: mustBe("bad usage of SubJSONOf operator"), Path: mustBe("DATA"), Summary: mustBe(`JSON unmarshal error: invalid numeric placeholder at line 1:5 (pos 5)`), }) checkError(t, "never tested", td.SubJSONOf(`[1, $000]`), expectedError{ Message: mustBe("bad usage of SubJSONOf operator"), Path: mustBe("DATA"), Summary: mustBe(`JSON unmarshal error: invalid numeric placeholder "$000", it should start at "$1" at line 1:4 (pos 4)`), }) checkError(t, "never tested", td.SubJSONOf(`[1, $1]`), expectedError{ Message: mustBe("bad usage of SubJSONOf operator"), Path: mustBe("DATA"), Summary: mustBe(`JSON unmarshal error: numeric placeholder "$1", but no params given at line 1:4 (pos 4)`), }) checkError(t, "never tested", td.SubJSONOf(`[1, 2, $3]`, td.Ignore()), expectedError{ Message: mustBe("bad usage of SubJSONOf operator"), Path: mustBe("DATA"), Summary: mustBe(`JSON unmarshal error: numeric placeholder "$3", but only one param given at line 1:7 (pos 7)`), }) // $^Operator checkError(t, "never tested", td.SubJSONOf(`[1, $^bad%]`), expectedError{ Message: mustBe("bad usage of SubJSONOf operator"), Path: mustBe("DATA"), Summary: mustBe(`JSON unmarshal error: $^ must be followed by an operator name at line 1:4 (pos 4)`), }) checkError(t, "never tested", td.SubJSONOf(`[1, "$^bad%"]`), expectedError{ Message: mustBe("bad usage of SubJSONOf operator"), Path: mustBe("DATA"), Summary: mustBe(`JSON unmarshal error: $^ must be followed by an operator name at line 1:5 (pos 5)`), }) // named placeholders checkError(t, "never tested", td.SubJSONOf(`[1, "$bad%"]`), expectedError{ Message: mustBe("bad usage of SubJSONOf operator"), Path: mustBe("DATA"), Summary: mustBe(`JSON unmarshal error: bad placeholder "$bad%" at line 1:5 (pos 5)`), }) checkError(t, "never tested", td.SubJSONOf(`[1, $unknown]`), expectedError{ Message: mustBe("bad usage of SubJSONOf operator"), Path: mustBe("DATA"), Summary: mustBe(`JSON unmarshal error: unknown placeholder "$unknown" at line 1:4 (pos 4)`), }) checkError(t, "never tested", td.SubJSONOf("null"), expectedError{ Message: mustBe("bad usage of SubJSONOf operator"), Path: mustBe("DATA"), Summary: mustBe("SubJSONOf() only accepts JSON objects {…}"), }) // // Stringification test.EqualStr(t, td.SubJSONOf(`{}`).String(), `SubJSONOf({})`) test.EqualStr(t, td.SubJSONOf(`{"foo":1, "bar":2}`).String(), ` SubJSONOf({ "bar": 2, "foo": 1 })`[1:]) test.EqualStr(t, td.SubJSONOf(`{"label": $value, "zip": $^NotZero}`, td.Tag("value", td.Bag( td.SubJSONOf(`{"name": $1,"age":$2}`, td.HasPrefix("Bob"), td.Between(12, 24), ), td.SubJSONOf(`{"name": $1}`, td.HasPrefix("Alice")), )), ).String(), ` SubJSONOf({ "label": "$value" /* Bag(SubJSONOf({ "age": "$2" /* 12 ≤ got ≤ 24 */, "name": "$1" /* HasPrefix("Bob") */ }), SubJSONOf({ "name": "$1" /* HasPrefix("Alice") */ })) */, "zip": NotZero() })`[1:]) // Erroneous op test.EqualStr(t, td.SubJSONOf(`123`).String(), "SubJSONOf()") } func TestSubJSONOfTypeBehind(t *testing.T) { equalTypes(t, td.SubJSONOf(`{"a":12}`), (map[string]any)(nil)) // Erroneous op equalTypes(t, td.SubJSONOf(`123`), nil) } func TestSuperJSONOf(t *testing.T) { type MyStruct struct { Name string `json:"name"` Age uint `json:"age"` Gender string `json:"gender"` Details string `json:"details"` } // // struct // got := MyStruct{Name: "Bob", Age: 42, Gender: "male", Details: "Nice"} // No placeholder checkOK(t, got, td.SuperJSONOf(`{"name": "Bob"}`)) // Numeric placeholders checkOK(t, got, td.SuperJSONOf(`{"name":"$1","age":$2}`, "Bob", 42)) // raw values checkOK(t, got, td.SuperJSONOf(`{"name":"$1","age":$2}`, td.Re(`^Bob`), td.Between(40, 45))) // Same using Flatten checkOK(t, got, td.SuperJSONOf(`{"name":"$1","age":$2}`, td.Flatten([]td.TestDeep{td.Re(`^Bob`), td.Between(40, 45)}), )) // Tag placeholders checkOK(t, got, td.SuperJSONOf(`{"name":"$name","gender":"$gender"}`, td.Tag("name", td.Re(`^Bob`)), td.Tag("gender", td.NotEmpty()))) // Mixed placeholders + operator for _, op := range []string{ "NotEmpty", "NotEmpty()", "$^NotEmpty", "$^NotEmpty()", `"$^NotEmpty"`, `"$^NotEmpty()"`, `r<$^NotEmpty>`, `r<$^NotEmpty()>`, } { checkOK(t, got, td.SuperJSONOf( `{"name":"$name","age":$1,"gender":`+op+`}`, td.Tag("age", td.Between(40, 45)), td.Tag("name", td.Re(`^Bob`))), "using operator %s", op) } // …with comments… checkOK(t, got, td.SuperJSONOf(` // This should be the JSON representation of MyStruct struct { // A person: "name": "$name", // The name of this person "age": $1, /* The age of this person: - placeholder unquoted, but could be without any change - to demonstrate a multi-lines comment */ "gender": $^NotEmpty // Shortcut to operator NotEmpty }`, td.Tag("age", td.Between(40, 45)), td.Tag("name", td.Re(`^Bob`)))) // // Errors checkError(t, func() {}, td.SuperJSONOf(`{}`), expectedError{ Message: mustBe("json.Marshal failed"), Summary: mustContain("json: unsupported type"), }) for i, n := range []any{ nil, (map[string]any)(nil), (map[string]bool)(nil), ([]int)(nil), } { checkError(t, n, td.SuperJSONOf(`{}`), expectedError{ Message: mustBe("values differ"), Got: mustBe("null"), Expected: mustBe("non-null"), }, "nil test #%d", i) } // // Fatal errors checkError(t, "never tested", td.SuperJSONOf(`[1, "$123bad"]`), expectedError{ Message: mustBe("bad usage of SuperJSONOf operator"), Path: mustBe("DATA"), Summary: mustBe(`JSON unmarshal error: invalid numeric placeholder at line 1:5 (pos 5)`), }) checkError(t, "never tested", td.SuperJSONOf(`[1, $000]`), expectedError{ Message: mustBe("bad usage of SuperJSONOf operator"), Path: mustBe("DATA"), Summary: mustBe(`JSON unmarshal error: invalid numeric placeholder "$000", it should start at "$1" at line 1:4 (pos 4)`), }) checkError(t, "never tested", td.SuperJSONOf(`[1, $1]`), expectedError{ Message: mustBe("bad usage of SuperJSONOf operator"), Path: mustBe("DATA"), Summary: mustBe(`JSON unmarshal error: numeric placeholder "$1", but no params given at line 1:4 (pos 4)`), }) checkError(t, "never tested", td.SuperJSONOf(`[1, 2, $3]`, td.Ignore()), expectedError{ Message: mustBe("bad usage of SuperJSONOf operator"), Path: mustBe("DATA"), Summary: mustBe(`JSON unmarshal error: numeric placeholder "$3", but only one param given at line 1:7 (pos 7)`), }) // $^Operator checkError(t, "never tested", td.SuperJSONOf(`[1, $^bad%]`), expectedError{ Message: mustBe("bad usage of SuperJSONOf operator"), Path: mustBe("DATA"), Summary: mustBe(`JSON unmarshal error: $^ must be followed by an operator name at line 1:4 (pos 4)`), }) checkError(t, "never tested", td.SuperJSONOf(`[1, "$^bad%"]`), expectedError{ Message: mustBe("bad usage of SuperJSONOf operator"), Path: mustBe("DATA"), Summary: mustBe(`JSON unmarshal error: $^ must be followed by an operator name at line 1:5 (pos 5)`), }) // named placeholders checkError(t, "never tested", td.SuperJSONOf(`[1, "$bad%"]`), expectedError{ Message: mustBe("bad usage of SuperJSONOf operator"), Path: mustBe("DATA"), Summary: mustBe(`JSON unmarshal error: bad placeholder "$bad%" at line 1:5 (pos 5)`), }) checkError(t, "never tested", td.SuperJSONOf(`[1, $unknown]`), expectedError{ Message: mustBe("bad usage of SuperJSONOf operator"), Path: mustBe("DATA"), Summary: mustBe(`JSON unmarshal error: unknown placeholder "$unknown" at line 1:4 (pos 4)`), }) checkError(t, "never tested", td.SuperJSONOf("null"), expectedError{ Message: mustBe("bad usage of SuperJSONOf operator"), Path: mustBe("DATA"), Summary: mustBe("SuperJSONOf() only accepts JSON objects {…}"), }) // // Stringification test.EqualStr(t, td.SuperJSONOf(`{}`).String(), `SuperJSONOf({})`) test.EqualStr(t, td.SuperJSONOf(`{"foo":1, "bar":2}`).String(), ` SuperJSONOf({ "bar": 2, "foo": 1 })`[1:]) test.EqualStr(t, td.SuperJSONOf(`{"label": $value, "zip": $^NotZero}`, td.Tag("value", td.Bag( td.SuperJSONOf(`{"name": $1,"age":$2}`, td.HasPrefix("Bob"), td.Between(12, 24), ), td.SuperJSONOf(`{"name": $1}`, td.HasPrefix("Alice")), )), ).String(), ` SuperJSONOf({ "label": "$value" /* Bag(SuperJSONOf({ "age": "$2" /* 12 ≤ got ≤ 24 */, "name": "$1" /* HasPrefix("Bob") */ }), SuperJSONOf({ "name": "$1" /* HasPrefix("Alice") */ })) */, "zip": NotZero() })`[1:]) // Erroneous op test.EqualStr(t, td.SuperJSONOf(`123`).String(), "SuperJSONOf()") } func TestSuperJSONOfTypeBehind(t *testing.T) { equalTypes(t, td.SuperJSONOf(`{"a":12}`), (map[string]any)(nil)) // Erroneous op equalTypes(t, td.SuperJSONOf(`123`), nil) } golang-github-maxatome-go-testdeep-1.14.0/td/td_keys_values.go000066400000000000000000000101161454313311600243320ustar00rootroot00000000000000// Copyright (c) 2019, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td import ( "reflect" "github.com/maxatome/go-testdeep/helpers/tdutil" "github.com/maxatome/go-testdeep/internal/ctxerr" "github.com/maxatome/go-testdeep/internal/util" ) type tdKVBase struct { tdSmugglerBase } func (b *tdKVBase) initKVBase(val any) bool { b.tdSmugglerBase = newSmugglerBase(val, 1) if vval := reflect.ValueOf(val); vval.IsValid() { if b.isTestDeeper { return true } if vval.Kind() == reflect.Slice { b.expectedValue = vval return true } } return false } type tdKeys struct { tdKVBase } var _ TestDeep = &tdKeys{} // summary(Keys): checks keys of a map // input(Keys): map // Keys is a smuggler operator. It takes a map and compares its // ordered keys to val. // // val can be a slice of items of the same type as the map keys: // // got := map[string]bool{"c": true, "a": false, "b": true} // td.Cmp(t, got, td.Keys([]string{"a", "b", "c"})) // succeeds, keys sorted // td.Cmp(t, got, td.Keys([]string{"c", "a", "b"})) // fails as not sorted // // as well as an other operator as [Bag], for example, to test keys in // an unsorted manner: // // got := map[string]bool{"c": true, "a": false, "b": true} // td.Cmp(t, got, td.Keys(td.Bag("c", "a", "b"))) // succeeds // // See also [Values] and [ContainsKey]. func Keys(val any) TestDeep { k := tdKeys{} if !k.initKVBase(val) { k.err = ctxerr.OpBadUsage("Keys", "(TESTDEEP_OPERATOR|SLICE)", val, 1, true) } return &k } func (k *tdKeys) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error { if k.err != nil { return ctx.CollectError(k.err) } if got.Kind() != reflect.Map { if ctx.BooleanError { return ctxerr.BooleanError } return ctx.CollectError(ctxerr.BadKind(got, reflect.Map.String())) } // Build a sorted slice of keys l := got.Len() keys := reflect.MakeSlice(reflect.SliceOf(got.Type().Key()), l, l) for i, k := range tdutil.MapSortedKeys(got) { keys.Index(i).Set(k) } return deepValueEqual(ctx.AddFunctionCall("keys"), keys, k.expectedValue) } func (k *tdKeys) String() string { if k.err != nil { return k.stringError() } if k.isTestDeeper { return "keys: " + k.expectedValue.Interface().(TestDeep).String() } return "keys=" + util.ToString(k.expectedValue.Interface()) } type tdValues struct { tdKVBase } var _ TestDeep = &tdValues{} // summary(Values): checks values of a map // input(Values): map // Values is a smuggler operator. It takes a map and compares its // ordered values to val. // // val can be a slice of items of the same type as the map values: // // got := map[int]string{3: "c", 1: "a", 2: "b"} // td.Cmp(t, got, td.Values([]string{"a", "b", "c"})) // succeeds, values sorted // td.Cmp(t, got, td.Values([]string{"c", "a", "b"})) // fails as not sorted // // as well as an other operator as [Bag], for example, to test values in // an unsorted manner: // // got := map[int]string{3: "c", 1: "a", 2: "b"} // td.Cmp(t, got, td.Values(td.Bag("c", "a", "b"))) // succeeds // // See also [Keys]. func Values(val any) TestDeep { v := tdValues{} if !v.initKVBase(val) { v.err = ctxerr.OpBadUsage("Values", "(TESTDEEP_OPERATOR|SLICE)", val, 1, true) } return &v } func (v *tdValues) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error { if v.err != nil { return ctx.CollectError(v.err) } if got.Kind() != reflect.Map { if ctx.BooleanError { return ctxerr.BooleanError } return ctx.CollectError(ctxerr.BadKind(got, reflect.Map.String())) } // Build a sorted slice of values l := got.Len() values := reflect.MakeSlice(reflect.SliceOf(got.Type().Elem()), l, l) for i, v := range tdutil.MapSortedValues(got) { values.Index(i).Set(v) } return deepValueEqual(ctx.AddFunctionCall("values"), values, v.expectedValue) } func (v *tdValues) String() string { if v.err != nil { return v.stringError() } if v.isTestDeeper { return "values: " + v.expectedValue.Interface().(TestDeep).String() } return "values=" + util.ToString(v.expectedValue.Interface()) } golang-github-maxatome-go-testdeep-1.14.0/td/td_keys_values_test.go000066400000000000000000000106071454313311600253760ustar00rootroot00000000000000// Copyright (c) 2019, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td_test import ( "testing" "github.com/maxatome/go-testdeep/internal/test" "github.com/maxatome/go-testdeep/td" ) func TestKeysValues(t *testing.T) { var m map[string]int // t.Run("nil map", func(t *testing.T) { checkOK(t, m, td.Keys([]string{})) checkOK(t, m, td.Values([]int{})) checkOK(t, m, td.Keys(td.Empty())) checkOK(t, m, td.Values(td.Empty())) checkError(t, m, td.Keys(td.NotEmpty()), expectedError{ Message: mustBe("empty"), Path: mustBe("keys(DATA)"), Expected: mustBe("not empty"), }) checkError(t, m, td.Values(td.NotEmpty()), expectedError{ Message: mustBe("empty"), Path: mustBe("values(DATA)"), Expected: mustBe("not empty"), }) }) // t.Run("non-nil but empty map", func(t *testing.T) { m = map[string]int{} checkOK(t, m, td.Keys([]string{})) checkOK(t, m, td.Values([]int{})) checkOK(t, m, td.Keys(td.Empty())) checkOK(t, m, td.Values(td.Empty())) checkError(t, m, td.Keys(td.NotEmpty()), expectedError{ Message: mustBe("empty"), Path: mustBe("keys(DATA)"), Expected: mustBe("not empty"), }) checkError(t, m, td.Values(td.NotEmpty()), expectedError{ Message: mustBe("empty"), Path: mustBe("values(DATA)"), Expected: mustBe("not empty"), }) }) // t.Run("Filled map", func(t *testing.T) { m = map[string]int{"a": 1, "b": 2, "c": 3, "d": 4, "e": 5, "f": 6} checkOK(t, m, td.Keys([]string{"a", "b", "c", "d", "e", "f"})) checkOK(t, m, td.Values([]int{1, 2, 3, 4, 5, 6})) checkOK(t, m, td.Keys(td.Bag("a", "b", "c", "d", "e", "f"))) checkOK(t, m, td.Values(td.Bag(1, 2, 3, 4, 5, 6))) checkOK(t, m, td.Keys(td.ArrayEach(td.Between("a", "f")))) checkOK(t, m, td.Values(td.ArrayEach(td.Between(1, 6)))) checkError(t, m, td.Keys(td.Empty()), expectedError{ Message: mustBe("not empty"), Path: mustBe("keys(DATA)"), Expected: mustBe("empty"), }) checkError(t, m, td.Values(td.Empty()), expectedError{ Message: mustBe("not empty"), Path: mustBe("values(DATA)"), Expected: mustBe("empty"), }) }) // t.Run("Errors", func(t *testing.T) { checkError(t, nil, td.Keys([]int{1, 2, 3}), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA"), Got: mustBe("nil"), Expected: mustContain("keys=([]int)"), }) checkError(t, nil, td.Values([]int{1, 2, 3}), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA"), Got: mustBe("nil"), Expected: mustContain("values=([]int)"), }) checkError(t, nil, td.Keys(td.Empty()), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA"), Got: mustBe("nil"), Expected: mustBe("keys: Empty()"), }) checkError(t, nil, td.Values(td.Empty()), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA"), Got: mustBe("nil"), Expected: mustBe("values: Empty()"), }) checkError(t, 123, td.Keys(td.Empty()), expectedError{ Message: mustBe("bad kind"), Path: mustBe("DATA"), Got: mustBe("int"), Expected: mustBe("map"), }) checkError(t, 123, td.Values(td.Empty()), expectedError{ Message: mustBe("bad kind"), Path: mustBe("DATA"), Got: mustBe("int"), Expected: mustBe("map"), }) }) // t.Run("Bad usage", func(t *testing.T) { checkError(t, "never tested", td.Keys(12), expectedError{ Message: mustBe("bad usage of Keys operator"), Path: mustBe("DATA"), Summary: mustBe("usage: Keys(TESTDEEP_OPERATOR|SLICE), but received int as 1st parameter"), }) checkError(t, "never tested", td.Values(12), expectedError{ Message: mustBe("bad usage of Values operator"), Path: mustBe("DATA"), Summary: mustBe("usage: Values(TESTDEEP_OPERATOR|SLICE), but received int as 1st parameter"), }) }) // Erroneous op test.EqualStr(t, td.Keys(12).String(), "Keys()") test.EqualStr(t, td.Values(12).String(), "Values()") } func TestKeysValuesTypeBehind(t *testing.T) { equalTypes(t, td.Keys([]string{}), nil) equalTypes(t, td.Values([]string{}), nil) // Erroneous op equalTypes(t, td.Keys(12), nil) equalTypes(t, td.Values(12), nil) } golang-github-maxatome-go-testdeep-1.14.0/td/td_lax.go000066400000000000000000000055541454313311600225760ustar00rootroot00000000000000// Copyright (c) 2019, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td import ( "reflect" "github.com/maxatome/go-testdeep/internal/ctxerr" "github.com/maxatome/go-testdeep/internal/util" ) type tdLax struct { tdSmugglerBase } var _ TestDeep = &tdLax{} // summary(Lax): temporarily enables [`BeLax` config flag] // input(Lax): all // Lax is a smuggler operator, it temporarily enables the BeLax config // flag before letting the comparison process continue its course. // // It is more commonly used as [CmpLax] function than as an operator. It // could be used when, for example, an operator is constructed once // but applied to different, but compatible types as in: // // bw := td.Between(20, 30) // intValue := 21 // floatValue := 21.89 // td.Cmp(t, intValue, bw) // no need to be lax here: same int types // td.Cmp(t, floatValue, td.Lax(bw)) // be lax please, as float64 ≠ int // // Note that in the latter case, [CmpLax] could be used as well: // // td.CmpLax(t, floatValue, bw) // // TypeBehind method returns the greatest convertible or more common // [reflect.Type] of expectedValue if it is a base type (bool, int*, // uint*, float*, complex*, string), the [reflect.Type] of // expectedValue otherwise, except if expectedValue is a [TestDeep] // operator. In this case, it delegates TypeBehind() to the operator. func Lax(expectedValue any) TestDeep { c := tdLax{ tdSmugglerBase: newSmugglerBase(expectedValue), } if !c.isTestDeeper { c.expectedValue = reflect.ValueOf(expectedValue) } return &c } func (l *tdLax) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error { ctx.BeLax = true return deepValueEqual(ctx, got, l.expectedValue) } func (l *tdLax) HandleInvalid() bool { return true // Knows how to handle untyped nil values (aka invalid values) } func (l *tdLax) String() string { return "Lax(" + util.ToString(l.expectedValue) + ")" } func (l *tdLax) TypeBehind() reflect.Type { // If the expected value is a TestDeep operator, delegate TypeBehind to it if l.isTestDeeper { return l.expectedValue.Interface().(TestDeep).TypeBehind() } // For base types, returns the greatest convertible or more common one switch l.expectedValue.Kind() { case reflect.Invalid: return nil case reflect.Bool: return reflect.TypeOf(false) case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: return reflect.TypeOf(int64(0)) case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: return reflect.TypeOf(uint64(0)) case reflect.Float32, reflect.Float64: return reflect.TypeOf(float64(0)) case reflect.Complex64, reflect.Complex128: return reflect.TypeOf(complex(128, -1)) case reflect.String: return reflect.TypeOf("") default: return l.expectedValue.Type() } } golang-github-maxatome-go-testdeep-1.14.0/td/td_lax_test.go000066400000000000000000000040171454313311600236260ustar00rootroot00000000000000// Copyright (c) 2019, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td_test import ( "testing" "github.com/maxatome/go-testdeep/internal/test" "github.com/maxatome/go-testdeep/td" ) func TestLax(t *testing.T) { checkOK(t, int64(1234), td.Lax(1234)) type MyInt int32 checkOK(t, int64(123), td.Lax(MyInt(123))) checkOK(t, MyInt(123), td.Lax(int64(123))) type gotStruct struct { name string age int } type expectedStruct struct { name string age int } checkOK(t, gotStruct{ name: "bob", age: 42, }, td.Lax(expectedStruct{ name: "bob", age: 42, })) checkOK(t, &gotStruct{ name: "bob", age: 42, }, td.Lax(&expectedStruct{ name: "bob", age: 42, })) checkError(t, int64(123), td.Between(120, 125), expectedError{ Message: mustBe("type mismatch"), }) checkOK(t, int64(123), td.Lax(td.Between(120, 125))) // nil cases checkOK(t, nil, td.Lax(nil)) checkOK(t, (*gotStruct)(nil), td.Lax((*expectedStruct)(nil))) checkOK(t, (*gotStruct)(nil), td.Lax(nil)) checkOK(t, (chan int)(nil), td.Lax(nil)) checkOK(t, (func())(nil), td.Lax(nil)) checkOK(t, (map[int]int)(nil), td.Lax(nil)) checkOK(t, ([]int)(nil), td.Lax(nil)) // // String test.EqualStr(t, td.Lax(6).String(), "Lax(6)") } func TestLaxTypeBehind(t *testing.T) { equalTypes(t, td.Lax(nil), nil) type MyBool bool equalTypes(t, td.Lax(MyBool(false)), false) equalTypes(t, td.Lax(0), int64(0)) equalTypes(t, td.Lax(uint8(0)), uint64(0)) equalTypes(t, td.Lax(float32(0)), float64(0)) equalTypes(t, td.Lax(complex64(complex(1, 1))), complex128(complex(1, 1))) type MyString string equalTypes(t, td.Lax(MyString("")), "") type MyBytes []byte equalTypes(t, td.Lax([]byte{}), []byte{}) equalTypes(t, td.Lax(MyBytes{}), MyBytes{}) // Another TestDeep operator delegation equalTypes(t, td.Lax(td.Struct(MyStruct{}, nil)), MyStruct{}) equalTypes(t, td.Lax(td.Any(1, 1.2)), nil) } golang-github-maxatome-go-testdeep-1.14.0/td/td_len_cap.go000066400000000000000000000122211454313311600234000ustar00rootroot00000000000000// Copyright (c) 2018, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td import ( "fmt" "math" "reflect" "github.com/maxatome/go-testdeep/internal/ctxerr" "github.com/maxatome/go-testdeep/internal/types" ) type tdLenCapBase struct { tdSmugglerBase } func (b *tdLenCapBase) initLenCapBase(val any) { b.tdSmugglerBase = newSmugglerBase(val, 1) // math.MaxInt appeared in go1.17 const ( maxUint = ^uint(0) maxInt = int(maxUint >> 1) minInt = -maxInt - 1 usage = "(TESTDEEP_OPERATOR|INT)" ) if val == nil { b.err = ctxerr.OpBadUsage(b.GetLocation().Func, usage, val, 1, true) return } if b.isTestDeeper { return } vval := reflect.ValueOf(val) // A len or capacity is always an int, but accept any MinInt ≤ num ≤ MaxInt, // so it can be used in JSON, SubJSONOf and SuperJSONOf as float64 switch vval.Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: num := vval.Int() if num >= int64(minInt) && num <= int64(maxInt) { b.expectedValue = reflect.ValueOf(int(num)) return } case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: num := vval.Uint() if num <= uint64(maxInt) { b.expectedValue = reflect.ValueOf(int(num)) return } case reflect.Float32, reflect.Float64: num := vval.Float() if num == math.Trunc(num) && num >= float64(minInt) && num <= float64(maxInt) { b.expectedValue = reflect.ValueOf(int(num)) return } default: b.err = ctxerr.OpBadUsage(b.GetLocation().Func, usage, val, 1, true) return } op := b.GetLocation().Func b.err = ctxerr.OpBad(op, "usage: "+op+usage+ ", but received an out of bounds or not integer 1st parameter (%v), should be in int range", val) } func (b *tdLenCapBase) isEqual(ctx ctxerr.Context, got int) (bool, *ctxerr.Error) { if b.isTestDeeper { return true, deepValueEqual(ctx, reflect.ValueOf(got), b.expectedValue) } if int64(got) == b.expectedValue.Int() { return true, nil } return false, nil } type tdLen struct { tdLenCapBase } var _ TestDeep = &tdLen{} // summary(Len): checks an array, slice, map, string or channel length // input(Len): array,slice,map,chan // Len is a smuggler operator. It takes data, applies len() function // on it and compares its result to expectedLen. Of course, the // compared value must be an array, a channel, a map, a slice or a // string. // // expectedLen can be an int value: // // td.Cmp(t, gotSlice, td.Len(12)) // // as well as an other operator: // // td.Cmp(t, gotSlice, td.Len(td.Between(3, 4))) // // See also [Cap]. func Len(expectedLen any) TestDeep { l := tdLen{} l.initLenCapBase(expectedLen) return &l } func (l *tdLen) String() string { if l.err != nil { return l.stringError() } if l.isTestDeeper { return "len: " + l.expectedValue.Interface().(TestDeep).String() } return fmt.Sprintf("len=%d", l.expectedValue.Int()) } func (l *tdLen) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error { if l.err != nil { return ctx.CollectError(l.err) } switch got.Kind() { case reflect.Array, reflect.Chan, reflect.Map, reflect.Slice, reflect.String: ret, err := l.isEqual(ctx.AddFunctionCall("len"), got.Len()) if ret { return err } if ctx.BooleanError { return ctxerr.BooleanError } return ctx.CollectError(&ctxerr.Error{ Message: "bad length", Got: types.RawInt(got.Len()), Expected: types.RawInt(l.expectedValue.Int()), }) default: if ctx.BooleanError { return ctxerr.BooleanError } return ctx.CollectError(ctxerr.BadKind(got, "array OR chan OR map OR slice OR string")) } } type tdCap struct { tdLenCapBase } var _ TestDeep = &tdCap{} // summary(Cap): checks an array, slice or channel capacity // input(Cap): array,slice,chan // Cap is a smuggler operator. It takes data, applies cap() function // on it and compares its result to expectedCap. Of course, the // compared value must be an array, a channel or a slice. // // expectedCap can be an int value: // // td.Cmp(t, gotSlice, td.Cap(12)) // // as well as an other operator: // // td.Cmp(t, gotSlice, td.Cap(td.Between(3, 4))) // // See also [Len]. func Cap(expectedCap any) TestDeep { c := tdCap{} c.initLenCapBase(expectedCap) return &c } func (c *tdCap) String() string { if c.err != nil { return c.stringError() } if c.isTestDeeper { return "cap: " + c.expectedValue.Interface().(TestDeep).String() } return fmt.Sprintf("cap=%d", c.expectedValue.Int()) } func (c *tdCap) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error { if c.err != nil { return ctx.CollectError(c.err) } switch got.Kind() { case reflect.Array, reflect.Chan, reflect.Slice: ret, err := c.isEqual(ctx.AddFunctionCall("cap"), got.Cap()) if ret { return err } if ctx.BooleanError { return ctxerr.BooleanError } return ctx.CollectError(&ctxerr.Error{ Message: "bad capacity", Got: types.RawInt(got.Cap()), Expected: types.RawInt(c.expectedValue.Int()), }) default: if ctx.BooleanError { return ctxerr.BooleanError } return ctx.CollectError(ctxerr.BadKind(got, "array OR chan OR slice")) } } golang-github-maxatome-go-testdeep-1.14.0/td/td_len_cap_test.go000066400000000000000000000165361454313311600244540ustar00rootroot00000000000000// Copyright (c) 2018, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td_test import ( "math" "testing" "github.com/maxatome/go-testdeep/internal/test" "github.com/maxatome/go-testdeep/td" ) func TestLen(t *testing.T) { checkOK(t, "abcd", td.Len(4)) checkOK(t, "abcd", td.Len(td.Between(4, 6))) checkOK(t, "abcd", td.Len(td.Between(6, 4))) checkOK(t, []byte("abcd"), td.Len(4)) checkOK(t, []byte("abcd"), td.Len(td.Between(4, 6))) checkOK(t, [5]int{}, td.Len(5)) checkOK(t, [5]int{}, td.Len(int8(5))) checkOK(t, [5]int{}, td.Len(int16(5))) checkOK(t, [5]int{}, td.Len(int32(5))) checkOK(t, [5]int{}, td.Len(int64(5))) checkOK(t, [5]int{}, td.Len(uint(5))) checkOK(t, [5]int{}, td.Len(uint8(5))) checkOK(t, [5]int{}, td.Len(uint16(5))) checkOK(t, [5]int{}, td.Len(uint32(5))) checkOK(t, [5]int{}, td.Len(uint64(5))) checkOK(t, [5]int{}, td.Len(float32(5))) checkOK(t, [5]int{}, td.Len(float64(5))) checkOK(t, [5]int{}, td.Len(td.Between(4, 6))) checkOK(t, map[int]bool{1: true, 2: false}, td.Len(2)) checkOK(t, map[int]bool{1: true, 2: false}, td.Len(td.Between(1, 6))) checkOK(t, make(chan int, 3), td.Len(0)) checkError(t, [5]int{}, td.Len(4), expectedError{ Message: mustBe("bad length"), Path: mustBe("DATA"), Got: mustBe("5"), Expected: mustBe("4"), }) checkError(t, [5]int{}, td.Len(td.Lt(4)), expectedError{ Message: mustBe("values differ"), Path: mustBe("len(DATA)"), Got: mustBe("5"), Expected: mustBe("< 4"), }) checkError(t, 123, td.Len(4), expectedError{ Message: mustBe("bad kind"), Path: mustBe("DATA"), Got: mustBe("int"), Expected: mustBe("array OR chan OR map OR slice OR string"), }) // // Bad usage checkError(t, "never tested", td.Len(nil), expectedError{ Message: mustBe("bad usage of Len operator"), Path: mustBe("DATA"), Summary: mustBe("usage: Len(TESTDEEP_OPERATOR|INT), but received nil as 1st parameter"), }) checkError(t, "never tested", td.Len("12"), expectedError{ Message: mustBe("bad usage of Len operator"), Path: mustBe("DATA"), Summary: mustBe("usage: Len(TESTDEEP_OPERATOR|INT), but received string as 1st parameter"), }) // out of bounds checkError(t, "never tested", td.Len(uint64(math.MaxUint64)), expectedError{ Message: mustBe("bad usage of Len operator"), Path: mustBe("DATA"), Summary: mustBe("usage: Len(TESTDEEP_OPERATOR|INT), but received an out of bounds or not integer 1st parameter (18446744073709551615), should be in int range"), }) checkError(t, "never tested", td.Len(float64(math.MaxUint64)), expectedError{ Message: mustBe("bad usage of Len operator"), Path: mustBe("DATA"), Summary: mustBe("usage: Len(TESTDEEP_OPERATOR|INT), but received an out of bounds or not integer 1st parameter (1.8446744073709552e+19), should be in int range"), }) checkError(t, "never tested", td.Len(float64(-math.MaxUint64)), expectedError{ Message: mustBe("bad usage of Len operator"), Path: mustBe("DATA"), Summary: mustBe("usage: Len(TESTDEEP_OPERATOR|INT), but received an out of bounds or not integer 1st parameter (-1.8446744073709552e+19), should be in int range"), }) checkError(t, "never tested", td.Len(3.1), expectedError{ Message: mustBe("bad usage of Len operator"), Path: mustBe("DATA"), Summary: mustBe("usage: Len(TESTDEEP_OPERATOR|INT), but received an out of bounds or not integer 1st parameter (3.1), should be in int range"), }) // // String test.EqualStr(t, td.Len(3).String(), "len=3") test.EqualStr(t, td.Len(td.Between(3, 8)).String(), "len: 3 ≤ got ≤ 8") test.EqualStr(t, td.Len(td.Gt(8)).String(), "len: > 8") // Erroneous test.EqualStr(t, td.Len("12").String(), "Len()") } func TestCap(t *testing.T) { checkOK(t, make([]byte, 0, 4), td.Cap(4)) checkOK(t, make([]byte, 0, 4), td.Cap(td.Between(4, 6))) checkOK(t, [5]int{}, td.Cap(5)) checkOK(t, [5]int{}, td.Cap(int8(5))) checkOK(t, [5]int{}, td.Cap(int16(5))) checkOK(t, [5]int{}, td.Cap(int32(5))) checkOK(t, [5]int{}, td.Cap(int64(5))) checkOK(t, [5]int{}, td.Cap(uint(5))) checkOK(t, [5]int{}, td.Cap(uint8(5))) checkOK(t, [5]int{}, td.Cap(uint16(5))) checkOK(t, [5]int{}, td.Cap(uint32(5))) checkOK(t, [5]int{}, td.Cap(uint64(5))) checkOK(t, [5]int{}, td.Cap(float32(5))) checkOK(t, [5]int{}, td.Cap(float64(5))) checkOK(t, [5]int{}, td.Cap(td.Between(4, 6))) checkOK(t, make(chan int, 3), td.Cap(3)) checkError(t, [5]int{}, td.Cap(4), expectedError{ Message: mustBe("bad capacity"), Path: mustBe("DATA"), Got: mustBe("5"), Expected: mustBe("4"), }) checkError(t, [5]int{}, td.Cap(td.Between(2, 4)), expectedError{ Message: mustBe("values differ"), Path: mustBe("cap(DATA)"), Got: mustBe("5"), Expected: mustBe("2 ≤ got ≤ 4"), }) checkError(t, map[int]int{1: 2}, td.Cap(1), expectedError{ Message: mustBe("bad kind"), Path: mustBe("DATA"), Got: mustBe("map (map[int]int type)"), Expected: mustBe("array OR chan OR slice"), }) // // Bad usage checkError(t, "never tested", td.Cap(nil), expectedError{ Message: mustBe("bad usage of Cap operator"), Path: mustBe("DATA"), Summary: mustBe("usage: Cap(TESTDEEP_OPERATOR|INT), but received nil as 1st parameter"), }) checkError(t, "never tested", td.Cap("12"), expectedError{ Message: mustBe("bad usage of Cap operator"), Path: mustBe("DATA"), Summary: mustBe("usage: Cap(TESTDEEP_OPERATOR|INT), but received string as 1st parameter"), }) // out of bounds checkError(t, "never tested", td.Cap(uint64(math.MaxUint64)), expectedError{ Message: mustBe("bad usage of Cap operator"), Path: mustBe("DATA"), Summary: mustBe("usage: Cap(TESTDEEP_OPERATOR|INT), but received an out of bounds or not integer 1st parameter (18446744073709551615), should be in int range"), }) checkError(t, "never tested", td.Cap(float64(math.MaxUint64)), expectedError{ Message: mustBe("bad usage of Cap operator"), Path: mustBe("DATA"), Summary: mustBe("usage: Cap(TESTDEEP_OPERATOR|INT), but received an out of bounds or not integer 1st parameter (1.8446744073709552e+19), should be in int range"), }) checkError(t, "never tested", td.Cap(float64(-math.MaxUint64)), expectedError{ Message: mustBe("bad usage of Cap operator"), Path: mustBe("DATA"), Summary: mustBe("usage: Cap(TESTDEEP_OPERATOR|INT), but received an out of bounds or not integer 1st parameter (-1.8446744073709552e+19), should be in int range"), }) checkError(t, "never tested", td.Cap(3.1), expectedError{ Message: mustBe("bad usage of Cap operator"), Path: mustBe("DATA"), Summary: mustBe("usage: Cap(TESTDEEP_OPERATOR|INT), but received an out of bounds or not integer 1st parameter (3.1), should be in int range"), }) // // String test.EqualStr(t, td.Cap(3).String(), "cap=3") test.EqualStr(t, td.Cap(td.Between(3, 8)).String(), "cap: 3 ≤ got ≤ 8") test.EqualStr(t, td.Cap(td.Gt(8)).String(), "cap: > 8") // Erroneous op test.EqualStr(t, td.Cap("12").String(), "Cap()") } func TestLenCapTypeBehind(t *testing.T) { equalTypes(t, td.Cap(3), nil) equalTypes(t, td.Len(3), nil) // Erroneous op equalTypes(t, td.Cap("12"), nil) equalTypes(t, td.Len("12"), nil) } golang-github-maxatome-go-testdeep-1.14.0/td/td_list.go000066400000000000000000000012361454313311600227560ustar00rootroot00000000000000// Copyright (c) 2018, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td import ( "reflect" "strings" "github.com/maxatome/go-testdeep/internal/flat" "github.com/maxatome/go-testdeep/internal/util" ) type tdList struct { baseOKNil items []reflect.Value } func newList(items ...any) tdList { return tdList{ baseOKNil: newBaseOKNil(4), items: flat.Values(items), } } func (l *tdList) String() string { var b strings.Builder b.WriteString(l.GetLocation().Func) return util.SliceToString(&b, l.items). String() } golang-github-maxatome-go-testdeep-1.14.0/td/td_map.go000066400000000000000000000237041454313311600225640ustar00rootroot00000000000000// Copyright (c) 2018, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td import ( "fmt" "reflect" "strings" "github.com/maxatome/go-testdeep/helpers/tdutil" "github.com/maxatome/go-testdeep/internal/ctxerr" "github.com/maxatome/go-testdeep/internal/dark" "github.com/maxatome/go-testdeep/internal/util" ) type mapKind uint8 const ( allMap mapKind = iota subMap superMap ) type tdMap struct { tdExpectedType expectedEntries []mapEntryInfo kind mapKind } var _ TestDeep = &tdMap{} type mapEntryInfo struct { key reflect.Value expected reflect.Value } // MapEntries allows to pass map entries to check in functions [Map], // [SubMapOf] and [SuperMapOf]. It is a map whose each key is the // expected entry key and the corresponding value the expected entry // value (which can be a [TestDeep] operator as well as a zero value.) type MapEntries map[any]any func newMap(model any, entries MapEntries, kind mapKind) *tdMap { vmodel := reflect.ValueOf(model) m := tdMap{ tdExpectedType: tdExpectedType{ base: newBase(4), }, kind: kind, } switch vmodel.Kind() { case reflect.Ptr: if vmodel.Type().Elem().Kind() != reflect.Map { break } m.isPtr = true if vmodel.IsNil() { m.expectedType = vmodel.Type().Elem() m.populateExpectedEntries(entries, reflect.Value{}) return &m } vmodel = vmodel.Elem() fallthrough case reflect.Map: m.expectedType = vmodel.Type() m.populateExpectedEntries(entries, vmodel) return &m } m.err = ctxerr.OpBadUsage( m.GetLocation().Func, "(MAP|&MAP, EXPECTED_ENTRIES)", model, 1, true) return &m } func (m *tdMap) populateExpectedEntries(entries MapEntries, expectedModel reflect.Value) { var keysInModel int if expectedModel.IsValid() { keysInModel = expectedModel.Len() } m.expectedEntries = make([]mapEntryInfo, 0, keysInModel+len(entries)) checkedEntries := make(map[any]bool, len(entries)) keyType := m.expectedType.Key() valueType := m.expectedType.Elem() var entryInfo mapEntryInfo for key, expectedValue := range entries { vkey := reflect.ValueOf(key) if !vkey.Type().AssignableTo(keyType) { m.err = ctxerr.OpBad( m.GetLocation().Func, "expected key %s type mismatch: %s != model key type (%s)", util.ToString(key), vkey.Type(), keyType) return } if expectedValue == nil { switch valueType.Kind() { case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice: entryInfo.expected = reflect.Zero(valueType) // change to a typed nil default: m.err = ctxerr.OpBad( m.GetLocation().Func, "expected key %s value cannot be nil as entries value type is %s", util.ToString(key), valueType) return } } else { entryInfo.expected = reflect.ValueOf(expectedValue) if _, ok := expectedValue.(TestDeep); !ok { if !entryInfo.expected.Type().AssignableTo(valueType) { m.err = ctxerr.OpBad( m.GetLocation().Func, "expected key %s value type mismatch: %s != model key type (%s)", util.ToString(key), entryInfo.expected.Type(), valueType) return } } } entryInfo.key = vkey m.expectedEntries = append(m.expectedEntries, entryInfo) checkedEntries[dark.MustGetInterface(vkey)] = true } // Check entries in model if keysInModel == 0 { return } tdutil.MapEach(expectedModel, func(k, v reflect.Value) bool { entryInfo.expected = v if checkedEntries[dark.MustGetInterface(k)] { m.err = ctxerr.OpBad( m.GetLocation().Func, "%s entry exists in both model & expectedEntries", util.ToString(k)) return false } entryInfo.key = k m.expectedEntries = append(m.expectedEntries, entryInfo) return true }) } // summary(Map): compares the contents of a map // input(Map): map,ptr(ptr on map) // Map operator compares the contents of a map against the non-zero // values of model (if any) and the values of expectedEntries. // // model must be the same type as compared data. // // expectedEntries can be nil, if no zero entries are expected and // no [TestDeep] operators are involved. // // During a match, all expected entries must be found and all data // entries must be expected to succeed. // // got := map[string]string{ // "foo": "test", // "bar": "wizz", // "zip": "buzz", // } // td.Cmp(t, got, td.Map( // map[string]string{ // "foo": "test", // "bar": "wizz", // }, // td.MapEntries{ // "zip": td.HasSuffix("zz"), // }), // ) // succeeds // // TypeBehind method returns the [reflect.Type] of model. // // See also [SubMapOf] and [SuperMapOf]. func Map(model any, expectedEntries MapEntries) TestDeep { return newMap(model, expectedEntries, allMap) } // summary(SubMapOf): compares the contents of a map but with // potentially some exclusions // input(SubMapOf): map,ptr(ptr on map) // SubMapOf operator compares the contents of a map against the non-zero // values of model (if any) and the values of expectedEntries. // // model must be the same type as compared data. // // expectedEntries can be nil, if no zero entries are expected and // no [TestDeep] operators are involved. // // During a match, each map entry should be matched by an expected // entry to succeed. But some expected entries can be missing from the // compared map. // // got := map[string]string{ // "foo": "test", // "zip": "buzz", // } // td.Cmp(t, got, td.SubMapOf( // map[string]string{ // "foo": "test", // "bar": "wizz", // }, // td.MapEntries{ // "zip": td.HasSuffix("zz"), // }), // ) // succeeds // // td.Cmp(t, got, td.SubMapOf( // map[string]string{ // "bar": "wizz", // }, // td.MapEntries{ // "zip": td.HasSuffix("zz"), // }), // ) // fails, extra {"foo": "test"} in got // // TypeBehind method returns the [reflect.Type] of model. // // See also [Map] and [SuperMapOf]. func SubMapOf(model any, expectedEntries MapEntries) TestDeep { return newMap(model, expectedEntries, subMap) } // summary(SuperMapOf): compares the contents of a map but with // potentially some extra entries // input(SuperMapOf): map,ptr(ptr on map) // SuperMapOf operator compares the contents of a map against the non-zero // values of model (if any) and the values of expectedEntries. // // model must be the same type as compared data. // // expectedEntries can be nil, if no zero entries are expected and // no [TestDeep] operators are involved. // // During a match, each expected entry should match in the compared // map. But some entries in the compared map may not be expected. // // got := map[string]string{ // "foo": "test", // "bar": "wizz", // "zip": "buzz", // } // td.Cmp(t, got, td.SuperMapOf( // map[string]string{ // "foo": "test", // }, // td.MapEntries{ // "zip": td.HasSuffix("zz"), // }), // ) // succeeds // // td.Cmp(t, got, td.SuperMapOf( // map[string]string{ // "foo": "test", // }, // td.MapEntries{ // "biz": td.HasSuffix("zz"), // }), // ) // fails, missing {"biz": …} in got // // TypeBehind method returns the [reflect.Type] of model. // // See also [SuperMapOf] and [SubMapOf]. func SuperMapOf(model any, expectedEntries MapEntries) TestDeep { return newMap(model, expectedEntries, superMap) } func (m *tdMap) Match(ctx ctxerr.Context, got reflect.Value) (err *ctxerr.Error) { if m.err != nil { return ctx.CollectError(m.err) } err = m.checkPtr(ctx, &got, true) if err != nil { return ctx.CollectError(err) } return m.match(ctx, got) } func (m *tdMap) match(ctx ctxerr.Context, got reflect.Value) (err *ctxerr.Error) { err = m.checkType(ctx, got) if err != nil { return ctx.CollectError(err) } var notFoundKeys []reflect.Value foundKeys := map[any]bool{} for _, entryInfo := range m.expectedEntries { gotValue := got.MapIndex(entryInfo.key) if !gotValue.IsValid() { notFoundKeys = append(notFoundKeys, entryInfo.key) continue } err = deepValueEqual(ctx.AddMapKey(entryInfo.key), gotValue, entryInfo.expected) if err != nil { return err } foundKeys[dark.MustGetInterface(entryInfo.key)] = true } const errorMessage = "comparing hash keys of %%" // For SuperMapOf we don't care about extra keys if m.kind == superMap { if len(notFoundKeys) == 0 { return nil } if ctx.BooleanError { return ctxerr.BooleanError } return ctx.CollectError(&ctxerr.Error{ Message: errorMessage, Summary: (tdSetResult{ Kind: keysSetResult, Missing: notFoundKeys, Sort: true, }).Summary(), }) } // No extra key to search, all got keys have been found if got.Len() == len(foundKeys) { if m.kind == subMap { return nil } // allMap if len(notFoundKeys) == 0 { return nil } if ctx.BooleanError { return ctxerr.BooleanError } return ctx.CollectError(&ctxerr.Error{ Message: errorMessage, Summary: (tdSetResult{ Kind: keysSetResult, Missing: notFoundKeys, Sort: true, }).Summary(), }) } if ctx.BooleanError { return ctxerr.BooleanError } // Retrieve extra keys res := tdSetResult{ Kind: keysSetResult, Missing: notFoundKeys, Extra: make([]reflect.Value, 0, got.Len()-len(foundKeys)), Sort: true, } for _, k := range tdutil.MapSortedKeys(got) { if !foundKeys[dark.MustGetInterface(k)] { res.Extra = append(res.Extra, k) } } return ctx.CollectError(&ctxerr.Error{ Message: errorMessage, Summary: res.Summary(), }) } func (m *tdMap) String() string { if m.err != nil { return m.stringError() } var buf strings.Builder if m.kind != allMap { buf.WriteString(m.GetLocation().Func) buf.WriteByte('(') } buf.WriteString(m.expectedTypeStr()) if len(m.expectedEntries) == 0 { buf.WriteString("{}") } else { buf.WriteString("{\n") for _, entryInfo := range m.expectedEntries { fmt.Fprintf(&buf, " %s: %s,\n", //nolint: errcheck util.ToString(entryInfo.key), util.ToString(entryInfo.expected)) } buf.WriteByte('}') } if m.kind != allMap { buf.WriteByte(')') } return buf.String() } golang-github-maxatome-go-testdeep-1.14.0/td/td_map_each.go000066400000000000000000000044211454313311600235370ustar00rootroot00000000000000// Copyright (c) 2018, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td import ( "reflect" "strings" "github.com/maxatome/go-testdeep/helpers/tdutil" "github.com/maxatome/go-testdeep/internal/ctxerr" "github.com/maxatome/go-testdeep/internal/types" "github.com/maxatome/go-testdeep/internal/util" ) type tdMapEach struct { baseOKNil expected reflect.Value } var _ TestDeep = &tdMapEach{} // summary(MapEach): compares each map entry // input(MapEach): map,ptr(ptr on map) // MapEach operator has to be applied on maps. It compares each value // of data map against expectedValue. During a match, all values have // to match to succeed. // // got := map[string]string{"test": "foo", "buzz": "bar"} // td.Cmp(t, got, td.MapEach("bar")) // fails, coz "foo" ≠ "bar" // td.Cmp(t, got, td.MapEach(td.Len(3))) // succeeds as values are 3 chars long func MapEach(expectedValue any) TestDeep { return &tdMapEach{ baseOKNil: newBaseOKNil(3), expected: reflect.ValueOf(expectedValue), } } func (m *tdMapEach) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error { if !got.IsValid() { if ctx.BooleanError { return ctxerr.BooleanError } return ctx.CollectError(&ctxerr.Error{ Message: "nil value", Got: types.RawString("nil"), Expected: types.RawString("map OR *map"), }) } switch got.Kind() { case reflect.Ptr: gotElem := got.Elem() if !gotElem.IsValid() { if ctx.BooleanError { return ctxerr.BooleanError } return ctx.CollectError(ctxerr.NilPointer(got, "non-nil *map")) } if gotElem.Kind() != reflect.Map { break } got = gotElem fallthrough case reflect.Map: var err *ctxerr.Error tdutil.MapEach(got, func(k, v reflect.Value) bool { err = deepValueEqual(ctx.AddMapKey(k), v, m.expected) return err == nil }) return err } if ctx.BooleanError { return ctxerr.BooleanError } return ctx.CollectError(ctxerr.BadKind(got, "map OR *map")) } func (m *tdMapEach) String() string { const prefix = "MapEach(" content := util.ToString(m.expected) if strings.Contains(content, "\n") { return prefix + util.IndentString(content, " ") + ")" } return prefix + content + ")" } golang-github-maxatome-go-testdeep-1.14.0/td/td_map_each_test.go000066400000000000000000000055571454313311600246110ustar00rootroot00000000000000// Copyright (c) 2018, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td_test import ( "testing" "github.com/maxatome/go-testdeep/internal/test" "github.com/maxatome/go-testdeep/td" ) func TestMapEach(t *testing.T) { type MyMap map[string]int checkOKForEach(t, []any{ map[string]int{"foo": 1, "bar": 1}, &map[string]int{"foo": 1, "bar": 1}, MyMap{"foo": 1, "bar": 1}, &MyMap{"foo": 1, "bar": 1}, }, td.MapEach(1)) checkOKForEach(t, []any{ map[int]string{1: "foo", 2: "bar"}, &map[int]string{1: "foo", 2: "bar"}, }, td.MapEach(td.Len(3))) checkOKForEach(t, []any{ map[string]int{}, &map[string]int{}, MyMap{}, &MyMap{}, }, td.MapEach(1)) checkOK(t, (map[string]int)(nil), td.MapEach(1)) checkOK(t, (MyMap)(nil), td.MapEach(1)) checkError(t, (*MyMap)(nil), td.MapEach(4), expectedError{ Message: mustBe("nil pointer"), Path: mustBe("DATA"), Got: mustBe("nil *map (*td_test.MyMap type)"), Expected: mustBe("non-nil *map"), }) checkOKForEach(t, []any{ map[string]int{"foo": 20, "bar": 22, "test": 29}, &map[string]int{"foo": 20, "bar": 22, "test": 29}, MyMap{"foo": 20, "bar": 22, "test": 29}, &MyMap{"foo": 20, "bar": 22, "test": 29}, }, td.MapEach(td.Between(20, 30))) checkError(t, nil, td.MapEach(4), expectedError{ Message: mustBe("nil value"), Path: mustBe("DATA"), Got: mustBe("nil"), Expected: mustBe("map OR *map"), }) checkErrorForEach(t, []any{ map[string]int{"foo": 4, "bar": 5, "test": 4}, &map[string]int{"foo": 4, "bar": 5, "test": 4}, MyMap{"foo": 4, "bar": 5, "test": 4}, &MyMap{"foo": 4, "bar": 5, "test": 4}, }, td.MapEach(4), expectedError{ Message: mustBe("values differ"), Path: mustBe(`DATA["bar"]`), Got: mustBe("5"), Expected: mustBe("4"), }) checkError(t, 666, td.MapEach(4), expectedError{ Message: mustBe("bad kind"), Path: mustBe("DATA"), Got: mustBe("int"), Expected: mustBe("map OR *map"), }) num := 666 checkError(t, &num, td.MapEach(4), expectedError{ Message: mustBe("bad kind"), Path: mustBe("DATA"), Got: mustBe("*int"), Expected: mustBe("map OR *map"), }) checkOK(t, map[string]any{"a": nil, "b": nil, "c": nil}, td.MapEach(nil)) checkError(t, map[string]any{"a": nil, "b": nil, "c": nil, "d": 66}, td.MapEach(nil), expectedError{ Message: mustBe("values differ"), Path: mustBe(`DATA["d"]`), Got: mustBe("66"), Expected: mustBe("nil"), }) // // String test.EqualStr(t, td.MapEach(4).String(), "MapEach(4)") test.EqualStr(t, td.MapEach(td.All(1, 2)).String(), `MapEach(All(1, 2))`) } func TestMapEachTypeBehind(t *testing.T) { equalTypes(t, td.MapEach(4), nil) } golang-github-maxatome-go-testdeep-1.14.0/td/td_map_test.go000066400000000000000000000334301454313311600236200ustar00rootroot00000000000000// Copyright (c) 2018, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td_test import ( "testing" "github.com/maxatome/go-testdeep/internal/test" "github.com/maxatome/go-testdeep/td" ) func TestMap(t *testing.T) { type MyMap map[string]int // // Map checkOK(t, (map[string]int)(nil), td.Map(map[string]int{}, nil)) checkError(t, nil, td.Map(map[string]int{}, nil), expectedError{ Message: mustBe("values differ"), Path: mustBe(`DATA`), Got: mustBe("nil"), Expected: mustBe("map[string]int{}"), }) gotMap := map[string]int{"foo": 1, "bar": 2} checkOK(t, gotMap, td.Map(map[string]int{"foo": 1, "bar": 2}, nil)) checkOK(t, gotMap, td.Map(map[string]int{"foo": 1}, td.MapEntries{"bar": 2})) checkOK(t, gotMap, td.Map(map[string]int{}, td.MapEntries{"foo": 1, "bar": 2})) checkOK(t, gotMap, td.Map((map[string]int)(nil), td.MapEntries{"foo": 1, "bar": 2})) one := 1 checkOK(t, map[string]*int{"foo": nil, "bar": &one}, td.Map(map[string]*int{}, td.MapEntries{"foo": nil, "bar": &one})) checkError(t, gotMap, td.Map(map[string]int{"foo": 1, "bar": 3}, nil), expectedError{ Message: mustBe("values differ"), Path: mustBe(`DATA["bar"]`), Got: mustBe("2"), Expected: mustBe("3"), }) checkError(t, gotMap, td.Map(map[string]int{}, nil), expectedError{ Message: mustBe("comparing hash keys of %%"), Path: mustBe("DATA"), Summary: mustMatch(`^Extra 2 keys: \("bar",\s+"foo"\)\z`), }) checkError(t, gotMap, td.Map(map[string]int{"test": 2}, nil), expectedError{ Message: mustBe("comparing hash keys of %%"), Path: mustBe("DATA"), Summary: mustMatch( `^ Missing key: \("test"\)\nExtra 2 keys: \("bar",\s+"foo"\)\z`), }) checkError(t, gotMap, td.Map(map[string]int{}, td.MapEntries{"test": 2}), expectedError{ Message: mustBe("comparing hash keys of %%"), Path: mustBe("DATA"), Summary: mustMatch( `^ Missing key: \("test"\)\nExtra 2 keys: \("bar",\s+"foo"\)\z`), }) checkError(t, gotMap, td.Map(map[string]int{}, td.MapEntries{"foo": 1, "bar": 2, "test": 2}), expectedError{ Message: mustBe("comparing hash keys of %%"), Path: mustBe("DATA"), Summary: mustBe(`Missing key: ("test")`), }) checkError(t, gotMap, td.Map(MyMap{}, td.MapEntries{"foo": 1, "bar": 2}), expectedError{ Message: mustBe("type mismatch"), Path: mustBe("DATA"), Got: mustBe("map[string]int"), Expected: mustBe("td_test.MyMap"), }) // // Map type gotTypedMap := MyMap{"foo": 1, "bar": 2} checkOK(t, gotTypedMap, td.Map(MyMap{"foo": 1, "bar": 2}, nil)) checkOK(t, gotTypedMap, td.Map(MyMap{"foo": 1}, td.MapEntries{"bar": 2})) checkOK(t, gotTypedMap, td.Map(MyMap{}, td.MapEntries{"foo": 1, "bar": 2})) checkOK(t, gotTypedMap, td.Map((MyMap)(nil), td.MapEntries{"foo": 1, "bar": 2})) checkOK(t, &gotTypedMap, td.Map(&MyMap{"foo": 1, "bar": 2}, nil)) checkOK(t, &gotTypedMap, td.Map(&MyMap{"foo": 1}, td.MapEntries{"bar": 2})) checkOK(t, &gotTypedMap, td.Map(&MyMap{}, td.MapEntries{"foo": 1, "bar": 2})) checkOK(t, &gotTypedMap, td.Map((*MyMap)(nil), td.MapEntries{"foo": 1, "bar": 2})) checkError(t, gotTypedMap, td.Map(MyMap{"foo": 1, "bar": 3}, nil), expectedError{ Message: mustBe("values differ"), Path: mustBe(`DATA["bar"]`), Got: mustBe("2"), Expected: mustBe("3"), }) checkError(t, gotTypedMap, td.Map(MyMap{}, nil), expectedError{ Message: mustBe("comparing hash keys of %%"), Path: mustBe("DATA"), Summary: mustMatch(`^Extra 2 keys: \("bar",\s+"foo"\)\z`), }) checkError(t, gotTypedMap, td.Map(MyMap{"test": 2}, nil), expectedError{ Message: mustBe("comparing hash keys of %%"), Path: mustBe("DATA"), Summary: mustMatch( `^ Missing key: \("test"\)\nExtra 2 keys: \("bar",\s+"foo"\)\z`), }) checkError(t, gotTypedMap, td.Map(MyMap{}, td.MapEntries{"test": 2}), expectedError{ Message: mustBe("comparing hash keys of %%"), Path: mustBe("DATA"), Summary: mustMatch( `^ Missing key: \("test"\)\nExtra 2 keys: \("bar",\s+"foo"\)\z`), }) checkError(t, gotTypedMap, td.Map(MyMap{}, td.MapEntries{"foo": 1, "bar": 2, "test": 2}), expectedError{ Message: mustBe("comparing hash keys of %%"), Path: mustBe("DATA"), Summary: mustBe(`Missing key: ("test")`), }) checkError(t, &gotTypedMap, td.Map(&MyMap{"foo": 1, "bar": 3}, nil), expectedError{ Message: mustBe("values differ"), Path: mustBe(`DATA["bar"]`), Got: mustBe("2"), Expected: mustBe("3"), }) checkError(t, &gotTypedMap, td.Map(&MyMap{}, nil), expectedError{ Message: mustBe("comparing hash keys of %%"), Path: mustBe("DATA"), Summary: mustMatch(`^Extra 2 keys: \("bar",\s+"foo"\)\z`), }) checkError(t, &gotTypedMap, td.Map(&MyMap{"test": 2}, nil), expectedError{ Message: mustBe("comparing hash keys of %%"), Path: mustBe("DATA"), Summary: mustMatch( `^ Missing key: \("test"\)\nExtra 2 keys: \("bar",\s+"foo"\)\z`), }) checkError(t, &gotTypedMap, td.Map(&MyMap{}, td.MapEntries{"test": 2}), expectedError{ Message: mustBe("comparing hash keys of %%"), Path: mustBe("DATA"), Summary: mustMatch( `^ Missing key: \("test"\)\nExtra 2 keys: \("bar",\s+"foo"\)\z`), }) checkError(t, &gotTypedMap, td.Map(&MyMap{}, td.MapEntries{"foo": 1, "bar": 2, "test": 2}), expectedError{ Message: mustBe("comparing hash keys of %%"), Path: mustBe("DATA"), Summary: mustBe(`Missing key: ("test")`), }) checkError(t, &gotMap, td.Map(&MyMap{}, nil), expectedError{ Message: mustBe("type mismatch"), Path: mustBe("DATA"), Got: mustBe("*map[string]int"), Expected: mustBe("*td_test.MyMap"), }) checkError(t, gotMap, td.Map(&MyMap{}, nil), expectedError{ Message: mustBe("type mismatch"), Path: mustBe("DATA"), Got: mustBe("map[string]int"), Expected: mustBe("*td_test.MyMap"), }) checkError(t, nil, td.Map(&MyMap{}, nil), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA"), Got: mustBe("nil"), Expected: mustBe("*td_test.MyMap{}"), }) checkError(t, nil, td.Map(MyMap{}, nil), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA"), Got: mustBe("nil"), Expected: mustBe("td_test.MyMap{}"), }) // // nil cases var ( gotNilMap map[string]int gotNilTypedMap MyMap ) checkOK(t, gotNilMap, td.Map(map[string]int{}, nil)) checkOK(t, gotNilTypedMap, td.Map(MyMap{}, nil)) checkOK(t, &gotNilTypedMap, td.Map(&MyMap{}, nil)) // Be lax... // Without Lax → error checkError(t, MyMap{}, td.Map(map[string]int{}, nil), expectedError{ Message: mustBe("type mismatch"), }) checkError(t, map[string]int{}, td.Map(MyMap{}, nil), expectedError{ Message: mustBe("type mismatch"), }) // With Lax → OK checkOK(t, MyMap{}, td.Lax(td.Map(map[string]int{}, nil))) checkOK(t, map[string]int{}, td.Lax(td.Map(MyMap{}, nil))) // // SuperMapOf checkOK(t, gotMap, td.SuperMapOf(map[string]int{"foo": 1}, nil)) checkOK(t, gotMap, td.SuperMapOf(map[string]int{"foo": 1}, td.MapEntries{"bar": 2})) checkOK(t, gotMap, td.SuperMapOf(map[string]int{}, td.MapEntries{"foo": 1, "bar": 2})) checkError(t, gotMap, td.SuperMapOf(map[string]int{"foo": 1, "bar": 3}, nil), expectedError{ Message: mustBe("values differ"), Path: mustBe(`DATA["bar"]`), Got: mustBe("2"), Expected: mustBe("3"), }) checkError(t, gotMap, td.SuperMapOf(map[string]int{"test": 2}, nil), expectedError{ Message: mustBe("comparing hash keys of %%"), Path: mustBe("DATA"), Summary: mustBe(`Missing key: ("test")`), }) checkError(t, gotMap, td.SuperMapOf(map[string]int{}, td.MapEntries{"test": 2}), expectedError{ Message: mustBe("comparing hash keys of %%"), Path: mustBe("DATA"), Summary: mustBe(`Missing key: ("test")`), }) checkOK(t, gotNilMap, td.SuperMapOf(map[string]int{}, nil)) checkOK(t, gotNilTypedMap, td.SuperMapOf(MyMap{}, nil)) checkOK(t, &gotNilTypedMap, td.SuperMapOf(&MyMap{}, nil)) // // SubMapOf checkOK(t, gotMap, td.SubMapOf(map[string]int{"foo": 1, "bar": 2, "tst": 3}, nil)) checkOK(t, gotMap, td.SubMapOf(map[string]int{"foo": 1, "tst": 3}, td.MapEntries{"bar": 2})) checkOK(t, gotMap, td.SubMapOf(map[string]int{}, td.MapEntries{"foo": 1, "bar": 2, "tst": 3})) checkError(t, gotMap, td.SubMapOf(map[string]int{"foo": 1, "bar": 3}, nil), expectedError{ Message: mustBe("values differ"), Path: mustBe(`DATA["bar"]`), Got: mustBe("2"), Expected: mustBe("3"), }) checkError(t, gotMap, td.SubMapOf(map[string]int{"foo": 1}, nil), expectedError{ Message: mustBe("comparing hash keys of %%"), Path: mustBe("DATA"), Summary: mustBe(`Extra key: ("bar")`), }) checkError(t, gotMap, td.SubMapOf(map[string]int{}, td.MapEntries{"foo": 1, "test": 2}), expectedError{ Message: mustBe("comparing hash keys of %%"), Path: mustBe("DATA"), Summary: mustBe(`Missing key: ("test") Extra key: ("bar")`), }) checkOK(t, gotNilMap, td.SubMapOf(map[string]int{"foo": 1}, nil)) checkOK(t, gotNilTypedMap, td.SubMapOf(MyMap{"foo": 1}, nil)) checkOK(t, &gotNilTypedMap, td.SubMapOf(&MyMap{"foo": 1}, nil)) // // Bad usage checkError(t, "never tested", td.Map("test", nil), expectedError{ Message: mustBe("bad usage of Map operator"), Path: mustBe("DATA"), Summary: mustContain("usage: Map("), }) checkError(t, "never tested", td.SuperMapOf("test", nil), expectedError{ Message: mustBe("bad usage of SuperMapOf operator"), Path: mustBe("DATA"), Summary: mustContain("usage: SuperMapOf("), }) checkError(t, "never tested", td.SubMapOf("test", nil), expectedError{ Message: mustBe("bad usage of SubMapOf operator"), Path: mustBe("DATA"), Summary: mustContain("usage: SubMapOf("), }) num := 12 checkError(t, "never tested", td.Map(&num, nil), expectedError{ Message: mustBe("bad usage of Map operator"), Path: mustBe("DATA"), Summary: mustContain("usage: Map("), }) checkError(t, "never tested", td.SuperMapOf(&num, nil), expectedError{ Message: mustBe("bad usage of SuperMapOf operator"), Path: mustBe("DATA"), Summary: mustContain("usage: SuperMapOf("), }) checkError(t, "never tested", td.SubMapOf(&num, nil), expectedError{ Message: mustBe("bad usage of SubMapOf operator"), Path: mustBe("DATA"), Summary: mustContain("usage: SubMapOf("), }) checkError(t, "never tested", td.Map(&MyMap{}, td.MapEntries{1: 2}), expectedError{ Message: mustBe("bad usage of Map operator"), Path: mustBe("DATA"), Summary: mustBe("expected key 1 type mismatch: int != model key type (string)"), }) checkError(t, "never tested", td.SuperMapOf(&MyMap{}, td.MapEntries{1: 2}), expectedError{ Message: mustBe("bad usage of SuperMapOf operator"), Path: mustBe("DATA"), Summary: mustBe("expected key 1 type mismatch: int != model key type (string)"), }) checkError(t, "never tested", td.SubMapOf(&MyMap{}, td.MapEntries{1: 2}), expectedError{ Message: mustBe("bad usage of SubMapOf operator"), Path: mustBe("DATA"), Summary: mustBe("expected key 1 type mismatch: int != model key type (string)"), }) checkError(t, "never tested", td.Map(&MyMap{}, td.MapEntries{"foo": nil}), expectedError{ Message: mustBe("bad usage of Map operator"), Path: mustBe("DATA"), Summary: mustBe(`expected key "foo" value cannot be nil as entries value type is int`), }) checkError(t, "never tested", td.Map(&MyMap{}, td.MapEntries{"foo": uint16(2)}), expectedError{ Message: mustBe("bad usage of Map operator"), Path: mustBe("DATA"), Summary: mustBe(`expected key "foo" value type mismatch: uint16 != model key type (int)`), }) checkError(t, "never tested", td.Map(&MyMap{"foo": 1}, td.MapEntries{"foo": 1}), expectedError{ Message: mustBe("bad usage of Map operator"), Path: mustBe("DATA"), Summary: mustBe(`"foo" entry exists in both model & expectedEntries`), }) // // String test.EqualStr(t, td.Map(MyMap{}, nil).String(), "td_test.MyMap{}") test.EqualStr(t, td.Map(&MyMap{}, nil).String(), "*td_test.MyMap{}") test.EqualStr(t, td.Map(&MyMap{"foo": 2}, nil).String(), `*td_test.MyMap{ "foo": 2, }`) test.EqualStr(t, td.SubMapOf(MyMap{}, nil).String(), "SubMapOf(td_test.MyMap{})") test.EqualStr(t, td.SubMapOf(&MyMap{}, nil).String(), "SubMapOf(*td_test.MyMap{})") test.EqualStr(t, td.SubMapOf(&MyMap{"foo": 2}, nil).String(), `SubMapOf(*td_test.MyMap{ "foo": 2, })`) test.EqualStr(t, td.SuperMapOf(MyMap{}, nil).String(), "SuperMapOf(td_test.MyMap{})") test.EqualStr(t, td.SuperMapOf(&MyMap{}, nil).String(), "SuperMapOf(*td_test.MyMap{})") test.EqualStr(t, td.SuperMapOf(&MyMap{"foo": 2}, nil).String(), `SuperMapOf(*td_test.MyMap{ "foo": 2, })`) // Erroneous op test.EqualStr(t, td.Map(12, nil).String(), "Map()") test.EqualStr(t, td.SubMapOf(12, nil).String(), "SubMapOf()") test.EqualStr(t, td.SuperMapOf(12, nil).String(), "SuperMapOf()") } func TestMapTypeBehind(t *testing.T) { type MyMap map[string]int // Map equalTypes(t, td.Map(map[string]int{}, nil), map[string]int{}) equalTypes(t, td.Map(MyMap{}, nil), MyMap{}) equalTypes(t, td.Map(&MyMap{}, nil), &MyMap{}) // SubMap equalTypes(t, td.SubMapOf(map[string]int{}, nil), map[string]int{}) equalTypes(t, td.SubMapOf(MyMap{}, nil), MyMap{}) equalTypes(t, td.SubMapOf(&MyMap{}, nil), &MyMap{}) // SuperMap equalTypes(t, td.SuperMapOf(map[string]int{}, nil), map[string]int{}) equalTypes(t, td.SuperMapOf(MyMap{}, nil), MyMap{}) equalTypes(t, td.SuperMapOf(&MyMap{}, nil), &MyMap{}) // Erroneous op equalTypes(t, td.Map(12, nil), nil) equalTypes(t, td.SubMapOf(12, nil), nil) equalTypes(t, td.SuperMapOf(12, nil), nil) } golang-github-maxatome-go-testdeep-1.14.0/td/td_nan.go000066400000000000000000000043571454313311600225660ustar00rootroot00000000000000// Copyright (c) 2018, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td import ( "math" "reflect" "github.com/maxatome/go-testdeep/internal/ctxerr" "github.com/maxatome/go-testdeep/internal/types" ) type tdNaN struct { base } var _ TestDeep = &tdNaN{} // summary(NaN): checks a floating number is [`math.NaN`] // input(NaN): float // NaN operator checks that data is a float and is not-a-number. // // got := math.NaN() // td.Cmp(t, got, td.NaN()) // succeeds // td.Cmp(t, 4.2, td.NaN()) // fails // // See also [NotNaN]. func NaN() TestDeep { return &tdNaN{ base: newBase(3), } } func (n *tdNaN) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error { switch got.Kind() { case reflect.Float32, reflect.Float64: if math.IsNaN(got.Float()) { return nil } return ctx.CollectError(&ctxerr.Error{ Message: "values differ", Got: got, Expected: n, }) } return ctx.CollectError(&ctxerr.Error{ Message: "type mismatch", Got: types.RawString(got.Type().String()), Expected: types.RawString("float32 OR float64"), }) } func (n *tdNaN) String() string { return "NaN" } type tdNotNaN struct { base } var _ TestDeep = &tdNotNaN{} // summary(NotNaN): checks a floating number is not [`math.NaN`] // input(NotNaN): float // NotNaN operator checks that data is a float and is not not-a-number. // // got := math.NaN() // td.Cmp(t, got, td.NotNaN()) // fails // td.Cmp(t, 4.2, td.NotNaN()) // succeeds // td.Cmp(t, 4, td.NotNaN()) // fails, as 4 is not a float // // See also [NaN]. func NotNaN() TestDeep { return &tdNotNaN{ base: newBase(3), } } func (n *tdNotNaN) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error { switch got.Kind() { case reflect.Float32, reflect.Float64: if !math.IsNaN(got.Float()) { return nil } return ctx.CollectError(&ctxerr.Error{ Message: "values differ", Got: got, Expected: n, }) } return ctx.CollectError(&ctxerr.Error{ Message: "type mismatch", Got: types.RawString(got.Type().String()), Expected: types.RawString("float32 OR float64"), }) } func (n *tdNotNaN) String() string { return "not NaN" } golang-github-maxatome-go-testdeep-1.14.0/td/td_nan_test.go000066400000000000000000000033531454313311600236200ustar00rootroot00000000000000// Copyright (c) 2018-2022, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td_test import ( "math" "testing" "github.com/maxatome/go-testdeep/td" ) func TestNaN(t *testing.T) { checkOK(t, math.NaN(), td.NaN()) checkOK(t, float32(math.NaN()), td.NaN()) checkError(t, float32(12), td.NaN(), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA"), Got: mustBe("(float32) 12"), Expected: mustBe("NaN"), }) checkError(t, float64(12), td.NaN(), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA"), Got: mustBe("12.0"), Expected: mustBe("NaN"), }) checkError(t, 12, td.NaN(), expectedError{ Message: mustBe("type mismatch"), Path: mustBe("DATA"), Got: mustBe("int"), Expected: mustBe("float32 OR float64"), }) } func TestNotNaN(t *testing.T) { checkOK(t, float64(12), td.NotNaN()) checkOK(t, float32(12), td.NotNaN()) checkError(t, float32(math.NaN()), td.NotNaN(), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA"), Got: mustBe("(float32) NaN"), Expected: mustBe("not NaN"), }) checkError(t, math.NaN(), td.NotNaN(), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA"), Got: mustBe("NaN"), Expected: mustBe("not NaN"), }) checkError(t, 12, td.NotNaN(), expectedError{ Message: mustBe("type mismatch"), Path: mustBe("DATA"), Got: mustBe("int"), Expected: mustBe("float32 OR float64"), }) } func TestNaNTypeBehind(t *testing.T) { equalTypes(t, td.NaN(), nil) equalTypes(t, td.NotNaN(), nil) } golang-github-maxatome-go-testdeep-1.14.0/td/td_nil.go000066400000000000000000000054441454313311600225720ustar00rootroot00000000000000// Copyright (c) 2018, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td import ( "reflect" "github.com/maxatome/go-testdeep/internal/ctxerr" ) type tdNil struct { baseOKNil } var _ TestDeep = &tdNil{} // summary(Nil): compares to nil // input(Nil): nil,slice,map,ptr,chan,func // Nil operator checks that data is nil (or is a non-nil interface, // but containing a nil pointer.) // // var got *int // td.Cmp(t, got, td.Nil()) // succeeds // td.Cmp(t, got, nil) // fails as (*int)(nil) ≠ untyped nil // td.Cmp(t, got, (*int)(nil)) // succeeds // // but: // // var got fmt.Stringer = (*bytes.Buffer)(nil) // td.Cmp(t, got, td.Nil()) // succeeds // td.Cmp(t, got, nil) // fails, as the interface is not nil // got = nil // td.Cmp(t, got, nil) // succeeds // // See also [Empty], [NotNil] and [Zero]. func Nil() TestDeep { return &tdNil{ baseOKNil: newBaseOKNil(3), } } func (n *tdNil) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error { if !got.IsValid() { return nil } switch got.Kind() { case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice: if got.IsNil() { return nil } } if ctx.BooleanError { return ctxerr.BooleanError } return ctx.CollectError(&ctxerr.Error{ Message: "non-nil", Got: got, Expected: n, }) } func (n *tdNil) String() string { return "nil" } type tdNotNil struct { baseOKNil } var _ TestDeep = &tdNotNil{} // summary(NotNil): checks that data is not nil // input(NotNil): nil,slice,map,ptr,chan,func // NotNil operator checks that data is not nil (or is a non-nil // interface, containing a non-nil pointer.) // // got := &Person{} // td.Cmp(t, got, td.NotNil()) // succeeds // td.Cmp(t, got, td.Not(nil)) // succeeds too, but be careful it is first // // because of got type *Person ≠ untyped nil so prefer NotNil() // // but: // // var got fmt.Stringer = (*bytes.Buffer)(nil) // td.Cmp(t, got, td.NotNil()) // fails // td.Cmp(t, got, td.Not(nil)) // succeeds, as the interface is not nil // // See also [Nil], [NotEmpty] and [NotZero]. func NotNil() TestDeep { return &tdNotNil{ baseOKNil: newBaseOKNil(3), } } func (n *tdNotNil) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error { if got.IsValid() { switch got.Kind() { case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice: if !got.IsNil() { return nil } // All other kinds are non-nil by nature default: return nil } } if ctx.BooleanError { return ctxerr.BooleanError } return ctx.CollectError(&ctxerr.Error{ Message: "nil value", Got: got, Expected: n, }) } func (n *tdNotNil) String() string { return "not nil" } golang-github-maxatome-go-testdeep-1.14.0/td/td_nil_test.go000066400000000000000000000055711454313311600236320ustar00rootroot00000000000000// Copyright (c) 2018, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td_test import ( "bytes" "fmt" "testing" "github.com/maxatome/go-testdeep/internal/test" "github.com/maxatome/go-testdeep/td" ) func TestNil(t *testing.T) { checkOK(t, (func())(nil), td.Nil()) checkOK(t, ([]int)(nil), td.Nil()) checkOK(t, (map[bool]bool)(nil), td.Nil()) checkOK(t, (*int)(nil), td.Nil()) checkOK(t, (chan int)(nil), td.Nil()) checkOK(t, nil, td.Nil()) checkOK(t, map[string]any{"foo": nil}, map[string]any{"foo": td.Nil()}, ) var got fmt.Stringer = (*bytes.Buffer)(nil) checkOK(t, got, td.Nil()) checkError(t, 42, td.Nil(), expectedError{ Message: mustBe("non-nil"), Path: mustBe("DATA"), Got: mustBe("42"), Expected: mustBe("nil"), }) num := 42 checkError(t, &num, td.Nil(), expectedError{ Message: mustBe("non-nil"), Path: mustBe("DATA"), Got: mustMatch(`\(\*int\).*42`), Expected: mustBe("nil"), }) // // String test.EqualStr(t, td.Nil().String(), "nil") } func TestNotNil(t *testing.T) { num := 42 checkOK(t, func() {}, td.NotNil()) checkOK(t, []int{}, td.NotNil()) checkOK(t, map[bool]bool{}, td.NotNil()) checkOK(t, &num, td.NotNil()) checkOK(t, 42, td.NotNil()) checkError(t, (func())(nil), td.NotNil(), expectedError{ Message: mustBe("nil value"), Path: mustBe("DATA"), Got: mustContain("nil"), Expected: mustBe("not nil"), }) checkError(t, ([]int)(nil), td.NotNil(), expectedError{ Message: mustBe("nil value"), Path: mustBe("DATA"), Got: mustContain("nil"), Expected: mustBe("not nil"), }) checkError(t, (map[bool]bool)(nil), td.NotNil(), expectedError{ Message: mustBe("nil value"), Path: mustBe("DATA"), Got: mustContain("nil"), Expected: mustBe("not nil"), }) checkError(t, (*int)(nil), td.NotNil(), expectedError{ Message: mustBe("nil value"), Path: mustBe("DATA"), Got: mustContain("nil"), Expected: mustBe("not nil"), }) checkError(t, (chan int)(nil), td.NotNil(), expectedError{ Message: mustBe("nil value"), Path: mustBe("DATA"), Got: mustContain("nil"), Expected: mustBe("not nil"), }) checkError(t, nil, td.NotNil(), expectedError{ Message: mustBe("nil value"), Path: mustBe("DATA"), Got: mustBe("nil"), Expected: mustBe("not nil"), }) var got fmt.Stringer = (*bytes.Buffer)(nil) checkError(t, got, td.NotNil(), expectedError{ Message: mustBe("nil value"), Path: mustBe("DATA"), Got: mustContain(""), Expected: mustBe("not nil"), }) // // String test.EqualStr(t, td.NotNil().String(), "not nil") } func TestNilTypeBehind(t *testing.T) { equalTypes(t, td.Nil(), nil) equalTypes(t, td.NotNil(), nil) } golang-github-maxatome-go-testdeep-1.14.0/td/td_none.go000066400000000000000000000041411454313311600227400ustar00rootroot00000000000000// Copyright (c) 2018, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td import ( "fmt" "reflect" "github.com/maxatome/go-testdeep/internal/ctxerr" ) type tdNone struct { tdList } var _ TestDeep = &tdNone{} // summary(None): no values have to match // input(None): all // None operator compares data against several not expected // values. During a match, none of them have to match to succeed. // // td.Cmp(t, 12, td.None(8, 10, 14)) // succeeds // td.Cmp(t, 12, td.None(8, 10, 12, 14)) // fails // // Note [Flatten] function can be used to group or reuse some values or // operators and so avoid boring and inefficient copies: // // prime := td.Flatten([]int{1, 2, 3, 5, 7, 11, 13}) // even := td.Flatten([]int{2, 4, 6, 8, 10, 12, 14}) // td.Cmp(t, 9, td.None(prime, even)) // succeeds // // See also [All], [Any] and [Not]. func None(notExpectedValues ...any) TestDeep { return &tdNone{ tdList: newList(notExpectedValues...), } } // summary(Not): value must not match // input(Not): all // Not operator compares data against the not expected value. During a // match, it must not match to succeed. // // Not is the same operator as [None] with only one argument. It is // provided as a more readable function when only one argument is // needed. // // td.Cmp(t, 12, td.Not(10)) // succeeds // td.Cmp(t, 12, td.Not(12)) // fails // // See also [None]. func Not(notExpected any) TestDeep { return &tdNone{ tdList: newList(notExpected), } } func (n *tdNone) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error { for idx, item := range n.items { if deepValueEqualFinalOK(ctx, got, item) { if ctx.BooleanError { return ctxerr.BooleanError } var mesg string if n.GetLocation().Func == "Not" { mesg = "comparing with Not" } else { mesg = fmt.Sprintf("comparing with None (part %d of %d is OK)", idx+1, len(n.items)) } return ctx.CollectError(&ctxerr.Error{ Message: mesg, Got: got, Expected: n, }) } } return nil } golang-github-maxatome-go-testdeep-1.14.0/td/td_none_test.go000066400000000000000000000035201454313311600237770ustar00rootroot00000000000000// Copyright (c) 2018-2022, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td_test import ( "testing" "github.com/maxatome/go-testdeep/internal/test" "github.com/maxatome/go-testdeep/td" ) func TestNone(t *testing.T) { checkOK(t, 6, td.None(7, 8, 9, nil)) checkOK(t, nil, td.None(7, 8, 9)) checkError(t, 6, td.None(6, 7), expectedError{ Message: mustBe("comparing with None (part 1 of 2 is OK)"), Path: mustBe("DATA"), Got: mustBe("6"), Expected: mustBe("None(6,\n 7)"), }) checkError(t, nil, td.None(7, nil), expectedError{ Message: mustBe("comparing with None (part 2 of 2 is OK)"), Path: mustBe("DATA"), Got: mustBe("nil"), Expected: mustBe("None(7,\n nil)"), }) // Lax checkError(t, float64(6), td.Lax(td.None(6, 7)), expectedError{ Message: mustBe("comparing with None (part 1 of 2 is OK)"), Path: mustBe("DATA"), Got: mustBe("6.0"), Expected: mustBe("None(6,\n 7)"), }) // // String test.EqualStr(t, td.None(6).String(), "None(6)") test.EqualStr(t, td.None(6, 7).String(), "None(6,\n 7)") } func TestNot(t *testing.T) { checkOK(t, 6, td.Not(7)) checkOK(t, nil, td.Not(7)) checkError(t, 6, td.Not(6), expectedError{ Message: mustBe("comparing with Not"), Path: mustBe("DATA"), Got: mustBe("6"), Expected: mustBe("Not(6)"), }) checkError(t, nil, td.Not(nil), expectedError{ Message: mustBe("comparing with Not"), Path: mustBe("DATA"), Got: mustBe("nil"), Expected: mustBe("Not(nil)"), }) // // String test.EqualStr(t, td.Not(6).String(), "Not(6)") } func TestNoneTypeBehind(t *testing.T) { equalTypes(t, td.None(6), nil) equalTypes(t, td.Not(6), nil) } golang-github-maxatome-go-testdeep-1.14.0/td/td_ptr.go000066400000000000000000000117641454313311600226170ustar00rootroot00000000000000// Copyright (c) 2018, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td import ( "reflect" "github.com/maxatome/go-testdeep/internal/ctxerr" "github.com/maxatome/go-testdeep/internal/types" ) type tdPtr struct { tdSmugglerBase } var _ TestDeep = &tdPtr{} // summary(Ptr): allows to easily test a pointer value // input(Ptr): ptr // Ptr is a smuggler operator. It takes the address of data and // compares it to val. // // val depends on data type. For example, if the compared data is an // *int, one can have: // // num := 12 // td.Cmp(t, &num, td.Ptr(12)) // succeeds // // as well as an other operator: // // num := 3 // td.Cmp(t, &num, td.Ptr(td.Between(3, 4))) // // TypeBehind method returns the [reflect.Type] of a pointer on val, // except if val is a [TestDeep] operator. In this case, it delegates // TypeBehind() to the operator and returns the [reflect.Type] of a // pointer on the returned value (if non-nil of course). // // See also [PPtr] and [Shallow]. func Ptr(val any) TestDeep { p := tdPtr{ tdSmugglerBase: newSmugglerBase(val), } vval := reflect.ValueOf(val) if !vval.IsValid() { p.err = ctxerr.OpBadUsage("Ptr", "(NON_NIL_VALUE)", val, 1, true) return &p } if !p.isTestDeeper { p.expectedValue = reflect.New(vval.Type()) p.expectedValue.Elem().Set(vval) } return &p } func (p *tdPtr) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error { if p.err != nil { return ctx.CollectError(p.err) } if got.Kind() != reflect.Ptr { if ctx.BooleanError { return ctxerr.BooleanError } return ctx.CollectError(&ctxerr.Error{ Message: "pointer type mismatch", Got: types.RawString(got.Type().String()), Expected: types.RawString(p.String()), }) } if p.isTestDeeper { return deepValueEqual(ctx.AddPtr(1), got.Elem(), p.expectedValue) } return deepValueEqual(ctx, got, p.expectedValue) } func (p *tdPtr) String() string { if p.err != nil { return p.stringError() } if p.isTestDeeper { return "*" } return p.expectedValue.Type().String() } func (p *tdPtr) TypeBehind() reflect.Type { if p.err != nil { return nil } // If the expected value is a TestDeep operator, delegate TypeBehind to it if p.isTestDeeper { typ := p.expectedValue.Interface().(TestDeep).TypeBehind() if typ == nil { return nil } // Add a level of pointer return reflect.New(typ).Type() } return p.expectedValue.Type() } type tdPPtr struct { tdSmugglerBase } var _ TestDeep = &tdPPtr{} // summary(PPtr): allows to easily test a pointer of pointer value // input(PPtr): ptr // PPtr is a smuggler operator. It takes the address of the address of // data and compares it to val. // // val depends on data type. For example, if the compared data is an // **int, one can have: // // num := 12 // pnum = &num // td.Cmp(t, &pnum, td.PPtr(12)) // succeeds // // as well as an other operator: // // num := 3 // pnum = &num // td.Cmp(t, &pnum, td.PPtr(td.Between(3, 4))) // succeeds // // It is more efficient and shorter to write than: // // td.Cmp(t, &pnum, td.Ptr(td.Ptr(val))) // succeeds too // // TypeBehind method returns the [reflect.Type] of a pointer on a // pointer on val, except if val is a [TestDeep] operator. In this // case, it delegates TypeBehind() to the operator and returns the // [reflect.Type] of a pointer on a pointer on the returned value (if // non-nil of course). // // See also [Ptr]. func PPtr(val any) TestDeep { p := tdPPtr{ tdSmugglerBase: newSmugglerBase(val), } vval := reflect.ValueOf(val) if !vval.IsValid() { p.err = ctxerr.OpBadUsage("PPtr", "(NON_NIL_VALUE)", val, 1, true) return &p } if !p.isTestDeeper { pVval := reflect.New(vval.Type()) pVval.Elem().Set(vval) p.expectedValue = reflect.New(pVval.Type()) p.expectedValue.Elem().Set(pVval) } return &p } func (p *tdPPtr) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error { if p.err != nil { return ctx.CollectError(p.err) } if got.Kind() != reflect.Ptr || got.Elem().Kind() != reflect.Ptr { if ctx.BooleanError { return ctxerr.BooleanError } return ctx.CollectError(&ctxerr.Error{ Message: "pointer type mismatch", Got: types.RawString(got.Type().String()), Expected: types.RawString(p.String()), }) } if p.isTestDeeper { return deepValueEqual(ctx.AddPtr(2), got.Elem().Elem(), p.expectedValue) } return deepValueEqual(ctx, got, p.expectedValue) } func (p *tdPPtr) String() string { if p.err != nil { return p.stringError() } if p.isTestDeeper { return "**" } return p.expectedValue.Type().String() } func (p *tdPPtr) TypeBehind() reflect.Type { if p.err != nil { return nil } // If the expected value is a TestDeep operator, delegate TypeBehind to it if p.isTestDeeper { typ := p.expectedValue.Interface().(TestDeep).TypeBehind() if typ == nil { return nil } // Add 2 levels of pointer return reflect.New(reflect.New(typ).Type()).Type() } return p.expectedValue.Type() } golang-github-maxatome-go-testdeep-1.14.0/td/td_ptr_test.go000066400000000000000000000134461454313311600236550ustar00rootroot00000000000000// Copyright (c) 2018, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td_test import ( "testing" "github.com/maxatome/go-testdeep/internal/test" "github.com/maxatome/go-testdeep/td" ) func TestPtr(t *testing.T) { // // Ptr num := 12 str := "test" pNum := &num pStr := &str pStruct := &struct{}{} checkOK(t, &num, td.Ptr(12)) checkOK(t, &str, td.Ptr("test")) checkOK(t, &struct{}{}, td.Ptr(struct{}{})) checkError(t, &num, td.Ptr(13), expectedError{ Message: mustBe("values differ"), Path: mustBe("*DATA"), Got: mustBe("12"), Expected: mustBe("13"), }) checkError(t, nil, td.Ptr(13), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA"), Got: mustBe("nil"), Expected: mustBe("*int"), }) checkError(t, (*int)(nil), td.Ptr(13), expectedError{ Message: mustBe("values differ"), Path: mustBe("*DATA"), // should be DATA, but seems hard to be done Got: mustBe("nil"), Expected: mustBe("13"), }) checkError(t, (*int)(nil), td.Ptr((*int)(nil)), expectedError{ Message: mustBe("type mismatch"), Path: mustBe("DATA"), Got: mustBe("*int"), Expected: mustBe("**int"), }) checkError(t, &num, td.Ptr("test"), expectedError{ Message: mustBe("type mismatch"), Path: mustBe("DATA"), Got: mustBe("*int"), Expected: mustBe("*string"), }) checkError(t, &num, td.Ptr(td.Any(11)), expectedError{ Message: mustBe("comparing with Any"), Path: mustBe("*DATA"), Got: mustBe("12"), Expected: mustBe("Any(11)"), }) checkError(t, &str, td.Ptr("foobar"), expectedError{ Message: mustBe("values differ"), Path: mustBe("*DATA"), Got: mustContain(`"test"`), Expected: mustContain(`"foobar"`), }) checkError(t, 13, td.Ptr(13), expectedError{ Message: mustBe("pointer type mismatch"), Path: mustBe("DATA"), Got: mustBe("int"), Expected: mustBe("*int"), }) checkError(t, &str, td.Ptr(12), expectedError{ Message: mustBe("type mismatch"), Path: mustBe("DATA"), Got: mustBe("*string"), Expected: mustBe("*int"), }) checkError(t, &pNum, td.Ptr(12), expectedError{ Message: mustBe("type mismatch"), Path: mustBe("DATA"), Got: mustBe("**int"), Expected: mustBe("*int"), }) checkError(t, &pStr, td.Ptr("test"), expectedError{ Message: mustBe("type mismatch"), Path: mustBe("DATA"), Got: mustBe("**string"), Expected: mustBe("*string"), }) // // PPtr checkOK(t, &pNum, td.PPtr(12)) checkOK(t, &pStr, td.PPtr("test")) checkOK(t, &pStruct, td.PPtr(struct{}{})) checkError(t, &pNum, td.PPtr(13), expectedError{ Message: mustBe("values differ"), Path: mustBe("**DATA"), Got: mustBe("12"), Expected: mustBe("13"), }) checkError(t, nil, td.PPtr(13), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA"), Got: mustBe("nil"), Expected: mustBe("**int"), }) checkError(t, &num, td.PPtr(13), expectedError{ Message: mustBe("pointer type mismatch"), Path: mustBe("DATA"), Got: mustBe("*int"), Expected: mustBe("**int"), }) checkError(t, &pStr, td.PPtr("foobar"), expectedError{ Message: mustBe("values differ"), Path: mustBe("**DATA"), }) checkError(t, &str, td.PPtr("foobar"), expectedError{ Message: mustBe("pointer type mismatch"), Path: mustBe("DATA"), Got: mustBe("*string"), Expected: mustBe("**string"), }) checkError(t, &pNum, td.PPtr(td.Any(11)), expectedError{ Message: mustBe("comparing with Any"), Path: mustBe("**DATA"), Got: mustBe("12"), Expected: mustBe("Any(11)"), }) pStruct = nil checkError(t, &pStruct, td.PPtr(struct{}{}), expectedError{ Message: mustBe("values differ"), Path: mustBe("**DATA"), // should be *DATA, but seems hard to be done Got: mustBe("nil"), Expected: mustContain("struct"), }) // // Bad usage checkError(t, "never tested", td.Ptr(nil), expectedError{ Message: mustBe("bad usage of Ptr operator"), Path: mustBe("DATA"), Summary: mustContain("usage: Ptr("), }) checkError(t, "never tested", td.Ptr(MyInterface(nil)), expectedError{ Message: mustBe("bad usage of Ptr operator"), Path: mustBe("DATA"), Summary: mustContain("usage: Ptr("), }) checkError(t, "never tested", td.PPtr(nil), expectedError{ Message: mustBe("bad usage of PPtr operator"), Path: mustBe("DATA"), Summary: mustContain("usage: PPtr("), }) checkError(t, "never tested", td.PPtr(MyInterface(nil)), expectedError{ Message: mustBe("bad usage of PPtr operator"), Path: mustBe("DATA"), Summary: mustContain("usage: PPtr("), }) // // String test.EqualStr(t, td.Ptr(13).String(), "*int") test.EqualStr(t, td.PPtr(13).String(), "**int") test.EqualStr(t, td.Ptr(td.Ptr(13)).String(), "*") test.EqualStr(t, td.PPtr(td.Ptr(13)).String(), "**") // Erroneous op test.EqualStr(t, td.Ptr(nil).String(), "Ptr()") test.EqualStr(t, td.PPtr(nil).String(), "PPtr()") } func TestPtrTypeBehind(t *testing.T) { var num int equalTypes(t, td.Ptr(6), &num) // Another TestDeep operator delegation var num64 int64 equalTypes(t, td.Ptr(td.Between(int64(1), int64(2))), &num64) equalTypes(t, td.Ptr(td.Any(1, 1.2)), nil) // Erroneous op equalTypes(t, td.Ptr(nil), nil) } func TestPPtrTypeBehind(t *testing.T) { var pnum *int equalTypes(t, td.PPtr(6), &pnum) // Another TestDeep operator delegation var pnum64 *int64 equalTypes(t, td.PPtr(td.Between(int64(1), int64(2))), &pnum64) equalTypes(t, td.PPtr(td.Any(1, 1.2)), nil) // Erroneous op equalTypes(t, td.PPtr(nil), nil) } golang-github-maxatome-go-testdeep-1.14.0/td/td_re.go000066400000000000000000000162021454313311600224100ustar00rootroot00000000000000// Copyright (c) 2018, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td import ( "fmt" "reflect" "regexp" "github.com/maxatome/go-testdeep/internal/ctxerr" "github.com/maxatome/go-testdeep/internal/dark" "github.com/maxatome/go-testdeep/internal/types" ) type tdRe struct { base re *regexp.Regexp captures reflect.Value numMatches int } var _ TestDeep = &tdRe{} func newRe(regIf any, capture ...any) *tdRe { r := &tdRe{ base: newBase(4), } const ( usageRe = "(STRING|*regexp.Regexp[, NON_NIL_CAPTURE])" usageReAll = "(STRING|*regexp.Regexp, NON_NIL_CAPTURE)" ) usage := usageRe if len(r.location.Func) != 2 { usage = usageReAll } switch len(capture) { case 0: case 1: if capture[0] != nil { r.captures = reflect.ValueOf(capture[0]) } default: r.err = ctxerr.OpTooManyParams(r.location.Func, usage) return r } switch reg := regIf.(type) { case *regexp.Regexp: r.re = reg case string: var err error r.re, err = regexp.Compile(reg) if err != nil { r.err = &ctxerr.Error{ Message: "invalid regexp given to " + r.location.Func + " operator", Summary: ctxerr.NewSummary(err.Error()), } } default: r.err = ctxerr.OpBadUsage(r.location.Func, usage, regIf, 1, false) } return r } // summary(Re): allows to apply a regexp on a string (or convertible), // []byte, error or fmt.Stringer interfaces, and even test the // captured groups // input(Re): str,slice([]byte),if(✓ + fmt.Stringer/error) // Re operator allows to apply a regexp on a string (or convertible), // []byte, error or [fmt.Stringer] interface (error interface is tested // before [fmt.Stringer].) // // reg is the regexp. It can be a string that is automatically // compiled using [regexp.Compile], or a [*regexp.Regexp]. // // Optional capture parameter can be used to match the contents of // regexp groups. Groups are presented as a []string or [][]byte // depending the original matched data. Note that an other operator // can be used here. // // td.Cmp(t, "foobar zip!", td.Re(`^foobar`)) // succeeds // td.Cmp(t, "John Doe", // td.Re(`^(\w+) (\w+)`, []string{"John", "Doe"})) // succeeds // td.Cmp(t, "John Doe", // td.Re(`^(\w+) (\w+)`, td.Bag("Doe", "John"))) // succeeds // // See also [ReAll]. func Re(reg any, capture ...any) TestDeep { r := newRe(reg, capture...) r.numMatches = 1 return r } // summary(ReAll): allows to successively apply a regexp on a string // (or convertible), []byte, error or fmt.Stringer interfaces, and // even test the captured groups // input(ReAll): str,slice([]byte),if(✓ + fmt.Stringer/error) // ReAll operator allows to successively apply a regexp on a string // (or convertible), []byte, error or [fmt.Stringer] interface (error // interface is tested before [fmt.Stringer]) and to match its groups // contents. // // reg is the regexp. It can be a string that is automatically // compiled using [regexp.Compile], or a [*regexp.Regexp]. // // capture is used to match the contents of regexp groups. Groups // are presented as a []string or [][]byte depending the original // matched data. Note that an other operator can be used here. // // td.Cmp(t, "John Doe", // td.ReAll(`(\w+)(?: |\z)`, []string{"John", "Doe"})) // succeeds // td.Cmp(t, "John Doe", // td.ReAll(`(\w+)(?: |\z)`, td.Bag("Doe", "John"))) // succeeds // // See also [Re]. func ReAll(reg, capture any) TestDeep { r := newRe(reg, capture) r.numMatches = -1 return r } func (r *tdRe) needCaptures() bool { return r.captures.IsValid() } func (r *tdRe) matchByteCaptures(ctx ctxerr.Context, got []byte, result [][][]byte) *ctxerr.Error { if len(result) == 0 { return r.doesNotMatch(ctx, got) } num := 0 for _, set := range result { num += len(set) - 1 } // Not perfect but cast captured groups to string // Special case to accepted expected []any type if r.captures.Type() == types.SliceInterface { captures := make([]any, 0, num) for _, set := range result { for _, match := range set[1:] { captures = append(captures, string(match)) } } return r.matchCaptures(ctx, captures) } captures := make([]string, 0, num) for _, set := range result { for _, match := range set[1:] { captures = append(captures, string(match)) } } return r.matchCaptures(ctx, captures) } func (r *tdRe) matchStringCaptures(ctx ctxerr.Context, got string, result [][]string) *ctxerr.Error { if len(result) == 0 { return r.doesNotMatch(ctx, got) } num := 0 for _, set := range result { num += len(set) - 1 } // Special case to accepted expected []any type if r.captures.Type() == types.SliceInterface { captures := make([]any, 0, num) for _, set := range result { for _, match := range set[1:] { captures = append(captures, match) } } return r.matchCaptures(ctx, captures) } captures := make([]string, 0, num) for _, set := range result { captures = append(captures, set[1:]...) } return r.matchCaptures(ctx, captures) } func (r *tdRe) matchCaptures(ctx ctxerr.Context, captures any) (err *ctxerr.Error) { return deepValueEqual( ctx.ResetPath("("+ctx.Path.String()+" =~ "+r.String()+")"), reflect.ValueOf(captures), r.captures) } func (r *tdRe) matchBool(ctx ctxerr.Context, got any, result bool) *ctxerr.Error { if result { return nil } return r.doesNotMatch(ctx, got) } func (r *tdRe) doesNotMatch(ctx ctxerr.Context, got any) *ctxerr.Error { if ctx.BooleanError { return ctxerr.BooleanError } return ctx.CollectError(&ctxerr.Error{ Message: "does not match Regexp", Got: got, Expected: types.RawString(r.re.String()), }) } func (r *tdRe) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error { if r.err != nil { return ctx.CollectError(r.err) } var str string switch got.Kind() { case reflect.String: str = got.String() case reflect.Slice: if got.Type().Elem().Kind() == reflect.Uint8 { gotBytes := got.Bytes() if r.needCaptures() { return r.matchByteCaptures(ctx, gotBytes, r.re.FindAllSubmatch(gotBytes, r.numMatches)) } return r.matchBool(ctx, gotBytes, r.re.Match(gotBytes)) } if ctx.BooleanError { return ctxerr.BooleanError } return ctx.CollectError(&ctxerr.Error{ Message: "bad slice type", Got: types.RawString("[]" + got.Type().Elem().Kind().String()), Expected: types.RawString("[]uint8"), }) default: var strOK bool iface := dark.MustGetInterface(got) switch gotVal := iface.(type) { case error: str = gotVal.Error() strOK = true case fmt.Stringer: str = gotVal.String() strOK = true default: } if !strOK { if ctx.BooleanError { return ctxerr.BooleanError } return ctx.CollectError(&ctxerr.Error{ Message: "bad type", Got: types.RawString(got.Type().String()), Expected: types.RawString( "string (convertible) OR fmt.Stringer OR error OR []uint8"), }) } } if r.needCaptures() { return r.matchStringCaptures(ctx, str, r.re.FindAllStringSubmatch(str, r.numMatches)) } return r.matchBool(ctx, str, r.re.MatchString(str)) } func (r *tdRe) String() string { if r.err != nil { return r.stringError() } return r.re.String() } golang-github-maxatome-go-testdeep-1.14.0/td/td_re_test.go000066400000000000000000000111721454313311600234500ustar00rootroot00000000000000// Copyright (c) 2018, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td_test import ( "errors" "regexp" "testing" "github.com/maxatome/go-testdeep/internal/test" "github.com/maxatome/go-testdeep/td" ) func TestRe(t *testing.T) { // // string checkOK(t, "foo bar test", td.Re("bar")) checkOK(t, "foo bar test", td.Re(regexp.MustCompile("test$"))) checkOK(t, "foo bar test", td.ReAll(`(\w+)`, td.Bag("bar", "test", "foo"))) type MyString string checkOK(t, MyString("Ho zz hoho"), td.ReAll("(?i)(ho)", []string{"Ho", "ho", "ho"})) checkOK(t, MyString("Ho zz hoho"), td.ReAll("(?i)(ho)", []any{"Ho", "ho", "ho"})) // error interface checkOK(t, errors.New("pipo bingo"), td.Re("bin")) // fmt.Stringer interface checkOK(t, MyStringer{}, td.Re("bin")) checkError(t, 12, td.Re("bar"), expectedError{ Message: mustBe("bad type"), Path: mustBe("DATA"), Got: mustBe("int"), Expected: mustBe( "string (convertible) OR fmt.Stringer OR error OR []uint8"), }) checkError(t, "foo bar test", td.Re("pipo"), expectedError{ Message: mustBe("does not match Regexp"), Path: mustBe("DATA"), Got: mustContain(`"foo bar test"`), Expected: mustBe("pipo"), }) checkError(t, "foo bar test", td.Re("(pi)(po)", []string{"pi", "po"}), expectedError{ Message: mustBe("does not match Regexp"), Path: mustBe("DATA"), Got: mustContain(`"foo bar test"`), Expected: mustBe("(pi)(po)"), }) checkError(t, "foo bar test", td.Re("(pi)(po)", []any{"pi", "po"}), expectedError{ Message: mustBe("does not match Regexp"), Path: mustBe("DATA"), Got: mustContain(`"foo bar test"`), Expected: mustBe("(pi)(po)"), }) // // bytes checkOK(t, []byte("foo bar test"), td.Re("bar")) checkOK(t, []byte("foo bar test"), td.ReAll(`(\w+)`, td.Bag("bar", "test", "foo"))) type MySlice []byte checkOK(t, MySlice("Ho zz hoho"), td.ReAll("(?i)(ho)", []string{"Ho", "ho", "ho"})) checkOK(t, MySlice("Ho zz hoho"), td.ReAll("(?i)(ho)", []any{"Ho", "ho", "ho"})) checkError(t, []int{12}, td.Re("bar"), expectedError{ Message: mustBe("bad slice type"), Path: mustBe("DATA"), Got: mustBe("[]int"), Expected: mustBe("[]uint8"), }) checkError(t, []byte("foo bar test"), td.Re("pipo"), expectedError{ Message: mustBe("does not match Regexp"), Path: mustBe("DATA"), Got: mustContain(`foo bar test`), Expected: mustBe("pipo"), }) checkError(t, []byte("foo bar test"), td.Re("(pi)(po)", []string{"pi", "po"}), expectedError{ Message: mustBe("does not match Regexp"), Path: mustBe("DATA"), Got: mustContain(`foo bar test`), Expected: mustBe("(pi)(po)"), }) checkError(t, []byte("foo bar test"), td.Re("(pi)(po)", []any{"pi", "po"}), expectedError{ Message: mustBe("does not match Regexp"), Path: mustBe("DATA"), Got: mustContain(`foo bar test`), Expected: mustBe("(pi)(po)"), }) // // Bad usage const ( ur = "(STRING|*regexp.Regexp[, NON_NIL_CAPTURE])" ua = "(STRING|*regexp.Regexp, NON_NIL_CAPTURE)" ) checkError(t, "never tested", td.Re(123), expectedError{ Message: mustBe("bad usage of Re operator"), Path: mustBe("DATA"), Summary: mustBe("usage: Re" + ur + ", but received int as 1st parameter"), }) checkError(t, "never tested", td.ReAll(123, nil), expectedError{ Message: mustBe("bad usage of ReAll operator"), Path: mustBe("DATA"), Summary: mustBe("usage: ReAll" + ua + ", but received int as 1st parameter"), }) checkError(t, "never tested", td.Re("bar", []string{}, 1), expectedError{ Message: mustBe("bad usage of Re operator"), Path: mustBe("DATA"), Summary: mustBe("usage: Re" + ur + ", too many parameters"), }) checkError(t, "never tested", td.ReAll(123, 456), expectedError{ Message: mustBe("bad usage of ReAll operator"), Path: mustBe("DATA"), Summary: mustBe("usage: ReAll" + ua + ", but received int as 1st parameter"), }) checkError(t, "never tested", td.ReAll(`12[3,4`, nil), expectedError{ Message: mustBe("invalid regexp given to ReAll operator"), Path: mustBe("DATA"), Summary: mustContain("error parsing regexp: "), }) // Erroneous op test.EqualStr(t, td.Re(123).String(), "Re()") test.EqualStr(t, td.ReAll(123, nil).String(), "ReAll()") } func TestReTypeBehind(t *testing.T) { equalTypes(t, td.Re("x"), nil) equalTypes(t, td.ReAll("x", nil), nil) // Erroneous op equalTypes(t, td.Re(123), nil) equalTypes(t, td.ReAll(123, nil), nil) } golang-github-maxatome-go-testdeep-1.14.0/td/td_recv.go000066400000000000000000000151311454313311600227410ustar00rootroot00000000000000// Copyright (c) 2022, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td import ( "fmt" "reflect" "time" "github.com/maxatome/go-testdeep/internal/ctxerr" "github.com/maxatome/go-testdeep/internal/types" ) // A RecvKind allows to match that nothing has been received on a // channel or that a channel has been closed when using [Recv] // operator. type RecvKind = types.RecvKind const ( _ RecvKind = (iota & 1) == 0 RecvNothing // nothing read on channel RecvClosed // channel closed ) type tdRecv struct { tdSmugglerBase timeout time.Duration } var _ TestDeep = &tdRecv{} // summary(Recv): checks the value read from a channel // input(Recv): chan,ptr(ptr on chan) // Recv is a smuggler operator. It reads from a channel or a pointer // to a channel and compares the read value to expectedValue. // // expectedValue can be any value including a [TestDeep] operator. It // can also be [RecvNothing] to test nothing can be read from the // channel or [RecvClosed] to check the channel is closed. // // If timeout is passed it should be only one item. It means: try to // read the channel during this duration to get a value before giving // up. If timeout is missing or ≤ 0, it defaults to 0 meaning Recv // does not wait for a value but gives up instantly if no value is // available on the channel. // // ch := make(chan int, 6) // td.Cmp(t, ch, td.Recv(td.RecvNothing)) // succeeds // td.Cmp(t, ch, td.Recv(42)) // fails, nothing to receive // // recv(DATA): values differ // // got: nothing received on channel // // expected: 42 // // ch <- 42 // td.Cmp(t, ch, td.Recv(td.RecvNothing)) // fails, 42 received instead // // recv(DATA): values differ // // got: 42 // // expected: nothing received on channel // // td.Cmp(t, ch, td.Recv(42)) // fails, nothing to receive anymore // // recv(DATA): values differ // // got: nothing received on channel // // expected: 42 // // ch <- 666 // td.Cmp(t, ch, td.Recv(td.Between(600, 700))) // succeeds // // close(ch) // td.Cmp(t, ch, td.Recv(td.RecvNothing)) // fails as channel is closed // // recv(DATA): values differ // // got: channel is closed // // expected: nothing received on channel // // td.Cmp(t, ch, td.Recv(td.RecvClosed)) // succeeds // // Note that for convenience Recv accepts pointer on channel: // // ch := make(chan int, 6) // ch <- 42 // td.Cmp(t, &ch, td.Recv(42)) // succeeds // // Each time Recv is called, it tries to consume one item from the // channel, immediately or, if given, before timeout duration. To // consume several items in a same [Cmp] call, one can use [All] // operator as in: // // ch := make(chan int, 6) // ch <- 1 // ch <- 2 // ch <- 3 // close(ch) // td.Cmp(t, ch, td.All( // succeeds // td.Recv(1), // td.Recv(2), // td.Recv(3), // td.Recv(td.RecvClosed), // )) // // To check nothing can be received during 100ms on channel ch (if // something is received before, including a close, it fails): // // td.Cmp(t, ch, td.Recv(td.RecvNothing, 100*time.Millisecond)) // // note that in case of success, the above [Cmp] call always lasts 100ms. // // To check 42 can be received from channel ch during the next 100ms // (if nothing is received during these 100ms or something different // from 42, including a close, it fails): // // td.Cmp(t, ch, td.Recv(42, 100*time.Millisecond)) // // note that in case of success, the above [Cmp] call lasts less than 100ms. // // A nil channel is not handled specifically, so it “is never ready // for communication” as specification says: // // var ch chan int // td.Cmp(t, ch, td.Recv(td.RecvNothing)) // always succeeds // td.Cmp(t, ch, td.Recv(42)) // or any other value, always fails // td.Cmp(t, ch, td.Recv(td.RecvClosed)) // always fails // // so to check if a channel is not nil before reading from it, one can // either do: // // td.Cmp(t, ch, td.All( // td.NotNil(), // td.Recv(42), // )) // // or // if td.Cmp(t, ch, td.NotNil()) { // td.Cmp(t, ch, td.Recv(42)) // } // // TypeBehind method returns the [reflect.Type] of expectedValue, // except if expectedValue is a [TestDeep] operator. In this case, it // delegates TypeBehind() to the operator. // // See also [Cap] and [Len]. func Recv(expectedValue any, timeout ...time.Duration) TestDeep { r := tdRecv{} r.tdSmugglerBase = newSmugglerBase(expectedValue, 0) if !r.isTestDeeper { r.expectedValue = reflect.ValueOf(expectedValue) } switch len(timeout) { case 0: case 1: r.timeout = timeout[0] default: r.err = ctxerr.OpTooManyParams(r.location.Func, "(EXPECTED[, TIMEOUT])") } return &r } func (r *tdRecv) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error { if r.err != nil { return ctx.CollectError(r.err) } switch got.Kind() { case reflect.Ptr: gotElem := got.Elem() if !gotElem.IsValid() { if ctx.BooleanError { return ctxerr.BooleanError } return ctx.CollectError(ctxerr.NilPointer(got, "non-nil *chan")) } if gotElem.Kind() != reflect.Chan { break } got = gotElem fallthrough case reflect.Chan: cases := [2]reflect.SelectCase{ { Dir: reflect.SelectRecv, Chan: got, }, } var timer *time.Timer if r.timeout > 0 { timer = time.NewTimer(r.timeout) cases[1] = reflect.SelectCase{ Dir: reflect.SelectRecv, Chan: reflect.ValueOf(timer.C), } } else { cases[1] = reflect.SelectCase{ Dir: reflect.SelectDefault, } } chosen, recv, recvOK := reflect.Select(cases[:]) if chosen == 1 && timer != nil { // check quickly both timeout & expected case didn't occur // concurrently and timeout masked the expected case cases[1] = reflect.SelectCase{ Dir: reflect.SelectDefault, } chosen, recv, recvOK = reflect.Select(cases[:]) } if chosen == 0 { if !recvOK { recv = reflect.ValueOf(RecvClosed) } if timer != nil { timer.Stop() } } else { recv = reflect.ValueOf(RecvNothing) } return deepValueEqual(ctx.AddFunctionCall("recv"), recv, r.expectedValue) } if ctx.BooleanError { return ctxerr.BooleanError } return ctx.CollectError(ctxerr.BadKind(got, "chan OR *chan")) } func (r *tdRecv) HandleInvalid() bool { return true // Knows how to handle untyped nil values (aka invalid values) } func (r *tdRecv) String() string { if r.err != nil { return r.stringError() } if r.isTestDeeper { return "recv: " + r.expectedValue.Interface().(TestDeep).String() } return fmt.Sprintf("recv=%d", r.expectedValue.Int()) } func (r *tdRecv) TypeBehind() reflect.Type { if r.err != nil { return nil } return r.internalTypeBehind() } golang-github-maxatome-go-testdeep-1.14.0/td/td_recv_test.go000066400000000000000000000142151454313311600240020ustar00rootroot00000000000000// Copyright (c) 2022, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td_test import ( "testing" "time" "github.com/maxatome/go-testdeep/internal/test" "github.com/maxatome/go-testdeep/internal/types" "github.com/maxatome/go-testdeep/td" ) func TestRecv(t *testing.T) { fillCh := func(ch chan int, val int) { ch <- val // td.Cmp ch <- val // EqDeeply aka boolean context ch <- val // EqDeeplyError ch <- val // interface + td.Cmp ch <- val // interface + EqDeeply aka boolean context ch <- val // interface + EqDeeplyError } mkCh := func(val int) chan int { ch := make(chan int, 6) fillCh(ch, val) close(ch) return ch } t.Run("all good", func(t *testing.T) { ch := mkCh(1) checkOK(t, ch, td.Recv(1)) checkOK(t, ch, td.Recv(td.RecvClosed, 10*time.Microsecond)) ch = mkCh(42) checkOK(t, ch, td.Recv(td.Between(40, 45))) checkOK(t, ch, td.Recv(td.RecvClosed)) }) t.Run("complete cycle", func(t *testing.T) { ch := make(chan int, 6) t.Run("empty", func(t *testing.T) { checkOK(t, ch, td.Recv(td.RecvNothing)) checkOK(t, ch, td.Recv(td.RecvNothing, 10*time.Microsecond)) checkOK(t, &ch, td.Recv(td.RecvNothing)) checkOK(t, &ch, td.Recv(td.RecvNothing, 10*time.Microsecond)) }) t.Run("just filled", func(t *testing.T) { fillCh(ch, 33) checkOK(t, ch, td.Recv(33)) fillCh(ch, 34) checkOK(t, &ch, td.Recv(34)) }) t.Run("nothing to recv on channel", func(t *testing.T) { checkError(t, ch, td.Recv(td.RecvClosed), expectedError{ Message: mustBe("values differ"), Path: mustBe("recv(DATA)"), Got: mustBe("nothing received on channel"), Expected: mustBe("channel is closed"), }) checkError(t, &ch, td.Recv(td.RecvClosed), expectedError{ Message: mustBe("values differ"), Path: mustBe("recv(DATA)"), Got: mustBe("nothing received on channel"), Expected: mustBe("channel is closed"), }) checkError(t, ch, td.Recv(42), expectedError{ Message: mustBe("values differ"), Path: mustBe("recv(DATA)"), Got: mustBe("nothing received on channel"), Expected: mustBe("42"), }) checkError(t, &ch, td.Recv(42), expectedError{ Message: mustBe("values differ"), Path: mustBe("recv(DATA)"), Got: mustBe("nothing received on channel"), Expected: mustBe("42"), }) }) close(ch) t.Run("closed channel", func(t *testing.T) { checkError(t, ch, td.Recv(td.RecvNothing), expectedError{ Message: mustBe("values differ"), Path: mustBe("recv(DATA)"), Got: mustBe("channel is closed"), Expected: mustBe("nothing received on channel"), }) checkError(t, &ch, td.Recv(td.RecvNothing), expectedError{ Message: mustBe("values differ"), Path: mustBe("recv(DATA)"), Got: mustBe("channel is closed"), Expected: mustBe("nothing received on channel"), }) checkError(t, ch, td.Recv(42), expectedError{ Message: mustBe("values differ"), Path: mustBe("recv(DATA)"), Got: mustBe("channel is closed"), Expected: mustBe("42"), }) checkError(t, &ch, td.Recv(42), expectedError{ Message: mustBe("values differ"), Path: mustBe("recv(DATA)"), Got: mustBe("channel is closed"), Expected: mustBe("42"), }) }) }) t.Run("nil channel", func(t *testing.T) { var ch chan int checkError(t, ch, td.Recv(42), expectedError{ Message: mustBe("values differ"), Path: mustBe("recv(DATA)"), Got: mustBe("nothing received on channel"), Expected: mustBe("42"), }) checkError(t, &ch, td.Recv(42), expectedError{ Message: mustBe("values differ"), Path: mustBe("recv(DATA)"), Got: mustBe("nothing received on channel"), Expected: mustBe("42"), }) }) t.Run("nil pointer", func(t *testing.T) { checkError(t, (*chan int)(nil), td.Recv(42), expectedError{ Message: mustBe("nil pointer"), Path: mustBe("DATA"), Got: mustBe("nil *chan (*chan int type)"), Expected: mustBe("non-nil *chan"), }) }) t.Run("chan any", func(t *testing.T) { ch := make(chan any, 6) fillCh := func(val any) { ch <- val // td.Cmp ch <- val // EqDeeply aka boolean context ch <- val // EqDeeplyError ch <- val // interface + td.Cmp ch <- val // interface + EqDeeply aka boolean context ch <- val // interface + EqDeeplyError } fillCh(1) checkOK(t, ch, td.Recv(1)) fillCh(nil) checkOK(t, ch, td.Recv(nil)) close(ch) checkOK(t, ch, td.Recv(td.RecvClosed)) }) t.Run("errors", func(t *testing.T) { checkError(t, "never tested", td.Recv(23, time.Second, time.Second), expectedError{ Message: mustBe("bad usage of Recv operator"), Path: mustBe("DATA"), Summary: mustBe("usage: Recv(EXPECTED[, TIMEOUT]), too many parameters"), }) checkError(t, 42, td.Recv(33), expectedError{ Message: mustBe("bad kind"), Path: mustBe("DATA"), Got: mustBe("int"), Expected: mustBe("chan OR *chan"), }) checkError(t, &struct{}{}, td.Recv(33), expectedError{ Message: mustBe("bad kind"), Path: mustBe("DATA"), Got: mustBe("*struct (*struct {} type)"), Expected: mustBe("chan OR *chan"), }) checkError(t, nil, td.Recv(33), expectedError{ Message: mustBe("bad kind"), Path: mustBe("DATA"), Got: mustBe("nil"), Expected: mustBe("chan OR *chan"), }) }) } func TestRecvString(t *testing.T) { test.EqualStr(t, td.Recv(3).String(), "recv=3") test.EqualStr(t, td.Recv(td.Between(3, 8)).String(), "recv: 3 ≤ got ≤ 8") test.EqualStr(t, td.Recv(td.Gt(8)).String(), "recv: > 8") // Erroneous op test.EqualStr(t, td.Recv(3, 0, 0).String(), "Recv()") } func TestRecvTypeBehind(t *testing.T) { equalTypes(t, td.Recv(3), 0) equalTypes(t, td.Recv(td.Between(3, 4)), 0) // Erroneous op equalTypes(t, td.Recv(3, 0, 0), nil) } func TestRecvKind(t *testing.T) { test.IsTrue(t, td.RecvNothing == types.RecvNothing) test.IsTrue(t, td.RecvClosed == types.RecvClosed) } golang-github-maxatome-go-testdeep-1.14.0/td/td_set.go000066400000000000000000000153211454313311600225760ustar00rootroot00000000000000// Copyright (c) 2018, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td // summary(Set): compares the contents of an array or a slice ignoring // duplicates and without taking care of the order of items // input(Set): array,slice,ptr(ptr on array/slice) // Set operator compares the contents of an array or a slice (or a // pointer on array/slice) ignoring duplicates and without taking care // of the order of items. // // During a match, each expected item should match in the compared // array/slice, and each array/slice item should be matched by an // expected item to succeed. // // td.Cmp(t, []int{1, 1, 2}, td.Set(1, 2)) // succeeds // td.Cmp(t, []int{1, 1, 2}, td.Set(2, 1)) // succeeds // td.Cmp(t, []int{1, 1, 2}, td.Set(1, 2, 3)) // fails, 3 is missing // // // works with slices/arrays of any type // td.Cmp(t, personSlice, td.Set( // Person{Name: "Bob", Age: 32}, // Person{Name: "Alice", Age: 26}, // )) // // To flatten a non-[]any slice/array, use [Flatten] function // and so avoid boring and inefficient copies: // // expected := []int{2, 1} // td.Cmp(t, []int{1, 1, 2}, td.Set(td.Flatten(expected))) // succeeds // // = td.Cmp(t, []int{1, 1, 2}, td.Set(2, 1)) // // exp1 := []int{2, 1} // exp2 := []int{5, 8} // td.Cmp(t, []int{1, 5, 1, 2, 8, 3, 3}, // td.Set(td.Flatten(exp1), 3, td.Flatten(exp2))) // succeeds // // = td.Cmp(t, []int{1, 5, 1, 2, 8, 3, 3}, td.Set(2, 1, 3, 5, 8)) // // TypeBehind method can return a non-nil [reflect.Type] if all items // known non-interface types are equal, or if only interface types // are found (mostly issued from [Isa]) and they are equal. // // See also [NotAny], [SubSetOf], [SuperSetOf] and [Bag]. func Set(expectedItems ...any) TestDeep { return newSetBase(allSet, true, expectedItems) } // summary(SubSetOf): compares the contents of an array or a slice // ignoring duplicates and without taking care of the order of items // but with potentially some exclusions // input(SubSetOf): array,slice,ptr(ptr on array/slice) // SubSetOf operator compares the contents of an array or a slice (or a // pointer on array/slice) ignoring duplicates and without taking care // of the order of items. // // During a match, each array/slice item should be matched by an // expected item to succeed. But some expected items can be missing // from the compared array/slice. // // td.Cmp(t, []int{1, 1}, td.SubSetOf(1, 2)) // succeeds // td.Cmp(t, []int{1, 1, 2}, td.SubSetOf(1, 3)) // fails, 2 is an extra item // // // works with slices/arrays of any type // td.Cmp(t, personSlice, td.SubSetOf( // Person{Name: "Bob", Age: 32}, // Person{Name: "Alice", Age: 26}, // )) // // To flatten a non-[]any slice/array, use [Flatten] function // and so avoid boring and inefficient copies: // // expected := []int{2, 1} // td.Cmp(t, []int{1, 1}, td.SubSetOf(td.Flatten(expected))) // succeeds // // = td.Cmp(t, []int{1, 1}, td.SubSetOf(2, 1)) // // exp1 := []int{2, 1} // exp2 := []int{5, 8} // td.Cmp(t, []int{1, 5, 1, 3, 3}, // td.SubSetOf(td.Flatten(exp1), 3, td.Flatten(exp2))) // succeeds // // = td.Cmp(t, []int{1, 5, 1, 3, 3}, td.SubSetOf(2, 1, 3, 5, 8)) // // TypeBehind method can return a non-nil [reflect.Type] if all items // known non-interface types are equal, or if only interface types // are found (mostly issued from [Isa]) and they are equal. // // See also [NotAny], [Set] and [SuperSetOf]. func SubSetOf(expectedItems ...any) TestDeep { return newSetBase(subSet, true, expectedItems) } // summary(SuperSetOf): compares the contents of an array or a slice // ignoring duplicates and without taking care of the order of items // but with potentially some extra items // input(SuperSetOf): array,slice,ptr(ptr on array/slice) // SuperSetOf operator compares the contents of an array or a slice (or // a pointer on array/slice) ignoring duplicates and without taking // care of the order of items. // // During a match, each expected item should match in the compared // array/slice. But some items in the compared array/slice may not be // expected. // // td.Cmp(t, []int{1, 1, 2}, td.SuperSetOf(1)) // succeeds // td.Cmp(t, []int{1, 1, 2}, td.SuperSetOf(1, 3)) // fails, 3 is missing // // // works with slices/arrays of any type // td.Cmp(t, personSlice, td.SuperSetOf( // Person{Name: "Bob", Age: 32}, // Person{Name: "Alice", Age: 26}, // )) // // To flatten a non-[]any slice/array, use [Flatten] function // and so avoid boring and inefficient copies: // // expected := []int{2, 1} // td.Cmp(t, []int{1, 1, 2, 8}, td.SuperSetOf(td.Flatten(expected))) // succeeds // // = td.Cmp(t, []int{1, 1, 2, 8}, td.SubSetOf(2, 1)) // // exp1 := []int{2, 1} // exp2 := []int{5, 8} // td.Cmp(t, []int{1, 5, 1, 8, 42, 3, 3}, // td.SuperSetOf(td.Flatten(exp1), 3, td.Flatten(exp2))) // succeeds // // = td.Cmp(t, []int{1, 5, 1, 8, 42, 3, 3}, td.SuperSetOf(2, 1, 3, 5, 8)) // // TypeBehind method can return a non-nil [reflect.Type] if all items // known non-interface types are equal, or if only interface types // are found (mostly issued from [Isa]) and they are equal. // // See also [NotAny], [Set] and [SubSetOf]. func SuperSetOf(expectedItems ...any) TestDeep { return newSetBase(superSet, true, expectedItems) } // summary(NotAny): compares the contents of an array or a slice, no // values have to match // input(NotAny): array,slice,ptr(ptr on array/slice) // NotAny operator checks that the contents of an array or a slice (or // a pointer on array/slice) does not contain any of "notExpectedItems". // // td.Cmp(t, []int{1}, td.NotAny(1, 2, 3)) // fails // td.Cmp(t, []int{5}, td.NotAny(1, 2, 3)) // succeeds // // // works with slices/arrays of any type // td.Cmp(t, personSlice, td.NotAny( // Person{Name: "Bob", Age: 32}, // Person{Name: "Alice", Age: 26}, // )) // // To flatten a non-[]any slice/array, use [Flatten] function // and so avoid boring and inefficient copies: // // notExpected := []int{2, 1} // td.Cmp(t, []int{4, 4, 3, 8}, td.NotAny(td.Flatten(notExpected))) // succeeds // // = td.Cmp(t, []int{4, 4, 3, 8}, td.NotAny(2, 1)) // // notExp1 := []int{2, 1} // notExp2 := []int{5, 8} // td.Cmp(t, []int{4, 4, 42, 8}, // td.NotAny(td.Flatten(notExp1), 3, td.Flatten(notExp2))) // succeeds // // = td.Cmp(t, []int{4, 4, 42, 8}, td.NotAny(2, 1, 3, 5, 8)) // // Beware that NotAny(…) is not equivalent to Not(Any(…)) but is like // Not(SuperSet(…)). // // TypeBehind method can return a non-nil [reflect.Type] if all items // known non-interface types are equal, or if only interface types // are found (mostly issued from [Isa]) and they are equal. // // See also [Set], [SubSetOf] and [SuperSetOf]. func NotAny(notExpectedItems ...any) TestDeep { return newSetBase(noneSet, true, notExpectedItems) } golang-github-maxatome-go-testdeep-1.14.0/td/td_set_base.go000066400000000000000000000074321454313311600235740ustar00rootroot00000000000000// Copyright (c) 2018, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td import ( "reflect" "strings" "github.com/maxatome/go-testdeep/internal/ctxerr" "github.com/maxatome/go-testdeep/internal/flat" "github.com/maxatome/go-testdeep/internal/util" ) type setKind uint8 const ( allSet setKind = iota subSet superSet noneSet ) type tdSetBase struct { baseOKNil kind setKind ignoreDups bool expectedItems []reflect.Value } func newSetBase(kind setKind, ignoreDups bool, expectedItems []any) *tdSetBase { return &tdSetBase{ baseOKNil: newBaseOKNil(4), kind: kind, ignoreDups: ignoreDups, expectedItems: flat.Values(expectedItems), } } func (s *tdSetBase) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error { switch got.Kind() { case reflect.Ptr: gotElem := got.Elem() if !gotElem.IsValid() { if ctx.BooleanError { return ctxerr.BooleanError } return ctx.CollectError(ctxerr.NilPointer(got, "non-nil *slice OR *array")) } if gotElem.Kind() != reflect.Array && gotElem.Kind() != reflect.Slice { break } got = gotElem fallthrough case reflect.Array, reflect.Slice: var ( gotLen = got.Len() foundItems []reflect.Value missingItems []reflect.Value foundGotIdxes = map[int]bool{} ) for _, expected := range s.expectedItems { found := false for idx := 0; len(foundGotIdxes) < gotLen && idx < gotLen; idx++ { if foundGotIdxes[idx] { continue } if deepValueEqualFinalOK(ctx, got.Index(idx), expected) { foundItems = append(foundItems, expected) foundGotIdxes[idx] = true found = true if !s.ignoreDups { break } } } if !found { missingItems = append(missingItems, expected) } } res := tdSetResult{ Kind: itemsSetResult, Sort: true, } if s.kind != noneSet { if s.kind != subSet { // In Set* cases with missing items, try a second pass. Perhaps // an already matching got item, matches another expected item? if s.ignoreDups && len(missingItems) > 0 { var newMissingItems []reflect.Value nextExpected: for _, expected := range missingItems { for idxGot := range foundGotIdxes { if deepValueEqualFinalOK(ctx, got.Index(idxGot), expected) { continue nextExpected } } newMissingItems = append(newMissingItems, expected) } missingItems = newMissingItems } if len(missingItems) > 0 { if ctx.BooleanError { return ctxerr.BooleanError } res.Missing = missingItems } } if len(foundGotIdxes) < gotLen && s.kind != superSet { if ctx.BooleanError { return ctxerr.BooleanError } notFoundRemain := gotLen - len(foundGotIdxes) res.Extra = make([]reflect.Value, 0, notFoundRemain) for idx := 0; notFoundRemain > 0; idx++ { if !foundGotIdxes[idx] { res.Extra = append(res.Extra, got.Index(idx)) notFoundRemain-- } } } } else if len(foundItems) > 0 { if ctx.BooleanError { return ctxerr.BooleanError } res.Extra = foundItems } if res.IsEmpty() { return nil } return ctx.CollectError(&ctxerr.Error{ Message: "comparing %% as a " + s.GetLocation().Func, Summary: res.Summary(), }) } if ctx.BooleanError { return ctxerr.BooleanError } return ctx.CollectError(ctxerr.BadKind(got, "slice OR array OR *slice OR *array")) } func (s *tdSetBase) String() string { var b strings.Builder b.WriteString(s.GetLocation().Func) return util.SliceToString(&b, s.expectedItems).String() } func (s *tdSetBase) TypeBehind() reflect.Type { typ := uniqTypeBehindSlice(s.expectedItems) if typ == nil { return nil } return reflect.SliceOf(typ) } golang-github-maxatome-go-testdeep-1.14.0/td/td_set_result.go000066400000000000000000000034401454313311600241730ustar00rootroot00000000000000// Copyright (c) 2018, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td import ( "fmt" "reflect" "sort" "github.com/maxatome/go-testdeep/helpers/tdutil" "github.com/maxatome/go-testdeep/internal/ctxerr" "github.com/maxatome/go-testdeep/internal/types" "github.com/maxatome/go-testdeep/internal/util" ) type tdSetResultKind uint8 const ( itemsSetResult tdSetResultKind = iota keysSetResult ) // Implements fmt.Stringer. func (k tdSetResultKind) String() string { switch k { case itemsSetResult: return "item" case keysSetResult: return "key" default: return "?" } } type tdSetResult struct { types.TestDeepStamp Missing []reflect.Value Extra []reflect.Value Kind tdSetResultKind Sort bool } func (r tdSetResult) IsEmpty() bool { return len(r.Missing) == 0 && len(r.Extra) == 0 } func (r tdSetResult) Summary() ctxerr.ErrorSummary { var summary ctxerr.ErrorSummaryItems if len(r.Missing) > 0 { var missing string if len(r.Missing) > 1 { if r.Sort { sort.Stable(tdutil.SortableValues(r.Missing)) } missing = fmt.Sprintf("Missing %d %ss", len(r.Missing), r.Kind) } else { missing = fmt.Sprintf("Missing %s", r.Kind) } summary = append(summary, ctxerr.ErrorSummaryItem{ Label: missing, Value: util.ToString(r.Missing), }) } if len(r.Extra) > 0 { var extra string if len(r.Extra) > 1 { if r.Sort { sort.Stable(tdutil.SortableValues(r.Extra)) } extra = fmt.Sprintf("Extra %d %ss", len(r.Extra), r.Kind) } else { extra = fmt.Sprintf("Extra %s", r.Kind) } summary = append(summary, ctxerr.ErrorSummaryItem{ Label: extra, Value: util.ToString(r.Extra), }) } return summary } golang-github-maxatome-go-testdeep-1.14.0/td/td_set_test.go000066400000000000000000000146051454313311600236410ustar00rootroot00000000000000// Copyright (c) 2018-2022, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td_test import ( "fmt" "testing" "github.com/maxatome/go-testdeep/internal/test" "github.com/maxatome/go-testdeep/td" ) func TestSet(t *testing.T) { type MyArray [5]int type MySlice []int for idx, got := range []any{ []int{1, 3, 4, 4, 5}, [...]int{1, 3, 4, 4, 5}, MySlice{1, 3, 4, 4, 5}, MyArray{1, 3, 4, 4, 5}, &MySlice{1, 3, 4, 4, 5}, &MyArray{1, 3, 4, 4, 5}, } { testName := fmt.Sprintf("Test #%d → %v", idx, got) // // Set checkOK(t, got, td.Set(5, 4, 1, 3), testName) checkOK(t, got, td.Set(5, 4, 1, 3, 3, 3, 3), testName) // duplicated fields checkOK(t, got, td.Set( td.Between(0, 5), td.Between(0, 5), td.Between(0, 5))) // dup too checkError(t, got, td.Set(5, 4), expectedError{ Message: mustBe("comparing %% as a Set"), Path: mustBe("DATA"), // items are sorted Summary: mustBe(`Extra 2 items: (1, 3)`), }, testName) checkError(t, got, td.Set(5, 4, 1, 3, 66), expectedError{ Message: mustBe("comparing %% as a Set"), Path: mustBe("DATA"), Summary: mustBe("Missing item: (66)"), }, testName) checkError(t, got, td.Set(5, 66, 4, 1, 3), expectedError{ Message: mustBe("comparing %% as a Set"), Path: mustBe("DATA"), Summary: mustBe("Missing item: (66)"), }, testName) checkError(t, got, td.Set(5, 67, 4, 1, 3, 66), expectedError{ Message: mustBe("comparing %% as a Set"), Path: mustBe("DATA"), Summary: mustBe("Missing 2 items: (66,\n 67)"), }, testName) checkError(t, got, td.Set(5, 66, 4, 3), expectedError{ Message: mustBe("comparing %% as a Set"), Path: mustBe("DATA"), Summary: mustBe("Missing item: (66)\n Extra item: (1)"), }, testName) // Lax checkOK(t, got, td.Lax(td.Set(5, float64(4), 1, 3)), testName) // // SubSetOf checkOK(t, got, td.SubSetOf(5, 4, 1, 3), testName) checkOK(t, got, td.SubSetOf(5, 4, 1, 3, 66), testName) checkError(t, got, td.SubSetOf(5, 66, 4, 3), expectedError{ Message: mustBe("comparing %% as a SubSetOf"), Path: mustBe("DATA"), Summary: mustBe("Extra item: (1)"), }, testName) // Lax checkOK(t, got, td.Lax(td.SubSetOf(5, float64(4), 1, 3)), testName) // // SuperSetOf checkOK(t, got, td.SuperSetOf(5, 4, 1, 3), testName) checkOK(t, got, td.SuperSetOf(5, 4), testName) checkError(t, got, td.SuperSetOf(5, 66, 4, 1, 3), expectedError{ Message: mustBe("comparing %% as a SuperSetOf"), Path: mustBe("DATA"), Summary: mustBe("Missing item: (66)"), }, testName) // Lax checkOK(t, got, td.Lax(td.SuperSetOf(5, float64(4), 1, 3)), testName) // // NotAny checkOK(t, got, td.NotAny(10, 20, 30), testName) checkError(t, got, td.NotAny(3, 66), expectedError{ Message: mustBe("comparing %% as a NotAny"), Path: mustBe("DATA"), Summary: mustBe("Extra item: (3)"), }, testName) // Lax checkOK(t, got, td.NotAny(float64(3)), testName) checkError(t, got, td.Lax(td.NotAny(float64(3))), expectedError{ Message: mustBe("comparing %% as a NotAny"), Path: mustBe("DATA"), Summary: mustBe("Extra item: (3.0)"), }, testName) } checkOK(t, []any{123, "foo", nil, "bar", nil}, td.Set("foo", "bar", 123, nil)) var nilSlice MySlice for idx, got := range []any{([]int)(nil), &nilSlice} { testName := fmt.Sprintf("Test #%d", idx) checkOK(t, got, td.Set(), testName) checkOK(t, got, td.SubSetOf(), testName) checkOK(t, got, td.SubSetOf(1, 2), testName) checkOK(t, got, td.SuperSetOf(), testName) checkOK(t, got, td.NotAny(), testName) checkOK(t, got, td.NotAny(1, 2), testName) } for idx, set := range []td.TestDeep{ td.Set(123), td.SubSetOf(123), td.SuperSetOf(123), td.NotAny(123), } { testName := fmt.Sprintf("Test #%d → %s", idx, set) checkError(t, 123, set, expectedError{ Message: mustBe("bad kind"), Path: mustBe("DATA"), Got: mustBe("int"), Expected: mustBe("slice OR array OR *slice OR *array"), }, testName) num := 123 checkError(t, &num, set, expectedError{ Message: mustBe("bad kind"), Path: mustBe("DATA"), Got: mustBe("*int"), Expected: mustBe("slice OR array OR *slice OR *array"), }, testName) var list *MySlice checkError(t, list, set, expectedError{ Message: mustBe("nil pointer"), Path: mustBe("DATA"), Got: mustBe("nil *slice (*td_test.MySlice type)"), Expected: mustBe("non-nil *slice OR *array"), }, testName) checkError(t, nil, set, expectedError{ Message: mustBe("bad kind"), Path: mustBe("DATA"), Got: mustBe("nil"), Expected: mustBe("slice OR array OR *slice OR *array"), }, testName) } // // String test.EqualStr(t, td.Set(1).String(), "Set(1)") test.EqualStr(t, td.Set(1, 2).String(), "Set(1,\n 2)") test.EqualStr(t, td.SubSetOf(1).String(), "SubSetOf(1)") test.EqualStr(t, td.SubSetOf(1, 2).String(), "SubSetOf(1,\n 2)") test.EqualStr(t, td.SuperSetOf(1).String(), "SuperSetOf(1)") test.EqualStr(t, td.SuperSetOf(1, 2).String(), "SuperSetOf(1,\n 2)") test.EqualStr(t, td.NotAny(1).String(), "NotAny(1)") test.EqualStr(t, td.NotAny(1, 2).String(), "NotAny(1,\n 2)") } func TestSetTypeBehind(t *testing.T) { equalTypes(t, td.Set(6, 5), ([]int)(nil)) equalTypes(t, td.Set(6, "foo"), nil) equalTypes(t, td.SubSetOf(6, 5), ([]int)(nil)) equalTypes(t, td.SubSetOf(6, "foo"), nil) equalTypes(t, td.SuperSetOf(6, 5), ([]int)(nil)) equalTypes(t, td.SuperSetOf(6, "foo"), nil) equalTypes(t, td.NotAny(6, 5), ([]int)(nil)) equalTypes(t, td.NotAny(6, "foo"), nil) // Always the same non-interface type (even if we encounter several // interface types) equalTypes(t, td.Set( td.Empty(), 5, td.Isa((*error)(nil)), // interface type (in fact pointer to ...) td.All(6, 7), td.Isa((*fmt.Stringer)(nil)), // interface type 8), ([]int)(nil)) // Only one interface type equalTypes(t, td.Set( td.Isa((*error)(nil)), td.Isa((*error)(nil)), td.Isa((*error)(nil)), ), ([]*error)(nil)) // Several interface types, cannot be sure equalTypes(t, td.Set( td.Isa((*error)(nil)), td.Isa((*fmt.Stringer)(nil)), ), nil) } golang-github-maxatome-go-testdeep-1.14.0/td/td_shallow.go000066400000000000000000000072201454313311600234530ustar00rootroot00000000000000// Copyright (c) 2018, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td import ( "fmt" "reflect" "unsafe" "github.com/maxatome/go-testdeep/internal/ctxerr" "github.com/maxatome/go-testdeep/internal/types" ) type tdShallow struct { base expectedKind reflect.Kind expectedPointer uintptr expectedStr string // in reflect.String case, to avoid contents GC } var _ TestDeep = &tdShallow{} func stringPointer(s string) uintptr { return (*reflect.StringHeader)(unsafe.Pointer(&s)).Data } // summary(Shallow): compares pointers only, not their contents // input(Shallow): nil,str,slice,map,ptr,chan,func // Shallow operator compares pointers only, not their contents. It // applies on channels, functions (with some restrictions), maps, // pointers, slices and strings. // // During a match, the compared data must be the same as expectedPtr // to succeed. // // a, b := 123, 123 // td.Cmp(t, &a, td.Shallow(&a)) // succeeds // td.Cmp(t, &a, td.Shallow(&b)) // fails even if a == b as &a != &b // // back := "foobarfoobar" // a, b := back[:6], back[6:] // // a == b but... // td.Cmp(t, &a, td.Shallow(&b)) // fails // // Be careful for slices and strings! Shallow can succeed but the // slices/strings not be identical because of their different // lengths. For example: // // a := "foobar yes!" // b := a[:1] // aka "f" // td.Cmp(t, &a, td.Shallow(&b)) // succeeds as both strings point to the same area, even if len() differ // // The same behavior occurs for slices: // // a := []int{1, 2, 3, 4, 5, 6} // b := a[:2] // aka []int{1, 2} // td.Cmp(t, &a, td.Shallow(&b)) // succeeds as both slices point to the same area, even if len() differ // // See also [Ptr]. func Shallow(expectedPtr any) TestDeep { vptr := reflect.ValueOf(expectedPtr) shallow := tdShallow{ base: newBase(3), expectedKind: vptr.Kind(), } // Note from reflect documentation: // If v's Kind is Func, the returned pointer is an underlying code // pointer, but not necessarily enough to identify a single function // uniquely. The only guarantee is that the result is zero if and // only if v is a nil func Value. switch shallow.expectedKind { case reflect.Chan, reflect.Func, reflect.Map, reflect.Ptr, reflect.Slice, reflect.UnsafePointer: shallow.expectedPointer = vptr.Pointer() case reflect.String: shallow.expectedStr = vptr.String() shallow.expectedPointer = stringPointer(shallow.expectedStr) default: shallow.err = ctxerr.OpBadUsage( "Shallow", "(CHANNEL|FUNC|MAP|PTR|SLICE|UNSAFE_PTR|STRING)", expectedPtr, 1, true) } return &shallow } func (s *tdShallow) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error { if s.err != nil { return ctx.CollectError(s.err) } if got.Kind() != s.expectedKind { if ctx.BooleanError { return ctxerr.BooleanError } return ctx.CollectError(ctxerr.BadKind(got, s.expectedKind.String())) } var ptr uintptr // Special case for strings if s.expectedKind == reflect.String { ptr = stringPointer(got.String()) } else { ptr = got.Pointer() } if ptr != s.expectedPointer { if ctx.BooleanError { return ctxerr.BooleanError } return ctx.CollectError(&ctxerr.Error{ Message: fmt.Sprintf("%s pointer mismatch", s.expectedKind), Got: types.RawString(fmt.Sprintf("0x%x", ptr)), Expected: types.RawString(fmt.Sprintf("0x%x", s.expectedPointer)), }) } return nil } func (s *tdShallow) String() string { if s.err != nil { return s.stringError() } return fmt.Sprintf("(%s) 0x%x", s.expectedKind, s.expectedPointer) } golang-github-maxatome-go-testdeep-1.14.0/td/td_shallow_test.go000066400000000000000000000077411454313311600245220ustar00rootroot00000000000000// Copyright (c) 2018, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td_test import ( "regexp" "testing" "github.com/maxatome/go-testdeep/internal/test" "github.com/maxatome/go-testdeep/td" ) func TestShallow(t *testing.T) { checkOK(t, nil, nil) // // Slice back := [...]int{1, 2, 3, 1, 2, 3} as := back[:3] bs := back[3:] checkError(t, bs, td.Shallow(back[:]), expectedError{ Message: mustBe("slice pointer mismatch"), Path: mustBe("DATA"), Got: mustContain("0x"), Expected: mustContain("0x"), }) checkOK(t, as, td.Shallow(back[:])) checkOK(t, ([]byte)(nil), ([]byte)(nil)) // // Map gotMap := map[string]bool{"a": true, "b": false} expectedMap := map[string]bool{"a": true, "b": false} checkError(t, gotMap, td.Shallow(expectedMap), expectedError{ Message: mustBe("map pointer mismatch"), Path: mustBe("DATA"), Got: mustContain("0x"), Expected: mustContain("0x"), }) expectedMap = gotMap checkOK(t, gotMap, td.Shallow(expectedMap)) checkOK(t, (map[string]bool)(nil), (map[string]bool)(nil)) // // Ptr type MyStruct struct { val int } gotPtr := &MyStruct{val: 12} expectedPtr := &MyStruct{val: 12} checkError(t, gotPtr, td.Shallow(expectedPtr), expectedError{ Message: mustBe("ptr pointer mismatch"), Path: mustBe("DATA"), Got: mustContain("0x"), Expected: mustContain("0x"), }) expectedPtr = gotPtr checkOK(t, gotPtr, td.Shallow(expectedPtr)) checkOK(t, (*MyStruct)(nil), (*MyStruct)(nil)) // // Func gotFunc := func(a int) int { return a * 2 } expectedFunc := func(a int) int { return a * 2 } checkError(t, gotFunc, td.Shallow(expectedFunc), expectedError{ Message: mustBe("func pointer mismatch"), Path: mustBe("DATA"), Got: mustContain("0x"), Expected: mustContain("0x"), }) expectedFunc = gotFunc checkOK(t, gotFunc, td.Shallow(expectedFunc)) checkOK(t, (func(a int) int)(nil), (func(a int) int)(nil)) // // Chan gotChan := make(chan int) expectedChan := make(chan int) checkError(t, gotChan, td.Shallow(expectedChan), expectedError{ Message: mustBe("chan pointer mismatch"), Path: mustBe("DATA"), Got: mustContain("0x"), Expected: mustContain("0x"), }) expectedChan = gotChan checkOK(t, gotChan, td.Shallow(expectedChan)) checkOK(t, (chan int)(nil), (chan int)(nil)) // // String backStr := "foobarfoobar!" a := backStr[:6] b := backStr[6:12] checkOK(t, a, td.Shallow(backStr)) checkOK(t, backStr, td.Shallow(a)) checkOK(t, b, td.Shallow(backStr[6:7])) checkError(t, backStr, td.Shallow(b), expectedError{ Message: mustBe("string pointer mismatch"), Path: mustBe("DATA"), Got: mustContain("0x"), Expected: mustContain("0x"), }) checkError(t, b, td.Shallow(backStr), expectedError{ Message: mustBe("string pointer mismatch"), Path: mustBe("DATA"), Got: mustContain("0x"), Expected: mustContain("0x"), }) // // Erroneous mix checkError(t, gotMap, td.Shallow(expectedChan), expectedError{ Message: mustBe("bad kind"), Path: mustBe("DATA"), Got: mustContain("map"), Expected: mustContain("chan"), }) // // Bad usage checkError(t, "never tested", td.Shallow(42), expectedError{ Message: mustBe("bad usage of Shallow operator"), Path: mustBe("DATA"), Summary: mustBe("usage: Shallow(CHANNEL|FUNC|MAP|PTR|SLICE|UNSAFE_PTR|STRING), but received int as 1st parameter"), }) // // reg := regexp.MustCompile(`^\(map\) 0x[a-f0-9]+\z`) if !reg.MatchString(td.Shallow(expectedMap).String()) { t.Errorf("Shallow().String() failed\n got: %s\nexpected: %s", td.Shallow(expectedMap).String(), reg) } // Erroneous op test.EqualStr(t, td.Shallow(42).String(), "Shallow()") } func TestShallowTypeBehind(t *testing.T) { equalTypes(t, td.Shallow(t), nil) // Erroneous op equalTypes(t, td.Shallow(42), nil) } golang-github-maxatome-go-testdeep-1.14.0/td/td_smuggle.go000066400000000000000000000563071454313311600234570ustar00rootroot00000000000000// Copyright (c) 2018-2023, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td import ( "bytes" "fmt" "io" "reflect" "strconv" "strings" "sync" "unicode" "unicode/utf8" "github.com/maxatome/go-testdeep/internal/ctxerr" "github.com/maxatome/go-testdeep/internal/types" "github.com/maxatome/go-testdeep/internal/util" ) // SmuggledGot can be returned by a [Smuggle] function to name the // transformed / returned value. type SmuggledGot struct { Name string Got any } const smuggled = "" var ( smuggleFnsMu sync.Mutex smuggleFns = map[any]reflect.Value{} nilError = reflect.New(types.Error).Elem() ) func (s SmuggledGot) contextAndGot(ctx ctxerr.Context) (ctxerr.Context, reflect.Value) { // If the Name starts with a Letter, prefix it by a "." var name string if s.Name != "" { first, _ := utf8.DecodeRuneInString(s.Name) if unicode.IsLetter(first) { name = "." } name += s.Name } else { name = smuggled } return ctx.AddCustomLevel(name), reflect.ValueOf(s.Got) } type tdSmuggle struct { tdSmugglerBase function reflect.Value argType reflect.Type str string } var _ TestDeep = &tdSmuggle{} type smuggleValue struct { Path string Value reflect.Value } var smuggleValueType = reflect.TypeOf(smuggleValue{}) type smuggleField struct { Name string Indexed bool } func joinFieldsPath(path []smuggleField) string { var buf strings.Builder for i, part := range path { if part.Indexed { fmt.Fprintf(&buf, "[%s]", part.Name) } else { if i > 0 { buf.WriteByte('.') } buf.WriteString(part.Name) } } return buf.String() } func splitFieldsPath(origPath string) ([]smuggleField, error) { if origPath == "" { return nil, fmt.Errorf("FIELD_PATH cannot be empty") } var res []smuggleField for path := origPath; len(path) > 0; { r, _ := utf8.DecodeRuneInString(path) switch r { case '[': path = path[1:] end := strings.IndexByte(path, ']') if end < 0 { return nil, fmt.Errorf("cannot find final ']' in FIELD_PATH %q", origPath) } res = append(res, smuggleField{Name: path[:end], Indexed: true}) path = path[end+1:] case '.': if len(res) == 0 { return nil, fmt.Errorf("'.' cannot be the first rune in FIELD_PATH %q", origPath) } path = path[1:] if path == "" { return nil, fmt.Errorf("final '.' in FIELD_PATH %q is not allowed", origPath) } r, _ = utf8.DecodeRuneInString(path) if r == '.' || r == '[' { return nil, fmt.Errorf("unexpected %q after '.' in FIELD_PATH %q", r, origPath) } fallthrough default: var field string end := strings.IndexAny(path, ".[") if end < 0 { field, path = path, "" } else { field, path = path[:end], path[end:] } for j, r := range field { if !unicode.IsLetter(r) && (j == 0 || !unicode.IsNumber(r)) { return nil, fmt.Errorf("unexpected %q in field name %q in FIELDS_PATH %q", r, field, origPath) } } res = append(res, smuggleField{Name: field}) } } return res, nil } func nilFieldErr(path []smuggleField) error { return fmt.Errorf("field %q is nil", joinFieldsPath(path)) } func buildFieldsPathFn(path string) (func(any) (smuggleValue, error), error) { parts, err := splitFieldsPath(path) if err != nil { return nil, err } return func(got any) (smuggleValue, error) { vgot := reflect.ValueOf(got) for idxPart, field := range parts { // Resolve all interface and pointer dereferences for { switch vgot.Kind() { case reflect.Interface, reflect.Ptr: if vgot.IsNil() { return smuggleValue{}, nilFieldErr(parts[:idxPart]) } vgot = vgot.Elem() continue } break } if !field.Indexed { if vgot.Kind() == reflect.Struct { vgot = vgot.FieldByName(field.Name) if !vgot.IsValid() { return smuggleValue{}, fmt.Errorf( "field %q not found", joinFieldsPath(parts[:idxPart+1])) } continue } if idxPart == 0 { return smuggleValue{}, fmt.Errorf("it is a %s and should be a struct", vgot.Kind()) } return smuggleValue{}, fmt.Errorf( "field %q is a %s and should be a struct", joinFieldsPath(parts[:idxPart]), vgot.Kind()) } switch vgot.Kind() { case reflect.Map: tkey := vgot.Type().Key() var vkey reflect.Value switch tkey.Kind() { case reflect.String: vkey = reflect.ValueOf(field.Name) case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: i, err := strconv.ParseInt(field.Name, 10, 64) if err != nil { return smuggleValue{}, fmt.Errorf( "field %q, %q is not an integer and so cannot match %s map key type", joinFieldsPath(parts[:idxPart+1]), field.Name, tkey) } vkey = reflect.ValueOf(i).Convert(tkey) case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: i, err := strconv.ParseUint(field.Name, 10, 64) if err != nil { return smuggleValue{}, fmt.Errorf( "field %q, %q is not an unsigned integer and so cannot match %s map key type", joinFieldsPath(parts[:idxPart+1]), field.Name, tkey) } vkey = reflect.ValueOf(i).Convert(tkey) case reflect.Float32, reflect.Float64: f, err := strconv.ParseFloat(field.Name, 64) if err != nil { return smuggleValue{}, fmt.Errorf( "field %q, %q is not a float and so cannot match %s map key type", joinFieldsPath(parts[:idxPart+1]), field.Name, tkey) } vkey = reflect.ValueOf(f).Convert(tkey) case reflect.Complex64, reflect.Complex128: c, err := strconv.ParseComplex(field.Name, 128) if err != nil { return smuggleValue{}, fmt.Errorf( "field %q, %q is not a complex number and so cannot match %s map key type", joinFieldsPath(parts[:idxPart+1]), field.Name, tkey) } vkey = reflect.ValueOf(c).Convert(tkey) default: return smuggleValue{}, fmt.Errorf( "field %q, %q cannot match unsupported %s map key type", joinFieldsPath(parts[:idxPart+1]), field.Name, tkey) } vgot = vgot.MapIndex(vkey) if !vgot.IsValid() { return smuggleValue{}, fmt.Errorf("field %q, %q map key not found", joinFieldsPath(parts[:idxPart+1]), field.Name) } case reflect.Slice, reflect.Array: i, err := strconv.ParseInt(field.Name, 10, 64) if err != nil { return smuggleValue{}, fmt.Errorf( "field %q, %q is not a slice/array index", joinFieldsPath(parts[:idxPart+1]), field.Name) } if i < 0 { i = int64(vgot.Len()) + i } if i < 0 || i >= int64(vgot.Len()) { return smuggleValue{}, fmt.Errorf( "field %q, %d is out of slice/array range (len %d)", joinFieldsPath(parts[:idxPart+1]), i, vgot.Len()) } vgot = vgot.Index(int(i)) default: if idxPart == 0 { return smuggleValue{}, fmt.Errorf("it is a %s, but a map, array or slice is expected", vgot.Kind()) } return smuggleValue{}, fmt.Errorf( "field %q is a %s, but a map, array or slice is expected", joinFieldsPath(parts[:idxPart]), vgot.Kind()) } } return smuggleValue{ Path: path, Value: vgot, }, nil }, nil } func getFieldsPathFn(fieldPath string) (reflect.Value, error) { smuggleFnsMu.Lock() defer smuggleFnsMu.Unlock() if vfn, ok := smuggleFns[fieldPath]; ok { return vfn, nil } fn, err := buildFieldsPathFn(fieldPath) if err != nil { return reflect.Value{}, err } vfn := reflect.ValueOf(fn) smuggleFns[fieldPath] = vfn return vfn, err } func getCaster(outType reflect.Type) reflect.Value { smuggleFnsMu.Lock() defer smuggleFnsMu.Unlock() if vfn, ok := smuggleFns[outType]; ok { return vfn } var fn reflect.Value switch outType.Kind() { case reflect.String: fn = buildCaster(outType, true) case reflect.Slice: if outType.Elem().Kind() == reflect.Uint8 { // Special case for slices of bytes: falls back on io.Reader if not []byte fn = buildCaster(outType, false) break } fallthrough default: // For all other types, take the received param and return // it. Smuggle already converted got to the type of param, so the // work is done. inOut := []reflect.Type{outType} fn = reflect.MakeFunc( reflect.FuncOf(inOut, inOut, false), func(args []reflect.Value) []reflect.Value { return args }, ) } smuggleFns[outType] = fn return fn } // buildCaster returns a function: // // func(in any) (out outType, err error) // // dynamically checks… // - if useString is false, as outType is a slice of bytes: // 1. in is a []byte or convertible to []byte // 2. in implements io.Reader // - if useString is true, as outType is a string: // 1. in is a []byte or convertible to string // 2. in implements io.Reader func buildCaster(outType reflect.Type, useString bool) reflect.Value { zeroRet := reflect.New(outType).Elem() return reflect.MakeFunc( reflect.FuncOf( []reflect.Type{types.Interface}, []reflect.Type{outType, types.Error}, false, ), func(args []reflect.Value) []reflect.Value { if args[0].IsNil() { return []reflect.Value{ zeroRet, reflect.ValueOf(&ctxerr.Error{ Message: "incompatible parameter type", Got: types.RawString("nil"), Expected: types.RawString(outType.String() + " or convertible or io.Reader"), }), } } // 1st & only arg is always an interface args[0] = args[0].Elem() if ok, convertible := types.IsTypeOrConvertible(args[0], outType); ok { if convertible { return []reflect.Value{args[0].Convert(outType), nilError} } return []reflect.Value{args[0], nilError} } // Our caller encures Interface() can be called safely switch ta := args[0].Interface().(type) { case io.Reader: var b bytes.Buffer if _, err := b.ReadFrom(ta); err != nil { return []reflect.Value{ zeroRet, reflect.ValueOf(&ctxerr.Error{ Message: "an error occurred while reading from io.Reader", Summary: ctxerr.NewSummary(err.Error()), }), } } var buf any if useString { buf = b.String() } else { buf = b.Bytes() } return []reflect.Value{ reflect.ValueOf(buf).Convert(outType), nilError, } default: return []reflect.Value{ zeroRet, reflect.ValueOf(&ctxerr.Error{ Message: "incompatible parameter type", Got: types.RawString(args[0].Type().String()), Expected: types.RawString(outType.String() + " or convertible or io.Reader"), }), } } }) } // summary(Smuggle): changes data contents or mutates it into another // type via a custom function or a struct fields-path before stepping // down in favor of generic comparison process // input(Smuggle): all // Smuggle operator allows to change data contents or mutate it into // another type before stepping down in favor of generic comparison // process. Of course it is a smuggler operator. So fn is a function // that must take one parameter whose type must be convertible to the // type of the compared value. // // As convenient shortcuts, fn can be a string specifying a // fields-path through structs, maps & slices, or any other type, in // this case a simple cast is done (see below for details). // // fn must return at least one value. These value will be compared as is // to expectedValue, here integer 28: // // td.Cmp(t, "0028", // td.Smuggle(func(value string) int { // num, _ := strconv.Atoi(value) // return num // }, 28), // ) // // or using an other [TestDeep] operator, here [Between](28, 30): // // td.Cmp(t, "0029", // td.Smuggle(func(value string) int { // num, _ := strconv.Atoi(value) // return num // }, td.Between(28, 30)), // ) // // fn can return a second boolean value, used to tell that a problem // occurred and so stop the comparison: // // td.Cmp(t, "0029", // td.Smuggle(func(value string) (int, bool) { // num, err := strconv.Atoi(value) // return num, err == nil // }, td.Between(28, 30)), // ) // // fn can return a third string value which is used to describe the // test when a problem occurred (false second boolean value): // // td.Cmp(t, "0029", // td.Smuggle(func(value string) (int, bool, string) { // num, err := strconv.Atoi(value) // if err != nil { // return 0, false, "string must contain a number" // } // return num, true, "" // }, td.Between(28, 30)), // ) // // Instead of returning (X, bool) or (X, bool, string), fn can // return (X, error). When a problem occurs, the returned error is // non-nil, as in: // // td.Cmp(t, "0029", // td.Smuggle(func(value string) (int, error) { // num, err := strconv.Atoi(value) // return num, err // }, td.Between(28, 30)), // ) // // Which can be simplified to: // // td.Cmp(t, "0029", td.Smuggle(strconv.Atoi, td.Between(28, 30))) // // Imagine you want to compare that the Year of a date is between 2010 // and 2020: // // td.Cmp(t, time.Date(2015, time.May, 1, 1, 2, 3, 0, time.UTC), // td.Smuggle(func(date time.Time) int { return date.Year() }, // td.Between(2010, 2020)), // ) // // In this case the data location forwarded to next test will be // something like "DATA.MyTimeField", but you can act on it // too by returning a [SmuggledGot] struct (by value or by address): // // td.Cmp(t, time.Date(2015, time.May, 1, 1, 2, 3, 0, time.UTC), // td.Smuggle(func(date time.Time) SmuggledGot { // return SmuggledGot{ // Name: "Year", // Got: date.Year(), // } // }, td.Between(2010, 2020)), // ) // // then the data location forwarded to next test will be something like // "DATA.MyTimeField.Year". The "." between the current path (here // "DATA.MyTimeField") and the returned Name "Year" is automatically // added when Name starts with a Letter. // // Note that [SmuggledGot] and [*SmuggledGot] returns are treated // equally, and they are only used when fn has only one returned value // or when the second boolean returned value is true. // // Of course, all cases can go together: // // // Accepts a "YYYY/mm/DD HH:MM:SS" string to produce a time.Time and tests // // whether this date is contained between 2 hours before now and now. // td.Cmp(t, "2020-01-25 12:13:14", // td.Smuggle(func(date string) (*SmuggledGot, bool, string) { // date, err := time.Parse("2006/01/02 15:04:05", date) // if err != nil { // return nil, false, `date must conform to "YYYY/mm/DD HH:MM:SS" format` // } // return &SmuggledGot{ // Name: "Date", // Got: date, // }, true, "" // }, td.Between(time.Now().Add(-2*time.Hour), time.Now())), // ) // // or: // // // Accepts a "YYYY/mm/DD HH:MM:SS" string to produce a time.Time and tests // // whether this date is contained between 2 hours before now and now. // td.Cmp(t, "2020-01-25 12:13:14", // td.Smuggle(func(date string) (*SmuggledGot, error) { // date, err := time.Parse("2006/01/02 15:04:05", date) // if err != nil { // return nil, err // } // return &SmuggledGot{ // Name: "Date", // Got: date, // }, nil // }, td.Between(time.Now().Add(-2*time.Hour), time.Now())), // ) // // Smuggle can also be used to access a struct field embedded in // several struct layers. // // type A struct{ Num int } // type B struct{ As map[string]*A } // type C struct{ B B } // got := C{B: B{As: map[string]*A{"foo": {Num: 12}}}} // // // Tests that got.B.A.Num is 12 // td.Cmp(t, got, // td.Smuggle(func(c C) int { // return c.B.As["foo"].Num // }, 12)) // // As brought up above, a fields-path can be passed as fn value // instead of a function pointer. Using this feature, the [Cmp] // call in the above example can be rewritten as follows: // // // Tests that got.B.As["foo"].Num is 12 // td.Cmp(t, got, td.Smuggle("B.As[foo].Num", 12)) // // Contrary to [JSONPointer] operator, private fields can be // followed. Arrays, slices and maps work using the index/key inside // square brackets (e.g. [12] or [foo]). Maps work only for simple key // types (string or numbers), without "" when using strings // (e.g. [foo]). // // Behind the scenes, a temporary function is automatically created to // achieve the same goal, but add some checks against nil values and // auto-dereference interfaces and pointers, even on several levels, // like in: // // type A struct{ N any } // num := 12 // pnum := &num // td.Cmp(t, A{N: &pnum}, td.Smuggle("N", 12)) // // Last but not least, a simple type can be passed as fn to operate // a cast, handling specifically strings and slices of bytes: // // td.Cmp(t, `{"foo":1}`, td.Smuggle(json.RawMessage{}, td.JSON(`{"foo":1}`))) // // or equally // td.Cmp(t, `{"foo":1}`, td.Smuggle(json.RawMessage(nil), td.JSON(`{"foo":1}`))) // // converts on the fly a string to a [json.RawMessage] so [JSON] operator // can parse it as JSON. This is mostly a shortcut for: // // td.Cmp(t, `{"foo":1}`, td.Smuggle( // func(r json.RawMessage) json.RawMessage { return r }, // td.JSON(`{"foo":1}`))) // // except that for strings and slices of bytes (like here), it accepts // [io.Reader] interface too: // // var body io.Reader // // … // td.Cmp(t, body, td.Smuggle(json.RawMessage{}, td.JSON(`{"foo":1}`))) // // or equally // td.Cmp(t, body, td.Smuggle(json.RawMessage(nil), td.JSON(`{"foo":1}`))) // // This last example allows to easily inject body content into JSON // operator. // // The difference between Smuggle and [Code] operators is that [Code] // is used to do a final comparison while Smuggle transforms the data // and then steps down in favor of generic comparison // process. Moreover, the type accepted as input for the function is // more lax to facilitate the writing of tests (e.g. the function can // accept a float64 and the got value be an int). See examples. On the // other hand, the output type is strict and must match exactly the // expected value type. The fields-path string fn shortcut and the // cast feature are not available with [Code] operator. // // TypeBehind method returns the [reflect.Type] of only parameter of // fn. For the case where fn is a fields-path, it is always // any, as the type can not be known in advance. // // See also [Code], [JSONPointer] and [Flatten]. // // [json.RawMessage]: https://pkg.go.dev/encoding/json#RawMessage func Smuggle(fn, expectedValue any) TestDeep { s := tdSmuggle{ tdSmugglerBase: newSmugglerBase(expectedValue), } const usage = "(FUNC|FIELDS_PATH|ANY_TYPE, TESTDEEP_OPERATOR|EXPECTED_VALUE)" const fullUsage = "Smuggle" + usage var vfn reflect.Value switch rfn := fn.(type) { case reflect.Type: switch rfn.Kind() { case reflect.Func, reflect.Invalid, reflect.Interface: s.err = ctxerr.OpBad("Smuggle", "usage: Smuggle%s, ANY_TYPE reflect.Type cannot be Func nor Interface", usage) return &s default: vfn = getCaster(rfn) s.str = "type:" + rfn.String() } case string: if rfn == "" { vfn = getCaster(reflect.TypeOf(fn)) s.str = "type:string" break } var err error vfn, err = getFieldsPathFn(rfn) if err != nil { s.err = ctxerr.OpBad("Smuggle", "Smuggle%s: %s", usage, err) return &s } s.str = strconv.Quote(rfn) default: vfn = reflect.ValueOf(fn) switch vfn.Kind() { case reflect.Func: s.str = vfn.Type().String() // nothing to check case reflect.Invalid, reflect.Interface: s.err = ctxerr.OpBad("Smuggle", "usage: Smuggle%s, ANY_TYPE cannot be nil nor Interface", usage) return &s default: typ := vfn.Type() vfn = getCaster(typ) s.str = "type:" + typ.String() } } fnType := vfn.Type() if fnType.IsVariadic() || fnType.NumIn() != 1 { s.err = ctxerr.OpBad("Smuggle", fullUsage+": FUNC must take only one non-variadic argument") return &s } switch fnType.NumOut() { case 3: // (value, bool, string) if fnType.Out(2).Kind() != reflect.String { break } fallthrough case 2: // (value, *bool*) or (value, *bool*, string) if fnType.Out(1).Kind() != reflect.Bool && // (value, *error*) (fnType.NumOut() > 2 || fnType.Out(1) != types.Error) { break } fallthrough case 1: // (value) if vfn.IsNil() { s.err = ctxerr.OpBad("Smuggle", "Smuggle(FUNC): FUNC cannot be a nil function") return &s } s.argType = fnType.In(0) s.function = vfn if !s.isTestDeeper { s.expectedValue = reflect.ValueOf(expectedValue) } return &s } s.err = ctxerr.OpBad("Smuggle", fullUsage+": FUNC must return value or (value, bool) or (value, bool, string) or (value, error)") return &s } func (s *tdSmuggle) laxConvert(got reflect.Value) (reflect.Value, bool) { if got.IsValid() { if types.IsConvertible(got, s.argType) { return got.Convert(s.argType), true } } else if s.argType == types.Interface { // nil only accepted if any expected return reflect.New(types.Interface).Elem(), true } return got, false } func (s *tdSmuggle) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error { if s.err != nil { return ctx.CollectError(s.err) } got, ok := s.laxConvert(got) if !ok { if ctx.BooleanError { return ctxerr.BooleanError } err := ctxerr.Error{ Message: "incompatible parameter type", Expected: types.RawString(s.argType.String()), } if got.IsValid() { err.Got = types.RawString(got.Type().String()) } else { err.Got = types.RawString("nil") } return ctx.CollectError(&err) } // Refuse to override unexported fields access in this case. It is a // choice, as we think it is better to work on surrounding struct // instead. if !got.CanInterface() { if ctx.BooleanError { return ctxerr.BooleanError } return ctx.CollectError(&ctxerr.Error{ Message: "cannot smuggle unexported field", Summary: ctxerr.NewSummary("work on surrounding struct instead"), }) } ret := s.function.Call([]reflect.Value{got}) if len(ret) == 1 || (ret[1].Kind() == reflect.Bool && ret[1].Bool()) || (ret[1].Kind() == reflect.Interface && ret[1].IsNil()) { newGot := ret[0] var newCtx ctxerr.Context if newGot.IsValid() { switch newGot.Type() { case smuggledGotType: newCtx, newGot = newGot.Interface().(SmuggledGot).contextAndGot(ctx) case smuggledGotPtrType: if smGot := newGot.Interface().(*SmuggledGot); smGot == nil { newCtx, newGot = ctx, reflect.ValueOf(nil) } else { newCtx, newGot = smGot.contextAndGot(ctx) } case smuggleValueType: smv := newGot.Interface().(smuggleValue) newCtx, newGot = ctx.AddCustomLevel("."+smv.Path), smv.Value default: newCtx = ctx.AddCustomLevel(smuggled) } } return deepValueEqual(newCtx, newGot, s.expectedValue) } if ctx.BooleanError { return ctxerr.BooleanError } var reason string switch len(ret) { case 3: // (value, false, string) reason = ret[2].String() case 2: // (value, error) if ret[1].Kind() == reflect.Interface { // For internal use only if cErr, ok := ret[1].Interface().(*ctxerr.Error); ok { return ctx.CollectError(cErr) } reason = ret[1].Interface().(error).Error() } // (value, false) } return ctx.CollectError(&ctxerr.Error{ Message: "ran smuggle code with %% as argument", Summary: ctxerr.NewSummaryReason(got, reason), }) } func (s *tdSmuggle) HandleInvalid() bool { return true // Knows how to handle untyped nil values (aka invalid values) } func (s *tdSmuggle) String() string { if s.err != nil { return s.stringError() } return "Smuggle(" + s.str + ", " + util.ToString(s.expectedValue) + ")" } func (s *tdSmuggle) TypeBehind() reflect.Type { if s.err != nil { return nil } return s.argType } golang-github-maxatome-go-testdeep-1.14.0/td/td_smuggle_private_test.go000066400000000000000000000115201454313311600262340ustar00rootroot00000000000000// Copyright (c) 2021, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td import ( "reflect" "testing" "github.com/maxatome/go-testdeep/internal/test" ) func TestFieldsPath(t *testing.T) { check := func(in string, expected ...string) []smuggleField { t.Helper() got, err := splitFieldsPath(in) test.NoError(t, err) var gotStr []string for _, s := range got { gotStr = append(gotStr, s.Name) } if !reflect.DeepEqual(gotStr, expected) { t.Errorf("Failed:\n got: %v\n expected: %v", got, expected) } test.EqualStr(t, in, joinFieldsPath(got)) return got } check("test", "test") check("test.foo.bar", "test", "foo", "bar") check("test.foo.bar", "test", "foo", "bar") check("test[foo.bar]", "test", "foo.bar") check("test[foo][bar]", "test", "foo", "bar") fp := check("test[foo][bar].zip", "test", "foo", "bar", "zip") // "." can be omitted just after "]" got, err := splitFieldsPath("test[foo][bar]zip") test.NoError(t, err) if !reflect.DeepEqual(got, fp) { t.Errorf("Failed:\n got: %v\n expected: %v", got, fp) } // // Errors checkErr := func(in, expectedErr string) { t.Helper() _, err := splitFieldsPath(in) if test.Error(t, err) { test.EqualStr(t, err.Error(), expectedErr) } } checkErr("", "FIELD_PATH cannot be empty") checkErr(".test", `'.' cannot be the first rune in FIELD_PATH ".test"`) checkErr("foo.bar.", `final '.' in FIELD_PATH "foo.bar." is not allowed`) checkErr("foo..bar", `unexpected '.' after '.' in FIELD_PATH "foo..bar"`) checkErr("foo.[bar]", `unexpected '[' after '.' in FIELD_PATH "foo.[bar]"`) checkErr("foo[bar", `cannot find final ']' in FIELD_PATH "foo[bar"`) checkErr("test.%foo", `unexpected '%' in field name "%foo" in FIELDS_PATH "test.%foo"`) checkErr("test.f%oo", `unexpected '%' in field name "f%oo" in FIELDS_PATH "test.f%oo"`) checkErr("foo[bar", `cannot find final ']' in FIELD_PATH "foo[bar"`) } func TestBuildFieldsPathFn(t *testing.T) { _, err := buildFieldsPathFn("bad[path") test.Error(t, err) // // Struct type Build struct { Field struct { Path string } Iface any } fn, err := buildFieldsPathFn("Field.Path.Bad") if test.NoError(t, err) { _, err = fn(Build{}) if test.Error(t, err) { test.EqualStr(t, err.Error(), `field "Field.Path" is a string and should be a struct`) } _, err = fn(123) if test.Error(t, err) { test.EqualStr(t, err.Error(), "it is a int and should be a struct") } } fn, err = buildFieldsPathFn("Field.Unknown") if test.NoError(t, err) { _, err = fn(Build{}) if test.Error(t, err) { test.EqualStr(t, err.Error(), `field "Field.Unknown" not found`) } } // // Map fn, err = buildFieldsPathFn("Iface[str].Field") if test.NoError(t, err) { _, err = fn(Build{Iface: map[int]Build{}}) if test.Error(t, err) { test.EqualStr(t, err.Error(), `field "Iface[str]", "str" is not an integer and so cannot match int map key type`) } _, err = fn(Build{Iface: map[uint]Build{}}) if test.Error(t, err) { test.EqualStr(t, err.Error(), `field "Iface[str]", "str" is not an unsigned integer and so cannot match uint map key type`) } _, err = fn(Build{Iface: map[float32]Build{}}) if test.Error(t, err) { test.EqualStr(t, err.Error(), `field "Iface[str]", "str" is not a float and so cannot match float32 map key type`) } _, err = fn(Build{Iface: map[complex128]Build{}}) if test.Error(t, err) { test.EqualStr(t, err.Error(), `field "Iface[str]", "str" is not a complex number and so cannot match complex128 map key type`) } _, err = fn(Build{Iface: map[struct{ A int }]Build{}}) if test.Error(t, err) { test.EqualStr(t, err.Error(), `field "Iface[str]", "str" cannot match unsupported struct { A int } map key type`) } _, err = fn(Build{Iface: map[string]Build{}}) if test.Error(t, err) { test.EqualStr(t, err.Error(), `field "Iface[str]", "str" map key not found`) } } // // Array / Slice fn, err = buildFieldsPathFn("Iface[str].Field") if test.NoError(t, err) { _, err = fn(Build{Iface: []int{}}) if test.Error(t, err) { test.EqualStr(t, err.Error(), `field "Iface[str]", "str" is not a slice/array index`) } } fn, err = buildFieldsPathFn("Iface[18].Field") if test.NoError(t, err) { _, err = fn(Build{Iface: []int{1, 2, 3}}) if test.Error(t, err) { test.EqualStr(t, err.Error(), `field "Iface[18]", 18 is out of slice/array range (len 3)`) } _, err = fn(Build{Iface: 42}) if test.Error(t, err) { test.EqualStr(t, err.Error(), `field "Iface" is a int, but a map, array or slice is expected`) } } fn, err = buildFieldsPathFn("[18].Field") if test.NoError(t, err) { _, err = fn(42) test.EqualStr(t, err.Error(), `it is a int, but a map, array or slice is expected`) } } golang-github-maxatome-go-testdeep-1.14.0/td/td_smuggle_test.go000066400000000000000000000502141454313311600245050ustar00rootroot00000000000000// Copyright (c) 2018-2023, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td_test import ( "bytes" "encoding/json" "errors" "fmt" "io" "reflect" "testing" "time" "github.com/maxatome/go-testdeep/internal/ctxerr" "github.com/maxatome/go-testdeep/internal/test" "github.com/maxatome/go-testdeep/td" ) // reArmReader is a bytes.Reader that re-arms when an error occurs, // typically on EOF. type reArmReader bytes.Reader var _ io.Reader = (*reArmReader)(nil) func newReArmReader(b []byte) *reArmReader { return (*reArmReader)(bytes.NewReader(b)) } func (r *reArmReader) Read(b []byte) (n int, err error) { n, err = (*bytes.Reader)(r).Read(b) if err != nil { (*bytes.Reader)(r).Seek(0, io.SeekStart) //nolint: errcheck } return } func (r *reArmReader) String() string { return "" } func TestSmuggle(t *testing.T) { num := 42 gotStruct := MyStruct{ MyStructMid: MyStructMid{ MyStructBase: MyStructBase{ ValBool: true, }, ValStr: "foobar", }, ValInt: 123, Ptr: &num, } gotTime, err := time.Parse(time.RFC3339, "2018-05-23T12:13:14Z") if err != nil { t.Fatal(err) } // // One returned value checkOK(t, gotTime, td.Smuggle( func(date time.Time) int { return date.Year() }, td.Between(2010, 2020))) checkOK(t, gotStruct, td.Smuggle( func(s MyStruct) td.SmuggledGot { return td.SmuggledGot{ Name: "ValStr", Got: s.ValStr, } }, td.Contains("oob"))) checkOK(t, gotStruct, td.Smuggle( func(s MyStruct) *td.SmuggledGot { return &td.SmuggledGot{ Name: "ValStr", Got: s.ValStr, } }, td.Contains("oob"))) // // 2 returned values checkOK(t, gotStruct, td.Smuggle( func(s MyStruct) (string, bool) { if s.ValStr == "" { return "", false } return s.ValStr, true }, td.Contains("oob"))) checkOK(t, gotStruct, td.Smuggle( func(s MyStruct) (td.SmuggledGot, bool) { if s.ValStr == "" { return td.SmuggledGot{}, false } return td.SmuggledGot{ Name: "ValStr", Got: s.ValStr, }, true }, td.Contains("oob"))) checkOK(t, gotStruct, td.Smuggle( func(s MyStruct) (*td.SmuggledGot, bool) { if s.ValStr == "" { return nil, false } return &td.SmuggledGot{ Name: "ValStr", Got: s.ValStr, }, true }, td.Contains("oob"))) // // 3 returned values checkOK(t, gotStruct, td.Smuggle( func(s MyStruct) (string, bool, string) { if s.ValStr == "" { return "", false, "ValStr must not be empty" } return s.ValStr, true, "" }, td.Contains("oob"))) checkOK(t, gotStruct, td.Smuggle( func(s MyStruct) (td.SmuggledGot, bool, string) { if s.ValStr == "" { return td.SmuggledGot{}, false, "ValStr must not be empty" } return td.SmuggledGot{ Name: "ValStr", Got: s.ValStr, }, true, "" }, td.Contains("oob"))) checkOK(t, gotStruct, td.Smuggle( func(s MyStruct) (*td.SmuggledGot, bool, string) { if s.ValStr == "" { return nil, false, "ValStr must not be empty" } return &td.SmuggledGot{ Name: "ValStr", Got: s.ValStr, }, true, "" }, td.Contains("oob"))) // // Convertible types checkOK(t, 123, td.Smuggle(func(n float64) int { return int(n) }, 123)) type xInt int checkOK(t, xInt(123), td.Smuggle(func(n int) int64 { return int64(n) }, int64(123))) checkOK(t, xInt(123), td.Smuggle(func(n uint32) int64 { return int64(n) }, int64(123))) checkOK(t, int32(123), td.Smuggle(func(n int64) int { return int(n) }, 123)) checkOK(t, gotTime, td.Smuggle(func(t fmt.Stringer) string { return t.String() }, "2018-05-23 12:13:14 +0000 UTC")) checkOK(t, []byte("{}"), td.Smuggle( func(x json.RawMessage) json.RawMessage { return x }, td.JSON(`{}`))) // // bytes slice caster variations checkOK(t, []byte(`{"foo":1}`), td.Smuggle(json.RawMessage{}, td.JSON(`{"foo":1}`))) checkOK(t, []byte(`{"foo":1}`), td.Smuggle(json.RawMessage(nil), td.JSON(`{"foo":1}`))) checkOK(t, []byte(`{"foo":1}`), td.Smuggle(reflect.TypeOf(json.RawMessage(nil)), td.JSON(`{"foo":1}`))) checkOK(t, `{"foo":1}`, td.Smuggle(json.RawMessage{}, td.JSON(`{"foo":1}`))) checkOK(t, newReArmReader([]byte(`{"foo":1}`)), // io.Reader first td.Smuggle(json.RawMessage{}, td.JSON(`{"foo":1}`))) checkError(t, nil, td.Smuggle(json.RawMessage{}, td.JSON(`{}`)), expectedError{ Message: mustBe("incompatible parameter type"), Path: mustBe("DATA"), Got: mustBe("nil"), Expected: mustBe("json.RawMessage or convertible or io.Reader"), }) checkError(t, MyStruct{}, td.Smuggle(json.RawMessage{}, td.JSON(`{}`)), expectedError{ Message: mustBe("incompatible parameter type"), Path: mustBe("DATA"), Got: mustBe("td_test.MyStruct"), Expected: mustBe("json.RawMessage or convertible or io.Reader"), }) checkError(t, errReader{}, // erroneous io.Reader td.Smuggle(json.RawMessage{}, td.JSON(`{}`)), expectedError{ Message: mustBe("an error occurred while reading from io.Reader"), Path: mustBe("DATA"), Summary: mustBe("an error occurred"), }) // // strings caster variations type myString string checkOK(t, `pipo bingo`, td.Smuggle("", td.HasSuffix("bingo"))) checkOK(t, []byte(`pipo bingo`), td.Smuggle(myString(""), td.HasSuffix("bingo"))) checkOK(t, []byte(`pipo bingo`), td.Smuggle(reflect.TypeOf(myString("")), td.HasSuffix("bingo"))) checkOK(t, newReArmReader([]byte(`pipo bingo`)), // io.Reader first td.Smuggle(myString(""), td.HasSuffix("bingo"))) checkError(t, nil, td.Smuggle("", "bingo"), expectedError{ Message: mustBe("incompatible parameter type"), Path: mustBe("DATA"), Got: mustBe("nil"), Expected: mustBe("string or convertible or io.Reader"), }) checkError(t, MyStruct{}, td.Smuggle(myString(""), "bingo"), expectedError{ Message: mustBe("incompatible parameter type"), Path: mustBe("DATA"), Got: mustBe("td_test.MyStruct"), Expected: mustBe("td_test.myString or convertible or io.Reader"), }) checkError(t, errReader{}, // erroneous io.Reader td.Smuggle("", "bingo"), expectedError{ Message: mustBe("an error occurred while reading from io.Reader"), Path: mustBe("DATA"), Summary: mustBe("an error occurred"), }) // // Any other caster variations checkOK(t, `pipo bingo`, td.Smuggle([]rune{}, td.Contains([]rune(`bing`)))) checkOK(t, `pipo bingo`, td.Smuggle(([]rune)(nil), td.Contains([]rune(`bing`)))) checkOK(t, `pipo bingo`, td.Smuggle(reflect.TypeOf([]rune{}), td.Contains([]rune(`bing`)))) checkOK(t, 123.456, td.Smuggle(int64(0), int64(123))) checkOK(t, 123.456, td.Smuggle(reflect.TypeOf(int64(0)), int64(123))) // // Errors checkError(t, "123", td.Smuggle(func(n float64) int { return int(n) }, 123), expectedError{ Message: mustBe("incompatible parameter type"), Path: mustBe("DATA"), Got: mustBe("string"), Expected: mustBe("float64"), }) checkError(t, nil, td.Smuggle(func(n int64) int { return int(n) }, 123), expectedError{ Message: mustBe("incompatible parameter type"), Path: mustBe("DATA"), Got: mustBe("nil"), Expected: mustBe("int64"), }) checkError(t, 12, td.Smuggle(func(n int) (int, bool) { return n, false }, 12), expectedError{ Message: mustBe("ran smuggle code with %% as argument"), Path: mustBe("DATA"), Summary: mustBe(" value: 12\nit failed but didn't say why"), }) type MyBool bool type MyString string checkError(t, 12, td.Smuggle(func(n int) (int, MyBool, MyString) { return n, false, "very custom error" }, 12), expectedError{ Message: mustBe("ran smuggle code with %% as argument"), Path: mustBe("DATA"), Summary: mustBe(" value: 12\nit failed coz: very custom error"), }) checkError(t, 12, td.Smuggle(func(n int) (int, error) { return n, errors.New("very custom error") }, 12), expectedError{ Message: mustBe("ran smuggle code with %% as argument"), Path: mustBe("DATA"), Summary: mustBe(" value: 12\nit failed coz: very custom error"), }) checkError(t, 12, td.Smuggle(func(n int) *td.SmuggledGot { return nil }, int64(13)), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA"), Got: mustBe("nil"), Expected: mustBe("(int64) 13"), }) // Internal use checkError(t, 12, td.Smuggle(func(n int) (int, error) { return n, &ctxerr.Error{ Message: "my message", Summary: ctxerr.NewSummary("my summary"), } }, 13), expectedError{ Message: mustBe("my message"), Path: mustBe("DATA"), Summary: mustBe("my summary"), }) // // Errors behind Smuggle() checkError(t, 12, td.Smuggle(func(n int) int64 { return int64(n) }, int64(13)), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA"), Got: mustBe("(int64) 12"), Expected: mustBe("(int64) 13"), }) checkError(t, gotStruct, td.Smuggle("MyStructMid.MyStructBase.ValBool", false), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA.MyStructMid.MyStructBase.ValBool"), Got: mustBe("true"), Expected: mustBe("false"), }) checkError(t, 12, td.Smuggle(func(n int) td.SmuggledGot { return td.SmuggledGot{ // With Name = "" Got: int64(n), } }, int64(13)), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA"), Got: mustBe("(int64) 12"), Expected: mustBe("(int64) 13"), }) checkError(t, 12, td.Smuggle(func(n int) *td.SmuggledGot { return &td.SmuggledGot{ Name: "", Got: int64(n), } }, int64(13)), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA"), // no dot added between DATA and Got: mustBe("(int64) 12"), Expected: mustBe("(int64) 13"), }) checkError(t, 12, td.Smuggle(func(n int) *td.SmuggledGot { return &td.SmuggledGot{ Name: "Int64", Got: int64(n), } }, int64(13)), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA.Int64"), // dot added between DATA and Int64 Got: mustBe("(int64) 12"), Expected: mustBe("(int64) 13"), }) // // Bad usage const usage = "Smuggle(FUNC|FIELDS_PATH|ANY_TYPE, TESTDEEP_OPERATOR|EXPECTED_VALUE): " checkError(t, "never tested", td.Smuggle(nil, 12), expectedError{ Message: mustBe("bad usage of Smuggle operator"), Path: mustBe("DATA"), Summary: mustBe("usage: " + usage[:len(usage)-2] + ", ANY_TYPE cannot be nil nor Interface"), }) checkError(t, nil, td.Smuggle(reflect.TypeOf((*fmt.Stringer)(nil)).Elem(), 1234), expectedError{ Message: mustBe("bad usage of Smuggle operator"), Path: mustBe("DATA"), Summary: mustBe("usage: " + usage[:len(usage)-2] + ", ANY_TYPE reflect.Type cannot be Func nor Interface"), }) checkError(t, nil, td.Smuggle(reflect.TypeOf(func() {}), 1234), expectedError{ Message: mustBe("bad usage of Smuggle operator"), Path: mustBe("DATA"), Summary: mustBe("usage: " + usage[:len(usage)-2] + ", ANY_TYPE reflect.Type cannot be Func nor Interface"), }) checkError(t, "never tested", td.Smuggle((func(string) int)(nil), 12), expectedError{ Message: mustBe("bad usage of Smuggle operator"), Path: mustBe("DATA"), Summary: mustBe("Smuggle(FUNC): FUNC cannot be a nil function"), }) checkError(t, "never tested", td.Smuggle("bad[path", 12), expectedError{ Message: mustBe("bad usage of Smuggle operator"), Path: mustBe("DATA"), Summary: mustBe(usage + `cannot find final ']' in FIELD_PATH "bad[path"`), }) // Bad number of args checkError(t, "never tested", td.Smuggle(func() int { return 0 }, 12), expectedError{ Message: mustBe("bad usage of Smuggle operator"), Path: mustBe("DATA"), Summary: mustBe(usage + "FUNC must take only one non-variadic argument"), }) checkError(t, "never tested", td.Smuggle(func(x ...int) int { return 0 }, 12), expectedError{ Message: mustBe("bad usage of Smuggle operator"), Path: mustBe("DATA"), Summary: mustBe(usage + "FUNC must take only one non-variadic argument"), }) checkError(t, "never tested", td.Smuggle(func(a int, b string) int { return 0 }, 12), expectedError{ Message: mustBe("bad usage of Smuggle operator"), Path: mustBe("DATA"), Summary: mustBe(usage + "FUNC must take only one non-variadic argument"), }) // Bad number of returned values const errMesg = usage + "FUNC must return value or (value, bool) or (value, bool, string) or (value, error)" checkError(t, "never tested", td.Smuggle(func(a int) {}, 12), expectedError{ Message: mustBe("bad usage of Smuggle operator"), Path: mustBe("DATA"), Summary: mustBe(errMesg), }) checkError(t, "never tested", td.Smuggle( func(a int) (int, bool, string, int) { return 0, false, "", 23 }, 12), expectedError{ Message: mustBe("bad usage of Smuggle operator"), Path: mustBe("DATA"), Summary: mustBe(errMesg), }) // Bad returned types checkError(t, "never tested", td.Smuggle(func(a int) (int, int) { return 0, 0 }, 12), expectedError{ Message: mustBe("bad usage of Smuggle operator"), Path: mustBe("DATA"), Summary: mustBe(errMesg), }) checkError(t, "never tested", td.Smuggle(func(a int) (int, bool, int) { return 0, false, 23 }, 12), expectedError{ Message: mustBe("bad usage of Smuggle operator"), Path: mustBe("DATA"), Summary: mustBe(errMesg), }) checkError(t, "never tested", td.Smuggle(func(a int) (int, error, string) { return 0, nil, "" }, 12), //nolint: staticcheck expectedError{ Message: mustBe("bad usage of Smuggle operator"), Path: mustBe("DATA"), Summary: mustBe(errMesg), }) // // String test.EqualStr(t, td.Smuggle(func(n int) int { return 0 }, 12).String(), "Smuggle(func(int) int, 12)") test.EqualStr(t, td.Smuggle(func(n int) (int, bool) { return 23, false }, 12).String(), "Smuggle(func(int) (int, bool), 12)") test.EqualStr(t, td.Smuggle(func(n int) (int, error) { return 23, nil }, 12).String(), "Smuggle(func(int) (int, error), 12)") test.EqualStr(t, td.Smuggle(func(n int) (int, MyBool, MyString) { return 23, false, "" }, 12). String(), "Smuggle(func(int) (int, td_test.MyBool, td_test.MyString), 12)") test.EqualStr(t, td.Smuggle(reflect.TypeOf(42), 23).String(), "Smuggle(type:int, 23)") test.EqualStr(t, td.Smuggle(666, 23).String(), "Smuggle(type:int, 23)") test.EqualStr(t, td.Smuggle("", 23).String(), "Smuggle(type:string, 23)") test.EqualStr(t, td.Smuggle("name", "bob").String(), `Smuggle("name", "bob")`) // Erroneous op test.EqualStr(t, td.Smuggle((func(int) int)(nil), 12).String(), "Smuggle()") } func TestSmuggleFieldsPath(t *testing.T) { num := 42 gotStruct := MyStruct{ MyStructMid: MyStructMid{ MyStructBase: MyStructBase{ ValBool: true, }, ValStr: "foobar", }, ValInt: 123, Ptr: &num, } type A struct { Num int Str string } type C struct { A A PA1 *A PA2 *A Iface1 any Iface2 any Iface3 any Iface4 any } type B struct { A A PA *A PppA ***A Iface any Iface2 any Iface3 any C *C } pa := &A{Num: 3, Str: "three"} ppa := &pa b := B{ A: A{Num: 1, Str: "one"}, PA: &A{Num: 2, Str: "two"}, PppA: &ppa, Iface: A{Num: 4, Str: "four"}, Iface2: &ppa, Iface3: nil, C: &C{ A: A{Num: 5, Str: "five"}, PA1: &A{Num: 6, Str: "six"}, PA2: nil, // explicit to be clear Iface1: A{Num: 7, Str: "seven"}, Iface2: &A{Num: 8, Str: "eight"}, Iface3: nil, // explicit to be clear Iface4: (*A)(nil), }, } // // OK checkOK(t, gotStruct, td.Smuggle("ValInt", 123)) checkOK(t, gotStruct, td.Smuggle("MyStructMid.ValStr", td.Contains("oob"))) checkOK(t, gotStruct, td.Smuggle("MyStructMid.MyStructBase.ValBool", true)) checkOK(t, gotStruct, td.Smuggle("ValBool", true)) // thanks to composition // OK across pointers checkOK(t, b, td.Smuggle("PA.Num", 2)) checkOK(t, b, td.Smuggle("PppA.Num", 3)) // OK with any checkOK(t, b, td.Smuggle("Iface.Num", 4)) checkOK(t, b, td.Smuggle("Iface2.Num", 3)) checkOK(t, b, td.Smuggle("C.Iface1.Num", 7)) checkOK(t, b, td.Smuggle("C.Iface2.Num", 8)) // Errors checkError(t, 12, td.Smuggle("foo.bar", 23), expectedError{ Message: mustBe("ran smuggle code with %% as argument"), Path: mustBe("DATA"), Summary: mustBe(" value: 12\nit failed coz: it is a int and should be a struct"), }) checkError(t, gotStruct, td.Smuggle("ValInt.bar", 23), expectedError{ Message: mustBe("ran smuggle code with %% as argument"), Path: mustBe("DATA"), Summary: mustContain("\nit failed coz: field \"ValInt\" is a int and should be a struct"), }) checkError(t, gotStruct, td.Smuggle("MyStructMid.ValStr.foobar", 23), expectedError{ Message: mustBe("ran smuggle code with %% as argument"), Path: mustBe("DATA"), Summary: mustContain("\nit failed coz: field \"MyStructMid.ValStr\" is a string and should be a struct"), }) checkError(t, gotStruct, td.Smuggle("foo.bar", 23), expectedError{ Message: mustBe("ran smuggle code with %% as argument"), Path: mustBe("DATA"), Summary: mustContain("\nit failed coz: field \"foo\" not found"), }) checkError(t, b, td.Smuggle("C.PA2.Num", 456), expectedError{ Message: mustBe("ran smuggle code with %% as argument"), Path: mustBe("DATA"), Summary: mustContain("\nit failed coz: field \"C.PA2\" is nil"), }) checkError(t, b, td.Smuggle("C.Iface3.Num", 456), expectedError{ Message: mustBe("ran smuggle code with %% as argument"), Path: mustBe("DATA"), Summary: mustContain("\nit failed coz: field \"C.Iface3\" is nil"), }) checkError(t, b, td.Smuggle("C.Iface4.Num", 456), expectedError{ Message: mustBe("ran smuggle code with %% as argument"), Path: mustBe("DATA"), Summary: mustContain("\nit failed coz: field \"C.Iface4\" is nil"), }) checkError(t, b, td.Smuggle("Iface3.Num", 456), expectedError{ Message: mustBe("ran smuggle code with %% as argument"), Path: mustBe("DATA"), Summary: mustContain("\nit failed coz: field \"Iface3\" is nil"), }) // Referencing maps and array/slices x := B{ Iface: map[string]any{ "test": []int{2, 3, 4}, }, C: &C{ Iface1: []any{ map[int]any{42: []string{"pipo"}, 66: [2]string{"foo", "bar"}}, map[int8]any{42: []string{"pipo"}}, map[int16]any{42: []string{"pipo"}}, map[int32]any{42: []string{"pipo"}}, map[int64]any{42: []string{"pipo"}}, map[uint]any{42: []string{"pipo"}}, map[uint8]any{42: []string{"pipo"}}, map[uint16]any{42: []string{"pipo"}}, map[uint32]any{42: []string{"pipo"}}, map[uint64]any{42: []string{"pipo"}}, map[uintptr]any{42: []string{"pipo"}}, map[float32]any{42: []string{"pipo"}}, map[float64]any{42: []string{"pipo"}}, }, }, } checkOK(t, x, td.Smuggle("Iface[test][1]", 3)) checkOK(t, x, td.Smuggle("C.Iface1[0][66][1]", "bar")) for i := 0; i < 12; i++ { checkOK(t, x, td.Smuggle(fmt.Sprintf("C.Iface1[%d][42][0]", i), "pipo")) checkOK(t, x, td.Smuggle(fmt.Sprintf("C.Iface1[%d][42][-1]", i-12), "pipo")) } checkOK(t, x, td.Lax(td.Smuggle("PppA", nil))) checkOK(t, x, td.Smuggle("PppA", td.Nil())) // type D struct { Iface any } got := D{ Iface: []any{ map[complex64]any{complex(42, 0): []string{"pipo"}}, map[complex128]any{complex(42, 0): []string{"pipo"}}, }, } for i := 0; i < 2; i++ { checkOK(t, got, td.Smuggle(fmt.Sprintf("Iface[%d][42][0]", i), "pipo")) checkOK(t, got, td.Smuggle(fmt.Sprintf("Iface[%d][42][0]", i-2), "pipo")) } } func TestSmuggleTypeBehind(t *testing.T) { // Type behind is the smuggle function parameter one equalTypes(t, td.Smuggle(func(n int) bool { return n != 0 }, true), 23) type MyTime time.Time equalTypes(t, td.Smuggle( func(t MyTime) time.Time { return time.Time(t) }, time.Now()), MyTime{}) equalTypes(t, td.Smuggle(func(from any) any { return from }, nil), reflect.TypeOf((*any)(nil)).Elem()) equalTypes(t, td.Smuggle("foo.bar", nil), reflect.TypeOf((*any)(nil)).Elem()) // Erroneous op equalTypes(t, td.Smuggle((func(int) int)(nil), 12), nil) } golang-github-maxatome-go-testdeep-1.14.0/td/td_smuggler_base.go000066400000000000000000000053041454313311600246220ustar00rootroot00000000000000// Copyright (c) 2018, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td import ( "encoding/json" "fmt" "reflect" "github.com/maxatome/go-testdeep/internal/ctxerr" ) // tdSmugglerBase is the base class of all smuggler TestDeep operators. type tdSmugglerBase struct { base expectedValue reflect.Value isTestDeeper bool } func newSmugglerBase(val any, depth ...int) (ret tdSmugglerBase) { callDepth := 4 if len(depth) > 0 { callDepth += depth[0] } ret.base = newBase(callDepth) // Initializes only if TestDeep operator. Other cases are specific. if _, ok := val.(TestDeep); ok { ret.expectedValue = reflect.ValueOf(val) ret.isTestDeeper = true } return } // internalTypeBehind returns the type behind expectedValue or nil if // it cannot be determined. func (s *tdSmugglerBase) internalTypeBehind() reflect.Type { if s.isTestDeeper { return s.expectedValue.Interface().(TestDeep).TypeBehind() } if s.expectedValue.IsValid() { return s.expectedValue.Type() } return nil } // jsonValueEqual compares "got" to expectedValue, trying to do it // using a JSON point of view. It is the caller responsibility to // ensure that "got" value is either a bool, float64, string, // []any, a map[string]any or simply nil. // // If the type behind expectedValue can be determined and is different // from "got" type, "got" value is JSON marshaled, then unmarshaled // in a new value of this type. This new value is then compared to // expectedValue. // // Otherwise, "got" value is compared as-is to expectedValue. func (s *tdSmugglerBase) jsonValueEqual(ctx ctxerr.Context, got any) *ctxerr.Error { expectedType := s.internalTypeBehind() // Unknown expected type (operator with nil TypeBehind() result or // untyped nil), lets deepValueEqual() handles the comparison using // BeLax flag if expectedType == nil { return deepValueEqual(ctx, reflect.ValueOf(got), s.expectedValue) } // Same type for got & expected type, no need to Marshal/Unmarshal if got != nil && expectedType == reflect.TypeOf(got) { return deepValueEqual(ctx, reflect.ValueOf(got), s.expectedValue) } // Unmarshal got into the expectedType b, _ := json.Marshal(got) // No error can occur here finalGot := reflect.New(expectedType) if err := json.Unmarshal(b, finalGot.Interface()); err != nil { if ctx.BooleanError { return ctxerr.BooleanError } return ctx.CollectError(&ctxerr.Error{ Message: fmt.Sprintf( "an error occurred while unmarshalling JSON into %s", expectedType), Summary: ctxerr.NewSummary(err.Error()), }) } return deepValueEqual(ctx, finalGot.Elem(), s.expectedValue) } golang-github-maxatome-go-testdeep-1.14.0/td/td_string.go000066400000000000000000000124311454313311600233100ustar00rootroot00000000000000// Copyright (c) 2018, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td import ( "fmt" "reflect" "strings" "github.com/maxatome/go-testdeep/internal/ctxerr" "github.com/maxatome/go-testdeep/internal/types" "github.com/maxatome/go-testdeep/internal/util" ) type tdStringBase struct { base expected string } func newStringBase(expected string) tdStringBase { return tdStringBase{ base: newBase(4), expected: expected, } } func getString(ctx ctxerr.Context, got reflect.Value) (string, *ctxerr.Error) { switch got.Kind() { case reflect.String: return got.String(), nil case reflect.Slice: if got.Type().Elem() == types.Uint8 { return string(got.Bytes()), nil } fallthrough default: if got.CanInterface() { switch iface := got.Interface().(type) { case error: return iface.Error(), nil case fmt.Stringer: return iface.String(), nil } } } if ctx.BooleanError { return "", ctxerr.BooleanError } return "", ctx.CollectError(&ctxerr.Error{ Message: "bad type", Got: types.RawString(got.Type().String()), Expected: types.RawString( "string (convertible) OR []byte (convertible) OR fmt.Stringer OR error"), }) } type tdString struct { tdStringBase } var _ TestDeep = &tdString{} // summary(String): checks a string, []byte, error or fmt.Stringer // interfaces string contents // input(String): str,slice([]byte),if(✓ + fmt.Stringer/error) // String operator allows to compare a string (or convertible), []byte // (or convertible), error or [fmt.Stringer] interface (error interface // is tested before [fmt.Stringer]). // // err := errors.New("error!") // td.Cmp(t, err, td.String("error!")) // succeeds // // bstr := bytes.NewBufferString("fmt.Stringer!") // td.Cmp(t, bstr, td.String("fmt.Stringer!")) // succeeds // // See also [Contains], [HasPrefix], [HasSuffix], [Re] and [ReAll]. func String(expected string) TestDeep { return &tdString{ tdStringBase: newStringBase(expected), } } func (s *tdString) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error { str, err := getString(ctx, got) if err != nil { return err } if str == s.expected { return nil } if ctx.BooleanError { return ctxerr.BooleanError } return ctx.CollectError(&ctxerr.Error{ Message: "does not match", Got: str, Expected: s, }) } func (s *tdString) String() string { return util.ToString(s.expected) } type tdHasPrefix struct { tdStringBase } var _ TestDeep = &tdHasPrefix{} // summary(HasPrefix): checks the prefix of a string, []byte, error or // fmt.Stringer interfaces // input(HasPrefix): str,slice([]byte),if(✓ + fmt.Stringer/error) // HasPrefix operator allows to compare the prefix of a string (or // convertible), []byte (or convertible), error or [fmt.Stringer] // interface (error interface is tested before [fmt.Stringer]). // // td.Cmp(t, []byte("foobar"), td.HasPrefix("foo")) // succeeds // // type Foobar string // td.Cmp(t, Foobar("foobar"), td.HasPrefix("foo")) // succeeds // // err := errors.New("error!") // td.Cmp(t, err, td.HasPrefix("err")) // succeeds // // bstr := bytes.NewBufferString("fmt.Stringer!") // td.Cmp(t, bstr, td.HasPrefix("fmt")) // succeeds // // See also [Contains], [HasSuffix], [Re], [ReAll] and [String]. func HasPrefix(expected string) TestDeep { return &tdHasPrefix{ tdStringBase: newStringBase(expected), } } func (s *tdHasPrefix) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error { str, err := getString(ctx, got) if err != nil { return err } if strings.HasPrefix(str, s.expected) { return nil } if ctx.BooleanError { return ctxerr.BooleanError } return ctx.CollectError(&ctxerr.Error{ Message: "has not prefix", Got: str, Expected: s, }) } func (s *tdHasPrefix) String() string { return "HasPrefix(" + util.ToString(s.expected) + ")" } type tdHasSuffix struct { tdStringBase } var _ TestDeep = &tdHasSuffix{} // summary(HasSuffix): checks the suffix of a string, []byte, error or // fmt.Stringer interfaces // input(HasSuffix): str,slice([]byte),if(✓ + fmt.Stringer/error) // HasSuffix operator allows to compare the suffix of a string (or // convertible), []byte (or convertible), error or [fmt.Stringer] // interface (error interface is tested before [fmt.Stringer]). // // td.Cmp(t, []byte("foobar"), td.HasSuffix("bar")) // succeeds // // type Foobar string // td.Cmp(t, Foobar("foobar"), td.HasSuffix("bar")) // succeeds // // err := errors.New("error!") // td.Cmp(t, err, td.HasSuffix("!")) // succeeds // // bstr := bytes.NewBufferString("fmt.Stringer!") // td.Cmp(t, bstr, td.HasSuffix("!")) // succeeds // // See also [Contains], [HasPrefix], [Re], [ReAll] and [String]. func HasSuffix(expected string) TestDeep { return &tdHasSuffix{ tdStringBase: newStringBase(expected), } } func (s *tdHasSuffix) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error { str, err := getString(ctx, got) if err != nil { return err } if strings.HasSuffix(str, s.expected) { return nil } if ctx.BooleanError { return ctxerr.BooleanError } return ctx.CollectError(&ctxerr.Error{ Message: "has not suffix", Got: str, Expected: s, }) } func (s *tdHasSuffix) String() string { return "HasSuffix(" + util.ToString(s.expected) + ")" } golang-github-maxatome-go-testdeep-1.14.0/td/td_string_test.go000066400000000000000000000075751454313311600243640ustar00rootroot00000000000000// Copyright (c) 2018, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td_test import ( "errors" "testing" "github.com/maxatome/go-testdeep/td" ) func TestString(t *testing.T) { checkOK(t, "foobar", td.String("foobar")) checkOK(t, []byte("foobar"), td.String("foobar")) type MyBytes []byte checkOK(t, MyBytes("foobar"), td.String("foobar")) type MyString string checkOK(t, MyString("foobar"), td.String("foobar")) // error interface checkOK(t, errors.New("pipo bingo"), td.String("pipo bingo")) // fmt.Stringer interface checkOK(t, MyStringer{}, td.String("pipo bingo")) checkError(t, "foo bar test", td.String("pipo"), expectedError{ Message: mustBe("does not match"), Path: mustBe("DATA"), Got: mustContain(`"foo bar test"`), Expected: mustContain(`"pipo"`), }) checkError(t, []int{1, 2}, td.String("bar"), expectedError{ Message: mustBe("bad type"), Path: mustBe("DATA"), Got: mustBe("[]int"), Expected: mustBe("string (convertible) OR []byte (convertible) OR fmt.Stringer OR error"), }) checkError(t, 12, td.String("bar"), expectedError{ Message: mustBe("bad type"), Path: mustBe("DATA"), Got: mustBe("int"), Expected: mustBe("string (convertible) OR []byte (convertible) OR fmt.Stringer OR error"), }) } func TestHasPrefix(t *testing.T) { checkOK(t, "foobar", td.HasPrefix("foo")) checkOK(t, []byte("foobar"), td.HasPrefix("foo")) type MyBytes []byte checkOK(t, MyBytes("foobar"), td.HasPrefix("foo")) type MyString string checkOK(t, MyString("foobar"), td.HasPrefix("foo")) // error interface checkOK(t, errors.New("pipo bingo"), td.HasPrefix("pipo")) // fmt.Stringer interface checkOK(t, MyStringer{}, td.HasPrefix("pipo")) checkError(t, "foo bar test", td.HasPrefix("pipo"), expectedError{ Message: mustBe("has not prefix"), Path: mustBe("DATA"), Got: mustContain(`"foo bar test"`), Expected: mustMatch(`^HasPrefix\(.*"pipo"`), }) checkError(t, []int{1, 2}, td.HasPrefix("bar"), expectedError{ Message: mustBe("bad type"), Path: mustBe("DATA"), Got: mustBe("[]int"), Expected: mustBe("string (convertible) OR []byte (convertible) OR fmt.Stringer OR error"), }) checkError(t, 12, td.HasPrefix("bar"), expectedError{ Message: mustBe("bad type"), Path: mustBe("DATA"), Got: mustBe("int"), Expected: mustBe("string (convertible) OR []byte (convertible) OR fmt.Stringer OR error"), }) } func TestHasSuffix(t *testing.T) { checkOK(t, "foobar", td.HasSuffix("bar")) checkOK(t, []byte("foobar"), td.HasSuffix("bar")) type MyBytes []byte checkOK(t, MyBytes("foobar"), td.HasSuffix("bar")) type MyString string checkOK(t, MyString("foobar"), td.HasSuffix("bar")) // error interface checkOK(t, errors.New("pipo bingo"), td.HasSuffix("bingo")) // fmt.Stringer interface checkOK(t, MyStringer{}, td.HasSuffix("bingo")) checkError(t, "foo bar test", td.HasSuffix("pipo"), expectedError{ Message: mustBe("has not suffix"), Path: mustBe("DATA"), Got: mustContain(`"foo bar test"`), Expected: mustMatch(`^HasSuffix\(.*"pipo"`), }) checkError(t, []int{1, 2}, td.HasSuffix("bar"), expectedError{ Message: mustBe("bad type"), Path: mustBe("DATA"), Got: mustBe("[]int"), Expected: mustBe("string (convertible) OR []byte (convertible) OR fmt.Stringer OR error"), }) checkError(t, 12, td.HasSuffix("bar"), expectedError{ Message: mustBe("bad type"), Path: mustBe("DATA"), Got: mustBe("int"), Expected: mustBe("string (convertible) OR []byte (convertible) OR fmt.Stringer OR error"), }) } func TestStringTypeBehind(t *testing.T) { equalTypes(t, td.String("x"), nil) equalTypes(t, td.HasPrefix("x"), nil) equalTypes(t, td.HasSuffix("x"), nil) } golang-github-maxatome-go-testdeep-1.14.0/td/td_struct.go000066400000000000000000000546641454313311600233440ustar00rootroot00000000000000// Copyright (c) 2018, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td import ( "bytes" "errors" "fmt" "os" "path" "reflect" "regexp" "sort" "strconv" "strings" "sync" "unicode" "unicode/utf8" "github.com/maxatome/go-testdeep/internal/ctxerr" "github.com/maxatome/go-testdeep/internal/dark" "github.com/maxatome/go-testdeep/internal/util" ) type tdStruct struct { tdExpectedType expectedFields fieldInfoSlice } var _ TestDeep = &tdStruct{} type fieldInfo struct { name string expected reflect.Value index []int unexported bool } type fieldInfoSlice []fieldInfo func (e fieldInfoSlice) Len() int { return len(e) } func (e fieldInfoSlice) Less(i, j int) bool { return e[i].name < e[j].name } func (e fieldInfoSlice) Swap(i, j int) { e[i], e[j] = e[j], e[i] } type fieldMatcher struct { name string match func(string) (bool, error) expected any order int ok bool } var ( reMatcherOnce sync.Once reMatcher *regexp.Regexp errNotAMatcher = errors.New("Not a matcher") ) // parseMatcher parses " [NUM] OP PATTERN " and returns 3 strings // corresponding to each part or nil if "s" is not a matcher. func parseMatcher(s string) []string { reMatcherOnce.Do(func() { reMatcher = regexp.MustCompile(`^(?:(\d+)\s*)?([=!]~?)\s*(.+)`) }) subs := reMatcher.FindStringSubmatch(strings.TrimSpace(s)) if subs != nil { subs = subs[1:] } return subs } // newFieldMatcher checks name matches "[NUM] OP PATTERN" where NUM // is an optional number used to sort patterns, OP is "=~", "!~", "=" // or "!" and PATTERN is a regexp (when OP is either "=~" or "!~") or // a shell pattern (when OP is either "=" or "!"). // // NUM, OP and PATTERN can be separated by spaces (or not). func newFieldMatcher(name string, expected any) (fieldMatcher, error) { subs := parseMatcher(name) if subs == nil { return fieldMatcher{}, errNotAMatcher } fm := fieldMatcher{ name: name, expected: expected, ok: subs[1][0] == '=', } if subs[0] != "" { fm.order, _ = strconv.Atoi(subs[0]) //nolint: errcheck } // Shell pattern if subs[1] == "=" || subs[1] == "!" { pattern := subs[2] fm.match = func(s string) (bool, error) { return path.Match(pattern, s) } return fm, nil } // Regexp r, err := regexp.Compile(subs[2]) if err != nil { return fieldMatcher{}, fmt.Errorf("bad regexp field %#q: %s", name, err) } fm.match = func(s string) (bool, error) { return r.MatchString(s), nil } return fm, nil } type fieldMatcherSlice []fieldMatcher func (m fieldMatcherSlice) Len() int { return len(m) } func (m fieldMatcherSlice) Less(i, j int) bool { if m[i].order != m[j].order { return m[i].order < m[j].order } return m[i].name < m[j].name } func (m fieldMatcherSlice) Swap(i, j int) { m[i], m[j] = m[j], m[i] } // StructFields allows to pass struct fields to check in functions // [Struct] and [SStruct]. It is a map whose each key is the expected // field name (or a regexp or a shell pattern matching a field name, // see [Struct] & [SStruct] docs for details) and the corresponding // value the expected field value (which can be a [TestDeep] operator // as well as a zero value.) type StructFields map[string]any // canonStructField canonicalizes name, a key in a StructFields map, // so it can be compared with other keys during a mergeStructFields(). // - "name" → "name" // - "> name " → ">name" // - " 22 =~ [A-Z].*At$ " → "22=~[A-Z].*At$" func canonStructField(name string) string { r, _ := utf8.DecodeRuneInString(name) if r == utf8.RuneError || unicode.IsLetter(r) { return name // shortcut } // Overwrite a field if strings.HasPrefix(name, ">") { nn := strings.TrimSpace(name[1:]) if 1+len(nn) == len(name) { return name // already canonicalized } return ">" + nn } // Matcher if subs := parseMatcher(name); subs != nil { if len(subs[0])+len(subs[1])+len(subs[2]) == len(name) { return name // already canonicalized } return subs[0] + subs[1] + subs[2] } // Will probably raise an error later as it cannot be a field, not // an overwritter and not a matcher return name } // mergeStructFields merges all sfs items into one StructFields and // returns it. func mergeStructFields(sfs ...StructFields) StructFields { switch len(sfs) { case 0: return nil case 1: return sfs[0] default: // Do a smart merge so "> pipo" replaces ">pipo " for example. canon2field := map[string]string{} ret := make(StructFields, len(sfs[0])) for _, sf := range sfs { for field, value := range sf { canon := canonStructField(field) if prevField, ok := canon2field[canon]; ok { delete(ret, prevField) delete(canon2field, canon) } else { delete(ret, canon) } if canon != field { canon2field[canon] = field } ret[field] = value } } return ret } } func newStruct(base base, vmodel reflect.Value) (*tdStruct, reflect.Value) { st := tdStruct{ tdExpectedType: tdExpectedType{ base: base, }, } switch vmodel.Kind() { case reflect.Ptr: if vmodel.Type().Elem().Kind() != reflect.Struct { break } st.isPtr = true if vmodel.IsNil() { st.expectedType = vmodel.Type().Elem() return &st, reflect.Value{} } vmodel = vmodel.Elem() fallthrough case reflect.Struct: st.expectedType = vmodel.Type() return &st, vmodel } st.err = ctxerr.OpBadUsage(st.location.Func, "(STRUCT|&STRUCT|nil, EXPECTED_FIELDS)", vmodel.Interface(), 1, true) return &st, reflect.Value{} } // structTypeString returns stringified t. It is the caller // responsibility to check t is a struct type. // - struct{} → "struct {}" // - pkg.MyType → "struct pkg.MyType" func structTypeString(t reflect.Type) string { if t.Name() == "" { return t.String() } return "struct " + t.String() } func anyStruct(base base, model reflect.Value, expectedFields StructFields, strict bool) *tdStruct { st, vmodel := newStruct(base, model) if st.err != nil { return st } st.expectedFields = make([]fieldInfo, 0, len(expectedFields)) checkedFields := make(map[string]bool, len(expectedFields)) var matchers fieldMatcherSlice //nolint: prealloc // Check that all given fields are available in model stType := st.expectedType for fieldName, expectedValue := range expectedFields { field, found := stType.FieldByName(fieldName) if found { st.addExpectedValue(field, expectedValue, "") if st.err != nil { return st } checkedFields[fieldName] = false continue } // overwrite model field: ">fieldName", "> fieldName" if strings.HasPrefix(fieldName, ">") { name := strings.TrimSpace(fieldName[1:]) field, found = stType.FieldByName(name) if !found { st.err = ctxerr.OpBad(st.location.Func, "%s has no field %q (from %q)", structTypeString(stType), name, fieldName) return st } st.addExpectedValue( field, expectedValue, fmt.Sprintf(" (from %q)", fieldName), ) if st.err != nil { return st } checkedFields[name] = true continue } // matcher: "=~At$", "!~At$", "=*At", "!*At" matcher, err := newFieldMatcher(fieldName, expectedValue) if err != nil { if err == errNotAMatcher { st.err = ctxerr.OpBad(st.location.Func, "%s has no field %q", structTypeString(stType), fieldName) } else { st.err = ctxerr.OpBad(st.location.Func, err.Error()) } return st } matchers = append(matchers, matcher) } // Get all field names allFields := map[string]struct{}{} stType.FieldByNameFunc(func(fieldName string) bool { allFields[fieldName] = struct{}{} return false }) // Check initialized fields in model if vmodel.IsValid() { for fieldName := range allFields { overwrite, alreadySet := checkedFields[fieldName] if overwrite { continue } field, _ := stType.FieldByName(fieldName) if field.Anonymous { continue } vfield := vmodel.FieldByIndex(field.Index) // Try to force access to unexported fields fieldIf, ok := dark.GetInterface(vfield, true) if !ok { // Probably in an environment where "unsafe" package is forbidden… :( fmt.Fprintf(os.Stderr, //nolint: errcheck "%s(): field %s is unexported and cannot be overridden, skip it from model.\n", st.location.Func, fieldName) continue } // If non-zero field if !reflect.DeepEqual(reflect.Zero(field.Type).Interface(), fieldIf) { if alreadySet { st.err = ctxerr.OpBad(st.location.Func, "non zero field %s in model already exists in expectedFields", fieldName) return st } st.expectedFields = append(st.expectedFields, fieldInfo{ name: fieldName, expected: vfield, index: field.Index, unexported: field.PkgPath != "", }) checkedFields[fieldName] = true } } } // At least one matcher (regexp/shell pattern) if matchers != nil { sort.Sort(matchers) // always process matchers in the same order for _, m := range matchers { for fieldName := range allFields { if _, ok := checkedFields[fieldName]; ok { continue } field, _ := stType.FieldByName(fieldName) if field.Anonymous { continue } ok, err := m.match(fieldName) if err != nil { st.err = ctxerr.OpBad(st.location.Func, "bad shell pattern field %#q: %s", m.name, err) return st } if ok == m.ok { st.addExpectedValue( field, m.expected, fmt.Sprintf(" (from pattern %#q)", m.name), ) if st.err != nil { return st } checkedFields[fieldName] = true } } } } // If strict, fill non explicitly expected fields to zero if strict { for fieldName := range allFields { if _, ok := checkedFields[fieldName]; ok { continue } field, _ := stType.FieldByName(fieldName) if field.Anonymous { continue } st.expectedFields = append(st.expectedFields, fieldInfo{ name: fieldName, expected: reflect.New(field.Type).Elem(), // zero index: field.Index, unexported: field.PkgPath != "", }) } } sort.Sort(st.expectedFields) return st } func (s *tdStruct) addExpectedValue(field reflect.StructField, expectedValue any, ctxInfo string) { var vexpectedValue reflect.Value if expectedValue == nil { switch field.Type.Kind() { case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice: vexpectedValue = reflect.Zero(field.Type) // change to a typed nil default: s.err = ctxerr.OpBad(s.location.Func, "expected value of field %s%s cannot be nil as it is a %s", field.Name, ctxInfo, field.Type) return } } else { vexpectedValue = reflect.ValueOf(expectedValue) if _, ok := expectedValue.(TestDeep); !ok { if !vexpectedValue.Type().AssignableTo(field.Type) { s.err = ctxerr.OpBad(s.location.Func, "type %s of field expected value %s%s differs from struct one (%s)", vexpectedValue.Type(), field.Name, ctxInfo, field.Type) return } } } s.expectedFields = append(s.expectedFields, fieldInfo{ name: field.Name, expected: vexpectedValue, index: field.Index, unexported: field.PkgPath != "", }) } // summary(Struct): compares the contents of a struct or a pointer on // a struct // input(Struct): struct,ptr(ptr on struct) // Struct operator compares the contents of a struct or a pointer on a // struct against the non-zero values of model (if any) and the // values of expectedFields. See [SStruct] to compares against zero // fields without specifying them in expectedFields. // // model must be the same type as compared data. If the expected type // is anonymous or private, model can be nil. In this case it is // considered lazy and determined each time the operator is involved // in a match, see below. // // expectedFields can be omitted, if no zero entries are expected // and no [TestDeep] operators are involved. If expectedFields // contains more than one item, all items are merged before their use, // from left to right. // // td.Cmp(t, got, td.Struct( // Person{ // Name: "John Doe", // }, // td.StructFields{ // "Children": 4, // }, // td.StructFields{ // "Age": td.Between(40, 45), // "Children": 0, // overwrite 4 // }), // ) // // It is an error to set a non-zero field in model AND to set the // same field in expectedFields, as in such cases the Struct // operator does not know if the user wants to override the non-zero // model field value or if it is an error. To explicitly override a // non-zero model in expectedFields, just prefix its name with a // ">" (followed by some optional spaces), as in: // // td.Cmp(t, got, td.Struct( // Person{ // Name: "John Doe", // Age: 23, // Children: 4, // }, // td.StructFields{ // "> Age": td.Between(40, 45), // ">Children": 0, // spaces after ">" are optional // }), // ) // // expectedFields can also contain regexps or shell patterns to // match multiple fields not explicitly listed in model and in // expectedFields. Regexps are prefixed by "=~" or "!~" to // respectively match or don't-match. Shell patterns are prefixed by "=" // or "!" to respectively match or don't-match. // // td.Cmp(t, got, td.Struct( // Person{ // Name: "John Doe", // }, // td.StructFields{ // "=*At": td.Lte(time.Now()), // matches CreatedAt & UpdatedAt fields using shell pattern // "=~^[a-z]": td.Ignore(), // explicitly ignore private fields using a regexp // }), // ) // // When several patterns can match a same field, it is advised to tell // go-testdeep in which order patterns should be tested, as once a // pattern matches a field, the other patterns are ignored for this // field. To do so, each pattern can be prefixed by a number, as in: // // td.Cmp(t, got, td.Struct( // Person{ // Name: "John Doe", // }, // td.StructFields{ // "1=*At": td.Lte(time.Now()), // "2=~^[a-z]": td.NotNil(), // }), // ) // // This way, "*At" shell pattern is always used before "^[a-z]" // regexp, so if a field "createdAt" exists it is tested against // time.Now() and never against [NotNil]. A pattern without a // prefix number is the same as specifying "0" as prefix. // // To make it clearer, some spaces can be added, as well as bigger // numbers used: // // td.Cmp(t, got, td.Struct( // Person{ // Name: "John Doe", // }, // td.StructFields{ // " 900 = *At": td.Lte(time.Now()), // "2000 =~ ^[a-z]": td.NotNil(), // }), // ) // // The following example combines all possibilities: // // td.Cmp(t, got, td.Struct( // Person{ // NickName: "Joe", // }, // td.StructFields{ // "Firstname": td.Any("John", "Johnny"), // "1 = *[nN]ame": td.NotEmpty(), // matches LastName, lastname, … // "2 ! [A-Z]*": td.NotZero(), // matches all private fields // "3 =~ ^(Crea|Upda)tedAt$": td.Gte(time.Now()), // "4 !~ ^(Dogs|Children)$": td.Zero(), // matches all remaining fields except Dogs and Children // "5 =~ .": td.NotNil(), // matches all remaining fields (same as "5 = *") // }), // ) // // If the expected type is private to the current package, it cannot // be passed as model. To overcome this limitation, model can be nil, // it is then considered as lazy. This way, the model is automatically // set during each match to the same type (still requiring struct or // struct pointer) of the compared data. Similarly, testing an // anonymous struct can be boring as all fields have to be re-declared // to define model. A nil model avoids that: // // got := struct { // name string // age int // }{"Bob", 42} // td.Cmp(t, got, td.Struct(nil, td.StructFields{"age": td.Between(40, 42)})) // // During a match, all expected fields must be found to // succeed. Non-expected fields (and so zero model fields) are // ignored. // // TypeBehind method returns the [reflect.Type] of model. // // See also [SStruct]. func Struct(model any, expectedFields ...StructFields) TestDeep { ef := mergeStructFields(expectedFields...) if model == nil { return newStructLazy(ef, false) } return anyStruct(newBase(3), reflect.ValueOf(model), ef, false) } // summary(SStruct): strictly compares the contents of a struct or a // pointer on a struct // input(SStruct): struct,ptr(ptr on struct) // SStruct operator (aka strict-[Struct]) compares the contents of a // struct or a pointer on a struct against values of model (if any) // and the values of expectedFields. The zero values are compared // too even if they are omitted from expectedFields: that is the // difference with [Struct] operator. // // model must be the same type as compared data. If the expected type // is private or anonymous, model can be nil. In this case it is // considered lazy and determined each time the operator is involved // in a match, see below. // // expectedFields can be omitted, if no [TestDeep] operators are // involved. If expectedFields contains more than one item, all // items are merged before their use, from left to right. // // To ignore a field, one has to specify it in expectedFields and // use the [Ignore] operator. // // td.Cmp(t, got, td.SStruct( // Person{ // Name: "John Doe", // }, // td.StructFields{ // "Children": 4, // }, // td.StructFields{ // "Age": td.Between(40, 45), // "Children": td.Ignore(), // overwrite 4 // }), // ) // // It is an error to set a non-zero field in model AND to set the // same field in expectedFields, as in such cases the SStruct // operator does not know if the user wants to override the non-zero // model field value or if it is an error. To explicitly override a // non-zero model in expectedFields, just prefix its name with a // ">" (followed by some optional spaces), as in: // // td.Cmp(t, got, td.SStruct( // Person{ // Name: "John Doe", // Age: 23, // Children: 4, // }, // td.StructFields{ // "> Age": td.Between(40, 45), // ">Children": 0, // spaces after ">" are optional // }), // ) // // expectedFields can also contain regexps or shell patterns to // match multiple fields not explicitly listed in model and in // expectedFields. Regexps are prefixed by "=~" or "!~" to // respectively match or don't-match. Shell patterns are prefixed by "=" // or "!" to respectively match or don't-match. // // td.Cmp(t, got, td.SStruct( // Person{ // Name: "John Doe", // }, // td.StructFields{ // "=*At": td.Lte(time.Now()), // matches CreatedAt & UpdatedAt fields using shell pattern // "=~^[a-z]": td.Ignore(), // explicitly ignore private fields using a regexp // }), // ) // // When several patterns can match a same field, it is advised to tell // go-testdeep in which order patterns should be tested, as once a // pattern matches a field, the other patterns are ignored for this // field. To do so, each pattern can be prefixed by a number, as in: // // td.Cmp(t, got, td.SStruct( // Person{ // Name: "John Doe", // }, // td.StructFields{ // "1=*At": td.Lte(time.Now()), // "2=~^[a-z]": td.NotNil(), // }), // ) // // This way, "*At" shell pattern is always used before "^[a-z]" // regexp, so if a field "createdAt" exists it is tested against // time.Now() and never against [NotNil]. A pattern without a // prefix number is the same as specifying "0" as prefix. // // To make it clearer, some spaces can be added, as well as bigger // numbers used: // // td.Cmp(t, got, td.SStruct( // Person{ // Name: "John Doe", // }, // td.StructFields{ // " 900 = *At": td.Lte(time.Now()), // "2000 =~ ^[a-z]": td.NotNil(), // }), // ) // // The following example combines all possibilities: // // td.Cmp(t, got, td.SStruct( // Person{ // NickName: "Joe", // }, // td.StructFields{ // "Firstname": td.Any("John", "Johnny"), // "1 = *[nN]ame": td.NotEmpty(), // matches LastName, lastname, … // "2 ! [A-Z]*": td.NotZero(), // matches all private fields // "3 =~ ^(Crea|Upda)tedAt$": td.Gte(time.Now()), // "4 !~ ^(Dogs|Children)$": td.Zero(), // matches all remaining fields except Dogs and Children // "5 =~ .": td.NotNil(), // matches all remaining fields (same as "5 = *") // }), // ) // // If the expected type is private to the current package, it cannot // be passed as model. To overcome this limitation, model can be nil, // it is then considered as lazy. This way, the model is automatically // set during each match to the same type (still requiring struct or // struct pointer) of the compared data. Similarly, testing an // anonymous struct can be boring as all fields have to be re-declared // to define model. A nil model avoids that: // // got := struct { // name string // age int // }{"Bob", 42} // td.Cmp(t, got, td.SStruct(nil, td.StructFields{ // "name": "Bob", // "age": td.Between(40, 42), // })) // // During a match, all expected and zero fields must be found to // succeed. // // TypeBehind method returns the [reflect.Type] of model. // // See also [SStruct]. func SStruct(model any, expectedFields ...StructFields) TestDeep { ef := mergeStructFields(expectedFields...) if model == nil { return newStructLazy(ef, false) } return anyStruct(newBase(3), reflect.ValueOf(model), ef, true) } func (s *tdStruct) Match(ctx ctxerr.Context, got reflect.Value) (err *ctxerr.Error) { if s.err != nil { return ctx.CollectError(s.err) } err = s.checkPtr(ctx, &got, false) if err != nil { return ctx.CollectError(err) } err = s.checkType(ctx, got) if err != nil { return ctx.CollectError(err) } ignoreUnexported := ctx.IgnoreUnexported || ctx.Hooks.IgnoreUnexported(got.Type()) for _, fieldInfo := range s.expectedFields { if ignoreUnexported && fieldInfo.unexported { continue } err = deepValueEqual(ctx.AddField(fieldInfo.name), got.FieldByIndex(fieldInfo.index), fieldInfo.expected) if err != nil { return } } return nil } func (s *tdStruct) String() string { if s.err != nil { return s.stringError() } buf := bytes.NewBufferString(s.location.Func) buf.WriteByte('(') if s.isPtr { buf.WriteByte('*') } buf.WriteString(s.expectedType.String()) if len(s.expectedFields) == 0 { buf.WriteString("{})") } else { buf.WriteString("{\n") maxLen := 0 for _, fieldInfo := range s.expectedFields { if len(fieldInfo.name) > maxLen { maxLen = len(fieldInfo.name) } } maxLen++ for _, fieldInfo := range s.expectedFields { fmt.Fprintf(buf, " %-*s %s\n", //nolint: errcheck maxLen, fieldInfo.name+":", util.ToString(fieldInfo.expected)) } buf.WriteString("})") } return buf.String() } golang-github-maxatome-go-testdeep-1.14.0/td/td_struct_lazy.go000066400000000000000000000037111454313311600243660ustar00rootroot00000000000000// Copyright (c) 2023, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td import ( "bytes" "fmt" "reflect" "sort" "github.com/maxatome/go-testdeep/internal/ctxerr" "github.com/maxatome/go-testdeep/internal/util" ) type tdStructLazy struct { base cache map[reflect.Type]*tdStruct expectedFields StructFields strict bool } var _ TestDeep = &tdStructLazy{} func newStructLazy(expectedFields StructFields, strict bool) TestDeep { return &tdStructLazy{ base: newBase(4), cache: map[reflect.Type]*tdStruct{}, expectedFields: expectedFields, strict: strict, } } func (s *tdStructLazy) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error { gotType := got.Type() tds := s.cache[gotType] if tds == nil { switch gotType.Kind() { case reflect.Struct: case reflect.Ptr: if gotType.Elem().Kind() == reflect.Struct { break } fallthrough default: if ctx.BooleanError { return ctxerr.BooleanError } return ctx.CollectError(ctxerr.BadKind(got, "struct OR *struct")) } tds = anyStruct(s.base, reflect.New(got.Type()).Elem(), s.expectedFields, s.strict) tds.location = s.location s.cache[gotType] = tds } return tds.Match(ctx, got) } func (s *tdStructLazy) String() string { buf := bytes.NewBufferString(s.location.Func) buf.WriteString("({") if len(s.expectedFields) > 0 { buf.WriteByte('\n') fields := make([]string, 0, len(s.expectedFields)) maxLen := 0 for name := range s.expectedFields { fields = append(fields, name) if len(name) > maxLen { maxLen = len(name) } } sort.Strings(fields) maxLen++ for _, name := range fields { fmt.Fprintf(buf, " %-*s %s\n", //nolint: errcheck maxLen, name+":", util.ToString(s.expectedFields[name])) } } buf.WriteString("})") return buf.String() } golang-github-maxatome-go-testdeep-1.14.0/td/td_struct_lazy_test.go000066400000000000000000000106121454313311600254230ustar00rootroot00000000000000// Copyright (c) 2023, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td_test import ( "testing" "github.com/maxatome/go-testdeep/internal/test" "github.com/maxatome/go-testdeep/td" ) func TestStructLazy(t *testing.T) { got := struct { ValInt int ValStr string }{0, "foobar"} t.Run("Struct OK", func(t *testing.T) { got.ValInt = 123 checkOK(t, got, td.Struct(nil, td.StructFields{ "ValStr": "foobar", })) checkOK(t, &got, td.Struct(nil, td.StructFields{ "ValInt": 123, "ValStr": "foobar", })) checkOK(t, &got, td.Struct(nil, td.StructFields{"=Val*": td.NotZero()})) }) t.Run("SStruct OK", func(t *testing.T) { got.ValInt = 0 checkOK(t, got, td.SStruct(nil, td.StructFields{ "ValStr": "foobar", })) checkOK(t, &got, td.SStruct(nil, td.StructFields{ "ValInt": 0, "ValStr": "foobar", })) got.ValInt = 123 checkOK(t, &got, td.SStruct(nil, td.StructFields{"=Val*": td.NotZero()})) }) got.ValInt = 666 ops := []struct { name string new func(any, ...td.StructFields) td.TestDeep }{ {"Struct", td.Struct}, {"SStruct", td.SStruct}, } for _, op := range ops { t.Run(op.name+" errors", func(t *testing.T) { under := mustContain("under operator " + op.name + " at td_struct_lazy_test.go:") badUsage := mustBe("bad usage of " + op.name + " operator") checkError(t, got, op.new(nil, td.StructFields{"Zip": 345}), expectedError{ Message: badUsage, Path: mustBe("DATA"), Summary: mustBe(`struct { ValInt int; ValStr string } has no field "Zip"`), Under: under, }) checkError(t, got, op.new(nil, td.StructFields{">\tZip": 345}), expectedError{ Message: badUsage, Path: mustBe("DATA"), Summary: mustBe(`struct { ValInt int; ValStr string } has no field "Zip" (from ">\tZip")`), Under: under, }) checkError(t, got, op.new(nil, td.StructFields{"ValInt": "zip"}), expectedError{ Message: badUsage, Path: mustBe("DATA"), Summary: mustBe("type string of field expected value ValInt differs from struct one (int)"), }) checkError(t, 123, op.new(nil, td.StructFields{}), expectedError{ Message: mustBe("bad kind"), Path: mustBe("DATA"), Got: mustContain("int"), Expected: mustContain("struct OR *struct"), Under: under, }) n := 123 checkError(t, &n, op.new(nil, td.StructFields{}), expectedError{ Message: mustBe("bad kind"), Path: mustBe("DATA"), Got: mustContain("*int"), Expected: mustContain("struct OR *struct"), Under: under, }) type myInt int checkError(t, myInt(123), op.new(nil, td.StructFields{}), expectedError{ Message: mustBe("bad kind"), Path: mustBe("DATA"), Got: mustContain("int (td_test.myInt type)"), Expected: mustContain("struct OR *struct"), Under: under, }) mi := myInt(123) checkError(t, &mi, op.new(nil, td.StructFields{}), expectedError{ Message: mustBe("bad kind"), Path: mustBe("DATA"), Got: mustContain("*int (*td_test.myInt type)"), Expected: mustContain("struct OR *struct"), Under: under, }) checkError(t, nil, op.new(nil, td.StructFields{}), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA"), Got: mustBe("nil"), Expected: mustContain("Struct({})"), Under: under, }) checkError(t, (*struct{ x int })(nil), op.new(nil, td.StructFields{}), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA"), Got: mustBe("(*struct { x int })()"), Expected: mustContain("non-nil"), Under: under, }) }) t.Run(op.name+" String", func(t *testing.T) { test.EqualStr(t, op.new(nil).String(), op.name+`({})`) test.EqualStr(t, op.new(nil, td.StructFields{ "ValBool": false, "= Val*": td.NotZero(), "> Foo": 12, "=~Bar[6-9]$": "zip", }).String(), op.name+`({ = Val*: NotZero() =~Bar[6-9]$: "zip" > Foo: 12 ValBool: false })`) }) } } func TestStructLazyTypeBehind(t *testing.T) { equalTypes(t, td.Struct(nil, nil), nil) equalTypes(t, td.SStruct(nil, nil), nil) } golang-github-maxatome-go-testdeep-1.14.0/td/td_struct_private_test.go000066400000000000000000000062651454313311600261270ustar00rootroot00000000000000// Copyright (c) 2021, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td import ( "reflect" "strings" "testing" "github.com/maxatome/go-testdeep/internal/test" ) func TestCanonStructField(t *testing.T) { for _, tst := range []struct{ got, expected string }{ {"", ""}, {"pipo", "pipo"}, {">pipo", ">pipo"}, {"> pipo ", ">pipo"}, {"123=~.*", "123=~.*"}, {" 123 =~ .* ", "123=~.*"}, {"&badField", "&badField"}, } { test.EqualStr(t, canonStructField(tst.got), tst.expected) } } func TestMergeStructFields(t *testing.T) { sfs := mergeStructFields() if sfs != nil { t.Errorf("not nil") } x := StructFields{} sfs = mergeStructFields(x) if reflect.ValueOf(sfs).Pointer() != reflect.ValueOf(x).Pointer() { t.Errorf("not x") } a := StructFields{"pipo": 1} b := StructFields{"pipo": 2} c := StructFields{"pipo": 3} sfs = mergeStructFields(a, b, c) if reflect.ValueOf(sfs).Pointer() == reflect.ValueOf(c).Pointer() { t.Errorf("is c") } test.EqualInt(t, len(sfs), 1) test.EqualInt(t, sfs["pipo"].(int), 3) a = StructFields{">pipo": 1} b = StructFields{"> pipo": 2} c = StructFields{">pipo ": 3} sfs = mergeStructFields(a, b, c) if reflect.ValueOf(sfs).Pointer() == reflect.ValueOf(c).Pointer() { t.Errorf("is c") } test.EqualInt(t, len(sfs), 1) test.EqualInt(t, sfs[">pipo "].(int), 3) a = StructFields{"1=~pipo": 1} b = StructFields{" 1 =~ pipo ": 2} c = StructFields{"1\t=~\tpipo": 3} sfs = mergeStructFields(a, b, c) if reflect.ValueOf(sfs).Pointer() == reflect.ValueOf(c).Pointer() { t.Errorf("is c") } test.EqualInt(t, len(sfs), 1) test.EqualInt(t, sfs["1\t=~\tpipo"].(int), 3) } func TestFieldMatcher(t *testing.T) { _, err := newFieldMatcher("pipo", 123) if test.Error(t, err) { if err != errNotAMatcher { t.Errorf("got %q, but %q was expected", err, errNotAMatcher) } } for _, tst := range []struct { name string order int match bool }{ // Regexp {name: "=~.*", match: true}, {name: "=~bc", match: true}, {name: "=~3$", match: true}, {name: "!~^b", match: false}, {name: "134=~bc", match: true, order: 134}, {name: "134 =~ bc", match: true, order: 134}, {name: " 134 =~ bc", match: true, order: 134}, // Shell pattern {name: "=*", match: true}, {name: "=*bc*", match: true}, {name: "=*3", match: true}, {name: "!b*", match: false}, {name: "134=*", match: true, order: 134}, {name: "134 = *", match: true, order: 134}, {name: " 134 = *", match: true, order: 134}, } { fm, err := newFieldMatcher(tst.name, 123) test.NoError(t, err, tst.name) test.EqualStr(t, fm.name, tst.name, tst.name) test.EqualInt(t, fm.expected.(int), 123, tst.name) test.EqualInt(t, fm.order, tst.order, tst.name) test.EqualBool(t, fm.ok, strings.ContainsRune(tst.name, '='), tst.name) if test.IsTrue(t, fm.match != nil, tst.name) { ok, err := fm.match("abc123") test.NoError(t, err, tst.name) test.EqualBool(t, ok, tst.match) } } _, err = newFieldMatcher("=~bad(*", 123) if test.Error(t, err) { test.IsTrue(t, strings.HasPrefix(err.Error(), "bad regexp field `=~bad(*`: ")) } } golang-github-maxatome-go-testdeep-1.14.0/td/td_struct_test.go000066400000000000000000000670671454313311600244040ustar00rootroot00000000000000// Copyright (c) 2018, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td_test import ( "bytes" "errors" "testing" "time" "github.com/maxatome/go-testdeep/internal/dark" "github.com/maxatome/go-testdeep/internal/test" "github.com/maxatome/go-testdeep/td" ) func TestStruct(t *testing.T) { gotStruct := MyStruct{ MyStructMid: MyStructMid{ MyStructBase: MyStructBase{ ValBool: true, }, ValStr: "foobar", }, ValInt: 123, } // // Using pointer checkOK(t, &gotStruct, td.Struct(&MyStruct{}, td.StructFields{ "ValBool": true, "ValStr": "foobar", "ValInt": 123, "Ptr": nil, })) checkOK(t, &gotStruct, td.Struct( &MyStruct{ MyStructMid: MyStructMid{ ValStr: "zip", }, ValInt: 666, }, td.StructFields{ "ValBool": true, "> ValStr": "foobar", ">ValInt": 123, })) checkOK(t, &gotStruct, td.Struct((*MyStruct)(nil), td.StructFields{ "ValBool": true, "ValStr": "foobar", "ValInt": 123, "Ptr": nil, })) checkError(t, 123, td.Struct(&MyStruct{}, td.StructFields{}), expectedError{ Message: mustBe("type mismatch"), Path: mustBe("DATA"), Got: mustContain("int"), Expected: mustContain("*td_test.MyStruct"), }) checkError(t, &MyStructBase{}, td.Struct(&MyStruct{}, td.StructFields{}), expectedError{ Message: mustBe("type mismatch"), Path: mustBe("DATA"), Got: mustContain("*td_test.MyStructBase"), Expected: mustContain("*td_test.MyStruct"), }) checkError(t, &gotStruct, td.Struct(&MyStruct{}, td.StructFields{ "ValBool": false, // ← does not match "ValStr": "foobar", "ValInt": 123, }), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA.ValBool"), Got: mustContain("true"), Expected: mustContain("false"), }) checkOK(t, &gotStruct, td.Struct(&MyStruct{ MyStructMid: MyStructMid{ MyStructBase: MyStructBase{ ValBool: true, }, ValStr: "foobar", }, ValInt: 123, }, nil)) checkError(t, &gotStruct, td.Struct(&MyStruct{ MyStructMid: MyStructMid{ MyStructBase: MyStructBase{ ValBool: true, }, ValStr: "foobax", // ← does not match }, ValInt: 123, }, nil), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA.ValStr"), Got: mustContain("foobar"), Expected: mustContain("foobax"), }) // Zero values checkOK(t, &MyStruct{}, td.Struct(&MyStruct{}, td.StructFields{ "ValBool": false, "ValStr": "", "ValInt": 0, })) // nil cases checkError(t, nil, td.Struct(&MyStruct{}, nil), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA"), Got: mustContain("nil"), Expected: mustContain("*td_test.MyStruct"), }) checkError(t, (*MyStruct)(nil), td.Struct(&MyStruct{}, nil), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA"), Got: mustContain("nil"), Expected: mustBe("non-nil"), }) // // Without pointer checkOK(t, gotStruct, td.Struct(MyStruct{}, td.StructFields{ "ValBool": true, "ValStr": "foobar", "ValInt": 123, })) checkOK(t, gotStruct, td.Struct( MyStruct{ MyStructMid: MyStructMid{ ValStr: "zip", }, ValInt: 666, }, td.StructFields{ "ValBool": true, "> ValStr": "foobar", ">ValInt": 123, })) checkError(t, 123, td.Struct(MyStruct{}, td.StructFields{}), expectedError{ Message: mustBe("type mismatch"), Path: mustBe("DATA"), Got: mustContain("int"), Expected: mustContain("td_test.MyStruct"), }) checkError(t, gotStruct, td.Struct(MyStruct{}, td.StructFields{ "ValBool": false, // ← does not match "ValStr": "foobar", "ValInt": 123, }), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA.ValBool"), Got: mustContain("true"), Expected: mustContain("false"), }) checkOK(t, gotStruct, td.Struct(MyStruct{ MyStructMid: MyStructMid{ MyStructBase: MyStructBase{ ValBool: true, }, ValStr: "foobar", }, ValInt: 123, }, nil)) checkError(t, gotStruct, td.Struct(MyStruct{ MyStructMid: MyStructMid{ MyStructBase: MyStructBase{ ValBool: true, }, ValStr: "foobax", // ← does not match }, ValInt: 123, }, nil), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA.ValStr"), Got: mustContain("foobar"), Expected: mustContain("foobax"), }) // Zero values checkOK(t, MyStruct{}, td.Struct(MyStruct{}, td.StructFields{ "ValBool": false, "ValStr": "", "ValInt": 0, })) // nil cases checkError(t, nil, td.Struct(MyStruct{}, nil), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA"), Got: mustContain("nil"), Expected: mustContain("td_test.MyStruct"), }) checkError(t, (*MyStruct)(nil), td.Struct(MyStruct{}, nil), expectedError{ Message: mustBe("type mismatch"), Path: mustBe("DATA"), Got: mustBe("*td_test.MyStruct"), Expected: mustBe("td_test.MyStruct"), }) // // Be lax... type Struct1 struct { name string age int } type Struct2 struct { name string age int } // Without Lax → error checkError(t, Struct1{name: "Bob", age: 42}, td.Struct(Struct2{name: "Bob", age: 42}, nil), expectedError{ Message: mustBe("type mismatch"), }) // With Lax → OK checkOK(t, Struct1{name: "Bob", age: 42}, td.Lax(td.Struct(Struct2{name: "Bob", age: 42}, nil))) // // IgnoreUnexported t.Run("IgnoreUnexported", func(tt *testing.T) { type SType struct { Public int private string } got := SType{Public: 42, private: "test"} expected := td.Struct(SType{Public: 42, private: "zip"}, nil) checkError(tt, got, expected, expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA.private"), Got: mustBe(`"test"`), Expected: mustBe(`"zip"`), }) // Ignore unexported globally defer func() { td.DefaultContextConfig.IgnoreUnexported = false }() td.DefaultContextConfig.IgnoreUnexported = true checkOK(tt, got, expected) td.DefaultContextConfig.IgnoreUnexported = false ttt := test.NewTestingTB(t.Name()) t := td.NewT(ttt).IgnoreUnexported(SType{}) // ignore only for SType test.IsTrue(tt, t.Cmp(got, expected)) }) // // Bad usage checkError(t, "never tested", td.Struct("test", nil), expectedError{ Message: mustBe("bad usage of Struct operator"), Path: mustBe("DATA"), Summary: mustBe("usage: Struct(STRUCT|&STRUCT|nil, EXPECTED_FIELDS), but received string as 1st parameter"), }) i := 12 checkError(t, "never tested", td.Struct(&i, nil), expectedError{ Message: mustBe("bad usage of Struct operator"), Path: mustBe("DATA"), Summary: mustBe("usage: Struct(STRUCT|&STRUCT|nil, EXPECTED_FIELDS), but received *int (ptr) as 1st parameter"), }) checkError(t, "never tested", td.Struct(&MyStruct{}, td.StructFields{"UnknownField": 123}), expectedError{ Message: mustBe("bad usage of Struct operator"), Path: mustBe("DATA"), Summary: mustBe(`struct td_test.MyStruct has no field "UnknownField"`), }) checkError(t, "never tested", td.Struct(&MyStruct{}, td.StructFields{">\tUnknownField": 123}), expectedError{ Message: mustBe("bad usage of Struct operator"), Path: mustBe("DATA"), Summary: mustBe(`struct td_test.MyStruct has no field "UnknownField" (from ">\tUnknownField")`), }) checkError(t, "never tested", td.Struct(&MyStruct{}, td.StructFields{"ValBool": 123}), expectedError{ Message: mustBe("bad usage of Struct operator"), Path: mustBe("DATA"), Summary: mustBe("type int of field expected value ValBool differs from struct one (bool)"), }) checkError(t, "never tested", td.Struct(&MyStruct{}, td.StructFields{">ValBool": 123}), expectedError{ Message: mustBe("bad usage of Struct operator"), Path: mustBe("DATA"), Summary: mustBe(`type int of field expected value ValBool (from ">ValBool") differs from struct one (bool)`), }) checkError(t, "never tested", td.Struct(&MyStruct{}, td.StructFields{"ValBool": nil}), expectedError{ Message: mustBe("bad usage of Struct operator"), Path: mustBe("DATA"), Summary: mustBe("expected value of field ValBool cannot be nil as it is a bool"), }) checkError(t, "never tested", td.Struct(&MyStruct{ MyStructMid: MyStructMid{ MyStructBase: MyStructBase{ ValBool: true, }, }, }, td.StructFields{"ValBool": false}), expectedError{ Message: mustBe("bad usage of Struct operator"), Path: mustBe("DATA"), Summary: mustBe("non zero field ValBool in model already exists in expectedFields"), }) // // String test.EqualStr(t, td.Struct(MyStruct{ MyStructMid: MyStructMid{ ValStr: "foobar", }, ValInt: 123, }, td.StructFields{ "ValBool": false, }).String(), `Struct(td_test.MyStruct{ ValBool: false ValInt: 123 ValStr: "foobar" })`) test.EqualStr(t, td.Struct(&MyStruct{ MyStructMid: MyStructMid{ ValStr: "foobar", }, ValInt: 123, }, td.StructFields{ "ValBool": false, }).String(), `Struct(*td_test.MyStruct{ ValBool: false ValInt: 123 ValStr: "foobar" })`) test.EqualStr(t, td.Struct(&MyStruct{}, td.StructFields{ "ValBool": false, "= Val*": td.NotZero(), }).String(), `Struct(*td_test.MyStruct{ ValBool: false ValInt: NotZero() ValStr: NotZero() })`) test.EqualStr(t, td.Struct(&MyStruct{}, td.StructFields{}).String(), `Struct(*td_test.MyStruct{})`) // Erroneous op test.EqualStr(t, td.Struct("test", nil).String(), "Struct()") } func TestStructPrivateFields(t *testing.T) { type privateKey struct { num int name string } type privateValue struct { value string weight int } type MyTime time.Time type structPrivateFields struct { byKey map[privateKey]*privateValue name string nameb []byte err error iface any properties []int birth time.Time birth2 MyTime next *structPrivateFields } d := func(rfc3339Date string) (ret time.Time) { var err error ret, err = time.Parse(time.RFC3339Nano, rfc3339Date) if err != nil { panic(err) } return } got := structPrivateFields{ byKey: map[privateKey]*privateValue{ {num: 1, name: "foo"}: {value: "test", weight: 12}, {num: 2, name: "bar"}: {value: "tset", weight: 23}, {num: 3, name: "zip"}: {value: "ttse", weight: 34}, }, name: "foobar", nameb: []byte("foobar"), err: errors.New("the error"), iface: 1234, properties: []int{20, 22, 23, 21}, birth: d("2018-04-01T10:11:12.123456789Z"), birth2: MyTime(d("2018-03-01T09:08:07.987654321Z")), next: &structPrivateFields{ byKey: map[privateKey]*privateValue{}, name: "sub", iface: bytes.NewBufferString("buffer!"), birth: d("2018-04-02T10:11:12.123456789Z"), birth2: MyTime(d("2018-03-02T09:08:07.987654321Z")), }, } checkOK(t, got, td.Struct(structPrivateFields{}, td.StructFields{ "name": "foobar", })) checkOK(t, got, td.Struct(structPrivateFields{}, td.StructFields{ "name": td.Re("^foo"), })) checkOK(t, got, td.Struct(structPrivateFields{}, td.StructFields{ "nameb": td.Re("^foo"), })) checkOKOrPanicIfUnsafeDisabled(t, got, td.Struct(structPrivateFields{}, td.StructFields{ "err": td.Re("error"), })) checkError(t, got, td.Struct(structPrivateFields{}, td.StructFields{ "iface": td.Re("buffer"), }), expectedError{ Message: mustBe("bad type"), Path: mustBe("DATA.iface"), Got: mustBe("int"), Expected: mustBe("string (convertible) OR fmt.Stringer OR error OR []uint8"), }) checkOKOrPanicIfUnsafeDisabled(t, got, td.Struct(structPrivateFields{}, td.StructFields{ "next": td.Struct(&structPrivateFields{}, td.StructFields{ "iface": td.Re("buffer"), }), })) checkOK(t, got, td.Struct(structPrivateFields{}, td.StructFields{ "properties": []int{20, 22, 23, 21}, })) checkOK(t, got, td.Struct(structPrivateFields{}, td.StructFields{ "properties": td.ArrayEach(td.Between(20, 23)), })) checkOK(t, got, td.Struct(structPrivateFields{}, td.StructFields{ "byKey": td.MapEach(td.Struct(&privateValue{}, td.StructFields{ "weight": td.Between(12, 34), "value": td.Any(td.HasPrefix("t"), td.HasSuffix("e")), })), })) checkOK(t, got, td.Struct(structPrivateFields{}, td.StructFields{ "byKey": td.SuperMapOf( map[privateKey]*privateValue{ {num: 3, name: "zip"}: {value: "ttse", weight: 34}, }, td.MapEntries{ privateKey{num: 2, name: "bar"}: &privateValue{value: "tset", weight: 23}, }), })) expected := td.Struct(structPrivateFields{}, td.StructFields{ "birth": td.TruncTime(d("2018-04-01T10:11:12Z"), time.Second), "birth2": td.TruncTime(MyTime(d("2018-03-01T09:08:07Z")), time.Second), }) if !dark.UnsafeDisabled { checkOK(t, got, expected) } else { checkError(t, got, expected, expectedError{ Message: mustBe("cannot compare"), Path: mustBe("DATA.birth"), Summary: mustBe("unexported field that cannot be overridden"), }) } checkError(t, got, td.Struct(structPrivateFields{}, td.StructFields{ "next": td.Struct(&structPrivateFields{}, td.StructFields{ "name": "sub", "birth": td.Code(func(t time.Time) bool { return true }), }), }), expectedError{ Message: mustBe("cannot compare unexported field"), Path: mustBe("DATA.next.birth"), Summary: mustBe("use Code() on surrounding struct instead"), }) checkError(t, got, td.Struct(structPrivateFields{}, td.StructFields{ "next": td.Struct(&structPrivateFields{}, td.StructFields{ "name": "sub", "birth": td.Smuggle( func(t time.Time) string { return t.String() }, "2018-04-01T10:11:12.123456789Z"), }), }), expectedError{ Message: mustBe("cannot smuggle unexported field"), Path: mustBe("DATA.next.birth"), Summary: mustBe("work on surrounding struct instead"), }) } func TestStructPatterns(t *testing.T) { type paAnon struct { alphaNum int betaNum int } type paTest struct { paAnon Num int } got := paTest{ paAnon: paAnon{ alphaNum: 1000, betaNum: 2000, }, Num: 666, } t.Run("Shell pattern", func(t *testing.T) { checkOK(t, got, td.Struct(paTest{Num: 666}, td.StructFields{ "=*Num": td.Gte(1000), // matches alphaNum & betaNum })) checkOK(t, got, td.Struct(paTest{Num: 666}, td.StructFields{ "=a*Num": td.Lt(0), // no remaining fields to match "=*": td.Gte(1000), // first, matches alphaNum & betaNum "=b*Num": td.Lt(0), // no remaining fields to match }), "Default sorting uses patterns") checkOK(t, got, td.Struct(paTest{Num: 666}, td.StructFields{ "1 = a*Num": td.Between(999, 1001), // matches alphaNum "2 = *": td.Gte(2000), // matches betaNum "3 = b*Num": td.Gt(3000), // no remaining fields to match }), "Explicitly sorted") checkOK(t, got, td.Struct(paTest{Num: 666}, td.StructFields{ "1 ! beta*": 1000, // matches alphaNum "2 = *": 2000, // matches betaNum }), "negative shell pattern") checkError(t, "never tested", td.Struct(paTest{Num: 666}, td.StructFields{"= al[pha": 123}), expectedError{ Message: mustBe("bad usage of Struct operator"), Path: mustBe("DATA"), Summary: mustContain("bad shell pattern field `= al[pha`: "), }) checkError(t, "never tested", td.Struct(paTest{Num: 666}, td.StructFields{"= alpha*": nil}), expectedError{ Message: mustBe("bad usage of Struct operator"), Path: mustBe("DATA"), Summary: mustBe("expected value of field alphaNum (from pattern `= alpha*`) cannot be nil as it is a int"), }) }) t.Run("Regexp", func(t *testing.T) { checkOK(t, got, td.Struct(paTest{Num: 666}, td.StructFields{ "=~Num$": td.Gte(1000), // matches alphaNum & betaNum })) checkOK(t, got, td.Struct(paTest{Num: 666}, td.StructFields{ "=~^a.*Num$": td.Lt(0), // no remaining fields to match "=~.": td.Gte(1000), // first, matches alphaNum & betaNum "=~^b.*Num$": td.Lt(0), // no remaining fields to match }), "Default sorting uses patterns") checkOK(t, got, td.Struct(paTest{Num: 666}, td.StructFields{ "1 =~ ^a.*Num$": td.Between(999, 1001), // matches alphaNum "2 =~ .": td.Gte(2000), // matches betaNum "3 =~ ^b.*Num$": td.Gt(3000), // no remaining fields to match }), "Explicitly sorted") checkOK(t, got, td.Struct(paTest{Num: 666}, td.StructFields{ "1 !~ ^beta": 1000, // matches alphaNum "2 =~ .": 2000, // matches betaNum }), "negative regexp") checkError(t, "never tested", td.Struct(paTest{Num: 666}, td.StructFields{"=~ al(*": 123}), expectedError{ Message: mustBe("bad usage of Struct operator"), Path: mustBe("DATA"), Summary: mustContain("bad regexp field `=~ al(*`: "), }) checkError(t, "never tested", td.Struct(paTest{Num: 666}, td.StructFields{"=~ alpha": nil}), expectedError{ Message: mustBe("bad usage of Struct operator"), Path: mustBe("DATA"), Summary: mustBe("expected value of field alphaNum (from pattern `=~ alpha`) cannot be nil as it is a int"), }) }) } func TestStructTypeBehind(t *testing.T) { equalTypes(t, td.Struct(MyStruct{}, nil), MyStruct{}) equalTypes(t, td.Struct(&MyStruct{}, nil), &MyStruct{}) // Erroneous op equalTypes(t, td.Struct("test", nil), nil) } func TestSStruct(t *testing.T) { gotStruct := MyStruct{ MyStructMid: MyStructMid{ MyStructBase: MyStructBase{ ValBool: true, }, ValStr: "foobar", }, ValInt: 123, } // // Using pointer checkOK(t, &gotStruct, td.SStruct(&MyStruct{}, td.StructFields{ "ValBool": true, "ValStr": "foobar", "ValInt": 123, // nil Ptr })) checkOK(t, &gotStruct, td.SStruct( &MyStruct{ MyStructMid: MyStructMid{ ValStr: "zip", }, ValInt: 666, }, td.StructFields{ "ValBool": true, "> ValStr": "foobar", ">ValInt": 123, })) checkOK(t, &gotStruct, td.SStruct((*MyStruct)(nil), td.StructFields{ "ValBool": true, "ValStr": "foobar", "ValInt": 123, // nil Ptr })) checkError(t, 123, td.SStruct(&MyStruct{}, td.StructFields{}), expectedError{ Message: mustBe("type mismatch"), Path: mustBe("DATA"), Got: mustContain("int"), Expected: mustContain("*td_test.MyStruct"), }) checkError(t, &MyStructBase{}, td.SStruct(&MyStruct{}, td.StructFields{}), expectedError{ Message: mustBe("type mismatch"), Path: mustBe("DATA"), Got: mustContain("*td_test.MyStructBase"), Expected: mustContain("*td_test.MyStruct"), }) checkError(t, &gotStruct, td.SStruct(&MyStruct{}, td.StructFields{ // ValBool false ← does not match "ValStr": "foobar", "ValInt": 123, }), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA.ValBool"), Got: mustContain("true"), Expected: mustContain("false"), }) checkOK(t, &gotStruct, td.SStruct(&MyStruct{ MyStructMid: MyStructMid{ MyStructBase: MyStructBase{ ValBool: true, }, ValStr: "foobar", }, ValInt: 123, }, nil)) checkError(t, &gotStruct, td.SStruct(&MyStruct{ MyStructMid: MyStructMid{ MyStructBase: MyStructBase{ ValBool: true, }, ValStr: "foobax", // ← does not match }, ValInt: 123, }, nil), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA.ValStr"), Got: mustContain("foobar"), Expected: mustContain("foobax"), }) // Zero values checkOK(t, &MyStruct{}, td.SStruct(&MyStruct{}, nil)) checkOK(t, &MyStruct{}, td.SStruct(&MyStruct{}, td.StructFields{})) // nil cases checkError(t, nil, td.SStruct(&MyStruct{}, nil), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA"), Got: mustContain("nil"), Expected: mustContain("*td_test.MyStruct"), }) checkError(t, (*MyStruct)(nil), td.SStruct(&MyStruct{}, nil), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA"), Got: mustContain("nil"), Expected: mustBe("non-nil"), }) // // Without pointer checkOK(t, gotStruct, td.SStruct(MyStruct{}, td.StructFields{ "ValBool": true, "ValStr": "foobar", "ValInt": 123, })) checkOK(t, gotStruct, td.SStruct( MyStruct{ MyStructMid: MyStructMid{ ValStr: "zip", }, ValInt: 666, }, td.StructFields{ "ValBool": true, "> ValStr": "foobar", ">ValInt": 123, })) checkError(t, 123, td.SStruct(MyStruct{}, td.StructFields{}), expectedError{ Message: mustBe("type mismatch"), Path: mustBe("DATA"), Got: mustContain("int"), Expected: mustContain("td_test.MyStruct"), }) checkError(t, gotStruct, td.SStruct(MyStruct{}, td.StructFields{ // "ValBool" false ← does not match "ValStr": "foobar", "ValInt": 123, }), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA.ValBool"), Got: mustContain("true"), Expected: mustContain("false"), }) checkOK(t, gotStruct, td.SStruct(MyStruct{ MyStructMid: MyStructMid{ MyStructBase: MyStructBase{ ValBool: true, }, ValStr: "foobar", }, ValInt: 123, }, nil)) checkError(t, gotStruct, td.SStruct(MyStruct{ MyStructMid: MyStructMid{ MyStructBase: MyStructBase{ ValBool: true, }, ValStr: "foobax", // ← does not match }, ValInt: 123, }, nil), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA.ValStr"), Got: mustContain("foobar"), Expected: mustContain("foobax"), }) // Zero values checkOK(t, MyStruct{}, td.Struct(MyStruct{}, td.StructFields{})) checkOK(t, MyStruct{}, td.Struct(MyStruct{}, nil)) // nil cases checkError(t, nil, td.SStruct(MyStruct{}, nil), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA"), Got: mustContain("nil"), Expected: mustContain("td_test.MyStruct"), }) checkError(t, (*MyStruct)(nil), td.SStruct(MyStruct{}, nil), expectedError{ Message: mustBe("type mismatch"), Path: mustBe("DATA"), Got: mustBe("*td_test.MyStruct"), Expected: mustBe("td_test.MyStruct"), }) // // Be lax... type Struct1 struct { name string age int } type Struct2 struct { name string age int } // Without Lax → error checkError(t, Struct1{name: "Bob", age: 42}, td.SStruct(Struct2{name: "Bob", age: 42}, nil), expectedError{ Message: mustBe("type mismatch"), }) // With Lax → OK checkOK(t, Struct1{name: "Bob", age: 42}, td.Lax(td.SStruct(Struct2{name: "Bob", age: 42}, nil))) // // IgnoreUnexported t.Run("IgnoreUnexported", func(tt *testing.T) { type SType struct { Public int private string } got := SType{Public: 42, private: "test"} expected := td.SStruct(SType{Public: 42}, nil) checkError(tt, got, expected, expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA.private"), Got: mustBe(`"test"`), Expected: mustBe(`""`), }) // Ignore unexported globally defer func() { td.DefaultContextConfig.IgnoreUnexported = false }() td.DefaultContextConfig.IgnoreUnexported = true checkOK(tt, got, expected) td.DefaultContextConfig.IgnoreUnexported = false ttt := test.NewTestingTB(t.Name()) t := td.NewT(ttt).IgnoreUnexported(SType{}) // ignore only for SType test.IsTrue(tt, t.Cmp(got, expected)) }) // // Bad usage checkError(t, "never tested", td.SStruct("test", nil), expectedError{ Message: mustBe("bad usage of SStruct operator"), Path: mustBe("DATA"), Summary: mustBe("usage: SStruct(STRUCT|&STRUCT|nil, EXPECTED_FIELDS), but received string as 1st parameter"), }) i := 12 checkError(t, "never tested", td.SStruct(&i, nil), expectedError{ Message: mustBe("bad usage of SStruct operator"), Path: mustBe("DATA"), Summary: mustBe("usage: SStruct(STRUCT|&STRUCT|nil, EXPECTED_FIELDS), but received *int (ptr) as 1st parameter"), }) checkError(t, "never tested", td.SStruct(&MyStruct{}, td.StructFields{"UnknownField": 123}), expectedError{ Message: mustBe("bad usage of SStruct operator"), Path: mustBe("DATA"), Summary: mustBe(`struct td_test.MyStruct has no field "UnknownField"`), }) checkError(t, "never tested", td.SStruct(&MyStruct{}, td.StructFields{">\tUnknownField": 123}), expectedError{ Message: mustBe("bad usage of SStruct operator"), Path: mustBe("DATA"), Summary: mustBe(`struct td_test.MyStruct has no field "UnknownField" (from ">\tUnknownField")`), }) checkError(t, "never tested", td.SStruct(&MyStruct{}, td.StructFields{"ValBool": 123}), expectedError{ Message: mustBe("bad usage of SStruct operator"), Path: mustBe("DATA"), Summary: mustBe("type int of field expected value ValBool differs from struct one (bool)"), }) checkError(t, "never tested", td.SStruct(&MyStruct{}, td.StructFields{">ValBool": 123}), expectedError{ Message: mustBe("bad usage of SStruct operator"), Path: mustBe("DATA"), Summary: mustBe(`type int of field expected value ValBool (from ">ValBool") differs from struct one (bool)`), }) checkError(t, "never tested", td.SStruct(&MyStruct{}, td.StructFields{"ValBool": nil}), expectedError{ Message: mustBe("bad usage of SStruct operator"), Path: mustBe("DATA"), Summary: mustBe("expected value of field ValBool cannot be nil as it is a bool"), }) checkError(t, "never tested", td.SStruct(&MyStruct{ MyStructMid: MyStructMid{ MyStructBase: MyStructBase{ ValBool: true, }, }, }, td.StructFields{"ValBool": false}), expectedError{ Message: mustBe("bad usage of SStruct operator"), Path: mustBe("DATA"), Summary: mustBe("non zero field ValBool in model already exists in expectedFields"), }) // // String test.EqualStr(t, td.SStruct(MyStruct{ MyStructMid: MyStructMid{ ValStr: "foobar", }, ValInt: 123, }, td.StructFields{ "ValBool": false, }).String(), `SStruct(td_test.MyStruct{ Ptr: (*int)() ValBool: false ValInt: 123 ValStr: "foobar" })`) test.EqualStr(t, td.SStruct(&MyStruct{ MyStructMid: MyStructMid{ ValStr: "foobar", }, ValInt: 123, }, td.StructFields{ "ValBool": false, }).String(), `SStruct(*td_test.MyStruct{ Ptr: (*int)() ValBool: false ValInt: 123 ValStr: "foobar" })`) test.EqualStr(t, td.SStruct(&MyStruct{}, td.StructFields{}).String(), `SStruct(*td_test.MyStruct{ Ptr: (*int)() ValBool: false ValInt: 0 ValStr: "" })`) // Erroneous op test.EqualStr(t, td.SStruct("test", nil).String(), "SStruct()") } func TestSStructPattern(t *testing.T) { // Patterns are already fully tested in TestStructPatterns type paAnon struct { alphaNum int betaNum int } type paTest struct { paAnon Num int } got := paTest{ paAnon: paAnon{ alphaNum: 1000, betaNum: 2000, }, Num: 666, } checkOK(t, got, td.SStruct(paTest{}, td.StructFields{ "=*Num": td.Gte(666), // matches Num, alphaNum & betaNum })) checkOK(t, got, td.SStruct(paTest{}, td.StructFields{ "=~Num$": td.Gte(666), // matches Num, alphaNum & betaNum })) checkOK(t, paTest{Num: 666}, td.SStruct(paTest{}, td.StructFields{ "=~^Num": 666, // only matches Num // remaining fields are tested as 0 })) } func TestSStructTypeBehind(t *testing.T) { equalTypes(t, td.SStruct(MyStruct{}, nil), MyStruct{}) equalTypes(t, td.SStruct(&MyStruct{}, nil), &MyStruct{}) // Erroneous op equalTypes(t, td.SStruct("test", nil), nil) } golang-github-maxatome-go-testdeep-1.14.0/td/td_tag.go000066400000000000000000000045511454313311600225610ustar00rootroot00000000000000// Copyright (c) 2019, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td import ( "reflect" "github.com/maxatome/go-testdeep/internal/ctxerr" "github.com/maxatome/go-testdeep/internal/util" ) type tdTag struct { tdSmugglerBase tag string } var _ TestDeep = &tdTag{} // summary(Tag): names an operator or a value. Only useful as a // parameter of JSON operator, to name placeholders // input(Tag): all // Tag is a smuggler operator. It only allows to name expectedValue, // which can be an operator or a value. The data is then compared // against expectedValue as if Tag was never called. It is only useful // as [JSON] operator parameter, to name placeholders. See [JSON] // operator for more details. // // td.Cmp(t, gotValue, // td.JSON(`{"fullname": $name, "age": $age, "gender": $gender}`, // td.Tag("name", td.HasPrefix("Foo")), // matches $name // td.Tag("age", td.Between(41, 43)), // matches $age // td.Tag("gender", "male"))) // matches $gender // // TypeBehind method is delegated to expectedValue one if // expectedValue is a [TestDeep] operator, otherwise it returns the // type of expectedValue (or nil if it is originally untyped nil). func Tag(tag string, expectedValue any) TestDeep { t := tdTag{ tdSmugglerBase: newSmugglerBase(expectedValue), tag: tag, } if err := util.CheckTag(tag); err != nil { t.err = ctxerr.OpBad("Tag", err.Error()) return &t } if !t.isTestDeeper { t.expectedValue = reflect.ValueOf(expectedValue) } return &t } func (t *tdTag) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error { if t.err != nil { return ctx.CollectError(t.err) } return deepValueEqual(ctx, got, t.expectedValue) } func (t *tdTag) HandleInvalid() bool { return true // Knows how to handle untyped nil values (aka invalid values) } func (t *tdTag) String() string { if t.err != nil { return t.stringError() } if t.isTestDeeper { return t.expectedValue.Interface().(TestDeep).String() } return util.ToString(t.expectedValue) } func (t *tdTag) TypeBehind() reflect.Type { if t.err != nil { return nil } if t.isTestDeeper { return t.expectedValue.Interface().(TestDeep).TypeBehind() } if t.expectedValue.IsValid() { return t.expectedValue.Type() } return nil } golang-github-maxatome-go-testdeep-1.14.0/td/td_tag_test.go000066400000000000000000000032551454313311600236200ustar00rootroot00000000000000// Copyright (c) 2019, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td_test import ( "testing" "github.com/maxatome/go-testdeep/internal/test" "github.com/maxatome/go-testdeep/internal/util" "github.com/maxatome/go-testdeep/td" ) func TestTag(t *testing.T) { // expected value checkOK(t, 12, td.Tag("number", 12)) checkOK(t, nil, td.Tag("number", nil)) checkError(t, 8, td.Tag("number", 9), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA"), Got: mustBe("8"), Expected: mustBe("9"), }) // expected operator checkOK(t, 12, td.Tag("number", td.Between(9, 13))) checkError(t, 8, td.Tag("number", td.Between(9, 13)), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA"), Got: mustBe("8"), Expected: mustBe("9 ≤ got ≤ 13"), }) // // Bad usage checkError(t, "never tested", td.Tag("1badTag", td.Between(9, 13)), expectedError{ Message: mustBe("bad usage of Tag operator"), Path: mustBe("DATA"), Summary: mustBe(util.ErrTagInvalid.Error()), }) // // String test.EqualStr(t, td.Tag("foo", td.Gt(4)).String(), td.Gt(4).String()) test.EqualStr(t, td.Tag("foo", 8).String(), "8") test.EqualStr(t, td.Tag("foo", nil).String(), "nil") // Erroneous op test.EqualStr(t, td.Tag("1badTag", 12).String(), "Tag()") } func TestTagTypeBehind(t *testing.T) { equalTypes(t, td.Tag("foo", 8), 0) equalTypes(t, td.Tag("foo", td.Gt(4)), 0) equalTypes(t, td.Tag("foo", nil), nil) // Erroneous op equalTypes(t, td.Tag("1badTag", 12), nil) } golang-github-maxatome-go-testdeep-1.14.0/td/td_trunc_time.go000066400000000000000000000072321454313311600241560ustar00rootroot00000000000000// Copyright (c) 2018, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td import ( "fmt" "reflect" "time" "github.com/maxatome/go-testdeep/internal/ctxerr" "github.com/maxatome/go-testdeep/internal/types" ) type tdTruncTime struct { tdExpectedType expectedTime time.Time trunc time.Duration } var _ TestDeep = &tdTruncTime{} // summary(TruncTime): compares time.Time (or assignable) values after // truncating them // input(TruncTime): struct(time.Time),ptr(todo) // TruncTime operator compares [time.Time] (or assignable) values // after truncating them to the optional trunc duration. See // [time.Time.Truncate] for details about the truncation. // // If trunc is missing, it defaults to 0. // // During comparison, location does not matter as [time.Time.Equal] // method is used behind the scenes: a time instant in two different // locations is the same time instant. // // Whatever the trunc value is, the monotonic clock is stripped // before the comparison against expectedTime. // // gotDate := time.Date(2018, time.March, 9, 1, 2, 3, 999999999, time.UTC). // In(time.FixedZone("UTC+2", 2)) // // expected := time.Date(2018, time.March, 9, 1, 2, 3, 0, time.UTC) // // td.Cmp(t, gotDate, td.TruncTime(expected)) // fails, ns differ // td.Cmp(t, gotDate, td.TruncTime(expected, time.Second)) // succeeds // // TypeBehind method returns the [reflect.Type] of expectedTime. func TruncTime(expectedTime any, trunc ...time.Duration) TestDeep { const usage = "(time.Time[, time.Duration])" t := tdTruncTime{ tdExpectedType: tdExpectedType{ base: newBase(3), }, } if len(trunc) > 1 { t.err = ctxerr.OpTooManyParams("TruncTime", usage) return &t } if len(trunc) == 1 { t.trunc = trunc[0] } vval := reflect.ValueOf(expectedTime) t.expectedType = vval.Type() if t.expectedType == types.Time { t.expectedTime = expectedTime.(time.Time).Truncate(t.trunc) return &t } if !t.expectedType.ConvertibleTo(types.Time) { // 1.17 ok as time.Time is a struct t.err = ctxerr.OpBad("TruncTime", "usage: TruncTime%s, 1st parameter must be time.Time or convertible to time.Time, but not %T", usage, expectedTime) return &t } t.expectedTime = vval.Convert(types.Time). Interface().(time.Time).Truncate(t.trunc) return &t } func (t *tdTruncTime) Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error { if t.err != nil { return ctx.CollectError(t.err) } err := t.checkType(ctx, got) if err != nil { return ctx.CollectError(err) } gotTime, err := getTime(ctx, got, got.Type() != types.Time) if err != nil { return ctx.CollectError(err) } gotTimeTrunc := gotTime.Truncate(t.trunc) if gotTimeTrunc.Equal(t.expectedTime) { return nil } // Fail if ctx.BooleanError { return ctxerr.BooleanError } var gotRawStr, gotTruncStr string if t.expectedType != types.Time && t.expectedType.Implements(types.FmtStringer) { gotRawStr = got.Interface().(fmt.Stringer).String() gotTruncStr = reflect.ValueOf(gotTimeTrunc).Convert(t.expectedType). Interface().(fmt.Stringer).String() } else { gotRawStr = gotTime.String() gotTruncStr = gotTimeTrunc.String() } return ctx.CollectError(&ctxerr.Error{ Message: "values differ", Got: types.RawString(gotRawStr + "\ntruncated to:\n" + gotTruncStr), Expected: t, }) } func (t *tdTruncTime) String() string { if t.err != nil { return t.stringError() } if t.expectedType.Implements(types.FmtStringer) { return reflect.ValueOf(t.expectedTime).Convert(t.expectedType). Interface().(fmt.Stringer).String() } return t.expectedTime.String() } golang-github-maxatome-go-testdeep-1.14.0/td/td_trunc_time_test.go000066400000000000000000000116531454313311600252170ustar00rootroot00000000000000// Copyright (c) 2018, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td_test import ( "testing" "time" "github.com/maxatome/go-testdeep/internal/test" "github.com/maxatome/go-testdeep/td" ) type ( MyTime time.Time MyTimeStr time.Time ) func (t MyTimeStr) String() string { return "<<" + time.Time(t).Format(time.RFC3339Nano) + ">>" } func TestTruncTime(t *testing.T) { // // Monotonic now := time.Now() nowWithoutMono := now.Truncate(0) // If monotonic clock available, check without TruncTime() if now != nowWithoutMono { // OK now contains a monotonic part != 0, so fail coz "==" used inside checkError(t, now, nowWithoutMono, expectedError{ Message: mustBe("values differ"), Path: mustContain("DATA"), }) } checkOK(t, now, td.TruncTime(nowWithoutMono)) // // time.Time gotDate := time.Date(2018, time.March, 9, 1, 2, 3, 4, time.UTC) // Time zone / location does not matter UTCp2 := time.FixedZone("UTC+2", 2) UTCm2 := time.FixedZone("UTC-2", 2) checkOK(t, gotDate, td.TruncTime(gotDate.In(UTCp2))) checkOK(t, gotDate, td.TruncTime(gotDate.In(UTCm2))) checkOK(t, gotDate.In(UTCm2), td.TruncTime(gotDate.In(UTCp2))) checkOK(t, gotDate.In(UTCp2), td.TruncTime(gotDate.In(UTCm2))) expDate := gotDate checkOK(t, gotDate, td.TruncTime(expDate)) checkOK(t, gotDate, td.TruncTime(expDate, time.Second)) checkOK(t, gotDate, td.TruncTime(expDate, time.Minute)) expDate = expDate.Add(time.Second) checkError(t, gotDate, td.TruncTime(expDate, time.Second), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA"), Got: mustBe("2018-03-09 01:02:03.000000004 +0000 UTC\n" + "truncated to:\n" + "2018-03-09 01:02:03 +0000 UTC"), Expected: mustBe("2018-03-09 01:02:04 +0000 UTC"), }) checkOK(t, gotDate, td.TruncTime(expDate, time.Minute)) checkError(t, gotDate, td.TruncTime(MyTime(gotDate)), expectedError{ Message: mustBe("type mismatch"), Path: mustBe("DATA"), Got: mustBe("time.Time"), Expected: mustBe("td_test.MyTime"), }) // // Type convertible to time.Time NOT implementing fmt.Stringer gotMyDate := MyTime(gotDate) expMyDate := MyTime(gotDate) checkOK(t, gotMyDate, td.TruncTime(expMyDate)) checkOK(t, gotMyDate, td.TruncTime(expMyDate, time.Second)) checkOK(t, gotMyDate, td.TruncTime(expMyDate, time.Minute)) expMyDate = MyTime(gotDate.Add(time.Second)) checkError(t, gotMyDate, td.TruncTime(expMyDate, time.Second), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA"), Got: mustBe("2018-03-09 01:02:03.000000004 +0000 UTC\n" + "truncated to:\n" + "2018-03-09 01:02:03 +0000 UTC"), Expected: mustBe("2018-03-09 01:02:04 +0000 UTC"), }) checkOK(t, gotMyDate, td.TruncTime(expMyDate, time.Minute)) checkError(t, MyTime(gotDate), td.TruncTime(gotDate), expectedError{ Message: mustBe("type mismatch"), Path: mustBe("DATA"), Got: mustBe("td_test.MyTime"), Expected: mustBe("time.Time"), }) // // Type convertible to time.Time implementing fmt.Stringer gotMyStrDate := MyTimeStr(gotDate) expMyStrDate := MyTimeStr(gotDate) checkOK(t, gotMyStrDate, td.TruncTime(expMyStrDate)) checkOK(t, gotMyStrDate, td.TruncTime(expMyStrDate, time.Second)) checkOK(t, gotMyStrDate, td.TruncTime(expMyStrDate, time.Minute)) expMyStrDate = MyTimeStr(gotDate.Add(time.Second)) checkError(t, gotMyStrDate, td.TruncTime(expMyStrDate, time.Second), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA"), Got: mustBe("<<2018-03-09T01:02:03.000000004Z>>\n" + "truncated to:\n" + "<<2018-03-09T01:02:03Z>>"), Expected: mustBe("<<2018-03-09T01:02:04Z>>"), }) checkOK(t, gotMyStrDate, td.TruncTime(expMyStrDate, time.Minute)) checkError(t, MyTimeStr(gotDate), td.TruncTime(gotDate), expectedError{ Message: mustBe("type mismatch"), Path: mustBe("DATA"), Got: mustBe("td_test.MyTimeStr"), Expected: mustBe("time.Time"), }) // // Bad usage checkError(t, "never tested", td.TruncTime("test"), expectedError{ Message: mustBe("bad usage of TruncTime operator"), Path: mustBe("DATA"), Summary: mustBe("usage: TruncTime(time.Time[, time.Duration]), 1st parameter must be time.Time or convertible to time.Time, but not string"), }) checkError(t, "never tested", td.TruncTime(1, 2, 3), expectedError{ Message: mustBe("bad usage of TruncTime operator"), Path: mustBe("DATA"), Summary: mustBe("usage: TruncTime(time.Time[, time.Duration]), too many parameters"), }) // Erroneous op test.EqualStr(t, td.TruncTime("test").String(), "TruncTime()") } func TestTruncTimeTypeBehind(t *testing.T) { type MyTime time.Time equalTypes(t, td.TruncTime(time.Time{}), time.Time{}) equalTypes(t, td.TruncTime(MyTime{}), MyTime{}) // Erroneous op equalTypes(t, td.TruncTime("test"), nil) } golang-github-maxatome-go-testdeep-1.14.0/td/td_zero.go000066400000000000000000000050731454313311600227650ustar00rootroot00000000000000// Copyright (c) 2018, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td import ( "reflect" "github.com/maxatome/go-testdeep/internal/ctxerr" ) type tdZero struct { baseOKNil } var _ TestDeep = &tdZero{} // summary(Zero): checks data against its zero'ed conterpart // input(Zero): all // Zero operator checks that data is zero regarding its type. // // - nil is the zero value of pointers, maps, slices, channels and functions; // - 0 is the zero value of numbers; // - "" is the 0 value of strings; // - false is the zero value of booleans; // - zero value of structs is the struct with no fields initialized. // // Beware that: // // td.Cmp(t, AnyStruct{}, td.Zero()) // is true // td.Cmp(t, &AnyStruct{}, td.Zero()) // is false, coz pointer ≠ nil // td.Cmp(t, &AnyStruct{}, td.Ptr(td.Zero())) // is true // // See also [Empty], [Nil] and [NotZero]. func Zero() TestDeep { return &tdZero{ baseOKNil: newBaseOKNil(3), } } func (z *tdZero) Match(ctx ctxerr.Context, got reflect.Value) (err *ctxerr.Error) { // nil case if !got.IsValid() { return nil } return deepValueEqual(ctx, got, reflect.New(got.Type()).Elem()) } func (z *tdZero) String() string { return "Zero()" } type tdNotZero struct { baseOKNil } var _ TestDeep = &tdNotZero{} // summary(NotZero): checks that data is not zero regarding its type // input(NotZero): all // NotZero operator checks that data is not zero regarding its type. // // - nil is the zero value of pointers, maps, slices, channels and functions; // - 0 is the zero value of numbers; // - "" is the 0 value of strings; // - false is the zero value of booleans; // - zero value of structs is the struct with no fields initialized. // // Beware that: // // td.Cmp(t, AnyStruct{}, td.NotZero()) // is false // td.Cmp(t, &AnyStruct{}, td.NotZero()) // is true, coz pointer ≠ nil // td.Cmp(t, &AnyStruct{}, td.Ptr(td.NotZero())) // is false // // See also [NotEmpty], [NotNil] and [Zero]. func NotZero() TestDeep { return &tdNotZero{ baseOKNil: newBaseOKNil(3), } } func (z *tdNotZero) Match(ctx ctxerr.Context, got reflect.Value) (err *ctxerr.Error) { if got.IsValid() && !deepValueEqualOK(got, reflect.New(got.Type()).Elem()) { return nil } if ctx.BooleanError { return ctxerr.BooleanError } return ctx.CollectError(&ctxerr.Error{ Message: "zero value", Got: got, Expected: z, }) } func (z *tdNotZero) String() string { return "NotZero()" } golang-github-maxatome-go-testdeep-1.14.0/td/td_zero_test.go000066400000000000000000000122151454313311600240200ustar00rootroot00000000000000// Copyright (c) 2018-2022, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td_test import ( "testing" "github.com/maxatome/go-testdeep/internal/test" "github.com/maxatome/go-testdeep/td" ) func TestZero(t *testing.T) { checkOK(t, 0, td.Zero()) checkOK(t, int64(0), td.Zero()) checkOK(t, float64(0), td.Zero()) checkOK(t, nil, td.Zero()) checkOK(t, (map[string]int)(nil), td.Zero()) checkOK(t, ([]int)(nil), td.Zero()) checkOK(t, [3]int{}, td.Zero()) checkOK(t, MyStruct{}, td.Zero()) checkOK(t, (*MyStruct)(nil), td.Zero()) checkOK(t, &MyStruct{}, td.Ptr(td.Zero())) checkOK(t, (chan int)(nil), td.Zero()) checkOK(t, (func())(nil), td.Zero()) checkOK(t, false, td.Zero()) checkError(t, 12, td.Zero(), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA"), Got: mustBe("12"), Expected: mustBe("0"), }) checkError(t, int64(12), td.Zero(), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA"), Got: mustBe("(int64) 12"), Expected: mustBe("(int64) 0"), }) checkError(t, float64(12), td.Zero(), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA"), Got: mustBe("12.0"), Expected: mustBe("0.0"), }) checkError(t, map[string]int{}, td.Zero(), expectedError{ Message: mustBe("nil map"), Path: mustBe("DATA"), Got: mustBe("not nil"), Expected: mustBe("nil"), }) checkError(t, []int{}, td.Zero(), expectedError{ Message: mustBe("nil slice"), Path: mustBe("DATA"), Got: mustBe("not nil"), Expected: mustBe("nil"), }) checkError(t, [3]int{0, 12}, td.Zero(), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA[1]"), Got: mustBe("12"), Expected: mustBe("0"), }) checkError(t, MyStruct{ValInt: 12}, td.Zero(), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA.ValInt"), Got: mustBe("12"), Expected: mustBe("0"), }) checkError(t, &MyStruct{}, td.Zero(), expectedError{ Message: mustBe("values differ"), Path: mustBe("*DATA"), // in fact, pointer on 0'ed struct contents Got: mustContain(`ValInt: (int) 0`), Expected: mustBe("nil"), }) checkError(t, true, td.Zero(), expectedError{ Message: mustBe("values differ"), Path: mustBe("DATA"), Got: mustBe("true"), Expected: mustBe("false"), }) // // String test.EqualStr(t, td.Zero().String(), "Zero()") } func TestNotZero(t *testing.T) { checkOK(t, 12, td.NotZero()) checkOK(t, int64(12), td.NotZero()) checkOK(t, float64(12), td.NotZero()) checkOK(t, map[string]int{}, td.NotZero()) checkOK(t, []int{}, td.NotZero()) checkOK(t, [3]int{1}, td.NotZero()) checkOK(t, MyStruct{ValInt: 1}, td.NotZero()) checkOK(t, &MyStruct{}, td.NotZero()) checkOK(t, make(chan int), td.NotZero()) checkOK(t, func() {}, td.NotZero()) checkOK(t, true, td.NotZero()) checkError(t, nil, td.NotZero(), expectedError{ Message: mustBe("zero value"), Path: mustBe("DATA"), Got: mustBe("nil"), Expected: mustBe("NotZero()"), }) checkError(t, 0, td.NotZero(), expectedError{ Message: mustBe("zero value"), Path: mustBe("DATA"), Got: mustBe("0"), Expected: mustBe("NotZero()"), }) checkError(t, int64(0), td.NotZero(), expectedError{ Message: mustBe("zero value"), Path: mustBe("DATA"), Got: mustBe("(int64) 0"), Expected: mustBe("NotZero()"), }) checkError(t, float64(0), td.NotZero(), expectedError{ Message: mustBe("zero value"), Path: mustBe("DATA"), Got: mustBe("0.0"), Expected: mustBe("NotZero()"), }) checkError(t, (map[string]int)(nil), td.NotZero(), expectedError{ Message: mustBe("zero value"), Path: mustBe("DATA"), Got: mustBe("(map[string]int) "), Expected: mustBe("NotZero()"), }) checkError(t, ([]int)(nil), td.NotZero(), expectedError{ Message: mustBe("zero value"), Path: mustBe("DATA"), Got: mustBe("([]int) "), Expected: mustBe("NotZero()"), }) checkError(t, [3]int{}, td.NotZero(), expectedError{ Message: mustBe("zero value"), Path: mustBe("DATA"), Got: mustContain("0"), Expected: mustBe("NotZero()"), }) checkError(t, MyStruct{}, td.NotZero(), expectedError{ Message: mustBe("zero value"), Path: mustBe("DATA"), Got: mustContain(`ValInt: (int) 0`), Expected: mustBe("NotZero()"), }) checkError(t, &MyStruct{}, td.Ptr(td.NotZero()), expectedError{ Message: mustBe("zero value"), Path: mustBe("*DATA"), // in fact, pointer on 0'ed struct contents Got: mustContain(`ValInt: (int) 0`), Expected: mustBe("NotZero()"), }) checkError(t, false, td.NotZero(), expectedError{ Message: mustBe("zero value"), Path: mustBe("DATA"), Got: mustBe("false"), Expected: mustBe("NotZero()"), }) // // String test.EqualStr(t, td.NotZero().String(), "NotZero()") } func TestZeroTypeBehind(t *testing.T) { equalTypes(t, td.Zero(), nil) equalTypes(t, td.NotZero(), nil) } golang-github-maxatome-go-testdeep-1.14.0/td/tuple.go000066400000000000000000000031641454313311600224470ustar00rootroot00000000000000// Copyright (c) 2020, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td import ( "reflect" "github.com/maxatome/go-testdeep/internal/flat" ) var tupleType = reflect.TypeOf(tuple{}) // A Tuple is an immutable container. It is used to easily compare // several values at once, typically when a function returns several // values: // // price := func(p float64) (float64, string, error) { // if p < 0 { // return 0, "", errors.New("negative price not supported") // } // return p * 1.2, "€", nil // } // // td.Cmp(t, // td.TupleFrom(price(10)), // td.TupleFrom(float64(12), "€", nil), // ) // // td.Cmp(t, // td.TupleFrom(price(-10)), // td.TupleFrom(float64(0), "", td.Not(nil)), // ) // // Once initialized with [TupleFrom], a Tuple is immutable. type Tuple interface { // Len returns t length, aka the number of items the tuple contains. Len() int // Index returns t's i'th element. It panics if i is out of range. Index(int) any } // TupleFrom returns a new [Tuple] initialized to the values of vals. // // td.TupleFrom(float64(0), "", td.Not(nil)) // // [Flatten] can be used to flatten non-[]any slice/array into a // new [Tuple]: // // ints := []int64{1, 2, 3} // td.TupleFrom(td.Flatten(ints), "OK", nil) // // is the same as: // // td.TupleFrom(int64(1), int64(2), int64(3), "OK", nil) func TupleFrom(vals ...any) Tuple { return tuple(flat.Interfaces(vals...)) } type tuple []any func (t tuple) Len() int { return len(t) } func (t tuple) Index(i int) any { return t[i] } golang-github-maxatome-go-testdeep-1.14.0/td/tuple_test.go000066400000000000000000000023251454313311600235040ustar00rootroot00000000000000// Copyright (c) 2020, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td_test import ( "errors" "testing" "github.com/maxatome/go-testdeep/internal/test" "github.com/maxatome/go-testdeep/td" ) func TestTuple(t *testing.T) { multi := func() (a int, b string, err error) { return 12, "test", errors.New("err") } tuple := td.TupleFrom(multi()) test.EqualInt(t, tuple.Len(), 3) test.EqualInt(t, tuple.Index(0).(int), 12) test.EqualStr(t, tuple.Index(1).(string), "test") test.EqualStr(t, tuple.Index(2).(error).Error(), "err") td.Cmp(t, td.TupleFrom(multi()), td.TupleFrom(12, "test", td.Not(nil)), ) price := func(p float64) (float64, string, error) { if p < 0 { return 0, "", errors.New("negative price not supported") } return p * 1.2, "€", nil } td.Cmp(t, td.TupleFrom(price(10)), td.TupleFrom(float64(12), "€", nil), ) td.Cmp(t, td.TupleFrom(price(-10)), td.TupleFrom(float64(0), "", td.Not(nil)), ) // With Flatten td.Cmp(t, td.TupleFrom(td.Flatten([]int64{1, 2, 3}), "OK", nil), td.TupleFrom(int64(1), int64(2), int64(3), "OK", nil), ) } golang-github-maxatome-go-testdeep-1.14.0/td/types.go000066400000000000000000000123461454313311600224640ustar00rootroot00000000000000// Copyright (c) 2018-2021, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td import ( "reflect" "strings" "testing" "github.com/maxatome/go-testdeep/internal/ctxerr" "github.com/maxatome/go-testdeep/internal/location" "github.com/maxatome/go-testdeep/internal/types" ) var ( tType = reflect.TypeOf((*T)(nil)) testDeeper = reflect.TypeOf((*TestDeep)(nil)).Elem() smuggledGotType = reflect.TypeOf(SmuggledGot{}) smuggledGotPtrType = reflect.TypeOf((*SmuggledGot)(nil)) recvKindType = reflect.TypeOf(RecvNothing) ) // TestingT is the minimal interface used by [Cmp] to report errors. It // is commonly implemented by [*testing.T] and [*testing.B]. type TestingT interface { Error(args ...any) Fatal(args ...any) Helper() } // TestingFT is a deprecated alias of [testing.TB]. Use [testing.TB] // directly in new code. type TestingFT = testing.TB // TestDeep is the representation of a [go-testdeep operator]. It is not // intended to be used directly, but through Cmp* functions. // // [go-testdeep operator]: https://go-testdeep.zetta.rocks/operators/ type TestDeep interface { types.TestDeepStringer location.GetLocationer // Match checks got against the operator. It returns nil if it matches. Match(ctx ctxerr.Context, got reflect.Value) *ctxerr.Error setLocation(int) replaceLocation(location.Location) // HandleInvalid returns true if the operator is able to handle // untyped nil value. Otherwise the untyped nil value is handled // generically. HandleInvalid() bool // TypeBehind returns the type handled by the operator or nil if it // is not known. tdhttp helper uses it to know how to unmarshal HTTP // responses bodies before comparing them using the operator. TypeBehind() reflect.Type // Error returns nil if the operator is operational, the // corresponding error otherwise. Error() error } // base is a base type providing some methods needed by the TestDeep // interface. type base struct { types.TestDeepStamp location location.Location err *ctxerr.Error } func pkgFunc(full string) (string, string) { // the/package.Foo → "the/package", "Foo" // the/package.(*T).Foo → "the/package", "(*T).Foo" // the/package.glob..func1 → "the/package", "glob..func1" sp := strings.LastIndexByte(full, '/') if sp < 0 { sp = 0 // std package without any '/' in name } dp := strings.IndexByte(full[sp:], '.') if dp < 0 { return full, "" } dp += sp return full[:dp], full[dp+1:] } // setLocation sets location using the stack trace going callDepth levels up. func (t *base) setLocation(callDepth int) { if callDepth < 0 { return } var ok bool t.location, ok = location.New(callDepth) if !ok { t.location.File = "???" t.location.Line = 0 return } // Here package is github.com/maxatome/go-testdeep, or its vendored // counterpart var pkg string pkg, t.location.Func = pkgFunc(t.location.Func) // Try to go one level upper, if we are still in go-testdeep package cmpLoc, ok := location.New(callDepth + 1) if ok { cmpPkg, _ := pkgFunc(cmpLoc.Func) if cmpPkg == pkg { t.location.File = cmpLoc.File t.location.Line = cmpLoc.Line t.location.BehindCmp = true } } } // replaceLocation replaces the location by loc. func (t *base) replaceLocation(loc location.Location) { t.location = loc } // GetLocation returns a copy of the location.Location where the TestDeep // operator has been created. func (t *base) GetLocation() location.Location { return t.location } // HandleInvalid tells go-testdeep internals that this operator does // not handle nil values directly. func (t base) HandleInvalid() bool { return false } // TypeBehind returns the type handled by the operator. Only few operators // knows the type they are handling. If they do not know, nil is // returned. func (t base) TypeBehind() reflect.Type { return nil } // Error returns nil if the operator is operational, the corresponding // error otherwise. func (t base) Error() error { if t.err == nil { return nil } return t.err } // stringError is a convenience method to call in String() // implementations when the operator is in error. func (t base) stringError() string { return t.GetLocation().Func + "()" } // MarshalJSON implements encoding/json.Marshaler only to returns an // error, as a TestDeep operator should never be JSON marshaled. So // it is better to tell the user he/she does a mistake. func (t base) MarshalJSON() ([]byte, error) { return nil, types.OperatorNotJSONMarshallableError(t.location.Func) } // newBase returns a new base struct with location.Location set to the // callDepth depth. func newBase(callDepth int) (b base) { b.setLocation(callDepth) return } // baseOKNil is a base type providing some methods needed by the TestDeep // interface, for operators handling nil values. type baseOKNil struct { base } // HandleInvalid tells go-testdeep internals that this operator // handles nil values directly. func (t baseOKNil) HandleInvalid() bool { return true } // newBaseOKNil returns a new baseOKNil struct with location.Location set to // the callDepth depth. func newBaseOKNil(callDepth int) (b baseOKNil) { b.setLocation(callDepth) return } golang-github-maxatome-go-testdeep-1.14.0/td/types_test.go000066400000000000000000000062101454313311600235140ustar00rootroot00000000000000// Copyright (c) 2019-2021, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td_test import ( "encoding/json" "strings" "testing" "github.com/maxatome/go-testdeep/helpers/tdutil" "github.com/maxatome/go-testdeep/internal/test" "github.com/maxatome/go-testdeep/td" ) func TestSetlocation(t *testing.T) { //nolint: gocritic //line types_test.go:10 tt := &tdutil.T{} ok := td.Cmp(tt, 12, 13) if !ok { test.EqualStr(t, tt.LogBuf(), ` types_test.go:11: Failed test DATA: values differ got: 12 expected: 13 `) } else { t.Error("Cmp returned true!") } //nolint: gocritic //line types_test.go:20 tt = &tdutil.T{} ok = td.Cmp(tt, 12, td.Any(13, 14, 15)) if !ok { test.EqualStr(t, tt.LogBuf(), ` types_test.go:21: Failed test DATA: comparing with Any got: 12 expected: Any(13, 14, 15) [under operator Any at types_test.go:23] `) } else { t.Error("Cmp returned true!") } //nolint: gocritic //line types_test.go:30 tt = &tdutil.T{} ok = td.CmpAny(tt, 12, []any{13, 14, 15}) if !ok { test.EqualStr(t, tt.LogBuf(), ` types_test.go:31: Failed test DATA: comparing with Any got: 12 expected: Any(13, 14, 15) `) } else { t.Error("CmpAny returned true!") } //nolint: gocritic //line types_test.go:40 tt = &tdutil.T{} ttt := td.NewT(tt) ok = ttt.Cmp( 12, td.Any(13, 14, 15)) if !ok { test.EqualStr(t, tt.LogBuf(), ` types_test.go:42: Failed test DATA: comparing with Any got: 12 expected: Any(13, 14, 15) [under operator Any at types_test.go:44] `) } else { t.Error("Cmp returned true!") } //nolint: gocritic //line types_test.go:50 tt = &tdutil.T{} ttt = td.NewT(tt) ok = ttt.Any( 12, []any{13, 14, 15}) if !ok { test.EqualStr(t, tt.LogBuf(), ` types_test.go:52: Failed test DATA: comparing with Any got: 12 expected: Any(13, 14, 15) `) } else { t.Error("Cmp returned true!") } //line /a/full/path/types_test.go:50 tt = &tdutil.T{} ttt = td.NewT(tt) ok = ttt.Any( 12, []any{13, 14, 15}) if !ok { test.EqualStr(t, tt.LogBuf(), ` types_test.go:52: Failed test DATA: comparing with Any got: 12 expected: Any(13, 14, 15) This is how we got here: TestSetlocation() /a/full/path/types_test.go:52 `) // at least one '/' in file name → "This is how we got here" } else { t.Error("Cmp returned true!") } } func TestError(t *testing.T) { test.NoError(t, td.Re(`x`).Error()) test.Error(t, td.Re(123).Error()) } func TestMarshalJSON(t *testing.T) { op := td.String("foo") _, err := json.Marshal(op) if test.Error(t, err) { test.IsTrue(t, strings.HasSuffix(err.Error(), "String TestDeep operator cannot be json.Marshal'led")) } } golang-github-maxatome-go-testdeep-1.14.0/td/uniq_type_behind.go000066400000000000000000000031071454313311600246410ustar00rootroot00000000000000// Copyright (c) 2018-2021, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td import ( "reflect" ) // uniqTypeBehindSlice can return a non-nil reflect.Type if all items // known non-interface types are equal, or if only interface types // are found (mostly issued from Isa()) and they are equal. func uniqTypeBehindSlice(items []reflect.Value) reflect.Type { var ( lastIfType, lastType, curType reflect.Type severalIfTypes bool ) for _, item := range items { if !item.IsValid() { return nil // no need to go further } if item.Type().Implements(testDeeper) { curType = item.Interface().(TestDeep).TypeBehind() // Ignore unknown TypeBehind if curType == nil { continue } // Ignore interfaces & interface pointers too (see Isa), but // keep them in mind in case we encounter always the same // interface pointer if curType.Kind() == reflect.Interface || (curType.Kind() == reflect.Ptr && curType.Elem().Kind() == reflect.Interface) { if lastIfType == nil { lastIfType = curType } else if lastIfType != curType { severalIfTypes = true } continue } } else { curType = item.Type() } if lastType != curType { if lastType != nil { return nil } lastType = curType } } // Only one type found if lastType != nil { return lastType } // Only one interface type found if lastIfType != nil && !severalIfTypes { return lastIfType } return nil } golang-github-maxatome-go-testdeep-1.14.0/td/utils.go000066400000000000000000000015261454313311600224560ustar00rootroot00000000000000// Copyright (c) 2018, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td import ( "reflect" "time" "github.com/maxatome/go-testdeep/internal/ctxerr" "github.com/maxatome/go-testdeep/internal/dark" "github.com/maxatome/go-testdeep/internal/types" ) // getTime returns the time.Time that is inside got or that can be // converted from got contents. func getTime(ctx ctxerr.Context, got reflect.Value, mustConvert bool) (time.Time, *ctxerr.Error) { var ( gotIf any ok bool ) if mustConvert { gotIf, ok = dark.GetInterface(got.Convert(types.Time), true) } else { gotIf, ok = dark.GetInterface(got, true) } if !ok { return time.Time{}, ctx.CannotCompareError() } return gotIf.(time.Time), nil } golang-github-maxatome-go-testdeep-1.14.0/td/utils_test.go000066400000000000000000000035051454313311600235140ustar00rootroot00000000000000// Copyright (c) 2018, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package td import ( "fmt" "reflect" "testing" "time" "github.com/maxatome/go-testdeep/internal/ctxerr" "github.com/maxatome/go-testdeep/internal/dark" "github.com/maxatome/go-testdeep/internal/test" ) func TestGetTime(t *testing.T) { type MyTime time.Time oneTime := time.Date(2018, 7, 14, 12, 11, 10, 0, time.UTC) // OK cases for idx, curTest := range []struct { ParamGot any ParamMustConvert bool ExpectedTime time.Time }{ { ParamGot: oneTime, ExpectedTime: oneTime, }, { ParamGot: MyTime(oneTime), ParamMustConvert: true, ExpectedTime: oneTime, }, } { testName := fmt.Sprintf("Test #%d: ", idx) tm, err := getTime(newContext(nil), reflect.ValueOf(curTest.ParamGot), curTest.ParamMustConvert) if !tm.Equal(curTest.ExpectedTime) { test.EqualErrorMessage(t, tm, curTest.ExpectedTime, testName+"time") } if err != nil { test.EqualErrorMessage(t, err, "no error", testName+"should NOT return an error") } } // Simulate error return from dark.GetInterface oldGetInterface := dark.GetInterface defer func() { dark.GetInterface = oldGetInterface }() dark.GetInterface = func(val reflect.Value, force bool) (any, bool) { return nil, false } // Error cases for idx, ctx := range []ctxerr.Context{newContext(nil), newBooleanContext()} { testName := fmt.Sprintf("Test #%d: ", idx) tm, err := getTime(ctx, reflect.ValueOf(oneTime), false) if !tm.Equal(time.Time{}) { test.EqualErrorMessage(t, tm, time.Time{}, testName+"time") } if err == nil { test.EqualErrorMessage(t, "no error", err, testName+"should return an error") } } } golang-github-maxatome-go-testdeep-1.14.0/td_compat.go000066400000000000000000000271021454313311600226570ustar00rootroot00000000000000// Copyright (c) 2020, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package testdeep import ( "github.com/maxatome/go-testdeep/td" ) // TestingT is a deprecated alias of [td.TestingT]. type TestingT = td.TestingT // TestingFT is a deprecated alias of [td.TestingFT], which is itself // a deprecated alias of [testing.TB]. type TestingFT = td.TestingFT // TestDeep is a deprecated alias of [td.TestDeep]. type TestDeep = td.TestDeep // ContextConfig is a deprecated alias of [td.ContextConfig]. type ContextConfig = td.ContextConfig // T is a deprecated alias of [td.T]. type T = td.T // ArrayEntries is a deprecated alias of [td.ArrayEntries]. type ArrayEntries = td.ArrayEntries // BoundsKind is a deprecated alias of [td.BoundsKind]. type BoundsKind = td.BoundsKind // MapEntries is a deprecated alias of [td.MapEntries]. type MapEntries = td.MapEntries // SmuggledGot is a deprecated alias of [td.SmuggledGot]. type SmuggledGot = td.SmuggledGot // StructFields is a deprecated alias of [td.StructFields]. type StructFields = td.StructFields // Incompatible change: testdeep.DefaultContextConfig must be replaced // by td.DefaultContextConfig. Commented here to raise an error if used. // var DefaultContextConfig = td.DefaultContextConfig // Cmp is a deprecated alias of [td.Cmp]. var Cmp = td.Cmp // CmpDeeply is a deprecated alias of [td.CmpDeeply]. var CmpDeeply = td.CmpDeeply // CmpTrue is a deprecated alias of [td.CmpTrue]. var CmpTrue = td.CmpTrue // CmpFalse is a deprecated alias of [td.CmpFalse]. var CmpFalse = td.CmpFalse // CmpError is a deprecated alias of [td.CmpError]. var CmpError = td.CmpError // CmpNoError is a deprecated alias of [td.CmpNoError]. var CmpNoError = td.CmpNoError // CmpPanic is a deprecated alias of [td.CmpPanic]. var CmpPanic = td.CmpPanic // CmpNotPanic is a deprecated alias of [td.CmpNotPanic]. var CmpNotPanic = td.CmpNotPanic // EqDeeply is a deprecated alias of [td.EqDeeply]. var EqDeeply = td.EqDeeply // EqDeeplyError is a deprecated alias of [td.EqDeeplyError]. var EqDeeplyError = td.EqDeeplyError // AddAnchorableStructType is a deprecated alias of [td.AddAnchorableStructType]. var AddAnchorableStructType = td.AddAnchorableStructType // NewT is a deprecated alias of [td.NewT]. var NewT = td.NewT // Assert is a deprecated alias of [td.Assert]. var Assert = td.Assert // Require is a deprecated alias of [td.Require]. var Require = td.Require // AssertRequire is a deprecated alias of [td.AssertRequire]. var AssertRequire = td.AssertRequire // CmpAll is a deprecated alias of [td.CmpAll]. var CmpAll = td.CmpAll // CmpAny is a deprecated alias of [td.CmpAny]. var CmpAny = td.CmpAny // CmpArray is a deprecated alias of [td.CmpArray]. var CmpArray = td.CmpArray // CmpArrayEach is a deprecated alias of [td.CmpArrayEach]. var CmpArrayEach = td.CmpArrayEach // CmpBag is a deprecated alias of [td.CmpBag]. var CmpBag = td.CmpBag // CmpBetween is a deprecated alias of [td.CmpBetween]. var CmpBetween = td.CmpBetween // CmpCap is a deprecated alias of [td.CmpCap]. var CmpCap = td.CmpCap // CmpCode is a deprecated alias of [td.CmpCode]. var CmpCode = td.CmpCode // CmpContains is a deprecated alias of [td.CmpContains]. var CmpContains = td.CmpContains // CmpContainsKey is a deprecated alias of [td.CmpContainsKey]. var CmpContainsKey = td.CmpContainsKey // CmpEmpty is a deprecated alias of [td.CmpEmpty]. var CmpEmpty = td.CmpEmpty // CmpGt is a deprecated alias of [td.CmpGt]. var CmpGt = td.CmpGt // CmpGte is a deprecated alias of [td.CmpGte]. var CmpGte = td.CmpGte // CmpHasPrefix is a deprecated alias of [td.CmpHasPrefix]. var CmpHasPrefix = td.CmpHasPrefix // CmpHasSuffix is a deprecated alias of [td.CmpHasSuffix]. var CmpHasSuffix = td.CmpHasSuffix // CmpIsa is a deprecated alias of [td.CmpIsa]. var CmpIsa = td.CmpIsa // CmpJSON is a deprecated alias of [td.CmpJSON]. var CmpJSON = td.CmpJSON // CmpKeys is a deprecated alias of [td.CmpKeys]. var CmpKeys = td.CmpKeys // CmpLax is a deprecated alias of [td.CmpLax]. var CmpLax = td.CmpLax // CmpLen is a deprecated alias of [td.CmpLen]. var CmpLen = td.CmpLen // CmpLt is a deprecated alias of [td.CmpLt]. var CmpLt = td.CmpLt // CmpLte is a deprecated alias of [td.CmpLte]. var CmpLte = td.CmpLte // CmpMap is a deprecated alias of [td.CmpMap]. var CmpMap = td.CmpMap // CmpMapEach is a deprecated alias of [td.CmpMapEach]. var CmpMapEach = td.CmpMapEach // CmpN is a deprecated alias of [td.CmpN]. var CmpN = td.CmpN // CmpNaN is a deprecated alias of [td.CmpNaN]. var CmpNaN = td.CmpNaN // CmpNil is a deprecated alias of [td.CmpNil]. var CmpNil = td.CmpNil // CmpNone is a deprecated alias of [td.CmpNone]. var CmpNone = td.CmpNone // CmpNot is a deprecated alias of [td.CmpNot]. var CmpNot = td.CmpNot // CmpNotAny is a deprecated alias of [td.CmpNotAny]. var CmpNotAny = td.CmpNotAny // CmpNotEmpty is a deprecated alias of [td.CmpNotEmpty]. var CmpNotEmpty = td.CmpNotEmpty // CmpNotNaN is a deprecated alias of [td.CmpNotNaN]. var CmpNotNaN = td.CmpNotNaN // CmpNotNil is a deprecated alias of [td.CmpNotNil]. var CmpNotNil = td.CmpNotNil // CmpNotZero is a deprecated alias of [td.CmpNotZero]. var CmpNotZero = td.CmpNotZero // CmpPPtr is a deprecated alias of [td.CmpPPtr]. var CmpPPtr = td.CmpPPtr // CmpPtr is a deprecated alias of [td.CmpPtr]. var CmpPtr = td.CmpPtr // CmpRe is a deprecated alias of [td.CmpRe]. var CmpRe = td.CmpRe // CmpReAll is a deprecated alias of [td.CmpReAll]. var CmpReAll = td.CmpReAll // CmpSet is a deprecated alias of [td.CmpSet]. var CmpSet = td.CmpSet // CmpShallow is a deprecated alias of [td.CmpShallow]. var CmpShallow = td.CmpShallow // CmpSlice is a deprecated alias of [td.CmpSlice]. var CmpSlice = td.CmpSlice // CmpSmuggle is a deprecated alias of [td.CmpSmuggle]. var CmpSmuggle = td.CmpSmuggle // CmpSStruct is a deprecated alias of [td.CmpSStruct]. var CmpSStruct = td.CmpSStruct // CmpString is a deprecated alias of [td.CmpString]. var CmpString = td.CmpString // CmpStruct is a deprecated alias of [td.CmpStruct]. var CmpStruct = td.CmpStruct // CmpSubBagOf is a deprecated alias of [td.CmpSubBagOf]. var CmpSubBagOf = td.CmpSubBagOf // CmpSubJSONOf is a deprecated alias of [td.CmpSubJSONOf]. var CmpSubJSONOf = td.CmpSubJSONOf // CmpSubMapOf is a deprecated alias of [td.CmpSubMapOf]. var CmpSubMapOf = td.CmpSubMapOf // CmpSubSetOf is a deprecated alias of [td.CmpSubSetOf]. var CmpSubSetOf = td.CmpSubSetOf // CmpSuperBagOf is a deprecated alias of [td.CmpSuperBagOf]. var CmpSuperBagOf = td.CmpSuperBagOf // CmpSuperJSONOf is a deprecated alias of [td.CmpSuperJSONOf]. var CmpSuperJSONOf = td.CmpSuperJSONOf // CmpSuperMapOf is a deprecated alias of [td.CmpSuperMapOf]. var CmpSuperMapOf = td.CmpSuperMapOf // CmpSuperSetOf is a deprecated alias of [td.CmpSuperSetOf]. var CmpSuperSetOf = td.CmpSuperSetOf // CmpTruncTime is a deprecated alias of [td.CmpTruncTime]. var CmpTruncTime = td.CmpTruncTime // CmpValues is a deprecated alias of [td.CmpValues]. var CmpValues = td.CmpValues // CmpZero is a deprecated alias of [td.CmpZero]. var CmpZero = td.CmpZero // All is a deprecated alias of [td.All]. var All = td.All // Any is a deprecated alias of [td.Any]. var Any = td.Any // Array is a deprecated alias of [td.Array]. var Array = td.Array // ArrayEach is a deprecated alias of [td.ArrayEach]. var ArrayEach = td.ArrayEach // Bag is a deprecated alias of [td.Bag]. var Bag = td.Bag // Between is a deprecated alias of [td.Between]. var Between = td.Between // Cap is a deprecated alias of [td.Cap]. var Cap = td.Cap // Catch is a deprecated alias of [td.Catch]. var Catch = td.Catch // Code is a deprecated alias of [td.Code]. var Code = td.Code // Contains is a deprecated alias of [td.Contains]. var Contains = td.Contains // ContainsKey is a deprecated alias of [td.ContainsKey]. var ContainsKey = td.ContainsKey // Delay is a deprecated alias of [td.ContainsKey]. var Delay = td.Delay // Empty is a deprecated alias of [td.Empty]. var Empty = td.Empty // Gt is a deprecated alias of [td.Gt]. var Gt = td.Gt // Gte is a deprecated alias of [td.Gte]. var Gte = td.Gte // HasPrefix is a deprecated alias of [td.HasPrefix]. var HasPrefix = td.HasPrefix // HasSuffix is a deprecated alias of [td.HasSuffix]. var HasSuffix = td.HasSuffix // Ignore is a deprecated alias of [td.Ignore]. var Ignore = td.Ignore // Isa is a deprecated alias of [td.Isa]. var Isa = td.Isa // JSON is a deprecated alias of [td.JSON]. var JSON = td.JSON // Keys is a deprecated alias of [td.Keys]. var Keys = td.Keys // Lax is a deprecated alias of [td.Lax]. var Lax = td.Lax // Len is a deprecated alias of [td.Len]. var Len = td.Len // Lt is a deprecated alias of [td.Lt]. var Lt = td.Lt // Lte is a deprecated alias of [td.Lte]. var Lte = td.Lte // Map is a deprecated alias of [td.Map]. var Map = td.Map // MapEach is a deprecated alias of [td.MapEach]. var MapEach = td.MapEach // N is a deprecated alias of [td.N]. var N = td.N // NaN is a deprecated alias of [td.NaN]. var NaN = td.NaN // Nil is a deprecated alias of [td.Nil]. var Nil = td.Nil // None is a deprecated alias of [td.None]. var None = td.None // Not is a deprecated alias of [td.Not]. var Not = td.Not // NotAny is a deprecated alias of [td.NotAny]. var NotAny = td.NotAny // NotEmpty is a deprecated alias of [td.NotEmpty]. var NotEmpty = td.NotEmpty // NotNaN is a deprecated alias of [td.NotNaN]. var NotNaN = td.NotNaN // NotNil is a deprecated alias of [td.NotNil]. var NotNil = td.NotNil // NotZero is a deprecated alias of [td.NotZero]. var NotZero = td.NotZero // Ptr is a deprecated alias of [td.Ptr]. var Ptr = td.Ptr // PPtr is a deprecated alias of [td.PPtr]. var PPtr = td.PPtr // Re is a deprecated alias of [td.Re]. var Re = td.Re // ReAll is a deprecated alias of [td.ReAll]. var ReAll = td.ReAll // Set is a deprecated alias of [td.Set]. var Set = td.Set // Shallow is a deprecated alias of [td.Shallow]. var Shallow = td.Shallow // Slice is a deprecated alias of [td.Slice]. var Slice = td.Slice // Smuggle is a deprecated alias of [td.Smuggle]. var Smuggle = td.Smuggle // String is a deprecated alias of [td.String]. var String = td.String // SStruct is a deprecated alias of [td.SStruct]. var SStruct = td.SStruct // Struct is a deprecated alias of [td.Struct]. var Struct = td.Struct // SubBagOf is a deprecated alias of [td.SubBagOf]. var SubBagOf = td.SubBagOf // SubJSONOf is a deprecated alias of [td.SubJSONOf]. var SubJSONOf = td.SubJSONOf // SubMapOf is a deprecated alias of [td.SubMapOf]. var SubMapOf = td.SubMapOf // SubSetOf is a deprecated alias of [td.SubSetOf]. var SubSetOf = td.SubSetOf // SuperBagOf is a deprecated alias of [td.SuperBagOf]. var SuperBagOf = td.SuperBagOf // SuperJSONOf is a deprecated alias of [td.SuperJSONOf]. var SuperJSONOf = td.SuperJSONOf // SuperMapOf is a deprecated alias of [td.SuperMapOf]. var SuperMapOf = td.SuperMapOf // SuperSetOf is a deprecated alias of [td.SuperSetOf]. var SuperSetOf = td.SuperSetOf // Tag is a deprecated alias of [td.Tag]. var Tag = td.Tag // TruncTime is a deprecated alias of [td.TruncTime]. var TruncTime = td.TruncTime // Values is a deprecated alias of [td.Values]. var Values = td.Values // Zero is a deprecated alias of [td.Zero]. var Zero = td.Zero // BoundsInIn is a deprecated alias of [td.BoundsInIn]. const BoundsInIn = td.BoundsInIn // BoundsInOut is a deprecated alias of [td.BoundsInOut]. const BoundsInOut = td.BoundsInOut // BoundsOutIn is a deprecated alias of [td.BoundsOutIn]. const BoundsOutIn = td.BoundsOutIn // BoundsOutOut is a deprecated alias of [td.BoundsOutOut]. const BoundsOutOut = td.BoundsOutOut golang-github-maxatome-go-testdeep-1.14.0/td_compat_test.go000066400000000000000000000226671454313311600237310ustar00rootroot00000000000000// Copyright (c) 2020, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. package testdeep_test import ( "errors" "math" "testing" "time" td "github.com/maxatome/go-testdeep" ) // These tests are only here to ensure that all obsolete but aliased // functions are still callable from outside. // // See https://pkg.go.dev/github.com/maxatome/go-testdeep/td for real // tests and examples. func TestCompat(tt *testing.T) { type MyStruct struct { Num int64 `json:"num"` Str string `json:"str"` } tt.Run("Cmp", func(t *testing.T) { td.Cmp(t, 1, 1) td.CmpDeeply(t, 1, 1) td.CmpTrue(t, true) td.CmpFalse(t, false) td.CmpError(t, errors.New("Error")) td.CmpNoError(t, nil) td.CmpPanic(t, func() { panic("boom!") }, "boom!") td.CmpNotPanic(t, func() {}) td.CmpTrue(t, td.EqDeeply(1, 1)) td.CmpNoError(t, td.EqDeeplyError(1, 1)) }) td.AddAnchorableStructType(func(nextAnchor int) MyStruct { return MyStruct{Num: 999999999 - int64(nextAnchor)} }) tt.Run("td.T", func(tt *testing.T) { t := td.NewT(tt) t.Cmp(1, 1) assert := td.Assert(tt) assert.Cmp(1, 1) require := td.Require(tt) require.Cmp(1, 1) assert, require = td.AssertRequire(tt) assert.Cmp(1, 1) require.Cmp(1, 1) }) tt.Run("All", func(t *testing.T) { td.Cmp(t, 1, td.All(1)) td.CmpAll(t, 1, []any{1}) }) tt.Run("Any", func(t *testing.T) { td.Cmp(t, 1, td.Any(3, 2, 1)) td.CmpAny(t, 1, []any{3, 2, 1}) }) tt.Run("Array", func(t *testing.T) { td.Cmp(t, [2]int{1, 2}, td.Array([2]int{}, td.ArrayEntries{0: 1, 1: 2})) td.CmpArray(t, [2]int{1, 2}, [2]int{}, td.ArrayEntries{0: 1, 1: 2}) }) tt.Run("ArrayEach", func(t *testing.T) { got := []int{1, 1} td.Cmp(t, got, td.ArrayEach(1)) td.CmpArrayEach(t, got, 1) }) tt.Run("Bag", func(t *testing.T) { got := []int{1, 2} td.Cmp(t, got, td.Bag(1, 2)) td.CmpBag(t, got, []any{1, 2}) }) tt.Run("Between", func(t *testing.T) { for _, bounds := range []td.BoundsKind{ td.BoundsInIn, td.BoundsInOut, td.BoundsOutIn, td.BoundsOutOut, } { td.Cmp(t, 5, td.Between(0, 10, bounds)) td.CmpBetween(t, 5, 0, 10, bounds) } }) tt.Run("Cap", func(t *testing.T) { got := make([]int, 2, 3) td.Cmp(t, got, td.Cap(3)) td.CmpCap(t, got, 3) }) tt.Run("Catch", func(t *testing.T) { var num int td.Cmp(t, 12, td.Catch(&num, 12)) td.Cmp(t, num, 12) }) tt.Run("Code", func(t *testing.T) { fn := func(n int) bool { return n == 5 } td.Cmp(t, 5, td.Code(fn)) td.CmpCode(t, 5, fn) }) tt.Run("Contains", func(t *testing.T) { td.Cmp(t, "foobar", td.Contains("ob")) td.CmpContains(t, "foobar", "ob") }) tt.Run("ContainsKey", func(t *testing.T) { got := map[string]bool{"a": false} td.Cmp(t, got, td.ContainsKey("a")) td.CmpContainsKey(t, got, "a") }) tt.Run("Empty", func(t *testing.T) { td.Cmp(t, "", td.Empty()) td.CmpEmpty(t, "") }) tt.Run("Gt", func(t *testing.T) { td.Cmp(t, 5, td.Gt(3)) td.CmpGt(t, 5, 3) }) tt.Run("Gte", func(t *testing.T) { td.Cmp(t, 5, td.Gte(3)) td.CmpGte(t, 5, 3) }) tt.Run("HasPrefix", func(t *testing.T) { td.Cmp(t, "foobar", td.HasPrefix("foo")) td.CmpHasPrefix(t, "foobar", "foo") }) tt.Run("HasSuffix", func(t *testing.T) { td.Cmp(t, "foobar", td.HasSuffix("bar")) td.CmpHasSuffix(t, "foobar", "bar") }) td.Cmp(tt, 42, td.Ignore()) tt.Run("Isa", func(t *testing.T) { td.Cmp(t, 2, td.Isa(0)) td.CmpIsa(t, 2, 0) }) tt.Run("JSON", func(t *testing.T) { td.Cmp(t, []int{1, 2}, td.JSON(`[1,$val]`, td.Tag("val", 2))) td.CmpJSON(t, []int{1, 2}, `[1,$val]`, []any{td.Tag("val", 2)}) }) tt.Run("Keys", func(t *testing.T) { got := map[string]bool{"a": false} td.Cmp(t, got, td.Keys([]string{"a"})) td.CmpKeys(t, got, []string{"a"}) }) tt.Run("Lax", func(t *testing.T) { td.Cmp(t, int64(42), td.Lax(42)) td.CmpLax(t, int64(42), 42) }) tt.Run("Len", func(t *testing.T) { got := make([]int, 2, 3) td.Cmp(t, got, td.Len(2)) td.CmpLen(t, got, 2) }) tt.Run("Lt", func(t *testing.T) { td.Cmp(t, 5, td.Lt(10)) td.CmpLt(t, 5, 10) }) tt.Run("Lte", func(t *testing.T) { td.Cmp(t, 5, td.Lte(10)) td.CmpLte(t, 5, 10) }) tt.Run("Map", func(t *testing.T) { got := map[string]bool{"a": false, "b": true} td.Cmp(t, got, td.Map(map[string]bool{"a": false}, td.MapEntries{"b": true})) td.CmpMap(t, got, map[string]bool{"a": false}, td.MapEntries{"b": true}) }) tt.Run("MapEach", func(t *testing.T) { got := map[string]int{"a": 1} td.Cmp(t, got, td.MapEach(1)) td.CmpMapEach(t, got, 1) }) tt.Run("N", func(t *testing.T) { td.Cmp(t, 12, td.N(10, 2)) td.CmpN(t, 12, 10, 2) }) tt.Run("NaN", func(t *testing.T) { td.Cmp(t, math.NaN(), td.NaN()) td.CmpNaN(t, math.NaN()) }) tt.Run("Nil", func(t *testing.T) { td.Cmp(t, nil, td.Nil()) td.CmpNil(t, nil) }) tt.Run("None", func(t *testing.T) { td.Cmp(t, 28, td.None(3, 4, 5)) td.CmpNone(t, 28, []any{3, 4, 5}) }) tt.Run("Not", func(t *testing.T) { td.Cmp(t, 28, td.Not(3)) td.CmpNot(t, 28, 3) }) tt.Run("NotAny", func(t *testing.T) { got := []int{5} td.Cmp(t, got, td.NotAny(1, 2, 3)) td.CmpNotAny(t, got, []any{1, 2, 3}) }) tt.Run("NotEmpty", func(t *testing.T) { td.Cmp(t, "OOO", td.NotEmpty()) td.CmpNotEmpty(t, "OOO") }) tt.Run("NotNaN", func(t *testing.T) { td.Cmp(t, 12., td.NotNaN()) td.CmpNotNaN(t, 12.) }) tt.Run("NotNil", func(t *testing.T) { td.Cmp(t, 4, td.NotNil()) td.CmpNotNil(t, 4) }) tt.Run("NotZero", func(t *testing.T) { td.Cmp(t, 3, td.NotZero()) td.CmpNotZero(t, 3) }) tt.Run("Ptr", func(t *testing.T) { num := 12 td.Cmp(t, &num, td.Ptr(12)) td.CmpPtr(t, &num, 12) }) tt.Run("PPtr", func(t *testing.T) { num := 12 pnum := &num td.Cmp(t, &pnum, td.PPtr(12)) td.CmpPPtr(t, &pnum, 12) }) tt.Run("Re", func(t *testing.T) { td.Cmp(t, "foobar", td.Re(`o+`)) td.CmpRe(t, "foobar", `o+`, nil) }) tt.Run("ReAll", func(t *testing.T) { td.Cmp(t, "foo bar", td.ReAll(`([a-z]+)(?: |\z)`, td.Bag("bar", "foo"))) td.CmpReAll(t, "foo bar", `([a-z]+)(?: |\z)`, td.Bag("bar", "foo")) }) tt.Run("Set", func(t *testing.T) { got := []int{1, 1, 2} td.Cmp(t, got, td.Set(2, 1)) td.CmpSet(t, got, []any{2, 1}) }) tt.Run("Shallow", func(t *testing.T) { got := []int{1, 2, 3} expected := got[:1] td.Cmp(t, got, td.Shallow(expected)) td.CmpShallow(t, got, expected) }) tt.Run("Slice", func(t *testing.T) { got := []int{1, 2} td.Cmp(t, got, td.Slice([]int{}, td.ArrayEntries{0: 1, 1: 2})) td.CmpSlice(t, got, []int{}, td.ArrayEntries{0: 1, 1: 2}) }) tt.Run("Smuggle", func(t *testing.T) { fn := func(v int) int { return v * 2 } td.Cmp(t, 5, td.Smuggle(fn, 10)) td.CmpSmuggle(t, 5, fn, 10) }) tt.Run("String", func(t *testing.T) { td.Cmp(t, "foo", td.String("foo")) td.CmpString(t, "foo", "foo") }) tt.Run("SStruct", func(t *testing.T) { got := MyStruct{ Num: 42, Str: "foo", } td.Cmp(t, got, td.SStruct(MyStruct{Num: 42}, td.StructFields{"Str": "foo"})) td.CmpSStruct(t, got, MyStruct{Num: 42}, td.StructFields{"Str": "foo"}) }) tt.Run("Struct", func(t *testing.T) { got := MyStruct{ Num: 42, Str: "foo", } td.Cmp(t, got, td.Struct(MyStruct{Num: 42}, td.StructFields{"Str": "foo"})) td.CmpStruct(t, got, MyStruct{Num: 42}, td.StructFields{"Str": "foo"}) }) tt.Run("SubBagOf", func(t *testing.T) { got := []int{1} td.Cmp(t, got, td.SubBagOf(1, 1, 2)) td.CmpSubBagOf(t, got, []any{1, 1, 2}) }) tt.Run("SubJSONOf", func(t *testing.T) { got := MyStruct{ Num: 42, Str: "foo", } td.Cmp(t, got, td.SubJSONOf(`{"num":42,"str":$str,"zip":45600}`, td.Tag("str", "foo"))) td.CmpSubJSONOf(t, got, `{"num":42,"str":$str,"zip":45600}`, []any{td.Tag("str", "foo")}) }) tt.Run("SubMapOf", func(t *testing.T) { got := map[string]int{"a": 1, "b": 2} td.Cmp(t, got, td.SubMapOf(map[string]int{"a": 1, "c": 3}, td.MapEntries{"b": 2})) td.CmpSubMapOf(t, got, map[string]int{"a": 1, "c": 3}, td.MapEntries{"b": 2}) }) tt.Run("SubSetOf", func(t *testing.T) { got := []int{1, 1} td.Cmp(t, got, td.SubSetOf(1, 2)) td.CmpSubSetOf(t, got, []any{1, 2}) }) tt.Run("SuperBagOf", func(t *testing.T) { got := []int{1, 1, 2} td.Cmp(t, got, td.SuperBagOf(1)) td.CmpSuperBagOf(t, got, []any{1}) }) tt.Run("SuperJSONOf", func(t *testing.T) { got := MyStruct{ Num: 42, Str: "foo", } td.Cmp(t, got, td.SuperJSONOf(`{"str":$str}`, td.Tag("str", "foo"))) td.CmpSuperJSONOf(t, got, `{"str":$str}`, []any{td.Tag("str", "foo")}) }) tt.Run("SuperMapOf", func(t *testing.T) { got := map[string]int{"a": 1, "b": 2, "c": 3} td.Cmp(t, got, td.SuperMapOf(map[string]int{"a": 1}, td.MapEntries{"b": 2})) td.CmpSuperMapOf(t, got, map[string]int{"a": 1}, td.MapEntries{"b": 2}) }) tt.Run("SuperSetOf", func(t *testing.T) { got := []int{1, 1, 2} td.Cmp(t, got, td.SuperSetOf(1)) td.CmpSuperSetOf(t, got, []any{1}) }) tt.Run("TruncTime", func(t *testing.T) { got, err := time.Parse(time.RFC3339Nano, "2020-02-22T12:34:56.789Z") if err != nil { t.Fatal(err) } expected, err := time.Parse(time.RFC3339, "2020-02-22T12:34:56Z") if err != nil { t.Fatal(err) } td.Cmp(t, got, td.TruncTime(expected, time.Second)) td.CmpTruncTime(t, got, expected, time.Second) }) tt.Run("Values", func(t *testing.T) { got := map[string]string{"a": "b"} td.Cmp(t, got, td.Values([]string{"b"})) td.CmpValues(t, got, []string{"b"}) }) tt.Run("Zero", func(t *testing.T) { td.Cmp(t, 0, td.Zero()) td.CmpZero(t, 0) }) } golang-github-maxatome-go-testdeep-1.14.0/testdeep.go000066400000000000000000000033231454313311600225210ustar00rootroot00000000000000// Copyright (c) 2018, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. // Package testdeep allows extremely flexible deep comparison. It is // built for testing. // // It is a go rewrite and adaptation of wonderful [Test::Deep] perl // module. // // In golang, comparing data structure is usually done using // [reflect.DeepEqual] or using a package that uses this function // behind the scene. // // This function works very well, but it is not flexible. Both // compared structures must match exactly. // // The purpose of go-testdeep is to do its best to introduce this // missing flexibility using ["operators"] when the expected value (or // one of its component) cannot be matched exactly. // // testdeep package should not be used in new code, even if it can for // backward compatibility reasons, but [td] package. // // All variables and types of testdeep package are aliases to // respectively functions and types of [td] package. They are only // here for compatibility purpose as // // import "github.com/maxatome/go-testdeep/td" // // should now be used, in preference of older, but still supported: // // import td "github.com/maxatome/go-testdeep" // // For easy HTTP API testing, see [tdhttp] package. // // For tests suites also just as easy, see [tdsuite] package. // // [Test::Deep]: https://metacpan.org/pod/Test::Deep // ["operators"]: https://go-testdeep.zetta.rocks/operators/ // [tdhttp]: https://pkg.go.dev/github.com/maxatome/go-testdeep/helpers/tdhttp // [tdsuite]: https://pkg.go.dev/github.com/maxatome/go-testdeep/helpers/tdsuite package testdeep // import "github.com/maxatome/go-testdeep" golang-github-maxatome-go-testdeep-1.14.0/tools/000077500000000000000000000000001454313311600215145ustar00rootroot00000000000000golang-github-maxatome-go-testdeep-1.14.0/tools/gen_funcs.pl000077500000000000000000001050561454313311600240320ustar00rootroot00000000000000#!/usr/bin/env perl # Copyright (c) 2018-2022, Maxime Soulé # All rights reserved. # # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. use strict; use warnings; use autodie; use 5.010; use IPC::Open2; die "usage $0 [-h]\n" if @ARGV != 0; (my $REPO_DIR = $0) =~ s,/[^/]+\z,/..,; -d $REPO_DIR or die "Cannot find repository directory ($REPO_DIR)\n"; # Check .golangci.yml vs .github/workflows/ci.yml if (open(my $fh, '<', "$REPO_DIR/.github/workflows/ci.yml")) { my($ci_min, $linter_min); while (defined(my $line = <$fh>)) { if ($line =~ /^\s+go-version: \[(\d+\.\d+)/) { $ci_min = $1; last; } } close $fh; $ci_min // die "*** Cannot extract min go version from .github/workflows/ci.yml\n"; undef $fh; open($fh, '<', "$REPO_DIR/.golangci.yml"); while (defined(my $line = <$fh>)) { if ($line =~ /^\s+go: '([\d.]+)'/) { $linter_min = $1; last; } } close $fh; $linter_min // die "*** Cannot extract min go version from .golangci.yml\n"; if ($ci_min ne $linter_min) { die "*** min go versions mismatch: ci=$ci_min linter=$linter_min\n"; } } my $SITE_REPO_DIR = "$REPO_DIR/../go-testdeep-site"; unless (-d $SITE_REPO_DIR) { if ($ENV{PROD_SITE}) { die "*** Cannot PROD_SITE as $SITE_REPO_DIR not found!\n"; } warn "*** WARNING: cannot find $SITE_REPO_DIR. Disabling site upgrade.\n"; undef $SITE_REPO_DIR; } my $DIR = "$REPO_DIR/td"; -d $DIR or die "Cannot find td/ directory ($DIR)\n"; my $URL_ZETTA = 'https://go-testdeep.zetta.rocks'; my $URL_GODEV = 'https://pkg.go.dev'; my $URL_GODOC = "$URL_GODEV/github.com/maxatome/go-testdeep"; my $HEADER = <<'EOH'; // Copyright (c) 2018-2022, Maxime Soulé // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. // // DO NOT EDIT!!! AUTOMATICALLY GENERATED!!! EOH my $args_comment_src = <<'EOC'; %arg{args...} are optional and allow to name the test. This name is used in case of failure to qualify the test. If %code{len(args) > 1} and the first item of %arg{args} is a string and contains a '%' rune then [fmt.Fprintf] is used to compose the name, else %arg{args} are passed to [fmt.Fprint]. Do not forget it is the name of the test, not the reason of a potential failure. EOC my $ARGS_COMMENT_GD = doc2godoc($args_comment_src); my $ARGS_COMMENT_MD = doc2md($args_comment_src); # These functions are variadics, but with only one possible param. In # this case, discard the variadic property and use a default value for # this optional parameter. my %IGNORE_VARIADIC = (Between => 'td.BoundsInIn', N => 0, Re => 'nil', Recv => 0, TruncTime => 0, # These operators accept several StructFields, # but we want only one here Struct => 'nil', SStruct => 'nil'); # Smuggler operators (automatically filled) my %SMUGGLER_OPERATORS; # These operators should be renamed when used as *T method my %RENAME_METHOD = (Lax => 'CmpLax', ErrorIs => 'CmpErrorIs'); # These operators do not have *T method nor Cmp shortcut my %ONLY_OPERATORS = map { $_ => 1 } qw(Catch Delay Ignore Tag); my @INPUT_LABELS = qw(nil bool str int float cplx array slice map struct ptr if chan func); my %INPUTS; @INPUTS{@INPUT_LABELS} = (); opendir(my $dh, $DIR); my(%funcs, %operators, %consts, %forbiddenOpsInJSON); while (readdir $dh) { if (/^td_.*\.go\z/ and not /_test.go\z/) { my $contents = slurp("$DIR/$_"); # Load the operators forbidden inside JSON() if ($_ eq 'td_json.go') { $contents =~ /^var forbiddenOpsInJSON = map\[string\]string\{(.*?)^\}/ms or die "$_: forbiddenOpsInJSON map not found\n"; @forbiddenOpsInJSON{$1 =~ /"([^"]+)":/g} = (); } while ($contents =~ /^const \(\n(.+)^\)\n/gms) { @consts{$1 =~ /^\t([A-Z]\w+)/mg} = (); } my %imports = map { ($_ => $_) } qw(fmt io ioutil os reflect testing); if ($contents =~ /^import \(\n(.+?)\s*\)\n/ms) { foreach my $pkg (split(/\n+/, $1)) { if ($pkg =~ /^\s*(\w+)\s+\"([^"]+)/) { $imports{$1} = $2; $imports{$2} = $2; } elsif ($pkg =~ m,^\s*"((?:.+/)?([^/"]+)),) { $imports{$2} = $1; $imports{$1} = $1; } else { die "$_: cannot parse import line <$pkg>\n"; } } } my %ops; while ($contents =~ m,^// summary\((\w+)\): (.*\n(?://.*\n)*),gm) { my($op, $summary) = ($1, $2); $summary =~ s,^// input\(.*,,sm; $ops{$op} = process_summary($summary =~ s,\n(?://|\z),,gr); } my %inputs; while ($contents =~ m,^// input\((\w+)\): (.*\n(?://.*\n)*),gm) { my $op = $1; foreach my $in (split(/\s*,\s*/, $2 =~ s,\n(?://|\z),,gr)) { if ($in eq 'all') { @{$inputs{$op}}{keys %INPUTS} = ('✓') x keys %INPUTS; next; } if ($in =~ /^(\w+)\((.*)\)\z/) { $inputs{$op}{$1} = process_summary($2); $in = $1; } else { $inputs{$op}{$in} = '✓'; } exists $INPUTS{$in} or die "$_: input($op) unknown input '$in'\n"; $inputs{$op}{if} //= '✓'; # interface } } my $num_smugglers = keys %SMUGGLER_OPERATORS; while ($contents =~ m,^(// ([A-Z]\w*) .*\n(?://.*\n)*)func \2\((.*?)\) TestDeep \{\n,gm) { exists $ops{$2} or die "$_: no summary($2) found\n"; exists $inputs{$2} or die "$_: no input($2) found\n"; my($doc, $func, $params) = ($1, $2, $3); if ($doc =~ /is a smuggler operator/) { $SMUGGLER_OPERATORS{$func} = 1; } my @args; foreach my $arg (split(/, /, $params)) { my %arg; @arg{qw(name type)} = split(/ /, $arg, 2); if (defined $arg{type} and $arg{variadic} = $arg{type} =~ s/^\.{3}//) { if (exists $IGNORE_VARIADIC{$func}) { $arg{default} = $IGNORE_VARIADIC{$func}; delete $arg{variadic}; } } push(@args, \%arg); } my $last_type; foreach my $arg (reverse @args) { if (defined(my $arg_type = $arg->{type}) and not $arg->{variadic}) { if (defined $last_type and $arg_type eq $last_type) { delete $arg->{type}; } $last_type = $arg_type; } } $funcs{$func}{args} = \@args unless $ONLY_OPERATORS{$func}; # "//" is OK, otherwise TAB is not allowed die "TAB detected in $func operator documentation\n" if $doc =~ m,(? $func, summary => delete $ops{$func}, input => delete $inputs{$func}, doc => $doc, signature => "func $func($params) TestDeep", args => \@args, imports => \%imports, }; } if (%ops) { die "$_: summary found without operator definition: " . join(', ', keys %ops) . "\n"; } if (%inputs) { die "$_: input found without operator definition: " . join(', ', keys %inputs) . "\n"; } if ($contents =~ m,^\ttdSmugglerBase(?! // ignored),m and $num_smugglers == keys %SMUGGLER_OPERATORS) { die "$_: this file should contain at least one smuggler operator\n"; } } } closedir($dh); %funcs or die "No TestDeep golang source file found!\n"; my $funcs_contents = my $t_contents = <{type}) { if ($arg->{type} ne 'any' or $arg->{variadic}) { $cmp_args .= ' any'; } last } } } else { $cmp_args .= ' any'; } my $call_args = ''; my @cmpt_args; foreach my $arg (@{$funcs{$func}{args}}) { $call_args .= ', ' unless $call_args eq ''; $call_args .= $arg->{name}; push(@cmpt_args, { name => $arg->{name} }); $cmp_args .= ", $arg->{name} "; if ($arg->{variadic}) { $call_args .= '...'; $cmp_args .= '[]'; } $cmp_args .= $arg->{type} if defined $arg->{type}; } my $cmp_doc = <{default}) { my $default = $last_arg->{default}; $default = "[$1]" if $default =~ /^td\.(.+)/ and exists $consts{$1}; $func_comment .= <{name} is here mandatory. $default value should be passed to mimic its absence in original [$func] call. EOF } $func_comment .= < $name // '', code => $code }); } } { open(my $fh, "| gofmt -s > '$DIR/cmp_funcs.go'"); print $fh $funcs_contents; close $fh; say "$DIR/cmp_funcs.go generated"; } { open(my $fh, "| gofmt -s > '$DIR/t.go'"); print $fh $t_contents; close $fh; say "$DIR/t.go generated"; } my $funcs_test_contents = <{name}; foreach my $info ([ "td.Cmp$func(t, ", "Cmp$func", \$funcs_test_contents ], [ "t.$method(", "T_$method",\$t_test_contents ]) { (my $code = $example->{code}) =~ s%td\.Cmp\(t,\s+($rparam),\s+td\.$func($rep)% my @params = extract_params("$func$name", $2); my $repl = $info->[0] . $1; for (my $i = 0; $i < @$args; $i++) { $repl .= ', '; if ($args->[$i]{variadic}) { if (defined $params[$i]) { $repl .= '[]' . $args->[$i]{type} . '{' . join(', ', @params[$i .. $#params]) . '}'; } else { $repl .= 'nil'; } last } $repl .= $params[$i] // $args->[$i]{default} // die("Internal error, no param: " . "$func$name -> #$i/$args->[$i]{name}!\n"); } $repl %egs; ${$info->[2]} .= <[1]$name() { $code} EOF } } } { # Cmp* examples open(my $fh, "| gofmt -s > '$DIR/example_cmp_test.go'"); print $fh $funcs_test_contents; close $fh; say "$DIR/example_cmp_test.go generated"; } { # T.* examples $t_test_contents =~ s/t := &testing\.T\{\}/t := td.NewT(&testing\.T\{\})/g; $t_test_contents =~ s/td\.Cmp\(t,/t.Cmp(/g; open(my $fh, "| gofmt -s > '$DIR/example_t_test.go'"); print $fh $t_test_contents; close $fh; say "$DIR/example_t_test.go generated"; } # # Check "args..." comment is the same everywhere it needs to be my @args_errors; $ARGS_COMMENT_GD = go_comment($ARGS_COMMENT_GD); foreach my $go_file (do { opendir(my $dh, $DIR); grep /(?[0]`]: https://pkg.go.dev/$_->[1]", [ 'fmt.Stringer' => 'fmt/#Stringer' ], [ 'time.Time' => 'time/#Time' ], [ 'math.NaN' => 'math/#NaN' ]) . "\n"; }; my @sorted_operators = sort { lc($a) cmp lc($b) } keys %operators; my $md_links = do { $common_links . join("\n", map qq([`$_`]: {{< ref "$_" >}}), @sorted_operators) . "\n\n" # Cmp* functions . join("\n", map qq([`Cmp$_`]: {{< ref "$_#cmp\L$_\E-shortcut" >}}), @sorted_funcs) . "\n\n" # T.Cmp* methods . join("\n", map { my $m = $RENAME_METHOD{$_} // $_; qq([`T.$m`]: {{< ref "$_#t\L$m\E-shortcut" >}}) } @sorted_funcs); }; my $gh_links = do { my $td_url = "$URL_ZETTA/operators/"; $common_links . join("\n", map qq([`$_`]: $td_url\L$_/), @sorted_operators) . "\n\n" # Cmp* functions . join("\n", map qq([`Cmp$_`]: $td_url\L$_/#cmp$_-shortcut), @sorted_funcs) . "\n\n" # T.Cmp* methods . join("\n", map { my $m = $RENAME_METHOD{$_} // $_; qq([`T.$m`]: $td_url\L$_/#t$m-shortcut) } @sorted_funcs); }; # README.md { my $readme = slurp("$REPO_DIR/README.md"); # Links $readme =~ s{().*()} {$1\n$gh_links\n$2}s; open(my $fh, '>', "$REPO_DIR/README.md.new"); print $fh $readme; close $fh; rename "$REPO_DIR/README.md.new", "$REPO_DIR/README.md"; say "$REPO_DIR/README.md modified"; } # Hugo if (defined $SITE_REPO_DIR) { my $op_examples = slurp("$DIR/example_test.go"); # Reload generated examples so they are properly gofmt'ed my $cmp_examples = slurp("$DIR/example_cmp_test.go"); my $t_examples = slurp("$DIR/example_t_test.go"); foreach my $operator (@sorted_operators) { # Rework each operator doc my $doc = process_doc($operators{$operator}); open(my $fh, '>', "$SITE_REPO_DIR/docs_src/content/operators/$operator.md"); print $fh < See also [ $operator godoc](https://pkg.go.dev/github.com/maxatome/go-testdeep/td#$operator). EOM my @examples; my $re = qr/^func Example${operator}(?:_(\w+))?\(\) \{\n(.+?)^\}$/ms; while ($op_examples =~ /$re/g) { my $name = ucfirst($1 // 'Base'); push(@examples, < 1 ? 's' : ''; print $fh @examples; } if (my $cmp = $operators{$operator}{cmp}) { $cmp->{imports} = $operators{$operator}{imports}; unshift(@{$cmp->{args}}, { name => 't' }); my $doc = process_doc($cmp); shift @{$cmp->{args}}; print $fh <{name} shortcut ```go $cmp->{signature} ``` $doc > See also [ $cmp->{name} godoc](https://pkg.go.dev/github.com/maxatome/go-testdeep/td#$cmp->{name}). EOM @examples = (); my $re = qr/func ExampleCmp${operator}(?:_(\w+))?\(\) \{\n(.+?)^\}$/ms; while ($cmp_examples =~ /$re/g) { my $name = ucfirst($1 // 'Base'); push(@examples, < 1 ? 's' : ''; print $fh @examples; } } if (my $t = $operators{$operator}{t}) { $t->{imports} = $operators{$operator}{imports}; unshift(@{$t->{args}}, { name => 't' }); my $doc = process_doc($t); shift @{$t->{args}}; print $fh <{name} shortcut ```go $t->{signature} ``` $doc > See also [ T.$t->{name} godoc](https://pkg.go.dev/github.com/maxatome/go-testdeep/td#T.$t->{name}). EOM @examples = (); my $re = qr/func ExampleT_$t->{name}(?:_(\w+))?\(\) \{\n(.+?)^\}$/ms; while ($t_examples =~ /$re/g) { my $name = ucfirst($1 // 'Base'); push(@examples, < 1 ? 's' : ''; print $fh @examples; } } close $fh; } # Dump operators { my $op_list_file = "$SITE_REPO_DIR/docs_src/content/operators/_index.md"; my $op_list = slurp($op_list_file); $op_list =~ s{().*()} { "$1\n" . join('', map qq![`$_`]({{< ref "$_" >}})\n: $operators{$_}{summary}\n\n!, @sorted_operators) . $2 }se or die "operators tags not found in $op_list_file\n"; $op_list =~ s{().*()} { "$1\n" . join('', map qq![`$_`]({{< ref "$_" >}})\n: $operators{$_}{summary}\n\n!, sort { lc($a) cmp lc($b) } keys %SMUGGLER_OPERATORS) . "$md_links\n$2" }se or die "smugglers tags not found in $op_list_file\n"; open(my $fh, '>', "$op_list_file.new"); print $fh $op_list; close $fh; rename "$op_list_file.new", $op_list_file; } # Dump matrices { my $matrix_file = "$SITE_REPO_DIR/docs_src/content/operators/matrix.md"; my $matrix = slurp($matrix_file); my $header = <<'EOH'; | Operator vs go type | nil | bool | string | {u,}int* | float* | complex* | array | slice | map | struct | pointer | interface¹ | chan | func | operator | | ------------------- | --- | ---- | ------ | -------- | ------ | -------- | ----- | ----- | --- | ------ | ------- | ---------- | ---- | ---- | -------- | EOH $matrix =~ s{().*()} { my $repl = "$1\n"; my $num = 0; foreach my $op (@sorted_operators) { $repl .= $header if $num++ % 10 == 0; $repl .= "| [`$op`]"; for my $label (@INPUT_LABELS) { $repl .= " | " . ($operators{$op}{input}{$label} // '✗'); } $repl .= " | [`$op`] |\n"; } "$repl\n$md_links\n$2" }se or die "op-go-matrix tags not found in $matrix_file\n"; my %by_input; while (my($op, $info) = each %operators) { while (my($label, $comment) = each %{$operators{$op}{input}}) { $by_input{$label}{$op} = $comment; } } $matrix =~ s{().*()} { my $repl = "$1\n"; foreach my $op (sort keys %{$by_input{$2}}) { my $comment = $by_input{$2}{$op}; next if $comment eq 'todo'; if ($comment eq '✓') { next if $2 eq 'if'; $comment = ''; } elsif ($2 eq 'if') { $comment =~ s/^✓ \+/ →/; } else { substr($comment, 0, 0, ' only '); } $repl .= "- [`$op`]$comment\n"; } $repl . $3 }gse or die "go-op-matrix tags not found in $matrix_file\n"; open(my $fh, '>', "$matrix_file.new"); print $fh $matrix; close $fh; rename "$matrix_file.new", $matrix_file; } # tdhttp example { my $example = slurp("$REPO_DIR/helpers/tdhttp/example_test.go"); my($import) = $example =~ /^(import \(.*?^\))$/ms; $import or die "tdhttp example, import not found!\n"; $example =~ s/.*^func Example\(\) \{\n\tt := &testing.T\{\}\n\n//ms or die "tdhttp example, func Example() not found!\n"; $example =~ s/fmt\.Printf/t.Logf/g or die "tdhttp example, fmt.Printf not found\n"; $example =~ s/fmt\.Println/t.Log/g or die "tdhttp example, fmt.Println not found\n"; $example =~ s,\n\t// Output:\n.*,},s or die "tdhttp example, Output: not found\n"; my $md_file = "$SITE_REPO_DIR/docs_src/content/helpers/_index.md"; my $final = slurp($md_file) =~ s{().*()} <$1 {{%expand "Main example" %}}```go package myapi $import func TestMyAPI(t *testing.T) { $example ```{{% /expand%}} $2>rs or die "tdhttp example not found in $md_file!"; open(my $out, '>', "$md_file.new"); print $out $final; close $out; rename "$md_file.new", $md_file; } # Final publish if ($ENV{PROD_SITE}) { # Delegate to go-testdeep-site repository chdir $SITE_REPO_DIR; exec './publish.sh'; } } # "" → "//" # "\txxx" → "//\txxx" # "xxx" → "// xxx" sub go_comment { shift =~ s{^(.?)} { $1 eq '' ? '//' : substr($1, 0, 1) eq "\t" ? "//$1" : "// $1" }egmr } sub doc2godoc { my $doc = shift; state $repl = { arg => sub { $_[0] }, code => sub { $_[0] }, godoc => sub { $_[0] } }; $doc =~ s/%([a-z]+)\{([^}]+)\}/($repl->{$1} or die $1)->($2)/egr; } sub doc2md { my $doc = shift; state $repl = { arg => sub { "*$_[0]*" }, code => sub { "`$_[0]`" }, godoc => sub { my($pkg, $fn) = split('\.', $_[0], 2); "[`$_[0]`](https://pkg.go.dev/$pkg/#$fn)" } }; $doc =~ s/%([a-z]+)\{([^}]+)\}/($repl->{$1} or die $1)->($2)/egr; } sub process_summary { my $text = shift; $text =~ s/(time\.Time|fmt.Stringer|error)/[`$1`]/g; $text =~ s/BeLax config flag/[`BeLax` config flag]/; $text =~ s/(\[\]byte|\bnil\b)/`$1`/g; return $text; } sub process_doc { my $op = shift; my $doc = $op->{doc}; $doc =~ s,^// ?,,mg if $doc =~ m,^//,; $doc =~ s/\n{3,}/\n\n/g; my($inEx, $inBul); $doc =~ s{^(?:(\n?\S) |(\n?)(\s+)(\S+))} < if (defined $1) { if ($inEx) { $inEx = ''; "```\n$1" } elsif ($inBul) { $inBul = ''; "\n$1" } else { $1 } } else { my($nl, $indent, $beg) = ($2, $3, $4); if ($inEx) { $nl . substr($indent, length($inEx)) . $beg } elsif ($inBul) { $nl . substr($indent, length($inBul)) . $beg } elsif ($beg =~ /^---/) { $inEx = $indent; "$nl```\n$beg" } elsif ($beg =~ /^-/) { $inBul = $indent; "\n-" } else { $inEx = $indent; "$nl```go\n$beg" } } >gemx; $doc .= "```\n" if $inEx; # Get & remove links at the end of comment my %links; while ($doc =~ s/^\[([^]\n]+)\]: (.+)\n\z//m) { $links{$1} = $2; } my @codes; $doc =~ s/^(```go\n.*?^```\n)/push(@codes, $1); "CODE<$#codes>"/gems; $doc =~ s{ (? \$\^[A-Za-z]+) # placeholder | \[TestDeep\](?\s+operators?) # operator | (? [^]\n]+) \] (?!\w) # link | (? (?:(?:\[\])+|\*+|\b) (?:bool\b |u?int(?:\*|(?:8|16|32|64)?\b) |float(?:\*|(?:32|64)\b) |complex(?:\*|(?:64|128)\b) |string\b |rune\b |byte\b |any(?!\s+(?:numeric|of|placeholder))) |\(\*byte\)\(nil\) |\bmap\[string\]any |\b(?:len|cap)\(\) |\bnil\b |\$(?:\d+|[a-zA-Z_]\w*)) # native_type | (? \berror\b) # error | (? \bTypeBehind(?:\(\)|\b)) # type_behind | \b(? smuggler\s+operator)\b # smuggler | (? BeLax\s+config\s+flag) # belax }{ if ($+{placeholder}) { "`$+{placeholder}`" } elsif ($+{operator}) { qq![TestDeep$+{operator}]({{< ref "operators" >}})!; } elsif (my $inner = $+{link}) { if ($links{$inner}) { qq![$inner]($links{$inner})!; } elsif ($operators{$inner}) { qq![`$inner`]({{< ref "$inner" >}})!; } # local exported identifier elsif ($inner =~ /^\*?([A-Z]\w*(?:\.[A-Z]\w*)?)\z/) { qq![`$inner`]($URL_GODOC/td#$1)!; } # imported package elsif ($inner =~ m,^\*?([a-z][\w/]*)(?:\.([A-Z]\w*(?:\.[A-Z]\w*)?))?\z, and my $full = $op->{imports}{$1}) { qq![`$inner`]($URL_GODEV/$full! . ($2 ? "#$2" : '') . ')'; } else { qq![$inner]!; } } elsif ($+{native_type}) { "`$+{native_type}`" } elsif ($+{error}) { "[`error`]($URL_GODEV/builtin#error)" } elsif ($+{type_behind}) { qq![`$+{type_behind}`]({{< ref "operators#typebehind-method" >}})! } elsif ($+{smuggler}) { qq![$+{smuggler}]({{< ref "operators#smuggler-operators" >}})! } elsif ($+{belax}) { qq![`BeLax` config flag]($URL_GODOC/td#ContextConfig.BeLax)!; } }geox; $doc =~ s/^See also /> See also /m; if ($op->{args} and @{$op->{args}}) { $doc =~ s/(?{name}), @{$op->{args}})}) (?!\w)/*$1*/gx; } return $doc =~ s/^CODE<(\d+)>/go_format($op, $codes[$1])/egmr; } sub go_format { my($operator, $code) = @_; $code =~ s/^```go\n// or return $code; $code =~ s/\n```\n\z//; my $pid = open2(my $fmt_out, my $fmt_in, 'gofmt', '-s'); my $root; if ($code =~ /^func/) { $root = 1; print $fmt_in <{name}.go:1 $code EOM } else { print $fmt_in <{name}.go:1 func x() { $code } EOM } close $fmt_in; my $new_code = do { <$fmt_out>; <$fmt_out>; <$fmt_out>; # skip 1st 3 lines local $/; <$fmt_out> }; chomp($new_code); unless ($root) { $new_code =~ s/[^\t]+//; $new_code =~ s/\n\}\z//; $new_code =~ s/^\t//gm; } waitpid $pid, 0; if ($? != 0) { die <{name} failed: $code EOD } $new_code =~ s/^(\t+)/" " x length $1/gme; if ($new_code ne $code) { die <{name} is not correctly indented: $code ------------------ should be ------------------ $new_code EOD } return "```go\n$new_code\n```\n"; } sub slurp { local $/; open(my $fh, '<', shift); <$fh> } golang-github-maxatome-go-testdeep-1.14.0/tools/import_spew_bypass.pl000077500000000000000000000020521454313311600260040ustar00rootroot00000000000000#!/usr/bin/env perl # Copyright (c) 2018, Maxime Soulé # All rights reserved. # # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. use strict; use warnings; use autodie; use 5.014; use HTTP::Tiny; my $SPEW_BASE_URL = 'https://raw.githubusercontent.com/davecgh/go-spew/master/spew/'; foreach my $file (qw(bypass.go bypasssafe.go)) { my $resp = HTTP::Tiny::->new->get("$SPEW_BASE_URL$file"); $resp->{success} or die "Failed to retrieve $file!\n"; unless ($resp->{content} =~ s/^package \Kspew$/dark/m) { die "'package spew' line not found in $file!\n"; } open(my $fh, '>', "internal/dark/$file"); say $fh < ' && ', ' ' => ' || '); $resp->{content} =~ s{^(?=// \+build (.*))} {"//go:build " . ($1 =~ s!([ ,])!$ops{$1}!gr) . "\n"}em; print $fh $resp->{content}; close $fh; }