From 2d33165b0fa76cdeeb6aa27b0bdc38a02112f71d Mon Sep 17 00:00:00 2001 From: Frederic BIDON Date: Fri, 3 Oct 2025 10:08:51 +0200 Subject: [PATCH] chore: modernized code base doc: updated readme, renamed EMERITUS into CONTRIBUTORS ci: adopted ci from swag (mono-repo style) lint: relinted packages (internal/spew left with some issues) lint: more refactoring to limit function complexity and code duplication Signed-off-by: Frederic BIDON Signed-off-by: Frederic BIDON --- .ci.gofmt.sh | 14 - .ci.govet.sh | 5 - .github/dependabot.yaml | 9 +- .github/workflows/go-test.yml | 219 +++++--- .golangci.yml | 30 +- EMERITUS.md => CONTRIBUTORS.md | 0 README.md | 62 ++- _codegen/internal/imports/imports.go | 32 +- _codegen/main.go | 81 ++- assert/assertion_compare.go | 464 ++++------------ assert/assertion_compare_test.go | 164 +++--- assert/assertion_order_test.go | 179 +++--- assert/assertions.go | 113 ++-- assert/assertions_test.go | 248 ++++----- assert/examples_test.go | 135 +++++ assert/yaml/yaml_default.go | 4 +- doc.go | 2 +- enable/yaml/assertions_test.go | 27 +- enable/yaml/enable_yaml.go | 2 +- enable/yaml/requirements_test.go | 57 +- .ci.gogenerate.sh => hack/gogenerate.sh | 0 .ci.readme.fmt.sh => hack/readme.fmt.sh | 0 hack/upgrade_modules.sh | 20 + internal/difflib/difflib.go | 524 ++++++++++-------- internal/difflib/difflib_test.go | 39 +- internal/spew/bypass.go | 11 +- internal/spew/bypasssafe.go | 3 +- internal/spew/common.go | 12 +- internal/spew/common_test.go | 32 +- internal/spew/config.go | 71 +-- internal/spew/doc.go | 150 ++--- internal/spew/dump.go | 40 +- internal/spew/dump_test.go | 105 ++-- internal/spew/dumpcgo_test.go | 39 +- internal/spew/dumpnocgo_test.go | 3 +- internal/spew/example_test.go | 8 +- internal/spew/format.go | 19 +- internal/spew/format_test.go | 26 +- internal/spew/internal_test.go | 2 +- internal/spew/internalunsafe_test.go | 6 +- internal/spew/spew.go | 24 +- internal/spew/spew_test.go | 7 +- internal/spew/testsrc/doc.go | 12 + .../spew/{testdata => testsrc}/dumpcgo.go | 13 +- require/examples_test.go | 1 + require/fixtures_test.go | 26 + require/forward_requirements_test.go | 35 +- require/require.go | 10 +- require/require_forward.go | 10 +- require/requirements.go | 2 +- require/requirements_test.go | 48 +- 51 files changed, 1629 insertions(+), 1516 deletions(-) delete mode 100755 .ci.gofmt.sh delete mode 100755 .ci.govet.sh rename EMERITUS.md => CONTRIBUTORS.md (100%) create mode 100644 assert/examples_test.go rename .ci.gogenerate.sh => hack/gogenerate.sh (100%) rename .ci.readme.fmt.sh => hack/readme.fmt.sh (100%) create mode 100755 hack/upgrade_modules.sh create mode 100644 internal/spew/testsrc/doc.go rename internal/spew/{testdata => testsrc}/dumpcgo.go (79%) create mode 100644 require/fixtures_test.go diff --git a/.ci.gofmt.sh b/.ci.gofmt.sh deleted file mode 100755 index 1ac21ef10..000000000 --- a/.ci.gofmt.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env bash - -if [ -n "$(gofmt -l .)" ]; then - echo "Go code is not formatted:" - gofmt -d . - exit 1 -fi - -go generate ./... -if [ -n "$(git status -s -uno)" ]; then - echo "Go generate output does not match commit." - echo "Did you forget to run go generate ./... ?" - exit 1 -fi diff --git a/.ci.govet.sh b/.ci.govet.sh deleted file mode 100755 index 9bdf4519b..000000000 --- a/.ci.govet.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash - -set -e - -go vet ./... diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml index 170c447df..aab9e12fd 100644 --- a/.github/dependabot.yaml +++ b/.github/dependabot.yaml @@ -14,6 +14,8 @@ updates: development-dependencies: patterns: - '*' + assignees: + - fredbi - package-ecosystem: "gomod" # We define 4 groups of dependencies to regroup update pull requests: @@ -30,11 +32,14 @@ updates: # 2. golang.org-dependencies are auto-merged # 3. go-openapi patch updates are auto-merged. Minor/major version updates require a manual merge. # 4. other dependencies require a manual merge - directory: "/" + directories: + - "**/*" schedule: interval: "weekly" day: "friday" open-pull-requests-limit: 4 + allow: + - dependency-type: all groups: development-dependencies: patterns: @@ -53,3 +58,5 @@ updates: - "github.com/go-openapi/*" - "github.com/stretchr/testify" - "golang.org/*" + assignees: + - fredbi diff --git a/.github/workflows/go-test.yml b/.github/workflows/go-test.yml index fa30a4fe6..6e94f71ad 100644 --- a/.github/workflows/go-test.yml +++ b/.github/workflows/go-test.yml @@ -1,5 +1,9 @@ name: go test +permissions: + contents: read + pull-requests: read + on: push: tags: @@ -9,104 +13,59 @@ on: pull_request: -permissions: - pull-requests: read - contents: read - jobs: - module-matrix: - name: Go module matrix - runs-on: ubuntu-latest - - outputs: - modules: ${{ steps.modules.outputs.modules }} - - steps: - - uses: actions/checkout@v5 - - name: Find go modules - id: modules - shell: bash - run: | - # This script finds all go modules declared in this repo and resolves to a relative path to each of those. - # - # NOTES: - # - # > * git bash on a windows runner should support GNU find. find flags should be supported by find on macos. - # > * with go.work file enabled, we may now collect all submodules with go list -m - # > - # > The outcome is currently only used for linting. Tests may now skip that part. - set -euxo pipefail - - root="$(git rev-parse --show-toplevel)" - cd "${root}" - - declare -i index=0 - declare -a all_mods - printf "modules=[" >> "$GITHUB_OUTPUT" - while read module_location ; do - if [ $index -gt 0 ] ; then - printf "," >> "$GITHUB_OUTPUT" - fi - relative_location=${module_location#"$root"/} - module_dir=${relative_location%"/go.mod"} - base_dir="${module_dir#"./"}" - printf " \"${base_dir}\"" >> "$GITHUB_OUTPUT" - all_mods+=("${base_dir}") - ((index++)) || true - done < <(go list -f '{{.Dir}}' -m) - printf "]" >> "$GITHUB_OUTPUT" - - echo "::notice title=Modules found::${all_mods[@]}" - - module-lint: - name: Go module lint + lint: + name: Go lint mono-repo runs-on: ubuntu-latest - needs: [ module-matrix ] - - strategy: - matrix: - # all sub modules in this repo must be linted separately - module: ${{ fromJSON(needs.module-matrix.outputs.modules) }} - steps: - - uses: actions/checkout@v5 - - uses: actions/setup-go@v6 + - + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + with: + fetch-depth: '0' + - + uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 with: go-version: stable check-latest: true cache: true cache-dependency-path: '**/go.sum' - - name: golangci-lint - uses: golangci/golangci-lint-action@v8 + - + name: Install golangci-lint + uses: golangci/golangci-lint-action@0a35821d5c230e903fcfe077583637dea1b27b47 # v9.0.0 with: version: latest - only-new-issues: true skip-cache: true - # golangci-lint doesn't support go.work to lint multiple modules in one single pass - working-directory: '${{ matrix.module }}' - - lint: - needs: [ module-lint ] - name: Lint - runs-on: ubuntu-latest - steps: - - name: Linting complete + install-only: true + - + name: Lint multiple modules + # golangci-lint doesn't support go.work to lint multiple modules in one single pass run: | - echo "All modules linted" + set -euxo pipefail + + git fetch origin master + git show --no-patch --oneline origin/master + + while read module_location ; do + pushd "${module_location}" + golangci-lint run --new-from-rev origin/master + popd + done < <(go list -f '{{.Dir}}' -m) module-test: name: Unit tests runs-on: ${{ matrix.os }} - needs: [ module-matrix ] + needs: [ lint ] strategy: matrix: os: [ ubuntu-latest, macos-latest, windows-latest ] go_version: ['oldstable', 'stable' ] + env: + TEST_REPORT: 'all_modules.report.${{ matrix.os }}.${{ matrix.go_version }}.json' steps: - - uses: actions/checkout@v5 - - uses: actions/setup-go@v6 + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 with: go-version: '${{ matrix.go_version }}' check-latest: true @@ -117,9 +76,9 @@ jobs: shell: bash env: # *.coverage.* pattern is automatically detected by codecov - COVER_PROFILE: 'all_modules.coverage.${{ matrix.os }}.${{ matrix.go_version }}.out' + COVER_PROFILE: 'all_modules.coverage.${{ matrix.os }}.${{ matrix.go_version }}.out' run: | - # when go1.25 becomes the oldstable, we may replace this bash with "go work test" + # when go1.25 becomes the oldstable, we may replace this bash with "go test work" declare -a ALL_MODULES BASH_MAJOR=$(echo $BASH_VERSION|cut -d'.' -f1) if [[ "${BASH_MAJOR}" -ge 4 ]] ; then @@ -134,15 +93,23 @@ jobs: # with go.work file enabled, go test recognizes sub-modules and collects all packages to be covered # without specifying -coverpkg. - go test -v -race -coverprofile="${COVER_PROFILE}" -covermode=atomic ${ALL_MODULES[@]} + go test -race -coverprofile="${COVER_PROFILE}" -covermode=atomic -json ${ALL_MODULES[@]}|tee -a "${TEST_REPORT}" - name: Upload coverage to codecov - uses: codecov/codecov-action@v5 + if: ${{ success() }} # we do this only if all previous steps succeeded + uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1 with: name: Multi modules aggregated coverage flags: '${{ matrix.go_version }}-${{ matrix.os }}' fail_ci_if_error: false - verbose: true + verbose: false + + - name: Upload JSON test Results + if: always() + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + with: + name: 'all_modules.report.${{ matrix.os }}.${{ matrix.go_version }}' + path: ${{ env.TEST_REPORT }} test: needs: [ module-test ] @@ -151,4 +118,94 @@ jobs: steps: - name: Tests complete run: | - echo "All tests completed" + echo "::notice title=Success::All tests completed" + + collect-reports: + if: always() + needs: [ module-test ] + name: Collect and merge test reports + runs-on: ubuntu-latest + steps: + - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 + with: + go-version: stable + check-latest: true + cache: true + + - name: Download all JSON artifacts + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + with: + run-id: "${{ github.run_id }}" + pattern: "all_modules.report.*" + # artifacts resolve as folders + path: reports/ + + - name: Convert test reports to a merged JUnit XML + # NOTE: codecov test reports only support JUnit format at this moment. See https://docs.codecov.com/docs/test-analytics. + # Ideally, codecov improve a bit their platform, so we may only need a single pass to CTRF format. + # + # As a contemplated alternative, we could use gotestsum above to produce the JUnit XML directly. + run: | + go install github.com/jstemmer/go-junit-report/v2@latest + cat reports/*/*.json | go-junit-report -parser gojson -out=reports/junit_report.xml + + - name: Upload test results to Codecov + if: always() + uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1 + with: + files: '**/junit_report.xml' + report_type: 'test_results' + fail_ci_if_error: false + handle_no_reports_found: true + verbose: true + + - name: Convert test reports to CTRF JSON + run: | + go install github.com/ctrf-io/go-ctrf-json-reporter/cmd/go-ctrf-json-reporter@v0.0.10 + + appName="swag" + buildNumber="${{ github.run_id }}" + appVersion="${{ github.event.pull_request.head.sha }}" + + while read report ; do + echo "::notice::converting report: ${report}" + #TEST_REPORT: 'all_modules.report.${{ matrix.os }}.${{ matrix.go_version }}.json' + reformated=$(echo "${report##*/}"|sed -E 's/(go)([[:digit:]]+)\.([[:digit:]]+)/\1\2\3/') # e.g. go1.24 becomes go124 + mapfile -d'.' -t -s 2 -n 2 split < <(echo $reformated) # skip the first 2 parts, stop on 2 more parts + osPlatform="${split[0]}" + osRelease="${split[1]}" + + go-ctrf-json-reporter \ + -verbose \ + -appName "${appName}" \ + -appVersion "${appVersion}" \ + -buildNumber "${buildNumber}" \ + -osPlatform "${osPlatform}" \ + -osRelease "${osRelease}" \ + -output "./reports/ctrf_report_${osPlatform}_${osRelease}.json" \ + -quiet < "${report}" + done < <(find reports -name \*.json) + + # NOTE: at this moment, we don't upload CTRF reports as artifacts. + # Some of the CTRF reports are therefore not available (flaky tests, history, ...). + # + # See https://github.com/ctrf-io/github-test-reporter?tab=readme-ov-file#report-showcase + # for more reporting possibilities. At the moment, we keep it simple, as most advanced features + # require a github token (thus adding the complexity of a separate workflow starting on pull_request_target). + # + # For the moment, we are contented with these simple reports. This is an opportunity to compare the insight they + # provide as compared to what is uploaded to codecov. + # + # Codecov analytics are pretty poor at this moment. On the other hand, they manage the bot that pushes back + # PR comments. + # + # They also handle the storage of past test reports, so as to assess flaky tests. + - name: Publish Test Summary Results + uses: ctrf-io/github-test-reporter@024bc4b64d997ca9da86833c6b9548c55c620e40 # v1.0.26 + with: + report-path: 'reports/ctrf_report_*.json' + use-suite-name: true + summary-report: true # post a report to the github actions summary + github-report: true + failed-folded-report: true + diff --git a/.golangci.yml b/.golangci.yml index 4129e7e57..776b1154c 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -2,36 +2,19 @@ version: "2" linters: default: all disable: - - cyclop - depguard - - errchkjson - - errorlint - - exhaustruct - - forcetypeassert + - err113 - funlen - - gochecknoglobals - - gochecknoinits - - gocognit - - godot - godox - gomoddirectives - - gosmopolitan - - inamedparam - - intrange - - ireturn - - lll - - musttag - - nestif + - exhaustruct - nlreturn - nonamedreturns - noinlineerr - paralleltest - recvcheck - testpackage - - thelper - - tagliatelle - tparallel - - unparam - varnamelen - whitespace - wrapcheck @@ -43,8 +26,15 @@ linters: goconst: min-len: 2 min-occurrences: 3 + cyclop: + max-complexity: 22 gocyclo: - min-complexity: 45 + min-complexity: 22 + exhaustive: + default-signifies-exhaustive: true + default-case-required: true + lll: + line-length: 180 exclusions: generated: lax presets: diff --git a/EMERITUS.md b/CONTRIBUTORS.md similarity index 100% rename from EMERITUS.md rename to CONTRIBUTORS.md diff --git a/README.md b/README.md index a6e7425b5..ebb60a69e 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,23 @@ -# Testify [![Build Status](https://github.com/go-openapi/testify/actions/workflows/go-test.yml/badge.svg)](https://github.com/go-openapi/testify/actions?query=workflow%3A"go+test") [![codecov](https://codecov.io/gh/go-openapi/testify/branch/master/graph/badge.svg)](https://codecov.io/gh/go-openapi/testify) +# Testify [![Slack Status](https://slackin.goswagger.io/badge.svg)](https://slackin.goswagger.io) [![license](https://img.shields.io/badge/license-Apache%20v2-orange.svg)](https://raw.githubusercontent.com/go-openapi/testify/master/LICENSE) [![Go Reference](https://pkg.go.dev/badge/github.com/go-openapi/testify.svg)](https://pkg.go.dev/github.com/go-openapi/testify) [![Go Report Card](https://goreportcard.com/badge/github.com/go-openapi/testify)](https://goreportcard.com/report/github.com/go-openapi/testify) + +[![Tests][test-badge]][test-url] [![Coverage][cov-badge]][cov-url] [![CI vuln scan][vuln-scan-badge]][vuln-scan-url] [![CodeQL][codeql-badge]][codeql-url] + + + +[![Release][release-badge]][release-url] [![Go Report Card][gocard-badge]][gocard-url] [![CodeFactor Grade][codefactor-badge]][codefactor-url] [![License][license-badge]][license-url] + + + +[![GoDoc][godoc-badge]][godoc-url] [![go version][goversion-badge]][goversion-url] ![Top language][top-badge] ![Commits since latest release][commits-badge] + +--- + ## Testify - Thou Shalt Write Tests A Go set of packages that provide tools for testifying that your code will behave as you intend. @@ -48,10 +61,11 @@ However, at `go-openapi` we would like to address the well-known issues in `test ## What's next with this project? 1. [x] The first release comes with zero dependencies and an unstable API (see below [our use case](#usage-at-go-openapi)) -2. This project is going to be injected as the main and sole test dependency of the `go-openapi` libraries and the `go-swagger` tool -3. Valuable pending pull requests from the original project could be merged (e.g. `JSONEqBytes`) or transformed as "enable" modules (e.g. colorized output) -4. Unclear assertions may be provided an alternative verb (e.g. `InDelta`) -5. Since we have leveled the go requirements to the rest of the go-openapi (currently go1.24) there is quite a bit of relinting lying ahead. +2. |x] This project is going to be injected as the main and sole test dependency of the `go-openapi` libraries +2. [ ] ... and the `go-swagger` tool +3. [ ) Valuable pending pull requests from the original project could be merged (e.g. `JSONEqBytes`) or transformed as "enable" modules (e.g. colorized output) +4. [ ] Unclear assertions may be provided an alternative verb (e.g. `InDelta`) +5. [ ] Since we have leveled the go requirements to the rest of the go-openapi (currently go1.24) there is quite a bit of relinting lying ahead. ### What won't come anytime soon @@ -122,10 +136,13 @@ See [the original README](./original.md) ## Licensing +`SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers` + +This library ships under the [SPDX-License-Identifier: Apache-2.0](./LICENSE). + See the license [NOTICE](./NOTICE), which recalls the licensing terms of all the pieces of software distributed with this fork, including internalized libraries. -* go-openapi/testify [SPDX-License-Identifier: Apache-2.0](./LICENSE) * stretchr/testify [SPDX-License-Identifier: MIT](./NOTICE) * github.com/davecgh/go-spew [SPDX-License-Identifier: ISC](./internal/spew/LICENSE) * github.com/pmezard/go-difflib [SPDX-License-Identifier: MIT-like](./internal/difflib/LICENSE) @@ -166,3 +183,36 @@ See also the [CONTRIBUTING guidelines](.github/CONTRIBUTING.md). ## [The original README](./original.md) + + +[test-badge]: https://github.com/go-openapi/testify/actions/workflows/go-test.yml/badge.svg +[test-url]: https://github.com/go-openapi/testify/actions/workflows/go-test.yml +[cov-badge]: https://codecov.io/gh/go-openapi/testify/branch/master/graph/badge.svg +[cov-url]: https://codecov.io/gh/go-openapi/testify +[vuln-scan-badge]: https://github.com/go-openapi/testify/actions/workflows/scanner.yml/badge.svg +[vuln-scan-url]: https://github.com/go-openapi/testify/actions/workflows/scanner.yml +[codeql-badge]: https://github.com/go-openapi/testify/actions/workflows/codeql.yml/badge.svg +[codeql-url]: https://github.com/go-openapi/testify/actions/workflows/codeql.yml + +[release-badge]: https://badge.fury.io/go/github.com%2Fgo-openapi%2Ftestify.svg +[release-url]: https://badge.fury.io/go/github.com%2Fgo-openapi%2Ftestify + +[gocard-badge]: https://goreportcard.com/badge/github.com/go-openapi/testify +[gocard-url]: https://goreportcard.com/report/github.com/go-openapi/testify +[codefactor-badge]: https://img.shields.io/codefactor/grade/github/go-openapi/testify +[codefactor-url]: https://www.codefactor.io/repository/github/go-openapi/testify + +[doc-badge]: https://img.shields.io/badge/doc-site-blue?link=https%3A%2F%2Fgoswagger.io%2Fgo-openapi%2F +[doc-url]: https://goswagger.io/go-openapi +[godoc-badge]: https://pkg.go.dev/badge/github.com/go-openapi/testify +[godoc-url]: http://pkg.go.dev/github.com/go-openapi/testify +[slack-badge]: https://slackin.goswagger.io/badge.svg +[slack-url]: https://slackin.goswagger.io + +[license-badge]: http://img.shields.io/badge/license-Apache%20v2-orange.svg +[license-url]: https://github.com/go-openapi/testify/?tab=Apache-2.0-1-ov-file#readme + +[goversion-badge]: https://img.shields.io/github/go-mod/go-version/go-openapi/testify +[goversion-url]: https://github.com/go-openapi/testify/blob/master/go.mod +[top-badge]: https://img.shields.io/github/languages/top/go-openapi/testify +[commits-badge]: https://img.shields.io/github/commits-since/go-openapi/testify/latest diff --git a/_codegen/internal/imports/imports.go b/_codegen/internal/imports/imports.go index a0c7e2b6a..264404742 100644 --- a/_codegen/internal/imports/imports.go +++ b/_codegen/internal/imports/imports.go @@ -36,13 +36,17 @@ type Importer interface { Imports() map[string]string } -// imports contains metadata about all the imports from a given package +type Imports struct { + *imports +} + +// imports contains metadata about all the imports from a given package. type imports struct { currentpkg string imp map[string]string } -// AddImportsFrom adds imports used in the passed type +// AddImportsFrom adds imports used in the passed type. func (imp *imports) AddImportsFrom(t types.Type) { switch el := t.(type) { case *types.Basic: @@ -60,8 +64,8 @@ func (imp *imports) AddImportsFrom(t types.Type) { } imp.imp[cleanImportPath(pkg.Path())] = pkg.Name() case *types.Tuple: - for i := 0; i < el.Len(); i++ { - imp.AddImportsFrom(el.At(i).Type()) + for v := range el.Variables() { + imp.AddImportsFrom(v.Type()) } default: } @@ -74,15 +78,15 @@ func cleanImportPath(ipath string) string { } func gopathlessImportPath(ipath string) string { - paths := strings.Split(os.Getenv("GOPATH"), ":") - for _, p := range paths { + paths := strings.SplitSeq(os.Getenv("GOPATH"), ":") + for p := range paths { ipath = strings.TrimPrefix(ipath, filepath.Join(p, "src")+string(filepath.Separator)) } return ipath } // vendorlessImportPath returns the devendorized version of the provided import path. -// e.g. "foo/bar/vendor/a/b" => "a/b" +// e.g. "foo/bar/vendor/a/b" => "a/b". func vendorlessImportPath(ipath string) string { // Devendorize for use in import statement. if i := strings.LastIndex(ipath, "/vendor/"); i >= 0 { @@ -94,15 +98,17 @@ func vendorlessImportPath(ipath string) string { return ipath } -// AddImportsFrom adds imports used in the passed type +// Imports adds imports used in the passed type. func (imp *imports) Imports() map[string]string { return imp.imp } -// New initializes a new structure to track packages imported by the currentpkg -func New(currentpkg string) Importer { - return &imports{ - currentpkg: currentpkg, - imp: make(map[string]string), +// New initializes a new structure to track packages imported by the currentpkg. +func New(currentpkg string) *Imports { + return &Imports{ + imports: &imports{ + currentpkg: currentpkg, + imp: make(map[string]string), + }, } } diff --git a/_codegen/main.go b/_codegen/main.go index 98b2a1bc0..434acdb8d 100644 --- a/_codegen/main.go +++ b/_codegen/main.go @@ -26,6 +26,7 @@ import ( "github.com/go-openapi/testify/v2/_codegen/internal/imports" ) +//nolint:gochecknoglobals // ok to register flags as globals var ( pkg = flag.String("assert-path", "github.com/go-openapi/testify/v2/assert", "Path to the assert package") includeF = flag.Bool("include-format-funcs", false, "include format functions such as Errorf and Equalf") @@ -73,7 +74,7 @@ func generateCode(importer imports.Importer, funcs []testFunc) error { // Generate funcs for _, fn := range funcs { - buff.Write([]byte("\n\n")) + buff.WriteString("\n\n") if err := tmplFunc.Execute(buff, &fn); err != nil { return err } @@ -99,19 +100,24 @@ func parseTemplates() (*template.Template, *template.Template, error) { if err != nil { return nil, nil, err } + var funcTemplate string if *tmplFile != "" { f, err := os.ReadFile(*tmplFile) if err != nil { return nil, nil, err } funcTemplate = string(f) + } else { + funcTemplate = defaultTemplate } + tmpl, err := template.New("function").Funcs(template.FuncMap{ "replace": strings.ReplaceAll, }).Parse(funcTemplate) if err != nil { return nil, nil, err } + return tmplHead, tmpl, nil } @@ -128,11 +134,15 @@ func outputFile() (*os.File, error) { // analyzeCode takes the types scope and the docs and returns the import // information and information about all the assertion functions. -func analyzeCode(scope *types.Scope, docs *doc.Package) (imports.Importer, []testFunc, error) { - testingT := scope.Lookup("TestingT").Type().Underlying().(*types.Interface) +func analyzeCode(scope *types.Scope, docs *doc.Package) (imports.Importer, []testFunc, error) { //nolint:ireturn // ok to use what stdlib does here + underlying := scope.Lookup("TestingT").Type().Underlying() + testingT, ok := underlying.(*types.Interface) + if !ok { + panic(fmt.Errorf("internal error: expected go type to resolve as *types.Interface but got: %T", underlying)) + } importer := imports.New(*outputPkg) - var funcs []testFunc + funcs := make([]testFunc, 0, len(docs.Funcs)) // Go through all the top level functions for _, fdocs := range docs.Funcs { // Find the function @@ -142,9 +152,15 @@ func analyzeCode(scope *types.Scope, docs *doc.Package) (imports.Importer, []tes if !ok { continue } + // Check function signature has at least two arguments - sig := fn.Type().(*types.Signature) - if sig.Params().Len() < 2 { + sig, ok := fn.Type().(*types.Signature) + if !ok { + return nil, nil, fmt.Errorf("internal error: expected go type to resolve as *types.Signature but got: %T", sig) + } + + const minParams = 2 + if sig.Params().Len() < minParams { continue } // Check first argument is of type testingT @@ -152,6 +168,7 @@ func analyzeCode(scope *types.Scope, docs *doc.Package) (imports.Importer, []tes if !ok { continue } + firstType, ok := first.Underlying().(*types.Interface) if !ok { continue @@ -168,10 +185,11 @@ func analyzeCode(scope *types.Scope, docs *doc.Package) (imports.Importer, []tes funcs = append(funcs, testFunc{*outputPkg, fdocs, fn}) importer.AddImportsFrom(sig.Params()) } + return importer, funcs, nil } -// parsePackageSource returns the types scope and the package documentation from the package +// parsePackageSource returns the types scope and the package documentation from the package. func parsePackageSource(pkg string) (*types.Scope, *doc.Package, error) { pd, err := build.Import(pkg, ".", 0) if err != nil { @@ -195,7 +213,7 @@ func parsePackageSource(pkg string) (*types.Scope, *doc.Package, error) { } cfg := types.Config{ - Importer: importer.For("source", nil), + Importer: importer.ForCompiler(token.NewFileSet(), "source", nil), } info := types.Info{ Defs: make(map[*ast.Ident]types.Object), @@ -207,7 +225,7 @@ func parsePackageSource(pkg string) (*types.Scope, *doc.Package, error) { scope := tp.Scope() - ap, _ := ast.NewPackage(fset, files, nil, nil) + ap, _ := ast.NewPackage(fset, files, nil, nil) //nolint:staticcheck // will need more work to upgrade docs := doc.New(ap, pkg, 0) return scope, docs, nil @@ -227,9 +245,12 @@ func (f *testFunc) Qualifier(p *types.Package) string { } func (f *testFunc) Params() string { - sig := f.TypeInfo.Type().(*types.Signature) + sig, ok := f.TypeInfo.Type().(*types.Signature) + if !ok { + panic(fmt.Errorf("internal error: expected go type to resolve as *types.Interface but got: %T", f.TypeInfo.Type())) + } params := sig.Params() - p := "" + var p strings.Builder comma := "" to := params.Len() var i int @@ -239,20 +260,28 @@ func (f *testFunc) Params() string { } for i = 1; i < to; i++ { param := params.At(i) - p += fmt.Sprintf("%s%s %s", comma, param.Name(), types.TypeString(param.Type(), f.Qualifier)) + p.WriteString(fmt.Sprintf("%s%s %s", comma, param.Name(), types.TypeString(param.Type(), f.Qualifier))) comma = ", " } if sig.Variadic() { param := params.At(params.Len() - 1) - p += fmt.Sprintf("%s%s ...%s", comma, param.Name(), types.TypeString(param.Type().(*types.Slice).Elem(), f.Qualifier)) + slice, ok := param.Type().(*types.Slice) + if !ok { + panic(fmt.Errorf("internal error: expected go type to resolve as *types.Slice but got: %T", param.Type())) + } + p.WriteString(fmt.Sprintf("%s%s ...%s", comma, param.Name(), types.TypeString(slice.Elem(), f.Qualifier))) } - return p + + return p.String() } func (f *testFunc) ForwardedParams() string { - sig := f.TypeInfo.Type().(*types.Signature) + sig, ok := f.TypeInfo.Type().(*types.Signature) + if !ok { + panic(fmt.Errorf("internal error: expected go type to resolve as *types.Signature but got: %T", sig)) + } params := sig.Params() - p := "" + var p strings.Builder comma := "" to := params.Len() var i int @@ -262,14 +291,14 @@ func (f *testFunc) ForwardedParams() string { } for i = 1; i < to; i++ { param := params.At(i) - p += fmt.Sprintf("%s%s", comma, param.Name()) + p.WriteString(fmt.Sprintf("%s%s", comma, param.Name())) comma = ", " } if sig.Variadic() { param := params.At(params.Len() - 1) - p += fmt.Sprintf("%s%s...", comma, param.Name()) + p.WriteString(fmt.Sprintf("%s%s...", comma, param.Name())) } - return p + return p.String() } func (f *testFunc) ParamsFormat() string { @@ -281,13 +310,13 @@ func (f *testFunc) ForwardedParamsFormat() string { } func (f *testFunc) Comment() string { - return "// " + strings.Replace(strings.TrimSpace(f.DocInfo.Doc), "\n", "\n// ", -1) + return "// " + strings.ReplaceAll(strings.TrimSpace(f.DocInfo.Doc), "\n", "\n// ") } func (f *testFunc) CommentFormat() string { - search := fmt.Sprintf("%s", f.DocInfo.Name) - replace := fmt.Sprintf("%sf", f.DocInfo.Name) - comment := strings.Replace(f.Comment(), search, replace, -1) + search := f.DocInfo.Name + replace := f.DocInfo.Name + "f" + comment := strings.ReplaceAll(f.Comment(), search, replace) exp := regexp.MustCompile(replace + `\(((\(\)|[^\n])+)\)`) return exp.ReplaceAllString(comment, replace+`($1, "error message %s", "formatted")`) } @@ -295,11 +324,11 @@ func (f *testFunc) CommentFormat() string { func (f *testFunc) CommentWithoutT(receiver string) string { search := fmt.Sprintf("assert.%s(t, ", f.DocInfo.Name) replace := fmt.Sprintf("%s.%s(", receiver, f.DocInfo.Name) - return strings.Replace(f.Comment(), search, replace, -1) + return strings.ReplaceAll(f.Comment(), search, replace) } // Standard header https://go.dev/s/generatedcode. -var headerTemplate = `// Code generated with github.com/go-openapi/testify/v2/_codegen; DO NOT EDIT. +const headerTemplate = `// Code generated with github.com/go-openapi/testify/v2/_codegen; DO NOT EDIT. package {{.Name}} @@ -309,7 +338,7 @@ import ( ) ` -var funcTemplate = `{{.Comment}} +const defaultTemplate = `{{.Comment}} func (fwd *AssertionsForwarder) {{.DocInfo.Name}}({{.Params}}) bool { return assert.{{.DocInfo.Name}}({{.ForwardedParams}}) }` diff --git a/assert/assertion_compare.go b/assert/assertion_compare.go index 4be942843..7cf84962a 100644 --- a/assert/assertion_compare.go +++ b/assert/assertion_compare.go @@ -2,8 +2,10 @@ package assert import ( "bytes" + "cmp" "fmt" "reflect" + "slices" "time" ) @@ -18,367 +20,149 @@ const ( compareGreater ) -var ( - intType = reflect.TypeOf(int(1)) - int8Type = reflect.TypeOf(int8(1)) - int16Type = reflect.TypeOf(int16(1)) - int32Type = reflect.TypeOf(int32(1)) - int64Type = reflect.TypeOf(int64(1)) - - uintType = reflect.TypeOf(uint(1)) - uint8Type = reflect.TypeOf(uint8(1)) - uint16Type = reflect.TypeOf(uint16(1)) - uint32Type = reflect.TypeOf(uint32(1)) - uint64Type = reflect.TypeOf(uint64(1)) - - uintptrType = reflect.TypeOf(uintptr(1)) - - float32Type = reflect.TypeOf(float32(1)) - float64Type = reflect.TypeOf(float64(1)) - - stringType = reflect.TypeOf("") - - timeType = reflect.TypeOf(time.Time{}) - bytesType = reflect.TypeOf([]byte{}) -) - func compare(obj1, obj2 any, kind reflect.Kind) (compareResult, bool) { obj1Value := reflect.ValueOf(obj1) obj2Value := reflect.ValueOf(obj2) - // throughout this switch we try and avoid calling .Convert() if possible, - // as this has a pretty big performance impact switch kind { case reflect.Int: - { - intobj1, ok := obj1.(int) - if !ok { - intobj1 = obj1Value.Convert(intType).Interface().(int) - } - intobj2, ok := obj2.(int) - if !ok { - intobj2 = obj2Value.Convert(intType).Interface().(int) - } - if intobj1 > intobj2 { - return compareGreater, true - } - if intobj1 == intobj2 { - return compareEqual, true - } - if intobj1 < intobj2 { - return compareLess, true - } - } + intobj1 := convertReflectValue[int](obj1, obj1Value) + intobj2 := convertReflectValue[int](obj2, obj2Value) + + return compareOrdered(intobj1, intobj2) case reflect.Int8: - { - int8obj1, ok := obj1.(int8) - if !ok { - int8obj1 = obj1Value.Convert(int8Type).Interface().(int8) - } - int8obj2, ok := obj2.(int8) - if !ok { - int8obj2 = obj2Value.Convert(int8Type).Interface().(int8) - } - if int8obj1 > int8obj2 { - return compareGreater, true - } - if int8obj1 == int8obj2 { - return compareEqual, true - } - if int8obj1 < int8obj2 { - return compareLess, true - } - } + int8obj1 := convertReflectValue[int8](obj1, obj1Value) + int8obj2 := convertReflectValue[int8](obj2, obj2Value) + + return compareOrdered(int8obj1, int8obj2) case reflect.Int16: - { - int16obj1, ok := obj1.(int16) - if !ok { - int16obj1 = obj1Value.Convert(int16Type).Interface().(int16) - } - int16obj2, ok := obj2.(int16) - if !ok { - int16obj2 = obj2Value.Convert(int16Type).Interface().(int16) - } - if int16obj1 > int16obj2 { - return compareGreater, true - } - if int16obj1 == int16obj2 { - return compareEqual, true - } - if int16obj1 < int16obj2 { - return compareLess, true - } - } + int16obj1 := convertReflectValue[int16](obj1, obj1Value) + int16obj2 := convertReflectValue[int16](obj2, obj2Value) + + return compareOrdered(int16obj1, int16obj2) case reflect.Int32: - { - int32obj1, ok := obj1.(int32) - if !ok { - int32obj1 = obj1Value.Convert(int32Type).Interface().(int32) - } - int32obj2, ok := obj2.(int32) - if !ok { - int32obj2 = obj2Value.Convert(int32Type).Interface().(int32) - } - if int32obj1 > int32obj2 { - return compareGreater, true - } - if int32obj1 == int32obj2 { - return compareEqual, true - } - if int32obj1 < int32obj2 { - return compareLess, true - } - } + int32obj1 := convertReflectValue[int32](obj1, obj1Value) + int32obj2 := convertReflectValue[int32](obj2, obj2Value) + + return compareOrdered(int32obj1, int32obj2) case reflect.Int64: - { - int64obj1, ok := obj1.(int64) - if !ok { - int64obj1 = obj1Value.Convert(int64Type).Interface().(int64) - } - int64obj2, ok := obj2.(int64) - if !ok { - int64obj2 = obj2Value.Convert(int64Type).Interface().(int64) - } - if int64obj1 > int64obj2 { - return compareGreater, true - } - if int64obj1 == int64obj2 { - return compareEqual, true - } - if int64obj1 < int64obj2 { - return compareLess, true - } - } + int64obj1 := convertReflectValue[int64](obj1, obj1Value) + int64obj2 := convertReflectValue[int64](obj2, obj2Value) + + return compareOrdered(int64obj1, int64obj2) case reflect.Uint: - { - uintobj1, ok := obj1.(uint) - if !ok { - uintobj1 = obj1Value.Convert(uintType).Interface().(uint) - } - uintobj2, ok := obj2.(uint) - if !ok { - uintobj2 = obj2Value.Convert(uintType).Interface().(uint) - } - if uintobj1 > uintobj2 { - return compareGreater, true - } - if uintobj1 == uintobj2 { - return compareEqual, true - } - if uintobj1 < uintobj2 { - return compareLess, true - } - } + uintobj1 := convertReflectValue[uint](obj1, obj1Value) + uintobj2 := convertReflectValue[uint](obj2, obj2Value) + + return compareOrdered(uintobj1, uintobj2) case reflect.Uint8: - { - uint8obj1, ok := obj1.(uint8) - if !ok { - uint8obj1 = obj1Value.Convert(uint8Type).Interface().(uint8) - } - uint8obj2, ok := obj2.(uint8) - if !ok { - uint8obj2 = obj2Value.Convert(uint8Type).Interface().(uint8) - } - if uint8obj1 > uint8obj2 { - return compareGreater, true - } - if uint8obj1 == uint8obj2 { - return compareEqual, true - } - if uint8obj1 < uint8obj2 { - return compareLess, true - } - } + uint8obj1 := convertReflectValue[uint8](obj1, obj1Value) + uint8obj2 := convertReflectValue[uint8](obj2, obj2Value) + + return compareOrdered(uint8obj1, uint8obj2) case reflect.Uint16: - { - uint16obj1, ok := obj1.(uint16) - if !ok { - uint16obj1 = obj1Value.Convert(uint16Type).Interface().(uint16) - } - uint16obj2, ok := obj2.(uint16) - if !ok { - uint16obj2 = obj2Value.Convert(uint16Type).Interface().(uint16) - } - if uint16obj1 > uint16obj2 { - return compareGreater, true - } - if uint16obj1 == uint16obj2 { - return compareEqual, true - } - if uint16obj1 < uint16obj2 { - return compareLess, true - } - } + uint16obj1 := convertReflectValue[uint16](obj1, obj1Value) + uint16obj2 := convertReflectValue[uint16](obj2, obj2Value) + + return compareOrdered(uint16obj1, uint16obj2) case reflect.Uint32: - { - uint32obj1, ok := obj1.(uint32) - if !ok { - uint32obj1 = obj1Value.Convert(uint32Type).Interface().(uint32) - } - uint32obj2, ok := obj2.(uint32) - if !ok { - uint32obj2 = obj2Value.Convert(uint32Type).Interface().(uint32) - } - if uint32obj1 > uint32obj2 { - return compareGreater, true - } - if uint32obj1 == uint32obj2 { - return compareEqual, true - } - if uint32obj1 < uint32obj2 { - return compareLess, true - } - } + uint32obj1 := convertReflectValue[uint32](obj1, obj1Value) + uint32obj2 := convertReflectValue[uint32](obj2, obj2Value) + + return compareOrdered(uint32obj1, uint32obj2) case reflect.Uint64: - { - uint64obj1, ok := obj1.(uint64) - if !ok { - uint64obj1 = obj1Value.Convert(uint64Type).Interface().(uint64) - } - uint64obj2, ok := obj2.(uint64) - if !ok { - uint64obj2 = obj2Value.Convert(uint64Type).Interface().(uint64) - } - if uint64obj1 > uint64obj2 { - return compareGreater, true - } - if uint64obj1 == uint64obj2 { - return compareEqual, true - } - if uint64obj1 < uint64obj2 { - return compareLess, true - } - } + uint64obj1 := convertReflectValue[uint64](obj1, obj1Value) + uint64obj2 := convertReflectValue[uint64](obj2, obj2Value) + + return compareOrdered(uint64obj1, uint64obj2) case reflect.Float32: - { - float32obj1, ok := obj1.(float32) - if !ok { - float32obj1 = obj1Value.Convert(float32Type).Interface().(float32) - } - float32obj2, ok := obj2.(float32) - if !ok { - float32obj2 = obj2Value.Convert(float32Type).Interface().(float32) - } - if float32obj1 > float32obj2 { - return compareGreater, true - } - if float32obj1 == float32obj2 { - return compareEqual, true - } - if float32obj1 < float32obj2 { - return compareLess, true - } - } + float32obj1 := convertReflectValue[float32](obj1, obj1Value) + float32obj2 := convertReflectValue[float32](obj2, obj2Value) + + return compareOrdered(float32obj1, float32obj2) case reflect.Float64: - { - float64obj1, ok := obj1.(float64) - if !ok { - float64obj1 = obj1Value.Convert(float64Type).Interface().(float64) - } - float64obj2, ok := obj2.(float64) - if !ok { - float64obj2 = obj2Value.Convert(float64Type).Interface().(float64) - } - if float64obj1 > float64obj2 { - return compareGreater, true - } - if float64obj1 == float64obj2 { - return compareEqual, true - } - if float64obj1 < float64obj2 { - return compareLess, true - } - } + float64obj1 := convertReflectValue[float64](obj1, obj1Value) + float64obj2 := convertReflectValue[float64](obj2, obj2Value) + + return compareOrdered(float64obj1, float64obj2) case reflect.String: - { - stringobj1, ok := obj1.(string) - if !ok { - stringobj1 = obj1Value.Convert(stringType).Interface().(string) - } - stringobj2, ok := obj2.(string) - if !ok { - stringobj2 = obj2Value.Convert(stringType).Interface().(string) - } - if stringobj1 > stringobj2 { - return compareGreater, true - } - if stringobj1 == stringobj2 { - return compareEqual, true - } - if stringobj1 < stringobj2 { - return compareLess, true - } - } + stringobj1 := convertReflectValue[string](obj1, obj1Value) + stringobj2 := convertReflectValue[string](obj2, obj2Value) + + return compareOrdered(stringobj1, stringobj2) + // Check for known struct types we can check for compare results. case reflect.Struct: - { - // All structs enter here. We're not interested in most types. - if !obj1Value.CanConvert(timeType) { - break - } - - // time.Time can be compared! - timeObj1, ok := obj1.(time.Time) - if !ok { - timeObj1 = obj1Value.Convert(timeType).Interface().(time.Time) - } - - timeObj2, ok := obj2.(time.Time) - if !ok { - timeObj2 = obj2Value.Convert(timeType).Interface().(time.Time) - } - - if timeObj1.Before(timeObj2) { - return compareLess, true - } - if timeObj1.Equal(timeObj2) { - return compareEqual, true - } - return compareGreater, true - } + return compareStruct(obj1, obj2, obj1Value, obj2Value) case reflect.Slice: - { - // We only care about the []byte type. - if !obj1Value.CanConvert(bytesType) { - break - } - - // []byte can be compared! - bytesObj1, ok := obj1.([]byte) - if !ok { - bytesObj1 = obj1Value.Convert(bytesType).Interface().([]byte) - - } - bytesObj2, ok := obj2.([]byte) - if !ok { - bytesObj2 = obj2Value.Convert(bytesType).Interface().([]byte) - } - - return compareResult(bytes.Compare(bytesObj1, bytesObj2)), true - } + return compareSlice(obj1, obj2, obj1Value, obj2Value) case reflect.Uintptr: - { - uintptrObj1, ok := obj1.(uintptr) - if !ok { - uintptrObj1 = obj1Value.Convert(uintptrType).Interface().(uintptr) - } - uintptrObj2, ok := obj2.(uintptr) - if !ok { - uintptrObj2 = obj2Value.Convert(uintptrType).Interface().(uintptr) - } - if uintptrObj1 > uintptrObj2 { - return compareGreater, true - } - if uintptrObj1 == uintptrObj2 { - return compareEqual, true - } - if uintptrObj1 < uintptrObj2 { - return compareLess, true - } + uintptrobj1 := convertReflectValue[string](obj1, obj1Value) + uintptrobj2 := convertReflectValue[string](obj2, obj2Value) + + return compareOrdered(uintptrobj1, uintptrobj2) + default: + return compareEqual, false + } +} + +func compareOrdered[T cmp.Ordered](obj1, obj2 T) (compareResult, bool) { + return compareResult(cmp.Compare(obj1, obj2)), true +} + +func compareStruct(obj1, obj2 any, obj1Value, obj2Value reflect.Value) (compareResult, bool) { + // all structs enter here. We're not interested in most types. + if !obj1Value.CanConvert(reflect.TypeFor[time.Time]()) { + return compareEqual, false + } + + // time.Time can be compared + timeobj1 := convertReflectValue[time.Time](obj1, obj1Value) + timeobj2 := convertReflectValue[time.Time](obj2, obj2Value) + + return compareTime(timeobj1, timeobj2) +} + +func compareSlice(obj1, obj2 any, obj1Value, obj2Value reflect.Value) (compareResult, bool) { + // we only care about the []byte type. + if !obj1Value.CanConvert(reflect.TypeFor[[]byte]()) { + return compareEqual, false + } + + // []byte can be compared + bytesobj1 := convertReflectValue[[]byte](obj1, obj1Value) + bytesobj2 := convertReflectValue[[]byte](obj2, obj2Value) + + return compareBytes(bytesobj1, bytesobj2) +} + +func compareTime(obj1, obj2 time.Time) (compareResult, bool) { + switch { + case obj1.Before(obj2): + return compareLess, true + case obj1.Equal(obj2): + return compareEqual, true + default: + return compareGreater, true + } +} + +func compareBytes(obj1, obj2 []byte) (compareResult, bool) { + return compareResult(bytes.Compare(obj1, obj2)), true +} + +func convertReflectValue[T any](obj any, value reflect.Value) T { //nolint:ireturn // false positive + // we try and avoid calling [reflect.Value.Convert()] whenever possible, + // as this has a pretty big performance impact + converted, ok := obj.(T) + if !ok { + converted, ok = value.Convert(reflect.TypeFor[T]()).Interface().(T) + if !ok { + panic("internal error: expected that reflect.Value.Convert yields its target type") } } - return compareEqual, false + return converted } // Greater asserts that the first element is greater than the second @@ -485,11 +269,5 @@ func compareTwoValues(t TestingT, e1 any, e2 any, allowedComparesResults []compa } func containsValue(values []compareResult, value compareResult) bool { - for _, v := range values { - if v == value { - return true - } - } - - return false + return slices.Contains(values, value) } diff --git a/assert/assertion_compare_test.go b/assert/assertion_compare_test.go index 802ef2158..4ab50d6c2 100644 --- a/assert/assertion_compare_test.go +++ b/assert/assertion_compare_test.go @@ -3,8 +3,11 @@ package assert import ( "bytes" "fmt" + "iter" "reflect" "runtime" + "slices" + "strings" "testing" "time" ) @@ -61,7 +64,12 @@ func TestCompare(t *testing.T) { {less: uintptr(1), greater: uintptr(2), cType: "uintptr"}, {less: customUintptr(1), greater: customUintptr(2), cType: "uint64"}, {less: time.Now(), greater: time.Now().Add(time.Hour), cType: "time.Time"}, - {less: time.Date(2024, 0, 0, 0, 0, 0, 0, time.Local), greater: time.Date(2263, 0, 0, 0, 0, 0, 0, time.Local), cType: "time.Time"}, + { + // using time.Local is ok in this context: this is precisely the goal of this test + less: time.Date(2024, 0, 0, 0, 0, 0, 0, time.Local), //nolint:gosmopolitan // ok. See above + greater: time.Date(2263, 0, 0, 0, 0, 0, 0, time.Local), //nolint:gosmopolitan // ok. See above + cType: "time.Time", + }, {less: customTime(time.Now()), greater: customTime(time.Now().Add(time.Hour)), cType: "time.Time"}, {less: []byte{1, 1}, greater: []byte{1, 2}, cType: "[]byte"}, {less: customBytes([]byte{1, 1}), greater: customBytes([]byte{1, 2}), cType: "[]byte"}, @@ -101,7 +109,7 @@ type outputT struct { helpers map[string]struct{} } -// Implements TestingT +// Implements TestingT. func (t *outputT) Errorf(format string, args ...any) { s := fmt.Sprintf(format, args...) t.buf.WriteString(s) @@ -128,6 +136,12 @@ func callerName(skip int) string { return frame.Function } +type compareFixture struct { + less any + greater any + msg string +} + func TestGreater(t *testing.T) { t.Parallel() @@ -145,28 +159,8 @@ func TestGreater(t *testing.T) { t.Error("Greater should return false") } - // Check error report - for _, currCase := range []struct { - less any - greater any - msg string - }{ - {less: "a", greater: "b", msg: `"a" is not greater than "b"`}, - {less: int(1), greater: int(2), msg: `"1" is not greater than "2"`}, - {less: int8(1), greater: int8(2), msg: `"1" is not greater than "2"`}, - {less: int16(1), greater: int16(2), msg: `"1" is not greater than "2"`}, - {less: int32(1), greater: int32(2), msg: `"1" is not greater than "2"`}, - {less: int64(1), greater: int64(2), msg: `"1" is not greater than "2"`}, - {less: uint8(1), greater: uint8(2), msg: `"1" is not greater than "2"`}, - {less: uint16(1), greater: uint16(2), msg: `"1" is not greater than "2"`}, - {less: uint32(1), greater: uint32(2), msg: `"1" is not greater than "2"`}, - {less: uint64(1), greater: uint64(2), msg: `"1" is not greater than "2"`}, - {less: float32(1.23), greater: float32(2.34), msg: `"1.23" is not greater than "2.34"`}, - {less: float64(1.23), greater: float64(2.34), msg: `"1.23" is not greater than "2.34"`}, - {less: uintptr(1), greater: uintptr(2), msg: `"1" is not greater than "2"`}, - {less: time.Time{}, greater: time.Time{}.Add(time.Hour), msg: `"0001-01-01 00:00:00 +0000 UTC" is not greater than "0001-01-01 01:00:00 +0000 UTC"`}, - {less: []byte{1, 1}, greater: []byte{1, 2}, msg: `"[1 1]" is not greater than "[1 2]"`}, - } { + // check error report + for currCase := range compareIncreasingFixtures() { out := &outputT{buf: bytes.NewBuffer(nil)} False(t, Greater(out, currCase.less, currCase.greater)) Contains(t, out.buf.String(), currCase.msg) @@ -191,31 +185,11 @@ func TestGreaterOrEqual(t *testing.T) { t.Error("GreaterOrEqual should return false") } - // Check error report - for _, currCase := range []struct { - less any - greater any - msg string - }{ - {less: "a", greater: "b", msg: `"a" is not greater than or equal to "b"`}, - {less: int(1), greater: int(2), msg: `"1" is not greater than or equal to "2"`}, - {less: int8(1), greater: int8(2), msg: `"1" is not greater than or equal to "2"`}, - {less: int16(1), greater: int16(2), msg: `"1" is not greater than or equal to "2"`}, - {less: int32(1), greater: int32(2), msg: `"1" is not greater than or equal to "2"`}, - {less: int64(1), greater: int64(2), msg: `"1" is not greater than or equal to "2"`}, - {less: uint8(1), greater: uint8(2), msg: `"1" is not greater than or equal to "2"`}, - {less: uint16(1), greater: uint16(2), msg: `"1" is not greater than or equal to "2"`}, - {less: uint32(1), greater: uint32(2), msg: `"1" is not greater than or equal to "2"`}, - {less: uint64(1), greater: uint64(2), msg: `"1" is not greater than or equal to "2"`}, - {less: float32(1.23), greater: float32(2.34), msg: `"1.23" is not greater than or equal to "2.34"`}, - {less: float64(1.23), greater: float64(2.34), msg: `"1.23" is not greater than or equal to "2.34"`}, - {less: uintptr(1), greater: uintptr(2), msg: `"1" is not greater than or equal to "2"`}, - {less: time.Time{}, greater: time.Time{}.Add(time.Hour), msg: `"0001-01-01 00:00:00 +0000 UTC" is not greater than or equal to "0001-01-01 01:00:00 +0000 UTC"`}, - {less: []byte{1, 1}, greater: []byte{1, 2}, msg: `"[1 1]" is not greater than or equal to "[1 2]"`}, - } { + // check error report + for currCase := range compareIncreasingFixtures() { out := &outputT{buf: bytes.NewBuffer(nil)} False(t, GreaterOrEqual(out, currCase.less, currCase.greater)) - Contains(t, out.buf.String(), currCase.msg) + Contains(t, out.buf.String(), strings.ReplaceAll(currCase.msg, "than", "than or equal to")) Contains(t, out.helpers, "github.com/go-openapi/testify/v2/assert.GreaterOrEqual") } } @@ -237,28 +211,8 @@ func TestLess(t *testing.T) { t.Error("Less should return false") } - // Check error report - for _, currCase := range []struct { - less any - greater any - msg string - }{ - {less: "a", greater: "b", msg: `"b" is not less than "a"`}, - {less: int(1), greater: int(2), msg: `"2" is not less than "1"`}, - {less: int8(1), greater: int8(2), msg: `"2" is not less than "1"`}, - {less: int16(1), greater: int16(2), msg: `"2" is not less than "1"`}, - {less: int32(1), greater: int32(2), msg: `"2" is not less than "1"`}, - {less: int64(1), greater: int64(2), msg: `"2" is not less than "1"`}, - {less: uint8(1), greater: uint8(2), msg: `"2" is not less than "1"`}, - {less: uint16(1), greater: uint16(2), msg: `"2" is not less than "1"`}, - {less: uint32(1), greater: uint32(2), msg: `"2" is not less than "1"`}, - {less: uint64(1), greater: uint64(2), msg: `"2" is not less than "1"`}, - {less: float32(1.23), greater: float32(2.34), msg: `"2.34" is not less than "1.23"`}, - {less: float64(1.23), greater: float64(2.34), msg: `"2.34" is not less than "1.23"`}, - {less: uintptr(1), greater: uintptr(2), msg: `"2" is not less than "1"`}, - {less: time.Time{}, greater: time.Time{}.Add(time.Hour), msg: `"0001-01-01 01:00:00 +0000 UTC" is not less than "0001-01-01 00:00:00 +0000 UTC"`}, - {less: []byte{1, 1}, greater: []byte{1, 2}, msg: `"[1 2]" is not less than "[1 1]"`}, - } { + // check error report + for currCase := range compareIncreasingFixtures3() { out := &outputT{buf: bytes.NewBuffer(nil)} False(t, Less(out, currCase.greater, currCase.less)) Contains(t, out.buf.String(), currCase.msg) @@ -283,31 +237,11 @@ func TestLessOrEqual(t *testing.T) { t.Error("LessOrEqual should return false") } - // Check error report - for _, currCase := range []struct { - less any - greater any - msg string - }{ - {less: "a", greater: "b", msg: `"b" is not less than or equal to "a"`}, - {less: int(1), greater: int(2), msg: `"2" is not less than or equal to "1"`}, - {less: int8(1), greater: int8(2), msg: `"2" is not less than or equal to "1"`}, - {less: int16(1), greater: int16(2), msg: `"2" is not less than or equal to "1"`}, - {less: int32(1), greater: int32(2), msg: `"2" is not less than or equal to "1"`}, - {less: int64(1), greater: int64(2), msg: `"2" is not less than or equal to "1"`}, - {less: uint8(1), greater: uint8(2), msg: `"2" is not less than or equal to "1"`}, - {less: uint16(1), greater: uint16(2), msg: `"2" is not less than or equal to "1"`}, - {less: uint32(1), greater: uint32(2), msg: `"2" is not less than or equal to "1"`}, - {less: uint64(1), greater: uint64(2), msg: `"2" is not less than or equal to "1"`}, - {less: float32(1.23), greater: float32(2.34), msg: `"2.34" is not less than or equal to "1.23"`}, - {less: float64(1.23), greater: float64(2.34), msg: `"2.34" is not less than or equal to "1.23"`}, - {less: uintptr(1), greater: uintptr(2), msg: `"2" is not less than or equal to "1"`}, - {less: time.Time{}, greater: time.Time{}.Add(time.Hour), msg: `"0001-01-01 01:00:00 +0000 UTC" is not less than or equal to "0001-01-01 00:00:00 +0000 UTC"`}, - {less: []byte{1, 1}, greater: []byte{1, 2}, msg: `"[1 2]" is not less than or equal to "[1 1]"`}, - } { + // check error report + for currCase := range compareIncreasingFixtures3() { out := &outputT{buf: bytes.NewBuffer(nil)} False(t, LessOrEqual(out, currCase.greater, currCase.less)) - Contains(t, out.buf.String(), currCase.msg) + Contains(t, out.buf.String(), strings.ReplaceAll(currCase.msg, "than", "than or equal to")) Contains(t, out.helpers, "github.com/go-openapi/testify/v2/assert.LessOrEqual") } } @@ -492,3 +426,49 @@ func TestComparingMsgAndArgsForwarding(t *testing.T) { Contains(t, out.buf.String(), expectedOutput) } } + +//nolint:dupl // factoring further the message to save a little duplication would make the test harder to read +func compareIncreasingFixtures() iter.Seq[compareFixture] { + return slices.Values( + []compareFixture{ + {less: "a", greater: "b", msg: `"a" is not greater than "b"`}, + {less: int(1), greater: int(2), msg: `"1" is not greater than "2"`}, + {less: int8(1), greater: int8(2), msg: `"1" is not greater than "2"`}, + {less: int16(1), greater: int16(2), msg: `"1" is not greater than "2"`}, + {less: int32(1), greater: int32(2), msg: `"1" is not greater than "2"`}, + {less: int64(1), greater: int64(2), msg: `"1" is not greater than "2"`}, + {less: uint8(1), greater: uint8(2), msg: `"1" is not greater than "2"`}, + {less: uint16(1), greater: uint16(2), msg: `"1" is not greater than "2"`}, + {less: uint32(1), greater: uint32(2), msg: `"1" is not greater than "2"`}, + {less: uint64(1), greater: uint64(2), msg: `"1" is not greater than "2"`}, + {less: float32(1.23), greater: float32(2.34), msg: `"1.23" is not greater than "2.34"`}, + {less: float64(1.23), greater: float64(2.34), msg: `"1.23" is not greater than "2.34"`}, + {less: uintptr(1), greater: uintptr(2), msg: `"1" is not greater than "2"`}, + {less: time.Time{}, greater: time.Time{}.Add(time.Hour), msg: `"0001-01-01 00:00:00 +0000 UTC" is not greater than "0001-01-01 01:00:00 +0000 UTC"`}, + {less: []byte{1, 1}, greater: []byte{1, 2}, msg: `"[1 1]" is not greater than "[1 2]"`}, + }, + ) +} + +//nolint:dupl // factoring further the message to save a little duplication would make the test harder to read +func compareIncreasingFixtures3() iter.Seq[compareFixture] { + return slices.Values( + []compareFixture{ + {less: "a", greater: "b", msg: `"b" is not less than "a"`}, + {less: int(1), greater: int(2), msg: `"2" is not less than "1"`}, + {less: int8(1), greater: int8(2), msg: `"2" is not less than "1"`}, + {less: int16(1), greater: int16(2), msg: `"2" is not less than "1"`}, + {less: int32(1), greater: int32(2), msg: `"2" is not less than "1"`}, + {less: int64(1), greater: int64(2), msg: `"2" is not less than "1"`}, + {less: uint8(1), greater: uint8(2), msg: `"2" is not less than "1"`}, + {less: uint16(1), greater: uint16(2), msg: `"2" is not less than "1"`}, + {less: uint32(1), greater: uint32(2), msg: `"2" is not less than "1"`}, + {less: uint64(1), greater: uint64(2), msg: `"2" is not less than "1"`}, + {less: float32(1.23), greater: float32(2.34), msg: `"2.34" is not less than "1.23"`}, + {less: float64(1.23), greater: float64(2.34), msg: `"2.34" is not less than "1.23"`}, + {less: uintptr(1), greater: uintptr(2), msg: `"2" is not less than "1"`}, + {less: time.Time{}, greater: time.Time{}.Add(time.Hour), msg: `"0001-01-01 01:00:00 +0000 UTC" is not less than "0001-01-01 00:00:00 +0000 UTC"`}, + {less: []byte{1, 1}, greater: []byte{1, 2}, msg: `"[1 2]" is not less than "[1 1]"`}, + }, + ) +} diff --git a/assert/assertion_order_test.go b/assert/assertion_order_test.go index f81c5a7bc..deea57c60 100644 --- a/assert/assertion_order_test.go +++ b/assert/assertion_order_test.go @@ -3,9 +3,16 @@ package assert import ( "bytes" "fmt" + "iter" + "slices" "testing" ) +type orderedFixture struct { + collection any + msg string +} + func TestIsIncreasing(t *testing.T) { t.Parallel() @@ -28,26 +35,7 @@ func TestIsIncreasing(t *testing.T) { } // Check error report - for _, currCase := range []struct { - collection any - msg string - }{ - {collection: []string{"b", "a"}, msg: `"b" is not less than "a"`}, - {collection: []int{2, 1}, msg: `"2" is not less than "1"`}, - {collection: []int{2, 1, 3, 4, 5, 6, 7}, msg: `"2" is not less than "1"`}, - {collection: []int{-1, 0, 2, 1}, msg: `"2" is not less than "1"`}, - {collection: []int8{2, 1}, msg: `"2" is not less than "1"`}, - {collection: []int16{2, 1}, msg: `"2" is not less than "1"`}, - {collection: []int32{2, 1}, msg: `"2" is not less than "1"`}, - {collection: []int64{2, 1}, msg: `"2" is not less than "1"`}, - {collection: []uint8{2, 1}, msg: `"2" is not less than "1"`}, - {collection: []uint16{2, 1}, msg: `"2" is not less than "1"`}, - {collection: []uint32{2, 1}, msg: `"2" is not less than "1"`}, - {collection: []uint64{2, 1}, msg: `"2" is not less than "1"`}, - {collection: []float32{2.34, 1.23}, msg: `"2.34" is not less than "1.23"`}, - {collection: []float64{2.34, 1.23}, msg: `"2.34" is not less than "1.23"`}, - {collection: struct{}{}, msg: `object struct {} is not an ordered collection`}, - } { + for currCase := range decreasingFixtures() { t.Run(fmt.Sprintf("%#v", currCase.collection), func(t *testing.T) { out := &outputT{buf: bytes.NewBuffer(nil)} False(t, IsIncreasing(out, currCase.collection)) @@ -78,26 +66,7 @@ func TestIsNonIncreasing(t *testing.T) { } // Check error report - for _, currCase := range []struct { - collection any - msg string - }{ - {collection: []string{"a", "b"}, msg: `"a" is not greater than or equal to "b"`}, - {collection: []int{1, 2}, msg: `"1" is not greater than or equal to "2"`}, - {collection: []int{1, 2, 7, 6, 5, 4, 3}, msg: `"1" is not greater than or equal to "2"`}, - {collection: []int{5, 4, 3, 1, 2}, msg: `"1" is not greater than or equal to "2"`}, - {collection: []int8{1, 2}, msg: `"1" is not greater than or equal to "2"`}, - {collection: []int16{1, 2}, msg: `"1" is not greater than or equal to "2"`}, - {collection: []int32{1, 2}, msg: `"1" is not greater than or equal to "2"`}, - {collection: []int64{1, 2}, msg: `"1" is not greater than or equal to "2"`}, - {collection: []uint8{1, 2}, msg: `"1" is not greater than or equal to "2"`}, - {collection: []uint16{1, 2}, msg: `"1" is not greater than or equal to "2"`}, - {collection: []uint32{1, 2}, msg: `"1" is not greater than or equal to "2"`}, - {collection: []uint64{1, 2}, msg: `"1" is not greater than or equal to "2"`}, - {collection: []float32{1.23, 2.34}, msg: `"1.23" is not greater than or equal to "2.34"`}, - {collection: []float64{1.23, 2.34}, msg: `"1.23" is not greater than or equal to "2.34"`}, - {collection: struct{}{}, msg: `object struct {} is not an ordered collection`}, - } { + for currCase := range increasingFixtures() { t.Run(fmt.Sprintf("%#v", currCase.collection), func(t *testing.T) { out := &outputT{buf: bytes.NewBuffer(nil)} False(t, IsNonIncreasing(out, currCase.collection)) @@ -128,26 +97,7 @@ func TestIsDecreasing(t *testing.T) { } // Check error report - for _, currCase := range []struct { - collection any - msg string - }{ - {collection: []string{"a", "b"}, msg: `"a" is not greater than "b"`}, - {collection: []int{1, 2}, msg: `"1" is not greater than "2"`}, - {collection: []int{1, 2, 7, 6, 5, 4, 3}, msg: `"1" is not greater than "2"`}, - {collection: []int{5, 4, 3, 1, 2}, msg: `"1" is not greater than "2"`}, - {collection: []int8{1, 2}, msg: `"1" is not greater than "2"`}, - {collection: []int16{1, 2}, msg: `"1" is not greater than "2"`}, - {collection: []int32{1, 2}, msg: `"1" is not greater than "2"`}, - {collection: []int64{1, 2}, msg: `"1" is not greater than "2"`}, - {collection: []uint8{1, 2}, msg: `"1" is not greater than "2"`}, - {collection: []uint16{1, 2}, msg: `"1" is not greater than "2"`}, - {collection: []uint32{1, 2}, msg: `"1" is not greater than "2"`}, - {collection: []uint64{1, 2}, msg: `"1" is not greater than "2"`}, - {collection: []float32{1.23, 2.34}, msg: `"1.23" is not greater than "2.34"`}, - {collection: []float64{1.23, 2.34}, msg: `"1.23" is not greater than "2.34"`}, - {collection: struct{}{}, msg: `object struct {} is not an ordered collection`}, - } { + for currCase := range increasingFixtures2() { t.Run(fmt.Sprintf("%#v", currCase.collection), func(t *testing.T) { out := &outputT{buf: bytes.NewBuffer(nil)} False(t, IsDecreasing(out, currCase.collection)) @@ -178,26 +128,7 @@ func TestIsNonDecreasing(t *testing.T) { } // Check error report - for _, currCase := range []struct { - collection any - msg string - }{ - {collection: []string{"b", "a"}, msg: `"b" is not less than or equal to "a"`}, - {collection: []int{2, 1}, msg: `"2" is not less than or equal to "1"`}, - {collection: []int{2, 1, 3, 4, 5, 6, 7}, msg: `"2" is not less than or equal to "1"`}, - {collection: []int{-1, 0, 2, 1}, msg: `"2" is not less than or equal to "1"`}, - {collection: []int8{2, 1}, msg: `"2" is not less than or equal to "1"`}, - {collection: []int16{2, 1}, msg: `"2" is not less than or equal to "1"`}, - {collection: []int32{2, 1}, msg: `"2" is not less than or equal to "1"`}, - {collection: []int64{2, 1}, msg: `"2" is not less than or equal to "1"`}, - {collection: []uint8{2, 1}, msg: `"2" is not less than or equal to "1"`}, - {collection: []uint16{2, 1}, msg: `"2" is not less than or equal to "1"`}, - {collection: []uint32{2, 1}, msg: `"2" is not less than or equal to "1"`}, - {collection: []uint64{2, 1}, msg: `"2" is not less than or equal to "1"`}, - {collection: []float32{2.34, 1.23}, msg: `"2.34" is not less than or equal to "1.23"`}, - {collection: []float64{2.34, 1.23}, msg: `"2.34" is not less than or equal to "1.23"`}, - {collection: struct{}{}, msg: `object struct {} is not an ordered collection`}, - } { + for currCase := range decreasingFixtures2() { t.Run(fmt.Sprintf("%#v", currCase.collection), func(t *testing.T) { out := &outputT{buf: bytes.NewBuffer(nil)} False(t, IsNonDecreasing(out, currCase.collection)) @@ -224,3 +155,91 @@ func TestOrderingMsgAndArgsForwarding(t *testing.T) { Contains(t, out.buf.String(), expectedOutput) } } + +func decreasingFixtures() iter.Seq[orderedFixture] { + return slices.Values( + []orderedFixture{ + {collection: []string{"b", "a"}, msg: `"b" is not less than "a"`}, + {collection: []int{2, 1}, msg: `"2" is not less than "1"`}, + {collection: []int{2, 1, 3, 4, 5, 6, 7}, msg: `"2" is not less than "1"`}, + {collection: []int{-1, 0, 2, 1}, msg: `"2" is not less than "1"`}, + {collection: []int8{2, 1}, msg: `"2" is not less than "1"`}, + {collection: []int16{2, 1}, msg: `"2" is not less than "1"`}, + {collection: []int32{2, 1}, msg: `"2" is not less than "1"`}, + {collection: []int64{2, 1}, msg: `"2" is not less than "1"`}, + {collection: []uint8{2, 1}, msg: `"2" is not less than "1"`}, + {collection: []uint16{2, 1}, msg: `"2" is not less than "1"`}, + {collection: []uint32{2, 1}, msg: `"2" is not less than "1"`}, + {collection: []uint64{2, 1}, msg: `"2" is not less than "1"`}, + {collection: []float32{2.34, 1.23}, msg: `"2.34" is not less than "1.23"`}, + {collection: []float64{2.34, 1.23}, msg: `"2.34" is not less than "1.23"`}, + {collection: struct{}{}, msg: `object struct {} is not an ordered collection`}, + }, + ) +} + +func increasingFixtures() iter.Seq[orderedFixture] { + return slices.Values( + []orderedFixture{ + {collection: []string{"a", "b"}, msg: `"a" is not greater than or equal to "b"`}, + {collection: []int{1, 2}, msg: `"1" is not greater than or equal to "2"`}, + {collection: []int{1, 2, 7, 6, 5, 4, 3}, msg: `"1" is not greater than or equal to "2"`}, + {collection: []int{5, 4, 3, 1, 2}, msg: `"1" is not greater than or equal to "2"`}, + {collection: []int8{1, 2}, msg: `"1" is not greater than or equal to "2"`}, + {collection: []int16{1, 2}, msg: `"1" is not greater than or equal to "2"`}, + {collection: []int32{1, 2}, msg: `"1" is not greater than or equal to "2"`}, + {collection: []int64{1, 2}, msg: `"1" is not greater than or equal to "2"`}, + {collection: []uint8{1, 2}, msg: `"1" is not greater than or equal to "2"`}, + {collection: []uint16{1, 2}, msg: `"1" is not greater than or equal to "2"`}, + {collection: []uint32{1, 2}, msg: `"1" is not greater than or equal to "2"`}, + {collection: []uint64{1, 2}, msg: `"1" is not greater than or equal to "2"`}, + {collection: []float32{1.23, 2.34}, msg: `"1.23" is not greater than or equal to "2.34"`}, + {collection: []float64{1.23, 2.34}, msg: `"1.23" is not greater than or equal to "2.34"`}, + {collection: struct{}{}, msg: `object struct {} is not an ordered collection`}, + }, + ) +} + +func increasingFixtures2() iter.Seq[orderedFixture] { + return slices.Values( + []orderedFixture{ + {collection: []string{"a", "b"}, msg: `"a" is not greater than "b"`}, + {collection: []int{1, 2}, msg: `"1" is not greater than "2"`}, + {collection: []int{1, 2, 7, 6, 5, 4, 3}, msg: `"1" is not greater than "2"`}, + {collection: []int{5, 4, 3, 1, 2}, msg: `"1" is not greater than "2"`}, + {collection: []int8{1, 2}, msg: `"1" is not greater than "2"`}, + {collection: []int16{1, 2}, msg: `"1" is not greater than "2"`}, + {collection: []int32{1, 2}, msg: `"1" is not greater than "2"`}, + {collection: []int64{1, 2}, msg: `"1" is not greater than "2"`}, + {collection: []uint8{1, 2}, msg: `"1" is not greater than "2"`}, + {collection: []uint16{1, 2}, msg: `"1" is not greater than "2"`}, + {collection: []uint32{1, 2}, msg: `"1" is not greater than "2"`}, + {collection: []uint64{1, 2}, msg: `"1" is not greater than "2"`}, + {collection: []float32{1.23, 2.34}, msg: `"1.23" is not greater than "2.34"`}, + {collection: []float64{1.23, 2.34}, msg: `"1.23" is not greater than "2.34"`}, + {collection: struct{}{}, msg: `object struct {} is not an ordered collection`}, + }, + ) +} + +func decreasingFixtures2() iter.Seq[orderedFixture] { + return slices.Values( + []orderedFixture{ + {collection: []string{"b", "a"}, msg: `"b" is not less than or equal to "a"`}, + {collection: []int{2, 1}, msg: `"2" is not less than or equal to "1"`}, + {collection: []int{2, 1, 3, 4, 5, 6, 7}, msg: `"2" is not less than or equal to "1"`}, + {collection: []int{-1, 0, 2, 1}, msg: `"2" is not less than or equal to "1"`}, + {collection: []int8{2, 1}, msg: `"2" is not less than or equal to "1"`}, + {collection: []int16{2, 1}, msg: `"2" is not less than or equal to "1"`}, + {collection: []int32{2, 1}, msg: `"2" is not less than or equal to "1"`}, + {collection: []int64{2, 1}, msg: `"2" is not less than or equal to "1"`}, + {collection: []uint8{2, 1}, msg: `"2" is not less than or equal to "1"`}, + {collection: []uint16{2, 1}, msg: `"2" is not less than or equal to "1"`}, + {collection: []uint32{2, 1}, msg: `"2" is not less than or equal to "1"`}, + {collection: []uint64{2, 1}, msg: `"2" is not less than or equal to "1"`}, + {collection: []float32{2.34, 1.23}, msg: `"2.34" is not less than or equal to "1.23"`}, + {collection: []float64{2.34, 1.23}, msg: `"2.34" is not less than or equal to "1.23"`}, + {collection: struct{}{}, msg: `object struct {} is not an ordered collection`}, + }, + ) +} diff --git a/assert/assertions.go b/assert/assertions.go index 966e5ae3a..5656f5c63 100644 --- a/assert/assertions.go +++ b/assert/assertions.go @@ -24,7 +24,7 @@ import ( //go:generate sh -c "cd ../_codegen && go build && cd - && ../_codegen/_codegen -output-package=assert -template=assertion_format.go.tmpl" -// TestingT is an interface wrapper around *testing.T +// TestingT is an interface wrapper around *testing.T. type TestingT interface { Errorf(format string, args ...any) } @@ -49,7 +49,7 @@ type ErrorAssertionFunc func(TestingT, error, ...any) bool // for table driven tests. type PanicAssertionFunc = func(t TestingT, f PanicTestFunc, msgAndArgs ...any) bool -// Comparison is a custom function that returns true on success and false on failure +// Comparison is a custom function that returns true on success and false on failure. type Comparison func() (success bool) /* @@ -120,7 +120,7 @@ func copyExportedFields(expected any) any { } else { result = reflect.MakeSlice(expectedType, expectedValue.Len(), expectedValue.Len()) } - for i := 0; i < expectedValue.Len(); i++ { + for i := range expectedValue.Len() { index := expectedValue.Index(i) if isNil(index) { continue @@ -195,7 +195,7 @@ func ObjectsAreEqualValues(expected, actual any) bool { // isNumericType returns true if the type is one of: // int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, -// float32, float64, complex64, complex128 +// float32, float64, complex64, complex128. func isNumericType(t reflect.Type) bool { return t.Kind() >= reflect.Int && t.Kind() <= reflect.Complex128 } @@ -311,7 +311,10 @@ func messageFromMsgAndArgs(msgAndArgs ...any) string { return fmt.Sprintf("%+v", msg) } if len(msgAndArgs) > 1 { - return fmt.Sprintf(msgAndArgs[0].(string), msgAndArgs[1:]...) + format, ok := msgAndArgs[0].(string) + if ok { + return fmt.Sprintf(format, msgAndArgs[1:]...) + } } return "" } @@ -341,7 +344,7 @@ type failNower interface { FailNow() } -// FailNow fails test +// FailNow fails test. func FailNow(t TestingT, failureMessage string, msgAndArgs ...any) bool { if h, ok := t.(tHelper); ok { h.Helper() @@ -362,7 +365,7 @@ func FailNow(t TestingT, failureMessage string, msgAndArgs ...any) bool { return false } -// Fail reports a failure through +// Fail reports a failure through. func Fail(t TestingT, failureMessage string, msgAndArgs ...any) bool { if h, ok := t.(tHelper); ok { h.Helper() @@ -410,11 +413,11 @@ func labeledOutput(content ...labeledContent) string { longestLabel = len(v.label) } } - var output string + var output strings.Builder for _, v := range content { - output += "\t" + v.label + ":" + strings.Repeat(" ", longestLabel-len(v.label)) + "\t" + indentMessageLines(v.content, longestLabel) + "\n" + output.WriteString("\t" + v.label + ":" + strings.Repeat(" ", longestLabel-len(v.label)) + "\t" + indentMessageLines(v.content, longestLabel) + "\n") } - return output + return output.String() } // Implements asserts that an object is implemented by the specified interface. @@ -619,9 +622,10 @@ func formatUnequalValues(expected, actual any) (e string, a string) { // This helps keep formatted error messages lines from exceeding the // bufio.MaxScanTokenSize max line length that the go testing framework imposes. func truncatingFormat(format string, data any) string { + const maxMessageSize = bufio.MaxScanTokenSize/2 - 100 + value := fmt.Sprintf(format, data) // Give us space for two truncated objects and the surrounding sentence. - maxMessageSize := bufio.MaxScanTokenSize/2 - 100 if len(value) > maxMessageSize { value = value[0:maxMessageSize] + "<... truncated>" } @@ -729,9 +733,9 @@ func isNil(object any) bool { reflect.Ptr, reflect.Slice, reflect.UnsafePointer: return value.IsNil() + default: + return false } - - return false } // Nil asserts that the specified object is nil. @@ -771,8 +775,9 @@ func isEmptyValue(objValue reflect.Value) bool { // non-nil pointers are empty if the value they point to is empty case reflect.Ptr: return isEmptyValue(objValue.Elem()) + default: + return false } - return false } // Empty asserts that the given value is "empty". @@ -936,7 +941,7 @@ func containsElement(list any, element any) (ok, found bool) { if listKind == reflect.Map { mapKeys := listValue.MapKeys() - for i := 0; i < len(mapKeys); i++ { + for i := range mapKeys { if ObjectsAreEqual(mapKeys[i].Interface(), element) { return true, true } @@ -944,7 +949,7 @@ func containsElement(list any, element any) (ok, found bool) { return true, false } - for i := 0; i < listValue.Len(); i++ { + for i := range listValue.Len() { if ObjectsAreEqual(listValue.Index(i).Interface(), element) { return true, true } @@ -1050,7 +1055,8 @@ func Subset(t TestingT, list, subset any, msgAndArgs ...any) (ok bool) { } subsetList = reflect.ValueOf(keys) } - for i := 0; i < subsetList.Len(); i++ { + + for i := range subsetList.Len() { element := subsetList.Index(i).Interface() ok, found := containsElement(list, element) if !ok { @@ -1118,7 +1124,7 @@ func NotSubset(t TestingT, list, subset any, msgAndArgs ...any) (ok bool) { } subsetList = reflect.ValueOf(keys) } - for i := 0; i < subsetList.Len(); i++ { + for i := range subsetList.Len() { element := subsetList.Index(i).Interface() ok, found := containsElement(list, element) if !ok { @@ -1136,7 +1142,7 @@ func NotSubset(t TestingT, list, subset any, msgAndArgs ...any) (ok bool) { // listB(array, slice...) ignoring the order of the elements. If there are duplicate elements, // the number of appearances of each of them in both lists should match. // -// assert.ElementsMatch(t, [1, 3, 2, 3], [1, 3, 3, 2]) +// assert.ElementsMatch(t, [1, 3, 2, 3], [1, 3, 3, 2]). func ElementsMatch(t TestingT, listA, listB any, msgAndArgs ...any) (ok bool) { if h, ok := t.(tHelper); ok { h.Helper() @@ -1180,10 +1186,10 @@ func diffLists(listA, listB any) (extraA, extraB []any) { // Mark indexes in bValue that we already used visited := make([]bool, bLen) - for i := 0; i < aLen; i++ { + for i := range aLen { element := aValue.Index(i).Interface() found := false - for j := 0; j < bLen; j++ { + for j := range bLen { if visited[j] { continue } @@ -1198,7 +1204,7 @@ func diffLists(listA, listB any) (extraA, extraB []any) { } } - for j := 0; j < bLen; j++ { + for j := range bLen { if visited[j] { continue } @@ -1237,7 +1243,7 @@ func formatListDiff(listA, listB any, extraA, extraB []any) string { // // assert.NotElementsMatch(t, [1, 1, 2, 3], [1, 2, 3]) -> true // -// assert.NotElementsMatch(t, [1, 2, 3], [1, 2, 4]) -> true +// assert.NotElementsMatch(t, [1, 2, 3], [1, 2, 4]) -> true. func NotElementsMatch(t TestingT, listA, listB any, msgAndArgs ...any) (ok bool) { if h, ok := t.(tHelper); ok { h.Helper() @@ -1504,7 +1510,7 @@ func InDeltaSlice(t TestingT, expected, actual any, delta float64, msgAndArgs .. actualSlice := reflect.ValueOf(actual) expectedSlice := reflect.ValueOf(expected) - for i := 0; i < actualSlice.Len(); i++ { + for i := range actualSlice.Len() { result := InDelta(t, actualSlice.Index(i).Interface(), expectedSlice.Index(i).Interface(), delta, msgAndArgs...) if !result { return result @@ -1562,7 +1568,7 @@ func calcRelativeError(expected, actual any) (float64, error) { af, aok := toFloat(expected) bf, bok := toFloat(actual) if !aok || !bok { - return 0, errors.New("Parameters must be numerical") + return 0, errors.New("parameters must be numerical") } if math.IsNaN(af) && math.IsNaN(bf) { return 0, nil @@ -1580,7 +1586,7 @@ func calcRelativeError(expected, actual any) (float64, error) { return math.Abs(af-bf) / math.Abs(af), nil } -// InEpsilon asserts that expected and actual have a relative error less than epsilon +// InEpsilon asserts that expected and actual have a relative error less than epsilon. func InEpsilon(t TestingT, expected, actual any, epsilon float64, msgAndArgs ...any) bool { if h, ok := t.(tHelper); ok { h.Helper() @@ -1625,7 +1631,7 @@ func InEpsilonSlice(t TestingT, expected, actual any, epsilon float64, msgAndArg return false } - for i := 0; i < expectedLen; i++ { + for i := range expectedLen { if !InEpsilon(t, expectedSlice.Index(i).Interface(), actualSlice.Index(i).Interface(), epsilon, "at index %d", i) { return false } @@ -1745,7 +1751,7 @@ func Regexp(t TestingT, rx any, str any, msgAndArgs ...any) bool { match := matchRegexp(rx, str) if !match { - Fail(t, fmt.Sprintf("Expect \"%v\" to match \"%v\"", str, rx), msgAndArgs...) + Fail(t, fmt.Sprintf(`Expect "%v" to match "%v"`, str, rx), msgAndArgs...) } return match @@ -1962,10 +1968,10 @@ func diff(expected any, actual any) string { var e, a string switch et { - case reflect.TypeOf(""): + case reflect.TypeFor[string](): e = reflect.ValueOf(expected).String() a = reflect.ValueOf(actual).String() - case reflect.TypeOf(time.Time{}): + case reflect.TypeFor[time.Time](): e = spewConfigStringerEnabled.Sdump(expected) a = spewConfigStringerEnabled.Sdump(actual) default: @@ -1993,22 +1999,27 @@ func isFunction(arg any) bool { return reflect.TypeOf(arg).Kind() == reflect.Func } -var spewConfig = spew.ConfigState{ - Indent: " ", - DisablePointerAddresses: true, - DisableCapacities: true, - SortKeys: true, - DisableMethods: true, - MaxDepth: 10, -} +const spewMaxDepth = 10 -var spewConfigStringerEnabled = spew.ConfigState{ - Indent: " ", - DisablePointerAddresses: true, - DisableCapacities: true, - SortKeys: true, - MaxDepth: 10, -} +//nolint:gochecknoglobals // spew is more easily configured using a global default config. This is okay in this context. +var ( + spewConfig = spew.ConfigState{ + Indent: " ", + DisablePointerAddresses: true, + DisableCapacities: true, + SortKeys: true, + DisableMethods: true, + MaxDepth: spewMaxDepth, + } + + spewConfigStringerEnabled = spew.ConfigState{ + Indent: " ", + DisablePointerAddresses: true, + DisableCapacities: true, + SortKeys: true, + MaxDepth: spewMaxDepth, + } +) type tHelper = interface { Helper() @@ -2293,7 +2304,7 @@ func NotErrorAs(t TestingT, err error, target any, msgAndArgs ...any) bool { func unwrapAll(err error) (errs []error) { errs = append(errs, err) - switch x := err.(type) { + switch x := err.(type) { //nolint:errorlint // false positive: this type switch is checking for interfaces case interface{ Unwrap() error }: err = x.Unwrap() if err == nil { @@ -2313,16 +2324,16 @@ func buildErrorChainString(err error, withType bool) string { return "" } - var chain string + var chain strings.Builder errs := unwrapAll(err) for i := range errs { if i != 0 { - chain += "\n\t" + chain.WriteString("\n\t") } - chain += fmt.Sprintf("%q", errs[i].Error()) + chain.WriteString(fmt.Sprintf("%q", errs[i].Error())) if withType { - chain += fmt.Sprintf(" (%T)", errs[i]) + chain.WriteString(fmt.Sprintf(" (%T)", errs[i])) } } - return chain + return chain.String() } diff --git a/assert/assertions_test.go b/assert/assertions_test.go index 87092210d..5e2151d43 100644 --- a/assert/assertions_test.go +++ b/assert/assertions_test.go @@ -3,7 +3,6 @@ package assert import ( "bufio" "bytes" - "encoding/json" "errors" "fmt" "io" @@ -18,6 +17,7 @@ import ( "time" ) +//nolint:gochecknoglobals // private globals to store zero values. Not exposed, not mutated: should be fine var ( i any zeros = []any{ @@ -85,18 +85,18 @@ var ( } ) -// AssertionTesterInterface defines an interface to be used for testing assertion methods +// AssertionTesterInterface defines an interface to be used for testing assertion methods. type AssertionTesterInterface interface { TestMethod() } -// AssertionTesterConformingObject is an object that conforms to the AssertionTesterInterface interface +// AssertionTesterConformingObject is an object that conforms to the AssertionTesterInterface interface. type AssertionTesterConformingObject struct{} func (a *AssertionTesterConformingObject) TestMethod() { } -// AssertionTesterNonConformingObject is an object that does not conform to the AssertionTesterInterface interface +// AssertionTesterNonConformingObject is an object that does not conform to the AssertionTesterInterface interface. type AssertionTesterNonConformingObject struct{} func TestObjectsAreEqual(t *testing.T) { @@ -149,7 +149,8 @@ func TestObjectsAreEqualValues(t *testing.T) { {uint32(10), int32(10), true}, {0, nil, false}, {nil, 0, false}, - {now, now.In(time.Local), false}, // should not be time zone independent + // should not be time zone independent + {now, now.In(time.Local), false}, //nolint:gosmopolitan // ok in this context: this is precisely the goal of this test {int(270), int8(14), false}, // should handle overflow/underflow {int8(14), int(270), false}, {[]int{270, 270}, []int8{14, 14}, false}, @@ -809,7 +810,21 @@ func TestStringEqual(t *testing.T) { msgAndArgs []any want string }{ - {equalWant: "hi, \nmy name is", equalGot: "what,\nmy name is", want: "\tassertions.go:\\d+: \n\t+Error Trace:\t\n\t+Error:\\s+Not equal:\\s+\n\\s+expected: \"hi, \\\\nmy name is\"\n\\s+actual\\s+: \"what,\\\\nmy name is\"\n\\s+Diff:\n\\s+-+ Expected\n\\s+\\++ Actual\n\\s+@@ -1,2 \\+1,2 @@\n\\s+-hi, \n\\s+\\+what,\n\\s+my name is"}, + { + equalWant: "hi, \nmy name is", + equalGot: "what,\nmy name is", + want: "\tassertions.go:\\d+: \n" + + "\t+Error Trace:\t\n+" + + "\t+Error:\\s+Not equal:\\s+\n" + + "\\s+expected: \"hi, \\\\nmy name is\"\n" + + "\\s+actual\\s+: " + "\"what,\\\\nmy name is\"\n" + + "\\s+Diff:\n" + + "\\s+-+ Expected\n\\s+\\++ " + + "Actual\n" + + "\\s+@@ -1,2 \\+1,2 @@\n" + + "\\s+-hi, \n\\s+\\+what,\n" + + "\\s+my name is", + }, } { mockT := &bufferT{} Equal(mockT, currCase.equalWant, currCase.equalGot, currCase.msgAndArgs...) @@ -826,10 +841,71 @@ func TestEqualFormatting(t *testing.T) { msgAndArgs []any want string }{ - {equalWant: "want", equalGot: "got", want: "\tassertions.go:\\d+: \n\t+Error Trace:\t\n\t+Error:\\s+Not equal:\\s+\n\\s+expected: \"want\"\n\\s+actual\\s+: \"got\"\n\\s+Diff:\n\\s+-+ Expected\n\\s+\\++ Actual\n\\s+@@ -1 \\+1 @@\n\\s+-want\n\\s+\\+got\n"}, - {equalWant: "want", equalGot: "got", msgAndArgs: []any{"hello, %v!", "world"}, want: "\tassertions.go:[0-9]+: \n\t+Error Trace:\t\n\t+Error:\\s+Not equal:\\s+\n\\s+expected: \"want\"\n\\s+actual\\s+: \"got\"\n\\s+Diff:\n\\s+-+ Expected\n\\s+\\++ Actual\n\\s+@@ -1 \\+1 @@\n\\s+-want\n\\s+\\+got\n\\s+Messages:\\s+hello, world!\n"}, - {equalWant: "want", equalGot: "got", msgAndArgs: []any{123}, want: "\tassertions.go:[0-9]+: \n\t+Error Trace:\t\n\t+Error:\\s+Not equal:\\s+\n\\s+expected: \"want\"\n\\s+actual\\s+: \"got\"\n\\s+Diff:\n\\s+-+ Expected\n\\s+\\++ Actual\n\\s+@@ -1 \\+1 @@\n\\s+-want\n\\s+\\+got\n\\s+Messages:\\s+123\n"}, - {equalWant: "want", equalGot: "got", msgAndArgs: []any{struct{ a string }{"hello"}}, want: "\tassertions.go:[0-9]+: \n\t+Error Trace:\t\n\t+Error:\\s+Not equal:\\s+\n\\s+expected: \"want\"\n\\s+actual\\s+: \"got\"\n\\s+Diff:\n\\s+-+ Expected\n\\s+\\++ Actual\n\\s+@@ -1 \\+1 @@\n\\s+-want\n\\s+\\+got\n\\s+Messages:\\s+{a:hello}\n"}, + { + equalWant: "want", + equalGot: "got", + want: "\tassertions.go:\\d+: \n" + + "\t+Error Trace:\t\n" + + "\t+Error:\\s+Not equal:\\s+\n" + + "\\s+expected: \"want\"\n" + + "\\s+actual\\s+: \"got\"\n" + + "\\s+Diff:\n\\s+-+ Expected\n\\s+\\++ " + + "Actual\n" + + "\\s+@@ -1 \\+1 @@\n" + + "\\s+-want\n" + + "\\s+\\+got\n", + }, + { + equalWant: "want", + equalGot: "got", + msgAndArgs: []any{"hello, %v!", "world"}, + want: "\tassertions.go:[0-9]+: \n" + + "\t+Error Trace:\t\n" + + "\t+Error:\\s+Not equal:\\s+\n" + + "\\s+expected: \"want\"\n" + + "\\s+actual\\s+: \"got\"\n" + + "\\s+Diff:\n" + + "\\s+-+ Expected\n" + + "\\s+\\++ Actual\n" + + "\\s+@@ -1 \\+1 @@\n" + + "\\s+-want\n" + + "\\s+\\+got\n" + + "\\s+Messages:\\s+hello, world!\n", + }, + { + equalWant: "want", + equalGot: "got", + msgAndArgs: []any{123}, + want: "\tassertions.go:[0-9]+: \n" + + "\t+Error Trace:\t\n" + + "\t+Error:\\s+Not equal:\\s+\n" + + "\\s+expected: \"want\"\n" + + "\\s+actual\\s+: \"got\"\n" + + "\\s+Diff:\n" + + "\\s+-+ Expected\n" + + "\\s+\\++ Actual\n" + + "\\s+@@ -1 \\+1 @@\n" + + "\\s+-want\n" + + "\\s+\\+got\n" + + "\\s+Messages:\\s+123\n", + }, + { + equalWant: "want", + equalGot: "got", + msgAndArgs: []any{struct{ a string }{"hello"}}, + want: "\tassertions.go:[0-9]+: \n" + + "\t+Error Trace:\t\n" + + "\t+Error:\\s+Not equal:\\s+\n" + + "\\s+expected: \"want\"\n" + + "\\s+actual\\s+: \"got\"\n" + + "\\s+Diff:\n" + + "\\s+-+ Expected\n" + + "\\s+\\++ Actual\n" + + "\\s+@@ -1 \\+1 @@\n" + + "\\s+-want\n" + + "\\s+\\+got\n" + + "\\s+Messages:\\s+{a:hello}\n", + }, } { mockT := &bufferT{} Equal(mockT, currCase.equalWant, currCase.equalGot, currCase.msgAndArgs...) @@ -1630,7 +1706,7 @@ func TestPanicsWithError(t *testing.T) { Contains(t, mockT.msg, `Error message: "actual panic err msg"`) succeeded = PanicsWithError(mockT, "expected panic err msg", func() { - panic(&PanicsWithErrorWrapper{"wrapped", errors.New("actual panic err msg")}) + panic(&PanicsWrapperError{"wrapped", errors.New("actual panic err msg")}) }) Equal(t, false, succeeded, "PanicsWithError should return false") Contains(t, mockT.msg, `Error message: "wrapped: actual panic err msg"`) @@ -1643,12 +1719,12 @@ func TestPanicsWithError(t *testing.T) { NotContains(t, mockT.msg, "Error message:", "PanicsWithError should not report error message if not due an error") } -type PanicsWithErrorWrapper struct { +type PanicsWrapperError struct { Prefix string Err error } -func (e PanicsWithErrorWrapper) Error() string { +func (e PanicsWrapperError) Error() string { return e.Prefix + ": " + e.Err.Error() } @@ -1897,9 +1973,8 @@ func Benchmark_isEmpty(b *testing.B) { b.ReportAllocs() v := new(int) - b.ResetTimer() - for i := 0; i < b.N; i++ { + for b.Loop() { isEmpty("") isEmpty(42) isEmpty(v) @@ -3001,7 +3076,7 @@ func TestDiffEmptyCases(t *testing.T) { Equal(t, "", diff([]int{1}, []bool{true})) } -// Ensure there are no data races +// Ensure there are no data races. func TestDiffRace(t *testing.T) { t.Parallel() @@ -3071,7 +3146,10 @@ type mockFailNowTestingT struct{} // Helper is like [testing.T.Helper] but does nothing. func (mockFailNowTestingT) Helper() {} -func (m *mockFailNowTestingT) Errorf(format string, args ...any) {} +func (m *mockFailNowTestingT) Errorf(format string, args ...any) { + _ = format + _ = args +} func (m *mockFailNowTestingT) FailNow() {} @@ -3110,48 +3188,18 @@ func BenchmarkBytesEqual(b *testing.B) { copy(s2, s) mockT := &mockFailNowTestingT{} - b.ResetTimer() - for i := 0; i < b.N; i++ { + + for b.Loop() { Equal(mockT, s, s2) } } func BenchmarkNotNil(b *testing.B) { - for i := 0; i < b.N; i++ { + for b.Loop() { NotNil(b, b) } } -func ExampleComparisonAssertionFunc() { - t := &testing.T{} // provided by test - - adder := func(x, y int) int { - return x + y - } - - type args struct { - x int - y int - } - - tests := []struct { - name string - args args - expect int - assertion ComparisonAssertionFunc - }{ - {"2+2=4", args{2, 2}, 4, Equal}, - {"2+2!=5", args{2, 2}, 5, NotEqual}, - {"2+3==5", args{2, 3}, 5, Exactly}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tt.assertion(t, tt.expect, adder(tt.args.x, tt.args.y)) - }) - } -} - func TestComparisonAssertionFunc(t *testing.T) { t.Parallel() @@ -3187,34 +3235,6 @@ func TestComparisonAssertionFunc(t *testing.T) { } } -func ExampleValueAssertionFunc() { - t := &testing.T{} // provided by test - - dumbParse := func(input string) any { - var x any - _ = json.Unmarshal([]byte(input), &x) - return x - } - - tests := []struct { - name string - arg string - assertion ValueAssertionFunc - }{ - {"true is not nil", "true", NotNil}, - {"empty string is nil", "", Nil}, - {"zero is not nil", "0", NotNil}, - {"zero is zero", "0", Zero}, - {"false is zero", "false", Zero}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tt.assertion(t, dumbParse(tt.arg)) - }) - } -} - func TestValueAssertionFunc(t *testing.T) { t.Parallel() @@ -3238,31 +3258,6 @@ func TestValueAssertionFunc(t *testing.T) { } } -func ExampleBoolAssertionFunc() { - t := &testing.T{} // provided by test - - isOkay := func(x int) bool { - return x >= 42 - } - - tests := []struct { - name string - arg int - assertion BoolAssertionFunc - }{ - {"-1 is bad", -1, False}, - {"42 is good", 42, True}, - {"41 is bad", 41, False}, - {"45 is cool", 45, True}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tt.assertion(t, isOkay(tt.arg)) - }) - } -} - func TestBoolAssertionFunc(t *testing.T) { t.Parallel() @@ -3282,32 +3277,6 @@ func TestBoolAssertionFunc(t *testing.T) { } } -func ExampleErrorAssertionFunc() { - t := &testing.T{} // provided by test - - dumbParseNum := func(input string, v any) error { - return json.Unmarshal([]byte(input), v) - } - - tests := []struct { - name string - arg string - assertion ErrorAssertionFunc - }{ - {"1.2 is number", "1.2", NoError}, - {"1.2.3 not number", "1.2.3", Error}, - {"true is not number", "true", Error}, - {"3 is number", "3", NoError}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - var x float64 - tt.assertion(t, dumbParseNum(tt.arg, &x)) - }) - } -} - func TestErrorAssertionFunc(t *testing.T) { t.Parallel() @@ -3327,25 +3296,6 @@ func TestErrorAssertionFunc(t *testing.T) { } } -func ExamplePanicAssertionFunc() { - t := &testing.T{} // provided by test - - tests := []struct { - name string - panicFn PanicTestFunc - assertion PanicAssertionFunc - }{ - {"with panic", func() { panic(nil) }, Panics}, - {"without panic", func() {}, NotPanics}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tt.assertion(t, tt.panicFn) - }) - } -} - func TestPanicAssertionFunc(t *testing.T) { t.Parallel() @@ -3485,7 +3435,7 @@ func TestEventuallyWithTFailNow(t *testing.T) { } // Check that a long running condition doesn't block Eventually. -// See issue 805 (and its long tail of following issues) +// See issue 805 (and its long tail of following issues). func TestEventuallyTimeout(t *testing.T) { t.Parallel() diff --git a/assert/examples_test.go b/assert/examples_test.go new file mode 100644 index 000000000..81fa92b7e --- /dev/null +++ b/assert/examples_test.go @@ -0,0 +1,135 @@ +//nolint:testableexamples // not possible at this moment to build a testable example that involves testing.T +package assert + +import ( + "encoding/json" + "testing" +) + +func ExampleComparisonAssertionFunc() { + t := &testing.T{} // provided by test + + adder := func(x, y int) int { + return x + y + } + + type args struct { + x int + y int + } + + tests := []struct { + name string + args args + expect int + assertion ComparisonAssertionFunc + }{ + {"2+2=4", args{2, 2}, 4, Equal}, + {"2+2!=5", args{2, 2}, 5, NotEqual}, + {"2+3==5", args{2, 3}, 5, Exactly}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.assertion(t, tt.expect, adder(tt.args.x, tt.args.y)) + }) + } +} + +func ExampleValueAssertionFunc() { + t := &testing.T{} // provided by test + + dumbParse := func(input string) any { + var x any + _ = json.Unmarshal([]byte(input), &x) + return x + } + + tests := []struct { + name string + arg string + assertion ValueAssertionFunc + }{ + {"true is not nil", "true", NotNil}, + {"empty string is nil", "", Nil}, + {"zero is not nil", "0", NotNil}, + {"zero is zero", "0", Zero}, + {"false is zero", "false", Zero}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.assertion(t, dumbParse(tt.arg)) + }) + } +} + +func ExampleBoolAssertionFunc() { + t := &testing.T{} // provided by test + + isOkay := func(x int) bool { + return x >= 42 + } + + tests := []struct { + name string + arg int + assertion BoolAssertionFunc + }{ + {"-1 is bad", -1, False}, + {"42 is good", 42, True}, + {"41 is bad", 41, False}, + {"45 is cool", 45, True}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.assertion(t, isOkay(tt.arg)) + }) + } +} + +func ExampleErrorAssertionFunc() { + t := &testing.T{} // provided by test + + dumbParseNum := func(input string, v any) error { + return json.Unmarshal([]byte(input), v) + } + + tests := []struct { + name string + arg string + assertion ErrorAssertionFunc + }{ + {"1.2 is number", "1.2", NoError}, + {"1.2.3 not number", "1.2.3", Error}, + {"true is not number", "true", Error}, + {"3 is number", "3", NoError}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var x float64 + tt.assertion(t, dumbParseNum(tt.arg, &x)) + }) + } +} + +func ExamplePanicAssertionFunc() { + t := &testing.T{} // provided by test + + tests := []struct { + name string + panicFn PanicTestFunc + assertion PanicAssertionFunc + }{ + {"with panic", func() { panic(nil) }, Panics}, + {"without panic", func() {}, NotPanics}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.assertion(t, tt.panicFn) + }) + } +} diff --git a/assert/yaml/yaml_default.go b/assert/yaml/yaml_default.go index f617ab6fa..f45b5041b 100644 --- a/assert/yaml/yaml_default.go +++ b/assert/yaml/yaml_default.go @@ -7,7 +7,7 @@ // [gopkg.in/yaml.v3] (for example for license compatibility reasons, see [PR #1120]). package yaml -var EnableYAMLUnmarshal func([]byte, any) error +var EnableYAMLUnmarshal func([]byte, any) error //nolint:gochecknoglobals // in this particular case, we need a global to enable the feature from another module // Unmarshal is just a wrapper of [gopkg.in/yaml.v3.Unmarshal]. func Unmarshal(in []byte, out any) error { @@ -19,7 +19,7 @@ You should enable a YAML library before running this test, e.g. by adding the following to your imports: import ( - _ "github.com/go-openapi/testify/v2/enable/yaml" + _ "github.com/go-openapi/testify/enable/yaml/v2" ) `, ) diff --git a/doc.go b/doc.go index 7d21e551e..e80e38bcc 100644 --- a/doc.go +++ b/doc.go @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 -// Module testify is a set of packages that provide many tools for testifying that your code will behave as you intend. +// Package testify is a set of packages that provide many tools for testifying that your code will behave as you intend. // // Testify contains the following packages: // diff --git a/enable/yaml/assertions_test.go b/enable/yaml/assertions_test.go index 3ea5a6d38..15c53c54c 100644 --- a/enable/yaml/assertions_test.go +++ b/enable/yaml/assertions_test.go @@ -24,32 +24,7 @@ func TestYAMLEq_HashOfArraysAndHashes(t *testing.T) { t.Parallel() mockT := new(testing.T) - expected := ` -numeric: 1.5 -array: - - foo: bar - - 1 - - "string" - - ["nested", "array", 5.5] -hash: - nested: hash - nested_slice: [this, is, nested] -string: "foo" -` - - actual := ` -numeric: 1.5 -hash: - nested: hash - nested_slice: [this, is, nested] -string: "foo" -array: - - foo: bar - - 1 - - "string" - - ["nested", "array", 5.5] -` - target.True(t, target.YAMLEq(mockT, expected, actual)) + target.True(t, target.YAMLEq(mockT, expectedYAML, actualYAML)) } func TestYAMLEq_Array(t *testing.T) { diff --git a/enable/yaml/enable_yaml.go b/enable/yaml/enable_yaml.go index 905a20a66..31a4e85d1 100644 --- a/enable/yaml/enable_yaml.go +++ b/enable/yaml/enable_yaml.go @@ -10,6 +10,6 @@ import ( yaml "go.yaml.in/yaml/v3" ) -func init() { +func init() { //nolint:gochecknoinits // we precisely want this init to run when importing the package yamlstub.EnableYAMLUnmarshal = yaml.Unmarshal } diff --git a/enable/yaml/requirements_test.go b/enable/yaml/requirements_test.go index da2443faa..8d5ae5a1a 100644 --- a/enable/yaml/requirements_test.go +++ b/enable/yaml/requirements_test.go @@ -6,6 +6,34 @@ import ( target "github.com/go-openapi/testify/v2/require" ) +const ( + expectedYAML = ` +numeric: 1.5 +array: + - foo: bar + - 1 + - "string" + - ["nested", "array", 5.5] +hash: + nested: hash + nested_slice: [this, is, nested] +string: "foo" +` + + actualYAML = ` +numeric: 1.5 +hash: + nested: hash + nested_slice: [this, is, nested] +string: "foo" +array: + - foo: bar + - 1 + - "string" + - ["nested", "array", 5.5] +` +) + func TestRequireYAMLEq_EqualYAMLString(t *testing.T) { t.Parallel() @@ -30,32 +58,7 @@ func TestRequireYAMLEq_HashOfArraysAndHashes(t *testing.T) { t.Parallel() mockT := new(MockT) - expected := ` -numeric: 1.5 -array: - - foo: bar - - 1 - - "string" - - ["nested", "array", 5.5] -hash: - nested: hash - nested_slice: [this, is, nested] -string: "foo" -` - - actual := ` -numeric: 1.5 -hash: - nested: hash - nested_slice: [this, is, nested] -string: "foo" -array: - - foo: bar - - 1 - - "string" - - ["nested", "array", 5.5] -` - target.YAMLEq(mockT, expected, actual) + target.YAMLEq(mockT, expectedYAML, actualYAML) if mockT.Failed { t.Error("Check should pass") } @@ -142,6 +145,6 @@ func (t *MockT) FailNow() { t.Failed = true } -func (t *MockT) Errorf(format string, args ...interface{}) { +func (t *MockT) Errorf(format string, args ...any) { _, _ = format, args } diff --git a/.ci.gogenerate.sh b/hack/gogenerate.sh similarity index 100% rename from .ci.gogenerate.sh rename to hack/gogenerate.sh diff --git a/.ci.readme.fmt.sh b/hack/readme.fmt.sh similarity index 100% rename from .ci.readme.fmt.sh rename to hack/readme.fmt.sh diff --git a/hack/upgrade_modules.sh b/hack/upgrade_modules.sh new file mode 100755 index 000000000..c2bc7fc79 --- /dev/null +++ b/hack/upgrade_modules.sh @@ -0,0 +1,20 @@ +#! /bin/bash +new_tag=$1 + +cd "$(git rev-parse --show-toplevel)" +while read -r dir +do + pushd $dir + go list -deps -test \ + -f '{{ if .DepOnly }}{{ with .Module }}{{ .Path }}{{ end }}{{ end }}'|\ + sort -u|\ + grep "go-openapi/swag"|\ + while read -r module ; do + go mod edit -require "${module}@${new_tag}" + done + + go mod tidy + popd +done < <(find . -name go.mod |xargs dirname) + +go work sync diff --git a/internal/difflib/difflib.go b/internal/difflib/difflib.go index 9984599b4..cdcf1e9ee 100644 --- a/internal/difflib/difflib.go +++ b/internal/difflib/difflib.go @@ -23,23 +23,10 @@ import ( "bytes" "fmt" "io" + "strconv" "strings" ) -func min(a, b int) int { - if a < b { - return a - } - return b -} - -func max(a, b int) int { - if a > b { - return a - } - return b -} - type Match struct { A int B int @@ -99,13 +86,13 @@ func NewMatcher(a, b []string) *SequenceMatcher { return &m } -// Set two sequences to be compared. +// SetSeqs sets two sequences to be compared. func (m *SequenceMatcher) SetSeqs(a, b []string) { m.SetSeq1(a) m.SetSeq2(b) } -// Set the first sequence to be compared. The second sequence to be compared is +// SetSeq1 sets the first sequence to be compared. The second sequence to be compared is // not changed. // // SequenceMatcher computes and caches detailed information about the second @@ -123,7 +110,7 @@ func (m *SequenceMatcher) SetSeq1(a []string) { m.opCodes = nil } -// Set the second sequence to be compared. The first sequence to be compared is +// SetSeq2 sets the second sequence to be compared. The first sequence to be compared is // not changed. func (m *SequenceMatcher) SetSeq2(b []string) { if &b == &m.b { @@ -136,153 +123,7 @@ func (m *SequenceMatcher) SetSeq2(b []string) { m.chainB() } -func (m *SequenceMatcher) chainB() { - // Populate line -> index mapping - b2j := map[string][]int{} - for i, s := range m.b { - indices := b2j[s] - indices = append(indices, i) - b2j[s] = indices - } - - // Purge junk elements - m.bJunk = map[string]struct{}{} - if m.IsJunk != nil { - junk := m.bJunk - for s, _ := range b2j { - if m.IsJunk(s) { - junk[s] = struct{}{} - } - } - for s, _ := range junk { - delete(b2j, s) - } - } - - // Purge remaining popular elements - popular := map[string]struct{}{} - n := len(m.b) - if m.autoJunk && n >= 200 { - ntest := n/100 + 1 - for s, indices := range b2j { - if len(indices) > ntest { - popular[s] = struct{}{} - } - } - for s, _ := range popular { - delete(b2j, s) - } - } - m.bPopular = popular - m.b2j = b2j -} - -func (m *SequenceMatcher) isBJunk(s string) bool { - _, ok := m.bJunk[s] - return ok -} - -// Find longest matching block in a[alo:ahi] and b[blo:bhi]. -// -// If IsJunk is not defined: -// -// Return (i,j,k) such that a[i:i+k] is equal to b[j:j+k], where -// -// alo <= i <= i+k <= ahi -// blo <= j <= j+k <= bhi -// -// and for all (i',j',k') meeting those conditions, -// -// k >= k' -// i <= i' -// and if i == i', j <= j' -// -// In other words, of all maximal matching blocks, return one that -// starts earliest in a, and of all those maximal matching blocks that -// start earliest in a, return the one that starts earliest in b. -// -// If IsJunk is defined, first the longest matching block is -// determined as above, but with the additional restriction that no -// junk element appears in the block. Then that block is extended as -// far as possible by matching (only) junk elements on both sides. So -// the resulting block never matches on junk except as identical junk -// happens to be adjacent to an "interesting" match. -// -// If no blocks match, return (alo, blo, 0). -func (m *SequenceMatcher) findLongestMatch(alo, ahi, blo, bhi int) Match { - // CAUTION: stripping common prefix or suffix would be incorrect. - // E.g., - // ab - // acab - // Longest matching block is "ab", but if common prefix is - // stripped, it's "a" (tied with "b"). UNIX(tm) diff does so - // strip, so ends up claiming that ab is changed to acab by - // inserting "ca" in the middle. That's minimal but unintuitive: - // "it's obvious" that someone inserted "ac" at the front. - // Windiff ends up at the same place as diff, but by pairing up - // the unique 'b's and then matching the first two 'a's. - besti, bestj, bestsize := alo, blo, 0 - - // find longest junk-free match - // during an iteration of the loop, j2len[j] = length of longest - // junk-free match ending with a[i-1] and b[j] - j2len := map[int]int{} - for i := alo; i != ahi; i++ { - // look at all instances of a[i] in b; note that because - // b2j has no junk keys, the loop is skipped if a[i] is junk - newj2len := map[int]int{} - for _, j := range m.b2j[m.a[i]] { - // a[i] matches b[j] - if j < blo { - continue - } - if j >= bhi { - break - } - k := j2len[j-1] + 1 - newj2len[j] = k - if k > bestsize { - besti, bestj, bestsize = i-k+1, j-k+1, k - } - } - j2len = newj2len - } - - // Extend the best by non-junk elements on each end. In particular, - // "popular" non-junk elements aren't in b2j, which greatly speeds - // the inner loop above, but also means "the best" match so far - // doesn't contain any junk *or* popular non-junk elements. - for besti > alo && bestj > blo && !m.isBJunk(m.b[bestj-1]) && - m.a[besti-1] == m.b[bestj-1] { - besti, bestj, bestsize = besti-1, bestj-1, bestsize+1 - } - for besti+bestsize < ahi && bestj+bestsize < bhi && - !m.isBJunk(m.b[bestj+bestsize]) && - m.a[besti+bestsize] == m.b[bestj+bestsize] { - bestsize += 1 - } - - // Now that we have a wholly interesting match (albeit possibly - // empty!), we may as well suck up the matching junk on each - // side of it too. Can't think of a good reason not to, and it - // saves post-processing the (possibly considerable) expense of - // figuring out what to do with it. In the case of an empty - // interesting match, this is clearly the right thing to do, - // because no other kind of match is possible in the regions. - for besti > alo && bestj > blo && m.isBJunk(m.b[bestj-1]) && - m.a[besti-1] == m.b[bestj-1] { - besti, bestj, bestsize = besti-1, bestj-1, bestsize+1 - } - for besti+bestsize < ahi && bestj+bestsize < bhi && - m.isBJunk(m.b[bestj+bestsize]) && - m.a[besti+bestsize] == m.b[bestj+bestsize] { - bestsize += 1 - } - - return Match{A: besti, B: bestj, Size: bestsize} -} - -// Return list of triples describing matching subsequences. +// GetMatchingBlocks return the list of triples describing matching subsequences. // // Each triple is of the form (i, j, n), and means that // a[i:i+n] == b[j:j+n]. The triples are monotonically increasing in @@ -346,7 +187,7 @@ func (m *SequenceMatcher) GetMatchingBlocks() []Match { return m.matchingBlocks } -// Return list of 5-tuples describing how to turn a into b. +// GetOpCodes return the list of 5-tuples describing how to turn a into b. // // Each tuple is of the form (tag, i1, i2, j1, j2). The first tuple // has i1 == j1 == 0, and remaining tuples have i1 == the i2 from the @@ -360,7 +201,7 @@ func (m *SequenceMatcher) GetMatchingBlocks() []Match { // // 'i' (insert): b[j1:j2] should be inserted at a[i1:i1], i1==i2 in this case. // -// 'e' (equal): a[i1:i2] == b[j1:j2] +// 'e' (equal): a[i1:i2] == b[j1:j2]. func (m *SequenceMatcher) GetOpCodes() []OpCode { if m.opCodes != nil { return m.opCodes @@ -376,13 +217,15 @@ func (m *SequenceMatcher) GetOpCodes() []OpCode { // the matching block, and move (i,j) beyond the match ai, bj, size := m.A, m.B, m.Size tag := byte(0) - if i < ai && j < bj { + switch { + case i < ai && j < bj: tag = 'r' - } else if i < ai { + case i < ai: tag = 'd' - } else if j < bj { + case j < bj: tag = 'i' } + if tag > 0 { opCodes = append(opCodes, OpCode{tag, i, ai, j, bj}) } @@ -397,7 +240,7 @@ func (m *SequenceMatcher) GetOpCodes() []OpCode { return m.opCodes } -// Isolate change clusters by eliminating ranges with no changes. +// GetGroupedOpCodes isolates change clusters by eliminating ranges with no changes. // // Return a generator of groups with up to n lines of context. // Each group is in the same format as returned by GetOpCodes(). @@ -407,7 +250,7 @@ func (m *SequenceMatcher) GetGroupedOpCodes(n int) [][]OpCode { } codes := m.GetOpCodes() if len(codes) == 0 { - codes = []OpCode{OpCode{'e', 0, 1, 0, 1}} + codes = []OpCode{{'e', 0, 1, 0, 1}} } // Fixup leading and trailing groups if they show no changes. if codes[0].Tag == 'e' { @@ -436,27 +279,177 @@ func (m *SequenceMatcher) GetGroupedOpCodes(n int) [][]OpCode { } group = append(group, OpCode{c.Tag, i1, i2, j1, j2}) } - if len(group) > 0 && !(len(group) == 1 && group[0].Tag == 'e') { + if len(group) > 0 && (len(group) != 1 || group[0].Tag != 'e') { groups = append(groups, group) } return groups } -// Convert range to the "ed" format +func (m *SequenceMatcher) chainB() { + // Populate line -> index mapping + b2j := map[string][]int{} + for i, s := range m.b { + indices := b2j[s] + indices = append(indices, i) + b2j[s] = indices + } + + // Purge junk elements + m.bJunk = map[string]struct{}{} + if m.IsJunk != nil { + junk := m.bJunk + for s := range b2j { + if m.IsJunk(s) { + junk[s] = struct{}{} + } + } + for s := range junk { + delete(b2j, s) + } + } + + // Purge remaining popular elements + const ( + hundred = 100 + maxDisplayElements = 2 * hundred + ) + popular := map[string]struct{}{} + n := len(m.b) + if m.autoJunk && n >= maxDisplayElements { + ntest := n/hundred + 1 + for s, indices := range b2j { + if len(indices) > ntest { + popular[s] = struct{}{} + } + } + for s := range popular { + delete(b2j, s) + } + } + m.bPopular = popular + m.b2j = b2j +} + +func (m *SequenceMatcher) isBJunk(s string) bool { + _, ok := m.bJunk[s] + return ok +} + +// Find longest matching block in a[alo:ahi] and b[blo:bhi]. +// +// If IsJunk is not defined: +// +// Return (i,j,k) such that a[i:i+k] is equal to b[j:j+k], where +// +// alo <= i <= i+k <= ahi +// blo <= j <= j+k <= bhi +// +// and for all (i',j',k') meeting those conditions, +// +// k >= k' +// i <= i' +// and if i == i', j <= j' +// +// In other words, of all maximal matching blocks, return one that +// starts earliest in a, and of all those maximal matching blocks that +// start earliest in a, return the one that starts earliest in b. +// +// If IsJunk is defined, first the longest matching block is +// determined as above, but with the additional restriction that no +// junk element appears in the block. Then that block is extended as +// far as possible by matching (only) junk elements on both sides. So +// the resulting block never matches on junk except as identical junk +// happens to be adjacent to an "interesting" match. +// +// If no blocks match, return (alo, blo, 0). +func (m *SequenceMatcher) findLongestMatch(alo, ahi, blo, bhi int) Match { + // CAUTION: stripping common prefix or suffix would be incorrect. + // E.g., + // ab + // acab + // Longest matching block is "ab", but if common prefix is + // stripped, it's "a" (tied with "b"). UNIX(tm) diff does so + // strip, so ends up claiming that ab is changed to acab by + // inserting "ca" in the middle. That's minimal but unintuitive: + // "it's obvious" that someone inserted "ac" at the front. + // Windiff ends up at the same place as diff, but by pairing up + // the unique 'b's and then matching the first two 'a's. + besti, bestj, bestsize := alo, blo, 0 + + // find longest junk-free match + // during an iteration of the loop, j2len[j] = length of longest + // junk-free match ending with a[i-1] and b[j] + j2len := map[int]int{} + for i := alo; i != ahi; i++ { + // look at all instances of a[i] in b; note that because + // b2j has no junk keys, the loop is skipped if a[i] is junk + newj2len := map[int]int{} + for _, j := range m.b2j[m.a[i]] { + // a[i] matches b[j] + if j < blo { + continue + } + if j >= bhi { + break + } + k := j2len[j-1] + 1 + newj2len[j] = k + if k > bestsize { + besti, bestj, bestsize = i-k+1, j-k+1, k + } + } + j2len = newj2len + } + + // Extend the best by non-junk elements on each end. In particular, + // "popular" non-junk elements aren't in b2j, which greatly speeds + // the inner loop above, but also means "the best" match so far + // doesn't contain any junk *or* popular non-junk elements. + for besti > alo && bestj > blo && !m.isBJunk(m.b[bestj-1]) && + m.a[besti-1] == m.b[bestj-1] { + besti, bestj, bestsize = besti-1, bestj-1, bestsize+1 + } + for besti+bestsize < ahi && bestj+bestsize < bhi && + !m.isBJunk(m.b[bestj+bestsize]) && + m.a[besti+bestsize] == m.b[bestj+bestsize] { + bestsize++ + } + + // Now that we have a wholly interesting match (albeit possibly + // empty!), we may as well suck up the matching junk on each + // side of it too. Can't think of a good reason not to, and it + // saves post-processing the (possibly considerable) expense of + // figuring out what to do with it. In the case of an empty + // interesting match, this is clearly the right thing to do, + // because no other kind of match is possible in the regions. + for besti > alo && bestj > blo && m.isBJunk(m.b[bestj-1]) && + m.a[besti-1] == m.b[bestj-1] { + besti, bestj, bestsize = besti-1, bestj-1, bestsize+1 + } + for besti+bestsize < ahi && bestj+bestsize < bhi && + m.isBJunk(m.b[bestj+bestsize]) && + m.a[besti+bestsize] == m.b[bestj+bestsize] { + bestsize++ + } + + return Match{A: besti, B: bestj, Size: bestsize} +} + +// Convert range to the "ed" format. func formatRangeUnified(start, stop int) string { // Per the diff spec at http://www.unix.org/single_unix_specification/ beginning := start + 1 // lines start numbering with one length := stop - start if length == 1 { - return fmt.Sprintf("%d", beginning) + return strconv.Itoa(beginning) } if length == 0 { - beginning -= 1 // empty ranges begin at line just before the range + beginning-- // empty ranges begin at line just before the range } return fmt.Sprintf("%d,%d", beginning, length) } -// Unified diff parameters +// UnifiedDiff holds the unified diff parameters. type UnifiedDiff struct { A []string // First sequence lines FromFile string // First file name @@ -468,7 +461,11 @@ type UnifiedDiff struct { Context int // Number of context lines } -// Compare two sequences of lines; generate the delta as a unified diff. +type formatter func(format string, args ...any) error +type printer func(string) error + +// WriteUnifiedDiff write the comparison between two sequences of lines. +// It generates the delta as a unified diff. // // Unified diffs are a compact way of showing line changes and a few // lines of context. The number of context lines is set by 'n' which @@ -484,16 +481,17 @@ type UnifiedDiff struct { // argument to "" so that the output will be uniformly newline free. // // The unidiff format normally has a header for filenames and modification -// times. Any or all of these may be specified using strings for // 'fromfile', 'tofile', 'fromfiledate', and 'tofiledate'. // The modification times are normally expressed in the ISO 8601 format. func WriteUnifiedDiff(writer io.Writer, diff UnifiedDiff) error { buf := bufio.NewWriter(writer) defer buf.Flush() - wf := func(format string, args ...interface{}) error { - _, err := buf.WriteString(fmt.Sprintf(format, args...)) + + wf := func(format string, args ...any) error { + _, err := fmt.Fprintf(buf, format, args...) return err } + ws := func(s string) error { _, err := buf.WriteString(s) return err @@ -503,70 +501,130 @@ func WriteUnifiedDiff(writer io.Writer, diff UnifiedDiff) error { diff.Eol = "\n" } - started := false m := NewMatcher(diff.A, diff.B) - for _, g := range m.GetGroupedOpCodes(diff.Context) { - if !started { - started = true - fromDate := "" - if len(diff.FromDate) > 0 { - fromDate = "\t" + diff.FromDate - } - toDate := "" - if len(diff.ToDate) > 0 { - toDate = "\t" + diff.ToDate - } - if diff.FromFile != "" || diff.ToFile != "" { - err := wf("--- %s%s%s", diff.FromFile, fromDate, diff.Eol) - if err != nil { - return err - } - err = wf("+++ %s%s%s", diff.ToFile, toDate, diff.Eol) - if err != nil { - return err - } - } + groups := m.GetGroupedOpCodes(diff.Context) + + if len(groups) == 0 { + return nil + } + + if err := writeFirstGroup(groups[0], diff, wf, ws); err != nil { + return err + } + + for _, g := range groups[1:] { + if err := writeGroup(g, diff, wf, ws); err != nil { + return err + } + } + + return nil +} + +func writeFirstGroup(g []OpCode, diff UnifiedDiff, wf formatter, ws printer) error { + fromDate := "" + if len(diff.FromDate) > 0 { + fromDate = "\t" + diff.FromDate + } + + toDate := "" + if len(diff.ToDate) > 0 { + toDate = "\t" + diff.ToDate + } + + if diff.FromFile != "" || diff.ToFile != "" { + err := wf("--- %s%s%s", diff.FromFile, fromDate, diff.Eol) + if err != nil { + return err } - first, last := g[0], g[len(g)-1] - range1 := formatRangeUnified(first.I1, last.I2) - range2 := formatRangeUnified(first.J1, last.J2) - if err := wf("@@ -%s +%s @@%s", range1, range2, diff.Eol); err != nil { + err = wf("+++ %s%s%s", diff.ToFile, toDate, diff.Eol) + if err != nil { return err } - for _, c := range g { - i1, i2, j1, j2 := c.I1, c.I2, c.J1, c.J2 - if c.Tag == 'e' { - for _, line := range diff.A[i1:i2] { - if err := ws(" " + line); err != nil { - return err - } - } - continue + } + + return writeGroup(g, diff, wf, ws) +} + +func writeGroup(group []OpCode, diff UnifiedDiff, wf formatter, ws printer) error { + first, last := group[0], group[len(group)-1] + range1 := formatRangeUnified(first.I1, last.I2) + range2 := formatRangeUnified(first.J1, last.J2) + if err := wf("@@ -%s +%s @@%s", range1, range2, diff.Eol); err != nil { + return err + } + + for _, c := range group { + // 'r' (replace) + // 'd' (delete) + // 'i' (insert) + // 'e' (equal) + if c.Tag != 'r' && c.Tag != 'e' && c.Tag != 'd' && c.Tag != 'i' { + continue + } + + i1, i2, j1, j2 := c.I1, c.I2, c.J1, c.J2 + switch c.Tag { + case 'e': + if err := writeEqual(diff.A[i1:i2], ws); err != nil { + return err + } + case 'd': + if err := writeReplaceOrDelete(diff.A[i1:i2], ws); err != nil { + return err } - if c.Tag == 'r' || c.Tag == 'd' { - for _, line := range diff.A[i1:i2] { - if err := ws("-" + line); err != nil { - return err - } - } + case 'i': + if err := writeReplaceOrInsert(diff.B[j1:j2], ws); err != nil { + return err } - if c.Tag == 'r' || c.Tag == 'i' { - for _, line := range diff.B[j1:j2] { - if err := ws("+" + line); err != nil { - return err - } - } + case 'r': + if err := writeReplaceOrDelete(diff.A[i1:i2], ws); err != nil { + return err + } + if err := writeReplaceOrInsert(diff.B[j1:j2], ws); err != nil { + return err } } } + + return nil +} + +func writeEqual(lines []string, ws printer) error { + for _, line := range lines { + if err := ws(" " + line); err != nil { + return err + } + } + + return nil +} + +func writeReplaceOrDelete(lines []string, ws printer) error { + for _, line := range lines { + if err := ws("-" + line); err != nil { + return err + } + } + + return nil +} + +func writeReplaceOrInsert(lines []string, ws printer) error { + for _, line := range lines { + if err := ws("+" + line); err != nil { + return err + } + } + return nil } -// Like WriteUnifiedDiff but returns the diff a string. +// GetUnifiedDiffString is like WriteUnifiedDiff but returns the diff a string. func GetUnifiedDiffString(diff UnifiedDiff) (string, error) { - w := &bytes.Buffer{} + w := new(bytes.Buffer) err := WriteUnifiedDiff(w, diff) - return string(w.Bytes()), err + return w.String(), err } // Convert range to the "ed" format. @@ -575,15 +633,15 @@ func formatRangeContext(start, stop int) string { beginning := start + 1 // lines start numbering with one length := stop - start if length == 0 { - beginning -= 1 // empty ranges begin at line just before the range + beginning-- // empty ranges begin at line just before the range } if length <= 1 { - return fmt.Sprintf("%d", beginning) + return strconv.Itoa(beginning) } return fmt.Sprintf("%d,%d", beginning, beginning+length-1) } -// Split a string on "\n" while preserving them. The output can be used +// SplitLines splits a string on "\n" while preserving them. The output can be used // as input for UnifiedDiff and ContextDiff structures. func SplitLines(s string) []string { lines := strings.SplitAfter(s, "\n") diff --git a/internal/difflib/difflib_test.go b/internal/difflib/difflib_test.go index 21d37cb42..20b0fc36d 100644 --- a/internal/difflib/difflib_test.go +++ b/internal/difflib/difflib_test.go @@ -3,19 +3,24 @@ package difflib import ( "bytes" "fmt" - "math" "reflect" "strings" "testing" ) +/* func assertAlmostEqual(t *testing.T, a, b float64, places int) { + t.Helper() + if math.Abs(a-b) > math.Pow10(-places) { t.Errorf("%.7f != %.7f", a, b) } } +*/ + +func assertEqual(t *testing.T, a, b any) { + t.Helper() -func assertEqual(t *testing.T, a, b interface{}) { if !reflect.DeepEqual(a, b) { t.Errorf("%v != %v", a, b) } @@ -24,9 +29,10 @@ func assertEqual(t *testing.T, a, b interface{}) { func splitChars(s string) []string { chars := make([]string, 0, len(s)) // Assume ASCII inputs - for i := 0; i != len(s); i++ { - chars = append(chars, string(s[i])) + for _, r := range s { + chars = append(chars, string(r)) } + return chars } @@ -39,7 +45,7 @@ func TestGetOptCodes(t *testing.T) { fmt.Fprintf(w, "%s a[%d:%d], (%s) b[%d:%d] (%s)\n", string(op.Tag), op.I1, op.I2, a[op.I1:op.I2], op.J1, op.J2, b[op.J1:op.J2]) } - result := string(w.Bytes()) + result := w.String() expected := `d a[0:1], (q) b[0:0] () e a[1:3], (ab) b[0:2] (ab) r a[3:4], (x) b[2:3] (y) @@ -74,7 +80,7 @@ func TestGroupedOpCodes(t *testing.T) { op.I1, op.I2, op.J1, op.J2) } } - result := string(w.Bytes()) + result := w.String() expected := `group e, 5, 8, 5, 8 i, 8, 8, 8, 9 @@ -186,7 +192,8 @@ func TestOutputFormatTabDelimiter(t *testing.T) { } ud, err := GetUnifiedDiffString(diff) assertEqual(t, err, nil) - assertEqual(t, SplitLines(ud)[:2], []string{ + result := SplitLines(ud)[:2] + assertEqual(t, result, []string{ "--- Original\t2005-01-26 23:30:50\n", "+++ Current\t2010-04-12 10:20:52\n", }) @@ -238,21 +245,23 @@ func TestSplitLines(t *testing.T) { } } -func benchmarkSplitLines(b *testing.B, count int) { - str := strings.Repeat("foo\n", count) +func benchmarkSplitLines(count int) func(*testing.B) { + return func(b *testing.B) { + str := strings.Repeat("foo\n", count) - b.ResetTimer() + b.ResetTimer() - n := 0 - for i := 0; i < b.N; i++ { - n += len(SplitLines(str)) + n := 0 + for b.Loop() { + n += len(SplitLines(str)) + } } } func BenchmarkSplitLines100(b *testing.B) { - benchmarkSplitLines(b, 100) + b.Run("splitLines", benchmarkSplitLines(100)) } func BenchmarkSplitLines10000(b *testing.B) { - benchmarkSplitLines(b, 10000) + b.Run("splitLines", benchmarkSplitLines(10000)) } diff --git a/internal/spew/bypass.go b/internal/spew/bypass.go index 792994785..e566ab6cb 100644 --- a/internal/spew/bypass.go +++ b/internal/spew/bypass.go @@ -18,7 +18,8 @@ // 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. -// +build !js,!appengine,!safe,!disableunsafe,go1.4 + +//go:build !js && !appengine && !safe && !disableunsafe && go1.4 package spew @@ -33,7 +34,7 @@ const ( UnsafeDisabled = false // ptrSize is the size of a pointer on the current arch. - ptrSize = unsafe.Sizeof((*byte)(nil)) + // ptrSize = unsafe.Sizeof((*byte)(nil)). ) type flag uintptr @@ -110,12 +111,14 @@ func init() { 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 + // a will have flagStickyRO set. a t0 } vA := reflect.ValueOf(t).FieldByName("A") diff --git a/internal/spew/bypasssafe.go b/internal/spew/bypasssafe.go index 205c28d68..b54e4d140 100644 --- a/internal/spew/bypasssafe.go +++ b/internal/spew/bypasssafe.go @@ -16,7 +16,8 @@ // 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. -// +build js appengine safe disableunsafe !go1.4 + +//go:build js || appengine || safe || disableunsafe || !go1.4 package spew diff --git a/internal/spew/common.go b/internal/spew/common.go index 1be8ce945..758210932 100644 --- a/internal/spew/common.go +++ b/internal/spew/common.go @@ -70,6 +70,7 @@ var hexDigits = "0123456789abcdef" // catchPanic handles any panics that might occur during the handleMethods // calls. func catchPanic(w io.Writer, v reflect.Value) { + _ = v // currently we do not render the panic value if err := recover(); err != nil { w.Write(panicBytes) fmt.Fprintf(w, "%v", err) @@ -150,7 +151,7 @@ func printBool(w io.Writer, val bool) { } // printInt outputs a signed integer value to Writer w. -func printInt(w io.Writer, val int64, base int) { +func printInt(w io.Writer, val int64, base int) { //nolint: unparam // we leave base even though it is alway 10, to remain consistent with FormatInt w.Write([]byte(strconv.FormatInt(val, base))) } @@ -270,8 +271,9 @@ func canSortSimply(kind reflect.Kind) bool { return true case reflect.Array: return true + default: + return false } - return false } // Len returns the number of values in the slice. It is part of the @@ -309,7 +311,7 @@ func valueSortLess(a, b reflect.Value) bool { case reflect.Array: // Compare the contents of both arrays. l := a.Len() - for i := 0; i < l; i++ { + for i := range l { av := a.Index(i) bv := b.Index(i) if av.Interface() == bv.Interface() { @@ -317,8 +319,10 @@ func valueSortLess(a, b reflect.Value) bool { } return valueSortLess(av, bv) } + fallthrough + default: + return a.String() < b.String() } - return a.String() < b.String() } // Less returns whether the value at index i should sort before the diff --git a/internal/spew/common_test.go b/internal/spew/common_test.go index a6c07e1ad..718d00250 100644 --- a/internal/spew/common_test.go +++ b/internal/spew/common_test.go @@ -19,6 +19,8 @@ package spew_test import ( "fmt" "reflect" + "slices" + "strings" "testing" "github.com/go-openapi/testify/v2/internal/spew" @@ -71,11 +73,12 @@ type embed struct { // embedwrap is used to test embedded structures. type embedwrap struct { *embed + e *embed } // panicer is used to intentionally cause a panic for testing spew properly -// handles them +// handles them. type panicer int func (p panicer) String() string { @@ -93,25 +96,22 @@ func (e customError) Error() string { // for a test error message. func stringizeWants(wants []string) string { s := "" + var sSb97 strings.Builder for i, want := range wants { if i > 0 { - s += fmt.Sprintf("want%d: %s", i+1, want) + sSb97.WriteString(fmt.Sprintf("want%d: %s", i+1, want)) } else { - s += "want: " + want + sSb97.WriteString("want: " + want) } } + s += sSb97.String() return s } // testFailed returns whether or not a test failed by checking if the result // of the test is in the slice of wanted strings. func testFailed(result string, wants []string) bool { - for _, want := range wants { - if result == want { - return false - } - } - return true + return !slices.Contains(wants, result) } type sortableStruct struct { @@ -131,9 +131,11 @@ type sortTestCase struct { expected []reflect.Value } -func helpTestSortValues(tests []sortTestCase, cs *spew.ConfigState, t *testing.T) { - getInterfaces := func(values []reflect.Value) []interface{} { - interfaces := []interface{}{} +func helpTestSortValues(t *testing.T, tests []sortTestCase, cs *spew.ConfigState) { + t.Helper() + + getInterfaces := func(values []reflect.Value) []any { + interfaces := []any{} for _, v := range values { interfaces = append(interfaces, v.Interface()) } @@ -225,7 +227,7 @@ func TestSortValues(t *testing.T) { }, } cs := spew.ConfigState{DisableMethods: true, SpewKeys: false} - helpTestSortValues(tests, &cs, t) + helpTestSortValues(t, tests, &cs) } // TestSortValuesWithMethods ensures the sort functionality for relect.Value @@ -260,7 +262,7 @@ func TestSortValuesWithMethods(t *testing.T) { }, } cs := spew.ConfigState{DisableMethods: false, SpewKeys: false} - helpTestSortValues(tests, &cs, t) + helpTestSortValues(t, tests, &cs) } // TestSortValuesWithSpew ensures the sort functionality for relect.Value @@ -294,5 +296,5 @@ func TestSortValuesWithSpew(t *testing.T) { }, } cs := spew.ConfigState{DisableMethods: true, SpewKeys: true} - helpTestSortValues(tests, &cs, t) + helpTestSortValues(t, tests, &cs) } diff --git a/internal/spew/config.go b/internal/spew/config.go index 2e3d22f31..c76a91035 100644 --- a/internal/spew/config.go +++ b/internal/spew/config.go @@ -112,7 +112,7 @@ var Config = ConfigState{Indent: " "} // This function is shorthand for the following syntax: // // fmt.Errorf(format, c.NewFormatter(a), c.NewFormatter(b)) -func (c *ConfigState) Errorf(format string, a ...interface{}) (err error) { +func (c *ConfigState) Errorf(format string, a ...any) (err error) { return fmt.Errorf(format, c.convertArgs(a)...) } @@ -124,7 +124,7 @@ func (c *ConfigState) Errorf(format string, a ...interface{}) (err error) { // This function is shorthand for the following syntax: // // fmt.Fprint(w, c.NewFormatter(a), c.NewFormatter(b)) -func (c *ConfigState) Fprint(w io.Writer, a ...interface{}) (n int, err error) { +func (c *ConfigState) Fprint(w io.Writer, a ...any) (n int, err error) { return fmt.Fprint(w, c.convertArgs(a)...) } @@ -136,7 +136,7 @@ func (c *ConfigState) Fprint(w io.Writer, a ...interface{}) (n int, err error) { // This function is shorthand for the following syntax: // // fmt.Fprintf(w, format, c.NewFormatter(a), c.NewFormatter(b)) -func (c *ConfigState) Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) { +func (c *ConfigState) Fprintf(w io.Writer, format string, a ...any) (n int, err error) { return fmt.Fprintf(w, format, c.convertArgs(a)...) } @@ -147,7 +147,7 @@ func (c *ConfigState) Fprintf(w io.Writer, format string, a ...interface{}) (n i // This function is shorthand for the following syntax: // // fmt.Fprintln(w, c.NewFormatter(a), c.NewFormatter(b)) -func (c *ConfigState) Fprintln(w io.Writer, a ...interface{}) (n int, err error) { +func (c *ConfigState) Fprintln(w io.Writer, a ...any) (n int, err error) { return fmt.Fprintln(w, c.convertArgs(a)...) } @@ -159,7 +159,7 @@ func (c *ConfigState) Fprintln(w io.Writer, a ...interface{}) (n int, err error) // This function is shorthand for the following syntax: // // fmt.Print(c.NewFormatter(a), c.NewFormatter(b)) -func (c *ConfigState) Print(a ...interface{}) (n int, err error) { +func (c *ConfigState) Print(a ...any) (n int, err error) { return fmt.Print(c.convertArgs(a)...) } @@ -171,7 +171,7 @@ func (c *ConfigState) Print(a ...interface{}) (n int, err error) { // This function is shorthand for the following syntax: // // fmt.Printf(format, c.NewFormatter(a), c.NewFormatter(b)) -func (c *ConfigState) Printf(format string, a ...interface{}) (n int, err error) { +func (c *ConfigState) Printf(format string, a ...any) (n int, err error) { return fmt.Printf(format, c.convertArgs(a)...) } @@ -183,7 +183,7 @@ func (c *ConfigState) Printf(format string, a ...interface{}) (n int, err error) // This function is shorthand for the following syntax: // // fmt.Println(c.NewFormatter(a), c.NewFormatter(b)) -func (c *ConfigState) Println(a ...interface{}) (n int, err error) { +func (c *ConfigState) Println(a ...any) (n int, err error) { return fmt.Println(c.convertArgs(a)...) } @@ -194,7 +194,7 @@ func (c *ConfigState) Println(a ...interface{}) (n int, err error) { // This function is shorthand for the following syntax: // // fmt.Sprint(c.NewFormatter(a), c.NewFormatter(b)) -func (c *ConfigState) Sprint(a ...interface{}) string { +func (c *ConfigState) Sprint(a ...any) string { return fmt.Sprint(c.convertArgs(a)...) } @@ -205,7 +205,7 @@ func (c *ConfigState) Sprint(a ...interface{}) string { // This function is shorthand for the following syntax: // // fmt.Sprintf(format, c.NewFormatter(a), c.NewFormatter(b)) -func (c *ConfigState) Sprintf(format string, a ...interface{}) string { +func (c *ConfigState) Sprintf(format string, a ...any) string { return fmt.Sprintf(format, c.convertArgs(a)...) } @@ -216,7 +216,7 @@ func (c *ConfigState) Sprintf(format string, a ...interface{}) string { // This function is shorthand for the following syntax: // // fmt.Sprintln(c.NewFormatter(a), c.NewFormatter(b)) -func (c *ConfigState) Sprintln(a ...interface{}) string { +func (c *ConfigState) Sprintln(a ...any) string { return fmt.Sprintln(c.convertArgs(a)...) } @@ -228,22 +228,23 @@ types similar to the standard %v format specifier. The custom formatter only responds to the %v (most compact), %+v (adds pointer addresses), %#v (adds types), and %#+v (adds types and pointer addresses) verb -combinations. Any other verbs such as %x and %q will be sent to the the -standard fmt package for formatting. In addition, the custom formatter ignores -the width and precision arguments (however they will still work on the format -specifiers not handled by the custom formatter). +combinations. + +Any other verbs such as %x and %q will be sent to the standard fmt package for formatting. +In addition, the custom formatter ignores the width and precision arguments +(however they will still work on the format specifiers not handled by the custom formatter). Typically this function shouldn't be called directly. It is much easier to make use of the custom formatter by calling one of the convenience functions such as c.Printf, c.Println, or c.Printf. */ -func (c *ConfigState) NewFormatter(v interface{}) fmt.Formatter { +func (c *ConfigState) NewFormatter(v any) fmt.Formatter { return newFormatter(c, v) } // Fdump formats and displays the passed arguments to io.Writer w. It formats // exactly the same as Dump. -func (c *ConfigState) Fdump(w io.Writer, a ...interface{}) { +func (c *ConfigState) Fdump(w io.Writer, a ...any) { fdump(c, w, a...) } @@ -254,15 +255,15 @@ pointer addresses used to indirect to the final value. It provides the following features over the built-in printing facilities provided by the fmt package: - * Pointers are dereferenced and followed - * Circular data structures are detected and handled properly - * Custom Stringer/error interfaces are optionally invoked, including - on unexported types - * Custom types which only implement the Stringer/error interfaces via - a pointer receiver are optionally invoked when passing non-pointer - variables - * Byte arrays and slices are dumped like the hexdump -C command which - includes offsets, byte values in hex, and ASCII output + - Pointers are dereferenced and followed + - Circular data structures are detected and handled properly + - Custom Stringer/error interfaces are optionally invoked, including + on unexported types + - Custom types which only implement the Stringer/error interfaces via + a pointer receiver are optionally invoked when passing non-pointer + variables + - Byte arrays and slices are dumped like the hexdump -C command which + includes offsets, byte values in hex, and ASCII output The configuration options are controlled by modifying the public members of c. See ConfigState for options documentation. @@ -270,13 +271,13 @@ of c. See ConfigState for options documentation. See Fdump if you would prefer dumping to an arbitrary io.Writer or Sdump to get the formatted result as a string. */ -func (c *ConfigState) Dump(a ...interface{}) { +func (c *ConfigState) Dump(a ...any) { fdump(c, os.Stdout, a...) } // Sdump returns a string with the passed arguments formatted exactly the same // as Dump. -func (c *ConfigState) Sdump(a ...interface{}) string { +func (c *ConfigState) Sdump(a ...any) string { var buf bytes.Buffer fdump(c, &buf, a...) return buf.String() @@ -285,8 +286,8 @@ func (c *ConfigState) Sdump(a ...interface{}) string { // convertArgs accepts a slice of arguments and returns a slice of the same // length with each argument converted to a spew Formatter interface using // the ConfigState associated with s. -func (c *ConfigState) convertArgs(args []interface{}) (formatters []interface{}) { - formatters = make([]interface{}, len(args)) +func (c *ConfigState) convertArgs(args []any) (formatters []any) { + formatters = make([]any, len(args)) for index, arg := range args { formatters[index] = newFormatter(c, arg) } @@ -295,12 +296,12 @@ func (c *ConfigState) convertArgs(args []interface{}) (formatters []interface{}) // NewDefaultConfig returns a ConfigState with the following default settings. // -// Indent: " " -// MaxDepth: 0 -// DisableMethods: false -// DisablePointerMethods: false -// ContinueOnMethod: false -// SortKeys: false +// Indent: " " +// MaxDepth: 0 +// DisableMethods: false +// DisablePointerMethods: false +// ContinueOnMethod: false +// SortKeys: false func NewDefaultConfig() *ConfigState { return &ConfigState{Indent: " "} } diff --git a/internal/spew/doc.go b/internal/spew/doc.go index aacaac6f1..7490ff22c 100644 --- a/internal/spew/doc.go +++ b/internal/spew/doc.go @@ -21,35 +21,36 @@ debugging. A quick overview of the additional features spew provides over the built-in printing facilities for Go data types are as follows: - * Pointers are dereferenced and followed - * Circular data structures are detected and handled properly - * Custom Stringer/error interfaces are optionally invoked, including - on unexported types - * Custom types which only implement the Stringer/error interfaces via - a pointer receiver are optionally invoked when passing non-pointer - variables - * Byte arrays and slices are dumped like the hexdump -C command which - includes offsets, byte values in hex, and ASCII output (only when using - Dump style) + - Pointers are dereferenced and followed + - Circular data structures are detected and handled properly + - Custom Stringer/error interfaces are optionally invoked, including + on unexported types + - Custom types which only implement the Stringer/error interfaces via + a pointer receiver are optionally invoked when passing non-pointer + variables + - Byte arrays and slices are dumped like the hexdump -C command which + includes offsets, byte values in hex, and ASCII output (only when using + Dump style) There are two different approaches spew allows for dumping Go data structures: - * Dump style which prints with newlines, customizable indentation, - and additional debug information such as types and all pointer addresses - used to indirect to the final value - * A custom Formatter interface that integrates cleanly with the standard fmt - package and replaces %v, %+v, %#v, and %#+v to provide inline printing - similar to the default %v while providing the additional functionality - outlined above and passing unsupported format verbs such as %x and %q - along to fmt + - Dump style which prints with newlines, customizable indentation, + and additional debug information such as types and all pointer addresses + used to indirect to the final value + - A custom Formatter interface that integrates cleanly with the standard fmt + package and replaces %v, %+v, %#v, and %#+v to provide inline printing + similar to the default %v while providing the additional functionality + outlined above and passing unsupported format verbs such as %x and %q + along to fmt -Quick Start +# Quick Start This section demonstrates how to quickly get started with spew. See the sections below for further details on formatting and configuration options. To dump a variable with full newlines, indentation, type, and pointer information use Dump, Fdump, or Sdump: + spew.Dump(myVar1, myVar2, ...) spew.Fdump(someWriter, myVar1, myVar2, ...) str := spew.Sdump(myVar1, myVar2, ...) @@ -58,12 +59,13 @@ Alternatively, if you would prefer to use format strings with a compacted inline printing style, use the convenience wrappers Printf, Fprintf, etc with %v (most compact), %+v (adds pointer addresses), %#v (adds types), or %#+v (adds types and pointer addresses): + spew.Printf("myVar1: %v -- myVar2: %+v", myVar1, myVar2) spew.Printf("myVar3: %#v -- myVar4: %#+v", myVar3, myVar4) spew.Fprintf(someWriter, "myVar1: %v -- myVar2: %+v", myVar1, myVar2) spew.Fprintf(someWriter, "myVar3: %#v -- myVar4: %#+v", myVar3, myVar4) -Configuration Options +# Configuration Options Configuration of spew is handled by fields in the ConfigState type. For convenience, all of the top-level functions use a global state available @@ -74,51 +76,52 @@ equivalent to the top-level functions. This allows concurrent configuration options. See the ConfigState documentation for more details. The following configuration options are available: - * Indent - String to use for each indentation level for Dump functions. - It is a single space by default. A popular alternative is "\t". - - * MaxDepth - Maximum number of levels to descend into nested data structures. - There is no limit by default. - - * DisableMethods - Disables invocation of error and Stringer interface methods. - Method invocation is enabled by default. - - * DisablePointerMethods - Disables invocation of error and Stringer interface methods on types - which only accept pointer receivers from non-pointer variables. - Pointer method invocation is enabled by default. - - * DisablePointerAddresses - DisablePointerAddresses specifies whether to disable the printing of - pointer addresses. This is useful when diffing data structures in tests. - - * DisableCapacities - DisableCapacities specifies whether to disable the printing of - capacities for arrays, slices, maps and channels. This is useful when - diffing data structures in tests. - - * ContinueOnMethod - Enables recursion into types after invoking error and Stringer interface - methods. Recursion after method invocation is disabled by default. - - * SortKeys - Specifies map keys should be sorted before being printed. Use - this to have a more deterministic, diffable output. Note that - only native types (bool, int, uint, floats, uintptr and string) - and types which implement error or Stringer interfaces are - supported with other types sorted according to the - reflect.Value.String() output which guarantees display - stability. Natural map order is used by default. - - * SpewKeys - Specifies that, as a last resort attempt, map keys should be - spewed to strings and sorted by those strings. This is only - considered if SortKeys is true. - -Dump Usage + + - Indent + String to use for each indentation level for Dump functions. + It is a single space by default. A popular alternative is "\t". + + - MaxDepth + Maximum number of levels to descend into nested data structures. + There is no limit by default. + + - DisableMethods + Disables invocation of error and Stringer interface methods. + Method invocation is enabled by default. + + - DisablePointerMethods + Disables invocation of error and Stringer interface methods on types + which only accept pointer receivers from non-pointer variables. + Pointer method invocation is enabled by default. + + - DisablePointerAddresses + specifies whether to disable the printing of + pointer addresses. This is useful when diffing data structures in tests. + + - DisableCapacities + specifies whether to disable the printing of + capacities for arrays, slices, maps and channels. This is useful when + diffing data structures in tests. + + - ContinueOnMethod + Enables recursion into types after invoking error and Stringer interface + methods. Recursion after method invocation is disabled by default. + + - SortKeys + Specifies map keys should be sorted before being printed. Use + this to have a more deterministic, diffable output. Note that + only native types (bool, int, uint, floats, uintptr and string) + and types which implement error or Stringer interfaces are + supported with other types sorted according to the + reflect.Value.String() output which guarantees display + stability. Natural map order is used by default. + + - SpewKeys + Specifies that, as a last resort attempt, map keys should be + spewed to strings and sorted by those strings. This is only + considered if SortKeys is true. + +# Dump Usage Simply call spew.Dump with a list of variables you want to dump: @@ -133,7 +136,7 @@ A third option is to call spew.Sdump to get the formatted output as a string: str := spew.Sdump(myVar1, myVar2, ...) -Sample Dump Output +# Sample Dump Output See the Dump example for details on the setup of the types and variables being shown here. @@ -146,17 +149,17 @@ shown here. ExportedField: (map[interface {}]interface {}) (len=1) { (string) (len=3) "one": (bool) true } - } Byte (and uint8) arrays and slices are displayed uniquely like the hexdump -C command as shown. + ([]uint8) (len=32 cap=32) { 00000000 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 20 |............... | 00000010 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f 30 |!"#$%&'()*+,-./0| 00000020 31 32 |12| } -Custom Formatter +# Custom Formatter Spew provides a custom formatter that implements the fmt.Formatter interface so that it integrates cleanly with standard fmt package printing functions. The @@ -165,12 +168,11 @@ standard %v format specifier. The custom formatter only responds to the %v (most compact), %+v (adds pointer addresses), %#v (adds types), or %#+v (adds types and pointer addresses) verb -combinations. Any other verbs such as %x and %q will be sent to the the -standard fmt package for formatting. In addition, the custom formatter ignores +combinations. Any other verbs such as %x and %q will be sent to the standard fmt package for formatting. In addition, the custom formatter ignores the width and precision arguments (however they will still work on the format specifiers not handled by the custom formatter). -Custom Formatter Usage +# Custom Formatter Usage The simplest way to make use of the spew custom formatter is to call one of the convenience functions such as spew.Printf, spew.Println, or spew.Printf. The @@ -184,15 +186,17 @@ functions have syntax you are most likely already familiar with: See the Index for the full list convenience functions. -Sample Formatter Output +# Sample Formatter Output Double pointer to a uint8: + %v: <**>5 %+v: <**>(0xf8400420d0->0xf8400420c8)5 %#v: (**uint8)5 %#+v: (**uint8)(0xf8400420d0->0xf8400420c8)5 Pointer to circular struct with a uint8 field and a pointer to itself: + %v: <*>{1 <*>} %+v: <*>(0xf84003e260){ui8:1 c:<*>(0xf84003e260)} %#v: (*main.circular){ui8:(uint8)1 c:(*main.circular)} @@ -201,7 +205,7 @@ Pointer to circular struct with a uint8 field and a pointer to itself: See the Printf example for details on the setup of variables being shown here. -Errors +# Errors Since it is possible for custom Stringer/error interfaces to panic, spew detects them and handles them internally by printing the panic information diff --git a/internal/spew/dump.go b/internal/spew/dump.go index f78d89fc1..adc14cfbc 100644 --- a/internal/spew/dump.go +++ b/internal/spew/dump.go @@ -31,7 +31,7 @@ import ( var ( // uint8Type is a reflect.Type representing a uint8. It is used to // convert cgo types to uint8 slices for hexdumping. - uint8Type = reflect.TypeOf(uint8(0)) + uint8Type = reflect.TypeFor[uint8]() //nolint:gochecknoglobals // ok to store reflect stuff as global private immutable vars // cCharRE is a regular expression that matches a cgo char. // It is used to detect character arrays to hexdump them. @@ -215,9 +215,9 @@ func (d *dumpState) dumpSlice(v reflect.Value) { // Convert and copy each element into a uint8 byte // slice. buf = make([]uint8, numEntries) - for i := 0; i < numEntries; i++ { + for i := range numEntries { vv := v.Index(i) - buf[i] = uint8(vv.Convert(uint8Type).Uint()) + buf[i] = uint8(vv.Convert(uint8Type).Uint()) //nolint:gosec // conversion is fine: the original type is uint8 } doHexDump = true } @@ -227,14 +227,14 @@ func (d *dumpState) dumpSlice(v reflect.Value) { if doHexDump { indent := strings.Repeat(d.cs.Indent, d.depth) str := indent + hex.Dump(buf) - str = strings.Replace(str, "\n", "\n"+indent, -1) + str = strings.ReplaceAll(str, "\n", "\n"+indent) str = strings.TrimRight(str, d.cs.Indent) d.w.Write([]byte(str)) return } // Recursively call dump for each item. - for i := 0; i < numEntries; i++ { + for i := range numEntries { d.dump(d.unpackValue(v.Index(i))) if i < (numEntries - 1) { d.w.Write(commaNewlineBytes) @@ -281,7 +281,9 @@ func (d *dumpState) dump(v reflect.Value) { valueLen, valueCap = v.Len(), v.Cap() case reflect.Map, reflect.String: valueLen = v.Len() + default: } + if valueLen != 0 || !d.cs.DisableCapacities && valueCap != 0 { d.w.Write(openParenBytes) if valueLen != 0 { @@ -412,7 +414,7 @@ func (d *dumpState) dump(v reflect.Value) { } else { vt := v.Type() numFields := v.NumField() - for i := 0; i < numFields; i++ { + for i := range numFields { d.indent() vtf := vt.Field(i) d.w.Write([]byte(vtf.Name)) @@ -450,7 +452,7 @@ func (d *dumpState) dump(v reflect.Value) { // fdump is a helper function to consolidate the logic from the various public // methods which take varying writers and config states. -func fdump(cs *ConfigState, w io.Writer, a ...interface{}) { +func fdump(cs *ConfigState, w io.Writer, a ...any) { for _, arg := range a { if arg == nil { w.Write(interfaceBytes) @@ -469,13 +471,13 @@ func fdump(cs *ConfigState, w io.Writer, a ...interface{}) { // Fdump formats and displays the passed arguments to io.Writer w. It formats // exactly the same as Dump. -func Fdump(w io.Writer, a ...interface{}) { +func Fdump(w io.Writer, a ...any) { fdump(&Config, w, a...) } // Sdump returns a string with the passed arguments formatted exactly the same // as Dump. -func Sdump(a ...interface{}) string { +func Sdump(a ...any) string { var buf bytes.Buffer fdump(&Config, &buf, a...) return buf.String() @@ -488,15 +490,15 @@ pointer addresses used to indirect to the final value. It provides the following features over the built-in printing facilities provided by the fmt package: - * Pointers are dereferenced and followed - * Circular data structures are detected and handled properly - * Custom Stringer/error interfaces are optionally invoked, including - on unexported types - * Custom types which only implement the Stringer/error interfaces via - a pointer receiver are optionally invoked when passing non-pointer - variables - * Byte arrays and slices are dumped like the hexdump -C command which - includes offsets, byte values in hex, and ASCII output + - Pointers are dereferenced and followed + - Circular data structures are detected and handled properly + - Custom Stringer/error interfaces are optionally invoked, including + on unexported types + - Custom types which only implement the Stringer/error interfaces via + a pointer receiver are optionally invoked when passing non-pointer + variables + - Byte arrays and slices are dumped like the hexdump -C command which + includes offsets, byte values in hex, and ASCII output The configuration options are controlled by an exported package global, spew.Config. See ConfigState for options documentation. @@ -504,6 +506,6 @@ spew.Config. See ConfigState for options documentation. See Fdump if you would prefer dumping to an arbitrary io.Writer or Sdump to get the formatted result as a string. */ -func Dump(a ...interface{}) { +func Dump(a ...any) { fdump(&Config, os.Stdout, a...) } diff --git a/internal/spew/dump_test.go b/internal/spew/dump_test.go index 5a90151b7..3f0c6439d 100644 --- a/internal/spew/dump_test.go +++ b/internal/spew/dump_test.go @@ -64,6 +64,7 @@ package spew_test import ( "bytes" "fmt" + "strconv" "testing" "unsafe" @@ -72,7 +73,7 @@ import ( // dumpTest is used to describe a test to be performed against the Dump method. type dumpTest struct { - in interface{} + in any wants []string } @@ -80,8 +81,8 @@ type dumpTest struct { var dumpTests = make([]dumpTest, 0) // addDumpTest is a helper method to append the passed input and desired result -// to dumpTests -func addDumpTest(in interface{}, wants ...string) { +// to dumpTests. +func addDumpTest(in any, wants ...string) { test := dumpTest{in, wants} dumpTests = append(dumpTests, test) } @@ -305,8 +306,8 @@ func addComplexDumpTests() { func addArrayDumpTests() { // Array containing standard ints. v := [3]int{1, 2, 3} - vLen := fmt.Sprintf("%d", len(v)) - vCap := fmt.Sprintf("%d", cap(v)) + vLen := strconv.Itoa(len(v)) + vCap := strconv.Itoa(cap(v)) nv := (*[3]int)(nil) pv := &v vAddr := fmt.Sprintf("%p", pv) @@ -324,11 +325,11 @@ func addArrayDumpTests() { v2i1 := pstringer("2") v2i2 := pstringer("3") v2 := [3]pstringer{v2i0, v2i1, v2i2} - v2i0Len := fmt.Sprintf("%d", len(v2i0)) - v2i1Len := fmt.Sprintf("%d", len(v2i1)) - v2i2Len := fmt.Sprintf("%d", len(v2i2)) - v2Len := fmt.Sprintf("%d", len(v2)) - v2Cap := fmt.Sprintf("%d", cap(v2)) + v2i0Len := strconv.Itoa(len(v2i0)) + v2i1Len := strconv.Itoa(len(v2i1)) + v2i2Len := strconv.Itoa(len(v2i2)) + v2Len := strconv.Itoa(len(v2)) + v2Cap := strconv.Itoa(cap(v2)) nv2 := (*[3]pstringer)(nil) pv2 := &v2 v2Addr := fmt.Sprintf("%p", pv2) @@ -352,11 +353,11 @@ func addArrayDumpTests() { // Array containing interfaces. v3i0 := "one" - v3 := [3]interface{}{v3i0, int(2), uint(3)} - v3i0Len := fmt.Sprintf("%d", len(v3i0)) - v3Len := fmt.Sprintf("%d", len(v3)) - v3Cap := fmt.Sprintf("%d", cap(v3)) - nv3 := (*[3]interface{})(nil) + v3 := [3]any{v3i0, int(2), uint(3)} + v3i0Len := strconv.Itoa(len(v3i0)) + v3Len := strconv.Itoa(len(v3)) + v3Cap := strconv.Itoa(cap(v3)) + nv3 := (*[3]any)(nil) pv3 := &v3 v3Addr := fmt.Sprintf("%p", pv3) pv3Addr := fmt.Sprintf("%p", &pv3) @@ -380,8 +381,8 @@ func addArrayDumpTests() { 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30, 0x31, 0x32, } - v4Len := fmt.Sprintf("%d", len(v4)) - v4Cap := fmt.Sprintf("%d", cap(v4)) + v4Len := strconv.Itoa(len(v4)) + v4Cap := strconv.Itoa(cap(v4)) nv4 := (*[34]byte)(nil) pv4 := &v4 v4Addr := fmt.Sprintf("%p", pv4) @@ -403,8 +404,8 @@ func addArrayDumpTests() { func addSliceDumpTests() { // Slice containing standard float32 values. v := []float32{3.14, 6.28, 12.56} - vLen := fmt.Sprintf("%d", len(v)) - vCap := fmt.Sprintf("%d", cap(v)) + vLen := strconv.Itoa(len(v)) + vCap := strconv.Itoa(cap(v)) nv := (*[]float32)(nil) pv := &v vAddr := fmt.Sprintf("%p", pv) @@ -422,11 +423,11 @@ func addSliceDumpTests() { v2i1 := pstringer("2") v2i2 := pstringer("3") v2 := []pstringer{v2i0, v2i1, v2i2} - v2i0Len := fmt.Sprintf("%d", len(v2i0)) - v2i1Len := fmt.Sprintf("%d", len(v2i1)) - v2i2Len := fmt.Sprintf("%d", len(v2i2)) - v2Len := fmt.Sprintf("%d", len(v2)) - v2Cap := fmt.Sprintf("%d", cap(v2)) + v2i0Len := strconv.Itoa(len(v2i0)) + v2i1Len := strconv.Itoa(len(v2i1)) + v2i2Len := strconv.Itoa(len(v2i2)) + v2Len := strconv.Itoa(len(v2)) + v2Cap := strconv.Itoa(cap(v2)) nv2 := (*[]pstringer)(nil) pv2 := &v2 v2Addr := fmt.Sprintf("%p", pv2) @@ -443,11 +444,11 @@ func addSliceDumpTests() { // Slice containing interfaces. v3i0 := "one" - v3 := []interface{}{v3i0, int(2), uint(3), nil} - v3i0Len := fmt.Sprintf("%d", len(v3i0)) - v3Len := fmt.Sprintf("%d", len(v3)) - v3Cap := fmt.Sprintf("%d", cap(v3)) - nv3 := (*[]interface{})(nil) + v3 := []any{v3i0, int(2), uint(3), nil} + v3i0Len := strconv.Itoa(len(v3i0)) + v3Len := strconv.Itoa(len(v3)) + v3Cap := strconv.Itoa(cap(v3)) + nv3 := (*[]any)(nil) pv3 := &v3 v3Addr := fmt.Sprintf("%p", pv3) pv3Addr := fmt.Sprintf("%p", &pv3) @@ -472,8 +473,8 @@ func addSliceDumpTests() { 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30, 0x31, 0x32, } - v4Len := fmt.Sprintf("%d", len(v4)) - v4Cap := fmt.Sprintf("%d", cap(v4)) + v4Len := strconv.Itoa(len(v4)) + v4Cap := strconv.Itoa(cap(v4)) nv4 := (*[]byte)(nil) pv4 := &v4 v4Addr := fmt.Sprintf("%p", pv4) @@ -508,7 +509,7 @@ func addSliceDumpTests() { func addStringDumpTests() { // Standard string. v := "test" - vLen := fmt.Sprintf("%d", len(v)) + vLen := strconv.Itoa(len(v)) nv := (*string)(nil) pv := &v vAddr := fmt.Sprintf("%p", pv) @@ -523,8 +524,8 @@ func addStringDumpTests() { func addInterfaceDumpTests() { // Nil interface. - var v interface{} - nv := (*interface{})(nil) + var v any + nv := (*any)(nil) pv := &v vAddr := fmt.Sprintf("%p", pv) pvAddr := fmt.Sprintf("%p", &pv) @@ -536,7 +537,7 @@ func addInterfaceDumpTests() { addDumpTest(nv, "(*"+vt+")()\n") // Sub-interface. - v2 := interface{}(uint16(65535)) + v2 := any(uint16(65535)) pv2 := &v2 v2Addr := fmt.Sprintf("%p", pv2) pv2Addr := fmt.Sprintf("%p", &pv2) @@ -552,9 +553,9 @@ func addMapDumpTests() { k := "one" kk := "two" m := map[string]int{k: 1, kk: 2} - klen := fmt.Sprintf("%d", len(k)) // not kLen to shut golint up - kkLen := fmt.Sprintf("%d", len(kk)) - mLen := fmt.Sprintf("%d", len(m)) + klen := strconv.Itoa(len(k)) // not kLen to shut golint up + kkLen := strconv.Itoa(len(kk)) + mLen := strconv.Itoa(len(m)) nilMap := map[string]int(nil) nm := (*map[string]int)(nil) pm := &m @@ -581,9 +582,9 @@ func addMapDumpTests() { k2 := pstringer("one") v2 := pstringer("1") m2 := map[pstringer]pstringer{k2: v2} - k2Len := fmt.Sprintf("%d", len(k2)) - v2Len := fmt.Sprintf("%d", len(v2)) - m2Len := fmt.Sprintf("%d", len(m2)) + k2Len := strconv.Itoa(len(k2)) + v2Len := strconv.Itoa(len(v2)) + m2Len := strconv.Itoa(len(m2)) nilMap2 := map[pstringer]pstringer(nil) nm2 := (*map[pstringer]pstringer)(nil) pm2 := &m2 @@ -607,11 +608,11 @@ func addMapDumpTests() { // Map with interface keys and values. k3 := "one" - k3Len := fmt.Sprintf("%d", len(k3)) - m3 := map[interface{}]interface{}{k3: 1} - m3Len := fmt.Sprintf("%d", len(m3)) - nilMap3 := map[interface{}]interface{}(nil) - nm3 := (*map[interface{}]interface{})(nil) + k3Len := strconv.Itoa(len(k3)) + m3 := map[any]any{k3: 1} + m3Len := strconv.Itoa(len(m3)) + nilMap3 := map[any]any(nil) + nm3 := (*map[any]any)(nil) pm3 := &m3 m3Addr := fmt.Sprintf("%p", pm3) pm3Addr := fmt.Sprintf("%p", &pm3) @@ -628,11 +629,11 @@ func addMapDumpTests() { // Map with nil interface value. k4 := "nil" - k4Len := fmt.Sprintf("%d", len(k4)) - m4 := map[string]interface{}{k4: nil} - m4Len := fmt.Sprintf("%d", len(m4)) - nilMap4 := map[string]interface{}(nil) - nm4 := (*map[string]interface{})(nil) + k4Len := strconv.Itoa(len(k4)) + m4 := map[string]any{k4: nil} + m4Len := strconv.Itoa(len(m4)) + nilMap4 := map[string]any(nil) + nm4 := (*map[string]any)(nil) pm4 := &m4 m4Addr := fmt.Sprintf("%p", pm4) pm4Addr := fmt.Sprintf("%p", &pm4) @@ -719,7 +720,7 @@ func addStructDumpTests() { // Struct that contains embedded struct and field to same struct. e := embed{"embedstr"} - eLen := fmt.Sprintf("%d", len("embedstr")) + eLen := strconv.Itoa(len("embedstr")) v4 := embedwrap{embed: &e, e: &e} nv4 := (*embedwrap)(nil) pv4 := &v4 diff --git a/internal/spew/dumpcgo_test.go b/internal/spew/dumpcgo_test.go index f1b38d771..c2bf5ae8a 100644 --- a/internal/spew/dumpcgo_test.go +++ b/internal/spew/dumpcgo_test.go @@ -19,25 +19,26 @@ // does not require cgo to run even though it does handle certain cgo types // specially. Rather than forcing all clients to require cgo and an external // C compiler just to run the tests, this scheme makes them optional. -// +build cgo,testcgo + +//go:build cgo && testcgo package spew_test import ( "fmt" - "github.com/go-openapi/testify/v2/internal/spew/testdata" + "github.com/go-openapi/testify/v2/internal/spew/testsrc" ) func addCgoDumpTests() { // C char pointer. - v := testdata.GetCgoCharPointer() - nv := testdata.GetCgoNullCharPointer() + v := testsrc.GetCgoCharPointer() + nv := testsrc.GetCgoNullCharPointer() pv := &v vcAddr := fmt.Sprintf("%p", v) vAddr := fmt.Sprintf("%p", pv) pvAddr := fmt.Sprintf("%p", &pv) - vt := "*testdata._Ctype_char" + vt := "*testsrc._Ctype_char" vs := "116" addDumpTest(v, "("+vt+")("+vcAddr+")("+vs+")\n") addDumpTest(pv, "(*"+vt+")("+vAddr+"->"+vcAddr+")("+vs+")\n") @@ -45,32 +46,32 @@ func addCgoDumpTests() { addDumpTest(nv, "("+vt+")()\n") // C char array. - v2, v2l, v2c := testdata.GetCgoCharArray() + v2, v2l, v2c := testsrc.GetCgoCharArray() v2Len := fmt.Sprintf("%d", v2l) v2Cap := fmt.Sprintf("%d", v2c) - v2t := "[6]testdata._Ctype_char" + v2t := "[6]testsrc._Ctype_char" v2s := "(len=" + v2Len + " cap=" + v2Cap + ") " + "{\n 00000000 74 65 73 74 32 00 " + " |test2.|\n}" addDumpTest(v2, "("+v2t+") "+v2s+"\n") // C unsigned char array. - v3, v3l, v3c := testdata.GetCgoUnsignedCharArray() + v3, v3l, v3c := testsrc.GetCgoUnsignedCharArray() v3Len := fmt.Sprintf("%d", v3l) v3Cap := fmt.Sprintf("%d", v3c) - v3t := "[6]testdata._Ctype_unsignedchar" - v3t2 := "[6]testdata._Ctype_uchar" + v3t := "[6]testsrc._Ctype_unsignedchar" + v3t2 := "[6]testsrc._Ctype_uchar" v3s := "(len=" + v3Len + " cap=" + v3Cap + ") " + "{\n 00000000 74 65 73 74 33 00 " + " |test3.|\n}" addDumpTest(v3, "("+v3t+") "+v3s+"\n", "("+v3t2+") "+v3s+"\n") // C signed char array. - v4, v4l, v4c := testdata.GetCgoSignedCharArray() + v4, v4l, v4c := testsrc.GetCgoSignedCharArray() v4Len := fmt.Sprintf("%d", v4l) v4Cap := fmt.Sprintf("%d", v4c) - v4t := "[6]testdata._Ctype_schar" - v4t2 := "testdata._Ctype_schar" + v4t := "[6]testsrc._Ctype_schar" + v4t2 := "testsrc._Ctype_schar" v4s := "(len=" + v4Len + " cap=" + v4Cap + ") " + "{\n (" + v4t2 + ") 116,\n (" + v4t2 + ") 101,\n (" + v4t2 + ") 115,\n (" + v4t2 + ") 116,\n (" + v4t2 + ") 52,\n (" + v4t2 + @@ -78,22 +79,22 @@ func addCgoDumpTests() { addDumpTest(v4, "("+v4t+") "+v4s+"\n") // C uint8_t array. - v5, v5l, v5c := testdata.GetCgoUint8tArray() + v5, v5l, v5c := testsrc.GetCgoUint8tArray() v5Len := fmt.Sprintf("%d", v5l) v5Cap := fmt.Sprintf("%d", v5c) - v5t := "[6]testdata._Ctype_uint8_t" - v5t2 := "[6]testdata._Ctype_uchar" + v5t := "[6]testsrc._Ctype_uint8_t" + v5t2 := "[6]testsrc._Ctype_uchar" v5s := "(len=" + v5Len + " cap=" + v5Cap + ") " + "{\n 00000000 74 65 73 74 35 00 " + " |test5.|\n}" addDumpTest(v5, "("+v5t+") "+v5s+"\n", "("+v5t2+") "+v5s+"\n") // C typedefed unsigned char array. - v6, v6l, v6c := testdata.GetCgoTypdefedUnsignedCharArray() + v6, v6l, v6c := testsrc.GetCgoTypdefedUnsignedCharArray() v6Len := fmt.Sprintf("%d", v6l) v6Cap := fmt.Sprintf("%d", v6c) - v6t := "[6]testdata._Ctype_custom_uchar_t" - v6t2 := "[6]testdata._Ctype_uchar" + v6t := "[6]testsrc._Ctype_custom_uchar_t" + v6t2 := "[6]testsrc._Ctype_uchar" v6s := "(len=" + v6Len + " cap=" + v6Cap + ") " + "{\n 00000000 74 65 73 74 36 00 " + " |test6.|\n}" diff --git a/internal/spew/dumpnocgo_test.go b/internal/spew/dumpnocgo_test.go index 52a0971fb..e026d3102 100644 --- a/internal/spew/dumpnocgo_test.go +++ b/internal/spew/dumpnocgo_test.go @@ -16,7 +16,8 @@ // when either cgo is not supported or "-tags testcgo" is not added to the go // test command line. This file intentionally does not setup any cgo tests in // this scenario. -// +build !cgo !testcgo + +//go:build !cgo || !testcgo package spew_test diff --git a/internal/spew/example_test.go b/internal/spew/example_test.go index bbf94e6bc..dc46124ca 100644 --- a/internal/spew/example_test.go +++ b/internal/spew/example_test.go @@ -47,7 +47,7 @@ type Bar struct { type Foo struct { unexportedField Bar - ExportedField map[interface{}]interface{} + ExportedField map[any]any } // This example demonstrates how to use Dump to dump variables to stdout. @@ -85,7 +85,7 @@ func ExampleDump() { // Setup some sample data structures for the example. bar := Bar{uintptr(0)} - s1 := Foo{bar, map[interface{}]interface{}{"one": true}} + s1 := Foo{bar, map[any]any{"one": true}} f := Flag(5) b := []byte{ 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, @@ -160,7 +160,7 @@ func ExampleConfigState() { } // This example demonstrates how to use ConfigState.Dump to dump variables to -// stdout +// stdout. func ExampleConfigState_Dump() { // See the top-level Dump example for details on the types used in this // example. @@ -171,7 +171,7 @@ func ExampleConfigState_Dump() { // Setup some sample data structures for the example. bar := Bar{uintptr(0)} - s1 := Foo{bar, map[interface{}]interface{}{"one": true}} + s1 := Foo{bar, map[any]any{"one": true}} // Dump using the ConfigState instances. scs.Dump(s1) diff --git a/internal/spew/format.go b/internal/spew/format.go index b04edb7d7..c92a38ea9 100644 --- a/internal/spew/format.go +++ b/internal/spew/format.go @@ -32,7 +32,7 @@ const supportedFlags = "0-+# " // be used to get a new Formatter which can be used directly as arguments // in standard fmt package printing calls. type formatState struct { - value interface{} + value any fs fmt.State depth int pointers map[uintptr]int @@ -270,7 +270,7 @@ func (f *formatState) format(v reflect.Value) { f.fs.Write(maxShortBytes) } else { numEntries := v.Len() - for i := 0; i < numEntries; i++ { + for i := range numEntries { if i > 0 { f.fs.Write(spaceBytes) } @@ -333,7 +333,7 @@ func (f *formatState) format(v reflect.Value) { f.fs.Write(maxShortBytes) } else { vt := v.Type() - for i := 0; i < numFields; i++ { + for i := range numFields { if i > 0 { f.fs.Write(spaceBytes) } @@ -391,7 +391,7 @@ func (f *formatState) Format(fs fmt.State, verb rune) { // newFormatter is a helper function to consolidate the logic from the various // public methods which take varying config states. -func newFormatter(cs *ConfigState, v interface{}) fmt.Formatter { +func newFormatter(cs *ConfigState, v any) fmt.Formatter { fs := &formatState{value: v, cs: cs} fs.pointers = make(map[uintptr]int) return fs @@ -405,15 +405,16 @@ types similar to the standard %v format specifier. The custom formatter only responds to the %v (most compact), %+v (adds pointer addresses), %#v (adds types), or %#+v (adds types and pointer addresses) verb -combinations. Any other verbs such as %x and %q will be sent to the the -standard fmt package for formatting. In addition, the custom formatter ignores -the width and precision arguments (however they will still work on the format -specifiers not handled by the custom formatter). +combinations. + +Any other verbs such as %x and %q will be sent to the standard fmt package for formatting. +In addition, the custom formatter ignores the width and precision arguments +(however they will still work on the format specifiers not handled by the custom formatter). Typically this function shouldn't be called directly. It is much easier to make use of the custom formatter by calling one of the convenience functions such as Printf, Println, or Fprintf. */ -func NewFormatter(v interface{}) fmt.Formatter { +func NewFormatter(v any) fmt.Formatter { return newFormatter(&Config, v) } diff --git a/internal/spew/format_test.go b/internal/spew/format_test.go index b47fd2251..98da74afc 100644 --- a/internal/spew/format_test.go +++ b/internal/spew/format_test.go @@ -78,7 +78,7 @@ import ( // formatterTest is used to describe a test to be performed against NewFormatter. type formatterTest struct { format string - in interface{} + in any wants []string } @@ -87,7 +87,7 @@ var formatterTests = make([]formatterTest, 0) // addFormatterTest is a helper method to append the passed input and desired // result to formatterTests. -func addFormatterTest(format string, in interface{}, wants ...string) { +func addFormatterTest(format string, in any, wants ...string) { test := formatterTest{format, in, wants} formatterTests = append(formatterTests, test) } @@ -553,8 +553,8 @@ func addArrayFormatterTests() { addFormatterTest("%#+v", nv2, "(*"+v2t+")"+"") // Array containing interfaces. - v3 := [3]interface{}{"one", int(2), uint(3)} - nv3 := (*[3]interface{})(nil) + v3 := [3]any{"one", int(2), uint(3)} + nv3 := (*[3]any)(nil) pv3 := &v3 v3Addr := fmt.Sprintf("%p", pv3) pv3Addr := fmt.Sprintf("%p", &pv3) @@ -634,8 +634,8 @@ func addSliceFormatterTests() { addFormatterTest("%#+v", nv2, "(*"+v2t+")"+"") // Slice containing interfaces. - v3 := []interface{}{"one", int(2), uint(3), nil} - nv3 := (*[]interface{})(nil) + v3 := []any{"one", int(2), uint(3), nil} + nv3 := (*[]any)(nil) pv3 := &v3 v3Addr := fmt.Sprintf("%p", pv3) pv3Addr := fmt.Sprintf("%p", &pv3) @@ -719,8 +719,8 @@ func addStringFormatterTests() { func addInterfaceFormatterTests() { // Nil interface. - var v interface{} - nv := (*interface{})(nil) + var v any + nv := (*any)(nil) pv := &v vAddr := fmt.Sprintf("%p", pv) pvAddr := fmt.Sprintf("%p", &pv) @@ -744,7 +744,7 @@ func addInterfaceFormatterTests() { addFormatterTest("%#+v", nv, "(*"+vt+")"+"") // Sub-interface. - v2 := interface{}(uint16(65535)) + v2 := any(uint16(65535)) pv2 := &v2 v2Addr := fmt.Sprintf("%p", pv2) pv2Addr := fmt.Sprintf("%p", &pv2) @@ -828,8 +828,8 @@ func addMapFormatterTests() { addFormatterTest("%#+v", nv2, "(*"+v2t+")"+"") // Map with interface keys and values. - v3 := map[interface{}]interface{}{"one": 1} - nv3 := (*map[interface{}]interface{})(nil) + v3 := map[any]any{"one": 1} + nv3 := (*map[any]any)(nil) pv3 := &v3 v3Addr := fmt.Sprintf("%p", pv3) pv3Addr := fmt.Sprintf("%p", &pv3) @@ -856,8 +856,8 @@ func addMapFormatterTests() { addFormatterTest("%#+v", nv3, "(*"+v3t+")"+"") // Map with nil interface value - v4 := map[string]interface{}{"nil": nil} - nv4 := (*map[string]interface{})(nil) + v4 := map[string]any{"nil": nil} + nv4 := (*map[string]any)(nil) pv4 := &v4 v4Addr := fmt.Sprintf("%p", pv4) pv4Addr := fmt.Sprintf("%p", &pv4) diff --git a/internal/spew/internal_test.go b/internal/spew/internal_test.go index e312b4fad..570fbd009 100644 --- a/internal/spew/internal_test.go +++ b/internal/spew/internal_test.go @@ -15,7 +15,7 @@ */ /* -This test file is part of the spew package rather than than the spew_test +This test file is part of the spew package rather than the spew_test package because it needs access to internals to properly test certain cases which are not possible via the public interface since they should never happen. */ diff --git a/internal/spew/internalunsafe_test.go b/internal/spew/internalunsafe_test.go index 80dc22177..bfd9d6d3f 100644 --- a/internal/spew/internalunsafe_test.go +++ b/internal/spew/internalunsafe_test.go @@ -16,10 +16,10 @@ // 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. -// +build !js,!appengine,!safe,!disableunsafe,go1.4 +//go:build !js && !appengine && !safe && !disableunsafe && go1.4 /* -This test file is part of the spew package rather than than the spew_test +This test file is part of the spew package rather than the spew_test package because it needs access to internals to properly test certain cases which are not possible via the public interface since they should never happen. */ @@ -46,7 +46,7 @@ func changeKind(v *reflect.Value, readOnly bool) { *flags |= flagKindMask } -// TestAddedReflectValue tests functionaly of the dump and formatter code which +// TestAddedReflectValue tests functionally of the dump and formatter code which // falls back to the standard fmt library for new types that might get added to // the language. func TestAddedReflectValue(t *testing.T) { diff --git a/internal/spew/spew.go b/internal/spew/spew.go index 32c0e3388..83bb6be6a 100644 --- a/internal/spew/spew.go +++ b/internal/spew/spew.go @@ -29,7 +29,7 @@ import ( // This function is shorthand for the following syntax: // // fmt.Errorf(format, spew.NewFormatter(a), spew.NewFormatter(b)) -func Errorf(format string, a ...interface{}) (err error) { +func Errorf(format string, a ...any) (err error) { return fmt.Errorf(format, convertArgs(a)...) } @@ -41,7 +41,7 @@ func Errorf(format string, a ...interface{}) (err error) { // This function is shorthand for the following syntax: // // fmt.Fprint(w, spew.NewFormatter(a), spew.NewFormatter(b)) -func Fprint(w io.Writer, a ...interface{}) (n int, err error) { +func Fprint(w io.Writer, a ...any) (n int, err error) { return fmt.Fprint(w, convertArgs(a)...) } @@ -53,7 +53,7 @@ func Fprint(w io.Writer, a ...interface{}) (n int, err error) { // This function is shorthand for the following syntax: // // fmt.Fprintf(w, format, spew.NewFormatter(a), spew.NewFormatter(b)) -func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) { +func Fprintf(w io.Writer, format string, a ...any) (n int, err error) { return fmt.Fprintf(w, format, convertArgs(a)...) } @@ -64,7 +64,7 @@ func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) { // This function is shorthand for the following syntax: // // fmt.Fprintln(w, spew.NewFormatter(a), spew.NewFormatter(b)) -func Fprintln(w io.Writer, a ...interface{}) (n int, err error) { +func Fprintln(w io.Writer, a ...any) (n int, err error) { return fmt.Fprintln(w, convertArgs(a)...) } @@ -76,7 +76,7 @@ func Fprintln(w io.Writer, a ...interface{}) (n int, err error) { // This function is shorthand for the following syntax: // // fmt.Print(spew.NewFormatter(a), spew.NewFormatter(b)) -func Print(a ...interface{}) (n int, err error) { +func Print(a ...any) (n int, err error) { return fmt.Print(convertArgs(a)...) } @@ -88,7 +88,7 @@ func Print(a ...interface{}) (n int, err error) { // This function is shorthand for the following syntax: // // fmt.Printf(format, spew.NewFormatter(a), spew.NewFormatter(b)) -func Printf(format string, a ...interface{}) (n int, err error) { +func Printf(format string, a ...any) (n int, err error) { return fmt.Printf(format, convertArgs(a)...) } @@ -100,7 +100,7 @@ func Printf(format string, a ...interface{}) (n int, err error) { // This function is shorthand for the following syntax: // // fmt.Println(spew.NewFormatter(a), spew.NewFormatter(b)) -func Println(a ...interface{}) (n int, err error) { +func Println(a ...any) (n int, err error) { return fmt.Println(convertArgs(a)...) } @@ -111,7 +111,7 @@ func Println(a ...interface{}) (n int, err error) { // This function is shorthand for the following syntax: // // fmt.Sprint(spew.NewFormatter(a), spew.NewFormatter(b)) -func Sprint(a ...interface{}) string { +func Sprint(a ...any) string { return fmt.Sprint(convertArgs(a)...) } @@ -122,7 +122,7 @@ func Sprint(a ...interface{}) string { // This function is shorthand for the following syntax: // // fmt.Sprintf(format, spew.NewFormatter(a), spew.NewFormatter(b)) -func Sprintf(format string, a ...interface{}) string { +func Sprintf(format string, a ...any) string { return fmt.Sprintf(format, convertArgs(a)...) } @@ -133,14 +133,14 @@ func Sprintf(format string, a ...interface{}) string { // This function is shorthand for the following syntax: // // fmt.Sprintln(spew.NewFormatter(a), spew.NewFormatter(b)) -func Sprintln(a ...interface{}) string { +func Sprintln(a ...any) string { return fmt.Sprintln(convertArgs(a)...) } // convertArgs accepts a slice of arguments and returns a slice of the same // length with each argument converted to a default spew Formatter interface. -func convertArgs(args []interface{}) (formatters []interface{}) { - formatters = make([]interface{}, len(args)) +func convertArgs(args []any) (formatters []any) { + formatters = make([]any, len(args)) for index, arg := range args { formatters[index] = NewFormatter(arg) } diff --git a/internal/spew/spew_test.go b/internal/spew/spew_test.go index 881551f3c..2e12c5236 100644 --- a/internal/spew/spew_test.go +++ b/internal/spew/spew_test.go @@ -19,7 +19,6 @@ package spew_test import ( "bytes" "fmt" - "io/ioutil" "os" "testing" @@ -92,7 +91,7 @@ type spewTest struct { cs *spew.ConfigState f spewFunc format string - in interface{} + in any want string } @@ -107,7 +106,7 @@ var spewTests []spewTest // redirStdout is a helper function to return the standard output from f as a // byte slice. func redirStdout(f func()) ([]byte, error) { - tempFile, err := ioutil.TempFile("", "ss-test") + tempFile, err := os.CreateTemp("", "ss-test") if err != nil { return nil, err } @@ -120,7 +119,7 @@ func redirStdout(f func()) ([]byte, error) { os.Stdout = origStdout tempFile.Close() - return ioutil.ReadFile(fileName) + return os.ReadFile(fileName) } func initSpewTests() { diff --git a/internal/spew/testsrc/doc.go b/internal/spew/testsrc/doc.go new file mode 100644 index 000000000..d93a71ad8 --- /dev/null +++ b/internal/spew/testsrc/doc.go @@ -0,0 +1,12 @@ +// Package testsrc is a package for testing spew with cgo. +// +// NOTE: Due to the following build constraints, this file will only be compiled +// when both cgo is supported and "-tags testcgo" is added to the go test +// command line. This code should really only be in the dumpcgo_test.go file, +// but unfortunately Go will not allow cgo in test files, so this is a +// workaround to allow cgo types to be tested. This configuration is used +// because spew itself does not require cgo to run even though it does handle +// certain cgo types specially. Rather than forcing all clients to require cgo +// and an external C compiler just to run the tests, this scheme makes them +// optional. +package testsrc diff --git a/internal/spew/testdata/dumpcgo.go b/internal/spew/testsrc/dumpcgo.go similarity index 79% rename from internal/spew/testdata/dumpcgo.go rename to internal/spew/testsrc/dumpcgo.go index 5c87dd456..f3b51fe70 100644 --- a/internal/spew/testdata/dumpcgo.go +++ b/internal/spew/testsrc/dumpcgo.go @@ -12,18 +12,9 @@ // 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 both cgo is supported and "-tags testcgo" is added to the go test -// command line. This code should really only be in the dumpcgo_test.go file, -// but unfortunately Go will not allow cgo in test files, so this is a -// workaround to allow cgo types to be tested. This configuration is used -// because spew itself does not require cgo to run even though it does handle -// certain cgo types specially. Rather than forcing all clients to require cgo -// and an external C compiler just to run the tests, this scheme makes them -// optional. -// +build cgo,testcgo +//go:build cgo && testcgo -package testdata +package testsrc /* #include diff --git a/require/examples_test.go b/require/examples_test.go index fc606c9f0..b7a30e7b0 100644 --- a/require/examples_test.go +++ b/require/examples_test.go @@ -1,3 +1,4 @@ +//nolint:testableexamples // not possible at this moment to build a testable example that involves testing.T package require_test import ( diff --git a/require/fixtures_test.go b/require/fixtures_test.go new file mode 100644 index 000000000..fcfbbed47 --- /dev/null +++ b/require/fixtures_test.go @@ -0,0 +1,26 @@ +package require + +const ( + // strings for JSON tests. + + notJSONString = "Not JSON" + fooBarObject = `{"foo": "bar"}` + simpleJSONObject = `{"hello": "world", "foo": "bar"}` + simpleJSONObjectReversed = `{"foo": "bar", "hello": "world"}` + simpleJSONNested = `["foo", {"hello": "world", "nested": "hash"}]` + simpleJSONNestedReversed = `["foo", {"nested": "hash", "hello": "world"}]` + simpleJSONNestedNotEq = `{"foo": "bar", {"nested": "hash", "hello": "world"}}` + simpleJSONArray = `["foo", {"hello": "world", "nested": "hash"}]` + simpleJSONArrayReversed = `[{ "hello": "world", "nested": "hash"}, "foo"]` + + nestedJSONObject = "{" + + "\r\n\t\"numeric\": 1.5,\r\n\t\"array\": " + + "[{\"foo\": \"bar\"}, 1, \"string\", " + + "[\"nested\", \"array\", 5.5]],\r\n\t\"hash\": " + + "{\"nested\": \"hash\", \"nested_slice\": [\"this\", \"is\", \"nested\"]},\r\n\t\"string\": \"foo\"\r\n}" + nestedJSONObjectShuffled = "{" + + "\r\n\t\"numeric\": 1.5,\r\n\t\"hash\": " + + "{\"nested\": \"hash\", \"nested_slice\": " + + "[\"this\", \"is\", \"nested\"]},\r\n\t\"string\": " + + "\"foo\",\r\n\t\"array\": [{\"foo\": \"bar\"}, 1, \"string\", [\"nested\", \"array\", 5.5]]\r\n}" +) diff --git a/require/forward_requirements_test.go b/require/forward_requirements_test.go index 376ea1f61..10f2b2b8b 100644 --- a/require/forward_requirements_test.go +++ b/require/forward_requirements_test.go @@ -1,7 +1,7 @@ package require import ( - "errors" + "fmt" "testing" "time" ) @@ -220,7 +220,7 @@ func TestNoErrorWrapper(t *testing.T) { mockT := new(MockT) mockRequire := New(mockT) - mockRequire.NoError(errors.New("some error")) + mockRequire.NoError(someError()) if !mockT.Failed { t.Error("Check should fail") } @@ -230,7 +230,7 @@ func TestErrorWrapper(t *testing.T) { t.Parallel() require := New(t) - require.Error(errors.New("some error")) + require.Error(someError()) mockT := new(MockT) mockRequire := New(mockT) @@ -244,11 +244,11 @@ func TestErrorContainsWrapper(t *testing.T) { t.Parallel() require := New(t) - require.ErrorContains(errors.New("some error: another error"), "some error") + require.ErrorContains(fmt.Errorf("some error: another error: %w", errSentinel), "some error") mockT := new(MockT) mockRequire := New(mockT) - mockRequire.ErrorContains(errors.New("some error: another error"), "different error") + mockRequire.ErrorContains(fmt.Errorf("some error: another error: %w", errSentinel), "different error") if !mockT.Failed { t.Error("Check should fail") } @@ -258,11 +258,11 @@ func TestEqualErrorWrapper(t *testing.T) { t.Parallel() require := New(t) - require.EqualError(errors.New("some error"), "some error") + require.EqualError(someError(), "some error: test error") mockT := new(MockT) mockRequire := New(mockT) - mockRequire.EqualError(errors.New("some error"), "Not some error") + mockRequire.EqualError(someError(), "Not some error") if !mockT.Failed { t.Error("Check should fail") } @@ -361,7 +361,7 @@ func TestJSONEqWrapper_EqualSONString(t *testing.T) { mockT := new(MockT) mockRequire := New(mockT) - mockRequire.JSONEq(`{"hello": "world", "foo": "bar"}`, `{"hello": "world", "foo": "bar"}`) + mockRequire.JSONEq(simpleJSONObject, simpleJSONObject) if mockT.Failed { t.Error("Check should pass") } @@ -373,7 +373,7 @@ func TestJSONEqWrapper_EquivalentButNotEqual(t *testing.T) { mockT := new(MockT) mockRequire := New(mockT) - mockRequire.JSONEq(`{"hello": "world", "foo": "bar"}`, `{"foo": "bar", "hello": "world"}`) + mockRequire.JSONEq(simpleJSONObject, simpleJSONObjectReversed) if mockT.Failed { t.Error("Check should pass") } @@ -385,8 +385,7 @@ func TestJSONEqWrapper_HashOfArraysAndHashes(t *testing.T) { mockT := new(MockT) mockRequire := New(mockT) - mockRequire.JSONEq("{\r\n\t\"numeric\": 1.5,\r\n\t\"array\": [{\"foo\": \"bar\"}, 1, \"string\", [\"nested\", \"array\", 5.5]],\r\n\t\"hash\": {\"nested\": \"hash\", \"nested_slice\": [\"this\", \"is\", \"nested\"]},\r\n\t\"string\": \"foo\"\r\n}", - "{\r\n\t\"numeric\": 1.5,\r\n\t\"hash\": {\"nested\": \"hash\", \"nested_slice\": [\"this\", \"is\", \"nested\"]},\r\n\t\"string\": \"foo\",\r\n\t\"array\": [{\"foo\": \"bar\"}, 1, \"string\", [\"nested\", \"array\", 5.5]]\r\n}") + mockRequire.JSONEq(nestedJSONObject, nestedJSONObjectShuffled) if mockT.Failed { t.Error("Check should pass") } @@ -398,7 +397,7 @@ func TestJSONEqWrapper_Array(t *testing.T) { mockT := new(MockT) mockRequire := New(mockT) - mockRequire.JSONEq(`["foo", {"hello": "world", "nested": "hash"}]`, `["foo", {"nested": "hash", "hello": "world"}]`) + mockRequire.JSONEq(simpleJSONNested, simpleJSONNestedReversed) if mockT.Failed { t.Error("Check should pass") } @@ -410,7 +409,7 @@ func TestJSONEqWrapper_HashAndArrayNotEquivalent(t *testing.T) { mockT := new(MockT) mockRequire := New(mockT) - mockRequire.JSONEq(`["foo", {"hello": "world", "nested": "hash"}]`, `{"foo": "bar", {"nested": "hash", "hello": "world"}}`) + mockRequire.JSONEq(simpleJSONNested, simpleJSONNestedNotEq) if !mockT.Failed { t.Error("Check should fail") } @@ -422,7 +421,7 @@ func TestJSONEqWrapper_HashesNotEquivalent(t *testing.T) { mockT := new(MockT) mockRequire := New(mockT) - mockRequire.JSONEq(`{"foo": "bar"}`, `{"foo": "bar", "hello": "world"}`) + mockRequire.JSONEq(fooBarObject, simpleJSONObjectReversed) if !mockT.Failed { t.Error("Check should fail") } @@ -434,7 +433,7 @@ func TestJSONEqWrapper_ActualIsNotJSON(t *testing.T) { mockT := new(MockT) mockRequire := New(mockT) - mockRequire.JSONEq(`{"foo": "bar"}`, "Not JSON") + mockRequire.JSONEq(fooBarObject, notJSONString) if !mockT.Failed { t.Error("Check should fail") } @@ -446,7 +445,7 @@ func TestJSONEqWrapper_ExpectedIsNotJSON(t *testing.T) { mockT := new(MockT) mockRequire := New(mockT) - mockRequire.JSONEq("Not JSON", `{"foo": "bar", "hello": "world"}`) + mockRequire.JSONEq(notJSONString, simpleJSONObjectReversed) if !mockT.Failed { t.Error("Check should fail") } @@ -458,7 +457,7 @@ func TestJSONEqWrapper_ExpectedAndActualNotJSON(t *testing.T) { mockT := new(MockT) mockRequire := New(mockT) - mockRequire.JSONEq("Not JSON", "Not JSON") + mockRequire.JSONEq(notJSONString, notJSONString) if !mockT.Failed { t.Error("Check should fail") } @@ -470,7 +469,7 @@ func TestJSONEqWrapper_ArraysOfDifferentOrder(t *testing.T) { mockT := new(MockT) mockRequire := New(mockT) - mockRequire.JSONEq(`["foo", {"hello": "world", "nested": "hash"}]`, `[{ "hello": "world", "nested": "hash"}, "foo"]`) + mockRequire.JSONEq(simpleJSONArray, simpleJSONArrayReversed) if !mockT.Failed { t.Error("Check should fail") } diff --git a/require/require.go b/require/require.go index bd5c021f2..1b2debdf9 100644 --- a/require/require.go +++ b/require/require.go @@ -91,7 +91,7 @@ func DirExistsf(t TestingT, path string, msg string, args ...any) { // listB(array, slice...) ignoring the order of the elements. If there are duplicate elements, // the number of appearances of each of them in both lists should match. // -// require.ElementsMatch(t, [1, 3, 2, 3], [1, 3, 3, 2]) +// require.ElementsMatch(t, [1, 3, 2, 3], [1, 3, 3, 2]). func ElementsMatch(t TestingT, listA any, listB any, msgAndArgs ...any) { if h, ok := t.(tHelper); ok { h.Helper() @@ -511,7 +511,7 @@ func Exactlyf(t TestingT, expected any, actual any, msg string, args ...any) { t.FailNow() } -// Fail reports a failure through +// Fail reports a failure through. func Fail(t TestingT, failureMessage string, msgAndArgs ...any) { if h, ok := t.(tHelper); ok { h.Helper() @@ -522,7 +522,7 @@ func Fail(t TestingT, failureMessage string, msgAndArgs ...any) { t.FailNow() } -// FailNow fails test +// FailNow fails test. func FailNow(t TestingT, failureMessage string, msgAndArgs ...any) { if h, ok := t.(tHelper); ok { h.Helper() @@ -947,7 +947,7 @@ func InDeltaf(t TestingT, expected any, actual any, delta float64, msg string, a t.FailNow() } -// InEpsilon asserts that expected and actual have a relative error less than epsilon +// InEpsilon asserts that expected and actual have a relative error less than epsilon. func InEpsilon(t TestingT, expected any, actual any, epsilon float64, msgAndArgs ...any) { if h, ok := t.(tHelper); ok { h.Helper() @@ -1508,7 +1508,7 @@ func NotContainsf(t TestingT, s any, contains any, msg string, args ...any) { // // require.NotElementsMatch(t, [1, 1, 2, 3], [1, 2, 3]) -> true // -// require.NotElementsMatch(t, [1, 2, 3], [1, 2, 4]) -> true +// require.NotElementsMatch(t, [1, 2, 3], [1, 2, 4]) -> true. func NotElementsMatch(t TestingT, listA any, listB any, msgAndArgs ...any) { if h, ok := t.(tHelper); ok { h.Helper() diff --git a/require/require_forward.go b/require/require_forward.go index b25b95da3..cbfe35ffd 100644 --- a/require/require_forward.go +++ b/require/require_forward.go @@ -73,7 +73,7 @@ func (a *Assertions) DirExistsf(path string, msg string, args ...any) { // listB(array, slice...) ignoring the order of the elements. If there are duplicate elements, // the number of appearances of each of them in both lists should match. // -// a.ElementsMatch([1, 3, 2, 3], [1, 3, 3, 2]) +// a.ElementsMatch([1, 3, 2, 3], [1, 3, 3, 2]). func (a *Assertions) ElementsMatch(listA any, listB any, msgAndArgs ...any) { if h, ok := a.t.(tHelper); ok { h.Helper() @@ -415,7 +415,7 @@ func (a *Assertions) Exactlyf(expected any, actual any, msg string, args ...any) Exactlyf(a.t, expected, actual, msg, args...) } -// Fail reports a failure through +// Fail reports a failure through. func (a *Assertions) Fail(failureMessage string, msgAndArgs ...any) { if h, ok := a.t.(tHelper); ok { h.Helper() @@ -423,7 +423,7 @@ func (a *Assertions) Fail(failureMessage string, msgAndArgs ...any) { Fail(a.t, failureMessage, msgAndArgs...) } -// FailNow fails test +// FailNow fails test. func (a *Assertions) FailNow(failureMessage string, msgAndArgs ...any) { if h, ok := a.t.(tHelper); ok { h.Helper() @@ -755,7 +755,7 @@ func (a *Assertions) InDeltaf(expected any, actual any, delta float64, msg strin InDeltaf(a.t, expected, actual, delta, msg, args...) } -// InEpsilon asserts that expected and actual have a relative error less than epsilon +// InEpsilon asserts that expected and actual have a relative error less than epsilon. func (a *Assertions) InEpsilon(expected any, actual any, epsilon float64, msgAndArgs ...any) { if h, ok := a.t.(tHelper); ok { h.Helper() @@ -1196,7 +1196,7 @@ func (a *Assertions) NotContainsf(s any, contains any, msg string, args ...any) // // a.NotElementsMatch([1, 1, 2, 3], [1, 2, 3]) -> true // -// a.NotElementsMatch([1, 2, 3], [1, 2, 4]) -> true +// a.NotElementsMatch([1, 2, 3], [1, 2, 4]) -> true. func (a *Assertions) NotElementsMatch(listA any, listB any, msgAndArgs ...any) { if h, ok := a.t.(tHelper); ok { h.Helper() diff --git a/require/requirements.go b/require/requirements.go index 0905df4b9..290110e7d 100644 --- a/require/requirements.go +++ b/require/requirements.go @@ -1,6 +1,6 @@ package require -// TestingT is an interface wrapper around *testing.T +// TestingT is an interface wrapper around *testing.T. type TestingT interface { Errorf(format string, args ...any) FailNow() diff --git a/require/requirements_test.go b/require/requirements_test.go index 372996b60..240557a0d 100644 --- a/require/requirements_test.go +++ b/require/requirements_test.go @@ -2,25 +2,26 @@ package require import ( "errors" + "fmt" "testing" "time" "github.com/go-openapi/testify/v2/assert" ) -// AssertionTesterInterface defines an interface to be used for testing assertion methods +// AssertionTesterInterface defines an interface to be used for testing assertion methods. type AssertionTesterInterface interface { TestMethod() } -// AssertionTesterConformingObject is an object that conforms to the AssertionTesterInterface interface +// AssertionTesterConformingObject is an object that conforms to the AssertionTesterInterface interface. type AssertionTesterConformingObject struct { } func (a *AssertionTesterConformingObject) TestMethod() { } -// AssertionTesterNonConformingObject is an object that does not conform to the AssertionTesterInterface interface +// AssertionTesterNonConformingObject is an object that does not conform to the AssertionTesterInterface interface. type AssertionTesterNonConformingObject struct { } @@ -209,7 +210,7 @@ func TestNoError(t *testing.T) { NoError(t, nil) mockT := new(MockT) - NoError(mockT, errors.New("some error")) + NoError(mockT, someError()) if !mockT.Failed { t.Error("Check should fail") } @@ -218,7 +219,7 @@ func TestNoError(t *testing.T) { func TestError(t *testing.T) { t.Parallel() - Error(t, errors.New("some error")) + Error(t, someError()) mockT := new(MockT) Error(mockT, nil) @@ -230,10 +231,10 @@ func TestError(t *testing.T) { func TestErrorContains(t *testing.T) { t.Parallel() - ErrorContains(t, errors.New("some error: another error"), "some error") + ErrorContains(t, fmt.Errorf("some error: another error: %w", errSentinel), "some error") mockT := new(MockT) - ErrorContains(mockT, errors.New("some error"), "different error") + ErrorContains(mockT, someError(), "different error") if !mockT.Failed { t.Error("Check should fail") } @@ -242,10 +243,10 @@ func TestErrorContains(t *testing.T) { func TestEqualError(t *testing.T) { t.Parallel() - EqualError(t, errors.New("some error"), "some error") + EqualError(t, someError(), "some error: test error") mockT := new(MockT) - EqualError(mockT, errors.New("some error"), "Not some error") + EqualError(mockT, someError(), "Not some error") if !mockT.Failed { t.Error("Check should fail") } @@ -330,7 +331,7 @@ func TestJSONEq_EqualSONString(t *testing.T) { t.Parallel() mockT := new(MockT) - JSONEq(mockT, `{"hello": "world", "foo": "bar"}`, `{"hello": "world", "foo": "bar"}`) + JSONEq(mockT, simpleJSONObject, simpleJSONObject) if mockT.Failed { t.Error("Check should pass") } @@ -340,7 +341,7 @@ func TestJSONEq_EquivalentButNotEqual(t *testing.T) { t.Parallel() mockT := new(MockT) - JSONEq(mockT, `{"hello": "world", "foo": "bar"}`, `{"foo": "bar", "hello": "world"}`) + JSONEq(mockT, simpleJSONObject, simpleJSONObjectReversed) if mockT.Failed { t.Error("Check should pass") } @@ -350,8 +351,7 @@ func TestJSONEq_HashOfArraysAndHashes(t *testing.T) { t.Parallel() mockT := new(MockT) - JSONEq(mockT, "{\r\n\t\"numeric\": 1.5,\r\n\t\"array\": [{\"foo\": \"bar\"}, 1, \"string\", [\"nested\", \"array\", 5.5]],\r\n\t\"hash\": {\"nested\": \"hash\", \"nested_slice\": [\"this\", \"is\", \"nested\"]},\r\n\t\"string\": \"foo\"\r\n}", - "{\r\n\t\"numeric\": 1.5,\r\n\t\"hash\": {\"nested\": \"hash\", \"nested_slice\": [\"this\", \"is\", \"nested\"]},\r\n\t\"string\": \"foo\",\r\n\t\"array\": [{\"foo\": \"bar\"}, 1, \"string\", [\"nested\", \"array\", 5.5]]\r\n}") + JSONEq(mockT, nestedJSONObject, nestedJSONObjectShuffled) if mockT.Failed { t.Error("Check should pass") } @@ -361,7 +361,7 @@ func TestJSONEq_Array(t *testing.T) { t.Parallel() mockT := new(MockT) - JSONEq(mockT, `["foo", {"hello": "world", "nested": "hash"}]`, `["foo", {"nested": "hash", "hello": "world"}]`) + JSONEq(mockT, simpleJSONNested, simpleJSONNestedReversed) if mockT.Failed { t.Error("Check should pass") } @@ -371,7 +371,7 @@ func TestJSONEq_HashAndArrayNotEquivalent(t *testing.T) { t.Parallel() mockT := new(MockT) - JSONEq(mockT, `["foo", {"hello": "world", "nested": "hash"}]`, `{"foo": "bar", {"nested": "hash", "hello": "world"}}`) + JSONEq(mockT, simpleJSONNested, simpleJSONNestedNotEq) if !mockT.Failed { t.Error("Check should fail") } @@ -381,7 +381,7 @@ func TestJSONEq_HashesNotEquivalent(t *testing.T) { t.Parallel() mockT := new(MockT) - JSONEq(mockT, `{"foo": "bar"}`, `{"foo": "bar", "hello": "world"}`) + JSONEq(mockT, fooBarObject, simpleJSONObjectReversed) if !mockT.Failed { t.Error("Check should fail") } @@ -391,7 +391,7 @@ func TestJSONEq_ActualIsNotJSON(t *testing.T) { t.Parallel() mockT := new(MockT) - JSONEq(mockT, `{"foo": "bar"}`, "Not JSON") + JSONEq(mockT, fooBarObject, notJSONString) if !mockT.Failed { t.Error("Check should fail") } @@ -401,7 +401,7 @@ func TestJSONEq_ExpectedIsNotJSON(t *testing.T) { t.Parallel() mockT := new(MockT) - JSONEq(mockT, "Not JSON", `{"foo": "bar", "hello": "world"}`) + JSONEq(mockT, notJSONString, simpleJSONObjectReversed) if !mockT.Failed { t.Error("Check should fail") } @@ -411,7 +411,7 @@ func TestJSONEq_ExpectedAndActualNotJSON(t *testing.T) { t.Parallel() mockT := new(MockT) - JSONEq(mockT, "Not JSON", "Not JSON") + JSONEq(mockT, notJSONString, notJSONString) if !mockT.Failed { t.Error("Check should fail") } @@ -421,7 +421,7 @@ func TestJSONEq_ArraysOfDifferentOrder(t *testing.T) { t.Parallel() mockT := new(MockT) - JSONEq(mockT, `["foo", {"hello": "world", "nested": "hash"}]`, `[{ "hello": "world", "nested": "hash"}, "foo"]`) + JSONEq(mockT, simpleJSONArray, simpleJSONArrayReversed) if !mockT.Failed { t.Error("Check should fail") } @@ -513,7 +513,7 @@ func TestErrorAssertionFunc(t *testing.T) { assertion ErrorAssertionFunc }{ {"noError", nil, NoError}, - {"error", errors.New("whoops"), Error}, + {"error", fmt.Errorf("whoops: %w", errSentinel), Error}, } for _, tt := range tests { @@ -553,3 +553,9 @@ func TestEventuallyWithTTrue(t *testing.T) { False(t, mockT.Failed, "Check should pass") Equal(t, 2, counter, "Condition is expected to be called 2 times") } + +var errSentinel = errors.New("test error") + +func someError() error { + return fmt.Errorf("some error: %w", errSentinel) +}