From b1b603b6f7fc9c211f64f03d00a4ec6d16d7171a Mon Sep 17 00:00:00 2001 From: not-matthias Date: Wed, 17 Dec 2025 19:11:35 +0100 Subject: [PATCH 01/11] fix(go-runner): ensure excludes dont filter files --- go-runner/src/builder/discovery.rs | 1 + ...enchmarks@example-with-excluded-names.snap | 19 ++++++++++++ go-runner/src/integration_tests.rs | 1 + ...snapshots@example-with-excluded-names.snap | 29 +++++++++++++++++++ go-runner/src/utils.rs | 8 ++++- .../example-with-excluded-names/go.mod | 3 ++ .../internal/tools/instrumentation.go | 6 ++++ .../internal/tools/node_modules.go | 6 ++++ .../internal/tools/target.go | 6 ++++ .../internal/tools/target_impl.go | 11 +++++++ .../example-with-excluded-names/main.go | 7 +++++ .../example-with-excluded-names/main_test.go | 11 +++++++ 12 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 go-runner/src/builder/snapshots/codspeed_go_runner__builder__discovery__tests__discover_benchmarks@example-with-excluded-names.snap create mode 100644 go-runner/src/snapshots/codspeed_go_runner__integration_tests__assert_results_snapshots@example-with-excluded-names.snap create mode 100644 go-runner/testdata/projects/example-with-excluded-names/go.mod create mode 100644 go-runner/testdata/projects/example-with-excluded-names/internal/tools/instrumentation.go create mode 100644 go-runner/testdata/projects/example-with-excluded-names/internal/tools/node_modules.go create mode 100644 go-runner/testdata/projects/example-with-excluded-names/internal/tools/target.go create mode 100644 go-runner/testdata/projects/example-with-excluded-names/internal/tools/target_impl.go create mode 100644 go-runner/testdata/projects/example-with-excluded-names/main.go create mode 100644 go-runner/testdata/projects/example-with-excluded-names/main_test.go diff --git a/go-runner/src/builder/discovery.rs b/go-runner/src/builder/discovery.rs index 4b0c0f74..6ba83fea 100644 --- a/go-runner/src/builder/discovery.rs +++ b/go-runner/src/builder/discovery.rs @@ -378,6 +378,7 @@ mod tests { #[case::example_with_main("example-with-main")] #[case::example_with_dot_go_folder("example-with-dot-go-folder")] #[case::example_with_test_package("example-with-test-package")] + #[case::example_with_excluded_names("example-with-excluded-names")] #[test_log::test] fn test_discover_benchmarks(#[case] project_name: &str) { let project_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")) diff --git a/go-runner/src/builder/snapshots/codspeed_go_runner__builder__discovery__tests__discover_benchmarks@example-with-excluded-names.snap b/go-runner/src/builder/snapshots/codspeed_go_runner__builder__discovery__tests__discover_benchmarks@example-with-excluded-names.snap new file mode 100644 index 00000000..a85a0ae4 --- /dev/null +++ b/go-runner/src/builder/snapshots/codspeed_go_runner__builder__discovery__tests__discover_benchmarks@example-with-excluded-names.snap @@ -0,0 +1,19 @@ +--- +source: go-runner/src/builder/discovery.rs +expression: packages +--- +[ + { + "raw_package": "[raw_package]", + "benchmarks": [ + { + "name": "BenchmarkProcessData", + "module_path": "example-with-excluded-names", + "import_alias": "benchmarkprocessdata_11906522046485331356", + "qualified_name": "benchmarkprocessdata_11906522046485331356.BenchmarkProcessData", + "file_path": "main_test.go", + "is_external": false + } + ] + } +] diff --git a/go-runner/src/integration_tests.rs b/go-runner/src/integration_tests.rs index d6e22e4d..8328b812 100644 --- a/go-runner/src/integration_tests.rs +++ b/go-runner/src/integration_tests.rs @@ -82,6 +82,7 @@ fn assert_results_snapshots(profile_dir: &Path, project_name: &str) { #[case::example_with_vendor("example-with-vendor")] #[case::example_with_test_package("example-with-test-package")] #[case::example_with_replace("example-with-replace")] +#[case::example_with_excluded_names("example-with-excluded-names")] #[test_log::test] fn test_build_and_run(#[case] project_name: &str) { let project_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")) diff --git a/go-runner/src/snapshots/codspeed_go_runner__integration_tests__assert_results_snapshots@example-with-excluded-names.snap b/go-runner/src/snapshots/codspeed_go_runner__integration_tests__assert_results_snapshots@example-with-excluded-names.snap new file mode 100644 index 00000000..9a1854a8 --- /dev/null +++ b/go-runner/src/snapshots/codspeed_go_runner__integration_tests__assert_results_snapshots@example-with-excluded-names.snap @@ -0,0 +1,29 @@ +--- +source: go-runner/src/integration_tests.rs +expression: results +--- +[ + { + "creator": { + "name": "codspeed-go", + "version": "[version]", + "pid": "[pid]" + }, + "instrument": { + "type": "walltime" + }, + "benchmarks": [ + { + "name": "BenchmarkProcessData", + "uri": "go-runner/testdata/projects/example-with-excluded-names/main_test.go::BenchmarkProcessData", + "config": { + "warmup_time_ns": null, + "min_round_time_ns": null, + "max_time_ns": null, + "max_rounds": null + }, + "stats": "[stats]" + } + ] + } +] diff --git a/go-runner/src/utils.rs b/go-runner/src/utils.rs index 43e80b1c..f3744365 100644 --- a/go-runner/src/utils.rs +++ b/go-runner/src/utils.rs @@ -4,7 +4,13 @@ use std::io; use std::path::{Path, PathBuf}; pub fn copy_dir_recursively(src: impl AsRef, dst: impl AsRef) -> io::Result<()> { - let excludes = vec!["node_modules".into(), "target".into()]; + let excludes = if cfg!(test) { + // Since we copy the whole .git directory for each test, we would also copy the target folder. + // Use path separator to match only the directory, not files like "target.go" or "target_impl.go" + vec!["target/".into()] + } else { + vec![] + }; let includes = vec![]; dircpy::copy_dir_advanced(src, dst, true, true, true, excludes, includes)?; Ok(()) diff --git a/go-runner/testdata/projects/example-with-excluded-names/go.mod b/go-runner/testdata/projects/example-with-excluded-names/go.mod new file mode 100644 index 00000000..f1dbe99a --- /dev/null +++ b/go-runner/testdata/projects/example-with-excluded-names/go.mod @@ -0,0 +1,3 @@ +module example-with-excluded-names + +go 1.24 diff --git a/go-runner/testdata/projects/example-with-excluded-names/internal/tools/instrumentation.go b/go-runner/testdata/projects/example-with-excluded-names/internal/tools/instrumentation.go new file mode 100644 index 00000000..a7bf8cfd --- /dev/null +++ b/go-runner/testdata/projects/example-with-excluded-names/internal/tools/instrumentation.go @@ -0,0 +1,6 @@ +package tools + +// Process processes data using target and node_modules utilities +func Process(data string) string { + return processTarget(data) + processNodeModules(data) +} diff --git a/go-runner/testdata/projects/example-with-excluded-names/internal/tools/node_modules.go b/go-runner/testdata/projects/example-with-excluded-names/internal/tools/node_modules.go new file mode 100644 index 00000000..27433d1e --- /dev/null +++ b/go-runner/testdata/projects/example-with-excluded-names/internal/tools/node_modules.go @@ -0,0 +1,6 @@ +package tools + +// NodeModulesHelper is a helper function accessed during testing +func NodeModulesHelper() string { + return "node_modules" +} diff --git a/go-runner/testdata/projects/example-with-excluded-names/internal/tools/target.go b/go-runner/testdata/projects/example-with-excluded-names/internal/tools/target.go new file mode 100644 index 00000000..299ffb0b --- /dev/null +++ b/go-runner/testdata/projects/example-with-excluded-names/internal/tools/target.go @@ -0,0 +1,6 @@ +package tools + +// TargetHelper is a helper function accessed during testing +func TargetHelper() string { + return "target" +} diff --git a/go-runner/testdata/projects/example-with-excluded-names/internal/tools/target_impl.go b/go-runner/testdata/projects/example-with-excluded-names/internal/tools/target_impl.go new file mode 100644 index 00000000..78c8c7e0 --- /dev/null +++ b/go-runner/testdata/projects/example-with-excluded-names/internal/tools/target_impl.go @@ -0,0 +1,11 @@ +package tools + +// processTarget is called from target.go +func processTarget(data string) string { + return "target:" + data +} + +// processNodeModules is called from node_modules.go +func processNodeModules(data string) string { + return "|nm:" + data +} diff --git a/go-runner/testdata/projects/example-with-excluded-names/main.go b/go-runner/testdata/projects/example-with-excluded-names/main.go new file mode 100644 index 00000000..776d605f --- /dev/null +++ b/go-runner/testdata/projects/example-with-excluded-names/main.go @@ -0,0 +1,7 @@ +package main + +import "example-with-excluded-names/internal/tools" + +func ProcessData(data string) string { + return tools.Process(data) +} diff --git a/go-runner/testdata/projects/example-with-excluded-names/main_test.go b/go-runner/testdata/projects/example-with-excluded-names/main_test.go new file mode 100644 index 00000000..92db0484 --- /dev/null +++ b/go-runner/testdata/projects/example-with-excluded-names/main_test.go @@ -0,0 +1,11 @@ +package main + +import ( + "testing" +) + +func BenchmarkProcessData(b *testing.B) { + for i := 0; i < b.N; i++ { + ProcessData("example-target") + } +} From f618cd986c802204d67616649da53f35e950cae9 Mon Sep 17 00:00:00 2001 From: not-matthias Date: Wed, 7 Jan 2026 11:55:33 +0100 Subject: [PATCH 02/11] feat: fork logr and stdr --- pkg/fork.sh | 25 +- pkg/logr/.github/dependabot.yml | 6 + pkg/logr/.github/workflows/apidiff.yaml | 32 + pkg/logr/.github/workflows/assign.yaml | 27 + pkg/logr/.github/workflows/lint.yaml | 29 + pkg/logr/.github/workflows/scorecard.yml | 64 + pkg/logr/.github/workflows/tests.yaml | 25 + pkg/logr/.golangci.yaml | 28 + pkg/logr/CHANGELOG.md | 6 + pkg/logr/CONTRIBUTING.md | 17 + pkg/logr/LICENSE | 201 +++ pkg/logr/README.md | 407 ++++++ pkg/logr/SECURITY.md | 18 + pkg/logr/_tools/apidiff.sh | 127 ++ pkg/logr/benchmark/README.md | 15 + pkg/logr/benchmark/benchmark_slog_test.go | 142 +++ pkg/logr/benchmark/benchmark_test.go | 283 +++++ pkg/logr/context.go | 33 + pkg/logr/context_noslog.go | 49 + pkg/logr/context_slog.go | 83 ++ pkg/logr/context_slog_test.go | 66 + pkg/logr/context_test.go | 50 + pkg/logr/discard.go | 24 + pkg/logr/discard_test.go | 54 + pkg/logr/example_marshaler_secret_test.go | 47 + pkg/logr/example_marshaler_test.go | 55 + pkg/logr/example_slogr_test.go | 79 ++ pkg/logr/example_test.go | 100 ++ pkg/logr/examples/slog/main.go | 102 ++ pkg/logr/examples/tab_logger.go | 97 ++ pkg/logr/examples/usage_example.go | 173 +++ pkg/logr/funcr/example/main.go | 65 + pkg/logr/funcr/example/main_noslog.go | 29 + pkg/logr/funcr/example/main_slog.go | 42 + pkg/logr/funcr/example_formatter_test.go | 103 ++ pkg/logr/funcr/example_test.go | 126 ++ pkg/logr/funcr/funcr.go | 914 ++++++++++++++ pkg/logr/funcr/funcr_test.go | 1400 +++++++++++++++++++++ pkg/logr/funcr/slogsink.go | 105 ++ pkg/logr/funcr/slogsink_test.go | 234 ++++ pkg/logr/internal/testhelp/slog.go | 79 ++ pkg/logr/internal/testhelp/slog_test.go | 33 + pkg/logr/logr.go | 520 ++++++++ pkg/logr/logr_test.go | 416 ++++++ pkg/logr/sloghandler.go | 192 +++ pkg/logr/slogr.go | 100 ++ pkg/logr/slogr/slogr.go | 61 + pkg/logr/slogr_test.go | 199 +++ pkg/logr/slogsink.go | 120 ++ pkg/logr/testimpls_slog_test.go | 140 +++ pkg/logr/testimpls_test.go | 92 ++ pkg/logr/testing/test.go | 37 + pkg/logr/testing/test_test.go | 52 + pkg/logr/testr/testr.go | 167 +++ pkg/logr/testr/testr_fuzz_test.go | 27 + pkg/logr/testr/testr_test.go | 89 ++ pkg/stdr/.github/workflows/apidiff.yaml | 29 + pkg/stdr/.github/workflows/lint.yaml | 42 + pkg/stdr/.github/workflows/tests.yaml | 22 + pkg/stdr/LICENSE | 201 +++ pkg/stdr/README.md | 6 + pkg/stdr/_tools/apidiff.sh | 127 ++ pkg/stdr/example/main.go | 58 + pkg/stdr/example_test.go | 58 + pkg/stdr/stdr.go | 170 +++ 65 files changed, 8517 insertions(+), 2 deletions(-) create mode 100644 pkg/logr/.github/dependabot.yml create mode 100644 pkg/logr/.github/workflows/apidiff.yaml create mode 100644 pkg/logr/.github/workflows/assign.yaml create mode 100644 pkg/logr/.github/workflows/lint.yaml create mode 100644 pkg/logr/.github/workflows/scorecard.yml create mode 100644 pkg/logr/.github/workflows/tests.yaml create mode 100644 pkg/logr/.golangci.yaml create mode 100644 pkg/logr/CHANGELOG.md create mode 100644 pkg/logr/CONTRIBUTING.md create mode 100644 pkg/logr/LICENSE create mode 100644 pkg/logr/README.md create mode 100644 pkg/logr/SECURITY.md create mode 100755 pkg/logr/_tools/apidiff.sh create mode 100644 pkg/logr/benchmark/README.md create mode 100644 pkg/logr/benchmark/benchmark_slog_test.go create mode 100644 pkg/logr/benchmark/benchmark_test.go create mode 100644 pkg/logr/context.go create mode 100644 pkg/logr/context_noslog.go create mode 100644 pkg/logr/context_slog.go create mode 100644 pkg/logr/context_slog_test.go create mode 100644 pkg/logr/context_test.go create mode 100644 pkg/logr/discard.go create mode 100644 pkg/logr/discard_test.go create mode 100644 pkg/logr/example_marshaler_secret_test.go create mode 100644 pkg/logr/example_marshaler_test.go create mode 100644 pkg/logr/example_slogr_test.go create mode 100644 pkg/logr/example_test.go create mode 100644 pkg/logr/examples/slog/main.go create mode 100644 pkg/logr/examples/tab_logger.go create mode 100644 pkg/logr/examples/usage_example.go create mode 100644 pkg/logr/funcr/example/main.go create mode 100644 pkg/logr/funcr/example/main_noslog.go create mode 100644 pkg/logr/funcr/example/main_slog.go create mode 100644 pkg/logr/funcr/example_formatter_test.go create mode 100644 pkg/logr/funcr/example_test.go create mode 100644 pkg/logr/funcr/funcr.go create mode 100644 pkg/logr/funcr/funcr_test.go create mode 100644 pkg/logr/funcr/slogsink.go create mode 100644 pkg/logr/funcr/slogsink_test.go create mode 100644 pkg/logr/internal/testhelp/slog.go create mode 100644 pkg/logr/internal/testhelp/slog_test.go create mode 100644 pkg/logr/logr.go create mode 100644 pkg/logr/logr_test.go create mode 100644 pkg/logr/sloghandler.go create mode 100644 pkg/logr/slogr.go create mode 100644 pkg/logr/slogr/slogr.go create mode 100644 pkg/logr/slogr_test.go create mode 100644 pkg/logr/slogsink.go create mode 100644 pkg/logr/testimpls_slog_test.go create mode 100644 pkg/logr/testimpls_test.go create mode 100644 pkg/logr/testing/test.go create mode 100644 pkg/logr/testing/test_test.go create mode 100644 pkg/logr/testr/testr.go create mode 100644 pkg/logr/testr/testr_fuzz_test.go create mode 100644 pkg/logr/testr/testr_test.go create mode 100644 pkg/stdr/.github/workflows/apidiff.yaml create mode 100644 pkg/stdr/.github/workflows/lint.yaml create mode 100644 pkg/stdr/.github/workflows/tests.yaml create mode 100644 pkg/stdr/LICENSE create mode 100644 pkg/stdr/README.md create mode 100755 pkg/stdr/_tools/apidiff.sh create mode 100644 pkg/stdr/example/main.go create mode 100644 pkg/stdr/example_test.go create mode 100644 pkg/stdr/stdr.go diff --git a/pkg/fork.sh b/pkg/fork.sh index 99f9b5e0..42ced4da 100755 --- a/pkg/fork.sh +++ b/pkg/fork.sh @@ -9,8 +9,15 @@ function replace_in_go_files() { function fix_project() { rm -rf .git go.mod go.sum - replace_in_go_files '"testing"' 'testing "github.com/CodSpeedHQ/codspeed-go/compat/testing"' - replace_in_go_files '"testing/slogtest"' 'slogtest "github.com/CodSpeedHQ/codspeed-go/compat/testing/slogtest"' + # Replace imports with patterns that only match in import statements + # Match "testing" at start of line (after whitespace) to avoid string literals + find . -type f -name "*.go" -exec sed -i 's/^[[:space:]]*"testing"$/testing "github.com\/CodSpeedHQ\/codspeed-go\/testing\/testing"/g' {} + + find . -type f -name "*.go" -exec sed -i 's/^[[:space:]]*"testing")/testing "github.com\/CodSpeedHQ\/codspeed-go\/testing\/testing")/g' {} + + find . -type f -name "*.go" -exec sed -i 's/^[[:space:]]*"testing",/testing "github.com\/CodSpeedHQ\/codspeed-go\/testing\/testing",/g' {} + + + find . -type f -name "*.go" -exec sed -i 's/^[[:space:]]*"testing\/slogtest"$/slogtest "github.com\/CodSpeedHQ\/codspeed-go\/testing\/testing\/slogtest"/g' {} + + find . -type f -name "*.go" -exec sed -i 's/^[[:space:]]*"testing\/slogtest")/slogtest "github.com\/CodSpeedHQ\/codspeed-go\/testing\/testing\/slogtest")/g' {} + + find . -type f -name "*.go" -exec sed -i 's/^[[:space:]]*"testing\/slogtest",/slogtest "github.com\/CodSpeedHQ\/codspeed-go\/testing\/testing\/slogtest",/g' {} + go mod tidy go fmt ./... @@ -27,3 +34,17 @@ pushd quicktest replace_in_go_files '"github.com/frankban/quicktest' '"github.com/CodSpeedHQ/codspeed-go/pkg/quicktest' fix_project popd + +git clone -b v1.4.3 https://github.com/go-logr/logr.git +pushd logr +replace_in_go_files '"github.com/go-logr/logr' '"github.com/CodSpeedHQ/codspeed-go/pkg/logr' +fix_project +popd + +git clone -b v1.2.2 https://github.com/go-logr/stdr.git +pushd stdr +replace_in_go_files '"github.com/go-logr/stdr' '"github.com/CodSpeedHQ/codspeed-go/pkg/stdr' +# stdr imports logr, so we need to replace those imports too +replace_in_go_files '"github.com/go-logr/logr' '"github.com/CodSpeedHQ/codspeed-go/pkg/logr' +fix_project +popd diff --git a/pkg/logr/.github/dependabot.yml b/pkg/logr/.github/dependabot.yml new file mode 100644 index 00000000..5ace4600 --- /dev/null +++ b/pkg/logr/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" diff --git a/pkg/logr/.github/workflows/apidiff.yaml b/pkg/logr/.github/workflows/apidiff.yaml new file mode 100644 index 00000000..3cb7db84 --- /dev/null +++ b/pkg/logr/.github/workflows/apidiff.yaml @@ -0,0 +1,32 @@ +name: Run apidiff + +on: [ pull_request ] + +permissions: + contents: read + +jobs: + apidiff: + runs-on: ubuntu-latest + if: github.base_ref + steps: + - name: Install Go + uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 + with: + go-version: 1.24.x + - name: Add GOBIN to PATH + run: echo "PATH=$(go env GOPATH)/bin:$PATH" >>$GITHUB_ENV + - name: Install dependencies + run: go install golang.org/x/exp/cmd/apidiff@latest + - name: Checkout old code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + ref: ${{ github.base_ref }} + path: "old" + - name: Checkout new code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + path: "new" + - name: APIDiff + run: ./_tools/apidiff.sh -d ../old + working-directory: "new" diff --git a/pkg/logr/.github/workflows/assign.yaml b/pkg/logr/.github/workflows/assign.yaml new file mode 100644 index 00000000..fdab0142 --- /dev/null +++ b/pkg/logr/.github/workflows/assign.yaml @@ -0,0 +1,27 @@ +name: Assign + +on: + issues: + types: [opened, reopened] + pull_request_target: + types: [opened, reopened] + +permissions: + contents: read + +jobs: + assign: + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + steps: + - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + script: | + github.rest.issues.addAssignees({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + assignees: ['thockin', 'pohly'] + }) diff --git a/pkg/logr/.github/workflows/lint.yaml b/pkg/logr/.github/workflows/lint.yaml new file mode 100644 index 00000000..d0bc3c18 --- /dev/null +++ b/pkg/logr/.github/workflows/lint.yaml @@ -0,0 +1,29 @@ +name: Run lint + +on: [ push, pull_request ] + +permissions: + contents: read + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: Update Go + uses: actions/setup-go@v5.5.0 + with: + go-version: '>=1.21.0' + cache: false + - name: Lint + uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8.0.0 + with: + # version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version + version: latest + + # Optional: show only new issues if it's a pull request. The default value is `false`. + # only-new-issues: true + + # Read args from .golangci.yaml + # args: diff --git a/pkg/logr/.github/workflows/scorecard.yml b/pkg/logr/.github/workflows/scorecard.yml new file mode 100644 index 00000000..470b0c06 --- /dev/null +++ b/pkg/logr/.github/workflows/scorecard.yml @@ -0,0 +1,64 @@ +# This workflow uses actions that are not certified by GitHub. They are provided +# by a third-party and are governed by separate terms of service, privacy +# policy, and support documentation. + +name: Scorecard supply-chain security +on: + # For Branch-Protection check. Only the default branch is supported. See + # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection + branch_protection_rule: + # To guarantee Maintained check is occasionally updated. See + # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained + schedule: + - cron: '28 21 * * 1' + push: + branches: [ "master" ] + +# Declare default permissions as read only. +permissions: read-all + +jobs: + analysis: + name: Scorecard analysis + runs-on: ubuntu-latest + permissions: + # Needed to upload the results to code-scanning dashboard. + security-events: write + # Needed to publish results and get a badge (see publish_results below). + id-token: write + + steps: + - name: "Checkout code" + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + + - name: "Run analysis" + uses: ossf/scorecard-action@f49aabe0b5af0936a0987cfb85d86b75731b0186 # v2.4.1 + with: + results_file: results.sarif + results_format: sarif + # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: + # you want to enable the Branch-Protection check on a *public* repository, or + # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat. + # repo_token: ${{ secrets.SCORECARD_TOKEN }} + + # - Publish results to OpenSSF REST API for easy access by consumers + # - Allows the repository to include the Scorecard badge. + # - See https://github.com/ossf/scorecard-action#publishing-results. + publish_results: true + + # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF + # format to the repository Actions tab. + - name: "Upload artifact" + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: SARIF file + path: results.sarif + retention-days: 5 + + # Upload the results to GitHub's code scanning dashboard. + - name: "Upload to code-scanning" + uses: github/codeql-action/upload-sarif@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 + with: + sarif_file: results.sarif diff --git a/pkg/logr/.github/workflows/tests.yaml b/pkg/logr/.github/workflows/tests.yaml new file mode 100644 index 00000000..cbaf9387 --- /dev/null +++ b/pkg/logr/.github/workflows/tests.yaml @@ -0,0 +1,25 @@ +name: Run tests + +on: [ push, pull_request ] + +permissions: + contents: read + +jobs: + test: + strategy: + matrix: + version: [ '1.18', '1.19', '1.20', '1.21', '1.22', '1.23', '1.24' ] + platform: [ ubuntu-latest, macos-latest, windows-latest ] + runs-on: ${{ matrix.platform }} + steps: + - name: Install Go + uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 + with: + go-version: ${{ matrix.version }} + - name: Checkout code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: Build + run: go build -v ./... + - name: Test + run: go test -v -race ./... diff --git a/pkg/logr/.golangci.yaml b/pkg/logr/.golangci.yaml new file mode 100644 index 00000000..0ed62c1a --- /dev/null +++ b/pkg/logr/.golangci.yaml @@ -0,0 +1,28 @@ +version: "2" + +run: + timeout: 1m + tests: true + +linters: + default: none + enable: # please keep this alphabetized + - asasalint + - asciicheck + - copyloopvar + - dupl + - errcheck + - forcetypeassert + - goconst + - gocritic + - govet + - ineffassign + - misspell + - musttag + - revive + - staticcheck + - unused + +issues: + max-issues-per-linter: 0 + max-same-issues: 10 diff --git a/pkg/logr/CHANGELOG.md b/pkg/logr/CHANGELOG.md new file mode 100644 index 00000000..c3569600 --- /dev/null +++ b/pkg/logr/CHANGELOG.md @@ -0,0 +1,6 @@ +# CHANGELOG + +## v1.0.0-rc1 + +This is the first logged release. Major changes (including breaking changes) +have occurred since earlier tags. diff --git a/pkg/logr/CONTRIBUTING.md b/pkg/logr/CONTRIBUTING.md new file mode 100644 index 00000000..5d37e294 --- /dev/null +++ b/pkg/logr/CONTRIBUTING.md @@ -0,0 +1,17 @@ +# Contributing + +Logr is open to pull-requests, provided they fit within the intended scope of +the project. Specifically, this library aims to be VERY small and minimalist, +with no external dependencies. + +## Compatibility + +This project intends to follow [semantic versioning](http://semver.org) and +is very strict about compatibility. Any proposed changes MUST follow those +rules. + +## Performance + +As a logging library, logr must be as light-weight as possible. Any proposed +code change must include results of running the [benchmark](./benchmark) +before and after the change. diff --git a/pkg/logr/LICENSE b/pkg/logr/LICENSE new file mode 100644 index 00000000..8dada3ed --- /dev/null +++ b/pkg/logr/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/pkg/logr/README.md b/pkg/logr/README.md new file mode 100644 index 00000000..7c7f0c69 --- /dev/null +++ b/pkg/logr/README.md @@ -0,0 +1,407 @@ +# A minimal logging API for Go + +[![Go Reference](https://pkg.go.dev/badge/github.com/go-logr/logr.svg)](https://pkg.go.dev/github.com/go-logr/logr) +[![Go Report Card](https://goreportcard.com/badge/github.com/go-logr/logr)](https://goreportcard.com/report/github.com/go-logr/logr) +[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/go-logr/logr/badge)](https://securityscorecards.dev/viewer/?platform=github.com&org=go-logr&repo=logr) + +logr offers an(other) opinion on how Go programs and libraries can do logging +without becoming coupled to a particular logging implementation. This is not +an implementation of logging - it is an API. In fact it is two APIs with two +different sets of users. + +The `Logger` type is intended for application and library authors. It provides +a relatively small API which can be used everywhere you want to emit logs. It +defers the actual act of writing logs (to files, to stdout, or whatever) to the +`LogSink` interface. + +The `LogSink` interface is intended for logging library implementers. It is a +pure interface which can be implemented by logging frameworks to provide the actual logging +functionality. + +This decoupling allows application and library developers to write code in +terms of `logr.Logger` (which has very low dependency fan-out) while the +implementation of logging is managed "up stack" (e.g. in or near `main()`.) +Application developers can then switch out implementations as necessary. + +Many people assert that libraries should not be logging, and as such efforts +like this are pointless. Those people are welcome to convince the authors of +the tens-of-thousands of libraries that *DO* write logs that they are all +wrong. In the meantime, logr takes a more practical approach. + +## Typical usage + +Somewhere, early in an application's life, it will make a decision about which +logging library (implementation) it actually wants to use. Something like: + +``` + func main() { + // ... other setup code ... + + // Create the "root" logger. We have chosen the "logimpl" implementation, + // which takes some initial parameters and returns a logr.Logger. + logger := logimpl.New(param1, param2) + + // ... other setup code ... +``` + +Most apps will call into other libraries, create structures to govern the flow, +etc. The `logr.Logger` object can be passed to these other libraries, stored +in structs, or even used as a package-global variable, if needed. For example: + +``` + app := createTheAppObject(logger) + app.Run() +``` + +Outside of this early setup, no other packages need to know about the choice of +implementation. They write logs in terms of the `logr.Logger` that they +received: + +``` + type appObject struct { + // ... other fields ... + logger logr.Logger + // ... other fields ... + } + + func (app *appObject) Run() { + app.logger.Info("starting up", "timestamp", time.Now()) + + // ... app code ... +``` + +## Background + +If the Go standard library had defined an interface for logging, this project +probably would not be needed. Alas, here we are. + +When the Go developers started developing such an interface with +[slog](https://github.com/golang/go/issues/56345), they adopted some of the +logr design but also left out some parts and changed others: + +| Feature | logr | slog | +|---------|------|------| +| High-level API | `Logger` (passed by value) | `Logger` (passed by [pointer](https://github.com/golang/go/issues/59126)) | +| Low-level API | `LogSink` | `Handler` | +| Stack unwinding | done by `LogSink` | done by `Logger` | +| Skipping helper functions | `WithCallDepth`, `WithCallStackHelper` | [not supported by Logger](https://github.com/golang/go/issues/59145) | +| Generating a value for logging on demand | `Marshaler` | `LogValuer` | +| Log levels | >= 0, higher meaning "less important" | positive and negative, with 0 for "info" and higher meaning "more important" | +| Error log entries | always logged, don't have a verbosity level | normal log entries with level >= `LevelError` | +| Passing logger via context | `NewContext`, `FromContext` | no API | +| Adding a name to a logger | `WithName` | no API | +| Modify verbosity of log entries in a call chain | `V` | no API | +| Grouping of key/value pairs | not supported | `WithGroup`, `GroupValue` | +| Pass context for extracting additional values | no API | API variants like `InfoCtx` | + +The high-level slog API is explicitly meant to be one of many different APIs +that can be layered on top of a shared `slog.Handler`. logr is one such +alternative API, with [interoperability](#slog-interoperability) provided by +some conversion functions. + +### Inspiration + +Before you consider this package, please read [this blog post by the +inimitable Dave Cheney][warning-makes-no-sense]. We really appreciate what +he has to say, and it largely aligns with our own experiences. + +### Differences from Dave's ideas + +The main differences are: + +1. Dave basically proposes doing away with the notion of a logging API in favor +of `fmt.Printf()`. We disagree, especially when you consider things like output +locations, timestamps, file and line decorations, and structured logging. This +package restricts the logging API to just 2 types of logs: info and error. + +Info logs are things you want to tell the user which are not errors. Error +logs are, well, errors. If your code receives an `error` from a subordinate +function call and is logging that `error` *and not returning it*, use error +logs. + +2. Verbosity-levels on info logs. This gives developers a chance to indicate +arbitrary grades of importance for info logs, without assigning names with +semantic meaning such as "warning", "trace", and "debug." Superficially this +may feel very similar, but the primary difference is the lack of semantics. +Because verbosity is a numerical value, it's safe to assume that an app running +with higher verbosity means more (and less important) logs will be generated. + +## Implementations (non-exhaustive) + +There are implementations for the following logging libraries: + +- **a function** (can bridge to non-structured libraries): [funcr](https://github.com/go-logr/logr/tree/master/funcr) +- **a testing.T** (for use in Go tests, with JSON-like output): [testr](https://github.com/go-logr/logr/tree/master/testr) +- **github.com/google/glog**: [glogr](https://github.com/go-logr/glogr) +- **k8s.io/klog** (for Kubernetes): [klogr](https://git.k8s.io/klog/klogr) +- **a testing.T** (with klog-like text output): [ktesting](https://git.k8s.io/klog/ktesting) +- **go.uber.org/zap**: [zapr](https://github.com/go-logr/zapr) +- **log** (the Go standard library logger): [stdr](https://github.com/go-logr/stdr) +- **github.com/sirupsen/logrus**: [logrusr](https://github.com/bombsimon/logrusr) +- **github.com/wojas/genericr**: [genericr](https://github.com/wojas/genericr) (makes it easy to implement your own backend) +- **logfmt** (Heroku style [logging](https://www.brandur.org/logfmt)): [logfmtr](https://github.com/iand/logfmtr) +- **github.com/rs/zerolog**: [zerologr](https://github.com/go-logr/zerologr) +- **github.com/go-kit/log**: [gokitlogr](https://github.com/tonglil/gokitlogr) (also compatible with github.com/go-kit/kit/log since v0.12.0) +- **bytes.Buffer** (writing to a buffer): [bufrlogr](https://github.com/tonglil/buflogr) (useful for ensuring values were logged, like during testing) + +## slog interoperability + +Interoperability goes both ways, using the `logr.Logger` API with a `slog.Handler` +and using the `slog.Logger` API with a `logr.LogSink`. `FromSlogHandler` and +`ToSlogHandler` convert between a `logr.Logger` and a `slog.Handler`. +As usual, `slog.New` can be used to wrap such a `slog.Handler` in the high-level +slog API. + +### Using a `logr.LogSink` as backend for slog + +Ideally, a logr sink implementation should support both logr and slog by +implementing both the normal logr interface(s) and `SlogSink`. Because +of a conflict in the parameters of the common `Enabled` method, it is [not +possible to implement both slog.Handler and logr.Sink in the same +type](https://github.com/golang/go/issues/59110). + +If both are supported, log calls can go from the high-level APIs to the backend +without the need to convert parameters. `FromSlogHandler` and `ToSlogHandler` can +convert back and forth without adding additional wrappers, with one exception: +when `Logger.V` was used to adjust the verbosity for a `slog.Handler`, then +`ToSlogHandler` has to use a wrapper which adjusts the verbosity for future +log calls. + +Such an implementation should also support values that implement specific +interfaces from both packages for logging (`logr.Marshaler`, `slog.LogValuer`, +`slog.GroupValue`). logr does not convert those. + +Not supporting slog has several drawbacks: +- Recording source code locations works correctly if the handler gets called + through `slog.Logger`, but may be wrong in other cases. That's because a + `logr.Sink` does its own stack unwinding instead of using the program counter + provided by the high-level API. +- slog levels <= 0 can be mapped to logr levels by negating the level without a + loss of information. But all slog levels > 0 (e.g. `slog.LevelWarning` as + used by `slog.Logger.Warn`) must be mapped to 0 before calling the sink + because logr does not support "more important than info" levels. +- The slog group concept is supported by prefixing each key in a key/value + pair with the group names, separated by a dot. For structured output like + JSON it would be better to group the key/value pairs inside an object. +- Special slog values and interfaces don't work as expected. +- The overhead is likely to be higher. + +These drawbacks are severe enough that applications using a mixture of slog and +logr should switch to a different backend. + +### Using a `slog.Handler` as backend for logr + +Using a plain `slog.Handler` without support for logr works better than the +other direction: +- All logr verbosity levels can be mapped 1:1 to their corresponding slog level + by negating them. +- Stack unwinding is done by the `SlogSink` and the resulting program + counter is passed to the `slog.Handler`. +- Names added via `Logger.WithName` are gathered and recorded in an additional + attribute with `logger` as key and the names separated by slash as value. +- `Logger.Error` is turned into a log record with `slog.LevelError` as level + and an additional attribute with `err` as key, if an error was provided. + +The main drawback is that `logr.Marshaler` will not be supported. Types should +ideally support both `logr.Marshaler` and `slog.Valuer`. If compatibility +with logr implementations without slog support is not important, then +`slog.Valuer` is sufficient. + +### Context support for slog + +Storing a logger in a `context.Context` is not supported by +slog. `NewContextWithSlogLogger` and `FromContextAsSlogLogger` can be +used to fill this gap. They store and retrieve a `slog.Logger` pointer +under the same context key that is also used by `NewContext` and +`FromContext` for `logr.Logger` value. + +When `NewContextWithSlogLogger` is followed by `FromContext`, the latter will +automatically convert the `slog.Logger` to a +`logr.Logger`. `FromContextAsSlogLogger` does the same for the other direction. + +With this approach, binaries which use either slog or logr are as efficient as +possible with no unnecessary allocations. This is also why the API stores a +`slog.Logger` pointer: when storing a `slog.Handler`, creating a `slog.Logger` +on retrieval would need to allocate one. + +The downside is that switching back and forth needs more allocations. Because +logr is the API that is already in use by different packages, in particular +Kubernetes, the recommendation is to use the `logr.Logger` API in code which +uses contextual logging. + +An alternative to adding values to a logger and storing that logger in the +context is to store the values in the context and to configure a logging +backend to extract those values when emitting log entries. This only works when +log calls are passed the context, which is not supported by the logr API. + +With the slog API, it is possible, but not +required. https://github.com/veqryn/slog-context is a package for slog which +provides additional support code for this approach. It also contains wrappers +for the context functions in logr, so developers who prefer to not use the logr +APIs directly can use those instead and the resulting code will still be +interoperable with logr. + +## FAQ + +### Conceptual + +#### Why structured logging? + +- **Structured logs are more easily queryable**: Since you've got + key-value pairs, it's much easier to query your structured logs for + particular values by filtering on the contents of a particular key -- + think searching request logs for error codes, Kubernetes reconcilers for + the name and namespace of the reconciled object, etc. + +- **Structured logging makes it easier to have cross-referenceable logs**: + Similarly to searchability, if you maintain conventions around your + keys, it becomes easy to gather all log lines related to a particular + concept. + +- **Structured logs allow better dimensions of filtering**: if you have + structure to your logs, you've got more precise control over how much + information is logged -- you might choose in a particular configuration + to log certain keys but not others, only log lines where a certain key + matches a certain value, etc., instead of just having v-levels and names + to key off of. + +- **Structured logs better represent structured data**: sometimes, the + data that you want to log is inherently structured (think tuple-link + objects.) Structured logs allow you to preserve that structure when + outputting. + +#### Why V-levels? + +**V-levels give operators an easy way to control the chattiness of log +operations**. V-levels provide a way for a given package to distinguish +the relative importance or verbosity of a given log message. Then, if +a particular logger or package is logging too many messages, the user +of the package can simply change the v-levels for that library. + +#### Why not named levels, like Info/Warning/Error? + +Read [Dave Cheney's post][warning-makes-no-sense]. Then read [Differences +from Dave's ideas](#differences-from-daves-ideas). + +#### Why not allow format strings, too? + +**Format strings negate many of the benefits of structured logs**: + +- They're not easily searchable without resorting to fuzzy searching, + regular expressions, etc. + +- They don't store structured data well, since contents are flattened into + a string. + +- They're not cross-referenceable. + +- They don't compress easily, since the message is not constant. + +(Unless you turn positional parameters into key-value pairs with numerical +keys, at which point you've gotten key-value logging with meaningless +keys.) + +### Practical + +#### Why key-value pairs, and not a map? + +Key-value pairs are *much* easier to optimize, especially around +allocations. Zap (a structured logger that inspired logr's interface) has +[performance measurements](https://github.com/uber-go/zap#performance) +that show this quite nicely. + +While the interface ends up being a little less obvious, you get +potentially better performance, plus avoid making users type +`map[string]string{}` every time they want to log. + +#### What if my V-levels differ between libraries? + +That's fine. Control your V-levels on a per-logger basis, and use the +`WithName` method to pass different loggers to different libraries. + +Generally, you should take care to ensure that you have relatively +consistent V-levels within a given logger, however, as this makes deciding +on what verbosity of logs to request easier. + +#### But I really want to use a format string! + +That's not actually a question. Assuming your question is "how do +I convert my mental model of logging with format strings to logging with +constant messages": + +1. Figure out what the error actually is, as you'd write in a TL;DR style, + and use that as a message. + +2. For every place you'd write a format specifier, look to the word before + it, and add that as a key value pair. + +For instance, consider the following examples (all taken from spots in the +Kubernetes codebase): + +- `klog.V(4).Infof("Client is returning errors: code %v, error %v", + responseCode, err)` becomes `logger.Error(err, "client returned an + error", "code", responseCode)` + +- `klog.V(4).Infof("Got a Retry-After %ds response for attempt %d to %v", + seconds, retries, url)` becomes `logger.V(4).Info("got a retry-after + response when requesting url", "attempt", retries, "after + seconds", seconds, "url", url)` + +If you *really* must use a format string, use it in a key's value, and +call `fmt.Sprintf` yourself. For instance: `log.Printf("unable to +reflect over type %T")` becomes `logger.Info("unable to reflect over +type", "type", fmt.Sprintf("%T"))`. In general though, the cases where +this is necessary should be few and far between. + +#### How do I choose my V-levels? + +This is basically the only hard constraint: increase V-levels to denote +more verbose or more debug-y logs. + +Otherwise, you can start out with `0` as "you always want to see this", +`1` as "common logging that you might *possibly* want to turn off", and +`10` as "I would like to performance-test your log collection stack." + +Then gradually choose levels in between as you need them, working your way +down from 10 (for debug and trace style logs) and up from 1 (for chattier +info-type logs). For reference, slog pre-defines -4 for debug logs +(corresponds to 4 in logr), which matches what is +[recommended for Kubernetes](https://github.com/kubernetes/community/blob/master/contributors/devel/sig-instrumentation/logging.md#what-method-to-use). + +#### How do I choose my keys? + +Keys are fairly flexible, and can hold more or less any string +value. For best compatibility with implementations and consistency +with existing code in other projects, there are a few conventions you +should consider. + +- Make your keys human-readable. +- Constant keys are generally a good idea. +- Be consistent across your codebase. +- Keys should naturally match parts of the message string. +- Use lower case for simple keys and + [lowerCamelCase](https://en.wiktionary.org/wiki/lowerCamelCase) for + more complex ones. Kubernetes is one example of a project that has + [adopted that + convention](https://github.com/kubernetes/community/blob/HEAD/contributors/devel/sig-instrumentation/migration-to-structured-logging.md#name-arguments). + +While key names are mostly unrestricted (and spaces are acceptable), +it's generally a good idea to stick to printable ascii characters, or at +least match the general character set of your log lines. + +#### Why should keys be constant values? + +The point of structured logging is to make later log processing easier. Your +keys are, effectively, the schema of each log message. If you use different +keys across instances of the same log line, you will make your structured logs +much harder to use. `Sprintf()` is for values, not for keys! + +#### Why is this not a pure interface? + +The Logger type is implemented as a struct in order to allow the Go compiler to +optimize things like high-V `Info` logs that are not triggered. Not all of +these implementations are implemented yet, but this structure was suggested as +a way to ensure they *can* be implemented. All of the real work is behind the +`LogSink` interface. + +[warning-makes-no-sense]: http://dave.cheney.net/2015/11/05/lets-talk-about-logging diff --git a/pkg/logr/SECURITY.md b/pkg/logr/SECURITY.md new file mode 100644 index 00000000..1ca756fc --- /dev/null +++ b/pkg/logr/SECURITY.md @@ -0,0 +1,18 @@ +# Security Policy + +If you have discovered a security vulnerability in this project, please report it +privately. **Do not disclose it as a public issue.** This gives us time to work with you +to fix the issue before public exposure, reducing the chance that the exploit will be +used before a patch is released. + +You may submit the report in the following ways: + +- send an email to go-logr-security@googlegroups.com +- send us a [private vulnerability report](https://github.com/go-logr/logr/security/advisories/new) + +Please provide the following information in your report: + +- A description of the vulnerability and its impact +- How to reproduce the issue + +We ask that you give us 90 days to work on a fix before public exposure. diff --git a/pkg/logr/_tools/apidiff.sh b/pkg/logr/_tools/apidiff.sh new file mode 100755 index 00000000..81ec70aa --- /dev/null +++ b/pkg/logr/_tools/apidiff.sh @@ -0,0 +1,127 @@ +#!/usr/bin/env bash + +# Copyright 2020 The Kubernetes Authors. +# Copyright 2021 The logr Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -o errexit +set -o nounset +set -o pipefail + +function usage { + local script="$(basename $0)" + + echo >&2 "Usage: ${script} [-r | -d ] + +This script should be run at the root of a module. + +-r + Compare the exported API of the local working copy with the + exported API of the local repo at the specified branch or tag. + +-d + Compare the exported API of the local working copy with the + exported API of the specified directory, which should point + to the root of a different version of the same module. + +Examples: + ${script} -r master + ${script} -r v1.10.0 + ${script} -r release-1.10 + ${script} -d /path/to/historical/version +" + exit 1 +} + +ref="" +dir="" +while getopts r:d: o +do case "$o" in + r) ref="$OPTARG";; + d) dir="$OPTARG";; + [?]) usage;; + esac +done + +# If REF and DIR are empty, print usage and error +if [[ -z "${ref}" && -z "${dir}" ]]; then + usage; +fi +# If REF and DIR are both set, print usage and error +if [[ -n "${ref}" && -n "${dir}" ]]; then + usage; +fi + +if ! which apidiff > /dev/null; then + echo "Installing golang.org/x/exp/cmd/apidiff" + pushd "${TMPDIR:-/tmp}" > /dev/null + GO111MODULE=off go get golang.org/x/exp/cmd/apidiff + popd > /dev/null +fi + +output=$(mktemp -d -t "apidiff.output.XXXX") +cleanup_output () { rm -fr "${output}"; } +trap cleanup_output EXIT + +# If ref is set, clone . to temp dir at $ref, and set $dir to the temp dir +clone="" +base="${dir}" +if [[ -n "${ref}" ]]; then + base="${ref}" + clone=$(mktemp -d -t "apidiff.clone.XXXX") + cleanup_clone_and_output () { rm -fr "${clone}"; cleanup_output; } + trap cleanup_clone_and_output EXIT + git clone . -q --no-tags "${clone}" + git -C "${clone}" co "${ref}" + dir="${clone}" +fi + +pushd "${dir}" >/dev/null + echo "Inspecting API of ${base}" + go list ./... > packages.txt + for pkg in $(cat packages.txt); do + mkdir -p "${output}/${pkg}" + apidiff -w "${output}/${pkg}/apidiff.output" "${pkg}" + done +popd >/dev/null + +retval=0 + +echo "Comparing with ${base}" +for pkg in $(go list ./...); do + # New packages are ok + if [ ! -f "${output}/${pkg}/apidiff.output" ]; then + continue + fi + + # Check for incompatible changes to previous packages + incompatible=$(apidiff -incompatible "${output}/${pkg}/apidiff.output" "${pkg}") + if [[ -n "${incompatible}" ]]; then + echo >&2 "FAIL: ${pkg} contains incompatible changes: +${incompatible} +" + retval=1 + fi +done + +# Check for removed packages +removed=$(comm -23 "${dir}/packages.txt" <(go list ./...)) +if [[ -n "${removed}" ]]; then + echo >&2 "FAIL: removed packages: +${removed} +" + retval=1 +fi + +exit $retval diff --git a/pkg/logr/benchmark/README.md b/pkg/logr/benchmark/README.md new file mode 100644 index 00000000..f2687494 --- /dev/null +++ b/pkg/logr/benchmark/README.md @@ -0,0 +1,15 @@ +# Benchmarking logr + +Any major changes to the logr library must be benchmarked before and after the +change. + +## Running the benchmark + +``` +$ go test -bench='.' -test.benchmem ./benchmark/ +``` + +## Fixing the benchmark + +If you think this benchmark can be improved, you are probably correct! PRs are +very welcome. diff --git a/pkg/logr/benchmark/benchmark_slog_test.go b/pkg/logr/benchmark/benchmark_slog_test.go new file mode 100644 index 00000000..6993e8ca --- /dev/null +++ b/pkg/logr/benchmark/benchmark_slog_test.go @@ -0,0 +1,142 @@ +//go:build go1.21 +// +build go1.21 + +/* +Copyright 2021 The logr Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package logr + +import ( + testing "github.com/CodSpeedHQ/codspeed-go/testing/testing" + "log/slog" + "os" + + "github.com/CodSpeedHQ/codspeed-go/pkg/logr" +) + +// +// slogSink wrapper of discard +// + +func BenchmarkSlogSinkLogInfoOneArg(b *testing.B) { + var log logr.Logger = logr.FromSlogHandler(logr.ToSlogHandler(logr.Discard())) //nolint:staticcheck + doInfoOneArg(b, log) +} + +func BenchmarkSlogSinkLogInfoSeveralArgs(b *testing.B) { + var log logr.Logger = logr.FromSlogHandler(logr.ToSlogHandler(logr.Discard())) //nolint:staticcheck + doInfoSeveralArgs(b, log) +} + +func BenchmarkSlogSinkLogInfoWithValues(b *testing.B) { + var log logr.Logger = logr.FromSlogHandler(logr.ToSlogHandler(logr.Discard())) //nolint:staticcheck + doInfoWithValues(b, log) +} + +func BenchmarkSlogSinkLogV0Info(b *testing.B) { + var log logr.Logger = logr.FromSlogHandler(logr.ToSlogHandler(logr.Discard())) //nolint:staticcheck + doV0Info(b, log) +} + +func BenchmarkSlogSinkLogV9Info(b *testing.B) { + var log logr.Logger = logr.FromSlogHandler(logr.ToSlogHandler(logr.Discard())) //nolint:staticcheck + doV9Info(b, log) +} + +func BenchmarkSlogSinkLogError(b *testing.B) { + var log logr.Logger = logr.FromSlogHandler(logr.ToSlogHandler(logr.Discard())) //nolint:staticcheck + doError(b, log) +} + +func BenchmarkSlogSinkWithValues(b *testing.B) { + var log logr.Logger = logr.FromSlogHandler(logr.ToSlogHandler(logr.Discard())) //nolint:staticcheck + doWithValues(b, log) +} + +func BenchmarkSlogSinkWithName(b *testing.B) { + var log logr.Logger = logr.FromSlogHandler(logr.ToSlogHandler(logr.Discard())) //nolint:staticcheck + doWithName(b, log) +} + +// +// slogSink wrapper of slog's JSONHandler, for comparison +// + +func makeSlogJSONLogger() logr.Logger { + devnull, _ := os.Open("/dev/null") + handler := slog.NewJSONHandler(devnull, nil) + return logr.FromSlogHandler(handler) +} + +func BenchmarkSlogJSONLogInfoOneArg(b *testing.B) { + var log logr.Logger = makeSlogJSONLogger() //nolint:staticcheck + doInfoOneArg(b, log) +} + +func BenchmarkSlogJSONLogInfoSeveralArgs(b *testing.B) { + var log logr.Logger = makeSlogJSONLogger() //nolint:staticcheck + doInfoSeveralArgs(b, log) +} + +func BenchmarkSlogJSONLogInfoWithValues(b *testing.B) { + var log logr.Logger = makeSlogJSONLogger() //nolint:staticcheck + doInfoWithValues(b, log) +} + +func BenchmarkSlogJSONLogV0Info(b *testing.B) { + var log logr.Logger = makeSlogJSONLogger() //nolint:staticcheck + doV0Info(b, log) +} + +func BenchmarkSlogJSONLogV9Info(b *testing.B) { + var log logr.Logger = makeSlogJSONLogger() //nolint:staticcheck + doV9Info(b, log) +} + +func BenchmarkSlogJSONLogError(b *testing.B) { + var log logr.Logger = makeSlogJSONLogger() //nolint:staticcheck + doError(b, log) +} + +func BenchmarkSlogJSONLogWithValues(b *testing.B) { + var log logr.Logger = makeSlogJSONLogger() //nolint:staticcheck + doWithValues(b, log) +} + +func BenchmarkSlogJSONWithName(b *testing.B) { + var log logr.Logger = makeSlogJSONLogger() //nolint:staticcheck + doWithName(b, log) +} + +func BenchmarkSlogJSONWithCallDepth(b *testing.B) { + var log logr.Logger = makeSlogJSONLogger() //nolint:staticcheck + doWithCallDepth(b, log) +} + +func BenchmarkSlogJSONLogInfoStringerValue(b *testing.B) { + var log logr.Logger = makeSlogJSONLogger() //nolint:staticcheck + doStringerValue(b, log) +} + +func BenchmarkSlogJSONLogInfoErrorValue(b *testing.B) { + var log logr.Logger = makeSlogJSONLogger() //nolint:staticcheck + doErrorValue(b, log) +} + +func BenchmarkSlogJSONLogInfoMarshalerValue(b *testing.B) { + var log logr.Logger = makeSlogJSONLogger() //nolint:staticcheck + doMarshalerValue(b, log) +} diff --git a/pkg/logr/benchmark/benchmark_test.go b/pkg/logr/benchmark/benchmark_test.go new file mode 100644 index 00000000..48c4dc9b --- /dev/null +++ b/pkg/logr/benchmark/benchmark_test.go @@ -0,0 +1,283 @@ +/* +Copyright 2021 The logr Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package logr + +import ( + "fmt" + testing "github.com/CodSpeedHQ/codspeed-go/testing/testing" + + "github.com/CodSpeedHQ/codspeed-go/pkg/logr" + "github.com/CodSpeedHQ/codspeed-go/pkg/logr/funcr" +) + +//go:noinline +func doInfoOneArg(b *testing.B, log logr.Logger) { + for i := 0; i < b.N; i++ { + log.Info("this is", "a", "string") + } +} + +//go:noinline +func doInfoSeveralArgs(b *testing.B, log logr.Logger) { + for i := 0; i < b.N; i++ { + log.Info("multi", + "bool", true, "string", "str", "int", 42, + "float", 3.14, "struct", struct{ X, Y int }{93, 76}) + } +} + +//go:noinline +func doInfoWithValues(b *testing.B, log logr.Logger) { + log = log.WithValues("k1", "str", "k2", 222, "k3", true, "k4", 1.0) + for i := 0; i < b.N; i++ { + log.Info("multi", + "bool", true, "string", "str", "int", 42, + "float", 3.14, "struct", struct{ X, Y int }{93, 76}) + } +} + +//go:noinline +func doV0Info(b *testing.B, log logr.Logger) { + for i := 0; i < b.N; i++ { + log.V(0).Info("multi", + "bool", true, "string", "str", "int", 42, + "float", 3.14, "struct", struct{ X, Y int }{93, 76}) + } +} + +//go:noinline +func doV9Info(b *testing.B, log logr.Logger) { + for i := 0; i < b.N; i++ { + log.V(9).Info("multi", + "bool", true, "string", "str", "int", 42, + "float", 3.14, "struct", struct{ X, Y int }{93, 76}) + } +} + +//go:noinline +func doError(b *testing.B, log logr.Logger) { + err := fmt.Errorf("error message") + for i := 0; i < b.N; i++ { + log.Error(err, "multi", + "bool", true, "string", "str", "int", 42, + "float", 3.14, "struct", struct{ X, Y int }{93, 76}) + } +} + +//go:noinline +func doWithValues(b *testing.B, log logr.Logger) { + for i := 0; i < b.N; i++ { + l := log.WithValues("k1", "v1", "k2", "v2") + _ = l + } +} + +//go:noinline +func doWithName(b *testing.B, log logr.Logger) { + for i := 0; i < b.N; i++ { + l := log.WithName("name") + _ = l + } +} + +//go:noinline +func doWithCallDepth(b *testing.B, log logr.Logger) { + for i := 0; i < b.N; i++ { + l := log.WithCallDepth(1) + _ = l + } +} + +type Tstringer struct{ s string } + +func (t Tstringer) String() string { + return t.s +} + +//go:noinline +func doStringerValue(b *testing.B, log logr.Logger) { + for i := 0; i < b.N; i++ { + log.Info("this is", "a", Tstringer{"stringer"}) + } +} + +type Terror struct{ s string } + +func (t Terror) Error() string { + return t.s +} + +//go:noinline +func doErrorValue(b *testing.B, log logr.Logger) { + for i := 0; i < b.N; i++ { + log.Info("this is", "an", Terror{"error"}) + } +} + +type Tmarshaler struct{ s string } + +func (t Tmarshaler) MarshalLog() any { + return t.s +} + +//go:noinline +func doMarshalerValue(b *testing.B, log logr.Logger) { + for i := 0; i < b.N; i++ { + log.Info("this is", "a", Tmarshaler{"marshaler"}) + } +} + +// +// discard +// + +func BenchmarkDiscardLogInfoOneArg(b *testing.B) { + var log logr.Logger = logr.Discard() //nolint:staticcheck + doInfoOneArg(b, log) +} + +func BenchmarkDiscardLogInfoSeveralArgs(b *testing.B) { + var log logr.Logger = logr.Discard() //nolint:staticcheck + doInfoSeveralArgs(b, log) +} + +func BenchmarkDiscardLogInfoWithValues(b *testing.B) { + var log logr.Logger = logr.Discard() //nolint:staticcheck + doInfoWithValues(b, log) +} + +func BenchmarkDiscardLogV0Info(b *testing.B) { + var log logr.Logger = logr.Discard() //nolint:staticcheck + doV0Info(b, log) +} + +func BenchmarkDiscardLogV9Info(b *testing.B) { + var log logr.Logger = logr.Discard() //nolint:staticcheck + doV9Info(b, log) +} + +func BenchmarkDiscardLogError(b *testing.B) { + var log logr.Logger = logr.Discard() //nolint:staticcheck + doError(b, log) +} + +func BenchmarkDiscardWithValues(b *testing.B) { + var log logr.Logger = logr.Discard() //nolint:staticcheck + doWithValues(b, log) +} + +func BenchmarkDiscardWithName(b *testing.B) { + var log logr.Logger = logr.Discard() //nolint:staticcheck + doWithName(b, log) +} + +// +// funcr +// + +func noopKV(_, _ string) {} +func noopJSON(_ string) {} + +func BenchmarkFuncrLogInfoOneArg(b *testing.B) { + var log logr.Logger = funcr.New(noopKV, funcr.Options{}) //nolint:staticcheck + doInfoOneArg(b, log) +} + +func BenchmarkFuncrJSONLogInfoOneArg(b *testing.B) { + var log logr.Logger = funcr.NewJSON(noopJSON, funcr.Options{}) //nolint:staticcheck + doInfoOneArg(b, log) +} + +func BenchmarkFuncrLogInfoSeveralArgs(b *testing.B) { + var log logr.Logger = funcr.New(noopKV, funcr.Options{}) //nolint:staticcheck + doInfoSeveralArgs(b, log) +} + +func BenchmarkFuncrJSONLogInfoSeveralArgs(b *testing.B) { + var log logr.Logger = funcr.NewJSON(noopJSON, funcr.Options{}) //nolint:staticcheck + doInfoSeveralArgs(b, log) +} + +func BenchmarkFuncrLogInfoWithValues(b *testing.B) { + var log logr.Logger = funcr.New(noopKV, funcr.Options{}) //nolint:staticcheck + doInfoWithValues(b, log) +} + +func BenchmarkFuncrJSONLogInfoWithValues(b *testing.B) { + var log logr.Logger = funcr.NewJSON(noopJSON, funcr.Options{}) //nolint:staticcheck + doInfoWithValues(b, log) +} + +func BenchmarkFuncrLogV0Info(b *testing.B) { + var log logr.Logger = funcr.New(noopKV, funcr.Options{}) //nolint:staticcheck + doV0Info(b, log) +} + +func BenchmarkFuncrJSONLogV0Info(b *testing.B) { + var log logr.Logger = funcr.NewJSON(noopJSON, funcr.Options{}) //nolint:staticcheck + doV0Info(b, log) +} + +func BenchmarkFuncrLogV9Info(b *testing.B) { + var log logr.Logger = funcr.New(noopKV, funcr.Options{}) //nolint:staticcheck + doV9Info(b, log) +} + +func BenchmarkFuncrJSONLogV9Info(b *testing.B) { + var log logr.Logger = funcr.NewJSON(noopJSON, funcr.Options{}) //nolint:staticcheck + doV9Info(b, log) +} + +func BenchmarkFuncrLogError(b *testing.B) { + var log logr.Logger = funcr.New(noopKV, funcr.Options{}) //nolint:staticcheck + doError(b, log) +} + +func BenchmarkFuncrJSONLogError(b *testing.B) { + var log logr.Logger = funcr.NewJSON(noopJSON, funcr.Options{}) //nolint:staticcheck + doError(b, log) +} + +func BenchmarkFuncrWithValues(b *testing.B) { + var log logr.Logger = funcr.New(noopKV, funcr.Options{}) //nolint:staticcheck + doWithValues(b, log) +} + +func BenchmarkFuncrWithName(b *testing.B) { + var log logr.Logger = funcr.New(noopKV, funcr.Options{}) //nolint:staticcheck + doWithName(b, log) +} + +func BenchmarkFuncrWithCallDepth(b *testing.B) { + var log logr.Logger = funcr.New(noopKV, funcr.Options{}) //nolint:staticcheck + doWithCallDepth(b, log) +} + +func BenchmarkFuncrJSONLogInfoStringerValue(b *testing.B) { + var log logr.Logger = funcr.NewJSON(noopJSON, funcr.Options{}) //nolint:staticcheck + doStringerValue(b, log) +} + +func BenchmarkFuncrJSONLogInfoErrorValue(b *testing.B) { + var log logr.Logger = funcr.NewJSON(noopJSON, funcr.Options{}) //nolint:staticcheck + doErrorValue(b, log) +} + +func BenchmarkFuncrJSONLogInfoMarshalerValue(b *testing.B) { + var log logr.Logger = funcr.NewJSON(noopJSON, funcr.Options{}) //nolint:staticcheck + doMarshalerValue(b, log) +} diff --git a/pkg/logr/context.go b/pkg/logr/context.go new file mode 100644 index 00000000..de8bcc3a --- /dev/null +++ b/pkg/logr/context.go @@ -0,0 +1,33 @@ +/* +Copyright 2023 The logr Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package logr + +// contextKey is how we find Loggers in a context.Context. With Go < 1.21, +// the value is always a Logger value. With Go >= 1.21, the value can be a +// Logger value or a slog.Logger pointer. +type contextKey struct{} + +// notFoundError exists to carry an IsNotFound method. +type notFoundError struct{} + +func (notFoundError) Error() string { + return "no logr.Logger was present" +} + +func (notFoundError) IsNotFound() bool { + return true +} diff --git a/pkg/logr/context_noslog.go b/pkg/logr/context_noslog.go new file mode 100644 index 00000000..f012f9a1 --- /dev/null +++ b/pkg/logr/context_noslog.go @@ -0,0 +1,49 @@ +//go:build !go1.21 +// +build !go1.21 + +/* +Copyright 2019 The logr Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package logr + +import ( + "context" +) + +// FromContext returns a Logger from ctx or an error if no Logger is found. +func FromContext(ctx context.Context) (Logger, error) { + if v, ok := ctx.Value(contextKey{}).(Logger); ok { + return v, nil + } + + return Logger{}, notFoundError{} +} + +// FromContextOrDiscard returns a Logger from ctx. If no Logger is found, this +// returns a Logger that discards all log messages. +func FromContextOrDiscard(ctx context.Context) Logger { + if v, ok := ctx.Value(contextKey{}).(Logger); ok { + return v + } + + return Discard() +} + +// NewContext returns a new Context, derived from ctx, which carries the +// provided Logger. +func NewContext(ctx context.Context, logger Logger) context.Context { + return context.WithValue(ctx, contextKey{}, logger) +} diff --git a/pkg/logr/context_slog.go b/pkg/logr/context_slog.go new file mode 100644 index 00000000..065ef0b8 --- /dev/null +++ b/pkg/logr/context_slog.go @@ -0,0 +1,83 @@ +//go:build go1.21 +// +build go1.21 + +/* +Copyright 2019 The logr Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package logr + +import ( + "context" + "fmt" + "log/slog" +) + +// FromContext returns a Logger from ctx or an error if no Logger is found. +func FromContext(ctx context.Context) (Logger, error) { + v := ctx.Value(contextKey{}) + if v == nil { + return Logger{}, notFoundError{} + } + + switch v := v.(type) { + case Logger: + return v, nil + case *slog.Logger: + return FromSlogHandler(v.Handler()), nil + default: + // Not reached. + panic(fmt.Sprintf("unexpected value type for logr context key: %T", v)) + } +} + +// FromContextAsSlogLogger returns a slog.Logger from ctx or nil if no such Logger is found. +func FromContextAsSlogLogger(ctx context.Context) *slog.Logger { + v := ctx.Value(contextKey{}) + if v == nil { + return nil + } + + switch v := v.(type) { + case Logger: + return slog.New(ToSlogHandler(v)) + case *slog.Logger: + return v + default: + // Not reached. + panic(fmt.Sprintf("unexpected value type for logr context key: %T", v)) + } +} + +// FromContextOrDiscard returns a Logger from ctx. If no Logger is found, this +// returns a Logger that discards all log messages. +func FromContextOrDiscard(ctx context.Context) Logger { + if logger, err := FromContext(ctx); err == nil { + return logger + } + return Discard() +} + +// NewContext returns a new Context, derived from ctx, which carries the +// provided Logger. +func NewContext(ctx context.Context, logger Logger) context.Context { + return context.WithValue(ctx, contextKey{}, logger) +} + +// NewContextWithSlogLogger returns a new Context, derived from ctx, which carries the +// provided slog.Logger. +func NewContextWithSlogLogger(ctx context.Context, logger *slog.Logger) context.Context { + return context.WithValue(ctx, contextKey{}, logger) +} diff --git a/pkg/logr/context_slog_test.go b/pkg/logr/context_slog_test.go new file mode 100644 index 00000000..6034641f --- /dev/null +++ b/pkg/logr/context_slog_test.go @@ -0,0 +1,66 @@ +//go:build go1.21 +// +build go1.21 + +/* +Copyright 2021 The logr Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package logr + +import ( + "context" + testing "github.com/CodSpeedHQ/codspeed-go/testing/testing" + "log/slog" + "os" +) + +func TestContextWithSlog(t *testing.T) { + ctx := context.Background() + + if out := FromContextAsSlogLogger(ctx); out != nil { + t.Errorf("expected no logger, got %#v", out) + } + + // Write as slog... + slogger := slog.New(slog.NewJSONHandler(os.Stderr, nil)) + sctx := NewContextWithSlogLogger(ctx, slogger) + + // ...read as logr + if out, err := FromContext(sctx); err != nil { + t.Errorf("unexpected error: %v", err) + } else if _, ok := out.sink.(*slogSink); !ok { + t.Errorf("expected output to be type *logr.slogSink, got %T", out.sink) + } + + // ...read as slog + if out := FromContextAsSlogLogger(sctx); out == nil { + t.Errorf("expected a *slog.JSONHandler, got nil") + } else if _, ok := out.Handler().(*slog.JSONHandler); !ok { + t.Errorf("expected output to be type *slog.JSONHandler, got %T", out.Handler()) + } + + // Write as logr... + logger := Discard() + lctx := NewContext(ctx, logger) + + // ...read as slog + if out := FromContextAsSlogLogger(lctx); out == nil { + t.Errorf("expected a *log.slogHandler, got nil") + } else if _, ok := out.Handler().(*slogHandler); !ok { + t.Errorf("expected output to be type *logr.slogHandler, got %T", out.Handler()) + } + + // ...read as logr is covered in the non-slog test +} diff --git a/pkg/logr/context_test.go b/pkg/logr/context_test.go new file mode 100644 index 00000000..4b797380 --- /dev/null +++ b/pkg/logr/context_test.go @@ -0,0 +1,50 @@ +/* +Copyright 2021 The logr Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package logr + +import ( + "context" + testing "github.com/CodSpeedHQ/codspeed-go/testing/testing" +) + +func TestContext(t *testing.T) { + ctx := context.Background() + + if out, err := FromContext(ctx); err == nil { + t.Errorf("expected error, got %#v", out) + } else if _, ok := err.(notFoundError); !ok { + t.Errorf("expected a notFoundError, got %#v", err) + } + + out := FromContextOrDiscard(ctx) + if out.sink != nil { + t.Errorf("expected a nil sink, got %#v", out) + } + + sink := &testLogSink{} + logger := New(sink) + lctx := NewContext(ctx, logger) + if out, err := FromContext(lctx); err != nil { + t.Errorf("unexpected error: %v", err) + } else if p, _ := out.sink.(*testLogSink); p != sink { + t.Errorf("expected output to be the same as input, got in=%p, out=%p", sink, p) + } + out = FromContextOrDiscard(lctx) + if p, _ := out.sink.(*testLogSink); p != sink { + t.Errorf("expected output to be the same as input, got in=%p, out=%p", sink, p) + } +} diff --git a/pkg/logr/discard.go b/pkg/logr/discard.go new file mode 100644 index 00000000..99fe8be9 --- /dev/null +++ b/pkg/logr/discard.go @@ -0,0 +1,24 @@ +/* +Copyright 2020 The logr Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package logr + +// Discard returns a Logger that discards all messages logged to it. It can be +// used whenever the caller is not interested in the logs. Logger instances +// produced by this function always compare as equal. +func Discard() Logger { + return New(nil) +} diff --git a/pkg/logr/discard_test.go b/pkg/logr/discard_test.go new file mode 100644 index 00000000..69cb3dd5 --- /dev/null +++ b/pkg/logr/discard_test.go @@ -0,0 +1,54 @@ +/* +Copyright 2020 The logr Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package logr + +import ( + "errors" + testing "github.com/CodSpeedHQ/codspeed-go/testing/testing" + "reflect" +) + +func TestDiscard(t *testing.T) { + l := Discard() + if l.GetSink() != nil { + t.Error("did not return the expected underlying type") + } + // Verify that none of the methods panic, there is not more we can test. + l.WithName("discard").WithValues("z", 5).Info("Hello world") + l.Info("Hello world", "x", 1, "y", 2) + l.V(1).Error(errors.New("foo"), "a", 123) + if l.Enabled() { + t.Error("discard loggers must always be disabled") + } +} + +func TestComparable(t *testing.T) { + a := Discard() + if !reflect.TypeOf(a).Comparable() { + t.Fatal("discard loggers must be comparable") + } + + b := Discard() + if a != b { + t.Fatal("any two discard Loggers must be equal") + } + + c := Discard().V(2) + if b != c { + t.Fatal("any two discard Loggers must be equal") + } +} diff --git a/pkg/logr/example_marshaler_secret_test.go b/pkg/logr/example_marshaler_secret_test.go new file mode 100644 index 00000000..33678580 --- /dev/null +++ b/pkg/logr/example_marshaler_secret_test.go @@ -0,0 +1,47 @@ +/* +Copyright 2021 The logr Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package logr_test + +import ( + "github.com/CodSpeedHQ/codspeed-go/pkg/logr" +) + +// ComplexObjectRef contains more fields than it wants to get logged. +type ComplexObjectRef struct { + Name string + Namespace string + Secret string +} + +func (ref ComplexObjectRef) MarshalLog() any { + return struct { + Name, Namespace string + }{ + Name: ref.Name, + Namespace: ref.Namespace, + } +} + +var _ logr.Marshaler = ComplexObjectRef{} + +func ExampleMarshaler_secret() { + l := NewStdoutLogger() + secret := ComplexObjectRef{Namespace: "kube-system", Name: "some-secret", Secret: "do-not-log-me"} + l.Info("simplified", "secret", secret) + // Output: + // "level"=0 "msg"="simplified" "secret"={"Name"="some-secret" "Namespace"="kube-system"} +} diff --git a/pkg/logr/example_marshaler_test.go b/pkg/logr/example_marshaler_test.go new file mode 100644 index 00000000..bb19e18e --- /dev/null +++ b/pkg/logr/example_marshaler_test.go @@ -0,0 +1,55 @@ +/* +Copyright 2021 The logr Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package logr_test + +import ( + "github.com/CodSpeedHQ/codspeed-go/pkg/logr" +) + +// ObjectRef references a Kubernetes object +type ObjectRef struct { + Name string `json:"name"` + Namespace string `json:"namespace,omitempty"` +} + +func (ref ObjectRef) String() string { + if ref.Namespace != "" { + return ref.Namespace + "/" + ref.Name + } + return ref.Name +} + +func (ref ObjectRef) MarshalLog() any { + // We implement fmt.Stringer for non-structured logging, but we want the + // raw struct when using structured logs. Some logr implementations call + // String if it is present, so we want to convert this struct to something + // that doesn't have that method. + type forLog ObjectRef // methods do not survive type definitions + return forLog(ref) +} + +var _ logr.Marshaler = ObjectRef{} + +func ExampleMarshaler() { + l := NewStdoutLogger() + pod := ObjectRef{Namespace: "kube-system", Name: "some-pod"} + l.Info("as string", "pod", pod.String()) + l.Info("as struct", "pod", pod) + // Output: + // "level"=0 "msg"="as string" "pod"="kube-system/some-pod" + // "level"=0 "msg"="as struct" "pod"={"name"="some-pod" "namespace"="kube-system"} +} diff --git a/pkg/logr/example_slogr_test.go b/pkg/logr/example_slogr_test.go new file mode 100644 index 00000000..2e3b95d8 --- /dev/null +++ b/pkg/logr/example_slogr_test.go @@ -0,0 +1,79 @@ +//go:build go1.21 +// +build go1.21 + +/* +Copyright 2023 The logr Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package logr_test + +import ( + "errors" + "fmt" + "log/slog" + "os" + + "github.com/CodSpeedHQ/codspeed-go/pkg/logr" + "github.com/CodSpeedHQ/codspeed-go/pkg/logr/funcr" +) + +var debugWithoutTime = &slog.HandlerOptions{ + ReplaceAttr: func(_ []string, a slog.Attr) slog.Attr { + if a.Key == "time" { + return slog.Attr{} + } + return a + }, + Level: slog.LevelDebug, +} + +func ExampleFromSlogHandler() { + logrLogger := logr.FromSlogHandler(slog.NewTextHandler(os.Stdout, debugWithoutTime)) + + logrLogger.Info("hello world") + logrLogger.Error(errors.New("fake error"), "ignore me") + logrLogger.WithValues("x", 1, "y", 2).WithValues("str", "abc").WithName("foo").WithName("bar").V(4).Info("with values, verbosity and name") + + // Output: + // level=INFO msg="hello world" + // level=ERROR msg="ignore me" err="fake error" + // level=DEBUG msg="with values, verbosity and name" x=1 y=2 str=abc logger=foo/bar +} + +func ExampleToSlogHandler() { + funcrLogger := funcr.New(func(prefix, args string) { + if prefix != "" { + fmt.Println(prefix, args) + } else { + fmt.Println(args) + } + }, funcr.Options{ + Verbosity: 10, + }) + + slogLogger := slog.New(logr.ToSlogHandler(funcrLogger)) + slogLogger.Info("hello world") + slogLogger.Error("ignore me", "err", errors.New("fake error")) + slogLogger.With("x", 1, "y", 2).WithGroup("group").With("str", "abc").Warn("with values and group") + + slogLogger = slog.New(logr.ToSlogHandler(funcrLogger.V(int(-slog.LevelDebug)))) + slogLogger.Info("info message reduced to debug level") + + // Output: + // "level"=0 "msg"="hello world" + // "msg"="ignore me" "error"=null "err"="fake error" + // "level"=0 "msg"="with values and group" "x"=1 "y"=2 "group"={"str"="abc"} + // "level"=4 "msg"="info message reduced to debug level" +} diff --git a/pkg/logr/example_test.go b/pkg/logr/example_test.go new file mode 100644 index 00000000..e5de3e1a --- /dev/null +++ b/pkg/logr/example_test.go @@ -0,0 +1,100 @@ +/* +Copyright 2021 The logr Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package logr_test + +import ( + "fmt" + + "github.com/CodSpeedHQ/codspeed-go/pkg/logr" + "github.com/CodSpeedHQ/codspeed-go/pkg/logr/funcr" +) + +// NewStdoutLogger returns a logr.Logger that prints to stdout. +func NewStdoutLogger() logr.Logger { + return funcr.New(func(prefix, args string) { + if prefix != "" { + fmt.Printf("%s: %s\n", prefix, args) + } else { + fmt.Println(args) + } + }, funcr.Options{}) +} + +func Example() { + l := NewStdoutLogger() + l.Info("default info log", "stringVal", "value", "intVal", 12345) + l.V(0).Info("V(0) info log", "stringVal", "value", "intVal", 12345) + l.Error(fmt.Errorf("an error"), "error log", "stringVal", "value", "intVal", 12345) + // Output: + // "level"=0 "msg"="default info log" "stringVal"="value" "intVal"=12345 + // "level"=0 "msg"="V(0) info log" "stringVal"="value" "intVal"=12345 + // "msg"="error log" "error"="an error" "stringVal"="value" "intVal"=12345 +} + +func ExampleLogger_Info() { + l := NewStdoutLogger() + l.Info("this is a V(0)-equivalent info log", "stringVal", "value", "intVal", 12345) + // Output: + // "level"=0 "msg"="this is a V(0)-equivalent info log" "stringVal"="value" "intVal"=12345 +} + +func ExampleLogger_Error() { + l := NewStdoutLogger() + l.Error(fmt.Errorf("the error"), "this is an error log", "stringVal", "value", "intVal", 12345) + l.Error(nil, "this is an error log with nil error", "stringVal", "value", "intVal", 12345) + // Output: + // "msg"="this is an error log" "error"="the error" "stringVal"="value" "intVal"=12345 + // "msg"="this is an error log with nil error" "error"=null "stringVal"="value" "intVal"=12345 +} + +func ExampleLogger_WithName() { + l := NewStdoutLogger() + l = l.WithName("name1") + l.Info("this is an info log", "stringVal", "value", "intVal", 12345) + l = l.WithName("name2") + l.Info("this is an info log", "stringVal", "value", "intVal", 12345) + // Output: + // name1: "level"=0 "msg"="this is an info log" "stringVal"="value" "intVal"=12345 + // name1/name2: "level"=0 "msg"="this is an info log" "stringVal"="value" "intVal"=12345 +} + +func ExampleLogger_WithValues() { + l := NewStdoutLogger() + l = l.WithValues("stringVal", "value", "intVal", 12345) + l = l.WithValues("boolVal", true) + l.Info("this is an info log", "floatVal", 3.1415) + // Output: + // "level"=0 "msg"="this is an info log" "stringVal"="value" "intVal"=12345 "boolVal"=true "floatVal"=3.1415 +} + +func ExampleLogger_V() { + l := NewStdoutLogger() + l.V(0).Info("V(0) info log") + l.V(1).Info("V(1) info log") + l.V(2).Info("V(2) info log") + // Output: + // "level"=0 "msg"="V(0) info log" +} + +func ExampleLogger_Enabled() { + l := NewStdoutLogger() + if loggerV := l.V(5); loggerV.Enabled() { + // Do something expensive. + loggerV.Info("this is an expensive log message") + } + // Output: +} diff --git a/pkg/logr/examples/slog/main.go b/pkg/logr/examples/slog/main.go new file mode 100644 index 00000000..f1f71f9b --- /dev/null +++ b/pkg/logr/examples/slog/main.go @@ -0,0 +1,102 @@ +//go:build go1.21 +// +build go1.21 + +/* +Copyright 2023 The logr Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package main is an example of using slogr. +package main + +import ( + "context" + "fmt" + "log/slog" + "os" + + "github.com/CodSpeedHQ/codspeed-go/pkg/logr" + "github.com/CodSpeedHQ/codspeed-go/pkg/logr/funcr" +) + +type e struct { + str string +} + +func (e e) Error() string { + return e.str +} + +func logrHelper(log logr.Logger, msg string) { + logrHelper2(log, msg) +} + +func logrHelper2(log logr.Logger, msg string) { + log.WithCallDepth(2).Info(msg) +} + +func slogHelper(log *slog.Logger, msg string) { + slogHelper2(log, msg) +} + +func slogHelper2(log *slog.Logger, msg string) { + // slog.Logger has no API for skipping helper functions, so this gets logged as call location. + log.Info(msg) +} + +func main() { + opts := slog.HandlerOptions{ + AddSource: true, + Level: slog.Level(-1), + } + handler := slog.NewJSONHandler(os.Stderr, &opts) + logrLogger := logr.FromSlogHandler(handler) + logrExample(logrLogger) + + logrLogger = funcr.NewJSON( + func(obj string) { fmt.Println(obj) }, + funcr.Options{ + LogCaller: funcr.All, + LogTimestamp: true, + Verbosity: 1, + }) + slogLogger := slog.New(logr.ToSlogHandler(logrLogger)) + slogExample(slogLogger) +} + +func logrExample(log logr.Logger) { + log = log.WithName("my") + log = log.WithName("logger") + log = log.WithName("name") + log = log.WithValues("saved", "value") + log.Info("1) hello", "val1", 1, "val2", map[string]int{"k": 1}) + log.V(1).Info("2) you should see this") + log.V(1).V(1).Info("you should NOT see this") + log.Error(nil, "3) uh oh", "trouble", true, "reasons", []float64{0.1, 0.11, 3.14}) + log.Error(e{"an error occurred"}, "4) goodbye", "code", -1) + logrHelper(log, "5) thru a helper") +} + +func slogExample(log *slog.Logger) { + // There's no guarantee that this logs the right source code location. + // It works for Go 1.21.0 by compensating in logr.ToSlogHandler + // for the additional callers, but those might change. + log = log.With("saved", "value") + log.Info("1) hello", "val1", 1, "val2", map[string]int{"k": 1}) + log.Log(context.TODO(), slog.Level(-1), "2) you should see this") + log.Log(context.TODO(), slog.Level(-2), "you should NOT see this") + log.Error("3) uh oh", "trouble", true, "reasons", []float64{0.1, 0.11, 3.14}) + log.Error("4) goodbye", "code", -1, "err", e{"an error occurred"}) + slogHelper(log, "5) thru a helper") +} diff --git a/pkg/logr/examples/tab_logger.go b/pkg/logr/examples/tab_logger.go new file mode 100644 index 00000000..51389acc --- /dev/null +++ b/pkg/logr/examples/tab_logger.go @@ -0,0 +1,97 @@ +/* +Copyright 2019 The logr Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package main implements a simple example of a logr.LogSink that logs to +// stderr in a tabular format. It is not intended to be a production logger. +package main + +import ( + "fmt" + "os" + "text/tabwriter" + + "github.com/CodSpeedHQ/codspeed-go/pkg/logr" +) + +// tabLogSink is a sample logr.LogSink that logs to stderr. +// It's terribly inefficient, and is only a basic example. +type tabLogSink struct { + name string + keyValues map[string]any + writer *tabwriter.Writer +} + +var _ logr.LogSink = &tabLogSink{} + +// Note that Init usually takes a pointer so it can modify the receiver to save +// runtime info. +func (*tabLogSink) Init(_ logr.RuntimeInfo) { +} + +func (tabLogSink) Enabled(_ int) bool { + return true +} + +func (l tabLogSink) Info(_ int, msg string, kvs ...any) { + _, _ = fmt.Fprintf(l.writer, "%s\t%s\t", l.name, msg) + for k, v := range l.keyValues { + _, _ = fmt.Fprintf(l.writer, "%s: %+v ", k, v) + } + for i := 0; i < len(kvs); i += 2 { + _, _ = fmt.Fprintf(l.writer, "%s: %+v ", kvs[i], kvs[i+1]) + } + _, _ = fmt.Fprintf(l.writer, "\n") + _ = l.writer.Flush() +} + +func (l tabLogSink) Error(err error, msg string, kvs ...any) { + kvs = append(kvs, "error", err) + l.Info(0, msg, kvs...) +} + +func (l tabLogSink) WithName(name string) logr.LogSink { + return &tabLogSink{ + name: l.name + "." + name, + keyValues: l.keyValues, + writer: l.writer, + } +} + +func (l tabLogSink) WithValues(kvs ...any) logr.LogSink { + newMap := make(map[string]any, len(l.keyValues)+len(kvs)/2) + for k, v := range l.keyValues { + newMap[k] = v + } + for i := 0; i < len(kvs); i += 2 { + k := kvs[i].(string) //nolint:forcetypeassert + v := kvs[i+1] + newMap[k] = v + } + return &tabLogSink{ + name: l.name, + keyValues: newMap, + writer: l.writer, + } +} + +// NewTabLogger is the main entry-point to this implementation. App developers +// call this somewhere near main() and thenceforth only deal with logr.Logger. +func NewTabLogger() logr.Logger { + sink := &tabLogSink{ + writer: tabwriter.NewWriter(os.Stderr, 40, 8, 2, '\t', 0), + } + return logr.New(sink) +} diff --git a/pkg/logr/examples/usage_example.go b/pkg/logr/examples/usage_example.go new file mode 100644 index 00000000..d746ecad --- /dev/null +++ b/pkg/logr/examples/usage_example.go @@ -0,0 +1,173 @@ +/* +Copyright 2019 The logr Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "fmt" + "math/rand" + "time" + + "github.com/CodSpeedHQ/codspeed-go/pkg/logr" +) + +// This application demonstrates the usage of logger. +// It's a simple reconciliation loop that pretends to +// receive notifications about updates from a some API +// server, make some changes, and then submit updates of +// its own. + +// This uses object-based logging. It's also possible +// (but a bit trickier) to use file-level "base" loggers. + +var objectMap = map[string]Object{ + "obj1": { + Name: "obj1", + Kind: "one", + Details: 33, + }, + "obj2": { + Name: "obj2", + Kind: "two", + Details: "hi", + }, + "obj3": { + Name: "obj3", + Kind: "one", + Details: 1, + }, +} + +// Object is an app construct that might want to be logged. +type Object struct { + Name string + Kind string + Details any +} + +// Client is a simulated client in this example app. +type Client struct { + objects map[string]Object + log logr.Logger +} + +// Get retrieves an object. +func (c *Client) Get(key string) (Object, error) { + c.log.V(1).Info("fetching object", "key", key) + obj, ok := c.objects[key] + if !ok { + return Object{}, fmt.Errorf("no object %s exists", key) + } + c.log.V(1).Info("pretending to deserialize object", "key", key, "json", "[insert real json here]") + return obj, nil +} + +// Save stores an object. +func (c *Client) Save(obj Object) error { + c.log.V(1).Info("saving object", "key", obj.Name, "object", obj) + if rand.Intn(2) == 0 { + return fmt.Errorf("couldn't save to %s", obj.Name) + } + c.log.V(1).Info("pretending to post object", "key", obj.Name, "url", "https://fake.test") + return nil +} + +// WatchNext waits for object updates. +func (c *Client) WatchNext() string { + time.Sleep(2 * time.Second) + + keyInd := rand.Intn(len(c.objects)) + + currInd := 0 + for key := range c.objects { + if currInd == keyInd { + return key + } + currInd++ + } + + c.log.Info("watch ended") + return "" +} + +// Controller is the main point of this example. +type Controller struct { + log logr.Logger + expectedKind string + client *Client +} + +// Run starts the example controller. +func (c *Controller) Run() { + c.log.Info("starting reconciliation") + + for key := c.client.WatchNext(); key != ""; key = c.client.WatchNext() { + // we can make more specific loggers if we always want to attach a particular named value + log := c.log.WithValues("key", key) + + // fetch our object + obj, err := c.client.Get(key) + if err != nil { + log.Error(err, "unable to reconcile object") + continue + } + + // make sure it's as expected + if obj.Kind != c.expectedKind { + log.Error(nil, "got object that wasn't expected kind", "actual-kind", obj.Kind, "object", obj) + continue + } + + // always log the object with log messages + log = log.WithValues("object", obj) + log.V(1).Info("reconciling object for key") + + // Do some complicated updates updates + obj.Details = obj.Details.(int) * 2 //nolint:forcetypeassert + + // actually save the updates + log.V(1).Info("updating object", "details", obj.Details) + if err := c.client.Save(obj); err != nil { + log.Error(err, "unable to reconcile object") + } + } + + c.log.Info("stopping reconciliation") +} + +// NewController allocates and initializes a Controller. +func NewController(log logr.Logger, objectKind string) *Controller { + ctrlLogger := log.WithName("controller").WithName(objectKind) + client := &Client{ + log: ctrlLogger.WithName("client"), + objects: objectMap, + } + return &Controller{ + log: ctrlLogger, + expectedKind: objectKind, + client: client, + } +} + +func main() { + // use a fake implementation just for demonstration purposes + log := NewTabLogger() + + // update objects with the "one" kind + ctrl := NewController(log, "one") + + ctrl.Run() +} diff --git a/pkg/logr/funcr/example/main.go b/pkg/logr/funcr/example/main.go new file mode 100644 index 00000000..fe69ef4b --- /dev/null +++ b/pkg/logr/funcr/example/main.go @@ -0,0 +1,65 @@ +/* +Copyright 2021 The logr Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package main is an example of using funcr. +package main + +import ( + "fmt" + + "github.com/CodSpeedHQ/codspeed-go/pkg/logr" + "github.com/CodSpeedHQ/codspeed-go/pkg/logr/funcr" +) + +type e struct { + str string +} + +func (e e) Error() string { + return e.str +} + +func helper(log logr.Logger, msg string) { + helper2(log, msg) +} + +func helper2(log logr.Logger, msg string) { + log.WithCallDepth(2).Info(msg) +} + +func main() { + // logr + log := funcr.NewJSON( + func(arg string) { fmt.Println(arg) }, + funcr.Options{ + LogCaller: funcr.All, + LogTimestamp: true, + Verbosity: 1, + }) + logrExample(log.WithName("logr").WithValues("mode", "funcr")) + + // slog (if possible) + doSlog(log) +} + +func logrExample(log logr.Logger) { + log.Info("hello", "val1", 1, "val2", map[string]int{"k": 1}) + log.V(1).Info("you should see this") + log.V(1).V(1).Info("you should NOT see this") + log.Error(nil, "uh oh", "trouble", true, "reasons", []float64{0.1, 0.11, 3.14}) + log.Error(e{"an error occurred"}, "goodbye", "code", -1) + helper(log, "thru a helper") +} diff --git a/pkg/logr/funcr/example/main_noslog.go b/pkg/logr/funcr/example/main_noslog.go new file mode 100644 index 00000000..8882fb34 --- /dev/null +++ b/pkg/logr/funcr/example/main_noslog.go @@ -0,0 +1,29 @@ +//go:build !go1.21 +// +build !go1.21 + +/* +Copyright 2023 The logr Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package main is an example of using funcr. +package main + +import ( + "github.com/CodSpeedHQ/codspeed-go/pkg/logr" +) + +func doSlog(log logr.Logger) { + log.Error(nil, "Sorry, slog is not supported on this version of Go") +} diff --git a/pkg/logr/funcr/example/main_slog.go b/pkg/logr/funcr/example/main_slog.go new file mode 100644 index 00000000..cb83dc7e --- /dev/null +++ b/pkg/logr/funcr/example/main_slog.go @@ -0,0 +1,42 @@ +//go:build go1.21 +// +build go1.21 + +/* +Copyright 2023 The logr Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package main is an example of using funcr. +package main + +import ( + "log/slog" + + "github.com/CodSpeedHQ/codspeed-go/pkg/logr" +) + +func doSlog(log logr.Logger) { + slogger := slog.New(logr.ToSlogHandler(log.WithName("slog").WithValues("mode", "slog"))) + slogExample(slogger) +} + +func slogExample(log *slog.Logger) { + log.Warn("hello", "val1", 1, "val2", map[string]int{"k": 1}) + log.Info("you should see this") + log.Debug("you should NOT see this") + log.Error("uh oh", "trouble", true, "reasons", []float64{0.1, 0.11, 3.14}) + log.With("attr1", 1, "attr2", 2).Info("with attrs") + log.WithGroup("groupname").Info("with group", "slog2", false) + log.WithGroup("group1").With("attr1", 1).WithGroup("group2").With("attr2", 2).Info("msg", "arg", "val") +} diff --git a/pkg/logr/funcr/example_formatter_test.go b/pkg/logr/funcr/example_formatter_test.go new file mode 100644 index 00000000..fb74f907 --- /dev/null +++ b/pkg/logr/funcr/example_formatter_test.go @@ -0,0 +1,103 @@ +/* +Copyright 2021 The logr Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package funcr_test + +import ( + "fmt" + "strings" + + "github.com/CodSpeedHQ/codspeed-go/pkg/logr" + "github.com/CodSpeedHQ/codspeed-go/pkg/logr/funcr" +) + +// NewStdoutLogger returns a logr.Logger that prints to stdout. +// It demonstrates how to implement a custom With* function which +// controls whether INFO or ERROR are printed in front of the log +// message. +func NewStdoutLogger() logr.Logger { + l := &stdoutlogger{ + Formatter: funcr.NewFormatter(funcr.Options{}), + } + return logr.New(l) +} + +type stdoutlogger struct { + funcr.Formatter + logMsgType bool +} + +func (l stdoutlogger) WithName(name string) logr.LogSink { + l.AddName(name) + return &l +} + +func (l stdoutlogger) WithValues(kvList ...any) logr.LogSink { + l.AddValues(kvList) + return &l +} + +func (l stdoutlogger) WithCallDepth(depth int) logr.LogSink { + l.AddCallDepth(depth) + return &l +} + +func (l stdoutlogger) Info(level int, msg string, kvList ...any) { + prefix, args := l.FormatInfo(level, msg, kvList) + l.write("INFO", prefix, args) +} + +func (l stdoutlogger) Error(err error, msg string, kvList ...any) { + prefix, args := l.FormatError(err, msg, kvList) + l.write("ERROR", prefix, args) +} + +func (l stdoutlogger) write(msgType, prefix, args string) { + var parts []string + if l.logMsgType { + parts = append(parts, msgType) + } + if prefix != "" { + parts = append(parts, prefix) + } + parts = append(parts, args) + fmt.Println(strings.Join(parts, ": ")) +} + +// WithLogMsgType returns a copy of the logger with new settings for +// logging the message type. It returns the original logger if the +// underlying LogSink is not a stdoutlogger. +func WithLogMsgType(log logr.Logger, logMsgType bool) logr.Logger { + if l, ok := log.GetSink().(*stdoutlogger); ok { + clone := *l + clone.logMsgType = logMsgType + log = log.WithSink(&clone) + } + return log +} + +// Assert conformance to the interfaces. +var _ logr.LogSink = &stdoutlogger{} +var _ logr.CallDepthLogSink = &stdoutlogger{} + +func ExampleFormatter() { + l := NewStdoutLogger() + l.Info("no message type") + WithLogMsgType(l, true).Info("with message type") + // Output: + // "level"=0 "msg"="no message type" + // INFO: "level"=0 "msg"="with message type" +} diff --git a/pkg/logr/funcr/example_test.go b/pkg/logr/funcr/example_test.go new file mode 100644 index 00000000..1d8986bd --- /dev/null +++ b/pkg/logr/funcr/example_test.go @@ -0,0 +1,126 @@ +/* +Copyright 2021 The logr Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package funcr_test + +import ( + "fmt" + + "github.com/CodSpeedHQ/codspeed-go/pkg/logr/funcr" +) + +func ExampleNew() { + log := funcr.New(func(prefix, args string) { + fmt.Println(prefix, args) + }, funcr.Options{}) + + log = log.WithName("MyLogger") + log = log.WithValues("savedKey", "savedValue") + log.Info("the message", "key", "value") + // Output: MyLogger "level"=0 "msg"="the message" "savedKey"="savedValue" "key"="value" +} + +func ExampleNewJSON() { + log := funcr.NewJSON(func(obj string) { + fmt.Println(obj) + }, funcr.Options{}) + + log = log.WithName("MyLogger") + log = log.WithValues("savedKey", "savedValue") + log.Info("the message", "key", "value") + // Output: {"logger":"MyLogger","level":0,"msg":"the message","savedKey":"savedValue","key":"value"} +} + +func ExampleUnderlier() { + log := funcr.New(func(prefix, args string) { + fmt.Println(prefix, args) + }, funcr.Options{}) + + if underlier, ok := log.GetSink().(funcr.Underlier); ok { + fn := underlier.GetUnderlying() + fn("hello", "world") + } + // Output: hello world +} + +func ExampleOptions() { + log := funcr.NewJSON( + func(obj string) { fmt.Println(obj) }, + funcr.Options{ + LogCaller: funcr.All, + Verbosity: 1, // V(2) and higher is ignored. + }) + log.V(0).Info("V(0) message", "key", "value") + log.V(1).Info("V(1) message", "key", "value") + log.V(2).Info("V(2) message", "key", "value") + // Output: + // {"logger":"","caller":{"file":"example_test.go","line":66},"level":0,"msg":"V(0) message","key":"value"} + // {"logger":"","caller":{"file":"example_test.go","line":67},"level":1,"msg":"V(1) message","key":"value"} +} + +func ExampleOptions_renderHooks() { + // prefix all builtin keys with "log:" + prefixSpecialKeys := func(kvList []any) []any { + for i := 0; i < len(kvList); i += 2 { + k, _ := kvList[i].(string) + kvList[i] = "log:" + k + } + return kvList + } + + // present saved values as a single JSON object + valuesAsObject := func(kvList []any) []any { + return []any{"labels", funcr.PseudoStruct(kvList)} + } + + log := funcr.NewJSON( + func(obj string) { fmt.Println(obj) }, + funcr.Options{ + RenderBuiltinsHook: prefixSpecialKeys, + RenderValuesHook: valuesAsObject, + }) + log = log.WithName("MyLogger") + log = log.WithValues("savedKey1", "savedVal1") + log = log.WithValues("savedKey2", "savedVal2") + log.Info("the message", "key", "value") + // Output: {"log:logger":"MyLogger","log:level":0,"log:msg":"the message","labels":{"savedKey1":"savedVal1","savedKey2":"savedVal2"},"key":"value"} +} + +func ExamplePseudoStruct() { + log := funcr.NewJSON( + func(obj string) { fmt.Println(obj) }, + funcr.Options{}) + kv := []any{ + "field1", 12345, + "field2", true, + } + log.Info("the message", "key", funcr.PseudoStruct(kv)) + // Output: {"logger":"","level":0,"msg":"the message","key":{"field1":12345,"field2":true}} +} + +func ExampleOptions_maxLogDepth() { + type List struct { + Next *List + } + l := List{} + l.Next = &l // recursive + + log := funcr.NewJSON( + func(obj string) { fmt.Println(obj) }, + funcr.Options{MaxLogDepth: 4}) + log.Info("recursive", "list", l) + // Output: {"logger":"","level":0,"msg":"recursive","list":{"Next":{"Next":{"Next":{"Next":{"Next":""}}}}}} +} diff --git a/pkg/logr/funcr/funcr.go b/pkg/logr/funcr/funcr.go new file mode 100644 index 00000000..9ed34847 --- /dev/null +++ b/pkg/logr/funcr/funcr.go @@ -0,0 +1,914 @@ +/* +Copyright 2021 The logr Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package funcr implements formatting of structured log messages and +// optionally captures the call site and timestamp. +// +// The simplest way to use it is via its implementation of a +// github.com/go-logr/logr.LogSink with output through an arbitrary +// "write" function. See New and NewJSON for details. +// +// # Custom LogSinks +// +// For users who need more control, a funcr.Formatter can be embedded inside +// your own custom LogSink implementation. This is useful when the LogSink +// needs to implement additional methods, for example. +// +// # Formatting +// +// This will respect logr.Marshaler, fmt.Stringer, and error interfaces for +// values which are being logged. When rendering a struct, funcr will use Go's +// standard JSON tags (all except "string"). +package funcr + +import ( + "bytes" + "encoding" + "encoding/json" + "fmt" + "path/filepath" + "reflect" + "runtime" + "strconv" + "strings" + "time" + + "github.com/CodSpeedHQ/codspeed-go/pkg/logr" +) + +// New returns a logr.Logger which is implemented by an arbitrary function. +func New(fn func(prefix, args string), opts Options) logr.Logger { + return logr.New(newSink(fn, NewFormatter(opts))) +} + +// NewJSON returns a logr.Logger which is implemented by an arbitrary function +// and produces JSON output. +func NewJSON(fn func(obj string), opts Options) logr.Logger { + fnWrapper := func(_, obj string) { + fn(obj) + } + return logr.New(newSink(fnWrapper, NewFormatterJSON(opts))) +} + +// Underlier exposes access to the underlying logging function. Since +// callers only have a logr.Logger, they have to know which +// implementation is in use, so this interface is less of an +// abstraction and more of a way to test type conversion. +type Underlier interface { + GetUnderlying() func(prefix, args string) +} + +func newSink(fn func(prefix, args string), formatter Formatter) logr.LogSink { + l := &fnlogger{ + Formatter: formatter, + write: fn, + } + // For skipping fnlogger.Info and fnlogger.Error. + l.AddCallDepth(1) // via Formatter + return l +} + +// Options carries parameters which influence the way logs are generated. +type Options struct { + // LogCaller tells funcr to add a "caller" key to some or all log lines. + // This has some overhead, so some users might not want it. + LogCaller MessageClass + + // LogCallerFunc tells funcr to also log the calling function name. This + // has no effect if caller logging is not enabled (see Options.LogCaller). + LogCallerFunc bool + + // LogTimestamp tells funcr to add a "ts" key to log lines. This has some + // overhead, so some users might not want it. + LogTimestamp bool + + // TimestampFormat tells funcr how to render timestamps when LogTimestamp + // is enabled. If not specified, a default format will be used. For more + // details, see docs for Go's time.Layout. + TimestampFormat string + + // LogInfoLevel tells funcr what key to use to log the info level. + // If not specified, the info level will be logged as "level". + // If this is set to "", the info level will not be logged at all. + LogInfoLevel *string + + // Verbosity tells funcr which V logs to produce. Higher values enable + // more logs. Info logs at or below this level will be written, while logs + // above this level will be discarded. + Verbosity int + + // RenderBuiltinsHook allows users to mutate the list of key-value pairs + // while a log line is being rendered. The kvList argument follows logr + // conventions - each pair of slice elements is comprised of a string key + // and an arbitrary value (verified and sanitized before calling this + // hook). The value returned must follow the same conventions. This hook + // can be used to audit or modify logged data. For example, you might want + // to prefix all of funcr's built-in keys with some string. This hook is + // only called for built-in (provided by funcr itself) key-value pairs. + // Equivalent hooks are offered for key-value pairs saved via + // logr.Logger.WithValues or Formatter.AddValues (see RenderValuesHook) and + // for user-provided pairs (see RenderArgsHook). + RenderBuiltinsHook func(kvList []any) []any + + // RenderValuesHook is the same as RenderBuiltinsHook, except that it is + // only called for key-value pairs saved via logr.Logger.WithValues. See + // RenderBuiltinsHook for more details. + RenderValuesHook func(kvList []any) []any + + // RenderArgsHook is the same as RenderBuiltinsHook, except that it is only + // called for key-value pairs passed directly to Info and Error. See + // RenderBuiltinsHook for more details. + RenderArgsHook func(kvList []any) []any + + // MaxLogDepth tells funcr how many levels of nested fields (e.g. a struct + // that contains a struct, etc.) it may log. Every time it finds a struct, + // slice, array, or map the depth is increased by one. When the maximum is + // reached, the value will be converted to a string indicating that the max + // depth has been exceeded. If this field is not specified, a default + // value will be used. + MaxLogDepth int +} + +// MessageClass indicates which category or categories of messages to consider. +type MessageClass int + +const ( + // None ignores all message classes. + None MessageClass = iota + // All considers all message classes. + All + // Info only considers info messages. + Info + // Error only considers error messages. + Error +) + +// fnlogger inherits some of its LogSink implementation from Formatter +// and just needs to add some glue code. +type fnlogger struct { + Formatter + write func(prefix, args string) +} + +func (l fnlogger) WithName(name string) logr.LogSink { + l.AddName(name) // via Formatter + return &l +} + +func (l fnlogger) WithValues(kvList ...any) logr.LogSink { + l.AddValues(kvList) // via Formatter + return &l +} + +func (l fnlogger) WithCallDepth(depth int) logr.LogSink { + l.AddCallDepth(depth) // via Formatter + return &l +} + +func (l fnlogger) Info(level int, msg string, kvList ...any) { + prefix, args := l.FormatInfo(level, msg, kvList) + l.write(prefix, args) +} + +func (l fnlogger) Error(err error, msg string, kvList ...any) { + prefix, args := l.FormatError(err, msg, kvList) + l.write(prefix, args) +} + +func (l fnlogger) GetUnderlying() func(prefix, args string) { + return l.write +} + +// Assert conformance to the interfaces. +var _ logr.LogSink = &fnlogger{} +var _ logr.CallDepthLogSink = &fnlogger{} +var _ Underlier = &fnlogger{} + +// NewFormatter constructs a Formatter which emits a JSON-like key=value format. +func NewFormatter(opts Options) Formatter { + return newFormatter(opts, outputKeyValue) +} + +// NewFormatterJSON constructs a Formatter which emits strict JSON. +func NewFormatterJSON(opts Options) Formatter { + return newFormatter(opts, outputJSON) +} + +// Defaults for Options. +const defaultTimestampFormat = "2006-01-02 15:04:05.000000" +const defaultMaxLogDepth = 16 + +func newFormatter(opts Options, outfmt outputFormat) Formatter { + if opts.TimestampFormat == "" { + opts.TimestampFormat = defaultTimestampFormat + } + if opts.MaxLogDepth == 0 { + opts.MaxLogDepth = defaultMaxLogDepth + } + if opts.LogInfoLevel == nil { + opts.LogInfoLevel = new(string) + *opts.LogInfoLevel = "level" + } + f := Formatter{ + outputFormat: outfmt, + prefix: "", + values: nil, + depth: 0, + opts: &opts, + } + return f +} + +// Formatter is an opaque struct which can be embedded in a LogSink +// implementation. It should be constructed with NewFormatter. Some of +// its methods directly implement logr.LogSink. +type Formatter struct { + outputFormat outputFormat + prefix string + values []any + valuesStr string + depth int + opts *Options + groupName string // for slog groups + groups []groupDef +} + +// outputFormat indicates which outputFormat to use. +type outputFormat int + +const ( + // outputKeyValue emits a JSON-like key=value format, but not strict JSON. + outputKeyValue outputFormat = iota + // outputJSON emits strict JSON. + outputJSON +) + +// groupDef represents a saved group. The values may be empty, but we don't +// know if we need to render the group until the final record is rendered. +type groupDef struct { + name string + values string +} + +// PseudoStruct is a list of key-value pairs that gets logged as a struct. +type PseudoStruct []any + +// render produces a log line, ready to use. +func (f Formatter) render(builtins, args []any) string { + // Empirically bytes.Buffer is faster than strings.Builder for this. + buf := bytes.NewBuffer(make([]byte, 0, 1024)) + + if f.outputFormat == outputJSON { + buf.WriteByte('{') // for the whole record + } + + // Render builtins + vals := builtins + if hook := f.opts.RenderBuiltinsHook; hook != nil { + vals = hook(f.sanitize(vals)) + } + f.flatten(buf, vals, false) // keys are ours, no need to escape + continuing := len(builtins) > 0 + + // Turn the inner-most group into a string + argsStr := func() string { + buf := bytes.NewBuffer(make([]byte, 0, 1024)) + + vals = args + if hook := f.opts.RenderArgsHook; hook != nil { + vals = hook(f.sanitize(vals)) + } + f.flatten(buf, vals, true) // escape user-provided keys + + return buf.String() + }() + + // Render the stack of groups from the inside out. + bodyStr := f.renderGroup(f.groupName, f.valuesStr, argsStr) + for i := len(f.groups) - 1; i >= 0; i-- { + grp := &f.groups[i] + if grp.values == "" && bodyStr == "" { + // no contents, so we must elide the whole group + continue + } + bodyStr = f.renderGroup(grp.name, grp.values, bodyStr) + } + + if bodyStr != "" { + if continuing { + buf.WriteByte(f.comma()) + } + buf.WriteString(bodyStr) + } + + if f.outputFormat == outputJSON { + buf.WriteByte('}') // for the whole record + } + + return buf.String() +} + +// renderGroup returns a string representation of the named group with rendered +// values and args. If the name is empty, this will return the values and args, +// joined. If the name is not empty, this will return a single key-value pair, +// where the value is a grouping of the values and args. If the values and +// args are both empty, this will return an empty string, even if the name was +// specified. +func (f Formatter) renderGroup(name string, values string, args string) string { + buf := bytes.NewBuffer(make([]byte, 0, 1024)) + + needClosingBrace := false + if name != "" && (values != "" || args != "") { + buf.WriteString(f.quoted(name, true)) // escape user-provided keys + buf.WriteByte(f.colon()) + buf.WriteByte('{') + needClosingBrace = true + } + + continuing := false + if values != "" { + buf.WriteString(values) + continuing = true + } + + if args != "" { + if continuing { + buf.WriteByte(f.comma()) + } + buf.WriteString(args) + } + + if needClosingBrace { + buf.WriteByte('}') + } + + return buf.String() +} + +// flatten renders a list of key-value pairs into a buffer. If escapeKeys is +// true, the keys are assumed to have non-JSON-compatible characters in them +// and must be evaluated for escapes. +// +// This function returns a potentially modified version of kvList, which +// ensures that there is a value for every key (adding a value if needed) and +// that each key is a string (substituting a key if needed). +func (f Formatter) flatten(buf *bytes.Buffer, kvList []any, escapeKeys bool) []any { + // This logic overlaps with sanitize() but saves one type-cast per key, + // which can be measurable. + if len(kvList)%2 != 0 { + kvList = append(kvList, noValue) + } + copied := false + for i := 0; i < len(kvList); i += 2 { + k, ok := kvList[i].(string) + if !ok { + if !copied { + newList := make([]any, len(kvList)) + copy(newList, kvList) + kvList = newList + copied = true + } + k = f.nonStringKey(kvList[i]) + kvList[i] = k + } + v := kvList[i+1] + + if i > 0 { + if f.outputFormat == outputJSON { + buf.WriteByte(f.comma()) + } else { + // In theory the format could be something we don't understand. In + // practice, we control it, so it won't be. + buf.WriteByte(' ') + } + } + + buf.WriteString(f.quoted(k, escapeKeys)) + buf.WriteByte(f.colon()) + buf.WriteString(f.pretty(v)) + } + return kvList +} + +func (f Formatter) quoted(str string, escape bool) string { + if escape { + return prettyString(str) + } + // this is faster + return `"` + str + `"` +} + +func (f Formatter) comma() byte { + if f.outputFormat == outputJSON { + return ',' + } + return ' ' +} + +func (f Formatter) colon() byte { + if f.outputFormat == outputJSON { + return ':' + } + return '=' +} + +func (f Formatter) pretty(value any) string { + return f.prettyWithFlags(value, 0, 0) +} + +const ( + flagRawStruct = 0x1 // do not print braces on structs +) + +// TODO: This is not fast. Most of the overhead goes here. +func (f Formatter) prettyWithFlags(value any, flags uint32, depth int) string { + if depth > f.opts.MaxLogDepth { + return `""` + } + + // Handle types that take full control of logging. + if v, ok := value.(logr.Marshaler); ok { + // Replace the value with what the type wants to get logged. + // That then gets handled below via reflection. + value = invokeMarshaler(v) + } + + // Handle types that want to format themselves. + switch v := value.(type) { + case fmt.Stringer: + value = invokeStringer(v) + case error: + value = invokeError(v) + } + + // Handling the most common types without reflect is a small perf win. + switch v := value.(type) { + case bool: + return strconv.FormatBool(v) + case string: + return prettyString(v) + case int: + return strconv.FormatInt(int64(v), 10) + case int8: + return strconv.FormatInt(int64(v), 10) + case int16: + return strconv.FormatInt(int64(v), 10) + case int32: + return strconv.FormatInt(int64(v), 10) + case int64: + return strconv.FormatInt(int64(v), 10) + case uint: + return strconv.FormatUint(uint64(v), 10) + case uint8: + return strconv.FormatUint(uint64(v), 10) + case uint16: + return strconv.FormatUint(uint64(v), 10) + case uint32: + return strconv.FormatUint(uint64(v), 10) + case uint64: + return strconv.FormatUint(v, 10) + case uintptr: + return strconv.FormatUint(uint64(v), 10) + case float32: + return strconv.FormatFloat(float64(v), 'f', -1, 32) + case float64: + return strconv.FormatFloat(v, 'f', -1, 64) + case complex64: + return `"` + strconv.FormatComplex(complex128(v), 'f', -1, 64) + `"` + case complex128: + return `"` + strconv.FormatComplex(v, 'f', -1, 128) + `"` + case PseudoStruct: + buf := bytes.NewBuffer(make([]byte, 0, 1024)) + v = f.sanitize(v) + if flags&flagRawStruct == 0 { + buf.WriteByte('{') + } + for i := 0; i < len(v); i += 2 { + if i > 0 { + buf.WriteByte(f.comma()) + } + k, _ := v[i].(string) // sanitize() above means no need to check success + // arbitrary keys might need escaping + buf.WriteString(prettyString(k)) + buf.WriteByte(f.colon()) + buf.WriteString(f.prettyWithFlags(v[i+1], 0, depth+1)) + } + if flags&flagRawStruct == 0 { + buf.WriteByte('}') + } + return buf.String() + } + + buf := bytes.NewBuffer(make([]byte, 0, 256)) + t := reflect.TypeOf(value) + if t == nil { + return "null" + } + v := reflect.ValueOf(value) + switch t.Kind() { + case reflect.Bool: + return strconv.FormatBool(v.Bool()) + case reflect.String: + return prettyString(v.String()) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return strconv.FormatInt(int64(v.Int()), 10) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return strconv.FormatUint(uint64(v.Uint()), 10) + case reflect.Float32: + return strconv.FormatFloat(float64(v.Float()), 'f', -1, 32) + case reflect.Float64: + return strconv.FormatFloat(v.Float(), 'f', -1, 64) + case reflect.Complex64: + return `"` + strconv.FormatComplex(complex128(v.Complex()), 'f', -1, 64) + `"` + case reflect.Complex128: + return `"` + strconv.FormatComplex(v.Complex(), 'f', -1, 128) + `"` + case reflect.Struct: + if flags&flagRawStruct == 0 { + buf.WriteByte('{') + } + printComma := false // testing i>0 is not enough because of JSON omitted fields + for i := 0; i < t.NumField(); i++ { + fld := t.Field(i) + if fld.PkgPath != "" { + // reflect says this field is only defined for non-exported fields. + continue + } + if !v.Field(i).CanInterface() { + // reflect isn't clear exactly what this means, but we can't use it. + continue + } + name := "" + omitempty := false + if tag, found := fld.Tag.Lookup("json"); found { + if tag == "-" { + continue + } + if comma := strings.Index(tag, ","); comma != -1 { + if n := tag[:comma]; n != "" { + name = n + } + rest := tag[comma:] + if strings.Contains(rest, ",omitempty,") || strings.HasSuffix(rest, ",omitempty") { + omitempty = true + } + } else { + name = tag + } + } + if omitempty && isEmpty(v.Field(i)) { + continue + } + if printComma { + buf.WriteByte(f.comma()) + } + printComma = true // if we got here, we are rendering a field + if fld.Anonymous && fld.Type.Kind() == reflect.Struct && name == "" { + buf.WriteString(f.prettyWithFlags(v.Field(i).Interface(), flags|flagRawStruct, depth+1)) + continue + } + if name == "" { + name = fld.Name + } + // field names can't contain characters which need escaping + buf.WriteString(f.quoted(name, false)) + buf.WriteByte(f.colon()) + buf.WriteString(f.prettyWithFlags(v.Field(i).Interface(), 0, depth+1)) + } + if flags&flagRawStruct == 0 { + buf.WriteByte('}') + } + return buf.String() + case reflect.Slice, reflect.Array: + // If this is outputing as JSON make sure this isn't really a json.RawMessage. + // If so just emit "as-is" and don't pretty it as that will just print + // it as [X,Y,Z,...] which isn't terribly useful vs the string form you really want. + if f.outputFormat == outputJSON { + if rm, ok := value.(json.RawMessage); ok { + // If it's empty make sure we emit an empty value as the array style would below. + if len(rm) > 0 { + buf.Write(rm) + } else { + buf.WriteString("null") + } + return buf.String() + } + } + buf.WriteByte('[') + for i := 0; i < v.Len(); i++ { + if i > 0 { + buf.WriteByte(f.comma()) + } + e := v.Index(i) + buf.WriteString(f.prettyWithFlags(e.Interface(), 0, depth+1)) + } + buf.WriteByte(']') + return buf.String() + case reflect.Map: + buf.WriteByte('{') + // This does not sort the map keys, for best perf. + it := v.MapRange() + i := 0 + for it.Next() { + if i > 0 { + buf.WriteByte(f.comma()) + } + // If a map key supports TextMarshaler, use it. + keystr := "" + if m, ok := it.Key().Interface().(encoding.TextMarshaler); ok { + txt, err := m.MarshalText() + if err != nil { + keystr = fmt.Sprintf("", err.Error()) + } else { + keystr = string(txt) + } + keystr = prettyString(keystr) + } else { + // prettyWithFlags will produce already-escaped values + keystr = f.prettyWithFlags(it.Key().Interface(), 0, depth+1) + if t.Key().Kind() != reflect.String { + // JSON only does string keys. Unlike Go's standard JSON, we'll + // convert just about anything to a string. + keystr = prettyString(keystr) + } + } + buf.WriteString(keystr) + buf.WriteByte(f.colon()) + buf.WriteString(f.prettyWithFlags(it.Value().Interface(), 0, depth+1)) + i++ + } + buf.WriteByte('}') + return buf.String() + case reflect.Ptr, reflect.Interface: + if v.IsNil() { + return "null" + } + return f.prettyWithFlags(v.Elem().Interface(), 0, depth) + } + return fmt.Sprintf(`""`, t.Kind().String()) +} + +func prettyString(s string) string { + // Avoid escaping (which does allocations) if we can. + if needsEscape(s) { + return strconv.Quote(s) + } + b := bytes.NewBuffer(make([]byte, 0, 1024)) + b.WriteByte('"') + b.WriteString(s) + b.WriteByte('"') + return b.String() +} + +// needsEscape determines whether the input string needs to be escaped or not, +// without doing any allocations. +func needsEscape(s string) bool { + for _, r := range s { + if !strconv.IsPrint(r) || r == '\\' || r == '"' { + return true + } + } + return false +} + +func isEmpty(v reflect.Value) bool { + switch v.Kind() { + case reflect.Array, reflect.Map, reflect.Slice, reflect.String: + return v.Len() == 0 + case reflect.Bool: + return !v.Bool() + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return v.Int() == 0 + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return v.Uint() == 0 + case reflect.Float32, reflect.Float64: + return v.Float() == 0 + case reflect.Complex64, reflect.Complex128: + return v.Complex() == 0 + case reflect.Interface, reflect.Ptr: + return v.IsNil() + } + return false +} + +func invokeMarshaler(m logr.Marshaler) (ret any) { + defer func() { + if r := recover(); r != nil { + ret = fmt.Sprintf("", r) + } + }() + return m.MarshalLog() +} + +func invokeStringer(s fmt.Stringer) (ret string) { + defer func() { + if r := recover(); r != nil { + ret = fmt.Sprintf("", r) + } + }() + return s.String() +} + +func invokeError(e error) (ret string) { + defer func() { + if r := recover(); r != nil { + ret = fmt.Sprintf("", r) + } + }() + return e.Error() +} + +// Caller represents the original call site for a log line, after considering +// logr.Logger.WithCallDepth and logr.Logger.WithCallStackHelper. The File and +// Line fields will always be provided, while the Func field is optional. +// Users can set the render hook fields in Options to examine logged key-value +// pairs, one of which will be {"caller", Caller} if the Options.LogCaller +// field is enabled for the given MessageClass. +type Caller struct { + // File is the basename of the file for this call site. + File string `json:"file"` + // Line is the line number in the file for this call site. + Line int `json:"line"` + // Func is the function name for this call site, or empty if + // Options.LogCallerFunc is not enabled. + Func string `json:"function,omitempty"` +} + +func (f Formatter) caller() Caller { + // +1 for this frame, +1 for Info/Error. + pc, file, line, ok := runtime.Caller(f.depth + 2) + if !ok { + return Caller{"", 0, ""} + } + fn := "" + if f.opts.LogCallerFunc { + if fp := runtime.FuncForPC(pc); fp != nil { + fn = fp.Name() + } + } + + return Caller{filepath.Base(file), line, fn} +} + +const noValue = "" + +func (f Formatter) nonStringKey(v any) string { + return fmt.Sprintf("", f.snippet(v)) +} + +// snippet produces a short snippet string of an arbitrary value. +func (f Formatter) snippet(v any) string { + const snipLen = 16 + + snip := f.pretty(v) + if len(snip) > snipLen { + snip = snip[:snipLen] + } + return snip +} + +// sanitize ensures that a list of key-value pairs has a value for every key +// (adding a value if needed) and that each key is a string (substituting a key +// if needed). +func (f Formatter) sanitize(kvList []any) []any { + if len(kvList)%2 != 0 { + kvList = append(kvList, noValue) + } + for i := 0; i < len(kvList); i += 2 { + _, ok := kvList[i].(string) + if !ok { + kvList[i] = f.nonStringKey(kvList[i]) + } + } + return kvList +} + +// startGroup opens a new group scope (basically a sub-struct), which locks all +// the current saved values and starts them anew. This is needed to satisfy +// slog. +func (f *Formatter) startGroup(name string) { + // Unnamed groups are just inlined. + if name == "" { + return + } + + n := len(f.groups) + f.groups = append(f.groups[:n:n], groupDef{f.groupName, f.valuesStr}) + + // Start collecting new values. + f.groupName = name + f.valuesStr = "" + f.values = nil +} + +// Init configures this Formatter from runtime info, such as the call depth +// imposed by logr itself. +// Note that this receiver is a pointer, so depth can be saved. +func (f *Formatter) Init(info logr.RuntimeInfo) { + f.depth += info.CallDepth +} + +// Enabled checks whether an info message at the given level should be logged. +func (f Formatter) Enabled(level int) bool { + return level <= f.opts.Verbosity +} + +// GetDepth returns the current depth of this Formatter. This is useful for +// implementations which do their own caller attribution. +func (f Formatter) GetDepth() int { + return f.depth +} + +// FormatInfo renders an Info log message into strings. The prefix will be +// empty when no names were set (via AddNames), or when the output is +// configured for JSON. +func (f Formatter) FormatInfo(level int, msg string, kvList []any) (prefix, argsStr string) { + args := make([]any, 0, 64) // using a constant here impacts perf + prefix = f.prefix + if f.outputFormat == outputJSON { + args = append(args, "logger", prefix) + prefix = "" + } + if f.opts.LogTimestamp { + args = append(args, "ts", time.Now().Format(f.opts.TimestampFormat)) + } + if policy := f.opts.LogCaller; policy == All || policy == Info { + args = append(args, "caller", f.caller()) + } + if key := *f.opts.LogInfoLevel; key != "" { + args = append(args, key, level) + } + args = append(args, "msg", msg) + return prefix, f.render(args, kvList) +} + +// FormatError renders an Error log message into strings. The prefix will be +// empty when no names were set (via AddNames), or when the output is +// configured for JSON. +func (f Formatter) FormatError(err error, msg string, kvList []any) (prefix, argsStr string) { + args := make([]any, 0, 64) // using a constant here impacts perf + prefix = f.prefix + if f.outputFormat == outputJSON { + args = append(args, "logger", prefix) + prefix = "" + } + if f.opts.LogTimestamp { + args = append(args, "ts", time.Now().Format(f.opts.TimestampFormat)) + } + if policy := f.opts.LogCaller; policy == All || policy == Error { + args = append(args, "caller", f.caller()) + } + args = append(args, "msg", msg) + var loggableErr any + if err != nil { + loggableErr = err.Error() + } + args = append(args, "error", loggableErr) + return prefix, f.render(args, kvList) +} + +// AddName appends the specified name. funcr uses '/' characters to separate +// name elements. Callers should not pass '/' in the provided name string, but +// this library does not actually enforce that. +func (f *Formatter) AddName(name string) { + if len(f.prefix) > 0 { + f.prefix += "/" + } + f.prefix += name +} + +// AddValues adds key-value pairs to the set of saved values to be logged with +// each log line. +func (f *Formatter) AddValues(kvList []any) { + // Three slice args forces a copy. + n := len(f.values) + f.values = append(f.values[:n:n], kvList...) + + vals := f.values + if hook := f.opts.RenderValuesHook; hook != nil { + vals = hook(f.sanitize(vals)) + } + + // Pre-render values, so we don't have to do it on each Info/Error call. + buf := bytes.NewBuffer(make([]byte, 0, 1024)) + f.flatten(buf, vals, true) // escape user-provided keys + f.valuesStr = buf.String() +} + +// AddCallDepth increases the number of stack-frames to skip when attributing +// the log line to a file and line. +func (f *Formatter) AddCallDepth(depth int) { + f.depth += depth +} diff --git a/pkg/logr/funcr/funcr_test.go b/pkg/logr/funcr/funcr_test.go new file mode 100644 index 00000000..f65e4a73 --- /dev/null +++ b/pkg/logr/funcr/funcr_test.go @@ -0,0 +1,1400 @@ +/* +Copyright 2021 The logr Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package funcr + +import ( + "encoding/json" + "fmt" + testing "github.com/CodSpeedHQ/codspeed-go/testing/testing" + "path/filepath" + "reflect" + "runtime" + + "github.com/CodSpeedHQ/codspeed-go/pkg/logr" +) + +// Will be handled via reflection instead of type assertions. +type substr string + +func ptrint(i int) *int { + return &i +} +func ptrstr(s string) *string { + return &s +} + +// point implements encoding.TextMarshaler and can be used as a map key. +type point struct{ x, y int } + +func (p point) MarshalText() ([]byte, error) { + return []byte(fmt.Sprintf("(%d, %d)", p.x, p.y)), nil +} + +// pointErr implements encoding.TextMarshaler but returns an error. +type pointErr struct{ x, y int } + +func (p pointErr) MarshalText() ([]byte, error) { + return nil, fmt.Errorf("uh oh: %d, %d", p.x, p.y) +} + +// Logging this should result in the MarshalLog() value. +type Tmarshaler struct{ val string } + +func (t Tmarshaler) MarshalLog() any { + return struct{ Inner string }{"I am a logr.Marshaler"} +} + +func (t Tmarshaler) String() string { + return "String(): you should not see this" +} + +func (t Tmarshaler) Error() string { + return "Error(): you should not see this" +} + +// Logging this should result in a panic. +type Tmarshalerpanic struct{ val string } + +func (t Tmarshalerpanic) MarshalLog() any { + panic("Tmarshalerpanic") +} + +// Logging this should result in the String() value. +type Tstringer struct{ val string } + +func (t Tstringer) String() string { + return "I am a fmt.Stringer" +} + +func (t Tstringer) Error() string { + return "Error(): you should not see this" +} + +// Logging this should result in a panic. +type Tstringerpanic struct{ val string } + +func (t Tstringerpanic) String() string { + panic("Tstringerpanic") +} + +// Logging this should result in the Error() value. +type Terror struct{ val string } + +func (t Terror) Error() string { + return "I am an error" +} + +// Logging this should result in a panic. +type Terrorpanic struct{ val string } + +func (t Terrorpanic) Error() string { + panic("Terrorpanic") +} + +type TjsontagsString struct { + String0 string `json:"-"` // first field ignored + String1 string `json:"string1"` // renamed + String2 string `json:"-"` // ignored + String3 string `json:"-,"` // named "-" + String4 string `json:"string4,omitempty"` // renamed, ignore if empty + String5 string `json:","` // no-op + String6 string `json:",omitempty"` // ignore if empty +} + +type TjsontagsBool struct { + Bool0 bool `json:"-"` // first field ignored + Bool1 bool `json:"bool1"` // renamed + Bool2 bool `json:"-"` // ignored + Bool3 bool `json:"-,"` // named "-" + Bool4 bool `json:"bool4,omitempty"` // renamed, ignore if empty + Bool5 bool `json:","` // no-op + Bool6 bool `json:",omitempty"` // ignore if empty +} + +type TjsontagsInt struct { + Int0 int `json:"-"` // first field ignored + Int1 int `json:"int1"` // renamed + Int2 int `json:"-"` // ignored + Int3 int `json:"-,"` // named "-" + Int4 int `json:"int4,omitempty"` // renamed, ignore if empty + Int5 int `json:","` // no-op + Int6 int `json:",omitempty"` // ignore if empty +} + +type TjsontagsUint struct { + Uint0 int `json:"-"` // first field ignored + Uint1 uint `json:"uint1"` // renamed + Uint2 uint `json:"-"` // ignored + Uint3 uint `json:"-,"` // named "-" + Uint4 uint `json:"uint4,omitempty"` // renamed, ignore if empty + Uint5 uint `json:","` // no-op + Uint6 uint `json:",omitempty"` // ignore if empty +} + +type TjsontagsFloat struct { + Float0 float64 `json:"-"` // first field ignored + Float1 float64 `json:"float1"` // renamed + Float2 float64 `json:"-"` // ignored + Float3 float64 `json:"-,"` // named "-" + Float4 float64 `json:"float4,omitempty"` // renamed, ignore if empty + Float5 float64 `json:","` // no-op + Float6 float64 `json:",omitempty"` // ignore if empty +} + +type TjsontagsComplex struct { + Complex0 complex128 `json:"-"` // first field ignored + Complex1 complex128 `json:"complex1"` // renamed + Complex2 complex128 `json:"-"` // ignored + Complex3 complex128 `json:"-,"` // named "-" + Complex4 complex128 `json:"complex4,omitempty"` // renamed, ignore if empty + Complex5 complex128 `json:","` // no-op + Complex6 complex128 `json:",omitempty"` // ignore if empty +} + +type TjsontagsPtr struct { + Ptr0 *string `json:"-"` // first field ignored + Ptr1 *string `json:"ptr1"` // renamed + Ptr2 *string `json:"-"` // ignored + Ptr3 *string `json:"-,"` // named "-" + Ptr4 *string `json:"ptr4,omitempty"` // renamed, ignore if empty + Ptr5 *string `json:","` // no-op + Ptr6 *string `json:",omitempty"` // ignore if empty +} + +type TjsontagsArray struct { + Array0 [2]string `json:"-"` // first field ignored + Array1 [2]string `json:"array1"` // renamed + Array2 [2]string `json:"-"` // ignored + Array3 [2]string `json:"-,"` // named "-" + Array4 [2]string `json:"array4,omitempty"` // renamed, ignore if empty + Array5 [2]string `json:","` // no-op + Array6 [2]string `json:",omitempty"` // ignore if empty +} + +type TjsontagsSlice struct { + Slice0 []string `json:"-"` // first field ignored + Slice1 []string `json:"slice1"` // renamed + Slice2 []string `json:"-"` // ignored + Slice3 []string `json:"-,"` // named "-" + Slice4 []string `json:"slice4,omitempty"` // renamed, ignore if empty + Slice5 []string `json:","` // no-op + Slice6 []string `json:",omitempty"` // ignore if empty +} + +type TjsontagsMap struct { + Map0 map[string]string `json:"-"` // first field ignored + Map1 map[string]string `json:"map1"` // renamed + Map2 map[string]string `json:"-"` // ignored + Map3 map[string]string `json:"-,"` // named "-" + Map4 map[string]string `json:"map4,omitempty"` // renamed, ignore if empty + Map5 map[string]string `json:","` // no-op + Map6 map[string]string `json:",omitempty"` // ignore if empty +} + +type Tinnerstruct struct { + Inner string +} +type Tinnerint int +type Tinnermap map[string]string +type Tinnerslice []string + +type Tembedstruct struct { + Tinnerstruct + Outer string +} + +type Tembednonstruct struct { + Tinnerint + Tinnermap + Tinnerslice +} + +type Tinner1 Tinnerstruct +type Tinner2 Tinnerstruct +type Tinner3 Tinnerstruct +type Tinner4 Tinnerstruct +type Tinner5 Tinnerstruct +type Tinner6 Tinnerstruct + +type Tembedjsontags struct { + Outer string + Tinner1 `json:"inner1"` + Tinner2 `json:"-"` + Tinner3 `json:"-,"` + Tinner4 `json:"inner4,omitempty"` + Tinner5 `json:","` + Tinner6 `json:"inner6,omitempty"` +} + +type Trawjson struct { + Message json.RawMessage `json:"message"` +} + +func TestPretty(t *testing.T) { + // used below + newStr := func(s string) *string { + return &s + } + + cases := []struct { + val any + exp string // used in cases where JSON can't handle it + }{{ + val: "strval", + }, { + val: "strval\nwith\t\"escapes\"", + }, { + val: substr("substrval"), + }, { + val: substr("substrval\nwith\t\"escapes\""), + }, { + val: true, + }, { + val: false, + }, { + val: int(93), + }, { + val: int8(93), + }, { + val: int16(93), + }, { + val: int32(93), + }, { + val: int64(93), + }, { + val: int(-93), + }, { + val: int8(-93), + }, { + val: int16(-93), + }, { + val: int32(-93), + }, { + val: int64(-93), + }, { + val: uint(93), + }, { + val: uint8(93), + }, { + val: uint16(93), + }, { + val: uint32(93), + }, { + val: uint64(93), + }, { + val: uintptr(93), + }, { + val: float32(93.76), + }, { + val: float64(93.76), + }, { + val: complex64(93i), + exp: `"(0+93i)"`, + }, { + val: complex128(93i), + exp: `"(0+93i)"`, + }, { + val: ptrint(93), + }, { + val: ptrstr("pstrval"), + }, { + val: []int{}, + }, { + val: []int(nil), + exp: `[]`, + }, { + val: []int{9, 3, 7, 6}, + }, { + val: []string{"str", "with\tescape"}, + }, { + val: []substr{"substr", "with\tescape"}, + }, { + val: [4]int{9, 3, 7, 6}, + }, { + val: [2]string{"str", "with\tescape"}, + }, { + val: [2]substr{"substr", "with\tescape"}, + }, { + val: struct { + Int int + notExported string + String string + }{ + 93, "you should not see this", "seventy-six", + }, + }, { + val: map[string]int{}, + }, { + val: map[string]int(nil), + exp: `{}`, + }, { + val: map[string]int{ + "nine": 3, + }, + }, { + val: map[string]int{ + "with\tescape": 76, + }, + }, { + val: map[substr]int{ + "nine": 3, + }, + }, { + val: map[substr]int{ + "with\tescape": 76, + }, + }, { + val: map[int]int{ + 9: 3, + }, + }, { + val: map[float64]int{ + 9.5: 3, + }, + exp: `{"9.5":3}`, + }, { + val: map[point]int{ + {x: 1, y: 2}: 3, + }, + }, { + val: map[pointErr]int{ + {x: 1, y: 2}: 3, + }, + exp: `{"":3}`, + }, { + val: struct { + X int `json:"x"` + Y int `json:"y"` + }{ + 93, 76, + }, + }, { + val: struct { + X []int + Y map[int]int + Z struct{ P, Q int } + }{ + []int{9, 3, 7, 6}, + map[int]int{9: 3}, + struct{ P, Q int }{9, 3}, + }, + }, { + val: []struct{ X, Y string }{ + {"nine", "three"}, + {"seven", "six"}, + {"with\t", "\tescapes"}, + }, + }, { + val: struct { + A *int + B *int + C any + D any + }{ + B: ptrint(1), + D: any(2), + }, + }, { + val: Tmarshaler{"foobar"}, + exp: `{"Inner":"I am a logr.Marshaler"}`, + }, { + val: &Tmarshaler{"foobar"}, + exp: `{"Inner":"I am a logr.Marshaler"}`, + }, { + val: (*Tmarshaler)(nil), + exp: `""`, + }, { + val: Tmarshalerpanic{"foobar"}, + exp: `""`, + }, { + val: Tstringer{"foobar"}, + exp: `"I am a fmt.Stringer"`, + }, { + val: &Tstringer{"foobar"}, + exp: `"I am a fmt.Stringer"`, + }, { + val: (*Tstringer)(nil), + exp: `""`, + }, { + val: Tstringerpanic{"foobar"}, + exp: `""`, + }, { + val: Terror{"foobar"}, + exp: `"I am an error"`, + }, { + val: &Terror{"foobar"}, + exp: `"I am an error"`, + }, { + val: (*Terror)(nil), + exp: `""`, + }, { + val: Terrorpanic{"foobar"}, + exp: `""`, + }, { + val: TjsontagsString{ + String1: "v1", + String2: "v2", + String3: "v3", + String4: "v4", + String5: "v5", + String6: "v6", + }, + }, { + val: TjsontagsString{}, + }, { + val: TjsontagsBool{ + Bool1: true, + Bool2: true, + Bool3: true, + Bool4: true, + Bool5: true, + Bool6: true, + }, + }, { + val: TjsontagsBool{}, + }, { + val: TjsontagsInt{ + Int1: 1, + Int2: 2, + Int3: 3, + Int4: 4, + Int5: 5, + Int6: 6, + }, + }, { + val: TjsontagsInt{}, + }, { + val: TjsontagsUint{ + Uint1: 1, + Uint2: 2, + Uint3: 3, + Uint4: 4, + Uint5: 5, + Uint6: 6, + }, + }, { + val: TjsontagsUint{}, + }, { + val: TjsontagsFloat{ + Float1: 1.1, + Float2: 2.2, + Float3: 3.3, + Float4: 4.4, + Float5: 5.5, + Float6: 6.6, + }, + }, { + val: TjsontagsFloat{}, + }, { + val: TjsontagsComplex{ + Complex1: 1i, + Complex2: 2i, + Complex3: 3i, + Complex4: 4i, + Complex5: 5i, + Complex6: 6i, + }, + exp: `{"complex1":"(0+1i)","-":"(0+3i)","complex4":"(0+4i)","Complex5":"(0+5i)","Complex6":"(0+6i)"}`, + }, { + val: TjsontagsComplex{}, + exp: `{"complex1":"(0+0i)","-":"(0+0i)","Complex5":"(0+0i)"}`, + }, { + val: TjsontagsPtr{ + Ptr1: newStr("1"), + Ptr2: newStr("2"), + Ptr3: newStr("3"), + Ptr4: newStr("4"), + Ptr5: newStr("5"), + Ptr6: newStr("6"), + }, + }, { + val: TjsontagsPtr{}, + }, { + val: TjsontagsArray{ + Array1: [2]string{"v1", "v1"}, + Array2: [2]string{"v2", "v2"}, + Array3: [2]string{"v3", "v3"}, + Array4: [2]string{"v4", "v4"}, + Array5: [2]string{"v5", "v5"}, + Array6: [2]string{"v6", "v6"}, + }, + }, { + val: TjsontagsArray{}, + }, { + val: TjsontagsSlice{ + Slice1: []string{"v1", "v1"}, + Slice2: []string{"v2", "v2"}, + Slice3: []string{"v3", "v3"}, + Slice4: []string{"v4", "v4"}, + Slice5: []string{"v5", "v5"}, + Slice6: []string{"v6", "v6"}, + }, + }, { + val: TjsontagsSlice{}, + exp: `{"slice1":[],"-":[],"Slice5":[]}`, + }, { + val: TjsontagsMap{ + Map1: map[string]string{"k1": "v1"}, + Map2: map[string]string{"k2": "v2"}, + Map3: map[string]string{"k3": "v3"}, + Map4: map[string]string{"k4": "v4"}, + Map5: map[string]string{"k5": "v5"}, + Map6: map[string]string{"k6": "v6"}, + }, + }, { + val: TjsontagsMap{}, + exp: `{"map1":{},"-":{},"Map5":{}}`, + }, { + val: Tembedstruct{}, + }, { + val: Tembednonstruct{}, + exp: `{"Tinnerint":0,"Tinnermap":{},"Tinnerslice":[]}`, + }, { + val: Tembedjsontags{}, + }, { + val: PseudoStruct(makeKV("f1", 1, "f2", true, "f3", []int{})), + exp: `{"f1":1,"f2":true,"f3":[]}`, + }, { + val: map[TjsontagsString]int{ + {String1: `"quoted"`, String4: `unquoted`}: 1, + }, + exp: `{"{\"string1\":\"\\\"quoted\\\"\",\"-\":\"\",\"string4\":\"unquoted\",\"String5\":\"\"}":1}`, + }, { + val: map[TjsontagsInt]int{ + {Int1: 1, Int2: 2}: 3, + }, + exp: `{"{\"int1\":1,\"-\":0,\"Int5\":0}":3}`, + }, { + val: map[[2]struct{ S string }]int{ + {{S: `"quoted"`}, {S: "unquoted"}}: 1, + }, + exp: `{"[{\"S\":\"\\\"quoted\\\"\"},{\"S\":\"unquoted\"}]":1}`, + }, { + val: TjsontagsComplex{}, + exp: `{"complex1":"(0+0i)","-":"(0+0i)","Complex5":"(0+0i)"}`, + }, { + val: TjsontagsPtr{ + Ptr1: newStr("1"), + Ptr2: newStr("2"), + Ptr3: newStr("3"), + Ptr4: newStr("4"), + Ptr5: newStr("5"), + Ptr6: newStr("6"), + }, + }, { + val: TjsontagsPtr{}, + }, { + val: TjsontagsArray{ + Array1: [2]string{"v1", "v1"}, + Array2: [2]string{"v2", "v2"}, + Array3: [2]string{"v3", "v3"}, + Array4: [2]string{"v4", "v4"}, + Array5: [2]string{"v5", "v5"}, + Array6: [2]string{"v6", "v6"}, + }, + }, { + val: TjsontagsArray{}, + }, { + val: TjsontagsSlice{ + Slice1: []string{"v1", "v1"}, + Slice2: []string{"v2", "v2"}, + Slice3: []string{"v3", "v3"}, + Slice4: []string{"v4", "v4"}, + Slice5: []string{"v5", "v5"}, + Slice6: []string{"v6", "v6"}, + }, + }, { + val: TjsontagsSlice{}, + exp: `{"slice1":[],"-":[],"Slice5":[]}`, + }, { + val: TjsontagsMap{ + Map1: map[string]string{"k1": "v1"}, + Map2: map[string]string{"k2": "v2"}, + Map3: map[string]string{"k3": "v3"}, + Map4: map[string]string{"k4": "v4"}, + Map5: map[string]string{"k5": "v5"}, + Map6: map[string]string{"k6": "v6"}, + }, + }, { + val: TjsontagsMap{}, + exp: `{"map1":{},"-":{},"Map5":{}}`, + }, { + val: Tembedstruct{}, + }, { + val: Tembednonstruct{}, + exp: `{"Tinnerint":0,"Tinnermap":{},"Tinnerslice":[]}`, + }, { + val: Tembedjsontags{}, + }, { + val: PseudoStruct(makeKV("f1", 1, "f2", true, "f3", []int{})), + exp: `{"f1":1,"f2":true,"f3":[]}`, + }, { + val: map[TjsontagsString]int{ + {String1: `"quoted"`, String4: `unquoted`}: 1, + }, + exp: `{"{\"string1\":\"\\\"quoted\\\"\",\"-\":\"\",\"string4\":\"unquoted\",\"String5\":\"\"}":1}`, + }, { + val: map[TjsontagsInt]int{ + {Int1: 1, Int2: 2}: 3, + }, + exp: `{"{\"int1\":1,\"-\":0,\"Int5\":0}":3}`, + }, { + val: map[[2]struct{ S string }]int{ + {{S: `"quoted"`}, {S: "unquoted"}}: 1, + }, + exp: `{"[{\"S\":\"\\\"quoted\\\"\"},{\"S\":\"unquoted\"}]":1}`, + }} + + f := NewFormatterJSON(Options{}) + for i, tc := range cases { + ours := f.pretty(tc.val) + want := "" + if tc.exp != "" { + want = tc.exp + } else { + jb, err := json.Marshal(tc.val) + if err != nil { + t.Fatalf("[%d]: unexpected error: %v\ngot: %q", i, err, ours) + } + want = string(jb) + } + if ours != want { + t.Errorf("[%d]:\n\texpected %q\n\tgot %q", i, want, ours) + } + } +} + +func makeKV(args ...any) []any { + return args +} + +func TestRender(t *testing.T) { + // used below + raw := &Trawjson{} + marshal := &TjsontagsInt{} + var err error + raw.Message, err = json.Marshal(marshal) + if err != nil { + t.Fatalf("json.Marshal error: %v", err) + } + + testCases := []struct { + name string + builtins []any + values []any + args []any + expectKV string + expectJSON string + }{{ + name: "nil", + expectKV: "", + expectJSON: "{}", + }, { + name: "empty", + builtins: []any{}, + values: []any{}, + args: []any{}, + expectKV: "", + expectJSON: "{}", + }, { + name: "primitives", + builtins: makeKV("int1", 1, "int2", 2), + values: makeKV("str1", "ABC", "str2", "DEF"), + args: makeKV("bool1", true, "bool2", false), + expectKV: `"int1"=1 "int2"=2 "str1"="ABC" "str2"="DEF" "bool1"=true "bool2"=false`, + expectJSON: `{"int1":1,"int2":2,"str1":"ABC","str2":"DEF","bool1":true,"bool2":false}`, + }, { + name: "pseudo structs", + builtins: makeKV("int", PseudoStruct(makeKV("intsub", 1))), + values: makeKV("str", PseudoStruct(makeKV("strsub", "2"))), + args: makeKV("bool", PseudoStruct(makeKV("boolsub", true))), + expectKV: `"int"={"intsub"=1} "str"={"strsub"="2"} "bool"={"boolsub"=true}`, + expectJSON: `{"int":{"intsub":1},"str":{"strsub":"2"},"bool":{"boolsub":true}}`, + }, { + name: "escapes", + builtins: makeKV("\"1\"", 1), // will not be escaped, but should never happen + values: makeKV("\tstr", "ABC"), // escaped + args: makeKV("bool\n", true), // escaped + expectKV: `""1""=1 "\tstr"="ABC" "bool\n"=true`, + expectJSON: `{""1"":1,"\tstr":"ABC","bool\n":true}`, + }, { + name: "missing value", + builtins: makeKV("builtin"), + values: makeKV("value"), + args: makeKV("arg"), + expectKV: `"builtin"="" "value"="" "arg"=""`, + expectJSON: `{"builtin":"","value":"","arg":""}`, + }, { + name: "non-string key int", + builtins: makeKV(123, "val"), // should never happen + values: makeKV(456, "val"), + args: makeKV(789, "val"), + expectKV: `""="val" ""="val" ""="val"`, + expectJSON: `{"":"val","":"val","":"val"}`, + }, { + name: "non-string key struct", + builtins: makeKV(struct { // will not be escaped, but should never happen + F1 string + F2 int + }{"builtin", 123}, "val"), + values: makeKV(struct { + F1 string + F2 int + }{"value", 456}, "val"), + args: makeKV(struct { + F1 string + F2 int + }{"arg", 789}, "val"), + expectKV: `""="val" ""="val" ""="val"`, + expectJSON: `{"":"val","":"val","":"val"}`, + }, { + name: "json rendering with json.RawMessage", + args: makeKV("key", raw), + expectKV: `"key"={"message"=[123 34 105 110 116 49 34 58 48 44 34 45 34 58 48 44 34 73 110 116 53 34 58 48 125]}`, + expectJSON: `{"key":{"message":{"int1":0,"-":0,"Int5":0}}}`, + }, { + name: "byte array not json.RawMessage", + args: makeKV("key", []byte{1, 2, 3, 4}), + expectKV: `"key"=[1 2 3 4]`, + expectJSON: `{"key":[1,2,3,4]}`, + }, { + name: "json rendering with empty json.RawMessage", + args: makeKV("key", &Trawjson{}), + expectKV: `"key"={"message"=[]}`, + expectJSON: `{"key":{"message":null}}`, + }} + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + test := func(t *testing.T, formatter Formatter, expect string) { + formatter.AddValues(tc.values) + r := formatter.render(tc.builtins, tc.args) + if r != expect { + t.Errorf("wrong output:\nexpected %q\n got %q", expect, r) + } + } + t.Run("KV", func(t *testing.T) { + test(t, NewFormatter(Options{}), tc.expectKV) + }) + t.Run("JSON", func(t *testing.T) { + test(t, NewFormatterJSON(Options{}), tc.expectJSON) + }) + }) + } +} + +func TestSanitize(t *testing.T) { + testCases := []struct { + name string + kv []any + expect []any + }{{ + name: "empty", + kv: []any{}, + expect: []any{}, + }, { + name: "already sane", + kv: makeKV("int", 1, "str", "ABC", "bool", true), + expect: makeKV("int", 1, "str", "ABC", "bool", true), + }, { + name: "missing value", + kv: makeKV("key"), + expect: makeKV("key", ""), + }, { + name: "non-string key int", + kv: makeKV(123, "val"), + expect: makeKV("", "val"), + }, { + name: "non-string key struct", + kv: makeKV(struct { + F1 string + F2 int + }{"f1", 8675309}, "val"), + expect: makeKV(``, "val"), + }} + + f := NewFormatterJSON(Options{}) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + r := f.sanitize(tc.kv) + if !reflect.DeepEqual(r, tc.expect) { + t.Errorf("wrong output:\nexpected %q\n got %q", tc.expect, r) + } + }) + } +} + +func TestEnabled(t *testing.T) { + t.Run("default V", func(t *testing.T) { + log := newSink(func(_, _ string) {}, NewFormatter(Options{})) + if !log.Enabled(0) { + t.Errorf("expected true") + } + if log.Enabled(1) { + t.Errorf("expected false") + } + }) + t.Run("V=9", func(t *testing.T) { + log := newSink(func(_, _ string) {}, NewFormatter(Options{Verbosity: 9})) + if !log.Enabled(8) { + t.Errorf("expected true") + } + if !log.Enabled(9) { + t.Errorf("expected true") + } + if log.Enabled(10) { + t.Errorf("expected false") + } + }) +} + +type capture struct { + log string +} + +func (c *capture) Func(prefix, args string) { + space := " " + if len(prefix) == 0 { + space = "" + } + c.log = prefix + space + args +} + +func TestInfo(t *testing.T) { + testCases := []struct { + name string + args []any + expectKV string + expectJSON string + }{{ + name: "just msg", + args: makeKV(), + expectKV: `"level"=0 "msg"="msg"`, + expectJSON: `{"logger":"","level":0,"msg":"msg"}`, + }, { + name: "primitives", + args: makeKV("int", 1, "str", "ABC", "bool", true), + expectKV: `"level"=0 "msg"="msg" "int"=1 "str"="ABC" "bool"=true`, + expectJSON: `{"logger":"","level":0,"msg":"msg","int":1,"str":"ABC","bool":true}`, + }} + + for _, tc := range testCases { + t.Run("KV: "+tc.name, func(t *testing.T) { + capt := &capture{} + sink := newSink(capt.Func, NewFormatter(Options{})) + sink.Info(0, "msg", tc.args...) + if capt.log != tc.expectKV { + t.Errorf("\nexpected %q\n got %q", tc.expectKV, capt.log) + } + }) + t.Run("JSON: "+tc.name, func(t *testing.T) { + capt := &capture{} + sink := newSink(capt.Func, NewFormatterJSON(Options{})) + sink.Info(0, "msg", tc.args...) + if capt.log != tc.expectJSON { + t.Errorf("\nexpected %q\n got %q", tc.expectJSON, capt.log) + } + }) + } +} + +func TestInfoWithCaller(t *testing.T) { + t.Run("KV: LogCaller=All", func(t *testing.T) { + capt := &capture{} + sink := newSink(capt.Func, NewFormatter(Options{LogCaller: All})) + sink.Info(0, "msg") + _, file, line, _ := runtime.Caller(0) + expect := fmt.Sprintf(`"caller"={"file"=%q "line"=%d} "level"=0 "msg"="msg"`, filepath.Base(file), line-1) + if capt.log != expect { + t.Errorf("\nexpected %q\n got %q", expect, capt.log) + } + sink.Error(fmt.Errorf("error"), "msg") + _, file, line, _ = runtime.Caller(0) + expect = fmt.Sprintf(`"caller"={"file"=%q "line"=%d} "msg"="msg" "error"="error"`, filepath.Base(file), line-1) + if capt.log != expect { + t.Errorf("\nexpected %q\n got %q", expect, capt.log) + } + }) + t.Run("JSON: LogCaller=All", func(t *testing.T) { + capt := &capture{} + sink := newSink(capt.Func, NewFormatterJSON(Options{LogCaller: All})) + sink.Info(0, "msg") + _, file, line, _ := runtime.Caller(0) + expect := fmt.Sprintf(`{"logger":"","caller":{"file":%q,"line":%d},"level":0,"msg":"msg"}`, filepath.Base(file), line-1) + if capt.log != expect { + t.Errorf("\nexpected %q\n got %q", expect, capt.log) + } + sink.Error(fmt.Errorf("error"), "msg") + _, file, line, _ = runtime.Caller(0) + expect = fmt.Sprintf(`{"logger":"","caller":{"file":%q,"line":%d},"msg":"msg","error":"error"}`, filepath.Base(file), line-1) + if capt.log != expect { + t.Errorf("\nexpected %q\n got %q", expect, capt.log) + } + }) + t.Run("KV: LogCaller=All, LogCallerFunc=true", func(t *testing.T) { + thisFunc := "github.com/CodSpeedHQ/codspeed-go/pkg/logr/funcr.TestInfoWithCaller.func3" + capt := &capture{} + sink := newSink(capt.Func, NewFormatter(Options{LogCaller: All, LogCallerFunc: true})) + sink.Info(0, "msg") + _, file, line, _ := runtime.Caller(0) + expect := fmt.Sprintf(`"caller"={"file"=%q "line"=%d "function"=%q} "level"=0 "msg"="msg"`, filepath.Base(file), line-1, thisFunc) + if capt.log != expect { + t.Errorf("\nexpected %q\n got %q", expect, capt.log) + } + sink.Error(fmt.Errorf("error"), "msg") + _, file, line, _ = runtime.Caller(0) + expect = fmt.Sprintf(`"caller"={"file"=%q "line"=%d "function"=%q} "msg"="msg" "error"="error"`, filepath.Base(file), line-1, thisFunc) + if capt.log != expect { + t.Errorf("\nexpected %q\n got %q", expect, capt.log) + } + }) + t.Run("JSON: LogCaller=All, LogCallerFunc=true", func(t *testing.T) { + thisFunc := "github.com/CodSpeedHQ/codspeed-go/pkg/logr/funcr.TestInfoWithCaller.func4" + capt := &capture{} + sink := newSink(capt.Func, NewFormatterJSON(Options{LogCaller: All, LogCallerFunc: true})) + sink.Info(0, "msg") + _, file, line, _ := runtime.Caller(0) + expect := fmt.Sprintf(`{"logger":"","caller":{"file":%q,"line":%d,"function":%q},"level":0,"msg":"msg"}`, filepath.Base(file), line-1, thisFunc) + if capt.log != expect { + t.Errorf("\nexpected %q\n got %q", expect, capt.log) + } + sink.Error(fmt.Errorf("error"), "msg") + _, file, line, _ = runtime.Caller(0) + expect = fmt.Sprintf(`{"logger":"","caller":{"file":%q,"line":%d,"function":%q},"msg":"msg","error":"error"}`, filepath.Base(file), line-1, thisFunc) + if capt.log != expect { + t.Errorf("\nexpected %q\n got %q", expect, capt.log) + } + }) + t.Run("LogCaller=Info", func(t *testing.T) { + capt := &capture{} + sink := newSink(capt.Func, NewFormatter(Options{LogCaller: Info})) + sink.Info(0, "msg") + _, file, line, _ := runtime.Caller(0) + expect := fmt.Sprintf(`"caller"={"file"=%q "line"=%d} "level"=0 "msg"="msg"`, filepath.Base(file), line-1) + if capt.log != expect { + t.Errorf("\nexpected %q\n got %q", expect, capt.log) + } + sink.Error(fmt.Errorf("error"), "msg") + expect = `"msg"="msg" "error"="error"` + if capt.log != expect { + t.Errorf("\nexpected %q\n got %q", expect, capt.log) + } + }) + t.Run("LogCaller=Error", func(t *testing.T) { + capt := &capture{} + sink := newSink(capt.Func, NewFormatter(Options{LogCaller: Error})) + sink.Info(0, "msg") + expect := `"level"=0 "msg"="msg"` + if capt.log != expect { + t.Errorf("\nexpected %q\n got %q", expect, capt.log) + } + sink.Error(fmt.Errorf("error"), "msg") + _, file, line, _ := runtime.Caller(0) + expect = fmt.Sprintf(`"caller"={"file"=%q "line"=%d} "msg"="msg" "error"="error"`, filepath.Base(file), line-1) + if capt.log != expect { + t.Errorf("\nexpected %q\n got %q", expect, capt.log) + } + }) + t.Run("LogCaller=None", func(t *testing.T) { + capt := &capture{} + sink := newSink(capt.Func, NewFormatter(Options{LogCaller: None})) + sink.Info(0, "msg") + expect := `"level"=0 "msg"="msg"` + if capt.log != expect { + t.Errorf("\nexpected %q\n got %q", expect, capt.log) + } + sink.Error(fmt.Errorf("error"), "msg") + expect = `"msg"="msg" "error"="error"` + if capt.log != expect { + t.Errorf("\nexpected %q\n got %q", expect, capt.log) + } + }) +} + +func TestError(t *testing.T) { + testCases := []struct { + name string + args []any + expectKV string + expectJSON string + }{{ + name: "just msg", + args: makeKV(), + expectKV: `"msg"="msg" "error"="err"`, + expectJSON: `{"logger":"","msg":"msg","error":"err"}`, + }, { + name: "primitives", + args: makeKV("int", 1, "str", "ABC", "bool", true), + expectKV: `"msg"="msg" "error"="err" "int"=1 "str"="ABC" "bool"=true`, + expectJSON: `{"logger":"","msg":"msg","error":"err","int":1,"str":"ABC","bool":true}`, + }} + + for _, tc := range testCases { + t.Run("KV: "+tc.name, func(t *testing.T) { + capt := &capture{} + sink := newSink(capt.Func, NewFormatter(Options{})) + sink.Error(fmt.Errorf("err"), "msg", tc.args...) + if capt.log != tc.expectKV { + t.Errorf("\nexpected %q\n got %q", tc.expectKV, capt.log) + } + }) + t.Run("JSON: "+tc.name, func(t *testing.T) { + capt := &capture{} + sink := newSink(capt.Func, NewFormatterJSON(Options{})) + sink.Error(fmt.Errorf("err"), "msg", tc.args...) + if capt.log != tc.expectJSON { + t.Errorf("\nexpected %q\n got %q", tc.expectJSON, capt.log) + } + }) + } +} + +func TestErrorWithCaller(t *testing.T) { + t.Run("KV: LogCaller=All", func(t *testing.T) { + capt := &capture{} + sink := newSink(capt.Func, NewFormatter(Options{LogCaller: All})) + sink.Error(fmt.Errorf("err"), "msg") + _, file, line, _ := runtime.Caller(0) + expect := fmt.Sprintf(`"caller"={"file"=%q "line"=%d} "msg"="msg" "error"="err"`, filepath.Base(file), line-1) + if capt.log != expect { + t.Errorf("\nexpected %q\n got %q", expect, capt.log) + } + }) + t.Run("JSON: LogCaller=All", func(t *testing.T) { + capt := &capture{} + sink := newSink(capt.Func, NewFormatterJSON(Options{LogCaller: All})) + sink.Error(fmt.Errorf("err"), "msg") + _, file, line, _ := runtime.Caller(0) + expect := fmt.Sprintf(`{"logger":"","caller":{"file":%q,"line":%d},"msg":"msg","error":"err"}`, filepath.Base(file), line-1) + if capt.log != expect { + t.Errorf("\nexpected %q\n got %q", expect, capt.log) + } + }) + t.Run("LogCaller=Error", func(t *testing.T) { + capt := &capture{} + sink := newSink(capt.Func, NewFormatter(Options{LogCaller: Error})) + sink.Error(fmt.Errorf("err"), "msg") + _, file, line, _ := runtime.Caller(0) + expect := fmt.Sprintf(`"caller"={"file"=%q "line"=%d} "msg"="msg" "error"="err"`, filepath.Base(file), line-1) + if capt.log != expect { + t.Errorf("\nexpected %q\n got %q", expect, capt.log) + } + }) + t.Run("LogCaller=Info", func(t *testing.T) { + capt := &capture{} + sink := newSink(capt.Func, NewFormatter(Options{LogCaller: Info})) + sink.Error(fmt.Errorf("err"), "msg") + expect := `"msg"="msg" "error"="err"` + if capt.log != expect { + t.Errorf("\nexpected %q\n got %q", expect, capt.log) + } + }) + t.Run("LogCaller=None", func(t *testing.T) { + capt := &capture{} + sink := newSink(capt.Func, NewFormatter(Options{LogCaller: None})) + sink.Error(fmt.Errorf("err"), "msg") + expect := `"msg"="msg" "error"="err"` + if capt.log != expect { + t.Errorf("\nexpected %q\n got %q", expect, capt.log) + } + }) +} + +func TestInfoWithName(t *testing.T) { + testCases := []struct { + name string + names []string + args []any + expectKV string + expectJSON string + }{{ + name: "one", + names: []string{"pfx1"}, + args: makeKV("k", "v"), + expectKV: `pfx1 "level"=0 "msg"="msg" "k"="v"`, + expectJSON: `{"logger":"pfx1","level":0,"msg":"msg","k":"v"}`, + }, { + name: "two", + names: []string{"pfx1", "pfx2"}, + args: makeKV("k", "v"), + expectKV: `pfx1/pfx2 "level"=0 "msg"="msg" "k"="v"`, + expectJSON: `{"logger":"pfx1/pfx2","level":0,"msg":"msg","k":"v"}`, + }} + + for _, tc := range testCases { + t.Run("KV: "+tc.name, func(t *testing.T) { + capt := &capture{} + sink := newSink(capt.Func, NewFormatter(Options{})) + for _, n := range tc.names { + sink = sink.WithName(n) + } + sink.Info(0, "msg", tc.args...) + if capt.log != tc.expectKV { + t.Errorf("\nexpected %q\n got %q", tc.expectKV, capt.log) + } + }) + t.Run("JSON: "+tc.name, func(t *testing.T) { + capt := &capture{} + sink := newSink(capt.Func, NewFormatterJSON(Options{})) + for _, n := range tc.names { + sink = sink.WithName(n) + } + sink.Info(0, "msg", tc.args...) + if capt.log != tc.expectJSON { + t.Errorf("\nexpected %q\n got %q", tc.expectJSON, capt.log) + } + }) + } +} + +func TestErrorWithName(t *testing.T) { + testCases := []struct { + name string + names []string + args []any + expectKV string + expectJSON string + }{{ + name: "one", + names: []string{"pfx1"}, + args: makeKV("k", "v"), + expectKV: `pfx1 "msg"="msg" "error"="err" "k"="v"`, + expectJSON: `{"logger":"pfx1","msg":"msg","error":"err","k":"v"}`, + }, { + name: "two", + names: []string{"pfx1", "pfx2"}, + args: makeKV("k", "v"), + expectKV: `pfx1/pfx2 "msg"="msg" "error"="err" "k"="v"`, + expectJSON: `{"logger":"pfx1/pfx2","msg":"msg","error":"err","k":"v"}`, + }} + + for _, tc := range testCases { + t.Run("KV: "+tc.name, func(t *testing.T) { + capt := &capture{} + sink := newSink(capt.Func, NewFormatter(Options{})) + for _, n := range tc.names { + sink = sink.WithName(n) + } + sink.Error(fmt.Errorf("err"), "msg", tc.args...) + if capt.log != tc.expectKV { + t.Errorf("\nexpected %q\n got %q", tc.expectKV, capt.log) + } + }) + t.Run("JSON: "+tc.name, func(t *testing.T) { + capt := &capture{} + sink := newSink(capt.Func, NewFormatterJSON(Options{})) + for _, n := range tc.names { + sink = sink.WithName(n) + } + sink.Error(fmt.Errorf("err"), "msg", tc.args...) + if capt.log != tc.expectJSON { + t.Errorf("\nexpected %q\n got %q", tc.expectJSON, capt.log) + } + }) + } +} + +func TestInfoWithValues(t *testing.T) { + testCases := []struct { + name string + values []any + args []any + expectKV string + expectJSON string + }{{ + name: "zero", + values: makeKV(), + args: makeKV("k", "v"), + expectKV: `"level"=0 "msg"="msg" "k"="v"`, + expectJSON: `{"logger":"","level":0,"msg":"msg","k":"v"}`, + }, { + name: "one", + values: makeKV("one", 1), + args: makeKV("k", "v"), + expectKV: `"level"=0 "msg"="msg" "one"=1 "k"="v"`, + expectJSON: `{"logger":"","level":0,"msg":"msg","one":1,"k":"v"}`, + }, { + name: "two", + values: makeKV("one", 1, "two", 2), + args: makeKV("k", "v"), + expectKV: `"level"=0 "msg"="msg" "one"=1 "two"=2 "k"="v"`, + expectJSON: `{"logger":"","level":0,"msg":"msg","one":1,"two":2,"k":"v"}`, + }, { + name: "dangling", + values: makeKV("dangling"), + args: makeKV("k", "v"), + expectKV: `"level"=0 "msg"="msg" "dangling"="" "k"="v"`, + expectJSON: `{"logger":"","level":0,"msg":"msg","dangling":"","k":"v"}`, + }} + + for _, tc := range testCases { + t.Run("KV: "+tc.name, func(t *testing.T) { + capt := &capture{} + sink := newSink(capt.Func, NewFormatter(Options{})) + sink = sink.WithValues(tc.values...) + sink.Info(0, "msg", tc.args...) + if capt.log != tc.expectKV { + t.Errorf("\nexpected %q\n got %q", tc.expectKV, capt.log) + } + }) + t.Run("JSON: "+tc.name, func(t *testing.T) { + capt := &capture{} + sink := newSink(capt.Func, NewFormatterJSON(Options{})) + sink = sink.WithValues(tc.values...) + sink.Info(0, "msg", tc.args...) + if capt.log != tc.expectJSON { + t.Errorf("\nexpected %q\n got %q", tc.expectJSON, capt.log) + } + }) + } +} + +func TestErrorWithValues(t *testing.T) { + testCases := []struct { + name string + values []any + args []any + expectKV string + expectJSON string + }{{ + name: "zero", + values: makeKV(), + args: makeKV("k", "v"), + expectKV: `"msg"="msg" "error"="err" "k"="v"`, + expectJSON: `{"logger":"","msg":"msg","error":"err","k":"v"}`, + }, { + name: "one", + values: makeKV("one", 1), + args: makeKV("k", "v"), + expectKV: `"msg"="msg" "error"="err" "one"=1 "k"="v"`, + expectJSON: `{"logger":"","msg":"msg","error":"err","one":1,"k":"v"}`, + }, { + name: "two", + values: makeKV("one", 1, "two", 2), + args: makeKV("k", "v"), + expectKV: `"msg"="msg" "error"="err" "one"=1 "two"=2 "k"="v"`, + expectJSON: `{"logger":"","msg":"msg","error":"err","one":1,"two":2,"k":"v"}`, + }, { + name: "dangling", + values: makeKV("dangling"), + args: makeKV("k", "v"), + expectKV: `"msg"="msg" "error"="err" "dangling"="" "k"="v"`, + expectJSON: `{"logger":"","msg":"msg","error":"err","dangling":"","k":"v"}`, + }} + + for _, tc := range testCases { + t.Run("KV: "+tc.name, func(t *testing.T) { + capt := &capture{} + sink := newSink(capt.Func, NewFormatter(Options{})) + sink = sink.WithValues(tc.values...) + sink.Error(fmt.Errorf("err"), "msg", tc.args...) + if capt.log != tc.expectKV { + t.Errorf("\nexpected %q\n got %q", tc.expectKV, capt.log) + } + }) + t.Run("JSON: "+tc.name, func(t *testing.T) { + capt := &capture{} + sink := newSink(capt.Func, NewFormatterJSON(Options{})) + sink = sink.WithValues(tc.values...) + sink.Error(fmt.Errorf("err"), "msg", tc.args...) + if capt.log != tc.expectJSON { + t.Errorf("\nexpected %q\n got %q", tc.expectJSON, capt.log) + } + }) + } +} + +func TestInfoWithCallDepth(t *testing.T) { + t.Run("one", func(t *testing.T) { + capt := &capture{} + sink := newSink(capt.Func, NewFormatter(Options{LogCaller: All})) + dSink, _ := sink.(logr.CallDepthLogSink) + sink = dSink.WithCallDepth(1) + sink.Info(0, "msg") + _, file, line, _ := runtime.Caller(1) + expect := fmt.Sprintf(`"caller"={"file"=%q "line"=%d} "level"=0 "msg"="msg"`, filepath.Base(file), line) + if capt.log != expect { + t.Errorf("\nexpected %q\n got %q", expect, capt.log) + } + }) +} + +func TestErrorWithCallDepth(t *testing.T) { + t.Run("one", func(t *testing.T) { + capt := &capture{} + sink := newSink(capt.Func, NewFormatter(Options{LogCaller: All})) + dSink, _ := sink.(logr.CallDepthLogSink) + sink = dSink.WithCallDepth(1) + sink.Error(fmt.Errorf("err"), "msg") + _, file, line, _ := runtime.Caller(1) + expect := fmt.Sprintf(`"caller"={"file"=%q "line"=%d} "msg"="msg" "error"="err"`, filepath.Base(file), line) + if capt.log != expect { + t.Errorf("\nexpected %q\n got %q", expect, capt.log) + } + }) +} + +func TestOptionsTimestampFormat(t *testing.T) { + capt := &capture{} + // This timestamp format contains none of the characters that are + // considered placeholders, so will produce a constant result. + sink := newSink(capt.Func, NewFormatter(Options{LogTimestamp: true, TimestampFormat: "TIMESTAMP"})) + dSink, _ := sink.(logr.CallDepthLogSink) + sink = dSink.WithCallDepth(1) + sink.Info(0, "msg") + expect := `"ts"="TIMESTAMP" "level"=0 "msg"="msg"` + if capt.log != expect { + t.Errorf("\nexpected %q\n got %q", expect, capt.log) + } +} + +func TestOptionsLogInfoLevel(t *testing.T) { + testCases := []struct { + name string + level *string + expect string + }{ + { + name: "custom key", + level: ptrstr("info_level"), + expect: `"info_level"=0 "msg"="msg"`, + }, + { + name: "no level", + level: ptrstr(""), + expect: `"msg"="msg"`, + }, + { + name: "default", + level: nil, + expect: `"level"=0 "msg"="msg"`, + }, + } + + for _, tc := range testCases { + t.Run("Run: "+tc.name, func(t *testing.T) { + capt := &capture{} + sink := newSink(capt.Func, NewFormatter(Options{LogInfoLevel: tc.level})) + dSink, _ := sink.(logr.CallDepthLogSink) + sink = dSink.WithCallDepth(1) + sink.Info(0, "msg") + if capt.log != tc.expect { + t.Errorf("\nexpected %q\n got %q", tc.expect, capt.log) + } + }) + } +} diff --git a/pkg/logr/funcr/slogsink.go b/pkg/logr/funcr/slogsink.go new file mode 100644 index 00000000..4367ba0a --- /dev/null +++ b/pkg/logr/funcr/slogsink.go @@ -0,0 +1,105 @@ +//go:build go1.21 +// +build go1.21 + +/* +Copyright 2023 The logr Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package funcr + +import ( + "context" + "log/slog" + + "github.com/CodSpeedHQ/codspeed-go/pkg/logr" +) + +var _ logr.SlogSink = &fnlogger{} + +const extraSlogSinkDepth = 3 // 2 for slog, 1 for SlogSink + +func (l fnlogger) Handle(_ context.Context, record slog.Record) error { + kvList := make([]any, 0, 2*record.NumAttrs()) + record.Attrs(func(attr slog.Attr) bool { + kvList = attrToKVs(attr, kvList) + return true + }) + + if record.Level >= slog.LevelError { + l.WithCallDepth(extraSlogSinkDepth).Error(nil, record.Message, kvList...) + } else { + level := l.levelFromSlog(record.Level) + l.WithCallDepth(extraSlogSinkDepth).Info(level, record.Message, kvList...) + } + return nil +} + +func (l fnlogger) WithAttrs(attrs []slog.Attr) logr.SlogSink { + kvList := make([]any, 0, 2*len(attrs)) + for _, attr := range attrs { + kvList = attrToKVs(attr, kvList) + } + l.AddValues(kvList) + return &l +} + +func (l fnlogger) WithGroup(name string) logr.SlogSink { + l.startGroup(name) + return &l +} + +// attrToKVs appends a slog.Attr to a logr-style kvList. It handle slog Groups +// and other details of slog. +func attrToKVs(attr slog.Attr, kvList []any) []any { + attrVal := attr.Value.Resolve() + if attrVal.Kind() == slog.KindGroup { + groupVal := attrVal.Group() + grpKVs := make([]any, 0, 2*len(groupVal)) + for _, attr := range groupVal { + grpKVs = attrToKVs(attr, grpKVs) + } + if attr.Key == "" { + // slog says we have to inline these + kvList = append(kvList, grpKVs...) + } else { + kvList = append(kvList, attr.Key, PseudoStruct(grpKVs)) + } + } else if attr.Key != "" { + kvList = append(kvList, attr.Key, attrVal.Any()) + } + + return kvList +} + +// levelFromSlog adjusts the level by the logger's verbosity and negates it. +// It ensures that the result is >= 0. This is necessary because the result is +// passed to a LogSink and that API did not historically document whether +// levels could be negative or what that meant. +// +// Some example usage: +// +// logrV0 := getMyLogger() +// logrV2 := logrV0.V(2) +// slogV2 := slog.New(logr.ToSlogHandler(logrV2)) +// slogV2.Debug("msg") // =~ logrV2.V(4) =~ logrV0.V(6) +// slogV2.Info("msg") // =~ logrV2.V(0) =~ logrV0.V(2) +// slogv2.Warn("msg") // =~ logrV2.V(-4) =~ logrV0.V(0) +func (l fnlogger) levelFromSlog(level slog.Level) int { + result := -level + if result < 0 { + result = 0 // because LogSink doesn't expect negative V levels + } + return int(result) +} diff --git a/pkg/logr/funcr/slogsink_test.go b/pkg/logr/funcr/slogsink_test.go new file mode 100644 index 00000000..04085eef --- /dev/null +++ b/pkg/logr/funcr/slogsink_test.go @@ -0,0 +1,234 @@ +//go:build go1.21 +// +build go1.21 + +/* +Copyright 2021 The logr Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package funcr + +import ( + "bytes" + "fmt" + testing "github.com/CodSpeedHQ/codspeed-go/testing/testing" + "log/slog" + "path/filepath" + "runtime" + + "github.com/CodSpeedHQ/codspeed-go/pkg/logr" + "github.com/CodSpeedHQ/codspeed-go/pkg/logr/internal/testhelp" +) + +func TestSlogSink(t *testing.T) { + testCases := []struct { + name string + withAttrs []any + withGroup string + args []any + expect string + }{{ + name: "just msg", + args: makeKV(), + expect: `{"logger":"","level":0,"msg":"msg"}`, + }, { + name: "primitives", + args: makeKV("int", 1, "str", "ABC", "bool", true), + expect: `{"logger":"","level":0,"msg":"msg","int":1,"str":"ABC","bool":true}`, + }, { + name: "with attrs", + withAttrs: makeKV("attrInt", 1, "attrStr", "ABC", "attrBool", true), + args: makeKV("int", 2), + expect: `{"logger":"","level":0,"msg":"msg","attrInt":1,"attrStr":"ABC","attrBool":true,"int":2}`, + }, { + name: "with group", + withGroup: "groupname", + args: makeKV("int", 1, "str", "ABC", "bool", true), + expect: `{"logger":"","level":0,"msg":"msg","groupname":{"int":1,"str":"ABC","bool":true}}`, + }, { + name: "with attrs and group", + withAttrs: makeKV("attrInt", 1, "attrStr", "ABC"), + withGroup: "groupname", + args: makeKV("int", 3, "bool", true), + expect: `{"logger":"","level":0,"msg":"msg","attrInt":1,"attrStr":"ABC","groupname":{"int":3,"bool":true}}`, + }} + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + capt := &capture{} + logger := logr.New(newSink(capt.Func, NewFormatterJSON(Options{}))) + slogger := slog.New(logr.ToSlogHandler(logger)) + if len(tc.withAttrs) > 0 { + slogger = slogger.With(tc.withAttrs...) + } + if tc.withGroup != "" { + slogger = slogger.WithGroup(tc.withGroup) + } + slogger.Info("msg", tc.args...) + if capt.log != tc.expect { + t.Errorf("\nexpected %q\n got %q", tc.expect, capt.log) + } + }) + } +} + +func TestSlogSinkGroups(t *testing.T) { + testCases := []struct { + name string + fn func(slogger *slog.Logger) + expect string + }{{ + name: "no group", + fn: func(slogger *slog.Logger) { + slogger. + Info("msg", "k", "v") + }, + expect: `{"logger":"","level":0,"msg":"msg","k":"v"}`, + }, { + name: "1 group with leaf args", + fn: func(slogger *slog.Logger) { + slogger. + WithGroup("g1"). + Info("msg", "k", "v") + }, + expect: `{"logger":"","level":0,"msg":"msg","g1":{"k":"v"}}`, + }, { + name: "1 group without leaf args", + fn: func(slogger *slog.Logger) { + slogger. + WithGroup("g1"). + Info("msg") + }, + expect: `{"logger":"","level":0,"msg":"msg"}`, + }, { + name: "1 group with value without leaf args", + fn: func(slogger *slog.Logger) { + slogger. + WithGroup("g1").With("k1", 1). + Info("msg") + }, + expect: `{"logger":"","level":0,"msg":"msg","g1":{"k1":1}}`, + }, { + name: "2 groups with values no leaf args", + fn: func(slogger *slog.Logger) { + slogger. + WithGroup("g1").With("k1", 1). + WithGroup("g2").With("k2", 2). + Info("msg") + }, + expect: `{"logger":"","level":0,"msg":"msg","g1":{"k1":1,"g2":{"k2":2}}}`, + }, { + name: "3 empty groups with no values or leaf args", + fn: func(slogger *slog.Logger) { + slogger. + WithGroup("g1"). + WithGroup("g2"). + WithGroup("g3"). + Info("msg") + }, + expect: `{"logger":"","level":0,"msg":"msg"}`, + }, { + name: "3 empty groups with no values but with leaf args", + fn: func(slogger *slog.Logger) { + slogger. + WithGroup("g1"). + WithGroup("g2"). + WithGroup("g3"). + Info("msg", "k", "v") + }, + expect: `{"logger":"","level":0,"msg":"msg","g1":{"g2":{"g3":{"k":"v"}}}}`, + }, { + name: "multiple groups with and without values", + fn: func(slogger *slog.Logger) { + slogger. + With("k0", 0). + WithGroup("g1"). + WithGroup("g2"). + WithGroup("g3").With("k3", 3). + WithGroup("g4"). + WithGroup("g5"). + WithGroup("g6").With("k6", 6). + WithGroup("g7"). + WithGroup("g8"). + WithGroup("g9"). + Info("msg") + }, + expect: `{"logger":"","level":0,"msg":"msg","k0":0,"g1":{"g2":{"g3":{"k3":3,"g4":{"g5":{"g6":{"k6":6}}}}}}}`, + }} + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + capt := &capture{} + logger := logr.New(newSink(capt.Func, NewFormatterJSON(Options{}))) + slogger := slog.New(logr.ToSlogHandler(logger)) + tc.fn(slogger) + if capt.log != tc.expect { + t.Errorf("\nexpected: `%s`\n got: `%s`", tc.expect, capt.log) + } + }) + } +} + +func TestSlogSinkWithCaller(t *testing.T) { + capt := &capture{} + logger := logr.New(newSink(capt.Func, NewFormatterJSON(Options{LogCaller: All}))) + slogger := slog.New(logr.ToSlogHandler(logger)) + slogger.Error("msg", "int", 1) + _, file, line, _ := runtime.Caller(0) + expect := fmt.Sprintf(`{"logger":"","caller":{"file":%q,"line":%d},"msg":"msg","error":null,"int":1}`, filepath.Base(file), line-1) + if capt.log != expect { + t.Errorf("\nexpected %q\n got %q", expect, capt.log) + } +} + +func TestRunSlogTests(t *testing.T) { + fn := func(buffer *bytes.Buffer) slog.Handler { + printfn := func(obj string) { + fmt.Fprintln(buffer, obj) + } + opts := Options{ + LogTimestamp: true, + Verbosity: 10, + RenderBuiltinsHook: func(kvList []any) []any { + mappedKVList := make([]any, len(kvList)) + for i := 0; i < len(kvList); i += 2 { + key := kvList[i] + switch key { + case "ts": + mappedKVList[i] = "time" + default: + mappedKVList[i] = key + } + mappedKVList[i+1] = kvList[i+1] + } + return mappedKVList + }, + } + logger := NewJSON(printfn, opts) + return logr.ToSlogHandler(logger) + } + exceptions := []string{ + "a Handler should ignore a zero Record.Time", // Time is generated by sink. + } + testhelp.RunSlogTests(t, fn, exceptions...) +} + +func TestLogrSlogConversion(t *testing.T) { + f := New(func(_, _ string) {}, Options{}) + f2 := logr.FromSlogHandler(logr.ToSlogHandler(f)) + if want, got := f, f2; got != want { + t.Helper() + t.Errorf("Expected %T %+v, got instead: %T %+v", want, want, got, got) + } +} diff --git a/pkg/logr/internal/testhelp/slog.go b/pkg/logr/internal/testhelp/slog.go new file mode 100644 index 00000000..160e1313 --- /dev/null +++ b/pkg/logr/internal/testhelp/slog.go @@ -0,0 +1,79 @@ +//go:build go1.21 +// +build go1.21 + +/* +Copyright 2023 The logr Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package testhelp holds helper functions for the testing of logr and built-in +// implementations. +package testhelp + +import ( + "bytes" + "encoding/json" + testing "github.com/CodSpeedHQ/codspeed-go/testing/testing" + slogtest "github.com/CodSpeedHQ/codspeed-go/testing/testing/slogtest" + "log/slog" + "strings" +) + +// RunSlogTests runs slogtest.TestHandler on a given slog.Handler, which is +// expected to emit JSON into the provided buffer. +func RunSlogTests(t *testing.T, createHandler func(buffer *bytes.Buffer) slog.Handler, exceptions ...string) { + var buffer bytes.Buffer + handler := createHandler(&buffer) + err := slogtest.TestHandler(handler, func() []map[string]any { + var ms []map[string]any + for _, line := range bytes.Split(buffer.Bytes(), []byte{'\n'}) { + if len(line) == 0 { + continue + } + var m map[string]any + if err := json.Unmarshal(line, &m); err != nil { + t.Errorf("%v: %q", err, string(line)) + } + ms = append(ms, m) + } + return ms + }) + + // Correlating failures with individual test cases is hard with the current API. + // See https://github.com/golang/go/issues/61758 + t.Logf("Output:\n%s", buffer.String()) + if err != nil { + if unwrappable, ok := err.(interface { + Unwrap() []error + }); ok { + for _, err := range unwrappable.Unwrap() { + if !containsOne(err.Error(), exceptions...) { + t.Errorf("Unexpected error: %v", err) + } + } + } else { + // Shouldn't be reached, errors from errors.Join can be split up. + t.Errorf("Unexpected errors:\n%v", err) + } + } +} + +func containsOne(hay string, needles ...string) bool { + for _, needle := range needles { + if strings.Contains(hay, needle) { + return true + } + } + return false +} diff --git a/pkg/logr/internal/testhelp/slog_test.go b/pkg/logr/internal/testhelp/slog_test.go new file mode 100644 index 00000000..44cd5bb4 --- /dev/null +++ b/pkg/logr/internal/testhelp/slog_test.go @@ -0,0 +1,33 @@ +//go:build go1.21 +// +build go1.21 + +/* +Copyright 2023 The logr Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package testhelp + +import ( + "bytes" + testing "github.com/CodSpeedHQ/codspeed-go/testing/testing" + "log/slog" +) + +func TestRunSlogTestsOnSlogSink(t *testing.T) { + // This proves that RunSlogTests works. + RunSlogTests(t, func(buffer *bytes.Buffer) slog.Handler { + return slog.NewJSONHandler(buffer, nil) + }) +} diff --git a/pkg/logr/logr.go b/pkg/logr/logr.go new file mode 100644 index 00000000..b4428e10 --- /dev/null +++ b/pkg/logr/logr.go @@ -0,0 +1,520 @@ +/* +Copyright 2019 The logr Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// This design derives from Dave Cheney's blog: +// http://dave.cheney.net/2015/11/05/lets-talk-about-logging + +// Package logr defines a general-purpose logging API and abstract interfaces +// to back that API. Packages in the Go ecosystem can depend on this package, +// while callers can implement logging with whatever backend is appropriate. +// +// # Usage +// +// Logging is done using a Logger instance. Logger is a concrete type with +// methods, which defers the actual logging to a LogSink interface. The main +// methods of Logger are Info() and Error(). Arguments to Info() and Error() +// are key/value pairs rather than printf-style formatted strings, emphasizing +// "structured logging". +// +// With Go's standard log package, we might write: +// +// log.Printf("setting target value %s", targetValue) +// +// With logr's structured logging, we'd write: +// +// logger.Info("setting target", "value", targetValue) +// +// Errors are much the same. Instead of: +// +// log.Printf("failed to open the pod bay door for user %s: %v", user, err) +// +// We'd write: +// +// logger.Error(err, "failed to open the pod bay door", "user", user) +// +// Info() and Error() are very similar, but they are separate methods so that +// LogSink implementations can choose to do things like attach additional +// information (such as stack traces) on calls to Error(). Error() messages are +// always logged, regardless of the current verbosity. If there is no error +// instance available, passing nil is valid. +// +// # Verbosity +// +// Often we want to log information only when the application in "verbose +// mode". To write log lines that are more verbose, Logger has a V() method. +// The higher the V-level of a log line, the less critical it is considered. +// Log-lines with V-levels that are not enabled (as per the LogSink) will not +// be written. Level V(0) is the default, and logger.V(0).Info() has the same +// meaning as logger.Info(). Negative V-levels have the same meaning as V(0). +// Error messages do not have a verbosity level and are always logged. +// +// Where we might have written: +// +// if flVerbose >= 2 { +// log.Printf("an unusual thing happened") +// } +// +// We can write: +// +// logger.V(2).Info("an unusual thing happened") +// +// # Logger Names +// +// Logger instances can have name strings so that all messages logged through +// that instance have additional context. For example, you might want to add +// a subsystem name: +// +// logger.WithName("compactor").Info("started", "time", time.Now()) +// +// The WithName() method returns a new Logger, which can be passed to +// constructors or other functions for further use. Repeated use of WithName() +// will accumulate name "segments". These name segments will be joined in some +// way by the LogSink implementation. It is strongly recommended that name +// segments contain simple identifiers (letters, digits, and hyphen), and do +// not contain characters that could muddle the log output or confuse the +// joining operation (e.g. whitespace, commas, periods, slashes, brackets, +// quotes, etc). +// +// # Saved Values +// +// Logger instances can store any number of key/value pairs, which will be +// logged alongside all messages logged through that instance. For example, +// you might want to create a Logger instance per managed object: +// +// With the standard log package, we might write: +// +// log.Printf("decided to set field foo to value %q for object %s/%s", +// targetValue, object.Namespace, object.Name) +// +// With logr we'd write: +// +// // Elsewhere: set up the logger to log the object name. +// obj.logger = mainLogger.WithValues( +// "name", obj.name, "namespace", obj.namespace) +// +// // later on... +// obj.logger.Info("setting foo", "value", targetValue) +// +// # Best Practices +// +// Logger has very few hard rules, with the goal that LogSink implementations +// might have a lot of freedom to differentiate. There are, however, some +// things to consider. +// +// The log message consists of a constant message attached to the log line. +// This should generally be a simple description of what's occurring, and should +// never be a format string. Variable information can then be attached using +// named values. +// +// Keys are arbitrary strings, but should generally be constant values. Values +// may be any Go value, but how the value is formatted is determined by the +// LogSink implementation. +// +// Logger instances are meant to be passed around by value. Code that receives +// such a value can call its methods without having to check whether the +// instance is ready for use. +// +// The zero logger (= Logger{}) is identical to Discard() and discards all log +// entries. Code that receives a Logger by value can simply call it, the methods +// will never crash. For cases where passing a logger is optional, a pointer to Logger +// should be used. +// +// # Key Naming Conventions +// +// Keys are not strictly required to conform to any specification or regex, but +// it is recommended that they: +// - be human-readable and meaningful (not auto-generated or simple ordinals) +// - be constant (not dependent on input data) +// - contain only printable characters +// - not contain whitespace or punctuation +// - use lower case for simple keys and lowerCamelCase for more complex ones +// +// These guidelines help ensure that log data is processed properly regardless +// of the log implementation. For example, log implementations will try to +// output JSON data or will store data for later database (e.g. SQL) queries. +// +// While users are generally free to use key names of their choice, it's +// generally best to avoid using the following keys, as they're frequently used +// by implementations: +// - "caller": the calling information (file/line) of a particular log line +// - "error": the underlying error value in the `Error` method +// - "level": the log level +// - "logger": the name of the associated logger +// - "msg": the log message +// - "stacktrace": the stack trace associated with a particular log line or +// error (often from the `Error` message) +// - "ts": the timestamp for a log line +// +// Implementations are encouraged to make use of these keys to represent the +// above concepts, when necessary (for example, in a pure-JSON output form, it +// would be necessary to represent at least message and timestamp as ordinary +// named values). +// +// # Break Glass +// +// Implementations may choose to give callers access to the underlying +// logging implementation. The recommended pattern for this is: +// +// // Underlier exposes access to the underlying logging implementation. +// // Since callers only have a logr.Logger, they have to know which +// // implementation is in use, so this interface is less of an abstraction +// // and more of way to test type conversion. +// type Underlier interface { +// GetUnderlying() +// } +// +// Logger grants access to the sink to enable type assertions like this: +// +// func DoSomethingWithImpl(log logr.Logger) { +// if underlier, ok := log.GetSink().(impl.Underlier); ok { +// implLogger := underlier.GetUnderlying() +// ... +// } +// } +// +// Custom `With*` functions can be implemented by copying the complete +// Logger struct and replacing the sink in the copy: +// +// // WithFooBar changes the foobar parameter in the log sink and returns a +// // new logger with that modified sink. It does nothing for loggers where +// // the sink doesn't support that parameter. +// func WithFoobar(log logr.Logger, foobar int) logr.Logger { +// if foobarLogSink, ok := log.GetSink().(FoobarSink); ok { +// log = log.WithSink(foobarLogSink.WithFooBar(foobar)) +// } +// return log +// } +// +// Don't use New to construct a new Logger with a LogSink retrieved from an +// existing Logger. Source code attribution might not work correctly and +// unexported fields in Logger get lost. +// +// Beware that the same LogSink instance may be shared by different logger +// instances. Calling functions that modify the LogSink will affect all of +// those. +package logr + +// New returns a new Logger instance. This is primarily used by libraries +// implementing LogSink, rather than end users. Passing a nil sink will create +// a Logger which discards all log lines. +func New(sink LogSink) Logger { + logger := Logger{} + logger.setSink(sink) + if sink != nil { + sink.Init(runtimeInfo) + } + return logger +} + +// setSink stores the sink and updates any related fields. It mutates the +// logger and thus is only safe to use for loggers that are not currently being +// used concurrently. +func (l *Logger) setSink(sink LogSink) { + l.sink = sink +} + +// GetSink returns the stored sink. +func (l Logger) GetSink() LogSink { + return l.sink +} + +// WithSink returns a copy of the logger with the new sink. +func (l Logger) WithSink(sink LogSink) Logger { + l.setSink(sink) + return l +} + +// Logger is an interface to an abstract logging implementation. This is a +// concrete type for performance reasons, but all the real work is passed on to +// a LogSink. Implementations of LogSink should provide their own constructors +// that return Logger, not LogSink. +// +// The underlying sink can be accessed through GetSink and be modified through +// WithSink. This enables the implementation of custom extensions (see "Break +// Glass" in the package documentation). Normally the sink should be used only +// indirectly. +type Logger struct { + sink LogSink + level int +} + +// Enabled tests whether this Logger is enabled. For example, commandline +// flags might be used to set the logging verbosity and disable some info logs. +func (l Logger) Enabled() bool { + // Some implementations of LogSink look at the caller in Enabled (e.g. + // different verbosity levels per package or file), but we only pass one + // CallDepth in (via Init). This means that all calls from Logger to the + // LogSink's Enabled, Info, and Error methods must have the same number of + // frames. In other words, Logger methods can't call other Logger methods + // which call these LogSink methods unless we do it the same in all paths. + return l.sink != nil && l.sink.Enabled(l.level) +} + +// Info logs a non-error message with the given key/value pairs as context. +// +// The msg argument should be used to add some constant description to the log +// line. The key/value pairs can then be used to add additional variable +// information. The key/value pairs must alternate string keys and arbitrary +// values. +func (l Logger) Info(msg string, keysAndValues ...any) { + if l.sink == nil { + return + } + if l.sink.Enabled(l.level) { // see comment in Enabled + if withHelper, ok := l.sink.(CallStackHelperLogSink); ok { + withHelper.GetCallStackHelper()() + } + l.sink.Info(l.level, msg, keysAndValues...) + } +} + +// Error logs an error, with the given message and key/value pairs as context. +// It functions similarly to Info, but may have unique behavior, and should be +// preferred for logging errors (see the package documentations for more +// information). The log message will always be emitted, regardless of +// verbosity level. +// +// The msg argument should be used to add context to any underlying error, +// while the err argument should be used to attach the actual error that +// triggered this log line, if present. The err parameter is optional +// and nil may be passed instead of an error instance. +func (l Logger) Error(err error, msg string, keysAndValues ...any) { + if l.sink == nil { + return + } + if withHelper, ok := l.sink.(CallStackHelperLogSink); ok { + withHelper.GetCallStackHelper()() + } + l.sink.Error(err, msg, keysAndValues...) +} + +// V returns a new Logger instance for a specific verbosity level, relative to +// this Logger. In other words, V-levels are additive. A higher verbosity +// level means a log message is less important. Negative V-levels are treated +// as 0. +func (l Logger) V(level int) Logger { + if l.sink == nil { + return l + } + if level < 0 { + level = 0 + } + l.level += level + return l +} + +// GetV returns the verbosity level of the logger. If the logger's LogSink is +// nil as in the Discard logger, this will always return 0. +func (l Logger) GetV() int { + // 0 if l.sink nil because of the if check in V above. + return l.level +} + +// WithValues returns a new Logger instance with additional key/value pairs. +// See Info for documentation on how key/value pairs work. +func (l Logger) WithValues(keysAndValues ...any) Logger { + if l.sink == nil { + return l + } + l.setSink(l.sink.WithValues(keysAndValues...)) + return l +} + +// WithName returns a new Logger instance with the specified name element added +// to the Logger's name. Successive calls with WithName append additional +// suffixes to the Logger's name. It's strongly recommended that name segments +// contain only letters, digits, and hyphens (see the package documentation for +// more information). +func (l Logger) WithName(name string) Logger { + if l.sink == nil { + return l + } + l.setSink(l.sink.WithName(name)) + return l +} + +// WithCallDepth returns a Logger instance that offsets the call stack by the +// specified number of frames when logging call site information, if possible. +// This is useful for users who have helper functions between the "real" call +// site and the actual calls to Logger methods. If depth is 0 the attribution +// should be to the direct caller of this function. If depth is 1 the +// attribution should skip 1 call frame, and so on. Successive calls to this +// are additive. +// +// If the underlying log implementation supports a WithCallDepth(int) method, +// it will be called and the result returned. If the implementation does not +// support CallDepthLogSink, the original Logger will be returned. +// +// To skip one level, WithCallStackHelper() should be used instead of +// WithCallDepth(1) because it works with implementions that support the +// CallDepthLogSink and/or CallStackHelperLogSink interfaces. +func (l Logger) WithCallDepth(depth int) Logger { + if l.sink == nil { + return l + } + if withCallDepth, ok := l.sink.(CallDepthLogSink); ok { + l.setSink(withCallDepth.WithCallDepth(depth)) + } + return l +} + +// WithCallStackHelper returns a new Logger instance that skips the direct +// caller when logging call site information, if possible. This is useful for +// users who have helper functions between the "real" call site and the actual +// calls to Logger methods and want to support loggers which depend on marking +// each individual helper function, like loggers based on testing.T. +// +// In addition to using that new logger instance, callers also must call the +// returned function. +// +// If the underlying log implementation supports a WithCallDepth(int) method, +// WithCallDepth(1) will be called to produce a new logger. If it supports a +// WithCallStackHelper() method, that will be also called. If the +// implementation does not support either of these, the original Logger will be +// returned. +func (l Logger) WithCallStackHelper() (func(), Logger) { + if l.sink == nil { + return func() {}, l + } + var helper func() + if withCallDepth, ok := l.sink.(CallDepthLogSink); ok { + l.setSink(withCallDepth.WithCallDepth(1)) + } + if withHelper, ok := l.sink.(CallStackHelperLogSink); ok { + helper = withHelper.GetCallStackHelper() + } else { + helper = func() {} + } + return helper, l +} + +// IsZero returns true if this logger is an uninitialized zero value +func (l Logger) IsZero() bool { + return l.sink == nil +} + +// RuntimeInfo holds information that the logr "core" library knows which +// LogSinks might want to know. +type RuntimeInfo struct { + // CallDepth is the number of call frames the logr library adds between the + // end-user and the LogSink. LogSink implementations which choose to print + // the original logging site (e.g. file & line) should climb this many + // additional frames to find it. + CallDepth int +} + +// runtimeInfo is a static global. It must not be changed at run time. +var runtimeInfo = RuntimeInfo{ + CallDepth: 1, +} + +// LogSink represents a logging implementation. End-users will generally not +// interact with this type. +type LogSink interface { + // Init receives optional information about the logr library for LogSink + // implementations that need it. + Init(info RuntimeInfo) + + // Enabled tests whether this LogSink is enabled at the specified V-level. + // For example, commandline flags might be used to set the logging + // verbosity and disable some info logs. + Enabled(level int) bool + + // Info logs a non-error message with the given key/value pairs as context. + // The level argument is provided for optional logging. This method will + // only be called when Enabled(level) is true. See Logger.Info for more + // details. + Info(level int, msg string, keysAndValues ...any) + + // Error logs an error, with the given message and key/value pairs as + // context. See Logger.Error for more details. + Error(err error, msg string, keysAndValues ...any) + + // WithValues returns a new LogSink with additional key/value pairs. See + // Logger.WithValues for more details. + WithValues(keysAndValues ...any) LogSink + + // WithName returns a new LogSink with the specified name appended. See + // Logger.WithName for more details. + WithName(name string) LogSink +} + +// CallDepthLogSink represents a LogSink that knows how to climb the call stack +// to identify the original call site and can offset the depth by a specified +// number of frames. This is useful for users who have helper functions +// between the "real" call site and the actual calls to Logger methods. +// Implementations that log information about the call site (such as file, +// function, or line) would otherwise log information about the intermediate +// helper functions. +// +// This is an optional interface and implementations are not required to +// support it. +type CallDepthLogSink interface { + // WithCallDepth returns a LogSink that will offset the call + // stack by the specified number of frames when logging call + // site information. + // + // If depth is 0, the LogSink should skip exactly the number + // of call frames defined in RuntimeInfo.CallDepth when Info + // or Error are called, i.e. the attribution should be to the + // direct caller of Logger.Info or Logger.Error. + // + // If depth is 1 the attribution should skip 1 call frame, and so on. + // Successive calls to this are additive. + WithCallDepth(depth int) LogSink +} + +// CallStackHelperLogSink represents a LogSink that knows how to climb +// the call stack to identify the original call site and can skip +// intermediate helper functions if they mark themselves as +// helper. Go's testing package uses that approach. +// +// This is useful for users who have helper functions between the +// "real" call site and the actual calls to Logger methods. +// Implementations that log information about the call site (such as +// file, function, or line) would otherwise log information about the +// intermediate helper functions. +// +// This is an optional interface and implementations are not required +// to support it. Implementations that choose to support this must not +// simply implement it as WithCallDepth(1), because +// Logger.WithCallStackHelper will call both methods if they are +// present. This should only be implemented for LogSinks that actually +// need it, as with testing.T. +type CallStackHelperLogSink interface { + // GetCallStackHelper returns a function that must be called + // to mark the direct caller as helper function when logging + // call site information. + GetCallStackHelper() func() +} + +// Marshaler is an optional interface that logged values may choose to +// implement. Loggers with structured output, such as JSON, should +// log the object return by the MarshalLog method instead of the +// original value. +type Marshaler interface { + // MarshalLog can be used to: + // - ensure that structs are not logged as strings when the original + // value has a String method: return a different type without a + // String method + // - select which fields of a complex type should get logged: + // return a simpler struct with fewer fields + // - log unexported fields: return a different struct + // with exported fields + // + // It may return any value of any type. + MarshalLog() any +} diff --git a/pkg/logr/logr_test.go b/pkg/logr/logr_test.go new file mode 100644 index 00000000..d4839ff5 --- /dev/null +++ b/pkg/logr/logr_test.go @@ -0,0 +1,416 @@ +/* +Copyright 2021 The logr Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package logr + +import ( + "errors" + "fmt" + testing "github.com/CodSpeedHQ/codspeed-go/testing/testing" + "reflect" + "runtime" +) + +func TestNew(t *testing.T) { + calledInit := 0 + + sink := &testLogSink{} + sink.fnInit = func(ri RuntimeInfo) { + if ri.CallDepth != 1 { + t.Errorf("expected runtimeInfo.CallDepth = 1, got %d", ri.CallDepth) + } + calledInit++ + } + logger := New(sink) + + if logger.sink == nil { + t.Errorf("expected sink to be set, got %v", logger.sink) + } + if calledInit != 1 { + t.Errorf("expected sink.Init() to be called once, got %d", calledInit) + } + if _, ok := logger.sink.(CallDepthLogSink); ok { + t.Errorf("expected conversion to CallDepthLogSink to fail") + } +} + +func TestNewCachesCallDepthInterface(t *testing.T) { + sink := &testCallDepthLogSink{} + logger := New(sink) + + if _, ok := logger.sink.(CallDepthLogSink); !ok { + t.Errorf("expected conversion to CallDepthLogSink to succeed") + } +} + +func TestEnabled(t *testing.T) { + calledEnabled := 0 + + sink := &testLogSink{} + sink.fnEnabled = func(_ int) bool { + calledEnabled++ + return true + } + logger := New(sink) + + if en := logger.Enabled(); en != true { + t.Errorf("expected true") + } + if calledEnabled != 1 { + t.Errorf("expected sink.Enabled() to be called once, got %d", calledEnabled) + } +} + +func TestError(t *testing.T) { + calledError := 0 + errInput := fmt.Errorf("error") + msgInput := "msg" + kvInput := []any{0, 1, 2} + + sink := &testLogSink{} + sink.fnError = func(err error, msg string, kv ...any) { + calledError++ + if err != errInput { + t.Errorf("unexpected err input, got %v", err) + } + if msg != msgInput { + t.Errorf("unexpected msg input, got %q", msg) + } + if !reflect.DeepEqual(kv, kvInput) { + t.Errorf("unexpected kv input, got %v", kv) + } + } + logger := New(sink) + + logger.Error(errInput, msgInput, kvInput...) + if calledError != 1 { + t.Errorf("expected sink.Error() to be called once, got %d", calledError) + } +} + +func TestV(t *testing.T) { + for name, logger := range map[string]Logger{ + "testLogSink": New(&testLogSink{}), + "Discard": Discard(), + "Zero": {}, + } { + t.Run(name, func(t *testing.T) { + adjust := func(level int) int { + if logger.GetSink() == nil { + // The Discard and the zero Logger short-cut the V call and don't + // change the verbosity level. + return 0 + } + return level + } + inputs := []struct { + name string + fn func() Logger + exp int + }{{ + name: "V(0)", + fn: func() Logger { return logger.V(0) }, + exp: 0, + }, { + name: "V(93)", + fn: func() Logger { return logger.V(93) }, + exp: adjust(93), + }, { + name: "V(70).V(6)", + fn: func() Logger { return logger.V(70).V(6) }, + exp: adjust(76), + }, { + name: "V(-1)", + fn: func() Logger { return logger.V(-1) }, + exp: 0, + }, { + name: "V(1).V(-1)", + fn: func() Logger { return logger.V(1).V(-1) }, + exp: adjust(1), + }} + for _, in := range inputs { + t.Run(in.name, func(t *testing.T) { + if want, got := in.exp, in.fn().GetV(); got != want { + t.Errorf("expected %d, got %d", want, got) + } + }) + } + }) + } +} + +func TestInfo(t *testing.T) { + calledEnabled := 0 + calledInfo := 0 + lvlInput := 0 + msgInput := "msg" + kvInput := []any{0, 1, 2} + + sink := &testLogSink{} + sink.fnEnabled = func(lvl int) bool { + calledEnabled++ + return lvl < 100 + } + sink.fnInfo = func(lvl int, msg string, kv ...any) { + calledInfo++ + if lvl != lvlInput { + t.Errorf("unexpected lvl input, got %v", lvl) + } + if msg != msgInput { + t.Errorf("unexpected msg input, got %q", msg) + } + if !reflect.DeepEqual(kv, kvInput) { + t.Errorf("unexpected kv input, got %v", kv) + } + } + logger := New(sink) + + calledEnabled = 0 + calledInfo = 0 + lvlInput = 0 + logger.Info(msgInput, kvInput...) + if calledEnabled != 1 { + t.Errorf("expected sink.Enabled() to be called once, got %d", calledEnabled) + } + if calledInfo != 1 { + t.Errorf("expected sink.Info() to be called once, got %d", calledInfo) + } + + calledEnabled = 0 + calledInfo = 0 + lvlInput = 0 + logger.V(0).Info(msgInput, kvInput...) + if calledEnabled != 1 { + t.Errorf("expected sink.Enabled() to be called once, got %d", calledEnabled) + } + if calledInfo != 1 { + t.Errorf("expected sink.Info() to be called once, got %d", calledInfo) + } + + calledEnabled = 0 + calledInfo = 0 + lvlInput = 93 + logger.V(93).Info(msgInput, kvInput...) + if calledEnabled != 1 { + t.Errorf("expected sink.Enabled() to be called once, got %d", calledEnabled) + } + if calledInfo != 1 { + t.Errorf("expected sink.Info() to be called once, got %d", calledInfo) + } + + calledEnabled = 0 + calledInfo = 0 + lvlInput = 100 + logger.V(100).Info(msgInput, kvInput...) + if calledEnabled != 1 { + t.Errorf("expected sink.Enabled() to be called once, got %d", calledEnabled) + } + if calledInfo != 0 { + t.Errorf("expected sink.Info() to not be called, got %d", calledInfo) + } +} + +func TestWithValues(t *testing.T) { + calledWithValues := 0 + kvInput := []any{"zero", 0, "one", 1, "two", 2} + + sink := &testLogSink{} + sink.fnWithValues = func(kv ...any) { + calledWithValues++ + if !reflect.DeepEqual(kv, kvInput) { + t.Errorf("unexpected kv input, got %v", kv) + } + } + logger := New(sink) + + out := logger.WithValues(kvInput...) + if calledWithValues != 1 { + t.Errorf("expected sink.WithValues() to be called once, got %d", calledWithValues) + } + if p, _ := out.sink.(*testLogSink); p == sink { + t.Errorf("expected output to be different from input, got in=%p, out=%p", sink, p) + } +} + +func TestWithName(t *testing.T) { + calledWithName := 0 + nameInput := "name" + + sink := &testLogSink{} + sink.fnWithName = func(name string) { + calledWithName++ + if name != nameInput { + t.Errorf("unexpected name input, got %q", name) + } + } + logger := New(sink) + + out := logger.WithName(nameInput) + if calledWithName != 1 { + t.Errorf("expected sink.WithName() to be called once, got %d", calledWithName) + } + if p, _ := out.sink.(*testLogSink); p == sink { + t.Errorf("expected output to be different from input, got in=%p, out=%p", sink, p) + } +} + +func TestWithCallDepthNotImplemented(t *testing.T) { + depthInput := 7 + + sink := &testLogSink{} + logger := New(sink) + + out := logger.WithCallDepth(depthInput) + if p, _ := out.sink.(*testLogSink); p != sink { + t.Errorf("expected output to be the same as input, got in=%p, out=%p", sink, p) + } +} + +func TestWithCallDepthImplemented(t *testing.T) { + calledWithCallDepth := 0 + depthInput := 7 + + sink := &testCallDepthLogSink{} + sink.fnWithCallDepth = func(depth int) { + calledWithCallDepth++ + if depth != depthInput { + t.Errorf("unexpected depth input, got %d", depth) + } + } + logger := New(sink) + + out := logger.WithCallDepth(depthInput) + if calledWithCallDepth != 1 { + t.Errorf("expected sink.WithCallDepth() to be called once, got %d", calledWithCallDepth) + } + p, _ := out.sink.(*testCallDepthLogSink) + if p == sink { + t.Errorf("expected output to be different from input, got in=%p, out=%p", sink, p) + } + if p.callDepth != depthInput { + t.Errorf("expected sink to have call depth %d, got %d", depthInput, p.callDepth) + } +} + +func TestWithCallDepthIncremental(t *testing.T) { + calledWithCallDepth := 0 + depthInput := 7 + + sink := &testCallDepthLogSink{} + sink.fnWithCallDepth = func(depth int) { + calledWithCallDepth++ + if depth != 1 { + t.Errorf("unexpected depth input, got %d", depth) + } + } + logger := New(sink) + + out := logger + for i := 0; i < depthInput; i++ { + out = out.WithCallDepth(1) + } + if calledWithCallDepth != depthInput { + t.Errorf("expected sink.WithCallDepth() to be called %d times, got %d", depthInput, calledWithCallDepth) + } + p, _ := out.sink.(*testCallDepthLogSink) + if p == sink { + t.Errorf("expected output to be different from input, got in=%p, out=%p", sink, p) + } + if p.callDepth != depthInput { + t.Errorf("expected sink to have call depth %d, got %d", depthInput, p.callDepth) + } +} + +func TestIsZero(t *testing.T) { + var l Logger + if !l.IsZero() { + t.Errorf("expected IsZero") + } + sink := &testLogSink{} + l = New(sink) + if l.IsZero() { + t.Errorf("expected not IsZero") + } + // Discard is the same as a nil sink. + l = Discard() + if !l.IsZero() { + t.Errorf("expected IsZero") + } +} + +func TestZeroValue(t *testing.T) { + // Make sure that the zero value is useful and equivalent to a Discard logger. + var l Logger + if l.Enabled() { + t.Errorf("expected not Enabled") + } + if !l.IsZero() { + t.Errorf("expected IsZero") + } + // Make sure that none of these methods cause a crash + l.Info("foo") + l.Error(errors.New("bar"), "some error") + if l.GetSink() != nil { + t.Errorf("expected nil from GetSink") + } + l2 := l.WithName("some-name").V(2).WithValues("foo", 1).WithCallDepth(1) + l2.Info("foo") + l2.Error(errors.New("bar"), "some error") + _, _ = l.WithCallStackHelper() +} + +func TestCallDepthConsistent(t *testing.T) { + sink := &testLogSink{} + + depth := 0 + expect := "github.com/CodSpeedHQ/codspeed-go/pkg/logr.TestCallDepthConsistent" + sink.fnInit = func(ri RuntimeInfo) { + depth = ri.CallDepth + 1 // 1 for these function pointers + if caller := getCaller(depth); caller != expect { + t.Errorf("identified wrong caller %q", caller) + } + + } + sink.fnEnabled = func(_ int) bool { + if caller := getCaller(depth); caller != expect { + t.Errorf("identified wrong caller %q", caller) + } + return true + } + sink.fnError = func(_ error, _ string, _ ...any) { + if caller := getCaller(depth); caller != expect { + t.Errorf("identified wrong caller %q", caller) + } + } + l := New(sink) + + l.Enabled() + l.Info("msg") + l.Error(nil, "msg") +} + +func getCaller(depth int) string { + // +1 for this frame, +1 for Info/Error/Enabled. + pc, _, _, ok := runtime.Caller(depth + 2) + if !ok { + return "" + } + fp := runtime.FuncForPC(pc) + if fp == nil { + return "" + } + return fp.Name() +} diff --git a/pkg/logr/sloghandler.go b/pkg/logr/sloghandler.go new file mode 100644 index 00000000..82d1ba49 --- /dev/null +++ b/pkg/logr/sloghandler.go @@ -0,0 +1,192 @@ +//go:build go1.21 +// +build go1.21 + +/* +Copyright 2023 The logr Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package logr + +import ( + "context" + "log/slog" +) + +type slogHandler struct { + // May be nil, in which case all logs get discarded. + sink LogSink + // Non-nil if sink is non-nil and implements SlogSink. + slogSink SlogSink + + // groupPrefix collects values from WithGroup calls. It gets added as + // prefix to value keys when handling a log record. + groupPrefix string + + // levelBias can be set when constructing the handler to influence the + // slog.Level of log records. A positive levelBias reduces the + // slog.Level value. slog has no API to influence this value after the + // handler got created, so it can only be set indirectly through + // Logger.V. + levelBias slog.Level +} + +var _ slog.Handler = &slogHandler{} + +// groupSeparator is used to concatenate WithGroup names and attribute keys. +const groupSeparator = "." + +// GetLevel is used for black box unit testing. +func (l *slogHandler) GetLevel() slog.Level { + return l.levelBias +} + +func (l *slogHandler) Enabled(_ context.Context, level slog.Level) bool { + return l.sink != nil && (level >= slog.LevelError || l.sink.Enabled(l.levelFromSlog(level))) +} + +func (l *slogHandler) Handle(ctx context.Context, record slog.Record) error { + if l.slogSink != nil { + // Only adjust verbosity level of log entries < slog.LevelError. + if record.Level < slog.LevelError { + record.Level -= l.levelBias + } + return l.slogSink.Handle(ctx, record) + } + + // No need to check for nil sink here because Handle will only be called + // when Enabled returned true. + + kvList := make([]any, 0, 2*record.NumAttrs()) + record.Attrs(func(attr slog.Attr) bool { + kvList = attrToKVs(attr, l.groupPrefix, kvList) + return true + }) + if record.Level >= slog.LevelError { + l.sinkWithCallDepth().Error(nil, record.Message, kvList...) + } else { + level := l.levelFromSlog(record.Level) + l.sinkWithCallDepth().Info(level, record.Message, kvList...) + } + return nil +} + +// sinkWithCallDepth adjusts the stack unwinding so that when Error or Info +// are called by Handle, code in slog gets skipped. +// +// This offset currently (Go 1.21.0) works for calls through +// slog.New(ToSlogHandler(...)). There's no guarantee that the call +// chain won't change. Wrapping the handler will also break unwinding. It's +// still better than not adjusting at all.... +// +// This cannot be done when constructing the handler because FromSlogHandler needs +// access to the original sink without this adjustment. A second copy would +// work, but then WithAttrs would have to be called for both of them. +func (l *slogHandler) sinkWithCallDepth() LogSink { + if sink, ok := l.sink.(CallDepthLogSink); ok { + return sink.WithCallDepth(2) + } + return l.sink +} + +func (l *slogHandler) WithAttrs(attrs []slog.Attr) slog.Handler { + if l.sink == nil || len(attrs) == 0 { + return l + } + + clone := *l + if l.slogSink != nil { + clone.slogSink = l.slogSink.WithAttrs(attrs) + clone.sink = clone.slogSink + } else { + kvList := make([]any, 0, 2*len(attrs)) + for _, attr := range attrs { + kvList = attrToKVs(attr, l.groupPrefix, kvList) + } + clone.sink = l.sink.WithValues(kvList...) + } + return &clone +} + +func (l *slogHandler) WithGroup(name string) slog.Handler { + if l.sink == nil { + return l + } + if name == "" { + // slog says to inline empty groups + return l + } + clone := *l + if l.slogSink != nil { + clone.slogSink = l.slogSink.WithGroup(name) + clone.sink = clone.slogSink + } else { + clone.groupPrefix = addPrefix(clone.groupPrefix, name) + } + return &clone +} + +// attrToKVs appends a slog.Attr to a logr-style kvList. It handle slog Groups +// and other details of slog. +func attrToKVs(attr slog.Attr, groupPrefix string, kvList []any) []any { + attrVal := attr.Value.Resolve() + if attrVal.Kind() == slog.KindGroup { + groupVal := attrVal.Group() + grpKVs := make([]any, 0, 2*len(groupVal)) + prefix := groupPrefix + if attr.Key != "" { + prefix = addPrefix(groupPrefix, attr.Key) + } + for _, attr := range groupVal { + grpKVs = attrToKVs(attr, prefix, grpKVs) + } + kvList = append(kvList, grpKVs...) + } else if attr.Key != "" { + kvList = append(kvList, addPrefix(groupPrefix, attr.Key), attrVal.Any()) + } + + return kvList +} + +func addPrefix(prefix, name string) string { + if prefix == "" { + return name + } + if name == "" { + return prefix + } + return prefix + groupSeparator + name +} + +// levelFromSlog adjusts the level by the logger's verbosity and negates it. +// It ensures that the result is >= 0. This is necessary because the result is +// passed to a LogSink and that API did not historically document whether +// levels could be negative or what that meant. +// +// Some example usage: +// +// logrV0 := getMyLogger() +// logrV2 := logrV0.V(2) +// slogV2 := slog.New(logr.ToSlogHandler(logrV2)) +// slogV2.Debug("msg") // =~ logrV2.V(4) =~ logrV0.V(6) +// slogV2.Info("msg") // =~ logrV2.V(0) =~ logrV0.V(2) +// slogv2.Warn("msg") // =~ logrV2.V(-4) =~ logrV0.V(0) +func (l *slogHandler) levelFromSlog(level slog.Level) int { + result := -level + result += l.levelBias // in case the original Logger had a V level + if result < 0 { + result = 0 // because LogSink doesn't expect negative V levels + } + return int(result) +} diff --git a/pkg/logr/slogr.go b/pkg/logr/slogr.go new file mode 100644 index 00000000..28a83d02 --- /dev/null +++ b/pkg/logr/slogr.go @@ -0,0 +1,100 @@ +//go:build go1.21 +// +build go1.21 + +/* +Copyright 2023 The logr Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package logr + +import ( + "context" + "log/slog" +) + +// FromSlogHandler returns a Logger which writes to the slog.Handler. +// +// The logr verbosity level is mapped to slog levels such that V(0) becomes +// slog.LevelInfo and V(4) becomes slog.LevelDebug. +func FromSlogHandler(handler slog.Handler) Logger { + if handler, ok := handler.(*slogHandler); ok { + if handler.sink == nil { + return Discard() + } + return New(handler.sink).V(int(handler.levelBias)) + } + return New(&slogSink{handler: handler}) +} + +// ToSlogHandler returns a slog.Handler which writes to the same sink as the Logger. +// +// The returned logger writes all records with level >= slog.LevelError as +// error log entries with LogSink.Error, regardless of the verbosity level of +// the Logger: +// +// logger := +// slog.New(ToSlogHandler(logger.V(10))).Error(...) -> logSink.Error(...) +// +// The level of all other records gets reduced by the verbosity +// level of the Logger and the result is negated. If it happens +// to be negative, then it gets replaced by zero because a LogSink +// is not expected to handled negative levels: +// +// slog.New(ToSlogHandler(logger)).Debug(...) -> logger.GetSink().Info(level=4, ...) +// slog.New(ToSlogHandler(logger)).Warning(...) -> logger.GetSink().Info(level=0, ...) +// slog.New(ToSlogHandler(logger)).Info(...) -> logger.GetSink().Info(level=0, ...) +// slog.New(ToSlogHandler(logger.V(4))).Info(...) -> logger.GetSink().Info(level=4, ...) +func ToSlogHandler(logger Logger) slog.Handler { + if sink, ok := logger.GetSink().(*slogSink); ok && logger.GetV() == 0 { + return sink.handler + } + + handler := &slogHandler{sink: logger.GetSink(), levelBias: slog.Level(logger.GetV())} + if slogSink, ok := handler.sink.(SlogSink); ok { + handler.slogSink = slogSink + } + return handler +} + +// SlogSink is an optional interface that a LogSink can implement to support +// logging through the slog.Logger or slog.Handler APIs better. It then should +// also support special slog values like slog.Group. When used as a +// slog.Handler, the advantages are: +// +// - stack unwinding gets avoided in favor of logging the pre-recorded PC, +// as intended by slog +// - proper grouping of key/value pairs via WithGroup +// - verbosity levels > slog.LevelInfo can be recorded +// - less overhead +// +// Both APIs (Logger and slog.Logger/Handler) then are supported equally +// well. Developers can pick whatever API suits them better and/or mix +// packages which use either API in the same binary with a common logging +// implementation. +// +// This interface is necessary because the type implementing the LogSink +// interface cannot also implement the slog.Handler interface due to the +// different prototype of the common Enabled method. +// +// An implementation could support both interfaces in two different types, but then +// additional interfaces would be needed to convert between those types in FromSlogHandler +// and ToSlogHandler. +type SlogSink interface { + LogSink + + Handle(ctx context.Context, record slog.Record) error + WithAttrs(attrs []slog.Attr) SlogSink + WithGroup(name string) SlogSink +} diff --git a/pkg/logr/slogr/slogr.go b/pkg/logr/slogr/slogr.go new file mode 100644 index 00000000..207a3036 --- /dev/null +++ b/pkg/logr/slogr/slogr.go @@ -0,0 +1,61 @@ +//go:build go1.21 +// +build go1.21 + +/* +Copyright 2023 The logr Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package slogr enables usage of a slog.Handler with logr.Logger as front-end +// API and of a logr.LogSink through the slog.Handler and thus slog.Logger +// APIs. +// +// See the README in the top-level [./logr] package for a discussion of +// interoperability. +// +// Deprecated: use the main logr package instead. +package slogr + +import ( + "log/slog" + + "github.com/CodSpeedHQ/codspeed-go/pkg/logr" +) + +// NewLogr returns a logr.Logger which writes to the slog.Handler. +// +// Deprecated: use [logr.FromSlogHandler] instead. +func NewLogr(handler slog.Handler) logr.Logger { + return logr.FromSlogHandler(handler) +} + +// NewSlogHandler returns a slog.Handler which writes to the same sink as the logr.Logger. +// +// Deprecated: use [logr.ToSlogHandler] instead. +func NewSlogHandler(logger logr.Logger) slog.Handler { + return logr.ToSlogHandler(logger) +} + +// ToSlogHandler returns a slog.Handler which writes to the same sink as the logr.Logger. +// +// Deprecated: use [logr.ToSlogHandler] instead. +func ToSlogHandler(logger logr.Logger) slog.Handler { + return logr.ToSlogHandler(logger) +} + +// SlogSink is an optional interface that a LogSink can implement to support +// logging through the slog.Logger or slog.Handler APIs better. +// +// Deprecated: use [logr.SlogSink] instead. +type SlogSink = logr.SlogSink diff --git a/pkg/logr/slogr_test.go b/pkg/logr/slogr_test.go new file mode 100644 index 00000000..034a5e4c --- /dev/null +++ b/pkg/logr/slogr_test.go @@ -0,0 +1,199 @@ +//go:build go1.21 +// +build go1.21 + +/* +Copyright 2023 The logr Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package logr + +import ( + "bytes" + "fmt" + testing "github.com/CodSpeedHQ/codspeed-go/testing/testing" + "io" + "log/slog" + "os" + "path" + "runtime" + "strings" + + "github.com/CodSpeedHQ/codspeed-go/pkg/logr/internal/testhelp" +) + +func TestToSlogHandler(t *testing.T) { + t.Run("from simple Logger", func(t *testing.T) { + logger := New(&testLogSink{}) + handler := ToSlogHandler(logger) + if _, ok := handler.(*slogHandler); !ok { + t.Errorf("expected type *slogHandler, got %T", handler) + } + }) + + t.Run("from slog-enabled Logger", func(t *testing.T) { + logger := New(&testSlogSink{}) + handler := ToSlogHandler(logger) + if _, ok := handler.(*slogHandler); !ok { + t.Errorf("expected type *slogHandler, got %T", handler) + } + }) + + t.Run("from slogSink Logger", func(t *testing.T) { + logger := New(&slogSink{handler: slog.NewJSONHandler(os.Stderr, nil)}) + handler := ToSlogHandler(logger) + if _, ok := handler.(*slog.JSONHandler); !ok { + t.Errorf("expected type *slog.JSONHandler, got %T", handler) + } + }) +} + +func TestFromSlogHandler(t *testing.T) { + t.Run("from slog Handler", func(t *testing.T) { + handler := slog.NewJSONHandler(os.Stderr, nil) + logger := FromSlogHandler(handler) + if _, ok := logger.sink.(*slogSink); !ok { + t.Errorf("expected type *slogSink, got %T", logger.sink) + } + }) + + t.Run("from simple slogHandler Handler", func(t *testing.T) { + handler := &slogHandler{sink: &testLogSink{}} + logger := FromSlogHandler(handler) + if _, ok := logger.sink.(*testLogSink); !ok { + t.Errorf("expected type *testSlogSink, got %T", logger.sink) + } + }) + + t.Run("from discard slogHandler Handler", func(t *testing.T) { + handler := &slogHandler{} + logger := FromSlogHandler(handler) + if logger != Discard() { + t.Errorf("expected type *testSlogSink, got %T", logger.sink) + } + }) +} + +var debugWithoutTime = &slog.HandlerOptions{ + ReplaceAttr: func(_ []string, a slog.Attr) slog.Attr { + if a.Key == "time" { + return slog.Attr{} + } + return a + }, + Level: slog.LevelDebug, +} + +func TestWithCallDepth(t *testing.T) { + debugWithCaller := *debugWithoutTime + debugWithCaller.AddSource = true + var buffer bytes.Buffer + logger := FromSlogHandler(slog.NewTextHandler(&buffer, &debugWithCaller)) + + logHelper := func(logger Logger) { + logger.WithCallDepth(1).Info("hello") + } + + logHelper(logger) + _, file, line, _ := runtime.Caller(0) + expectedSource := fmt.Sprintf("%s:%d", path.Base(file), line-1) + actual := buffer.String() + if !strings.Contains(actual, expectedSource) { + t.Errorf("expected log entry with %s as caller source code location, got instead:\n%s", expectedSource, actual) + } +} + +func TestRunSlogTestsOnNaiveSlogHandler(t *testing.T) { + // This proves that slogHandler passes slog's own tests when given a + // LogSink which does not implement SlogSink. + exceptions := []string{ + // logr sinks handle time themselves + "a Handler should ignore a zero Record.Time", + // slogHandler does not do groups "properly", so these all fail with + // "missing group". It's looking for `"G":{"a":"b"}` and getting + // `"G.a": "b"`. + // + // NOTE: These make a weird coupling to Go versions. Newer Go versions + // don't need some of these exceptions, but older ones do. It's unclear + // if that is because something changed in slog or if the test was + // removed. + "a Handler should handle Group attributes", + "a Handler should handle the WithGroup method", + "a Handler should handle multiple WithGroup and WithAttr calls", + "a Handler should not output groups for an empty Record", + "a Handler should not output groups if there are no attributes", + "a Handler should not output nested groups if there are no attributes", + "a Handler should call Resolve on attribute values in groups", + "a Handler should call Resolve on attribute values in groups from WithAttrs", + } + testhelp.RunSlogTests(t, func(buffer *bytes.Buffer) slog.Handler { + // We want a known-good Logger that emits JSON but is not a slogHandler + // or SlogSink (since those get special treatment). We can trust that + // the slog JSONHandler works. + handler := slog.NewJSONHandler(buffer, nil) + sink := &passthruLogSink{handler: handler} // passthruLogSink does not implement SlogSink. + logger := New(sink) + return ToSlogHandler(logger) + }, exceptions...) +} + +func TestRunSlogTestsOnEnlightenedSlogHandler(t *testing.T) { + // This proves that slogHandler passes slog's own tests when given a + // LogSink which implements SlogSink. + exceptions := []string{} + testhelp.RunSlogTests(t, func(buffer *bytes.Buffer) slog.Handler { + // We want a known-good Logger that emits JSON and implements SlogSink, + // to cover those paths. We can trust that the slog JSONHandler works. + handler := slog.NewJSONHandler(buffer, nil) + sink := &passthruSlogSink{handler: handler} // passthruSlogSink implements SlogSink. + logger := New(sink) + return ToSlogHandler(logger) + }, exceptions...) +} + +func TestSlogSinkOnDiscard(_ *testing.T) { + // Compile-test + logger := slog.New(ToSlogHandler(Discard())) + logger.WithGroup("foo").With("x", 1).Info("hello") +} + +func TestConversion(t *testing.T) { + d := Discard() + d2 := FromSlogHandler(ToSlogHandler(d)) + expectEqual(t, d, d2) + + e := Logger{} + e2 := FromSlogHandler(ToSlogHandler(e)) + expectEqual(t, e, e2) + + text := slog.NewTextHandler(io.Discard, nil) + text2 := ToSlogHandler(FromSlogHandler(text)) + expectEqual(t, text, text2) + + text3 := ToSlogHandler(FromSlogHandler(text).V(1)) + if handler, ok := text3.(interface { + GetLevel() slog.Level + }); ok { + expectEqual(t, handler.GetLevel(), slog.Level(1)) + } else { + t.Errorf("Expected a slogHandler which implements V(1), got instead: %T %+v", text3, text3) + } +} + +func expectEqual(t *testing.T, expected, actual any) { + if expected != actual { + t.Helper() + t.Errorf("Expected %T %+v, got instead: %T %+v", expected, expected, actual, actual) + } +} diff --git a/pkg/logr/slogsink.go b/pkg/logr/slogsink.go new file mode 100644 index 00000000..4060fcbc --- /dev/null +++ b/pkg/logr/slogsink.go @@ -0,0 +1,120 @@ +//go:build go1.21 +// +build go1.21 + +/* +Copyright 2023 The logr Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package logr + +import ( + "context" + "log/slog" + "runtime" + "time" +) + +var ( + _ LogSink = &slogSink{} + _ CallDepthLogSink = &slogSink{} + _ Underlier = &slogSink{} +) + +// Underlier is implemented by the LogSink returned by NewFromLogHandler. +type Underlier interface { + // GetUnderlying returns the Handler used by the LogSink. + GetUnderlying() slog.Handler +} + +const ( + // nameKey is used to log the `WithName` values as an additional attribute. + nameKey = "logger" + + // errKey is used to log the error parameter of Error as an additional attribute. + errKey = "err" +) + +type slogSink struct { + callDepth int + name string + handler slog.Handler +} + +func (l *slogSink) Init(info RuntimeInfo) { + l.callDepth = info.CallDepth +} + +func (l *slogSink) GetUnderlying() slog.Handler { + return l.handler +} + +func (l *slogSink) WithCallDepth(depth int) LogSink { + newLogger := *l + newLogger.callDepth += depth + return &newLogger +} + +func (l *slogSink) Enabled(level int) bool { + return l.handler.Enabled(context.Background(), slog.Level(-level)) +} + +func (l *slogSink) Info(level int, msg string, kvList ...interface{}) { + l.log(nil, msg, slog.Level(-level), kvList...) +} + +func (l *slogSink) Error(err error, msg string, kvList ...interface{}) { + l.log(err, msg, slog.LevelError, kvList...) +} + +func (l *slogSink) log(err error, msg string, level slog.Level, kvList ...interface{}) { + var pcs [1]uintptr + // skip runtime.Callers, this function, Info/Error, and all helper functions above that. + runtime.Callers(3+l.callDepth, pcs[:]) + + record := slog.NewRecord(time.Now(), level, msg, pcs[0]) + if l.name != "" { + record.AddAttrs(slog.String(nameKey, l.name)) + } + if err != nil { + record.AddAttrs(slog.Any(errKey, err)) + } + record.Add(kvList...) + _ = l.handler.Handle(context.Background(), record) +} + +func (l slogSink) WithName(name string) LogSink { + if l.name != "" { + l.name += "/" + } + l.name += name + return &l +} + +func (l slogSink) WithValues(kvList ...interface{}) LogSink { + l.handler = l.handler.WithAttrs(kvListToAttrs(kvList...)) + return &l +} + +func kvListToAttrs(kvList ...interface{}) []slog.Attr { + // We don't need the record itself, only its Add method. + record := slog.NewRecord(time.Time{}, 0, "", 0) + record.Add(kvList...) + attrs := make([]slog.Attr, 0, record.NumAttrs()) + record.Attrs(func(attr slog.Attr) bool { + attrs = append(attrs, attr) + return true + }) + return attrs +} diff --git a/pkg/logr/testimpls_slog_test.go b/pkg/logr/testimpls_slog_test.go new file mode 100644 index 00000000..5ae26903 --- /dev/null +++ b/pkg/logr/testimpls_slog_test.go @@ -0,0 +1,140 @@ +//go:build go1.21 +// +build go1.21 + +/* +Copyright 2023 The logr Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package logr + +import ( + "context" + "log/slog" + "time" +) + +var _ SlogSink = &testSlogSink{} + +// testSlogSink is a trivial SlogSink implementation, just for testing, which +// calls (optional) hooks on each method. +type testSlogSink struct { + // embed a plain LogSink + testLogSink + + attrs []slog.Attr + groups []string + + fnHandle func(ss *testSlogSink, ctx context.Context, record slog.Record) + fnWithAttrs func(ss *testSlogSink, attrs []slog.Attr) + fnWithGroup func(ss *testSlogSink, name string) +} + +func (ss *testSlogSink) Handle(ctx context.Context, record slog.Record) error { + if ss.fnHandle != nil { + ss.fnHandle(ss, ctx, record) + } + return nil +} + +func (ss *testSlogSink) WithAttrs(attrs []slog.Attr) SlogSink { + if ss.fnWithAttrs != nil { + ss.fnWithAttrs(ss, attrs) + } + out := *ss + n := len(out.attrs) + out.attrs = append(out.attrs[:n:n], attrs...) + return &out +} + +func (ss *testSlogSink) WithGroup(name string) SlogSink { + if ss.fnWithGroup != nil { + ss.fnWithGroup(ss, name) + } + out := *ss + n := len(out.groups) + out.groups = append(out.groups[:n:n], name) + return &out +} + +// passthruLogSink is a trivial LogSink implementation, which implements the +// logr.LogSink methods in terms of a slog.Handler. +type passthruLogSink struct { + handler slog.Handler +} + +func (pl passthruLogSink) Init(RuntimeInfo) {} + +func (pl passthruLogSink) Enabled(int) bool { return true } + +func (pl passthruLogSink) Error(_ error, msg string, kvList ...interface{}) { + var record slog.Record + record.Message = msg + record.Level = slog.LevelError + record.Time = time.Now() + record.Add(kvList...) + _ = pl.handler.Handle(context.Background(), record) +} + +func (pl passthruLogSink) Info(_ int, msg string, kvList ...interface{}) { + var record slog.Record + record.Message = msg + record.Level = slog.LevelInfo + record.Time = time.Now() + record.Add(kvList...) + _ = pl.handler.Handle(context.Background(), record) +} + +func (pl passthruLogSink) WithName(string) LogSink { return &pl } + +func (pl passthruLogSink) WithValues(kvList ...interface{}) LogSink { + var values slog.Record + values.Add(kvList...) + var attrs []slog.Attr + add := func(attr slog.Attr) bool { + attrs = append(attrs, attr) + return true + } + values.Attrs(add) + + pl.handler = pl.handler.WithAttrs(attrs) + return &pl +} + +// passthruSlogSink is a trivial SlogSink implementation, which stubs out the +// logr.LogSink methods and passes Logr.SlogSink thru to a slog.Handler. +type passthruSlogSink struct { + handler slog.Handler +} + +func (ps passthruSlogSink) Init(RuntimeInfo) {} +func (ps passthruSlogSink) Enabled(int) bool { return true } +func (ps passthruSlogSink) Error(error, string, ...interface{}) {} +func (ps passthruSlogSink) Info(int, string, ...interface{}) {} +func (ps passthruSlogSink) WithName(string) LogSink { return &ps } +func (ps passthruSlogSink) WithValues(...interface{}) LogSink { return &ps } + +func (ps *passthruSlogSink) Handle(ctx context.Context, record slog.Record) error { + return ps.handler.Handle(ctx, record) +} + +func (ps passthruSlogSink) WithAttrs(attrs []slog.Attr) SlogSink { + ps.handler = ps.handler.WithAttrs(attrs) + return &ps +} + +func (ps passthruSlogSink) WithGroup(name string) SlogSink { + ps.handler = ps.handler.WithGroup(name) + return &ps +} diff --git a/pkg/logr/testimpls_test.go b/pkg/logr/testimpls_test.go new file mode 100644 index 00000000..08571db9 --- /dev/null +++ b/pkg/logr/testimpls_test.go @@ -0,0 +1,92 @@ +/* +Copyright 2021 The logr Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package logr + +// testLogSink is a trivial LogSink implementation, just for testing, which +// calls (optional) hooks on each method. +type testLogSink struct { + fnInit func(ri RuntimeInfo) + fnEnabled func(lvl int) bool + fnInfo func(lvl int, msg string, kv ...any) + fnError func(err error, msg string, kv ...any) + fnWithValues func(kv ...any) + fnWithName func(name string) + + withValues []any +} + +var _ LogSink = &testLogSink{} + +func (ls *testLogSink) Init(ri RuntimeInfo) { + if ls.fnInit != nil { + ls.fnInit(ri) + } +} + +func (ls *testLogSink) Enabled(lvl int) bool { + if ls.fnEnabled != nil { + return ls.fnEnabled(lvl) + } + return false +} + +func (ls *testLogSink) Info(lvl int, msg string, kv ...any) { + if ls.fnInfo != nil { + ls.fnInfo(lvl, msg, kv...) + } +} + +func (ls *testLogSink) Error(err error, msg string, kv ...any) { + if ls.fnError != nil { + ls.fnError(err, msg, kv...) + } +} + +func (ls *testLogSink) WithValues(kv ...any) LogSink { + if ls.fnWithValues != nil { + ls.fnWithValues(kv...) + } + out := *ls + n := len(out.withValues) + out.withValues = append(out.withValues[:n:n], kv...) + return &out +} + +func (ls *testLogSink) WithName(name string) LogSink { + if ls.fnWithName != nil { + ls.fnWithName(name) + } + out := *ls + return &out +} + +type testCallDepthLogSink struct { + testLogSink + callDepth int + fnWithCallDepth func(depth int) +} + +var _ CallDepthLogSink = &testCallDepthLogSink{} + +func (ls *testCallDepthLogSink) WithCallDepth(depth int) LogSink { + if ls.fnWithCallDepth != nil { + ls.fnWithCallDepth(depth) + } + out := *ls + out.callDepth += depth + return &out +} diff --git a/pkg/logr/testing/test.go b/pkg/logr/testing/test.go new file mode 100644 index 00000000..1607dc84 --- /dev/null +++ b/pkg/logr/testing/test.go @@ -0,0 +1,37 @@ +/* +Copyright 2019 The logr Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package testing provides support for using logr in tests. +// Deprecated. See github.com/go-logr/logr/testr instead. +package testing + +import "github.com/CodSpeedHQ/codspeed-go/pkg/logr/testr" + +// NewTestLogger returns a logr.Logger that prints through a testing.T object. +// Deprecated. See github.com/go-logr/logr/testr.New instead. +var NewTestLogger = testr.New + +// Options carries parameters which influence the way logs are generated. +// Deprecated. See github.com/go-logr/logr/testr.Options instead. +type Options = testr.Options + +// NewTestLoggerWithOptions returns a logr.Logger that prints through a testing.T object. +// Deprecated. See github.com/go-logr/logr/testr.NewWithOptions instead. +var NewTestLoggerWithOptions = testr.NewWithOptions + +// Underlier exposes access to the underlying testing.T instance. +// Deprecated. See github.com/go-logr/logr/testr.Underlier instead. +type Underlier = testr.Underlier diff --git a/pkg/logr/testing/test_test.go b/pkg/logr/testing/test_test.go new file mode 100644 index 00000000..1fd799ba --- /dev/null +++ b/pkg/logr/testing/test_test.go @@ -0,0 +1,52 @@ +/* +Copyright 2021 The logr Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package testing + +import ( + "fmt" + testing "github.com/CodSpeedHQ/codspeed-go/testing/testing" + + "github.com/CodSpeedHQ/codspeed-go/pkg/logr" +) + +func TestLogger(t *testing.T) { + log := NewTestLogger(t) + log.Info("info") + log.V(0).Info("V(0).info") + log.V(1).Info("v(1).info") + log.Error(fmt.Errorf("error"), "error") + log.WithName("testing").Info("with prefix") + Helper(log, "hello world") + + log = NewTestLoggerWithOptions(t, Options{ + LogTimestamp: true, + Verbosity: 1, + }) + log.V(1).Info("v(1).info with options") +} + +func Helper(log logr.Logger, msg string) { + helper, log := log.WithCallStackHelper() + helper() + helper2(log, msg) +} + +func helper2(log logr.Logger, msg string) { + helper, log := log.WithCallStackHelper() + helper() + log.Info(msg) +} diff --git a/pkg/logr/testr/testr.go b/pkg/logr/testr/testr.go new file mode 100644 index 00000000..8aa76ec9 --- /dev/null +++ b/pkg/logr/testr/testr.go @@ -0,0 +1,167 @@ +/* +Copyright 2019 The logr Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package testr provides support for using logr in tests. +package testr + +import ( + testing "github.com/CodSpeedHQ/codspeed-go/testing/testing" + + "github.com/CodSpeedHQ/codspeed-go/pkg/logr" + "github.com/CodSpeedHQ/codspeed-go/pkg/logr/funcr" +) + +// New returns a logr.Logger that prints through a testing.T object. +// Info logs are only enabled at V(0). +func New(t *testing.T) logr.Logger { + return NewWithOptions(t, Options{}) +} + +// Options carries parameters which influence the way logs are generated. +type Options struct { + // LogTimestamp tells the logger to add a "ts" key to log + // lines. This has some overhead, so some users might not want + // it. + LogTimestamp bool + + // Verbosity tells the logger which V logs to be write. + // Higher values enable more logs. + Verbosity int +} + +// NewWithOptions returns a logr.Logger that prints through a testing.T object. +// In contrast to the simpler New, output formatting can be configured. +func NewWithOptions(t *testing.T, opts Options) logr.Logger { + l := &testlogger{ + testloggerInterface: newLoggerInterfaceWithOptions(t, opts), + } + return logr.New(l) +} + +// TestingT is an interface wrapper around testing.T, testing.B and testing.F. +type TestingT interface { + Helper() + Log(args ...any) +} + +// NewWithInterface returns a logr.Logger that prints through a +// TestingT object. +// In contrast to the simpler New, output formatting can be configured. +func NewWithInterface(t TestingT, opts Options) logr.Logger { + l := newLoggerInterfaceWithOptions(t, opts) + return logr.New(&l) +} + +func newLoggerInterfaceWithOptions(t TestingT, opts Options) testloggerInterface { + return testloggerInterface{ + t: t, + Formatter: funcr.NewFormatter(funcr.Options{ + LogTimestamp: opts.LogTimestamp, + Verbosity: opts.Verbosity, + }), + } +} + +// Underlier exposes access to the underlying testing.T instance. Since +// callers only have a logr.Logger, they have to know which +// implementation is in use, so this interface is less of an +// abstraction and more of a way to test type conversion. +type Underlier interface { + GetUnderlying() *testing.T +} + +// UnderlierInterface exposes access to the underlying TestingT instance. Since +// callers only have a logr.Logger, they have to know which +// implementation is in use, so this interface is less of an +// abstraction and more of a way to test type conversion. +type UnderlierInterface interface { + GetUnderlying() TestingT +} + +// Info logging implementation shared between testLogger and testLoggerInterface. +func logInfo(t TestingT, formatInfo func(int, string, []any) (string, string), level int, msg string, kvList ...any) { + prefix, args := formatInfo(level, msg, kvList) + t.Helper() + if prefix != "" { + args = prefix + ": " + args + } + t.Log(args) +} + +// Error logging implementation shared between testLogger and testLoggerInterface. +func logError(t TestingT, formatError func(error, string, []any) (string, string), err error, msg string, kvList ...any) { + prefix, args := formatError(err, msg, kvList) + t.Helper() + if prefix != "" { + args = prefix + ": " + args + } + t.Log(args) +} + +// This type exists to wrap and modify the method-set of testloggerInterface. +// In particular, it changes the GetUnderlying() method. +type testlogger struct { + testloggerInterface +} + +func (l testlogger) GetUnderlying() *testing.T { + // This method is defined on testlogger, so the only type this could + // possibly be is testing.T, even though that's not guaranteed by the type + // system itself. + return l.t.(*testing.T) //nolint:forcetypeassert +} + +type testloggerInterface struct { + funcr.Formatter + t TestingT +} + +func (l testloggerInterface) WithName(name string) logr.LogSink { + l.AddName(name) // via Formatter + return &l +} + +func (l testloggerInterface) WithValues(kvList ...any) logr.LogSink { + l.AddValues(kvList) // via Formatter + return &l +} + +func (l testloggerInterface) GetCallStackHelper() func() { + return l.t.Helper +} + +func (l testloggerInterface) Info(level int, msg string, kvList ...any) { + l.t.Helper() + logInfo(l.t, l.FormatInfo, level, msg, kvList...) +} + +func (l testloggerInterface) Error(err error, msg string, kvList ...any) { + l.t.Helper() + logError(l.t, l.FormatError, err, msg, kvList...) +} + +func (l testloggerInterface) GetUnderlying() TestingT { + return l.t +} + +// Assert conformance to the interfaces. +var _ logr.LogSink = &testlogger{} +var _ logr.CallStackHelperLogSink = &testlogger{} +var _ Underlier = &testlogger{} + +var _ logr.LogSink = &testloggerInterface{} +var _ logr.CallStackHelperLogSink = &testloggerInterface{} +var _ UnderlierInterface = &testloggerInterface{} diff --git a/pkg/logr/testr/testr_fuzz_test.go b/pkg/logr/testr/testr_fuzz_test.go new file mode 100644 index 00000000..ea5217d2 --- /dev/null +++ b/pkg/logr/testr/testr_fuzz_test.go @@ -0,0 +1,27 @@ +//go:build go1.18 +// +build go1.18 + +/* +Copyright 2022 The logr Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package testr + +import "testing" + +func TestLoggerTestingF(_ *testing.T) { + f := &testing.F{} + _ = NewWithInterface(f, Options{}) +} diff --git a/pkg/logr/testr/testr_test.go b/pkg/logr/testr/testr_test.go new file mode 100644 index 00000000..2631f838 --- /dev/null +++ b/pkg/logr/testr/testr_test.go @@ -0,0 +1,89 @@ +/* +Copyright 2021 The logr Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package testr + +import ( + "fmt" + testing "github.com/CodSpeedHQ/codspeed-go/testing/testing" + + "github.com/CodSpeedHQ/codspeed-go/pkg/logr" +) + +func TestLogger(t *testing.T) { + log := New(t) + log.Info("info") + log.V(0).Info("V(0).info") + log.V(1).Info("v(1).info") + log.Error(fmt.Errorf("error"), "error") + log.WithName("testing").WithValues("value", "test").Info("with prefix") + log.WithName("testing").Error(fmt.Errorf("error"), "with prefix") + Helper(log, "hello world") + + log = NewWithOptions(t, Options{ + LogTimestamp: true, + Verbosity: 1, + }) + log.V(1).Info("v(1).info with options") + + underlier, ok := log.GetSink().(Underlier) + if !ok { + t.Fatal("couldn't get underlier") + } + if t != underlier.GetUnderlying() { + t.Error("invalid underlier") + } +} + +func TestLoggerInterface(t *testing.T) { + log := NewWithInterface(t, Options{}) + log.Info("info") + log.V(0).Info("V(0).info") + log.V(1).Info("v(1).info") + log.Error(fmt.Errorf("error"), "error") + log.WithName("testing").WithValues("value", "test").Info("with prefix") + log.WithName("testing").Error(fmt.Errorf("error"), "with prefix") + Helper(log, "hello world") + + underlier, ok := log.GetSink().(UnderlierInterface) + if !ok { + t.Fatal("couldn't get underlier") + } + underlierT, ok := underlier.GetUnderlying().(*testing.T) + if !ok { + t.Fatal("couldn't get underlying *testing.T") + } + if t != underlierT { + t.Error("invalid underlier") + } +} + +func TestLoggerTestingB(_ *testing.T) { + b := &testing.B{} + _ = NewWithInterface(b, Options{}) +} + +func Helper(log logr.Logger, msg string) { + helper, log := log.WithCallStackHelper() + helper() + helper2(log, msg) +} + +func helper2(log logr.Logger, msg string) { + helper, log := log.WithCallStackHelper() + helper() + log.Info(msg) +} diff --git a/pkg/stdr/.github/workflows/apidiff.yaml b/pkg/stdr/.github/workflows/apidiff.yaml new file mode 100644 index 00000000..340fffb0 --- /dev/null +++ b/pkg/stdr/.github/workflows/apidiff.yaml @@ -0,0 +1,29 @@ +name: Run apidiff + +on: [ pull_request ] + +jobs: + apidiff: + runs-on: ubuntu-latest + if: github.base_ref + steps: + - name: Install Go + uses: actions/setup-go@v2 + with: + go-version: 1.16.x + - name: Add GOBIN to PATH + run: echo "PATH=$(go env GOPATH)/bin:$PATH" >>$GITHUB_ENV + - name: Install dependencies + run: GO111MODULE=off go get golang.org/x/exp/cmd/apidiff + - name: Checkout old code + uses: actions/checkout@v2 + with: + ref: ${{ github.base_ref }} + path: "old" + - name: Checkout new code + uses: actions/checkout@v2 + with: + path: "new" + - name: APIDiff + run: ./_tools/apidiff.sh -d ../old + working-directory: "new" diff --git a/pkg/stdr/.github/workflows/lint.yaml b/pkg/stdr/.github/workflows/lint.yaml new file mode 100644 index 00000000..d76ae317 --- /dev/null +++ b/pkg/stdr/.github/workflows/lint.yaml @@ -0,0 +1,42 @@ +name: Run lint + +on: [ push, pull_request ] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v2 + - name: Lint + uses: golangci/golangci-lint-action@v2 + with: + # version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version + version: latest + + # Optional: show only new issues if it's a pull request. The default value is `false`. + # only-new-issues: true + + # golangci-lint command line arguments. + args: + -v + --max-same-issues 10 + --disable-all + --exclude-use-default=false + -E asciicheck + -E deadcode + -E errcheck + -E forcetypeassert + -E gocritic + -E gofmt + -E goimports + -E gosimple + -E govet + -E ineffassign + -E misspell + -E revive + -E staticcheck + -E structcheck + -E typecheck + -E unused + -E varcheck diff --git a/pkg/stdr/.github/workflows/tests.yaml b/pkg/stdr/.github/workflows/tests.yaml new file mode 100644 index 00000000..e99538e8 --- /dev/null +++ b/pkg/stdr/.github/workflows/tests.yaml @@ -0,0 +1,22 @@ +name: Run tests + +on: [ push, pull_request ] + +jobs: + test: + strategy: + matrix: + go-versions: [ 1.14.x, 1.15.x, 1.16.x ] + platform: [ ubuntu-latest, macos-latest, windows-latest ] + runs-on: ${{ matrix.platform }} + steps: + - name: Install Go + uses: actions/setup-go@v2 + with: + go-version: ${{ matrix.go-version }} + - name: Checkout code + uses: actions/checkout@v2 + - name: Build + run: go build -v ./... + - name: Test + run: go test -v -race ./... diff --git a/pkg/stdr/LICENSE b/pkg/stdr/LICENSE new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/pkg/stdr/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/pkg/stdr/README.md b/pkg/stdr/README.md new file mode 100644 index 00000000..51586678 --- /dev/null +++ b/pkg/stdr/README.md @@ -0,0 +1,6 @@ +# Minimal Go logging using logr and Go's standard library + +[![Go Reference](https://pkg.go.dev/badge/github.com/go-logr/stdr.svg)](https://pkg.go.dev/github.com/go-logr/stdr) + +This package implements the [logr interface](https://github.com/go-logr/logr) +in terms of Go's standard log package(https://pkg.go.dev/log). diff --git a/pkg/stdr/_tools/apidiff.sh b/pkg/stdr/_tools/apidiff.sh new file mode 100755 index 00000000..81ec70aa --- /dev/null +++ b/pkg/stdr/_tools/apidiff.sh @@ -0,0 +1,127 @@ +#!/usr/bin/env bash + +# Copyright 2020 The Kubernetes Authors. +# Copyright 2021 The logr Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -o errexit +set -o nounset +set -o pipefail + +function usage { + local script="$(basename $0)" + + echo >&2 "Usage: ${script} [-r | -d ] + +This script should be run at the root of a module. + +-r + Compare the exported API of the local working copy with the + exported API of the local repo at the specified branch or tag. + +-d + Compare the exported API of the local working copy with the + exported API of the specified directory, which should point + to the root of a different version of the same module. + +Examples: + ${script} -r master + ${script} -r v1.10.0 + ${script} -r release-1.10 + ${script} -d /path/to/historical/version +" + exit 1 +} + +ref="" +dir="" +while getopts r:d: o +do case "$o" in + r) ref="$OPTARG";; + d) dir="$OPTARG";; + [?]) usage;; + esac +done + +# If REF and DIR are empty, print usage and error +if [[ -z "${ref}" && -z "${dir}" ]]; then + usage; +fi +# If REF and DIR are both set, print usage and error +if [[ -n "${ref}" && -n "${dir}" ]]; then + usage; +fi + +if ! which apidiff > /dev/null; then + echo "Installing golang.org/x/exp/cmd/apidiff" + pushd "${TMPDIR:-/tmp}" > /dev/null + GO111MODULE=off go get golang.org/x/exp/cmd/apidiff + popd > /dev/null +fi + +output=$(mktemp -d -t "apidiff.output.XXXX") +cleanup_output () { rm -fr "${output}"; } +trap cleanup_output EXIT + +# If ref is set, clone . to temp dir at $ref, and set $dir to the temp dir +clone="" +base="${dir}" +if [[ -n "${ref}" ]]; then + base="${ref}" + clone=$(mktemp -d -t "apidiff.clone.XXXX") + cleanup_clone_and_output () { rm -fr "${clone}"; cleanup_output; } + trap cleanup_clone_and_output EXIT + git clone . -q --no-tags "${clone}" + git -C "${clone}" co "${ref}" + dir="${clone}" +fi + +pushd "${dir}" >/dev/null + echo "Inspecting API of ${base}" + go list ./... > packages.txt + for pkg in $(cat packages.txt); do + mkdir -p "${output}/${pkg}" + apidiff -w "${output}/${pkg}/apidiff.output" "${pkg}" + done +popd >/dev/null + +retval=0 + +echo "Comparing with ${base}" +for pkg in $(go list ./...); do + # New packages are ok + if [ ! -f "${output}/${pkg}/apidiff.output" ]; then + continue + fi + + # Check for incompatible changes to previous packages + incompatible=$(apidiff -incompatible "${output}/${pkg}/apidiff.output" "${pkg}") + if [[ -n "${incompatible}" ]]; then + echo >&2 "FAIL: ${pkg} contains incompatible changes: +${incompatible} +" + retval=1 + fi +done + +# Check for removed packages +removed=$(comm -23 "${dir}/packages.txt" <(go list ./...)) +if [[ -n "${removed}" ]]; then + echo >&2 "FAIL: removed packages: +${removed} +" + retval=1 +fi + +exit $retval diff --git a/pkg/stdr/example/main.go b/pkg/stdr/example/main.go new file mode 100644 index 00000000..65b6ff7f --- /dev/null +++ b/pkg/stdr/example/main.go @@ -0,0 +1,58 @@ +/* +Copyright 2019 The logr Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + stdlog "log" + "os" + + "github.com/CodSpeedHQ/codspeed-go/pkg/logr" + "github.com/CodSpeedHQ/codspeed-go/pkg/stdr" +) + +type e struct { + str string +} + +func (e e) Error() string { + return e.str +} + +func helper(log logr.Logger, msg string) { + helper2(log, msg) +} + +func helper2(log logr.Logger, msg string) { + log.WithCallDepth(2).Info(msg) +} + +func main() { + stdr.SetVerbosity(1) + log := stdr.NewWithOptions(stdlog.New(os.Stderr, "", stdlog.LstdFlags), stdr.Options{LogCaller: stdr.All}) + log = log.WithName("MyName") + example(log.WithValues("module", "example")) +} + +// If this were in another package, all it would depend on in logr, not stdr. +func example(log logr.Logger) { + log.Info("hello", "val1", 1, "val2", map[string]int{"k": 1}) + log.V(1).Info("you should see this") + log.V(1).V(1).Info("you should NOT see this") + log.Error(nil, "uh oh", "trouble", true, "reasons", []float64{0.1, 0.11, 3.14}) + log.Error(e{"an error occurred"}, "goodbye", "code", -1) + helper(log, "thru a helper") +} diff --git a/pkg/stdr/example_test.go b/pkg/stdr/example_test.go new file mode 100644 index 00000000..405ff86e --- /dev/null +++ b/pkg/stdr/example_test.go @@ -0,0 +1,58 @@ +/* +Copyright 2021 The logr Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package stdr_test + +import ( + "errors" + "log" + "os" + + "github.com/CodSpeedHQ/codspeed-go/pkg/stdr" +) + +var errSome = errors.New("some error") + +func newStdLogger(flags int) stdr.StdLogger { + return log.New(os.Stdout, "", flags) +} + +func ExampleNew() { + log := stdr.New(newStdLogger(log.Lshortfile)) + log.Info("info message with default options") + log.Error(errSome, "error message with default options") + log.Info("invalid key", 42, "answer") + log.Info("missing value", "answer") + // Output: + // example_test.go:35: "level"=0 "msg"="info message with default options" + // example_test.go:36: "msg"="error message with default options" "error"="some error" + // example_test.go:37: "level"=0 "msg"="invalid key" ""="answer" + // example_test.go:38: "level"=0 "msg"="missing value" "answer"="" +} + +func ExampleNew_withName() { + log := stdr.New(newStdLogger(0)) + log.WithName("hello").WithName("world").Info("thanks for the fish") + // Output: + // hello/world: "level"=0 "msg"="thanks for the fish" +} + +func ExampleNewWithOptions() { + log := stdr.NewWithOptions(newStdLogger(0), stdr.Options{LogCaller: stdr.All}) + log.Info("with LogCaller=All") + // Output: + // "caller"={"file":"example_test.go","line":55} "level"=0 "msg"="with LogCaller=All" +} diff --git a/pkg/stdr/stdr.go b/pkg/stdr/stdr.go new file mode 100644 index 00000000..0faae908 --- /dev/null +++ b/pkg/stdr/stdr.go @@ -0,0 +1,170 @@ +/* +Copyright 2019 The logr Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package stdr implements github.com/go-logr/logr.Logger in terms of +// Go's standard log package. +package stdr + +import ( + "log" + "os" + + "github.com/CodSpeedHQ/codspeed-go/pkg/logr" + "github.com/CodSpeedHQ/codspeed-go/pkg/logr/funcr" +) + +// The global verbosity level. See SetVerbosity(). +var globalVerbosity int + +// SetVerbosity sets the global level against which all info logs will be +// compared. If this is greater than or equal to the "V" of the logger, the +// message will be logged. A higher value here means more logs will be written. +// The previous verbosity value is returned. This is not concurrent-safe - +// callers must be sure to call it from only one goroutine. +func SetVerbosity(v int) int { + old := globalVerbosity + globalVerbosity = v + return old +} + +// New returns a logr.Logger which is implemented by Go's standard log package, +// or something like it. If std is nil, this will use a default logger +// instead. +// +// Example: stdr.New(log.New(os.Stderr, "", log.LstdFlags|log.Lshortfile))) +func New(std StdLogger) logr.Logger { + return NewWithOptions(std, Options{}) +} + +// NewWithOptions returns a logr.Logger which is implemented by Go's standard +// log package, or something like it. See New for details. +func NewWithOptions(std StdLogger, opts Options) logr.Logger { + if std == nil { + // Go's log.Default() is only available in 1.16 and higher. + std = log.New(os.Stderr, "", log.LstdFlags) + } + + if opts.Depth < 0 { + opts.Depth = 0 + } + + fopts := funcr.Options{ + LogCaller: funcr.MessageClass(opts.LogCaller), + } + + sl := &logger{ + Formatter: funcr.NewFormatter(fopts), + std: std, + } + + // For skipping our own logger.Info/Error. + sl.Formatter.AddCallDepth(1 + opts.Depth) + + return logr.New(sl) +} + +// Options carries parameters which influence the way logs are generated. +type Options struct { + // Depth biases the assumed number of call frames to the "true" caller. + // This is useful when the calling code calls a function which then calls + // stdr (e.g. a logging shim to another API). Values less than zero will + // be treated as zero. + Depth int + + // LogCaller tells stdr to add a "caller" key to some or all log lines. + // Go's log package has options to log this natively, too. + LogCaller MessageClass + + // TODO: add an option to log the date/time +} + +// MessageClass indicates which category or categories of messages to consider. +type MessageClass int + +const ( + // None ignores all message classes. + None MessageClass = iota + // All considers all message classes. + All + // Info only considers info messages. + Info + // Error only considers error messages. + Error +) + +// StdLogger is the subset of the Go stdlib log.Logger API that is needed for +// this adapter. +type StdLogger interface { + // Output is the same as log.Output and log.Logger.Output. + Output(calldepth int, logline string) error +} + +type logger struct { + funcr.Formatter + std StdLogger +} + +var _ logr.LogSink = &logger{} +var _ logr.CallDepthLogSink = &logger{} + +func (l logger) Enabled(level int) bool { + return globalVerbosity >= level +} + +func (l logger) Info(level int, msg string, kvList ...interface{}) { + prefix, args := l.FormatInfo(level, msg, kvList) + if prefix != "" { + args = prefix + ": " + args + } + _ = l.std.Output(l.Formatter.GetDepth()+1, args) +} + +func (l logger) Error(err error, msg string, kvList ...interface{}) { + prefix, args := l.FormatError(err, msg, kvList) + if prefix != "" { + args = prefix + ": " + args + } + _ = l.std.Output(l.Formatter.GetDepth()+1, args) +} + +func (l logger) WithName(name string) logr.LogSink { + l.Formatter.AddName(name) + return &l +} + +func (l logger) WithValues(kvList ...interface{}) logr.LogSink { + l.Formatter.AddValues(kvList) + return &l +} + +func (l logger) WithCallDepth(depth int) logr.LogSink { + l.Formatter.AddCallDepth(depth) + return &l +} + +// Underlier exposes access to the underlying logging implementation. Since +// callers only have a logr.Logger, they have to know which implementation is +// in use, so this interface is less of an abstraction and more of way to test +// type conversion. +type Underlier interface { + GetUnderlying() StdLogger +} + +// GetUnderlying returns the StdLogger underneath this logger. Since StdLogger +// is itself an interface, the result may or may not be a Go log.Logger. +func (l logger) GetUnderlying() StdLogger { + return l.std +} From ca64ae5613d6a903e95f3e8ad0c809d883537b5b Mon Sep 17 00:00:00 2001 From: not-matthias Date: Wed, 17 Dec 2025 19:27:48 +0100 Subject: [PATCH 03/11] feat(go-runner): patch imports of stdr and logr --- go-runner/src/builder/patcher.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/go-runner/src/builder/patcher.rs b/go-runner/src/builder/patcher.rs index 7b5a1065..8eb56f61 100644 --- a/go-runner/src/builder/patcher.rs +++ b/go-runner/src/builder/patcher.rs @@ -145,6 +145,22 @@ impl Patcher { "\"github.com/CodSpeedHQ/codspeed-go/pkg/quicktest\"", ); + // Replace logr + subpackages + for logr_pkg in &["testr", "funcr", "slogr", "benchmark", "testing"] { + find_replace_range( + &format!("github.com/go-logr/logr/{}", logr_pkg), + &format!("\"github.com/CodSpeedHQ/codspeed-go/pkg/logr/{logr_pkg}\""), + ); + } + find_replace_range( + "github.com/go-logr/logr", + "\"github.com/CodSpeedHQ/codspeed-go/pkg/logr\"", + ); + find_replace_range( + "github.com/go-logr/stdr", + "\"github.com/CodSpeedHQ/codspeed-go/pkg/stdr\"", + ); + // Apply replacements in reverse order to avoid shifting positions for (range, replacement) in replacements .into_iter() From 12b95abbc05475b8881de1fc394a286d324ad6ad Mon Sep 17 00:00:00 2001 From: not-matthias Date: Wed, 17 Dec 2025 19:33:54 +0100 Subject: [PATCH 04/11] chore(example): add logr test --- example/compat/logr_test.go | 36 +++++++++++++++++++ example/go.mod | 1 + example/go.sum | 2 ++ ...y__tests__discover_benchmarks@example.snap | 24 +++++++++++++ ...sts__assert_results_snapshots@example.snap | 33 +++++++++++++++++ 5 files changed, 96 insertions(+) create mode 100644 example/compat/logr_test.go diff --git a/example/compat/logr_test.go b/example/compat/logr_test.go new file mode 100644 index 00000000..c6afcf00 --- /dev/null +++ b/example/compat/logr_test.go @@ -0,0 +1,36 @@ +package example + +import ( + "fmt" + "testing" + + "github.com/go-logr/logr/testr" +) + +func TestLogrTestr(t *testing.T) { + logger := testr.NewWithOptions(t, testr.Options{Verbosity: 2}) + + logger.Info("starting test", "test", "logr") + logger.V(0).Info("V(0).info") + logger.V(1).Info("V(1).info") + logger.Error(fmt.Errorf("test error"), "error") + + childLogger := logger.WithValues("request_id", "12345") + childLogger.Info("child logger message") + + loggerWithName := childLogger.WithName("child") + loggerWithName.Info("named child logger") +} + +func BenchmarkLogrTestr(b *testing.B) { + t := &testing.T{} + logger := testr.New(t) + + for b.Loop() { + // Log operations within the benchmark + logger.Info("benchmark message") + logger.V(1).Info("verbose benchmark") + childLogger := logger.WithValues("id", "test") + childLogger.Info("child message") + } +} diff --git a/example/go.mod b/example/go.mod index f148fce1..6d92f779 100644 --- a/example/go.mod +++ b/example/go.mod @@ -4,6 +4,7 @@ go 1.24.3 require ( github.com/frankban/quicktest v1.14.6 + github.com/go-logr/logr v1.4.3 github.com/stretchr/testify v1.11.1 github.com/thejerf/slogassert v0.3.4 ) diff --git a/example/go.sum b/example/go.sum index f8457574..40c6dfa7 100644 --- a/example/go.sum +++ b/example/go.sum @@ -3,6 +3,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= diff --git a/go-runner/src/builder/snapshots/codspeed_go_runner__builder__discovery__tests__discover_benchmarks@example.snap b/go-runner/src/builder/snapshots/codspeed_go_runner__builder__discovery__tests__discover_benchmarks@example.snap index 0ca54f63..4cc00584 100644 --- a/go-runner/src/builder/snapshots/codspeed_go_runner__builder__discovery__tests__discover_benchmarks@example.snap +++ b/go-runner/src/builder/snapshots/codspeed_go_runner__builder__discovery__tests__discover_benchmarks@example.snap @@ -115,6 +115,14 @@ expression: packages { "raw_package": "[raw_package]", "benchmarks": [ + { + "name": "BenchmarkLogrTestr", + "module_path": "example/compat", + "import_alias": "benchmarklogrtestr_3904647701036713533", + "qualified_name": "benchmarklogrtestr_3904647701036713533.BenchmarkLogrTestr", + "file_path": "compat/logr_test.go", + "is_external": false + }, { "name": "BenchmarkQuicktest", "module_path": "example/compat", @@ -131,6 +139,22 @@ expression: packages "file_path": "compat/slogassert_test.go", "is_external": false }, + { + "name": "BenchmarkStdr", + "module_path": "example/compat", + "import_alias": "benchmarkstdr_3904647701036713533", + "qualified_name": "benchmarkstdr_3904647701036713533.BenchmarkStdr", + "file_path": "compat/stdr_test.go", + "is_external": false + }, + { + "name": "BenchmarkStdrWithName", + "module_path": "example/compat", + "import_alias": "benchmarkstdrwithname_3904647701036713533", + "qualified_name": "benchmarkstdrwithname_3904647701036713533.BenchmarkStdrWithName", + "file_path": "compat/stdr_test.go", + "is_external": false + }, { "name": "BenchmarkTestifyWithNew", "module_path": "example/compat", diff --git a/go-runner/src/snapshots/codspeed_go_runner__integration_tests__assert_results_snapshots@example.snap b/go-runner/src/snapshots/codspeed_go_runner__integration_tests__assert_results_snapshots@example.snap index 6330f1f4..20249e13 100644 --- a/go-runner/src/snapshots/codspeed_go_runner__integration_tests__assert_results_snapshots@example.snap +++ b/go-runner/src/snapshots/codspeed_go_runner__integration_tests__assert_results_snapshots@example.snap @@ -292,6 +292,17 @@ expression: results "type": "walltime" }, "benchmarks": [ + { + "name": "BenchmarkLogrTestr", + "uri": "example/compat/logr_test.go::BenchmarkLogrTestr", + "config": { + "warmup_time_ns": null, + "min_round_time_ns": null, + "max_time_ns": null, + "max_rounds": null + }, + "stats": "[stats]" + }, { "name": "BenchmarkQuicktest", "uri": "example/compat/quicktest_test.go::BenchmarkQuicktest", @@ -303,6 +314,28 @@ expression: results }, "stats": "[stats]" }, + { + "name": "BenchmarkStdr", + "uri": "example/compat/stdr_test.go::BenchmarkStdr", + "config": { + "warmup_time_ns": null, + "min_round_time_ns": null, + "max_time_ns": null, + "max_rounds": null + }, + "stats": "[stats]" + }, + { + "name": "BenchmarkStdrWithName", + "uri": "example/compat/stdr_test.go::BenchmarkStdrWithName", + "config": { + "warmup_time_ns": null, + "min_round_time_ns": null, + "max_time_ns": null, + "max_rounds": null + }, + "stats": "[stats]" + }, { "name": "BenchmarkTestifyWithNew", "uri": "example/compat/testify_test.go::BenchmarkTestifyWithNew", From 2a09293b9244d85790ff842bbceb400c6267e5a4 Mon Sep 17 00:00:00 2001 From: not-matthias Date: Wed, 17 Dec 2025 19:59:09 +0100 Subject: [PATCH 05/11] chore(example): add stdr test --- example/compat/stdr_test.go | 55 +++++++++++++++++++++++++++++++++++++ example/go.mod | 3 +- example/go.sum | 6 +++- 3 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 example/compat/stdr_test.go diff --git a/example/compat/stdr_test.go b/example/compat/stdr_test.go new file mode 100644 index 00000000..3d7979a9 --- /dev/null +++ b/example/compat/stdr_test.go @@ -0,0 +1,55 @@ +package example + +import ( + "errors" + "log" + "testing" + + "github.com/go-logr/stdr" +) + +func TestStdr(t *testing.T) { + logger := stdr.New(log.Default()) + + logger.Info("info message", "key", "value") + logger.Error(errors.New("test error"), "error message") + + namedLogger := logger.WithName("myapp") + namedLogger.Info("named logger message") + + childLogger := namedLogger.WithValues("request_id", "12345") + childLogger.Info("child logger message") + + childLogger.V(1).Info("verbose message") +} + +func TestStdrWithOptions(t *testing.T) { + options := stdr.Options{ + LogCaller: stdr.Error, + Depth: 1, + } + + logger := stdr.NewWithOptions(log.Default(), options) + + logger.Info("info with options") + logger.Error(errors.New("err"), "error with options") +} + +func BenchmarkStdr(b *testing.B) { + logger := stdr.New(log.Default()) + + for b.Loop() { + logger.Info("benchmark message", "iteration", b.N) + childLogger := logger.WithValues("id", "test") + childLogger.Info("child message") + } +} + +func BenchmarkStdrWithName(b *testing.B) { + logger := stdr.New(log.Default()).WithName("bench") + + for b.Loop() { + logger.Info("named benchmark", "count", b.N) + logger.V(1).Info("verbose benchmark") + } +} diff --git a/example/go.mod b/example/go.mod index 6d92f779..e8930564 100644 --- a/example/go.mod +++ b/example/go.mod @@ -5,13 +5,14 @@ go 1.24.3 require ( github.com/frankban/quicktest v1.14.6 github.com/go-logr/logr v1.4.3 + github.com/go-logr/stdr v1.2.2 github.com/stretchr/testify v1.11.1 github.com/thejerf/slogassert v0.3.4 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect - github.com/google/go-cmp v0.5.9 // indirect + github.com/google/go-cmp v0.7.0 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/kr/text v0.2.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect diff --git a/example/go.sum b/example/go.sum index 40c6dfa7..87a5ffbf 100644 --- a/example/go.sum +++ b/example/go.sum @@ -3,10 +3,14 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= From 17de271626e037238c5709c45879f4871e4c6ba9 Mon Sep 17 00:00:00 2001 From: not-matthias Date: Wed, 7 Jan 2026 11:47:47 +0100 Subject: [PATCH 06/11] chore: add small test for import mismatch This ensures that we always patch the logr and stdr package correclty, since they also depend on each other. We must not just update the imports, we also must patch them like we do in `patcher.rs` --- go-runner/src/integration_tests.rs | 1 + ..._snapshots@example-logr-type-mismatch.snap | 29 +++++++++++++++ .../example-logr-type-mismatch/README.md | 37 +++++++++++++++++++ .../example-logr-type-mismatch/go.mod | 8 ++++ .../example-logr-type-mismatch/go.sum | 5 +++ .../internal/global/benchmark_test.go | 14 +++++++ .../internal/global/logging.go | 29 +++++++++++++++ .../example-logr-type-mismatch/logging.go | 29 +++++++++++++++ .../logging_test.go | 15 ++++++++ 9 files changed, 167 insertions(+) create mode 100644 go-runner/src/snapshots/codspeed_go_runner__integration_tests__assert_results_snapshots@example-logr-type-mismatch.snap create mode 100644 go-runner/testdata/projects/example-logr-type-mismatch/README.md create mode 100644 go-runner/testdata/projects/example-logr-type-mismatch/go.mod create mode 100644 go-runner/testdata/projects/example-logr-type-mismatch/go.sum create mode 100644 go-runner/testdata/projects/example-logr-type-mismatch/internal/global/benchmark_test.go create mode 100644 go-runner/testdata/projects/example-logr-type-mismatch/internal/global/logging.go create mode 100644 go-runner/testdata/projects/example-logr-type-mismatch/logging.go create mode 100644 go-runner/testdata/projects/example-logr-type-mismatch/logging_test.go diff --git a/go-runner/src/integration_tests.rs b/go-runner/src/integration_tests.rs index 8328b812..fd02a6d2 100644 --- a/go-runner/src/integration_tests.rs +++ b/go-runner/src/integration_tests.rs @@ -83,6 +83,7 @@ fn assert_results_snapshots(profile_dir: &Path, project_name: &str) { #[case::example_with_test_package("example-with-test-package")] #[case::example_with_replace("example-with-replace")] #[case::example_with_excluded_names("example-with-excluded-names")] +#[case::example_logr_type_mismatch("example-logr-type-mismatch")] #[test_log::test] fn test_build_and_run(#[case] project_name: &str) { let project_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")) diff --git a/go-runner/src/snapshots/codspeed_go_runner__integration_tests__assert_results_snapshots@example-logr-type-mismatch.snap b/go-runner/src/snapshots/codspeed_go_runner__integration_tests__assert_results_snapshots@example-logr-type-mismatch.snap new file mode 100644 index 00000000..b2ce991b --- /dev/null +++ b/go-runner/src/snapshots/codspeed_go_runner__integration_tests__assert_results_snapshots@example-logr-type-mismatch.snap @@ -0,0 +1,29 @@ +--- +source: go-runner/src/integration_tests.rs +expression: results +--- +[ + { + "creator": { + "name": "codspeed-go", + "version": "[version]", + "pid": "[pid]" + }, + "instrument": { + "type": "walltime" + }, + "benchmarks": [ + { + "name": "BenchmarkLogger", + "uri": "go-runner/testdata/projects/example-logr-type-mismatch/internal/global/benchmark_test.go::BenchmarkLogger", + "config": { + "warmup_time_ns": null, + "min_round_time_ns": null, + "max_time_ns": null, + "max_rounds": null + }, + "stats": "[stats]" + } + ] + } +] diff --git a/go-runner/testdata/projects/example-logr-type-mismatch/README.md b/go-runner/testdata/projects/example-logr-type-mismatch/README.md new file mode 100644 index 00000000..7fe2bc81 --- /dev/null +++ b/go-runner/testdata/projects/example-logr-type-mismatch/README.md @@ -0,0 +1,37 @@ +# Example: logr Type Mismatch + +This example demonstrates a type mismatch issue that occurs when patching imports for packages that use `github.com/go-logr/logr` types across multiple packages. + +## The Issue + +When the runner patches imports to replace `github.com/go-logr/logr` with `github.com/CodSpeedHQ/codspeed-go/pkg/logr`: + +1. Source files in the root package (`logging.go`) get patched +2. Source files in sub-packages (`internal/global/logging.go`) get patched +3. Test files also get patched and renamed (`logging_test.go` → `logging_codspeed.go`) +4. However, when compiling the `global` package benchmarks, there's a type mismatch: + - Some code references `*"github.com/go-logr/logr".Logger` + - Other code references `*"github.com/CodSpeedHQ/codspeed-go/pkg/logr".Logger` + +## How to Test + +### Standard Go (should work): +```bash +cd go-runner/testdata/projects/example-logr-type-mismatch +go test -bench=. ./... +``` + +### CodSpeed Go Runner (currently broken): +```bash +cd go-runner +cargo run -- test -bench=. testdata/projects/example-logr-type-mismatch +``` + +Expected behavior: Should compile and run benchmarks successfully +Actual behavior: Compilation fails with type mismatch errors between original and patched logr.Logger types + +## Related + +This reproduces the issue seen in the `opentelemetry-go` integration test where: +- `./internal_logging.go:24:10: cannot use &l (value of type *"github.com/go-logr/logr".Logger) as *"github.com/CodSpeedHQ/codspeed-go/pkg/logr".Logger` +- `./internal_logging_codspeed.go:25:13: cannot use stdr.New(...) (value of struct type "github.com/go-logr/logr".Logger) as "github.com/CodSpeedHQ/codspeed-go/pkg/logr".Logger` diff --git a/go-runner/testdata/projects/example-logr-type-mismatch/go.mod b/go-runner/testdata/projects/example-logr-type-mismatch/go.mod new file mode 100644 index 00000000..2ad6c29d --- /dev/null +++ b/go-runner/testdata/projects/example-logr-type-mismatch/go.mod @@ -0,0 +1,8 @@ +module example.com/logr-test + +go 1.23.0 + +require ( + github.com/go-logr/logr v1.4.3 + github.com/go-logr/stdr v1.2.2 +) diff --git a/go-runner/testdata/projects/example-logr-type-mismatch/go.sum b/go-runner/testdata/projects/example-logr-type-mismatch/go.sum new file mode 100644 index 00000000..8c3f29ba --- /dev/null +++ b/go-runner/testdata/projects/example-logr-type-mismatch/go.sum @@ -0,0 +1,5 @@ +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= diff --git a/go-runner/testdata/projects/example-logr-type-mismatch/internal/global/benchmark_test.go b/go-runner/testdata/projects/example-logr-type-mismatch/internal/global/benchmark_test.go new file mode 100644 index 00000000..afff0fc7 --- /dev/null +++ b/go-runner/testdata/projects/example-logr-type-mismatch/internal/global/benchmark_test.go @@ -0,0 +1,14 @@ +package global + +import ( + "testing" +) + +func BenchmarkLogger(b *testing.B) { + logger := GetInternalLogger() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + logger.Info("benchmark message") + } +} diff --git a/go-runner/testdata/projects/example-logr-type-mismatch/internal/global/logging.go b/go-runner/testdata/projects/example-logr-type-mismatch/internal/global/logging.go new file mode 100644 index 00000000..73a9da4f --- /dev/null +++ b/go-runner/testdata/projects/example-logr-type-mismatch/internal/global/logging.go @@ -0,0 +1,29 @@ +package global + +import ( + "log" + "os" + "sync/atomic" + + "github.com/go-logr/logr" + "github.com/go-logr/stdr" +) + +// internalLogger holds a reference to the logr.Logger used within the global package +var internalLogger = func() *atomic.Pointer[logr.Logger] { + l := stdr.New(log.New(os.Stderr, "", log.LstdFlags)) + + p := new(atomic.Pointer[logr.Logger]) + p.Store(&l) + return p +}() + +// SetInternalLogger sets the internal Logger to l. +func SetInternalLogger(l logr.Logger) { + internalLogger.Store(&l) +} + +// GetInternalLogger returns the internal logger. +func GetInternalLogger() logr.Logger { + return *internalLogger.Load() +} diff --git a/go-runner/testdata/projects/example-logr-type-mismatch/logging.go b/go-runner/testdata/projects/example-logr-type-mismatch/logging.go new file mode 100644 index 00000000..2f772fd1 --- /dev/null +++ b/go-runner/testdata/projects/example-logr-type-mismatch/logging.go @@ -0,0 +1,29 @@ +package example + +import ( + "log" + "os" + "sync/atomic" + + "github.com/go-logr/logr" + "github.com/go-logr/stdr" +) + +// globalLogger holds a reference to the logr.Logger +var globalLogger = func() *atomic.Pointer[logr.Logger] { + l := stdr.New(log.New(os.Stderr, "", log.LstdFlags)) + + p := new(atomic.Pointer[logr.Logger]) + p.Store(&l) + return p +}() + +// SetLogger sets the global Logger to l. +func SetLogger(l logr.Logger) { + globalLogger.Store(&l) +} + +// GetLogger returns the global logger. +func GetLogger() logr.Logger { + return *globalLogger.Load() +} diff --git a/go-runner/testdata/projects/example-logr-type-mismatch/logging_test.go b/go-runner/testdata/projects/example-logr-type-mismatch/logging_test.go new file mode 100644 index 00000000..d566d734 --- /dev/null +++ b/go-runner/testdata/projects/example-logr-type-mismatch/logging_test.go @@ -0,0 +1,15 @@ +package example_test + +import ( + "io" + "log" + + "github.com/go-logr/stdr" + + example "example.com/logr-test" +) + +func ExampleSetLogger() { + logger := stdr.New(log.New(io.Discard, "", 0)) + example.SetLogger(logger) +} From f092d36cf092177412aec9fc7d6bafe619831137 Mon Sep 17 00:00:00 2001 From: not-matthias Date: Wed, 7 Jan 2026 15:57:16 +0100 Subject: [PATCH 07/11] feat(go-runner): add support for using functions from internal test files in external test packages --- go-runner/src/builder/discovery.rs | 78 +++++++++++++- ...ample-external-test-unexported-access.snap | 43 ++++++++ ..._benchmarks@example-with-test-package.snap | 3 + ...very__tests__discover_benchmarks@hugo.snap | 22 ++++ ..._discover_benchmarks@opentelemetry-go.snap | 3 + ...overy__tests__discover_benchmarks@zap.snap | 13 +++ ...y__tests__discover_benchmarks@zerolog.snap | 11 ++ go-runner/src/builder/templater.rs | 14 +++ go-runner/src/integration_tests.rs | 1 + ...ample-external-test-unexported-access.snap | 63 +++++++++++ .../README.md | 101 ++++++++++++++++++ .../go.mod | 3 + .../internal_test.go | 30 ++++++ .../main.go | 6 ++ .../main_test.go | 45 ++++++++ 15 files changed, 434 insertions(+), 2 deletions(-) create mode 100644 go-runner/src/builder/snapshots/codspeed_go_runner__builder__discovery__tests__discover_benchmarks@example-external-test-unexported-access.snap create mode 100644 go-runner/src/snapshots/codspeed_go_runner__integration_tests__assert_results_snapshots@example-external-test-unexported-access.snap create mode 100644 go-runner/testdata/projects/example-external-test-unexported-access/README.md create mode 100644 go-runner/testdata/projects/example-external-test-unexported-access/go.mod create mode 100644 go-runner/testdata/projects/example-external-test-unexported-access/internal_test.go create mode 100644 go-runner/testdata/projects/example-external-test-unexported-access/main.go create mode 100644 go-runner/testdata/projects/example-external-test-unexported-access/main_test.go diff --git a/go-runner/src/builder/discovery.rs b/go-runner/src/builder/discovery.rs index 6ba83fea..9ca4a15b 100644 --- a/go-runner/src/builder/discovery.rs +++ b/go-runner/src/builder/discovery.rs @@ -248,25 +248,85 @@ impl GoBenchmark { pub struct BenchmarkPackage { raw_package: GoPackage, pub benchmarks: Vec, + + /// Internal test files that should be renamed alongside external test files. + /// For external test packages (package foo_test), this contains files from + /// the companion internal test package (package foo in *_test.go files). + /// These files define helper functions that external tests depend on. + /// + /// This field will only be set/used for external tests. + #[serde(skip_serializing_if = "Vec::is_empty")] + internal_test_files: Vec, } impl BenchmarkPackage { - fn new(package: GoPackage, benchmarks: Vec) -> Self { + fn new( + package: GoPackage, + benchmarks: Vec, + internal_test_files: Vec, + ) -> Self { Self { raw_package: package, benchmarks, + internal_test_files, } } + /// Returns the internal test files that should be renamed for this package. + pub fn internal_test_files(&self) -> &[String] { + &self.internal_test_files + } + pub fn from_project( go_project_path: &Path, packages: &[String], ) -> anyhow::Result> { + use std::collections::HashMap; + let mut raw_packages = Self::run_go_list(go_project_path, packages)?; // Sort packages by import path to ensure deterministic order raw_packages.sort_by(|a, b| a.import_path.cmp(&b.import_path)); + // Pre-pass: Find internal test helper files from ForTest packages that have no benchmarks. + // These are internal test files (package foo) that external tests (package foo_test) may depend on. + // Key: (dir, for_test) -> internal test files (TestGoFiles) + let mut helper_tests: HashMap<(PathBuf, String), Vec> = HashMap::new(); + for pkg in &raw_packages { + // Skip external test packages - we only want internal test packages + if pkg.is_external_test_package() { + continue; + } + + // Must have ForTest field (indicates this is a test package) + let Some(for_test) = &pkg.for_test else { + continue; + }; + + // Must be a test executable + if !pkg.import_path.ends_with(".test]") { + continue; + } + + // Must have internal test files (TestGoFiles) + let Some(test_go_files) = &pkg.test_go_files else { + continue; + }; + if test_go_files.is_empty() { + continue; + } + + // This is a helper-only internal test package - store its TestGoFiles + debug!( + "Found internal test helper files for {}: {:?}", + for_test, test_go_files + ); + helper_tests + .entry((pkg.dir.clone(), for_test.clone())) + .or_default() + .extend(test_go_files.clone()); + } + let mut packages = Vec::new(); for package in raw_packages { // Filter 1: Must have test files @@ -318,7 +378,20 @@ impl BenchmarkPackage { continue; } - packages.push(BenchmarkPackage::new(package, benchmarks)); + // For external test packages, attach any internal helper test files + let internal_helpers = if package.is_external_test_package() { + if let Some(for_test) = &package.for_test { + helper_tests + .get(&(package.dir.clone(), for_test.clone())) + .cloned() + .unwrap_or_default() + } else { + Vec::new() + } + } else { + Vec::new() + }; + packages.push(BenchmarkPackage::new(package, benchmarks, internal_helpers)); } Ok(packages) @@ -379,6 +452,7 @@ mod tests { #[case::example_with_dot_go_folder("example-with-dot-go-folder")] #[case::example_with_test_package("example-with-test-package")] #[case::example_with_excluded_names("example-with-excluded-names")] + #[case::example_external_test_unexported_access("example-external-test-unexported-access")] #[test_log::test] fn test_discover_benchmarks(#[case] project_name: &str) { let project_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")) diff --git a/go-runner/src/builder/snapshots/codspeed_go_runner__builder__discovery__tests__discover_benchmarks@example-external-test-unexported-access.snap b/go-runner/src/builder/snapshots/codspeed_go_runner__builder__discovery__tests__discover_benchmarks@example-external-test-unexported-access.snap new file mode 100644 index 00000000..3160c56a --- /dev/null +++ b/go-runner/src/builder/snapshots/codspeed_go_runner__builder__discovery__tests__discover_benchmarks@example-external-test-unexported-access.snap @@ -0,0 +1,43 @@ +--- +source: go-runner/src/builder/discovery.rs +expression: packages +--- +[ + { + "raw_package": "[raw_package]", + "benchmarks": [ + { + "name": "BenchmarkSomething", + "module_path": "example-external-test-unexported-access", + "import_alias": "benchmarksomething_16535781784731887632", + "qualified_name": "benchmarksomething_16535781784731887632.BenchmarkSomething", + "file_path": "internal_test.go", + "is_external": false + } + ] + }, + { + "raw_package": "[raw_package]", + "benchmarks": [ + { + "name": "BenchmarkPublicFunction", + "module_path": "example-external-test-unexported-access", + "import_alias": "benchmarkpublicfunction_16535781784731887632", + "qualified_name": "benchmarkpublicfunction_16535781784731887632.BenchmarkPublicFunction", + "file_path": "main_test.go", + "is_external": true + }, + { + "name": "BenchmarkPublicFunctionMultiple", + "module_path": "example-external-test-unexported-access", + "import_alias": "benchmarkpublicfunctionmultiple_16535781784731887632", + "qualified_name": "benchmarkpublicfunctionmultiple_16535781784731887632.BenchmarkPublicFunctionMultiple", + "file_path": "main_test.go", + "is_external": true + } + ], + "internal_test_files": [ + "internal_test.go" + ] + } +] diff --git a/go-runner/src/builder/snapshots/codspeed_go_runner__builder__discovery__tests__discover_benchmarks@example-with-test-package.snap b/go-runner/src/builder/snapshots/codspeed_go_runner__builder__discovery__tests__discover_benchmarks@example-with-test-package.snap index b0bf1a83..4dbc6a52 100644 --- a/go-runner/src/builder/snapshots/codspeed_go_runner__builder__discovery__tests__discover_benchmarks@example-with-test-package.snap +++ b/go-runner/src/builder/snapshots/codspeed_go_runner__builder__discovery__tests__discover_benchmarks@example-with-test-package.snap @@ -69,6 +69,9 @@ expression: packages "file_path": "fib_external/fib_integration_test.go", "is_external": true } + ], + "internal_test_files": [ + "fib_test.go" ] } ] diff --git a/go-runner/src/builder/snapshots/codspeed_go_runner__builder__discovery__tests__discover_benchmarks@hugo.snap b/go-runner/src/builder/snapshots/codspeed_go_runner__builder__discovery__tests__discover_benchmarks@hugo.snap index 1e27a141..d159f982 100644 --- a/go-runner/src/builder/snapshots/codspeed_go_runner__builder__discovery__tests__discover_benchmarks@hugo.snap +++ b/go-runner/src/builder/snapshots/codspeed_go_runner__builder__discovery__tests__discover_benchmarks@hugo.snap @@ -256,6 +256,9 @@ expression: packages "file_path": "helpers/url_test.go", "is_external": true } + ], + "internal_test_files": [ + "emoji_test.go" ] }, { @@ -428,6 +431,9 @@ expression: packages "file_path": "hugolib/doctree/nodeshiftree_test.go", "is_external": true } + ], + "internal_test_files": [ + "dimensions_test.go" ] }, { @@ -483,6 +489,10 @@ expression: packages "file_path": "identity/identity_test.go", "is_external": true } + ], + "internal_test_files": [ + "predicate_identity_test.go", + "question_test.go" ] }, { @@ -612,6 +622,9 @@ expression: packages "file_path": "markup/goldmark/goldmark_integration_test.go", "is_external": true } + ], + "internal_test_files": [ + "autoid_test.go" ] }, { @@ -785,6 +798,9 @@ expression: packages "file_path": "related/related_integration_test.go", "is_external": true } + ], + "internal_test_files": [ + "inverted_index_test.go" ] }, { @@ -819,6 +835,9 @@ expression: packages "file_path": "resources/image_test.go", "is_external": true } + ], + "internal_test_files": [ + "resource_test.go" ] }, { @@ -1327,6 +1346,9 @@ expression: packages "file_path": "tpl/tplimpl/templatestore_integration_test.go", "is_external": true } + ], + "internal_test_files": [ + "templatedescriptor_test.go" ] }, { diff --git a/go-runner/src/builder/snapshots/codspeed_go_runner__builder__discovery__tests__discover_benchmarks@opentelemetry-go.snap b/go-runner/src/builder/snapshots/codspeed_go_runner__builder__discovery__tests__discover_benchmarks@opentelemetry-go.snap index aaa5d298..57530df0 100644 --- a/go-runner/src/builder/snapshots/codspeed_go_runner__builder__discovery__tests__discover_benchmarks@opentelemetry-go.snap +++ b/go-runner/src/builder/snapshots/codspeed_go_runner__builder__discovery__tests__discover_benchmarks@opentelemetry-go.snap @@ -110,6 +110,9 @@ expression: packages "file_path": "attribute/set_test.go", "is_external": true } + ], + "internal_test_files": [ + "filter_test.go" ] }, { diff --git a/go-runner/src/builder/snapshots/codspeed_go_runner__builder__discovery__tests__discover_benchmarks@zap.snap b/go-runner/src/builder/snapshots/codspeed_go_runner__builder__discovery__tests__discover_benchmarks@zap.snap index 73a76c38..cbcb1412 100644 --- a/go-runner/src/builder/snapshots/codspeed_go_runner__builder__discovery__tests__discover_benchmarks@zap.snap +++ b/go-runner/src/builder/snapshots/codspeed_go_runner__builder__discovery__tests__discover_benchmarks@zap.snap @@ -354,6 +354,19 @@ expression: packages "file_path": "zapcore/tee_logger_bench_test.go", "is_external": true } + ], + "internal_test_files": [ + "buffered_write_syncer_bench_test.go", + "buffered_write_syncer_test.go", + "clock_test.go", + "entry_test.go", + "json_encoder_impl_test.go", + "leak_test.go", + "level_strings_test.go", + "level_test.go", + "memory_encoder_test.go", + "write_syncer_bench_test.go", + "write_syncer_test.go" ] }, { diff --git a/go-runner/src/builder/snapshots/codspeed_go_runner__builder__discovery__tests__discover_benchmarks@zerolog.snap b/go-runner/src/builder/snapshots/codspeed_go_runner__builder__discovery__tests__discover_benchmarks@zerolog.snap index 35866cfd..504e7044 100644 --- a/go-runner/src/builder/snapshots/codspeed_go_runner__builder__discovery__tests__discover_benchmarks@zerolog.snap +++ b/go-runner/src/builder/snapshots/codspeed_go_runner__builder__discovery__tests__discover_benchmarks@zerolog.snap @@ -107,6 +107,17 @@ expression: packages "file_path": "console_test.go", "is_external": true } + ], + "internal_test_files": [ + "array_test.go", + "benchmark_test.go", + "ctx_test.go", + "event_test.go", + "hook_test.go", + "log_test.go", + "sampler_test.go", + "syslog_test.go", + "writer_test.go" ] }, { diff --git a/go-runner/src/builder/templater.rs b/go-runner/src/builder/templater.rs index b7e81e25..d763fa37 100644 --- a/go-runner/src/builder/templater.rs +++ b/go-runner/src/builder/templater.rs @@ -117,6 +117,20 @@ impl CodspeedContext { // They're now package main and will be built from the subdirectory self.patcher .rename_and_move_test_files(&test_file_paths, &codspeed_dir)?; + + // Also rename internal test files in place so they are accessible in + // the tests during `go build`. This allows external tests to call + // functions defined in internal test files (e.g., mylib.SetTestState()). + let internal_test_paths: Vec = package + .internal_test_files() + .iter() + .map(|f| package_path.join(f)) + .collect(); + self.patcher.rename_test_files(&internal_test_paths)?; + info!( + "Renamed {} internal test files for external test package", + internal_test_paths.len() + ); } else { // For internal test packages: rename _test.go to _codspeed.go in place self.patcher.rename_test_files(&test_file_paths)?; diff --git a/go-runner/src/integration_tests.rs b/go-runner/src/integration_tests.rs index fd02a6d2..f0b357cf 100644 --- a/go-runner/src/integration_tests.rs +++ b/go-runner/src/integration_tests.rs @@ -84,6 +84,7 @@ fn assert_results_snapshots(profile_dir: &Path, project_name: &str) { #[case::example_with_replace("example-with-replace")] #[case::example_with_excluded_names("example-with-excluded-names")] #[case::example_logr_type_mismatch("example-logr-type-mismatch")] +#[case::example_external_test_unexported_access("example-external-test-unexported-access")] #[test_log::test] fn test_build_and_run(#[case] project_name: &str) { let project_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")) diff --git a/go-runner/src/snapshots/codspeed_go_runner__integration_tests__assert_results_snapshots@example-external-test-unexported-access.snap b/go-runner/src/snapshots/codspeed_go_runner__integration_tests__assert_results_snapshots@example-external-test-unexported-access.snap new file mode 100644 index 00000000..e057013c --- /dev/null +++ b/go-runner/src/snapshots/codspeed_go_runner__integration_tests__assert_results_snapshots@example-external-test-unexported-access.snap @@ -0,0 +1,63 @@ +--- +source: go-runner/src/integration_tests.rs +expression: results +--- +[ + { + "creator": { + "name": "codspeed-go", + "version": "[version]", + "pid": "[pid]" + }, + "instrument": { + "type": "walltime" + }, + "benchmarks": [ + { + "name": "BenchmarkPublicFunction", + "uri": "go-runner/testdata/projects/example-external-test-unexported-access/main_test.go::BenchmarkPublicFunction", + "config": { + "warmup_time_ns": null, + "min_round_time_ns": null, + "max_time_ns": null, + "max_rounds": null + }, + "stats": "[stats]" + }, + { + "name": "BenchmarkPublicFunctionMultiple", + "uri": "go-runner/testdata/projects/example-external-test-unexported-access/main_test.go::BenchmarkPublicFunctionMultiple", + "config": { + "warmup_time_ns": null, + "min_round_time_ns": null, + "max_time_ns": null, + "max_rounds": null + }, + "stats": "[stats]" + } + ] + }, + { + "creator": { + "name": "codspeed-go", + "version": "[version]", + "pid": "[pid]" + }, + "instrument": { + "type": "walltime" + }, + "benchmarks": [ + { + "name": "BenchmarkSomething::test", + "uri": "go-runner/testdata/projects/example-external-test-unexported-access/internal_test.go::BenchmarkSomething::test", + "config": { + "warmup_time_ns": null, + "min_round_time_ns": null, + "max_time_ns": null, + "max_rounds": null + }, + "stats": "[stats]" + } + ] + } +] diff --git a/go-runner/testdata/projects/example-external-test-unexported-access/README.md b/go-runner/testdata/projects/example-external-test-unexported-access/README.md new file mode 100644 index 00000000..48263953 --- /dev/null +++ b/go-runner/testdata/projects/example-external-test-unexported-access/README.md @@ -0,0 +1,101 @@ +# Example: External Test Package Calling Internal Test Functions + +## The Issue + +When running benchmarks in an external test package (`package foo_test`) that calls functions defined in internal test files (`foo_test.go` in `package foo`), the codspeed-go runner fails with compilation errors like: + +``` +Error: codspeed/main_test_codspeed.go:XX:YY: undefined: mylib.SetTestState +Error: codspeed/main_test_codspeed.go:XX:YY: undefined: mylib.GetTestState +``` + +### Why This Happens + +In Go's testing architecture: + +1. **Internal Test Files** (`foo_test.go` in `package foo`): + - Part of the same package as source code + - Can define test-only helper functions and variables + - Only compiled when building with `go test`, not with `go build` + +2. **External Test Files** (`foo_test.go` in `package foo_test`): + - Separate package for black-box testing + - Can call functions from both the package's source AND internal test files + - Only available during `go test` builds + +3. **codspeed-go's Problem**: + - The runner renames internal test files from `*_test.go` to `*_codspeed.go` + - It then uses `go build` (not `go test`) to compile + - When `go build` compiles, the original `*_test.go` files are ignored + - The renamed `*_codspeed.go` files are compiled, but they're now regular files with test code + - External test files that reference functions defined only in internal test files fail + +### Example Structure + +``` +mylib/ +├── main.go # package mylib - main implementation +├── internal_test.go # package mylib - internal test helpers (SetTestState, GetTestState) +├── main_test.go # package mylib_test - external test benchmarks +├── go.mod # Module definition +└── README.md # This file +``` + +The `internal_test.go` file: +- Declares `package mylib` (internal test) +- Defines helper functions: `SetTestState()`, `GetTestState()` +- These are only available when `go test` is used + +The `main_test.go` file: +- Declares `package mylib_test` (external test package) +- Calls `mylib.SetTestState()` and `mylib.GetTestState()` in helper functions +- Has benchmarks that use these helpers +- Works with `go test` because internal test functions are compiled +- Fails with codspeed-go because those functions aren't compiled with `go build` + +## How to Test + +### Standard Go (works fine): +```bash +cd go-runner/testdata/projects/example-external-test-unexported-access +go test -bench=. +``` + +Expected behavior: Both benchmarks run successfully. + +### CodSpeed Go Runner (currently fails): +```bash +cd go-runner +cargo run -- test -bench=. testdata/projects/example-external-test-unexported-access +``` + +Expected error: +``` +Error: codspeed/main_codspeed.go:XX:YY: undefined: mylib.SetTestState +Error: codspeed/main_codspeed.go:XX:YY: undefined: mylib.GetTestState +``` + +## Root Cause Analysis + +1. **Discovery** finds external test package (`package mylib_test`) in `main_test.go` +2. **Patcher** also processes internal test file `internal_test.go` (renames to `internal_codspeed.go`) +3. **Templater** moves external test file to `codspeed/main_test_codspeed.go` +4. **Build** uses `go build` which: + - Compiles `internal_codspeed.go` (renamed internal test file) as regular code + - Compiles `codspeed/main_test_codspeed.go` (external test moved to subdirectory) + - BUT: The internal test code functions are now in the parent package (`mylib`), not in `package mylib`'s test namespace + - The external test now can't access these functions because they're in a subdirectory and the package namespace is confused + +## Real-World Impact + +This affects projects like OpenTelemetry Go (and others) that have: +- Internal test files defining mock/setup functions (e.g., `SetHostIDProvider`) +- External test files in separate packages that call those mock functions +- Benchmark functions in the external test files + +## Potential Solutions + +1. **Keep internal tests as `*_test.go`**: Don't rename internal test files - find a way to compile test code differently +2. **Merge test setup into main code**: Move internal test helper functions to main package (not ideal) +3. **Use build tags**: Use `//go:build codspeed` to conditionally include test helpers when building with codspeed-go +4. **Generate wrapper code**: Create wrapper functions in the runner that provide the test helpers diff --git a/go-runner/testdata/projects/example-external-test-unexported-access/go.mod b/go-runner/testdata/projects/example-external-test-unexported-access/go.mod new file mode 100644 index 00000000..b26e5d32 --- /dev/null +++ b/go-runner/testdata/projects/example-external-test-unexported-access/go.mod @@ -0,0 +1,3 @@ +module example-external-test-unexported-access + +go 1.24.3 diff --git a/go-runner/testdata/projects/example-external-test-unexported-access/internal_test.go b/go-runner/testdata/projects/example-external-test-unexported-access/internal_test.go new file mode 100644 index 00000000..5713f234 --- /dev/null +++ b/go-runner/testdata/projects/example-external-test-unexported-access/internal_test.go @@ -0,0 +1,30 @@ +package mylib + +import ( + "testing" +) + +// state is unexported and only accessible to test files +var testState = 0 + +// SetTestState is an unexported function defined in internal test file +// This function is only available during test builds +func SetTestState(value int) { + testState = value +} + +// GetTestState is an unexported function defined in internal test file +func GetTestState() int { + return testState +} + +// InternalTestHelper is another test helper in the internal test file +func InternalTestHelper(b *testing.B, value int) { + SetTestState(value) +} + +func BenchmarkSomething(b *testing.B) { + b.Run("test", func(b *testing.B) { + InternalTestHelper(b, 10) + }) +} diff --git a/go-runner/testdata/projects/example-external-test-unexported-access/main.go b/go-runner/testdata/projects/example-external-test-unexported-access/main.go new file mode 100644 index 00000000..f666a486 --- /dev/null +++ b/go-runner/testdata/projects/example-external-test-unexported-access/main.go @@ -0,0 +1,6 @@ +package mylib + +// PublicFunction is exported +func PublicFunction(value int) int { + return value * 2 +} diff --git a/go-runner/testdata/projects/example-external-test-unexported-access/main_test.go b/go-runner/testdata/projects/example-external-test-unexported-access/main_test.go new file mode 100644 index 00000000..b7d74b8e --- /dev/null +++ b/go-runner/testdata/projects/example-external-test-unexported-access/main_test.go @@ -0,0 +1,45 @@ +package mylib_test + +import ( + "testing" + + "example-external-test-unexported-access" +) + +// setupTest is a helper function in the external test package +// that calls unexported functions from the mylib internal test file +func setupTest(value int) { + mylib.SetTestState(value) +} + +// getTestState is a helper function that reads unexported state from internal test +func getTestState() int { + return mylib.GetTestState() +} + +// cleanupTest is a helper that resets state using internal test function +func cleanupTest() { + mylib.SetTestState(0) +} + +func BenchmarkPublicFunction(b *testing.B) { + setupTest(5) + defer cleanupTest() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + result := mylib.PublicFunction(getTestState()) + _ = result + } +} + +func BenchmarkPublicFunctionMultiple(b *testing.B) { + for j := 1; j <= 10; j++ { + setupTest(j) + for i := 0; i < b.N; i++ { + result := mylib.PublicFunction(j) + _ = result + } + } + cleanupTest() +} From 9cffdb9cb14557e6142b2f6309cfb21c1efbcff2 Mon Sep 17 00:00:00 2001 From: not-matthias Date: Wed, 7 Jan 2026 16:29:19 +0100 Subject: [PATCH 08/11] chore: fork testify --- go.mod | 4 + go.sum | 10 + pkg/fork.sh | 9 + pkg/logr/testr/testr_fuzz_test.go | 2 +- pkg/testify/.ci.gofmt.sh | 14 + pkg/testify/.ci.gogenerate.sh | 16 + pkg/testify/.ci.govet.sh | 5 + pkg/testify/.ci.readme.fmt.sh | 12 + .../.github/ISSUE_TEMPLATE/bug_report.md | 23 + .../.github/ISSUE_TEMPLATE/feature_request.md | 20 + pkg/testify/.github/dependabot.yml | 10 + pkg/testify/.github/pull_request_template.md | 15 + pkg/testify/.github/workflows/main.yml | 41 + pkg/testify/.github/workflows/release.yml | 21 + pkg/testify/.gitignore | 30 + pkg/testify/.mdsf.json | 12 + pkg/testify/CONTRIBUTING.md | 49 + pkg/testify/EMERITUS.md | 12 + pkg/testify/LICENSE | 21 + pkg/testify/MAINTAINERS.md | 17 + pkg/testify/README.md | 366 ++ pkg/testify/_codegen/.gitignore | 1 + pkg/testify/_codegen/go.mod | 8 + pkg/testify/_codegen/go.sum | 21 + pkg/testify/_codegen/main.go | 315 ++ pkg/testify/assert/assertion_compare.go | 495 +++ pkg/testify/assert/assertion_compare_test.go | 494 +++ pkg/testify/assert/assertion_format.go | 866 ++++ pkg/testify/assert/assertion_format.go.tmpl | 5 + pkg/testify/assert/assertion_forward.go | 1723 ++++++++ pkg/testify/assert/assertion_forward.go.tmpl | 5 + pkg/testify/assert/assertion_order.go | 81 + pkg/testify/assert/assertion_order_test.go | 213 + pkg/testify/assert/assertions.go | 2295 ++++++++++ pkg/testify/assert/assertions_test.go | 3917 +++++++++++++++++ pkg/testify/assert/doc.go | 50 + pkg/testify/assert/errors.go | 10 + pkg/testify/assert/forward_assertions.go | 16 + pkg/testify/assert/forward_assertions_test.go | 843 ++++ pkg/testify/assert/http_assertions.go | 165 + pkg/testify/assert/http_assertions_test.go | 232 + .../assert/internal/unsafetests/doc.go | 4 + .../internal/unsafetests/unsafetests_test.go | 34 + pkg/testify/assert/yaml/yaml_custom.go | 24 + pkg/testify/assert/yaml/yaml_default.go | 36 + pkg/testify/assert/yaml/yaml_fail.go | 17 + pkg/testify/doc.go | 17 + pkg/testify/http/doc.go | 2 + pkg/testify/http/test_response_writer.go | 49 + pkg/testify/http/test_round_tripper.go | 18 + pkg/testify/mock/doc.go | 44 + pkg/testify/mock/mock.go | 1298 ++++++ pkg/testify/mock/mock_test.go | 2443 ++++++++++ pkg/testify/package_test.go | 13 + pkg/testify/require/doc.go | 31 + pkg/testify/require/forward_requirements.go | 16 + .../require/forward_requirements_test.go | 623 +++ pkg/testify/require/require.go | 2180 +++++++++ pkg/testify/require/require.go.tmpl | 6 + pkg/testify/require/require_forward.go | 1724 ++++++++ pkg/testify/require/require_forward.go.tmpl | 5 + pkg/testify/require/requirements.go | 29 + pkg/testify/require/requirements_test.go | 790 ++++ pkg/testify/suite/doc.go | 70 + pkg/testify/suite/interfaces.go | 66 + pkg/testify/suite/stats.go | 50 + pkg/testify/suite/stats_test.go | 40 + pkg/testify/suite/suite.go | 237 + pkg/testify/suite/suite_test.go | 753 ++++ 69 files changed, 23082 insertions(+), 1 deletion(-) create mode 100755 pkg/testify/.ci.gofmt.sh create mode 100755 pkg/testify/.ci.gogenerate.sh create mode 100755 pkg/testify/.ci.govet.sh create mode 100755 pkg/testify/.ci.readme.fmt.sh create mode 100644 pkg/testify/.github/ISSUE_TEMPLATE/bug_report.md create mode 100644 pkg/testify/.github/ISSUE_TEMPLATE/feature_request.md create mode 100644 pkg/testify/.github/dependabot.yml create mode 100644 pkg/testify/.github/pull_request_template.md create mode 100644 pkg/testify/.github/workflows/main.yml create mode 100644 pkg/testify/.github/workflows/release.yml create mode 100644 pkg/testify/.gitignore create mode 100644 pkg/testify/.mdsf.json create mode 100644 pkg/testify/CONTRIBUTING.md create mode 100644 pkg/testify/EMERITUS.md create mode 100644 pkg/testify/LICENSE create mode 100644 pkg/testify/MAINTAINERS.md create mode 100644 pkg/testify/README.md create mode 100644 pkg/testify/_codegen/.gitignore create mode 100644 pkg/testify/_codegen/go.mod create mode 100644 pkg/testify/_codegen/go.sum create mode 100644 pkg/testify/_codegen/main.go create mode 100644 pkg/testify/assert/assertion_compare.go create mode 100644 pkg/testify/assert/assertion_compare_test.go create mode 100644 pkg/testify/assert/assertion_format.go create mode 100644 pkg/testify/assert/assertion_format.go.tmpl create mode 100644 pkg/testify/assert/assertion_forward.go create mode 100644 pkg/testify/assert/assertion_forward.go.tmpl create mode 100644 pkg/testify/assert/assertion_order.go create mode 100644 pkg/testify/assert/assertion_order_test.go create mode 100644 pkg/testify/assert/assertions.go create mode 100644 pkg/testify/assert/assertions_test.go create mode 100644 pkg/testify/assert/doc.go create mode 100644 pkg/testify/assert/errors.go create mode 100644 pkg/testify/assert/forward_assertions.go create mode 100644 pkg/testify/assert/forward_assertions_test.go create mode 100644 pkg/testify/assert/http_assertions.go create mode 100644 pkg/testify/assert/http_assertions_test.go create mode 100644 pkg/testify/assert/internal/unsafetests/doc.go create mode 100644 pkg/testify/assert/internal/unsafetests/unsafetests_test.go create mode 100644 pkg/testify/assert/yaml/yaml_custom.go create mode 100644 pkg/testify/assert/yaml/yaml_default.go create mode 100644 pkg/testify/assert/yaml/yaml_fail.go create mode 100644 pkg/testify/doc.go create mode 100644 pkg/testify/http/doc.go create mode 100644 pkg/testify/http/test_response_writer.go create mode 100644 pkg/testify/http/test_round_tripper.go create mode 100644 pkg/testify/mock/doc.go create mode 100644 pkg/testify/mock/mock.go create mode 100644 pkg/testify/mock/mock_test.go create mode 100644 pkg/testify/package_test.go create mode 100644 pkg/testify/require/doc.go create mode 100644 pkg/testify/require/forward_requirements.go create mode 100644 pkg/testify/require/forward_requirements_test.go create mode 100644 pkg/testify/require/require.go create mode 100644 pkg/testify/require/require.go.tmpl create mode 100644 pkg/testify/require/require_forward.go create mode 100644 pkg/testify/require/require_forward.go.tmpl create mode 100644 pkg/testify/require/requirements.go create mode 100644 pkg/testify/require/requirements_test.go create mode 100644 pkg/testify/suite/doc.go create mode 100644 pkg/testify/suite/interfaces.go create mode 100644 pkg/testify/suite/stats.go create mode 100644 pkg/testify/suite/stats_test.go create mode 100644 pkg/testify/suite/suite.go create mode 100644 pkg/testify/suite/suite_test.go diff --git a/go.mod b/go.mod index 74556f70..0a25fbab 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,12 @@ module github.com/CodSpeedHQ/codspeed-go go 1.24.3 require ( + github.com/davecgh/go-spew v1.1.1 github.com/google/go-cmp v0.7.0 github.com/kr/pretty v0.3.1 + github.com/pmezard/go-difflib v1.0.0 + github.com/stretchr/objx v0.5.3 + gopkg.in/yaml.v3 v3.0.1 ) require ( diff --git a/go.sum b/go.sum index faf8f26a..9541d377 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,6 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -6,5 +8,13 @@ github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3x github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= +github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/fork.sh b/pkg/fork.sh index 42ced4da..1953c616 100755 --- a/pkg/fork.sh +++ b/pkg/fork.sh @@ -15,6 +15,9 @@ function fix_project() { find . -type f -name "*.go" -exec sed -i 's/^[[:space:]]*"testing")/testing "github.com\/CodSpeedHQ\/codspeed-go\/testing\/testing")/g' {} + find . -type f -name "*.go" -exec sed -i 's/^[[:space:]]*"testing",/testing "github.com\/CodSpeedHQ\/codspeed-go\/testing\/testing",/g' {} + + # Handle single import statements: import "testing" + find . -type f -name "*.go" -exec sed -i 's/^import "testing"$/import testing "github.com\/CodSpeedHQ\/codspeed-go\/testing\/testing"/g' {} + + find . -type f -name "*.go" -exec sed -i 's/^[[:space:]]*"testing\/slogtest"$/slogtest "github.com\/CodSpeedHQ\/codspeed-go\/testing\/testing\/slogtest"/g' {} + find . -type f -name "*.go" -exec sed -i 's/^[[:space:]]*"testing\/slogtest")/slogtest "github.com\/CodSpeedHQ\/codspeed-go\/testing\/testing\/slogtest")/g' {} + find . -type f -name "*.go" -exec sed -i 's/^[[:space:]]*"testing\/slogtest",/slogtest "github.com\/CodSpeedHQ\/codspeed-go\/testing\/testing\/slogtest",/g' {} + @@ -48,3 +51,9 @@ replace_in_go_files '"github.com/go-logr/stdr' '"github.com/CodSpeedHQ/codspeed- replace_in_go_files '"github.com/go-logr/logr' '"github.com/CodSpeedHQ/codspeed-go/pkg/logr' fix_project popd + +git clone -b v1.11.1 https://github.com/stretchr/testify.git +pushd testify +replace_in_go_files '"github.com/stretchr/testify' '"github.com/CodSpeedHQ/codspeed-go/pkg/testify' +fix_project +popd diff --git a/pkg/logr/testr/testr_fuzz_test.go b/pkg/logr/testr/testr_fuzz_test.go index ea5217d2..df651c08 100644 --- a/pkg/logr/testr/testr_fuzz_test.go +++ b/pkg/logr/testr/testr_fuzz_test.go @@ -19,7 +19,7 @@ limitations under the License. package testr -import "testing" +import testing "github.com/CodSpeedHQ/codspeed-go/testing/testing" func TestLoggerTestingF(_ *testing.T) { f := &testing.F{} diff --git a/pkg/testify/.ci.gofmt.sh b/pkg/testify/.ci.gofmt.sh new file mode 100755 index 00000000..1ac21ef1 --- /dev/null +++ b/pkg/testify/.ci.gofmt.sh @@ -0,0 +1,14 @@ +#!/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/pkg/testify/.ci.gogenerate.sh b/pkg/testify/.ci.gogenerate.sh new file mode 100755 index 00000000..5b564209 --- /dev/null +++ b/pkg/testify/.ci.gogenerate.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +# If GOMOD is defined we are running with Go Modules enabled, either +# automatically or via the GO111MODULE=on environment variable. Codegen only +# works with modules, so skip generation if modules is not in use. +if [[ -z "$(go env GOMOD)" ]]; then + echo "Skipping go generate because modules not enabled and required" + exit 0 +fi + +go generate ./... +if [ -n "$(git diff)" ]; then + echo "Go generate had not been run" + git diff + exit 1 +fi diff --git a/pkg/testify/.ci.govet.sh b/pkg/testify/.ci.govet.sh new file mode 100755 index 00000000..9bdf4519 --- /dev/null +++ b/pkg/testify/.ci.govet.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +set -e + +go vet ./... diff --git a/pkg/testify/.ci.readme.fmt.sh b/pkg/testify/.ci.readme.fmt.sh new file mode 100755 index 00000000..b3d6a1d0 --- /dev/null +++ b/pkg/testify/.ci.readme.fmt.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +# Verify that the code snippets in README.md are formatted. +# The tool https://github.com/hougesen/mdsf is used. + +if [ -n "$(mdsf verify --config .mdsf.json --log-level error README.md 2>&1)" ]; then + echo "Go code in the README.md is not formatted." + echo "Did you forget to run 'mdsf format --config .mdsf.json README.md'?" + mdsf format --config .mdsf.json README.md + git diff + exit 1 +fi diff --git a/pkg/testify/.github/ISSUE_TEMPLATE/bug_report.md b/pkg/testify/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..374aa3c3 --- /dev/null +++ b/pkg/testify/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,23 @@ +--- +name: Bug report +about: Format to report a bug +title: '' +labels: bug +assignees: '' + +--- + + + + +## Description + + +## Step To Reproduce + + +## Expected behavior + + +## Actual behavior + diff --git a/pkg/testify/.github/ISSUE_TEMPLATE/feature_request.md b/pkg/testify/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..2cb8bebb --- /dev/null +++ b/pkg/testify/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Propose a new feature +title: '' +labels: enhancement +assignees: '' + +--- + + + + +## Description + + +## Proposed solution + + +## Use case + diff --git a/pkg/testify/.github/dependabot.yml b/pkg/testify/.github/dependabot.yml new file mode 100644 index 00000000..bf6944e7 --- /dev/null +++ b/pkg/testify/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: +- package-ecosystem: gomod + directory: / + schedule: + interval: daily +- package-ecosystem: github-actions + directory: / + schedule: + interval: daily diff --git a/pkg/testify/.github/pull_request_template.md b/pkg/testify/.github/pull_request_template.md new file mode 100644 index 00000000..ba09b9b3 --- /dev/null +++ b/pkg/testify/.github/pull_request_template.md @@ -0,0 +1,15 @@ +## Summary + + +## Changes + + + + +## Motivation + + + + +## Related issues + diff --git a/pkg/testify/.github/workflows/main.yml b/pkg/testify/.github/workflows/main.yml new file mode 100644 index 00000000..4c39ac04 --- /dev/null +++ b/pkg/testify/.github/workflows/main.yml @@ -0,0 +1,41 @@ +name: All builds +on: [push, pull_request] + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + go_version: + - stable + - oldstable + steps: + - uses: actions/checkout@v5 + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.go_version }} + - run: npm install -g mdsf-cli + - run: ./.ci.gogenerate.sh + - run: ./.ci.gofmt.sh + - run: ./.ci.readme.fmt.sh + - run: ./.ci.govet.sh + - run: go test -v -race ./... + test: + runs-on: ubuntu-latest + strategy: + matrix: + go_version: + - "1.17" + - "1.18" + - "1.19" + - "1.20" + - "1.21" + - "1.22" + steps: + - uses: actions/checkout@v5 + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.go_version }} + - run: go test -v -race ./... diff --git a/pkg/testify/.github/workflows/release.yml b/pkg/testify/.github/workflows/release.yml new file mode 100644 index 00000000..c094648b --- /dev/null +++ b/pkg/testify/.github/workflows/release.yml @@ -0,0 +1,21 @@ +name: Create release from new tag + +# this flow will be run only when new tags are pushed that match our pattern +on: + push: + tags: + - "v[0-9]+.[0-9]+.[0-9]+" + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Create GitHub release from tag + uses: softprops/action-gh-release@v2 + with: + generate_release_notes: true diff --git a/pkg/testify/.gitignore b/pkg/testify/.gitignore new file mode 100644 index 00000000..6e1bb22a --- /dev/null +++ b/pkg/testify/.gitignore @@ -0,0 +1,30 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe + +.DS_Store + +# Output of "go test -c" +/assert/assert.test +/require/require.test +/suite/suite.test +/mock/mock.test diff --git a/pkg/testify/.mdsf.json b/pkg/testify/.mdsf.json new file mode 100644 index 00000000..884bbbeb --- /dev/null +++ b/pkg/testify/.mdsf.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://raw.githubusercontent.com/hougesen/mdsf/main/schemas/v0.8.2/mdsf.schema.json", + "format_finished_document": false, + "languages": { + "go": [ + [ + "gofmt", + "goimports" + ] + ] + } +} diff --git a/pkg/testify/CONTRIBUTING.md b/pkg/testify/CONTRIBUTING.md new file mode 100644 index 00000000..b1ca0533 --- /dev/null +++ b/pkg/testify/CONTRIBUTING.md @@ -0,0 +1,49 @@ +# Contributing to Testify + +So you'd like to contribute to Testify? First of all, thank you! Testify is widely used, so each +contribution has a significant impact within the Golang community! Below you'll find everything you +need to know to get up to speed on the project. + +## Philosophy + +The Testify maintainers generally attempt to follow widely accepted practices within the Golang +community. That being said, the first priority is always to make sure that the package is useful to +the community. A few general guidelines are listed here: + +*Keep it simple (whenever practical)* - Try not to expand the API unless the new surface area +provides meaningful benefits. For example, don't add functions because they might be useful to +someone, someday. Add what is useful to specific users, today. + +*Ease of use is paramount* - This means good documentation and package organization. It also means +that we should try hard to use meaningful, descriptive function names, avoid breaking the API +unnecessarily, and try not to surprise the user. + +*Quality isn't an afterthought* - Testify is a testing library, so it seems reasonable that we +should have a decent test suite. This is doubly important because a bug in Testify doesn't just mean +a bug in our users' code, it means a bug in our users' tests, which means a potentially unnoticed +and hard-to-find bug in our users' code. + +## Pull Requests + +We welcome pull requests! Please include the following in the description: + + * Motivation, why your change is important or helpful + * Example usage (if applicable) + * Whether you intend to add / change behavior or fix a bug + +Please be aware that the maintainers may ask for changes. This isn't a commentary on the quality of +your idea or your code. Testify is the result of many contributions from many individuals, so we +need to enforce certain practices and patterns to keep the package easy for others to understand. +Essentially, we recognize that there are often many good ways to do a given thing, but we have to +pick one and stick with it. + +See `MAINTAINERS.md` for a list of users who can approve / merge your changes. + +## Issues + +If you find a bug or think of a useful feature you'd like to see added to Testify, the best thing +you can do is make the necessary changes and open a pull request (see above). If that isn't an +option, or if you'd like to discuss your change before you write the code, open an issue! + +Please provide enough context in the issue description that other members of the community can +easily understand what it is that you'd like to see. diff --git a/pkg/testify/EMERITUS.md b/pkg/testify/EMERITUS.md new file mode 100644 index 00000000..70982b46 --- /dev/null +++ b/pkg/testify/EMERITUS.md @@ -0,0 +1,12 @@ +# Emeritus + +We would like to acknowledge previous testify maintainers and their huge contributions to our collective success: + + * @matryer + * @glesica + * @ernesto-jimenez + * @mvdkleijn + * @georgelesica-wf + * @bencampbell-wf + +We thank these members for their service to this community. diff --git a/pkg/testify/LICENSE b/pkg/testify/LICENSE new file mode 100644 index 00000000..4b0421cf --- /dev/null +++ b/pkg/testify/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2012-2020 Mat Ryer, Tyler Bunnell and contributors. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/pkg/testify/MAINTAINERS.md b/pkg/testify/MAINTAINERS.md new file mode 100644 index 00000000..120c4639 --- /dev/null +++ b/pkg/testify/MAINTAINERS.md @@ -0,0 +1,17 @@ +# Testify Maintainers + +The individuals listed below are active in the project and have the ability to approve and merge +pull requests. + + * @boyan-soubachov + * @dolmen + * @MovieStoreGuy + * @brackendawson + +## Approvers + +The individuals listed below are active in the project and have the ability to approve pull +requests. + + * @arjunmahishi + * @ccoVeille diff --git a/pkg/testify/README.md b/pkg/testify/README.md new file mode 100644 index 00000000..f7a839d3 --- /dev/null +++ b/pkg/testify/README.md @@ -0,0 +1,366 @@ +Testify - Thou Shalt Write Tests +================================ + +> [!NOTE] +> Testify is being maintained at v1, no breaking changes will be accepted in this repo. +> [See discussion about v2](https://github.com/stretchr/testify/discussions/1560). + +[![Build Status](https://github.com/stretchr/testify/actions/workflows/main.yml/badge.svg?branch=master)](https://github.com/stretchr/testify/actions/workflows/main.yml) [![Go Report Card](https://goreportcard.com/badge/github.com/stretchr/testify)](https://goreportcard.com/report/github.com/stretchr/testify) [![PkgGoDev](https://pkg.go.dev/badge/github.com/stretchr/testify)](https://pkg.go.dev/github.com/stretchr/testify) + +Go code (golang) set of packages that provide many tools for testifying that your code will behave as you intend. + +Features include: + + * [Easy assertions](#assert-package) + * [Mocking](#mock-package) + * [Testing suite interfaces and functions](#suite-package) + +Get started: + + * Install testify with [one line of code](#installation), or [update it with another](#staying-up-to-date) + * For an introduction to writing test code in Go, see https://go.dev/doc/code#Testing + * Check out the API Documentation https://pkg.go.dev/github.com/stretchr/testify + * Use [testifylint](https://github.com/Antonboom/testifylint) (via [golangci-lint](https://golangci-lint.run/)) to avoid common mistakes + * A little about [Test-Driven Development (TDD)](https://en.wikipedia.org/wiki/Test-driven_development) + +[`assert`](https://pkg.go.dev/github.com/stretchr/testify/assert "API documentation") package +------------------------------------------------------------------------------------------- + +The `assert` package provides some helpful methods that allow you to write better test code in Go. + + * Prints friendly, easy to read failure descriptions + * Allows for very readable code + * Optionally annotate each assertion with a message + +See it in action: + +```go +package yours + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSomething(t *testing.T) { + // assert equality + assert.Equal(t, 123, 123, "they should be equal") + + // assert inequality + assert.NotEqual(t, 123, 456, "they should not be equal") + + // assert for nil (good for errors) + assert.Nil(t, object) + + // assert for not nil (good when you expect something) + if assert.NotNil(t, object) { + // now we know that object isn't nil, we are safe to make + // further assertions without causing any errors + assert.Equal(t, "Something", object.Value) + } +} +``` + + * Every assert func takes the `testing.T` object as the first argument. This is how it writes the errors out through the normal `go test` capabilities. + * Every assert func returns a bool indicating whether the assertion was successful or not, this is useful for if you want to go on making further assertions under certain conditions. + +if you assert many times, use the below: + +```go +package yours + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSomething(t *testing.T) { + assert := assert.New(t) + + // assert equality + assert.Equal(123, 123, "they should be equal") + + // assert inequality + assert.NotEqual(123, 456, "they should not be equal") + + // assert for nil (good for errors) + assert.Nil(object) + + // assert for not nil (good when you expect something) + if assert.NotNil(object) { + // now we know that object isn't nil, we are safe to make + // further assertions without causing any errors + assert.Equal("Something", object.Value) + } +} +``` + +[`require`](https://pkg.go.dev/github.com/stretchr/testify/require "API documentation") package +--------------------------------------------------------------------------------------------- + +The `require` package provides same global functions as the `assert` package, but instead of returning a boolean result they terminate current test. +These functions must be called from the goroutine running the test or benchmark function, not from other goroutines created during the test. +Otherwise race conditions may occur. + +See [t.FailNow](https://pkg.go.dev/testing#T.FailNow) for details. + +[`mock`](https://pkg.go.dev/github.com/stretchr/testify/mock "API documentation") package +---------------------------------------------------------------------------------------- + +The `mock` package provides a mechanism for easily writing mock objects that can be used in place of real objects when writing test code. + +An example test function that tests a piece of code that relies on an external object `testObj`, can set up expectations (testify) and assert that they indeed happened: + +```go +package yours + +import ( + "testing" + + "github.com/stretchr/testify/mock" +) + +/* + Test objects +*/ + +// MyMockedObject is a mocked object that implements an interface +// that describes an object that the code I am testing relies on. +type MyMockedObject struct { + mock.Mock +} + +// DoSomething is a method on MyMockedObject that implements some interface +// and just records the activity, and returns what the Mock object tells it to. +// +// In the real object, this method would do something useful, but since this +// is a mocked object - we're just going to stub it out. +// +// NOTE: This method is not being tested here, code that uses this object is. +func (m *MyMockedObject) DoSomething(number int) (bool, error) { + args := m.Called(number) + return args.Bool(0), args.Error(1) +} + +/* + Actual test functions +*/ + +// TestSomething is an example of how to use our test object to +// make assertions about some target code we are testing. +func TestSomething(t *testing.T) { + // create an instance of our test object + testObj := new(MyMockedObject) + + // set up expectations + testObj.On("DoSomething", 123).Return(true, nil) + + // call the code we are testing + targetFuncThatDoesSomethingWithObj(testObj) + + // assert that the expectations were met + testObj.AssertExpectations(t) +} + +// TestSomethingWithPlaceholder is a second example of how to use our test object to +// make assertions about some target code we are testing. +// This time using a placeholder. Placeholders might be used when the +// data being passed in is normally dynamically generated and cannot be +// predicted beforehand (eg. containing hashes that are time sensitive) +func TestSomethingWithPlaceholder(t *testing.T) { + // create an instance of our test object + testObj := new(MyMockedObject) + + // set up expectations with a placeholder in the argument list + testObj.On("DoSomething", mock.Anything).Return(true, nil) + + // call the code we are testing + targetFuncThatDoesSomethingWithObj(testObj) + + // assert that the expectations were met + testObj.AssertExpectations(t) + +} + +// TestSomethingElse2 is a third example that shows how you can use +// the Unset method to cleanup handlers and then add new ones. +func TestSomethingElse2(t *testing.T) { + // create an instance of our test object + testObj := new(MyMockedObject) + + // set up expectations with a placeholder in the argument list + mockCall := testObj.On("DoSomething", mock.Anything).Return(true, nil) + + // call the code we are testing + targetFuncThatDoesSomethingWithObj(testObj) + + // assert that the expectations were met + testObj.AssertExpectations(t) + + // remove the handler now so we can add another one that takes precedence + mockCall.Unset() + + // return false now instead of true + testObj.On("DoSomething", mock.Anything).Return(false, nil) + + testObj.AssertExpectations(t) +} +``` + +For more information on how to write mock code, check out the [API documentation for the `mock` package](https://pkg.go.dev/github.com/stretchr/testify/mock). + +You can use the [mockery tool](https://vektra.github.io/mockery/latest/) to autogenerate the mock code against an interface as well, making using mocks much quicker. + +[`suite`](https://pkg.go.dev/github.com/stretchr/testify/suite "API documentation") package +----------------------------------------------------------------------------------------- +> [!WARNING] +> The suite package does not support parallel tests. See [#934](https://github.com/stretchr/testify/issues/934). + +The `suite` package provides functionality that you might be used to from more common object-oriented languages. With it, you can build a testing suite as a struct, build setup/teardown methods and testing methods on your struct, and run them with 'go test' as per normal. + +An example suite is shown below: + +```go +// Basic imports +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +// Define the suite, and absorb the built-in basic suite +// functionality from testify - including a T() method which +// returns the current testing context +type ExampleTestSuite struct { + suite.Suite + VariableThatShouldStartAtFive int +} + +// Make sure that VariableThatShouldStartAtFive is set to five +// before each test +func (suite *ExampleTestSuite) SetupTest() { + suite.VariableThatShouldStartAtFive = 5 +} + +// All methods that begin with "Test" are run as tests within a +// suite. +func (suite *ExampleTestSuite) TestExample() { + assert.Equal(suite.T(), 5, suite.VariableThatShouldStartAtFive) +} + +// In order for 'go test' to run this suite, we need to create +// a normal test function and pass our suite to suite.Run +func TestExampleTestSuite(t *testing.T) { + suite.Run(t, new(ExampleTestSuite)) +} +``` + +For a more complete example, using all of the functionality provided by the suite package, look at our [example testing suite](https://github.com/stretchr/testify/blob/master/suite/suite_test.go) + +For more information on writing suites, check out the [API documentation for the `suite` package](https://pkg.go.dev/github.com/stretchr/testify/suite). + +`Suite` object has assertion methods: + +```go +// Basic imports +import ( + "testing" + + "github.com/stretchr/testify/suite" +) + +// Define the suite, and absorb the built-in basic suite +// functionality from testify - including assertion methods. +type ExampleTestSuite struct { + suite.Suite + VariableThatShouldStartAtFive int +} + +// Make sure that VariableThatShouldStartAtFive is set to five +// before each test +func (suite *ExampleTestSuite) SetupTest() { + suite.VariableThatShouldStartAtFive = 5 +} + +// All methods that begin with "Test" are run as tests within a +// suite. +func (suite *ExampleTestSuite) TestExample() { + suite.Equal(suite.VariableThatShouldStartAtFive, 5) +} + +// In order for 'go test' to run this suite, we need to create +// a normal test function and pass our suite to suite.Run +func TestExampleTestSuite(t *testing.T) { + suite.Run(t, new(ExampleTestSuite)) +} +``` + +------ + +Installation +============ + +To install Testify, use `go get`: + + go get github.com/stretchr/testify + +This will then make the following packages available to you: + + github.com/stretchr/testify/assert + github.com/stretchr/testify/require + github.com/stretchr/testify/mock + github.com/stretchr/testify/suite + github.com/stretchr/testify/http (deprecated) + +Import the `testify/assert` package into your code using this template: + +```go +package yours + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSomething(t *testing.T) { + assert.True(t, true, "True is true!") +} +``` + +------ + +Staying up to date +================== + +To update Testify to the latest version, use `go get -u github.com/stretchr/testify`. + +------ + +Supported go versions +================== + +We currently support the most recent major Go versions from 1.19 onward. + +------ + +Contributing +============ + +Please feel free to submit issues, fork the repository and send pull requests! + +When submitting an issue, we ask that you please include a complete test function that demonstrates the issue. Extra credit for those using Testify to write the test code that demonstrates it. + +Code generation is used. [Look for `Code generated with`](https://github.com/search?q=repo%3Astretchr%2Ftestify%20%22Code%20generated%20with%22&type=code) at the top of some files. Run `go generate ./...` to update generated files. + +We also chat on the [Gophers Slack](https://gophers.slack.com) group in the `#testify` and `#testify-dev` channels. + +------ + +License +======= + +This project is licensed under the terms of the MIT license. diff --git a/pkg/testify/_codegen/.gitignore b/pkg/testify/_codegen/.gitignore new file mode 100644 index 00000000..0a25bc68 --- /dev/null +++ b/pkg/testify/_codegen/.gitignore @@ -0,0 +1 @@ +_codegen diff --git a/pkg/testify/_codegen/go.mod b/pkg/testify/_codegen/go.mod new file mode 100644 index 00000000..d5eb4edd --- /dev/null +++ b/pkg/testify/_codegen/go.mod @@ -0,0 +1,8 @@ +module github.com/stretchr/testify/_codegen + +go 1.11 + +require ( + github.com/ernesto-jimenez/gogen v0.0.0-20180125220232-d7d4131e6607 + github.com/stretchr/testify v1.11.1 // indirect +) diff --git a/pkg/testify/_codegen/go.sum b/pkg/testify/_codegen/go.sum new file mode 100644 index 00000000..dac0426b --- /dev/null +++ b/pkg/testify/_codegen/go.sum @@ -0,0 +1,21 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/ernesto-jimenez/gogen v0.0.0-20180125220232-d7d4131e6607 h1:cTavhURetDkezJCvxFggiyLeP40Mrk/TtVg2+ycw1Es= +github.com/ernesto-jimenez/gogen v0.0.0-20180125220232-d7d4131e6607/go.mod h1:Cg4fM0vhYWOZdgM7RIOSTRNIc8/VT7CXClC3Ni86lu4= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/testify/_codegen/main.go b/pkg/testify/_codegen/main.go new file mode 100644 index 00000000..11a30dbc --- /dev/null +++ b/pkg/testify/_codegen/main.go @@ -0,0 +1,315 @@ +// This program reads all assertion functions from the assert package and +// automatically generates the corresponding requires and forwarded assertions + +package main + +import ( + "bytes" + "flag" + "fmt" + "go/ast" + "go/build" + "go/doc" + "go/format" + "go/importer" + "go/parser" + "go/token" + "go/types" + "io" + "log" + "os" + "path" + "regexp" + "strings" + "text/template" + + "github.com/ernesto-jimenez/gogen/imports" +) + +var ( + pkg = flag.String("assert-path", "github.com/CodSpeedHQ/codspeed-go/pkg/testify/assert", "Path to the assert package") + includeF = flag.Bool("include-format-funcs", false, "include format functions such as Errorf and Equalf") + outputPkg = flag.String("output-package", "", "package for the resulting code") + tmplFile = flag.String("template", "", "What file to load the function template from") + out = flag.String("out", "", "What file to write the source code to") +) + +func main() { + flag.Parse() + + scope, docs, err := parsePackageSource(*pkg) + if err != nil { + log.Fatal(err) + } + + importer, funcs, err := analyzeCode(scope, docs) + if err != nil { + log.Fatal(err) + } + + if err := generateCode(importer, funcs); err != nil { + log.Fatal(err) + } +} + +func generateCode(importer imports.Importer, funcs []testFunc) error { + buff := bytes.NewBuffer(nil) + + tmplHead, tmplFunc, err := parseTemplates() + if err != nil { + return err + } + + // Generate header + if err := tmplHead.Execute(buff, struct { + Name string + Imports map[string]string + }{ + *outputPkg, + importer.Imports(), + }); err != nil { + return err + } + + // Generate funcs + for _, fn := range funcs { + buff.Write([]byte("\n\n")) + if err := tmplFunc.Execute(buff, &fn); err != nil { + return err + } + } + + code, err := format.Source(buff.Bytes()) + if err != nil { + return err + } + + // Write file + output, err := outputFile() + if err != nil { + return err + } + defer output.Close() + _, err = io.Copy(output, bytes.NewReader(code)) + return err +} + +func parseTemplates() (*template.Template, *template.Template, error) { + tmplHead, err := template.New("header").Parse(headerTemplate) + if err != nil { + return nil, nil, err + } + if *tmplFile != "" { + f, err := os.ReadFile(*tmplFile) + if err != nil { + return nil, nil, err + } + funcTemplate = string(f) + } + tmpl, err := template.New("function").Funcs(template.FuncMap{ + "replace": strings.ReplaceAll, + }).Parse(funcTemplate) + if err != nil { + return nil, nil, err + } + return tmplHead, tmpl, nil +} + +func outputFile() (*os.File, error) { + filename := *out + if filename == "-" || (filename == "" && *tmplFile == "") { + return os.Stdout, nil + } + if filename == "" { + filename = strings.TrimSuffix(strings.TrimSuffix(*tmplFile, ".tmpl"), ".go") + ".go" + } + return os.Create(filename) +} + +// 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) + + importer := imports.New(*outputPkg) + var funcs []testFunc + // Go through all the top level functions + for _, fdocs := range docs.Funcs { + // Find the function + obj := scope.Lookup(fdocs.Name) + + fn, ok := obj.(*types.Func) + if !ok { + continue + } + // Check function signature has at least two arguments + sig := fn.Type().(*types.Signature) + if sig.Params().Len() < 2 { + continue + } + // Check first argument is of type testingT + first, ok := sig.Params().At(0).Type().(*types.Named) + if !ok { + continue + } + firstType, ok := first.Underlying().(*types.Interface) + if !ok { + continue + } + if !types.Implements(firstType, testingT) { + continue + } + + // Skip functions ending with f + if strings.HasSuffix(fdocs.Name, "f") && !*includeF { + continue + } + + 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 +func parsePackageSource(pkg string) (*types.Scope, *doc.Package, error) { + pd, err := build.Import(pkg, ".", 0) + if err != nil { + return nil, nil, err + } + + fset := token.NewFileSet() + files := make(map[string]*ast.File) + fileList := make([]*ast.File, len(pd.GoFiles)) + for i, fname := range pd.GoFiles { + src, err := os.ReadFile(path.Join(pd.Dir, fname)) + if err != nil { + return nil, nil, err + } + f, err := parser.ParseFile(fset, fname, src, parser.ParseComments|parser.AllErrors) + if err != nil { + return nil, nil, err + } + files[fname] = f + fileList[i] = f + } + + cfg := types.Config{ + Importer: importer.For("source", nil), + } + info := types.Info{ + Defs: make(map[*ast.Ident]types.Object), + } + tp, err := cfg.Check(pkg, fset, fileList, &info) + if err != nil { + return nil, nil, err + } + + scope := tp.Scope() + + ap, _ := ast.NewPackage(fset, files, nil, nil) + docs := doc.New(ap, pkg, 0) + + return scope, docs, nil +} + +type testFunc struct { + CurrentPkg string + DocInfo *doc.Func + TypeInfo *types.Func +} + +func (f *testFunc) Qualifier(p *types.Package) string { + if p == nil || p.Name() == f.CurrentPkg { + return "" + } + return p.Name() +} + +func (f *testFunc) Params() string { + sig := f.TypeInfo.Type().(*types.Signature) + params := sig.Params() + p := "" + comma := "" + to := params.Len() + var i int + + if sig.Variadic() { + to-- + } + 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)) + 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)) + } + return p +} + +func (f *testFunc) ForwardedParams() string { + sig := f.TypeInfo.Type().(*types.Signature) + params := sig.Params() + p := "" + comma := "" + to := params.Len() + var i int + + if sig.Variadic() { + to-- + } + for i = 1; i < to; i++ { + param := params.At(i) + p += 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()) + } + return p +} + +func (f *testFunc) ParamsFormat() string { + return strings.Replace(f.Params(), "msgAndArgs", "msg string, args", 1) +} + +func (f *testFunc) ForwardedParamsFormat() string { + return strings.Replace(f.ForwardedParams(), "msgAndArgs", "append([]interface{}{msg}, args...)", 1) +} + +func (f *testFunc) Comment() string { + return "// " + strings.Replace(strings.TrimSpace(f.DocInfo.Doc), "\n", "\n// ", -1) +} + +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) + exp := regexp.MustCompile(replace + `\(((\(\)|[^\n])+)\)`) + return exp.ReplaceAllString(comment, replace+`($1, "error message %s", "formatted")`) +} + +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) +} + +// Standard header https://go.dev/s/generatedcode. +var headerTemplate = `// Code generated with github.com/stretchr/testify/_codegen; DO NOT EDIT. + +package {{.Name}} + +import ( +{{range $path, $name := .Imports}} + {{$name}} "{{$path}}"{{end}} +) +` + +var funcTemplate = `{{.Comment}} +func (fwd *AssertionsForwarder) {{.DocInfo.Name}}({{.Params}}) bool { + return assert.{{.DocInfo.Name}}({{.ForwardedParams}}) +}` diff --git a/pkg/testify/assert/assertion_compare.go b/pkg/testify/assert/assertion_compare.go new file mode 100644 index 00000000..ffb24e8e --- /dev/null +++ b/pkg/testify/assert/assertion_compare.go @@ -0,0 +1,495 @@ +package assert + +import ( + "bytes" + "fmt" + "reflect" + "time" +) + +// Deprecated: CompareType has only ever been for internal use and has accidentally been published since v1.6.0. Do not use it. +type CompareType = compareResult + +type compareResult int + +const ( + compareLess compareResult = iota - 1 + compareEqual + 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 interface{}, 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 + } + } + 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 + } + } + 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 + } + } + 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 + } + } + 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 + } + } + 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 + } + } + 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 + } + } + 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 + } + } + 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 + } + } + 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 + } + } + 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 + } + } + 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 + } + } + 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 + } + } + // 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 + } + 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 + } + 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 + } + } + } + + return compareEqual, false +} + +// Greater asserts that the first element is greater than the second +// +// assert.Greater(t, 2, 1) +// assert.Greater(t, float64(2), float64(1)) +// assert.Greater(t, "b", "a") +func Greater(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + failMessage := fmt.Sprintf("\"%v\" is not greater than \"%v\"", e1, e2) + return compareTwoValues(t, e1, e2, []compareResult{compareGreater}, failMessage, msgAndArgs...) +} + +// GreaterOrEqual asserts that the first element is greater than or equal to the second +// +// assert.GreaterOrEqual(t, 2, 1) +// assert.GreaterOrEqual(t, 2, 2) +// assert.GreaterOrEqual(t, "b", "a") +// assert.GreaterOrEqual(t, "b", "b") +func GreaterOrEqual(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + failMessage := fmt.Sprintf("\"%v\" is not greater than or equal to \"%v\"", e1, e2) + return compareTwoValues(t, e1, e2, []compareResult{compareGreater, compareEqual}, failMessage, msgAndArgs...) +} + +// Less asserts that the first element is less than the second +// +// assert.Less(t, 1, 2) +// assert.Less(t, float64(1), float64(2)) +// assert.Less(t, "a", "b") +func Less(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + failMessage := fmt.Sprintf("\"%v\" is not less than \"%v\"", e1, e2) + return compareTwoValues(t, e1, e2, []compareResult{compareLess}, failMessage, msgAndArgs...) +} + +// LessOrEqual asserts that the first element is less than or equal to the second +// +// assert.LessOrEqual(t, 1, 2) +// assert.LessOrEqual(t, 2, 2) +// assert.LessOrEqual(t, "a", "b") +// assert.LessOrEqual(t, "b", "b") +func LessOrEqual(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + failMessage := fmt.Sprintf("\"%v\" is not less than or equal to \"%v\"", e1, e2) + return compareTwoValues(t, e1, e2, []compareResult{compareLess, compareEqual}, failMessage, msgAndArgs...) +} + +// Positive asserts that the specified element is positive +// +// assert.Positive(t, 1) +// assert.Positive(t, 1.23) +func Positive(t TestingT, e interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + zero := reflect.Zero(reflect.TypeOf(e)) + failMessage := fmt.Sprintf("\"%v\" is not positive", e) + return compareTwoValues(t, e, zero.Interface(), []compareResult{compareGreater}, failMessage, msgAndArgs...) +} + +// Negative asserts that the specified element is negative +// +// assert.Negative(t, -1) +// assert.Negative(t, -1.23) +func Negative(t TestingT, e interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + zero := reflect.Zero(reflect.TypeOf(e)) + failMessage := fmt.Sprintf("\"%v\" is not negative", e) + return compareTwoValues(t, e, zero.Interface(), []compareResult{compareLess}, failMessage, msgAndArgs...) +} + +func compareTwoValues(t TestingT, e1 interface{}, e2 interface{}, allowedComparesResults []compareResult, failMessage string, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + + e1Kind := reflect.ValueOf(e1).Kind() + e2Kind := reflect.ValueOf(e2).Kind() + if e1Kind != e2Kind { + return Fail(t, "Elements should be the same type", msgAndArgs...) + } + + compareResult, isComparable := compare(e1, e2, e1Kind) + if !isComparable { + return Fail(t, fmt.Sprintf(`Can not compare type "%T"`, e1), msgAndArgs...) + } + + if !containsValue(allowedComparesResults, compareResult) { + return Fail(t, failMessage, msgAndArgs...) + } + + return true +} + +func containsValue(values []compareResult, value compareResult) bool { + for _, v := range values { + if v == value { + return true + } + } + + return false +} diff --git a/pkg/testify/assert/assertion_compare_test.go b/pkg/testify/assert/assertion_compare_test.go new file mode 100644 index 00000000..ab4c233c --- /dev/null +++ b/pkg/testify/assert/assertion_compare_test.go @@ -0,0 +1,494 @@ +package assert + +import ( + "bytes" + "fmt" + testing "github.com/CodSpeedHQ/codspeed-go/testing/testing" + "reflect" + "runtime" + "time" +) + +func TestCompare(t *testing.T) { + t.Parallel() + + type customString string + type customInt int + type customInt8 int8 + type customInt16 int16 + type customInt32 int32 + type customInt64 int64 + type customUInt uint + type customUInt8 uint8 + type customUInt16 uint16 + type customUInt32 uint32 + type customUInt64 uint64 + type customFloat32 float32 + type customFloat64 float64 + type customUintptr uintptr + type customTime time.Time + type customBytes []byte + for _, currCase := range []struct { + less interface{} + greater interface{} + cType string + }{ + {less: customString("a"), greater: customString("b"), cType: "string"}, + {less: "a", greater: "b", cType: "string"}, + {less: customInt(1), greater: customInt(2), cType: "int"}, + {less: int(1), greater: int(2), cType: "int"}, + {less: customInt8(1), greater: customInt8(2), cType: "int8"}, + {less: int8(1), greater: int8(2), cType: "int8"}, + {less: customInt16(1), greater: customInt16(2), cType: "int16"}, + {less: int16(1), greater: int16(2), cType: "int16"}, + {less: customInt32(1), greater: customInt32(2), cType: "int32"}, + {less: int32(1), greater: int32(2), cType: "int32"}, + {less: customInt64(1), greater: customInt64(2), cType: "int64"}, + {less: int64(1), greater: int64(2), cType: "int64"}, + {less: customUInt(1), greater: customUInt(2), cType: "uint"}, + {less: uint8(1), greater: uint8(2), cType: "uint8"}, + {less: customUInt8(1), greater: customUInt8(2), cType: "uint8"}, + {less: uint16(1), greater: uint16(2), cType: "uint16"}, + {less: customUInt16(1), greater: customUInt16(2), cType: "uint16"}, + {less: uint32(1), greater: uint32(2), cType: "uint32"}, + {less: customUInt32(1), greater: customUInt32(2), cType: "uint32"}, + {less: uint64(1), greater: uint64(2), cType: "uint64"}, + {less: customUInt64(1), greater: customUInt64(2), cType: "uint64"}, + {less: float32(1.23), greater: float32(2.34), cType: "float32"}, + {less: customFloat32(1.23), greater: customFloat32(2.23), cType: "float32"}, + {less: float64(1.23), greater: float64(2.34), cType: "float64"}, + {less: customFloat64(1.23), greater: customFloat64(2.34), cType: "float64"}, + {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"}, + {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"}, + } { + resLess, isComparable := compare(currCase.less, currCase.greater, reflect.ValueOf(currCase.less).Kind()) + if !isComparable { + t.Error("object should be comparable for type " + currCase.cType) + } + + if resLess != compareLess { + t.Errorf("object less (%v) should be less than greater (%v) for type "+currCase.cType, + currCase.less, currCase.greater) + } + + resGreater, isComparable := compare(currCase.greater, currCase.less, reflect.ValueOf(currCase.less).Kind()) + if !isComparable { + t.Error("object are comparable for type " + currCase.cType) + } + + if resGreater != compareGreater { + t.Errorf("object greater should be greater than less for type " + currCase.cType) + } + + resEqual, isComparable := compare(currCase.less, currCase.less, reflect.ValueOf(currCase.less).Kind()) + if !isComparable { + t.Error("object are comparable for type " + currCase.cType) + } + + if resEqual != 0 { + t.Errorf("objects should be equal for type " + currCase.cType) + } + } +} + +type outputT struct { + buf *bytes.Buffer + helpers map[string]struct{} +} + +// Implements TestingT +func (t *outputT) Errorf(format string, args ...interface{}) { + s := fmt.Sprintf(format, args...) + t.buf.WriteString(s) +} + +func (t *outputT) Helper() { + if t.helpers == nil { + t.helpers = make(map[string]struct{}) + } + t.helpers[callerName(1)] = struct{}{} +} + +// callerName gives the function name (qualified with a package path) +// for the caller after skip frames (where 0 means the current function). +func callerName(skip int) string { + // Make room for the skip PC. + var pc [1]uintptr + n := runtime.Callers(skip+2, pc[:]) // skip + runtime.Callers + callerName + if n == 0 { + panic("testing: zero callers found") + } + frames := runtime.CallersFrames(pc[:n]) + frame, _ := frames.Next() + return frame.Function +} + +func TestGreater(t *testing.T) { + t.Parallel() + + mockT := new(testing.T) + + if !Greater(mockT, 2, 1) { + t.Error("Greater should return true") + } + + if Greater(mockT, 1, 1) { + t.Error("Greater should return false") + } + + if Greater(mockT, 1, 2) { + t.Error("Greater should return false") + } + + // Check error report + for _, currCase := range []struct { + less interface{} + greater interface{} + 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]"`}, + } { + out := &outputT{buf: bytes.NewBuffer(nil)} + False(t, Greater(out, currCase.less, currCase.greater)) + Contains(t, out.buf.String(), currCase.msg) + Contains(t, out.helpers, "github.com/CodSpeedHQ/codspeed-go/pkg/testify/assert.Greater") + } +} + +func TestGreaterOrEqual(t *testing.T) { + t.Parallel() + + mockT := new(testing.T) + + if !GreaterOrEqual(mockT, 2, 1) { + t.Error("GreaterOrEqual should return true") + } + + if !GreaterOrEqual(mockT, 1, 1) { + t.Error("GreaterOrEqual should return true") + } + + if GreaterOrEqual(mockT, 1, 2) { + t.Error("GreaterOrEqual should return false") + } + + // Check error report + for _, currCase := range []struct { + less interface{} + greater interface{} + 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]"`}, + } { + out := &outputT{buf: bytes.NewBuffer(nil)} + False(t, GreaterOrEqual(out, currCase.less, currCase.greater)) + Contains(t, out.buf.String(), currCase.msg) + Contains(t, out.helpers, "github.com/CodSpeedHQ/codspeed-go/pkg/testify/assert.GreaterOrEqual") + } +} + +func TestLess(t *testing.T) { + t.Parallel() + + mockT := new(testing.T) + + if !Less(mockT, 1, 2) { + t.Error("Less should return true") + } + + if Less(mockT, 1, 1) { + t.Error("Less should return false") + } + + if Less(mockT, 2, 1) { + t.Error("Less should return false") + } + + // Check error report + for _, currCase := range []struct { + less interface{} + greater interface{} + 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]"`}, + } { + out := &outputT{buf: bytes.NewBuffer(nil)} + False(t, Less(out, currCase.greater, currCase.less)) + Contains(t, out.buf.String(), currCase.msg) + Contains(t, out.helpers, "github.com/CodSpeedHQ/codspeed-go/pkg/testify/assert.Less") + } +} + +func TestLessOrEqual(t *testing.T) { + t.Parallel() + + mockT := new(testing.T) + + if !LessOrEqual(mockT, 1, 2) { + t.Error("LessOrEqual should return true") + } + + if !LessOrEqual(mockT, 1, 1) { + t.Error("LessOrEqual should return true") + } + + if LessOrEqual(mockT, 2, 1) { + t.Error("LessOrEqual should return false") + } + + // Check error report + for _, currCase := range []struct { + less interface{} + greater interface{} + 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]"`}, + } { + out := &outputT{buf: bytes.NewBuffer(nil)} + False(t, LessOrEqual(out, currCase.greater, currCase.less)) + Contains(t, out.buf.String(), currCase.msg) + Contains(t, out.helpers, "github.com/CodSpeedHQ/codspeed-go/pkg/testify/assert.LessOrEqual") + } +} + +func TestPositive(t *testing.T) { + t.Parallel() + + mockT := new(testing.T) + + if !Positive(mockT, 1) { + t.Error("Positive should return true") + } + + if !Positive(mockT, 1.23) { + t.Error("Positive should return true") + } + + if Positive(mockT, -1) { + t.Error("Positive should return false") + } + + if Positive(mockT, -1.23) { + t.Error("Positive should return false") + } + + // Check error report + for _, currCase := range []struct { + e interface{} + msg string + }{ + {e: int(-1), msg: `"-1" is not positive`}, + {e: int8(-1), msg: `"-1" is not positive`}, + {e: int16(-1), msg: `"-1" is not positive`}, + {e: int32(-1), msg: `"-1" is not positive`}, + {e: int64(-1), msg: `"-1" is not positive`}, + {e: float32(-1.23), msg: `"-1.23" is not positive`}, + {e: float64(-1.23), msg: `"-1.23" is not positive`}, + } { + out := &outputT{buf: bytes.NewBuffer(nil)} + False(t, Positive(out, currCase.e)) + Contains(t, out.buf.String(), currCase.msg) + Contains(t, out.helpers, "github.com/CodSpeedHQ/codspeed-go/pkg/testify/assert.Positive") + } +} + +func TestNegative(t *testing.T) { + t.Parallel() + + mockT := new(testing.T) + + if !Negative(mockT, -1) { + t.Error("Negative should return true") + } + + if !Negative(mockT, -1.23) { + t.Error("Negative should return true") + } + + if Negative(mockT, 1) { + t.Error("Negative should return false") + } + + if Negative(mockT, 1.23) { + t.Error("Negative should return false") + } + + // Check error report + for _, currCase := range []struct { + e interface{} + msg string + }{ + {e: int(1), msg: `"1" is not negative`}, + {e: int8(1), msg: `"1" is not negative`}, + {e: int16(1), msg: `"1" is not negative`}, + {e: int32(1), msg: `"1" is not negative`}, + {e: int64(1), msg: `"1" is not negative`}, + {e: float32(1.23), msg: `"1.23" is not negative`}, + {e: float64(1.23), msg: `"1.23" is not negative`}, + } { + out := &outputT{buf: bytes.NewBuffer(nil)} + False(t, Negative(out, currCase.e)) + Contains(t, out.buf.String(), currCase.msg) + Contains(t, out.helpers, "github.com/CodSpeedHQ/codspeed-go/pkg/testify/assert.Negative") + } +} + +func Test_compareTwoValuesDifferentValuesTypes(t *testing.T) { + t.Parallel() + + mockT := new(testing.T) + + for _, currCase := range []struct { + v1 interface{} + v2 interface{} + compareResult bool + }{ + {v1: 123, v2: "abc"}, + {v1: "abc", v2: 123456}, + {v1: float64(12), v2: "123"}, + {v1: "float(12)", v2: float64(1)}, + } { + result := compareTwoValues(mockT, currCase.v1, currCase.v2, []compareResult{compareLess, compareEqual, compareGreater}, "testFailMessage") + False(t, result) + } +} + +func Test_compareTwoValuesNotComparableValues(t *testing.T) { + t.Parallel() + + mockT := new(testing.T) + + type CompareStruct struct { + } + + for _, currCase := range []struct { + v1 interface{} + v2 interface{} + }{ + {v1: CompareStruct{}, v2: CompareStruct{}}, + {v1: map[string]int{}, v2: map[string]int{}}, + {v1: make([]int, 5), v2: make([]int, 5)}, + } { + result := compareTwoValues(mockT, currCase.v1, currCase.v2, []compareResult{compareLess, compareEqual, compareGreater}, "testFailMessage") + False(t, result) + } +} + +func Test_compareTwoValuesCorrectCompareResult(t *testing.T) { + t.Parallel() + + mockT := new(testing.T) + + for _, currCase := range []struct { + v1 interface{} + v2 interface{} + allowedResults []compareResult + }{ + {v1: 1, v2: 2, allowedResults: []compareResult{compareLess}}, + {v1: 1, v2: 2, allowedResults: []compareResult{compareLess, compareEqual}}, + {v1: 2, v2: 2, allowedResults: []compareResult{compareGreater, compareEqual}}, + {v1: 2, v2: 2, allowedResults: []compareResult{compareEqual}}, + {v1: 2, v2: 1, allowedResults: []compareResult{compareEqual, compareGreater}}, + {v1: 2, v2: 1, allowedResults: []compareResult{compareGreater}}, + } { + result := compareTwoValues(mockT, currCase.v1, currCase.v2, currCase.allowedResults, "testFailMessage") + True(t, result) + } +} + +func Test_containsValue(t *testing.T) { + t.Parallel() + + for _, currCase := range []struct { + values []compareResult + value compareResult + result bool + }{ + {values: []compareResult{compareGreater}, value: compareGreater, result: true}, + {values: []compareResult{compareGreater, compareLess}, value: compareGreater, result: true}, + {values: []compareResult{compareGreater, compareLess}, value: compareLess, result: true}, + {values: []compareResult{compareGreater, compareLess}, value: compareEqual, result: false}, + } { + result := containsValue(currCase.values, currCase.value) + Equal(t, currCase.result, result) + } +} + +func TestComparingMsgAndArgsForwarding(t *testing.T) { + msgAndArgs := []interface{}{"format %s %x", "this", 0xc001} + expectedOutput := "format this c001\n" + funcs := []func(t TestingT){ + func(t TestingT) { Greater(t, 1, 2, msgAndArgs...) }, + func(t TestingT) { GreaterOrEqual(t, 1, 2, msgAndArgs...) }, + func(t TestingT) { Less(t, 2, 1, msgAndArgs...) }, + func(t TestingT) { LessOrEqual(t, 2, 1, msgAndArgs...) }, + func(t TestingT) { Positive(t, 0, msgAndArgs...) }, + func(t TestingT) { Negative(t, 0, msgAndArgs...) }, + } + for _, f := range funcs { + out := &outputT{buf: bytes.NewBuffer(nil)} + f(out) + Contains(t, out.buf.String(), expectedOutput) + } +} diff --git a/pkg/testify/assert/assertion_format.go b/pkg/testify/assert/assertion_format.go new file mode 100644 index 00000000..c592f6ad --- /dev/null +++ b/pkg/testify/assert/assertion_format.go @@ -0,0 +1,866 @@ +// Code generated with github.com/stretchr/testify/_codegen; DO NOT EDIT. + +package assert + +import ( + http "net/http" + url "net/url" + time "time" +) + +// Conditionf uses a Comparison to assert a complex condition. +func Conditionf(t TestingT, comp Comparison, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return Condition(t, comp, append([]interface{}{msg}, args...)...) +} + +// Containsf asserts that the specified string, list(array, slice...) or map contains the +// specified substring or element. +// +// assert.Containsf(t, "Hello World", "World", "error message %s", "formatted") +// assert.Containsf(t, ["Hello", "World"], "World", "error message %s", "formatted") +// assert.Containsf(t, {"Hello": "World"}, "Hello", "error message %s", "formatted") +func Containsf(t TestingT, s interface{}, contains interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return Contains(t, s, contains, append([]interface{}{msg}, args...)...) +} + +// DirExistsf checks whether a directory exists in the given path. It also fails +// if the path is a file rather a directory or there is an error checking whether it exists. +func DirExistsf(t TestingT, path string, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return DirExists(t, path, append([]interface{}{msg}, args...)...) +} + +// ElementsMatchf asserts that the specified listA(array, slice...) is equal to specified +// 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.ElementsMatchf(t, [1, 3, 2, 3], [1, 3, 3, 2], "error message %s", "formatted") +func ElementsMatchf(t TestingT, listA interface{}, listB interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return ElementsMatch(t, listA, listB, append([]interface{}{msg}, args...)...) +} + +// Emptyf asserts that the given value is "empty". +// +// [Zero values] are "empty". +// +// Arrays are "empty" if every element is the zero value of the type (stricter than "empty"). +// +// Slices, maps and channels with zero length are "empty". +// +// Pointer values are "empty" if the pointer is nil or if the pointed value is "empty". +// +// assert.Emptyf(t, obj, "error message %s", "formatted") +// +// [Zero values]: https://go.dev/ref/spec#The_zero_value +func Emptyf(t TestingT, object interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return Empty(t, object, append([]interface{}{msg}, args...)...) +} + +// Equalf asserts that two objects are equal. +// +// assert.Equalf(t, 123, 123, "error message %s", "formatted") +// +// Pointer variable equality is determined based on the equality of the +// referenced values (as opposed to the memory addresses). Function equality +// cannot be determined and will always fail. +func Equalf(t TestingT, expected interface{}, actual interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return Equal(t, expected, actual, append([]interface{}{msg}, args...)...) +} + +// EqualErrorf asserts that a function returned an error (i.e. not `nil`) +// and that it is equal to the provided error. +// +// actualObj, err := SomeFunction() +// assert.EqualErrorf(t, err, expectedErrorString, "error message %s", "formatted") +func EqualErrorf(t TestingT, theError error, errString string, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return EqualError(t, theError, errString, append([]interface{}{msg}, args...)...) +} + +// EqualExportedValuesf asserts that the types of two objects are equal and their public +// fields are also equal. This is useful for comparing structs that have private fields +// that could potentially differ. +// +// type S struct { +// Exported int +// notExported int +// } +// assert.EqualExportedValuesf(t, S{1, 2}, S{1, 3}, "error message %s", "formatted") => true +// assert.EqualExportedValuesf(t, S{1, 2}, S{2, 3}, "error message %s", "formatted") => false +func EqualExportedValuesf(t TestingT, expected interface{}, actual interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return EqualExportedValues(t, expected, actual, append([]interface{}{msg}, args...)...) +} + +// EqualValuesf asserts that two objects are equal or convertible to the larger +// type and equal. +// +// assert.EqualValuesf(t, uint32(123), int32(123), "error message %s", "formatted") +func EqualValuesf(t TestingT, expected interface{}, actual interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return EqualValues(t, expected, actual, append([]interface{}{msg}, args...)...) +} + +// Errorf asserts that a function returned an error (i.e. not `nil`). +// +// actualObj, err := SomeFunction() +// assert.Errorf(t, err, "error message %s", "formatted") +func Errorf(t TestingT, err error, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return Error(t, err, append([]interface{}{msg}, args...)...) +} + +// ErrorAsf asserts that at least one of the errors in err's chain matches target, and if so, sets target to that error value. +// This is a wrapper for errors.As. +func ErrorAsf(t TestingT, err error, target interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return ErrorAs(t, err, target, append([]interface{}{msg}, args...)...) +} + +// ErrorContainsf asserts that a function returned an error (i.e. not `nil`) +// and that the error contains the specified substring. +// +// actualObj, err := SomeFunction() +// assert.ErrorContainsf(t, err, expectedErrorSubString, "error message %s", "formatted") +func ErrorContainsf(t TestingT, theError error, contains string, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return ErrorContains(t, theError, contains, append([]interface{}{msg}, args...)...) +} + +// ErrorIsf asserts that at least one of the errors in err's chain matches target. +// This is a wrapper for errors.Is. +func ErrorIsf(t TestingT, err error, target error, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return ErrorIs(t, err, target, append([]interface{}{msg}, args...)...) +} + +// Eventuallyf asserts that given condition will be met in waitFor time, +// periodically checking target function each tick. +// +// assert.Eventuallyf(t, func() bool { return true; }, time.Second, 10*time.Millisecond, "error message %s", "formatted") +func Eventuallyf(t TestingT, condition func() bool, waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return Eventually(t, condition, waitFor, tick, append([]interface{}{msg}, args...)...) +} + +// EventuallyWithTf asserts that given condition will be met in waitFor time, +// periodically checking target function each tick. In contrast to Eventually, +// it supplies a CollectT to the condition function, so that the condition +// function can use the CollectT to call other assertions. +// The condition is considered "met" if no errors are raised in a tick. +// The supplied CollectT collects all errors from one tick (if there are any). +// If the condition is not met before waitFor, the collected errors of +// the last tick are copied to t. +// +// externalValue := false +// go func() { +// time.Sleep(8*time.Second) +// externalValue = true +// }() +// assert.EventuallyWithTf(t, func(c *assert.CollectT, "error message %s", "formatted") { +// // add assertions as needed; any assertion failure will fail the current tick +// assert.True(c, externalValue, "expected 'externalValue' to be true") +// }, 10*time.Second, 1*time.Second, "external state has not changed to 'true'; still false") +func EventuallyWithTf(t TestingT, condition func(collect *CollectT), waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return EventuallyWithT(t, condition, waitFor, tick, append([]interface{}{msg}, args...)...) +} + +// Exactlyf asserts that two objects are equal in value and type. +// +// assert.Exactlyf(t, int32(123), int64(123), "error message %s", "formatted") +func Exactlyf(t TestingT, expected interface{}, actual interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return Exactly(t, expected, actual, append([]interface{}{msg}, args...)...) +} + +// Failf reports a failure through +func Failf(t TestingT, failureMessage string, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return Fail(t, failureMessage, append([]interface{}{msg}, args...)...) +} + +// FailNowf fails test +func FailNowf(t TestingT, failureMessage string, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return FailNow(t, failureMessage, append([]interface{}{msg}, args...)...) +} + +// Falsef asserts that the specified value is false. +// +// assert.Falsef(t, myBool, "error message %s", "formatted") +func Falsef(t TestingT, value bool, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return False(t, value, append([]interface{}{msg}, args...)...) +} + +// FileExistsf checks whether a file exists in the given path. It also fails if +// the path points to a directory or there is an error when trying to check the file. +func FileExistsf(t TestingT, path string, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return FileExists(t, path, append([]interface{}{msg}, args...)...) +} + +// Greaterf asserts that the first element is greater than the second +// +// assert.Greaterf(t, 2, 1, "error message %s", "formatted") +// assert.Greaterf(t, float64(2), float64(1), "error message %s", "formatted") +// assert.Greaterf(t, "b", "a", "error message %s", "formatted") +func Greaterf(t TestingT, e1 interface{}, e2 interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return Greater(t, e1, e2, append([]interface{}{msg}, args...)...) +} + +// GreaterOrEqualf asserts that the first element is greater than or equal to the second +// +// assert.GreaterOrEqualf(t, 2, 1, "error message %s", "formatted") +// assert.GreaterOrEqualf(t, 2, 2, "error message %s", "formatted") +// assert.GreaterOrEqualf(t, "b", "a", "error message %s", "formatted") +// assert.GreaterOrEqualf(t, "b", "b", "error message %s", "formatted") +func GreaterOrEqualf(t TestingT, e1 interface{}, e2 interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return GreaterOrEqual(t, e1, e2, append([]interface{}{msg}, args...)...) +} + +// HTTPBodyContainsf asserts that a specified handler returns a +// body that contains a string. +// +// assert.HTTPBodyContainsf(t, myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky", "error message %s", "formatted") +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPBodyContainsf(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return HTTPBodyContains(t, handler, method, url, values, str, append([]interface{}{msg}, args...)...) +} + +// HTTPBodyNotContainsf asserts that a specified handler returns a +// body that does not contain a string. +// +// assert.HTTPBodyNotContainsf(t, myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky", "error message %s", "formatted") +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPBodyNotContainsf(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return HTTPBodyNotContains(t, handler, method, url, values, str, append([]interface{}{msg}, args...)...) +} + +// HTTPErrorf asserts that a specified handler returns an error status code. +// +// assert.HTTPErrorf(t, myHandler, "POST", "/a/b/c", url.Values{"a": []string{"b", "c"}} +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPErrorf(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return HTTPError(t, handler, method, url, values, append([]interface{}{msg}, args...)...) +} + +// HTTPRedirectf asserts that a specified handler returns a redirect status code. +// +// assert.HTTPRedirectf(t, myHandler, "GET", "/a/b/c", url.Values{"a": []string{"b", "c"}} +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPRedirectf(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return HTTPRedirect(t, handler, method, url, values, append([]interface{}{msg}, args...)...) +} + +// HTTPStatusCodef asserts that a specified handler returns a specified status code. +// +// assert.HTTPStatusCodef(t, myHandler, "GET", "/notImplemented", nil, 501, "error message %s", "formatted") +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPStatusCodef(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, statuscode int, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return HTTPStatusCode(t, handler, method, url, values, statuscode, append([]interface{}{msg}, args...)...) +} + +// HTTPSuccessf asserts that a specified handler returns a success status code. +// +// assert.HTTPSuccessf(t, myHandler, "POST", "http://www.google.com", nil, "error message %s", "formatted") +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPSuccessf(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return HTTPSuccess(t, handler, method, url, values, append([]interface{}{msg}, args...)...) +} + +// Implementsf asserts that an object is implemented by the specified interface. +// +// assert.Implementsf(t, (*MyInterface)(nil), new(MyObject), "error message %s", "formatted") +func Implementsf(t TestingT, interfaceObject interface{}, object interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return Implements(t, interfaceObject, object, append([]interface{}{msg}, args...)...) +} + +// InDeltaf asserts that the two numerals are within delta of each other. +// +// assert.InDeltaf(t, math.Pi, 22/7.0, 0.01, "error message %s", "formatted") +func InDeltaf(t TestingT, expected interface{}, actual interface{}, delta float64, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return InDelta(t, expected, actual, delta, append([]interface{}{msg}, args...)...) +} + +// InDeltaMapValuesf is the same as InDelta, but it compares all values between two maps. Both maps must have exactly the same keys. +func InDeltaMapValuesf(t TestingT, expected interface{}, actual interface{}, delta float64, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return InDeltaMapValues(t, expected, actual, delta, append([]interface{}{msg}, args...)...) +} + +// InDeltaSlicef is the same as InDelta, except it compares two slices. +func InDeltaSlicef(t TestingT, expected interface{}, actual interface{}, delta float64, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return InDeltaSlice(t, expected, actual, delta, append([]interface{}{msg}, args...)...) +} + +// InEpsilonf asserts that expected and actual have a relative error less than epsilon +func InEpsilonf(t TestingT, expected interface{}, actual interface{}, epsilon float64, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return InEpsilon(t, expected, actual, epsilon, append([]interface{}{msg}, args...)...) +} + +// InEpsilonSlicef is the same as InEpsilon, except it compares each value from two slices. +func InEpsilonSlicef(t TestingT, expected interface{}, actual interface{}, epsilon float64, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return InEpsilonSlice(t, expected, actual, epsilon, append([]interface{}{msg}, args...)...) +} + +// IsDecreasingf asserts that the collection is decreasing +// +// assert.IsDecreasingf(t, []int{2, 1, 0}, "error message %s", "formatted") +// assert.IsDecreasingf(t, []float{2, 1}, "error message %s", "formatted") +// assert.IsDecreasingf(t, []string{"b", "a"}, "error message %s", "formatted") +func IsDecreasingf(t TestingT, object interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return IsDecreasing(t, object, append([]interface{}{msg}, args...)...) +} + +// IsIncreasingf asserts that the collection is increasing +// +// assert.IsIncreasingf(t, []int{1, 2, 3}, "error message %s", "formatted") +// assert.IsIncreasingf(t, []float{1, 2}, "error message %s", "formatted") +// assert.IsIncreasingf(t, []string{"a", "b"}, "error message %s", "formatted") +func IsIncreasingf(t TestingT, object interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return IsIncreasing(t, object, append([]interface{}{msg}, args...)...) +} + +// IsNonDecreasingf asserts that the collection is not decreasing +// +// assert.IsNonDecreasingf(t, []int{1, 1, 2}, "error message %s", "formatted") +// assert.IsNonDecreasingf(t, []float{1, 2}, "error message %s", "formatted") +// assert.IsNonDecreasingf(t, []string{"a", "b"}, "error message %s", "formatted") +func IsNonDecreasingf(t TestingT, object interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return IsNonDecreasing(t, object, append([]interface{}{msg}, args...)...) +} + +// IsNonIncreasingf asserts that the collection is not increasing +// +// assert.IsNonIncreasingf(t, []int{2, 1, 1}, "error message %s", "formatted") +// assert.IsNonIncreasingf(t, []float{2, 1}, "error message %s", "formatted") +// assert.IsNonIncreasingf(t, []string{"b", "a"}, "error message %s", "formatted") +func IsNonIncreasingf(t TestingT, object interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return IsNonIncreasing(t, object, append([]interface{}{msg}, args...)...) +} + +// IsNotTypef asserts that the specified objects are not of the same type. +// +// assert.IsNotTypef(t, &NotMyStruct{}, &MyStruct{}, "error message %s", "formatted") +func IsNotTypef(t TestingT, theType interface{}, object interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return IsNotType(t, theType, object, append([]interface{}{msg}, args...)...) +} + +// IsTypef asserts that the specified objects are of the same type. +// +// assert.IsTypef(t, &MyStruct{}, &MyStruct{}, "error message %s", "formatted") +func IsTypef(t TestingT, expectedType interface{}, object interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return IsType(t, expectedType, object, append([]interface{}{msg}, args...)...) +} + +// JSONEqf asserts that two JSON strings are equivalent. +// +// assert.JSONEqf(t, `{"hello": "world", "foo": "bar"}`, `{"foo": "bar", "hello": "world"}`, "error message %s", "formatted") +func JSONEqf(t TestingT, expected string, actual string, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return JSONEq(t, expected, actual, append([]interface{}{msg}, args...)...) +} + +// Lenf asserts that the specified object has specific length. +// Lenf also fails if the object has a type that len() not accept. +// +// assert.Lenf(t, mySlice, 3, "error message %s", "formatted") +func Lenf(t TestingT, object interface{}, length int, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return Len(t, object, length, append([]interface{}{msg}, args...)...) +} + +// Lessf asserts that the first element is less than the second +// +// assert.Lessf(t, 1, 2, "error message %s", "formatted") +// assert.Lessf(t, float64(1), float64(2), "error message %s", "formatted") +// assert.Lessf(t, "a", "b", "error message %s", "formatted") +func Lessf(t TestingT, e1 interface{}, e2 interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return Less(t, e1, e2, append([]interface{}{msg}, args...)...) +} + +// LessOrEqualf asserts that the first element is less than or equal to the second +// +// assert.LessOrEqualf(t, 1, 2, "error message %s", "formatted") +// assert.LessOrEqualf(t, 2, 2, "error message %s", "formatted") +// assert.LessOrEqualf(t, "a", "b", "error message %s", "formatted") +// assert.LessOrEqualf(t, "b", "b", "error message %s", "formatted") +func LessOrEqualf(t TestingT, e1 interface{}, e2 interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return LessOrEqual(t, e1, e2, append([]interface{}{msg}, args...)...) +} + +// Negativef asserts that the specified element is negative +// +// assert.Negativef(t, -1, "error message %s", "formatted") +// assert.Negativef(t, -1.23, "error message %s", "formatted") +func Negativef(t TestingT, e interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return Negative(t, e, append([]interface{}{msg}, args...)...) +} + +// Neverf asserts that the given condition doesn't satisfy in waitFor time, +// periodically checking the target function each tick. +// +// assert.Neverf(t, func() bool { return false; }, time.Second, 10*time.Millisecond, "error message %s", "formatted") +func Neverf(t TestingT, condition func() bool, waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return Never(t, condition, waitFor, tick, append([]interface{}{msg}, args...)...) +} + +// Nilf asserts that the specified object is nil. +// +// assert.Nilf(t, err, "error message %s", "formatted") +func Nilf(t TestingT, object interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return Nil(t, object, append([]interface{}{msg}, args...)...) +} + +// NoDirExistsf checks whether a directory does not exist in the given path. +// It fails if the path points to an existing _directory_ only. +func NoDirExistsf(t TestingT, path string, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return NoDirExists(t, path, append([]interface{}{msg}, args...)...) +} + +// NoErrorf asserts that a function returned no error (i.e. `nil`). +// +// actualObj, err := SomeFunction() +// if assert.NoErrorf(t, err, "error message %s", "formatted") { +// assert.Equal(t, expectedObj, actualObj) +// } +func NoErrorf(t TestingT, err error, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return NoError(t, err, append([]interface{}{msg}, args...)...) +} + +// NoFileExistsf checks whether a file does not exist in a given path. It fails +// if the path points to an existing _file_ only. +func NoFileExistsf(t TestingT, path string, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return NoFileExists(t, path, append([]interface{}{msg}, args...)...) +} + +// NotContainsf asserts that the specified string, list(array, slice...) or map does NOT contain the +// specified substring or element. +// +// assert.NotContainsf(t, "Hello World", "Earth", "error message %s", "formatted") +// assert.NotContainsf(t, ["Hello", "World"], "Earth", "error message %s", "formatted") +// assert.NotContainsf(t, {"Hello": "World"}, "Earth", "error message %s", "formatted") +func NotContainsf(t TestingT, s interface{}, contains interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return NotContains(t, s, contains, append([]interface{}{msg}, args...)...) +} + +// NotElementsMatchf asserts that the specified listA(array, slice...) is NOT equal to specified +// 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 not match. +// This is an inverse of ElementsMatch. +// +// assert.NotElementsMatchf(t, [1, 1, 2, 3], [1, 1, 2, 3], "error message %s", "formatted") -> false +// +// assert.NotElementsMatchf(t, [1, 1, 2, 3], [1, 2, 3], "error message %s", "formatted") -> true +// +// assert.NotElementsMatchf(t, [1, 2, 3], [1, 2, 4], "error message %s", "formatted") -> true +func NotElementsMatchf(t TestingT, listA interface{}, listB interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return NotElementsMatch(t, listA, listB, append([]interface{}{msg}, args...)...) +} + +// NotEmptyf asserts that the specified object is NOT [Empty]. +// +// if assert.NotEmptyf(t, obj, "error message %s", "formatted") { +// assert.Equal(t, "two", obj[1]) +// } +func NotEmptyf(t TestingT, object interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return NotEmpty(t, object, append([]interface{}{msg}, args...)...) +} + +// NotEqualf asserts that the specified values are NOT equal. +// +// assert.NotEqualf(t, obj1, obj2, "error message %s", "formatted") +// +// Pointer variable equality is determined based on the equality of the +// referenced values (as opposed to the memory addresses). +func NotEqualf(t TestingT, expected interface{}, actual interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return NotEqual(t, expected, actual, append([]interface{}{msg}, args...)...) +} + +// NotEqualValuesf asserts that two objects are not equal even when converted to the same type +// +// assert.NotEqualValuesf(t, obj1, obj2, "error message %s", "formatted") +func NotEqualValuesf(t TestingT, expected interface{}, actual interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return NotEqualValues(t, expected, actual, append([]interface{}{msg}, args...)...) +} + +// NotErrorAsf asserts that none of the errors in err's chain matches target, +// but if so, sets target to that error value. +func NotErrorAsf(t TestingT, err error, target interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return NotErrorAs(t, err, target, append([]interface{}{msg}, args...)...) +} + +// NotErrorIsf asserts that none of the errors in err's chain matches target. +// This is a wrapper for errors.Is. +func NotErrorIsf(t TestingT, err error, target error, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return NotErrorIs(t, err, target, append([]interface{}{msg}, args...)...) +} + +// NotImplementsf asserts that an object does not implement the specified interface. +// +// assert.NotImplementsf(t, (*MyInterface)(nil), new(MyObject), "error message %s", "formatted") +func NotImplementsf(t TestingT, interfaceObject interface{}, object interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return NotImplements(t, interfaceObject, object, append([]interface{}{msg}, args...)...) +} + +// NotNilf asserts that the specified object is not nil. +// +// assert.NotNilf(t, err, "error message %s", "formatted") +func NotNilf(t TestingT, object interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return NotNil(t, object, append([]interface{}{msg}, args...)...) +} + +// NotPanicsf asserts that the code inside the specified PanicTestFunc does NOT panic. +// +// assert.NotPanicsf(t, func(){ RemainCalm() }, "error message %s", "formatted") +func NotPanicsf(t TestingT, f PanicTestFunc, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return NotPanics(t, f, append([]interface{}{msg}, args...)...) +} + +// NotRegexpf asserts that a specified regexp does not match a string. +// +// assert.NotRegexpf(t, regexp.MustCompile("starts"), "it's starting", "error message %s", "formatted") +// assert.NotRegexpf(t, "^start", "it's not starting", "error message %s", "formatted") +func NotRegexpf(t TestingT, rx interface{}, str interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return NotRegexp(t, rx, str, append([]interface{}{msg}, args...)...) +} + +// NotSamef asserts that two pointers do not reference the same object. +// +// assert.NotSamef(t, ptr1, ptr2, "error message %s", "formatted") +// +// Both arguments must be pointer variables. Pointer variable sameness is +// determined based on the equality of both type and value. +func NotSamef(t TestingT, expected interface{}, actual interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return NotSame(t, expected, actual, append([]interface{}{msg}, args...)...) +} + +// NotSubsetf asserts that the list (array, slice, or map) does NOT contain all +// elements given in the subset (array, slice, or map). +// Map elements are key-value pairs unless compared with an array or slice where +// only the map key is evaluated. +// +// assert.NotSubsetf(t, [1, 3, 4], [1, 2], "error message %s", "formatted") +// assert.NotSubsetf(t, {"x": 1, "y": 2}, {"z": 3}, "error message %s", "formatted") +// assert.NotSubsetf(t, [1, 3, 4], {1: "one", 2: "two"}, "error message %s", "formatted") +// assert.NotSubsetf(t, {"x": 1, "y": 2}, ["z"], "error message %s", "formatted") +func NotSubsetf(t TestingT, list interface{}, subset interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return NotSubset(t, list, subset, append([]interface{}{msg}, args...)...) +} + +// NotZerof asserts that i is not the zero value for its type. +func NotZerof(t TestingT, i interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return NotZero(t, i, append([]interface{}{msg}, args...)...) +} + +// Panicsf asserts that the code inside the specified PanicTestFunc panics. +// +// assert.Panicsf(t, func(){ GoCrazy() }, "error message %s", "formatted") +func Panicsf(t TestingT, f PanicTestFunc, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return Panics(t, f, append([]interface{}{msg}, args...)...) +} + +// PanicsWithErrorf asserts that the code inside the specified PanicTestFunc +// panics, and that the recovered panic value is an error that satisfies the +// EqualError comparison. +// +// assert.PanicsWithErrorf(t, "crazy error", func(){ GoCrazy() }, "error message %s", "formatted") +func PanicsWithErrorf(t TestingT, errString string, f PanicTestFunc, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return PanicsWithError(t, errString, f, append([]interface{}{msg}, args...)...) +} + +// PanicsWithValuef asserts that the code inside the specified PanicTestFunc panics, and that +// the recovered panic value equals the expected panic value. +// +// assert.PanicsWithValuef(t, "crazy error", func(){ GoCrazy() }, "error message %s", "formatted") +func PanicsWithValuef(t TestingT, expected interface{}, f PanicTestFunc, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return PanicsWithValue(t, expected, f, append([]interface{}{msg}, args...)...) +} + +// Positivef asserts that the specified element is positive +// +// assert.Positivef(t, 1, "error message %s", "formatted") +// assert.Positivef(t, 1.23, "error message %s", "formatted") +func Positivef(t TestingT, e interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return Positive(t, e, append([]interface{}{msg}, args...)...) +} + +// Regexpf asserts that a specified regexp matches a string. +// +// assert.Regexpf(t, regexp.MustCompile("start"), "it's starting", "error message %s", "formatted") +// assert.Regexpf(t, "start...$", "it's not starting", "error message %s", "formatted") +func Regexpf(t TestingT, rx interface{}, str interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return Regexp(t, rx, str, append([]interface{}{msg}, args...)...) +} + +// Samef asserts that two pointers reference the same object. +// +// assert.Samef(t, ptr1, ptr2, "error message %s", "formatted") +// +// Both arguments must be pointer variables. Pointer variable sameness is +// determined based on the equality of both type and value. +func Samef(t TestingT, expected interface{}, actual interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return Same(t, expected, actual, append([]interface{}{msg}, args...)...) +} + +// Subsetf asserts that the list (array, slice, or map) contains all elements +// given in the subset (array, slice, or map). +// Map elements are key-value pairs unless compared with an array or slice where +// only the map key is evaluated. +// +// assert.Subsetf(t, [1, 2, 3], [1, 2], "error message %s", "formatted") +// assert.Subsetf(t, {"x": 1, "y": 2}, {"x": 1}, "error message %s", "formatted") +// assert.Subsetf(t, [1, 2, 3], {1: "one", 2: "two"}, "error message %s", "formatted") +// assert.Subsetf(t, {"x": 1, "y": 2}, ["x"], "error message %s", "formatted") +func Subsetf(t TestingT, list interface{}, subset interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return Subset(t, list, subset, append([]interface{}{msg}, args...)...) +} + +// Truef asserts that the specified value is true. +// +// assert.Truef(t, myBool, "error message %s", "formatted") +func Truef(t TestingT, value bool, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return True(t, value, append([]interface{}{msg}, args...)...) +} + +// WithinDurationf asserts that the two times are within duration delta of each other. +// +// assert.WithinDurationf(t, time.Now(), time.Now(), 10*time.Second, "error message %s", "formatted") +func WithinDurationf(t TestingT, expected time.Time, actual time.Time, delta time.Duration, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return WithinDuration(t, expected, actual, delta, append([]interface{}{msg}, args...)...) +} + +// WithinRangef asserts that a time is within a time range (inclusive). +// +// assert.WithinRangef(t, time.Now(), time.Now().Add(-time.Second), time.Now().Add(time.Second), "error message %s", "formatted") +func WithinRangef(t TestingT, actual time.Time, start time.Time, end time.Time, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return WithinRange(t, actual, start, end, append([]interface{}{msg}, args...)...) +} + +// YAMLEqf asserts that two YAML strings are equivalent. +func YAMLEqf(t TestingT, expected string, actual string, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return YAMLEq(t, expected, actual, append([]interface{}{msg}, args...)...) +} + +// Zerof asserts that i is the zero value for its type. +func Zerof(t TestingT, i interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return Zero(t, i, append([]interface{}{msg}, args...)...) +} diff --git a/pkg/testify/assert/assertion_format.go.tmpl b/pkg/testify/assert/assertion_format.go.tmpl new file mode 100644 index 00000000..d2bb0b81 --- /dev/null +++ b/pkg/testify/assert/assertion_format.go.tmpl @@ -0,0 +1,5 @@ +{{.CommentFormat}} +func {{.DocInfo.Name}}f(t TestingT, {{.ParamsFormat}}) bool { + if h, ok := t.(tHelper); ok { h.Helper() } + return {{.DocInfo.Name}}(t, {{.ForwardedParamsFormat}}) +} diff --git a/pkg/testify/assert/assertion_forward.go b/pkg/testify/assert/assertion_forward.go new file mode 100644 index 00000000..58db9284 --- /dev/null +++ b/pkg/testify/assert/assertion_forward.go @@ -0,0 +1,1723 @@ +// Code generated with github.com/stretchr/testify/_codegen; DO NOT EDIT. + +package assert + +import ( + http "net/http" + url "net/url" + time "time" +) + +// Condition uses a Comparison to assert a complex condition. +func (a *Assertions) Condition(comp Comparison, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Condition(a.t, comp, msgAndArgs...) +} + +// Conditionf uses a Comparison to assert a complex condition. +func (a *Assertions) Conditionf(comp Comparison, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Conditionf(a.t, comp, msg, args...) +} + +// Contains asserts that the specified string, list(array, slice...) or map contains the +// specified substring or element. +// +// a.Contains("Hello World", "World") +// a.Contains(["Hello", "World"], "World") +// a.Contains({"Hello": "World"}, "Hello") +func (a *Assertions) Contains(s interface{}, contains interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Contains(a.t, s, contains, msgAndArgs...) +} + +// Containsf asserts that the specified string, list(array, slice...) or map contains the +// specified substring or element. +// +// a.Containsf("Hello World", "World", "error message %s", "formatted") +// a.Containsf(["Hello", "World"], "World", "error message %s", "formatted") +// a.Containsf({"Hello": "World"}, "Hello", "error message %s", "formatted") +func (a *Assertions) Containsf(s interface{}, contains interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Containsf(a.t, s, contains, msg, args...) +} + +// DirExists checks whether a directory exists in the given path. It also fails +// if the path is a file rather a directory or there is an error checking whether it exists. +func (a *Assertions) DirExists(path string, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return DirExists(a.t, path, msgAndArgs...) +} + +// DirExistsf checks whether a directory exists in the given path. It also fails +// if the path is a file rather a directory or there is an error checking whether it exists. +func (a *Assertions) DirExistsf(path string, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return DirExistsf(a.t, path, msg, args...) +} + +// ElementsMatch asserts that the specified listA(array, slice...) is equal to specified +// 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]) +func (a *Assertions) ElementsMatch(listA interface{}, listB interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return ElementsMatch(a.t, listA, listB, msgAndArgs...) +} + +// ElementsMatchf asserts that the specified listA(array, slice...) is equal to specified +// 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.ElementsMatchf([1, 3, 2, 3], [1, 3, 3, 2], "error message %s", "formatted") +func (a *Assertions) ElementsMatchf(listA interface{}, listB interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return ElementsMatchf(a.t, listA, listB, msg, args...) +} + +// Empty asserts that the given value is "empty". +// +// [Zero values] are "empty". +// +// Arrays are "empty" if every element is the zero value of the type (stricter than "empty"). +// +// Slices, maps and channels with zero length are "empty". +// +// Pointer values are "empty" if the pointer is nil or if the pointed value is "empty". +// +// a.Empty(obj) +// +// [Zero values]: https://go.dev/ref/spec#The_zero_value +func (a *Assertions) Empty(object interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Empty(a.t, object, msgAndArgs...) +} + +// Emptyf asserts that the given value is "empty". +// +// [Zero values] are "empty". +// +// Arrays are "empty" if every element is the zero value of the type (stricter than "empty"). +// +// Slices, maps and channels with zero length are "empty". +// +// Pointer values are "empty" if the pointer is nil or if the pointed value is "empty". +// +// a.Emptyf(obj, "error message %s", "formatted") +// +// [Zero values]: https://go.dev/ref/spec#The_zero_value +func (a *Assertions) Emptyf(object interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Emptyf(a.t, object, msg, args...) +} + +// Equal asserts that two objects are equal. +// +// a.Equal(123, 123) +// +// Pointer variable equality is determined based on the equality of the +// referenced values (as opposed to the memory addresses). Function equality +// cannot be determined and will always fail. +func (a *Assertions) Equal(expected interface{}, actual interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Equal(a.t, expected, actual, msgAndArgs...) +} + +// EqualError asserts that a function returned an error (i.e. not `nil`) +// and that it is equal to the provided error. +// +// actualObj, err := SomeFunction() +// a.EqualError(err, expectedErrorString) +func (a *Assertions) EqualError(theError error, errString string, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return EqualError(a.t, theError, errString, msgAndArgs...) +} + +// EqualErrorf asserts that a function returned an error (i.e. not `nil`) +// and that it is equal to the provided error. +// +// actualObj, err := SomeFunction() +// a.EqualErrorf(err, expectedErrorString, "error message %s", "formatted") +func (a *Assertions) EqualErrorf(theError error, errString string, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return EqualErrorf(a.t, theError, errString, msg, args...) +} + +// EqualExportedValues asserts that the types of two objects are equal and their public +// fields are also equal. This is useful for comparing structs that have private fields +// that could potentially differ. +// +// type S struct { +// Exported int +// notExported int +// } +// a.EqualExportedValues(S{1, 2}, S{1, 3}) => true +// a.EqualExportedValues(S{1, 2}, S{2, 3}) => false +func (a *Assertions) EqualExportedValues(expected interface{}, actual interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return EqualExportedValues(a.t, expected, actual, msgAndArgs...) +} + +// EqualExportedValuesf asserts that the types of two objects are equal and their public +// fields are also equal. This is useful for comparing structs that have private fields +// that could potentially differ. +// +// type S struct { +// Exported int +// notExported int +// } +// a.EqualExportedValuesf(S{1, 2}, S{1, 3}, "error message %s", "formatted") => true +// a.EqualExportedValuesf(S{1, 2}, S{2, 3}, "error message %s", "formatted") => false +func (a *Assertions) EqualExportedValuesf(expected interface{}, actual interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return EqualExportedValuesf(a.t, expected, actual, msg, args...) +} + +// EqualValues asserts that two objects are equal or convertible to the larger +// type and equal. +// +// a.EqualValues(uint32(123), int32(123)) +func (a *Assertions) EqualValues(expected interface{}, actual interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return EqualValues(a.t, expected, actual, msgAndArgs...) +} + +// EqualValuesf asserts that two objects are equal or convertible to the larger +// type and equal. +// +// a.EqualValuesf(uint32(123), int32(123), "error message %s", "formatted") +func (a *Assertions) EqualValuesf(expected interface{}, actual interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return EqualValuesf(a.t, expected, actual, msg, args...) +} + +// Equalf asserts that two objects are equal. +// +// a.Equalf(123, 123, "error message %s", "formatted") +// +// Pointer variable equality is determined based on the equality of the +// referenced values (as opposed to the memory addresses). Function equality +// cannot be determined and will always fail. +func (a *Assertions) Equalf(expected interface{}, actual interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Equalf(a.t, expected, actual, msg, args...) +} + +// Error asserts that a function returned an error (i.e. not `nil`). +// +// actualObj, err := SomeFunction() +// a.Error(err) +func (a *Assertions) Error(err error, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Error(a.t, err, msgAndArgs...) +} + +// ErrorAs asserts that at least one of the errors in err's chain matches target, and if so, sets target to that error value. +// This is a wrapper for errors.As. +func (a *Assertions) ErrorAs(err error, target interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return ErrorAs(a.t, err, target, msgAndArgs...) +} + +// ErrorAsf asserts that at least one of the errors in err's chain matches target, and if so, sets target to that error value. +// This is a wrapper for errors.As. +func (a *Assertions) ErrorAsf(err error, target interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return ErrorAsf(a.t, err, target, msg, args...) +} + +// ErrorContains asserts that a function returned an error (i.e. not `nil`) +// and that the error contains the specified substring. +// +// actualObj, err := SomeFunction() +// a.ErrorContains(err, expectedErrorSubString) +func (a *Assertions) ErrorContains(theError error, contains string, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return ErrorContains(a.t, theError, contains, msgAndArgs...) +} + +// ErrorContainsf asserts that a function returned an error (i.e. not `nil`) +// and that the error contains the specified substring. +// +// actualObj, err := SomeFunction() +// a.ErrorContainsf(err, expectedErrorSubString, "error message %s", "formatted") +func (a *Assertions) ErrorContainsf(theError error, contains string, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return ErrorContainsf(a.t, theError, contains, msg, args...) +} + +// ErrorIs asserts that at least one of the errors in err's chain matches target. +// This is a wrapper for errors.Is. +func (a *Assertions) ErrorIs(err error, target error, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return ErrorIs(a.t, err, target, msgAndArgs...) +} + +// ErrorIsf asserts that at least one of the errors in err's chain matches target. +// This is a wrapper for errors.Is. +func (a *Assertions) ErrorIsf(err error, target error, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return ErrorIsf(a.t, err, target, msg, args...) +} + +// Errorf asserts that a function returned an error (i.e. not `nil`). +// +// actualObj, err := SomeFunction() +// a.Errorf(err, "error message %s", "formatted") +func (a *Assertions) Errorf(err error, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Errorf(a.t, err, msg, args...) +} + +// Eventually asserts that given condition will be met in waitFor time, +// periodically checking target function each tick. +// +// a.Eventually(func() bool { return true; }, time.Second, 10*time.Millisecond) +func (a *Assertions) Eventually(condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Eventually(a.t, condition, waitFor, tick, msgAndArgs...) +} + +// EventuallyWithT asserts that given condition will be met in waitFor time, +// periodically checking target function each tick. In contrast to Eventually, +// it supplies a CollectT to the condition function, so that the condition +// function can use the CollectT to call other assertions. +// The condition is considered "met" if no errors are raised in a tick. +// The supplied CollectT collects all errors from one tick (if there are any). +// If the condition is not met before waitFor, the collected errors of +// the last tick are copied to t. +// +// externalValue := false +// go func() { +// time.Sleep(8*time.Second) +// externalValue = true +// }() +// a.EventuallyWithT(func(c *assert.CollectT) { +// // add assertions as needed; any assertion failure will fail the current tick +// assert.True(c, externalValue, "expected 'externalValue' to be true") +// }, 10*time.Second, 1*time.Second, "external state has not changed to 'true'; still false") +func (a *Assertions) EventuallyWithT(condition func(collect *CollectT), waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return EventuallyWithT(a.t, condition, waitFor, tick, msgAndArgs...) +} + +// EventuallyWithTf asserts that given condition will be met in waitFor time, +// periodically checking target function each tick. In contrast to Eventually, +// it supplies a CollectT to the condition function, so that the condition +// function can use the CollectT to call other assertions. +// The condition is considered "met" if no errors are raised in a tick. +// The supplied CollectT collects all errors from one tick (if there are any). +// If the condition is not met before waitFor, the collected errors of +// the last tick are copied to t. +// +// externalValue := false +// go func() { +// time.Sleep(8*time.Second) +// externalValue = true +// }() +// a.EventuallyWithTf(func(c *assert.CollectT, "error message %s", "formatted") { +// // add assertions as needed; any assertion failure will fail the current tick +// assert.True(c, externalValue, "expected 'externalValue' to be true") +// }, 10*time.Second, 1*time.Second, "external state has not changed to 'true'; still false") +func (a *Assertions) EventuallyWithTf(condition func(collect *CollectT), waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return EventuallyWithTf(a.t, condition, waitFor, tick, msg, args...) +} + +// Eventuallyf asserts that given condition will be met in waitFor time, +// periodically checking target function each tick. +// +// a.Eventuallyf(func() bool { return true; }, time.Second, 10*time.Millisecond, "error message %s", "formatted") +func (a *Assertions) Eventuallyf(condition func() bool, waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Eventuallyf(a.t, condition, waitFor, tick, msg, args...) +} + +// Exactly asserts that two objects are equal in value and type. +// +// a.Exactly(int32(123), int64(123)) +func (a *Assertions) Exactly(expected interface{}, actual interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Exactly(a.t, expected, actual, msgAndArgs...) +} + +// Exactlyf asserts that two objects are equal in value and type. +// +// a.Exactlyf(int32(123), int64(123), "error message %s", "formatted") +func (a *Assertions) Exactlyf(expected interface{}, actual interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Exactlyf(a.t, expected, actual, msg, args...) +} + +// Fail reports a failure through +func (a *Assertions) Fail(failureMessage string, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Fail(a.t, failureMessage, msgAndArgs...) +} + +// FailNow fails test +func (a *Assertions) FailNow(failureMessage string, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return FailNow(a.t, failureMessage, msgAndArgs...) +} + +// FailNowf fails test +func (a *Assertions) FailNowf(failureMessage string, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return FailNowf(a.t, failureMessage, msg, args...) +} + +// Failf reports a failure through +func (a *Assertions) Failf(failureMessage string, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Failf(a.t, failureMessage, msg, args...) +} + +// False asserts that the specified value is false. +// +// a.False(myBool) +func (a *Assertions) False(value bool, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return False(a.t, value, msgAndArgs...) +} + +// Falsef asserts that the specified value is false. +// +// a.Falsef(myBool, "error message %s", "formatted") +func (a *Assertions) Falsef(value bool, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Falsef(a.t, value, msg, args...) +} + +// FileExists checks whether a file exists in the given path. It also fails if +// the path points to a directory or there is an error when trying to check the file. +func (a *Assertions) FileExists(path string, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return FileExists(a.t, path, msgAndArgs...) +} + +// FileExistsf checks whether a file exists in the given path. It also fails if +// the path points to a directory or there is an error when trying to check the file. +func (a *Assertions) FileExistsf(path string, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return FileExistsf(a.t, path, msg, args...) +} + +// Greater asserts that the first element is greater than the second +// +// a.Greater(2, 1) +// a.Greater(float64(2), float64(1)) +// a.Greater("b", "a") +func (a *Assertions) Greater(e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Greater(a.t, e1, e2, msgAndArgs...) +} + +// GreaterOrEqual asserts that the first element is greater than or equal to the second +// +// a.GreaterOrEqual(2, 1) +// a.GreaterOrEqual(2, 2) +// a.GreaterOrEqual("b", "a") +// a.GreaterOrEqual("b", "b") +func (a *Assertions) GreaterOrEqual(e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return GreaterOrEqual(a.t, e1, e2, msgAndArgs...) +} + +// GreaterOrEqualf asserts that the first element is greater than or equal to the second +// +// a.GreaterOrEqualf(2, 1, "error message %s", "formatted") +// a.GreaterOrEqualf(2, 2, "error message %s", "formatted") +// a.GreaterOrEqualf("b", "a", "error message %s", "formatted") +// a.GreaterOrEqualf("b", "b", "error message %s", "formatted") +func (a *Assertions) GreaterOrEqualf(e1 interface{}, e2 interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return GreaterOrEqualf(a.t, e1, e2, msg, args...) +} + +// Greaterf asserts that the first element is greater than the second +// +// a.Greaterf(2, 1, "error message %s", "formatted") +// a.Greaterf(float64(2), float64(1), "error message %s", "formatted") +// a.Greaterf("b", "a", "error message %s", "formatted") +func (a *Assertions) Greaterf(e1 interface{}, e2 interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Greaterf(a.t, e1, e2, msg, args...) +} + +// HTTPBodyContains asserts that a specified handler returns a +// body that contains a string. +// +// a.HTTPBodyContains(myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky") +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPBodyContains(handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return HTTPBodyContains(a.t, handler, method, url, values, str, msgAndArgs...) +} + +// HTTPBodyContainsf asserts that a specified handler returns a +// body that contains a string. +// +// a.HTTPBodyContainsf(myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky", "error message %s", "formatted") +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPBodyContainsf(handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return HTTPBodyContainsf(a.t, handler, method, url, values, str, msg, args...) +} + +// HTTPBodyNotContains asserts that a specified handler returns a +// body that does not contain a string. +// +// a.HTTPBodyNotContains(myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky") +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPBodyNotContains(handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return HTTPBodyNotContains(a.t, handler, method, url, values, str, msgAndArgs...) +} + +// HTTPBodyNotContainsf asserts that a specified handler returns a +// body that does not contain a string. +// +// a.HTTPBodyNotContainsf(myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky", "error message %s", "formatted") +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPBodyNotContainsf(handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return HTTPBodyNotContainsf(a.t, handler, method, url, values, str, msg, args...) +} + +// HTTPError asserts that a specified handler returns an error status code. +// +// a.HTTPError(myHandler, "POST", "/a/b/c", url.Values{"a": []string{"b", "c"}} +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPError(handler http.HandlerFunc, method string, url string, values url.Values, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return HTTPError(a.t, handler, method, url, values, msgAndArgs...) +} + +// HTTPErrorf asserts that a specified handler returns an error status code. +// +// a.HTTPErrorf(myHandler, "POST", "/a/b/c", url.Values{"a": []string{"b", "c"}} +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPErrorf(handler http.HandlerFunc, method string, url string, values url.Values, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return HTTPErrorf(a.t, handler, method, url, values, msg, args...) +} + +// HTTPRedirect asserts that a specified handler returns a redirect status code. +// +// a.HTTPRedirect(myHandler, "GET", "/a/b/c", url.Values{"a": []string{"b", "c"}} +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPRedirect(handler http.HandlerFunc, method string, url string, values url.Values, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return HTTPRedirect(a.t, handler, method, url, values, msgAndArgs...) +} + +// HTTPRedirectf asserts that a specified handler returns a redirect status code. +// +// a.HTTPRedirectf(myHandler, "GET", "/a/b/c", url.Values{"a": []string{"b", "c"}} +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPRedirectf(handler http.HandlerFunc, method string, url string, values url.Values, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return HTTPRedirectf(a.t, handler, method, url, values, msg, args...) +} + +// HTTPStatusCode asserts that a specified handler returns a specified status code. +// +// a.HTTPStatusCode(myHandler, "GET", "/notImplemented", nil, 501) +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPStatusCode(handler http.HandlerFunc, method string, url string, values url.Values, statuscode int, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return HTTPStatusCode(a.t, handler, method, url, values, statuscode, msgAndArgs...) +} + +// HTTPStatusCodef asserts that a specified handler returns a specified status code. +// +// a.HTTPStatusCodef(myHandler, "GET", "/notImplemented", nil, 501, "error message %s", "formatted") +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPStatusCodef(handler http.HandlerFunc, method string, url string, values url.Values, statuscode int, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return HTTPStatusCodef(a.t, handler, method, url, values, statuscode, msg, args...) +} + +// HTTPSuccess asserts that a specified handler returns a success status code. +// +// a.HTTPSuccess(myHandler, "POST", "http://www.google.com", nil) +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPSuccess(handler http.HandlerFunc, method string, url string, values url.Values, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return HTTPSuccess(a.t, handler, method, url, values, msgAndArgs...) +} + +// HTTPSuccessf asserts that a specified handler returns a success status code. +// +// a.HTTPSuccessf(myHandler, "POST", "http://www.google.com", nil, "error message %s", "formatted") +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPSuccessf(handler http.HandlerFunc, method string, url string, values url.Values, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return HTTPSuccessf(a.t, handler, method, url, values, msg, args...) +} + +// Implements asserts that an object is implemented by the specified interface. +// +// a.Implements((*MyInterface)(nil), new(MyObject)) +func (a *Assertions) Implements(interfaceObject interface{}, object interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Implements(a.t, interfaceObject, object, msgAndArgs...) +} + +// Implementsf asserts that an object is implemented by the specified interface. +// +// a.Implementsf((*MyInterface)(nil), new(MyObject), "error message %s", "formatted") +func (a *Assertions) Implementsf(interfaceObject interface{}, object interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Implementsf(a.t, interfaceObject, object, msg, args...) +} + +// InDelta asserts that the two numerals are within delta of each other. +// +// a.InDelta(math.Pi, 22/7.0, 0.01) +func (a *Assertions) InDelta(expected interface{}, actual interface{}, delta float64, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return InDelta(a.t, expected, actual, delta, msgAndArgs...) +} + +// InDeltaMapValues is the same as InDelta, but it compares all values between two maps. Both maps must have exactly the same keys. +func (a *Assertions) InDeltaMapValues(expected interface{}, actual interface{}, delta float64, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return InDeltaMapValues(a.t, expected, actual, delta, msgAndArgs...) +} + +// InDeltaMapValuesf is the same as InDelta, but it compares all values between two maps. Both maps must have exactly the same keys. +func (a *Assertions) InDeltaMapValuesf(expected interface{}, actual interface{}, delta float64, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return InDeltaMapValuesf(a.t, expected, actual, delta, msg, args...) +} + +// InDeltaSlice is the same as InDelta, except it compares two slices. +func (a *Assertions) InDeltaSlice(expected interface{}, actual interface{}, delta float64, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return InDeltaSlice(a.t, expected, actual, delta, msgAndArgs...) +} + +// InDeltaSlicef is the same as InDelta, except it compares two slices. +func (a *Assertions) InDeltaSlicef(expected interface{}, actual interface{}, delta float64, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return InDeltaSlicef(a.t, expected, actual, delta, msg, args...) +} + +// InDeltaf asserts that the two numerals are within delta of each other. +// +// a.InDeltaf(math.Pi, 22/7.0, 0.01, "error message %s", "formatted") +func (a *Assertions) InDeltaf(expected interface{}, actual interface{}, delta float64, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return InDeltaf(a.t, expected, actual, delta, msg, args...) +} + +// InEpsilon asserts that expected and actual have a relative error less than epsilon +func (a *Assertions) InEpsilon(expected interface{}, actual interface{}, epsilon float64, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return InEpsilon(a.t, expected, actual, epsilon, msgAndArgs...) +} + +// InEpsilonSlice is the same as InEpsilon, except it compares each value from two slices. +func (a *Assertions) InEpsilonSlice(expected interface{}, actual interface{}, epsilon float64, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return InEpsilonSlice(a.t, expected, actual, epsilon, msgAndArgs...) +} + +// InEpsilonSlicef is the same as InEpsilon, except it compares each value from two slices. +func (a *Assertions) InEpsilonSlicef(expected interface{}, actual interface{}, epsilon float64, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return InEpsilonSlicef(a.t, expected, actual, epsilon, msg, args...) +} + +// InEpsilonf asserts that expected and actual have a relative error less than epsilon +func (a *Assertions) InEpsilonf(expected interface{}, actual interface{}, epsilon float64, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return InEpsilonf(a.t, expected, actual, epsilon, msg, args...) +} + +// IsDecreasing asserts that the collection is decreasing +// +// a.IsDecreasing([]int{2, 1, 0}) +// a.IsDecreasing([]float{2, 1}) +// a.IsDecreasing([]string{"b", "a"}) +func (a *Assertions) IsDecreasing(object interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return IsDecreasing(a.t, object, msgAndArgs...) +} + +// IsDecreasingf asserts that the collection is decreasing +// +// a.IsDecreasingf([]int{2, 1, 0}, "error message %s", "formatted") +// a.IsDecreasingf([]float{2, 1}, "error message %s", "formatted") +// a.IsDecreasingf([]string{"b", "a"}, "error message %s", "formatted") +func (a *Assertions) IsDecreasingf(object interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return IsDecreasingf(a.t, object, msg, args...) +} + +// IsIncreasing asserts that the collection is increasing +// +// a.IsIncreasing([]int{1, 2, 3}) +// a.IsIncreasing([]float{1, 2}) +// a.IsIncreasing([]string{"a", "b"}) +func (a *Assertions) IsIncreasing(object interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return IsIncreasing(a.t, object, msgAndArgs...) +} + +// IsIncreasingf asserts that the collection is increasing +// +// a.IsIncreasingf([]int{1, 2, 3}, "error message %s", "formatted") +// a.IsIncreasingf([]float{1, 2}, "error message %s", "formatted") +// a.IsIncreasingf([]string{"a", "b"}, "error message %s", "formatted") +func (a *Assertions) IsIncreasingf(object interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return IsIncreasingf(a.t, object, msg, args...) +} + +// IsNonDecreasing asserts that the collection is not decreasing +// +// a.IsNonDecreasing([]int{1, 1, 2}) +// a.IsNonDecreasing([]float{1, 2}) +// a.IsNonDecreasing([]string{"a", "b"}) +func (a *Assertions) IsNonDecreasing(object interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return IsNonDecreasing(a.t, object, msgAndArgs...) +} + +// IsNonDecreasingf asserts that the collection is not decreasing +// +// a.IsNonDecreasingf([]int{1, 1, 2}, "error message %s", "formatted") +// a.IsNonDecreasingf([]float{1, 2}, "error message %s", "formatted") +// a.IsNonDecreasingf([]string{"a", "b"}, "error message %s", "formatted") +func (a *Assertions) IsNonDecreasingf(object interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return IsNonDecreasingf(a.t, object, msg, args...) +} + +// IsNonIncreasing asserts that the collection is not increasing +// +// a.IsNonIncreasing([]int{2, 1, 1}) +// a.IsNonIncreasing([]float{2, 1}) +// a.IsNonIncreasing([]string{"b", "a"}) +func (a *Assertions) IsNonIncreasing(object interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return IsNonIncreasing(a.t, object, msgAndArgs...) +} + +// IsNonIncreasingf asserts that the collection is not increasing +// +// a.IsNonIncreasingf([]int{2, 1, 1}, "error message %s", "formatted") +// a.IsNonIncreasingf([]float{2, 1}, "error message %s", "formatted") +// a.IsNonIncreasingf([]string{"b", "a"}, "error message %s", "formatted") +func (a *Assertions) IsNonIncreasingf(object interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return IsNonIncreasingf(a.t, object, msg, args...) +} + +// IsNotType asserts that the specified objects are not of the same type. +// +// a.IsNotType(&NotMyStruct{}, &MyStruct{}) +func (a *Assertions) IsNotType(theType interface{}, object interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return IsNotType(a.t, theType, object, msgAndArgs...) +} + +// IsNotTypef asserts that the specified objects are not of the same type. +// +// a.IsNotTypef(&NotMyStruct{}, &MyStruct{}, "error message %s", "formatted") +func (a *Assertions) IsNotTypef(theType interface{}, object interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return IsNotTypef(a.t, theType, object, msg, args...) +} + +// IsType asserts that the specified objects are of the same type. +// +// a.IsType(&MyStruct{}, &MyStruct{}) +func (a *Assertions) IsType(expectedType interface{}, object interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return IsType(a.t, expectedType, object, msgAndArgs...) +} + +// IsTypef asserts that the specified objects are of the same type. +// +// a.IsTypef(&MyStruct{}, &MyStruct{}, "error message %s", "formatted") +func (a *Assertions) IsTypef(expectedType interface{}, object interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return IsTypef(a.t, expectedType, object, msg, args...) +} + +// JSONEq asserts that two JSON strings are equivalent. +// +// a.JSONEq(`{"hello": "world", "foo": "bar"}`, `{"foo": "bar", "hello": "world"}`) +func (a *Assertions) JSONEq(expected string, actual string, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return JSONEq(a.t, expected, actual, msgAndArgs...) +} + +// JSONEqf asserts that two JSON strings are equivalent. +// +// a.JSONEqf(`{"hello": "world", "foo": "bar"}`, `{"foo": "bar", "hello": "world"}`, "error message %s", "formatted") +func (a *Assertions) JSONEqf(expected string, actual string, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return JSONEqf(a.t, expected, actual, msg, args...) +} + +// Len asserts that the specified object has specific length. +// Len also fails if the object has a type that len() not accept. +// +// a.Len(mySlice, 3) +func (a *Assertions) Len(object interface{}, length int, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Len(a.t, object, length, msgAndArgs...) +} + +// Lenf asserts that the specified object has specific length. +// Lenf also fails if the object has a type that len() not accept. +// +// a.Lenf(mySlice, 3, "error message %s", "formatted") +func (a *Assertions) Lenf(object interface{}, length int, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Lenf(a.t, object, length, msg, args...) +} + +// Less asserts that the first element is less than the second +// +// a.Less(1, 2) +// a.Less(float64(1), float64(2)) +// a.Less("a", "b") +func (a *Assertions) Less(e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Less(a.t, e1, e2, msgAndArgs...) +} + +// LessOrEqual asserts that the first element is less than or equal to the second +// +// a.LessOrEqual(1, 2) +// a.LessOrEqual(2, 2) +// a.LessOrEqual("a", "b") +// a.LessOrEqual("b", "b") +func (a *Assertions) LessOrEqual(e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return LessOrEqual(a.t, e1, e2, msgAndArgs...) +} + +// LessOrEqualf asserts that the first element is less than or equal to the second +// +// a.LessOrEqualf(1, 2, "error message %s", "formatted") +// a.LessOrEqualf(2, 2, "error message %s", "formatted") +// a.LessOrEqualf("a", "b", "error message %s", "formatted") +// a.LessOrEqualf("b", "b", "error message %s", "formatted") +func (a *Assertions) LessOrEqualf(e1 interface{}, e2 interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return LessOrEqualf(a.t, e1, e2, msg, args...) +} + +// Lessf asserts that the first element is less than the second +// +// a.Lessf(1, 2, "error message %s", "formatted") +// a.Lessf(float64(1), float64(2), "error message %s", "formatted") +// a.Lessf("a", "b", "error message %s", "formatted") +func (a *Assertions) Lessf(e1 interface{}, e2 interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Lessf(a.t, e1, e2, msg, args...) +} + +// Negative asserts that the specified element is negative +// +// a.Negative(-1) +// a.Negative(-1.23) +func (a *Assertions) Negative(e interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Negative(a.t, e, msgAndArgs...) +} + +// Negativef asserts that the specified element is negative +// +// a.Negativef(-1, "error message %s", "formatted") +// a.Negativef(-1.23, "error message %s", "formatted") +func (a *Assertions) Negativef(e interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Negativef(a.t, e, msg, args...) +} + +// Never asserts that the given condition doesn't satisfy in waitFor time, +// periodically checking the target function each tick. +// +// a.Never(func() bool { return false; }, time.Second, 10*time.Millisecond) +func (a *Assertions) Never(condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Never(a.t, condition, waitFor, tick, msgAndArgs...) +} + +// Neverf asserts that the given condition doesn't satisfy in waitFor time, +// periodically checking the target function each tick. +// +// a.Neverf(func() bool { return false; }, time.Second, 10*time.Millisecond, "error message %s", "formatted") +func (a *Assertions) Neverf(condition func() bool, waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Neverf(a.t, condition, waitFor, tick, msg, args...) +} + +// Nil asserts that the specified object is nil. +// +// a.Nil(err) +func (a *Assertions) Nil(object interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Nil(a.t, object, msgAndArgs...) +} + +// Nilf asserts that the specified object is nil. +// +// a.Nilf(err, "error message %s", "formatted") +func (a *Assertions) Nilf(object interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Nilf(a.t, object, msg, args...) +} + +// NoDirExists checks whether a directory does not exist in the given path. +// It fails if the path points to an existing _directory_ only. +func (a *Assertions) NoDirExists(path string, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return NoDirExists(a.t, path, msgAndArgs...) +} + +// NoDirExistsf checks whether a directory does not exist in the given path. +// It fails if the path points to an existing _directory_ only. +func (a *Assertions) NoDirExistsf(path string, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return NoDirExistsf(a.t, path, msg, args...) +} + +// NoError asserts that a function returned no error (i.e. `nil`). +// +// actualObj, err := SomeFunction() +// if a.NoError(err) { +// assert.Equal(t, expectedObj, actualObj) +// } +func (a *Assertions) NoError(err error, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return NoError(a.t, err, msgAndArgs...) +} + +// NoErrorf asserts that a function returned no error (i.e. `nil`). +// +// actualObj, err := SomeFunction() +// if a.NoErrorf(err, "error message %s", "formatted") { +// assert.Equal(t, expectedObj, actualObj) +// } +func (a *Assertions) NoErrorf(err error, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return NoErrorf(a.t, err, msg, args...) +} + +// NoFileExists checks whether a file does not exist in a given path. It fails +// if the path points to an existing _file_ only. +func (a *Assertions) NoFileExists(path string, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return NoFileExists(a.t, path, msgAndArgs...) +} + +// NoFileExistsf checks whether a file does not exist in a given path. It fails +// if the path points to an existing _file_ only. +func (a *Assertions) NoFileExistsf(path string, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return NoFileExistsf(a.t, path, msg, args...) +} + +// NotContains asserts that the specified string, list(array, slice...) or map does NOT contain the +// specified substring or element. +// +// a.NotContains("Hello World", "Earth") +// a.NotContains(["Hello", "World"], "Earth") +// a.NotContains({"Hello": "World"}, "Earth") +func (a *Assertions) NotContains(s interface{}, contains interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return NotContains(a.t, s, contains, msgAndArgs...) +} + +// NotContainsf asserts that the specified string, list(array, slice...) or map does NOT contain the +// specified substring or element. +// +// a.NotContainsf("Hello World", "Earth", "error message %s", "formatted") +// a.NotContainsf(["Hello", "World"], "Earth", "error message %s", "formatted") +// a.NotContainsf({"Hello": "World"}, "Earth", "error message %s", "formatted") +func (a *Assertions) NotContainsf(s interface{}, contains interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return NotContainsf(a.t, s, contains, msg, args...) +} + +// NotElementsMatch asserts that the specified listA(array, slice...) is NOT equal to specified +// 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 not match. +// This is an inverse of ElementsMatch. +// +// a.NotElementsMatch([1, 1, 2, 3], [1, 1, 2, 3]) -> false +// +// a.NotElementsMatch([1, 1, 2, 3], [1, 2, 3]) -> true +// +// a.NotElementsMatch([1, 2, 3], [1, 2, 4]) -> true +func (a *Assertions) NotElementsMatch(listA interface{}, listB interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return NotElementsMatch(a.t, listA, listB, msgAndArgs...) +} + +// NotElementsMatchf asserts that the specified listA(array, slice...) is NOT equal to specified +// 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 not match. +// This is an inverse of ElementsMatch. +// +// a.NotElementsMatchf([1, 1, 2, 3], [1, 1, 2, 3], "error message %s", "formatted") -> false +// +// a.NotElementsMatchf([1, 1, 2, 3], [1, 2, 3], "error message %s", "formatted") -> true +// +// a.NotElementsMatchf([1, 2, 3], [1, 2, 4], "error message %s", "formatted") -> true +func (a *Assertions) NotElementsMatchf(listA interface{}, listB interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return NotElementsMatchf(a.t, listA, listB, msg, args...) +} + +// NotEmpty asserts that the specified object is NOT [Empty]. +// +// if a.NotEmpty(obj) { +// assert.Equal(t, "two", obj[1]) +// } +func (a *Assertions) NotEmpty(object interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return NotEmpty(a.t, object, msgAndArgs...) +} + +// NotEmptyf asserts that the specified object is NOT [Empty]. +// +// if a.NotEmptyf(obj, "error message %s", "formatted") { +// assert.Equal(t, "two", obj[1]) +// } +func (a *Assertions) NotEmptyf(object interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return NotEmptyf(a.t, object, msg, args...) +} + +// NotEqual asserts that the specified values are NOT equal. +// +// a.NotEqual(obj1, obj2) +// +// Pointer variable equality is determined based on the equality of the +// referenced values (as opposed to the memory addresses). +func (a *Assertions) NotEqual(expected interface{}, actual interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return NotEqual(a.t, expected, actual, msgAndArgs...) +} + +// NotEqualValues asserts that two objects are not equal even when converted to the same type +// +// a.NotEqualValues(obj1, obj2) +func (a *Assertions) NotEqualValues(expected interface{}, actual interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return NotEqualValues(a.t, expected, actual, msgAndArgs...) +} + +// NotEqualValuesf asserts that two objects are not equal even when converted to the same type +// +// a.NotEqualValuesf(obj1, obj2, "error message %s", "formatted") +func (a *Assertions) NotEqualValuesf(expected interface{}, actual interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return NotEqualValuesf(a.t, expected, actual, msg, args...) +} + +// NotEqualf asserts that the specified values are NOT equal. +// +// a.NotEqualf(obj1, obj2, "error message %s", "formatted") +// +// Pointer variable equality is determined based on the equality of the +// referenced values (as opposed to the memory addresses). +func (a *Assertions) NotEqualf(expected interface{}, actual interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return NotEqualf(a.t, expected, actual, msg, args...) +} + +// NotErrorAs asserts that none of the errors in err's chain matches target, +// but if so, sets target to that error value. +func (a *Assertions) NotErrorAs(err error, target interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return NotErrorAs(a.t, err, target, msgAndArgs...) +} + +// NotErrorAsf asserts that none of the errors in err's chain matches target, +// but if so, sets target to that error value. +func (a *Assertions) NotErrorAsf(err error, target interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return NotErrorAsf(a.t, err, target, msg, args...) +} + +// NotErrorIs asserts that none of the errors in err's chain matches target. +// This is a wrapper for errors.Is. +func (a *Assertions) NotErrorIs(err error, target error, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return NotErrorIs(a.t, err, target, msgAndArgs...) +} + +// NotErrorIsf asserts that none of the errors in err's chain matches target. +// This is a wrapper for errors.Is. +func (a *Assertions) NotErrorIsf(err error, target error, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return NotErrorIsf(a.t, err, target, msg, args...) +} + +// NotImplements asserts that an object does not implement the specified interface. +// +// a.NotImplements((*MyInterface)(nil), new(MyObject)) +func (a *Assertions) NotImplements(interfaceObject interface{}, object interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return NotImplements(a.t, interfaceObject, object, msgAndArgs...) +} + +// NotImplementsf asserts that an object does not implement the specified interface. +// +// a.NotImplementsf((*MyInterface)(nil), new(MyObject), "error message %s", "formatted") +func (a *Assertions) NotImplementsf(interfaceObject interface{}, object interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return NotImplementsf(a.t, interfaceObject, object, msg, args...) +} + +// NotNil asserts that the specified object is not nil. +// +// a.NotNil(err) +func (a *Assertions) NotNil(object interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return NotNil(a.t, object, msgAndArgs...) +} + +// NotNilf asserts that the specified object is not nil. +// +// a.NotNilf(err, "error message %s", "formatted") +func (a *Assertions) NotNilf(object interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return NotNilf(a.t, object, msg, args...) +} + +// NotPanics asserts that the code inside the specified PanicTestFunc does NOT panic. +// +// a.NotPanics(func(){ RemainCalm() }) +func (a *Assertions) NotPanics(f PanicTestFunc, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return NotPanics(a.t, f, msgAndArgs...) +} + +// NotPanicsf asserts that the code inside the specified PanicTestFunc does NOT panic. +// +// a.NotPanicsf(func(){ RemainCalm() }, "error message %s", "formatted") +func (a *Assertions) NotPanicsf(f PanicTestFunc, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return NotPanicsf(a.t, f, msg, args...) +} + +// NotRegexp asserts that a specified regexp does not match a string. +// +// a.NotRegexp(regexp.MustCompile("starts"), "it's starting") +// a.NotRegexp("^start", "it's not starting") +func (a *Assertions) NotRegexp(rx interface{}, str interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return NotRegexp(a.t, rx, str, msgAndArgs...) +} + +// NotRegexpf asserts that a specified regexp does not match a string. +// +// a.NotRegexpf(regexp.MustCompile("starts"), "it's starting", "error message %s", "formatted") +// a.NotRegexpf("^start", "it's not starting", "error message %s", "formatted") +func (a *Assertions) NotRegexpf(rx interface{}, str interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return NotRegexpf(a.t, rx, str, msg, args...) +} + +// NotSame asserts that two pointers do not reference the same object. +// +// a.NotSame(ptr1, ptr2) +// +// Both arguments must be pointer variables. Pointer variable sameness is +// determined based on the equality of both type and value. +func (a *Assertions) NotSame(expected interface{}, actual interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return NotSame(a.t, expected, actual, msgAndArgs...) +} + +// NotSamef asserts that two pointers do not reference the same object. +// +// a.NotSamef(ptr1, ptr2, "error message %s", "formatted") +// +// Both arguments must be pointer variables. Pointer variable sameness is +// determined based on the equality of both type and value. +func (a *Assertions) NotSamef(expected interface{}, actual interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return NotSamef(a.t, expected, actual, msg, args...) +} + +// NotSubset asserts that the list (array, slice, or map) does NOT contain all +// elements given in the subset (array, slice, or map). +// Map elements are key-value pairs unless compared with an array or slice where +// only the map key is evaluated. +// +// a.NotSubset([1, 3, 4], [1, 2]) +// a.NotSubset({"x": 1, "y": 2}, {"z": 3}) +// a.NotSubset([1, 3, 4], {1: "one", 2: "two"}) +// a.NotSubset({"x": 1, "y": 2}, ["z"]) +func (a *Assertions) NotSubset(list interface{}, subset interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return NotSubset(a.t, list, subset, msgAndArgs...) +} + +// NotSubsetf asserts that the list (array, slice, or map) does NOT contain all +// elements given in the subset (array, slice, or map). +// Map elements are key-value pairs unless compared with an array or slice where +// only the map key is evaluated. +// +// a.NotSubsetf([1, 3, 4], [1, 2], "error message %s", "formatted") +// a.NotSubsetf({"x": 1, "y": 2}, {"z": 3}, "error message %s", "formatted") +// a.NotSubsetf([1, 3, 4], {1: "one", 2: "two"}, "error message %s", "formatted") +// a.NotSubsetf({"x": 1, "y": 2}, ["z"], "error message %s", "formatted") +func (a *Assertions) NotSubsetf(list interface{}, subset interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return NotSubsetf(a.t, list, subset, msg, args...) +} + +// NotZero asserts that i is not the zero value for its type. +func (a *Assertions) NotZero(i interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return NotZero(a.t, i, msgAndArgs...) +} + +// NotZerof asserts that i is not the zero value for its type. +func (a *Assertions) NotZerof(i interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return NotZerof(a.t, i, msg, args...) +} + +// Panics asserts that the code inside the specified PanicTestFunc panics. +// +// a.Panics(func(){ GoCrazy() }) +func (a *Assertions) Panics(f PanicTestFunc, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Panics(a.t, f, msgAndArgs...) +} + +// PanicsWithError asserts that the code inside the specified PanicTestFunc +// panics, and that the recovered panic value is an error that satisfies the +// EqualError comparison. +// +// a.PanicsWithError("crazy error", func(){ GoCrazy() }) +func (a *Assertions) PanicsWithError(errString string, f PanicTestFunc, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return PanicsWithError(a.t, errString, f, msgAndArgs...) +} + +// PanicsWithErrorf asserts that the code inside the specified PanicTestFunc +// panics, and that the recovered panic value is an error that satisfies the +// EqualError comparison. +// +// a.PanicsWithErrorf("crazy error", func(){ GoCrazy() }, "error message %s", "formatted") +func (a *Assertions) PanicsWithErrorf(errString string, f PanicTestFunc, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return PanicsWithErrorf(a.t, errString, f, msg, args...) +} + +// PanicsWithValue asserts that the code inside the specified PanicTestFunc panics, and that +// the recovered panic value equals the expected panic value. +// +// a.PanicsWithValue("crazy error", func(){ GoCrazy() }) +func (a *Assertions) PanicsWithValue(expected interface{}, f PanicTestFunc, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return PanicsWithValue(a.t, expected, f, msgAndArgs...) +} + +// PanicsWithValuef asserts that the code inside the specified PanicTestFunc panics, and that +// the recovered panic value equals the expected panic value. +// +// a.PanicsWithValuef("crazy error", func(){ GoCrazy() }, "error message %s", "formatted") +func (a *Assertions) PanicsWithValuef(expected interface{}, f PanicTestFunc, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return PanicsWithValuef(a.t, expected, f, msg, args...) +} + +// Panicsf asserts that the code inside the specified PanicTestFunc panics. +// +// a.Panicsf(func(){ GoCrazy() }, "error message %s", "formatted") +func (a *Assertions) Panicsf(f PanicTestFunc, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Panicsf(a.t, f, msg, args...) +} + +// Positive asserts that the specified element is positive +// +// a.Positive(1) +// a.Positive(1.23) +func (a *Assertions) Positive(e interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Positive(a.t, e, msgAndArgs...) +} + +// Positivef asserts that the specified element is positive +// +// a.Positivef(1, "error message %s", "formatted") +// a.Positivef(1.23, "error message %s", "formatted") +func (a *Assertions) Positivef(e interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Positivef(a.t, e, msg, args...) +} + +// Regexp asserts that a specified regexp matches a string. +// +// a.Regexp(regexp.MustCompile("start"), "it's starting") +// a.Regexp("start...$", "it's not starting") +func (a *Assertions) Regexp(rx interface{}, str interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Regexp(a.t, rx, str, msgAndArgs...) +} + +// Regexpf asserts that a specified regexp matches a string. +// +// a.Regexpf(regexp.MustCompile("start"), "it's starting", "error message %s", "formatted") +// a.Regexpf("start...$", "it's not starting", "error message %s", "formatted") +func (a *Assertions) Regexpf(rx interface{}, str interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Regexpf(a.t, rx, str, msg, args...) +} + +// Same asserts that two pointers reference the same object. +// +// a.Same(ptr1, ptr2) +// +// Both arguments must be pointer variables. Pointer variable sameness is +// determined based on the equality of both type and value. +func (a *Assertions) Same(expected interface{}, actual interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Same(a.t, expected, actual, msgAndArgs...) +} + +// Samef asserts that two pointers reference the same object. +// +// a.Samef(ptr1, ptr2, "error message %s", "formatted") +// +// Both arguments must be pointer variables. Pointer variable sameness is +// determined based on the equality of both type and value. +func (a *Assertions) Samef(expected interface{}, actual interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Samef(a.t, expected, actual, msg, args...) +} + +// Subset asserts that the list (array, slice, or map) contains all elements +// given in the subset (array, slice, or map). +// Map elements are key-value pairs unless compared with an array or slice where +// only the map key is evaluated. +// +// a.Subset([1, 2, 3], [1, 2]) +// a.Subset({"x": 1, "y": 2}, {"x": 1}) +// a.Subset([1, 2, 3], {1: "one", 2: "two"}) +// a.Subset({"x": 1, "y": 2}, ["x"]) +func (a *Assertions) Subset(list interface{}, subset interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Subset(a.t, list, subset, msgAndArgs...) +} + +// Subsetf asserts that the list (array, slice, or map) contains all elements +// given in the subset (array, slice, or map). +// Map elements are key-value pairs unless compared with an array or slice where +// only the map key is evaluated. +// +// a.Subsetf([1, 2, 3], [1, 2], "error message %s", "formatted") +// a.Subsetf({"x": 1, "y": 2}, {"x": 1}, "error message %s", "formatted") +// a.Subsetf([1, 2, 3], {1: "one", 2: "two"}, "error message %s", "formatted") +// a.Subsetf({"x": 1, "y": 2}, ["x"], "error message %s", "formatted") +func (a *Assertions) Subsetf(list interface{}, subset interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Subsetf(a.t, list, subset, msg, args...) +} + +// True asserts that the specified value is true. +// +// a.True(myBool) +func (a *Assertions) True(value bool, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return True(a.t, value, msgAndArgs...) +} + +// Truef asserts that the specified value is true. +// +// a.Truef(myBool, "error message %s", "formatted") +func (a *Assertions) Truef(value bool, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Truef(a.t, value, msg, args...) +} + +// WithinDuration asserts that the two times are within duration delta of each other. +// +// a.WithinDuration(time.Now(), time.Now(), 10*time.Second) +func (a *Assertions) WithinDuration(expected time.Time, actual time.Time, delta time.Duration, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return WithinDuration(a.t, expected, actual, delta, msgAndArgs...) +} + +// WithinDurationf asserts that the two times are within duration delta of each other. +// +// a.WithinDurationf(time.Now(), time.Now(), 10*time.Second, "error message %s", "formatted") +func (a *Assertions) WithinDurationf(expected time.Time, actual time.Time, delta time.Duration, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return WithinDurationf(a.t, expected, actual, delta, msg, args...) +} + +// WithinRange asserts that a time is within a time range (inclusive). +// +// a.WithinRange(time.Now(), time.Now().Add(-time.Second), time.Now().Add(time.Second)) +func (a *Assertions) WithinRange(actual time.Time, start time.Time, end time.Time, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return WithinRange(a.t, actual, start, end, msgAndArgs...) +} + +// WithinRangef asserts that a time is within a time range (inclusive). +// +// a.WithinRangef(time.Now(), time.Now().Add(-time.Second), time.Now().Add(time.Second), "error message %s", "formatted") +func (a *Assertions) WithinRangef(actual time.Time, start time.Time, end time.Time, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return WithinRangef(a.t, actual, start, end, msg, args...) +} + +// YAMLEq asserts that two YAML strings are equivalent. +func (a *Assertions) YAMLEq(expected string, actual string, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return YAMLEq(a.t, expected, actual, msgAndArgs...) +} + +// YAMLEqf asserts that two YAML strings are equivalent. +func (a *Assertions) YAMLEqf(expected string, actual string, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return YAMLEqf(a.t, expected, actual, msg, args...) +} + +// Zero asserts that i is the zero value for its type. +func (a *Assertions) Zero(i interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Zero(a.t, i, msgAndArgs...) +} + +// Zerof asserts that i is the zero value for its type. +func (a *Assertions) Zerof(i interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Zerof(a.t, i, msg, args...) +} diff --git a/pkg/testify/assert/assertion_forward.go.tmpl b/pkg/testify/assert/assertion_forward.go.tmpl new file mode 100644 index 00000000..188bb9e1 --- /dev/null +++ b/pkg/testify/assert/assertion_forward.go.tmpl @@ -0,0 +1,5 @@ +{{.CommentWithoutT "a"}} +func (a *Assertions) {{.DocInfo.Name}}({{.Params}}) bool { + if h, ok := a.t.(tHelper); ok { h.Helper() } + return {{.DocInfo.Name}}(a.t, {{.ForwardedParams}}) +} diff --git a/pkg/testify/assert/assertion_order.go b/pkg/testify/assert/assertion_order.go new file mode 100644 index 00000000..2fdf80fd --- /dev/null +++ b/pkg/testify/assert/assertion_order.go @@ -0,0 +1,81 @@ +package assert + +import ( + "fmt" + "reflect" +) + +// isOrdered checks that collection contains orderable elements. +func isOrdered(t TestingT, object interface{}, allowedComparesResults []compareResult, failMessage string, msgAndArgs ...interface{}) bool { + objKind := reflect.TypeOf(object).Kind() + if objKind != reflect.Slice && objKind != reflect.Array { + return false + } + + objValue := reflect.ValueOf(object) + objLen := objValue.Len() + + if objLen <= 1 { + return true + } + + value := objValue.Index(0) + valueInterface := value.Interface() + firstValueKind := value.Kind() + + for i := 1; i < objLen; i++ { + prevValue := value + prevValueInterface := valueInterface + + value = objValue.Index(i) + valueInterface = value.Interface() + + compareResult, isComparable := compare(prevValueInterface, valueInterface, firstValueKind) + + if !isComparable { + return Fail(t, fmt.Sprintf(`Can not compare type "%T" and "%T"`, value, prevValue), msgAndArgs...) + } + + if !containsValue(allowedComparesResults, compareResult) { + return Fail(t, fmt.Sprintf(failMessage, prevValue, value), msgAndArgs...) + } + } + + return true +} + +// IsIncreasing asserts that the collection is increasing +// +// assert.IsIncreasing(t, []int{1, 2, 3}) +// assert.IsIncreasing(t, []float{1, 2}) +// assert.IsIncreasing(t, []string{"a", "b"}) +func IsIncreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) bool { + return isOrdered(t, object, []compareResult{compareLess}, "\"%v\" is not less than \"%v\"", msgAndArgs...) +} + +// IsNonIncreasing asserts that the collection is not increasing +// +// assert.IsNonIncreasing(t, []int{2, 1, 1}) +// assert.IsNonIncreasing(t, []float{2, 1}) +// assert.IsNonIncreasing(t, []string{"b", "a"}) +func IsNonIncreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) bool { + return isOrdered(t, object, []compareResult{compareEqual, compareGreater}, "\"%v\" is not greater than or equal to \"%v\"", msgAndArgs...) +} + +// IsDecreasing asserts that the collection is decreasing +// +// assert.IsDecreasing(t, []int{2, 1, 0}) +// assert.IsDecreasing(t, []float{2, 1}) +// assert.IsDecreasing(t, []string{"b", "a"}) +func IsDecreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) bool { + return isOrdered(t, object, []compareResult{compareGreater}, "\"%v\" is not greater than \"%v\"", msgAndArgs...) +} + +// IsNonDecreasing asserts that the collection is not decreasing +// +// assert.IsNonDecreasing(t, []int{1, 1, 2}) +// assert.IsNonDecreasing(t, []float{1, 2}) +// assert.IsNonDecreasing(t, []string{"a", "b"}) +func IsNonDecreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) bool { + return isOrdered(t, object, []compareResult{compareLess, compareEqual}, "\"%v\" is not less than or equal to \"%v\"", msgAndArgs...) +} diff --git a/pkg/testify/assert/assertion_order_test.go b/pkg/testify/assert/assertion_order_test.go new file mode 100644 index 00000000..3e42d8db --- /dev/null +++ b/pkg/testify/assert/assertion_order_test.go @@ -0,0 +1,213 @@ +package assert + +import ( + "bytes" + testing "github.com/CodSpeedHQ/codspeed-go/testing/testing" +) + +func TestIsIncreasing(t *testing.T) { + t.Parallel() + + mockT := new(testing.T) + + if !IsIncreasing(mockT, []int{1, 2}) { + t.Error("IsIncreasing should return true") + } + + if !IsIncreasing(mockT, []int{1, 2, 3, 4, 5}) { + t.Error("IsIncreasing should return true") + } + + if IsIncreasing(mockT, []int{1, 1}) { + t.Error("IsIncreasing should return false") + } + + if IsIncreasing(mockT, []int{2, 1}) { + t.Error("IsIncreasing should return false") + } + + // Check error report + for _, currCase := range []struct { + collection interface{} + 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"`}, + } { + out := &outputT{buf: bytes.NewBuffer(nil)} + False(t, IsIncreasing(out, currCase.collection)) + Contains(t, out.buf.String(), currCase.msg) + } +} + +func TestIsNonIncreasing(t *testing.T) { + t.Parallel() + + mockT := new(testing.T) + + if !IsNonIncreasing(mockT, []int{2, 1}) { + t.Error("IsNonIncreasing should return true") + } + + if !IsNonIncreasing(mockT, []int{5, 4, 4, 3, 2, 1}) { + t.Error("IsNonIncreasing should return true") + } + + if !IsNonIncreasing(mockT, []int{1, 1}) { + t.Error("IsNonIncreasing should return true") + } + + if IsNonIncreasing(mockT, []int{1, 2}) { + t.Error("IsNonIncreasing should return false") + } + + // Check error report + for _, currCase := range []struct { + collection interface{} + 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"`}, + } { + out := &outputT{buf: bytes.NewBuffer(nil)} + False(t, IsNonIncreasing(out, currCase.collection)) + Contains(t, out.buf.String(), currCase.msg) + } +} + +func TestIsDecreasing(t *testing.T) { + t.Parallel() + + mockT := new(testing.T) + + if !IsDecreasing(mockT, []int{2, 1}) { + t.Error("IsDecreasing should return true") + } + + if !IsDecreasing(mockT, []int{5, 4, 3, 2, 1}) { + t.Error("IsDecreasing should return true") + } + + if IsDecreasing(mockT, []int{1, 1}) { + t.Error("IsDecreasing should return false") + } + + if IsDecreasing(mockT, []int{1, 2}) { + t.Error("IsDecreasing should return false") + } + + // Check error report + for _, currCase := range []struct { + collection interface{} + 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"`}, + } { + out := &outputT{buf: bytes.NewBuffer(nil)} + False(t, IsDecreasing(out, currCase.collection)) + Contains(t, out.buf.String(), currCase.msg) + } +} + +func TestIsNonDecreasing(t *testing.T) { + t.Parallel() + + mockT := new(testing.T) + + if !IsNonDecreasing(mockT, []int{1, 2}) { + t.Error("IsNonDecreasing should return true") + } + + if !IsNonDecreasing(mockT, []int{1, 1, 2, 3, 4, 5}) { + t.Error("IsNonDecreasing should return true") + } + + if !IsNonDecreasing(mockT, []int{1, 1}) { + t.Error("IsNonDecreasing should return false") + } + + if IsNonDecreasing(mockT, []int{2, 1}) { + t.Error("IsNonDecreasing should return false") + } + + // Check error report + for _, currCase := range []struct { + collection interface{} + 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"`}, + } { + out := &outputT{buf: bytes.NewBuffer(nil)} + False(t, IsNonDecreasing(out, currCase.collection)) + Contains(t, out.buf.String(), currCase.msg) + } +} + +func TestOrderingMsgAndArgsForwarding(t *testing.T) { + t.Parallel() + + msgAndArgs := []interface{}{"format %s %x", "this", 0xc001} + expectedOutput := "format this c001\n" + collection := []int{1, 2, 1} + funcs := []func(t TestingT){ + func(t TestingT) { IsIncreasing(t, collection, msgAndArgs...) }, + func(t TestingT) { IsNonIncreasing(t, collection, msgAndArgs...) }, + func(t TestingT) { IsDecreasing(t, collection, msgAndArgs...) }, + func(t TestingT) { IsNonDecreasing(t, collection, msgAndArgs...) }, + } + for _, f := range funcs { + out := &outputT{buf: bytes.NewBuffer(nil)} + f(out) + Contains(t, out.buf.String(), expectedOutput) + } +} diff --git a/pkg/testify/assert/assertions.go b/pkg/testify/assert/assertions.go new file mode 100644 index 00000000..e1ea4b6e --- /dev/null +++ b/pkg/testify/assert/assertions.go @@ -0,0 +1,2295 @@ +package assert + +import ( + "bufio" + "bytes" + "encoding/json" + "errors" + "fmt" + "math" + "os" + "reflect" + "regexp" + "runtime" + "runtime/debug" + "strings" + "time" + "unicode" + "unicode/utf8" + + "github.com/davecgh/go-spew/spew" + "github.com/pmezard/go-difflib/difflib" + + // Wrapper around gopkg.in/yaml.v3 + "github.com/CodSpeedHQ/codspeed-go/pkg/testify/assert/yaml" +) + +//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 +type TestingT interface { + Errorf(format string, args ...interface{}) +} + +// ComparisonAssertionFunc is a common function prototype when comparing two values. Can be useful +// for table driven tests. +type ComparisonAssertionFunc func(TestingT, interface{}, interface{}, ...interface{}) bool + +// ValueAssertionFunc is a common function prototype when validating a single value. Can be useful +// for table driven tests. +type ValueAssertionFunc func(TestingT, interface{}, ...interface{}) bool + +// BoolAssertionFunc is a common function prototype when validating a bool value. Can be useful +// for table driven tests. +type BoolAssertionFunc func(TestingT, bool, ...interface{}) bool + +// ErrorAssertionFunc is a common function prototype when validating an error value. Can be useful +// for table driven tests. +type ErrorAssertionFunc func(TestingT, error, ...interface{}) bool + +// PanicAssertionFunc is a common function prototype when validating a panic value. Can be useful +// for table driven tests. +type PanicAssertionFunc = func(t TestingT, f PanicTestFunc, msgAndArgs ...interface{}) bool + +// Comparison is a custom function that returns true on success and false on failure +type Comparison func() (success bool) + +/* + Helper functions +*/ + +// ObjectsAreEqual determines if two objects are considered equal. +// +// This function does no assertion of any kind. +func ObjectsAreEqual(expected, actual interface{}) bool { + if expected == nil || actual == nil { + return expected == actual + } + + exp, ok := expected.([]byte) + if !ok { + return reflect.DeepEqual(expected, actual) + } + + act, ok := actual.([]byte) + if !ok { + return false + } + if exp == nil || act == nil { + return exp == nil && act == nil + } + return bytes.Equal(exp, act) +} + +// copyExportedFields iterates downward through nested data structures and creates a copy +// that only contains the exported struct fields. +func copyExportedFields(expected interface{}) interface{} { + if isNil(expected) { + return expected + } + + expectedType := reflect.TypeOf(expected) + expectedKind := expectedType.Kind() + expectedValue := reflect.ValueOf(expected) + + switch expectedKind { + case reflect.Struct: + result := reflect.New(expectedType).Elem() + for i := 0; i < expectedType.NumField(); i++ { + field := expectedType.Field(i) + isExported := field.IsExported() + if isExported { + fieldValue := expectedValue.Field(i) + if isNil(fieldValue) || isNil(fieldValue.Interface()) { + continue + } + newValue := copyExportedFields(fieldValue.Interface()) + result.Field(i).Set(reflect.ValueOf(newValue)) + } + } + return result.Interface() + + case reflect.Ptr: + result := reflect.New(expectedType.Elem()) + unexportedRemoved := copyExportedFields(expectedValue.Elem().Interface()) + result.Elem().Set(reflect.ValueOf(unexportedRemoved)) + return result.Interface() + + case reflect.Array, reflect.Slice: + var result reflect.Value + if expectedKind == reflect.Array { + result = reflect.New(reflect.ArrayOf(expectedValue.Len(), expectedType.Elem())).Elem() + } else { + result = reflect.MakeSlice(expectedType, expectedValue.Len(), expectedValue.Len()) + } + for i := 0; i < expectedValue.Len(); i++ { + index := expectedValue.Index(i) + if isNil(index) { + continue + } + unexportedRemoved := copyExportedFields(index.Interface()) + result.Index(i).Set(reflect.ValueOf(unexportedRemoved)) + } + return result.Interface() + + case reflect.Map: + result := reflect.MakeMap(expectedType) + for _, k := range expectedValue.MapKeys() { + index := expectedValue.MapIndex(k) + unexportedRemoved := copyExportedFields(index.Interface()) + result.SetMapIndex(k, reflect.ValueOf(unexportedRemoved)) + } + return result.Interface() + + default: + return expected + } +} + +// ObjectsExportedFieldsAreEqual determines if the exported (public) fields of two objects are +// considered equal. This comparison of only exported fields is applied recursively to nested data +// structures. +// +// This function does no assertion of any kind. +// +// Deprecated: Use [EqualExportedValues] instead. +func ObjectsExportedFieldsAreEqual(expected, actual interface{}) bool { + expectedCleaned := copyExportedFields(expected) + actualCleaned := copyExportedFields(actual) + return ObjectsAreEqualValues(expectedCleaned, actualCleaned) +} + +// ObjectsAreEqualValues gets whether two objects are equal, or if their +// values are equal. +func ObjectsAreEqualValues(expected, actual interface{}) bool { + if ObjectsAreEqual(expected, actual) { + return true + } + + expectedValue := reflect.ValueOf(expected) + actualValue := reflect.ValueOf(actual) + if !expectedValue.IsValid() || !actualValue.IsValid() { + return false + } + + expectedType := expectedValue.Type() + actualType := actualValue.Type() + if !expectedType.ConvertibleTo(actualType) { + return false + } + + if !isNumericType(expectedType) || !isNumericType(actualType) { + // Attempt comparison after type conversion + return reflect.DeepEqual( + expectedValue.Convert(actualType).Interface(), actual, + ) + } + + // If BOTH values are numeric, there are chances of false positives due + // to overflow or underflow. So, we need to make sure to always convert + // the smaller type to a larger type before comparing. + if expectedType.Size() >= actualType.Size() { + return actualValue.Convert(expectedType).Interface() == expected + } + + return expectedValue.Convert(actualType).Interface() == actual +} + +// isNumericType returns true if the type is one of: +// int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, +// float32, float64, complex64, complex128 +func isNumericType(t reflect.Type) bool { + return t.Kind() >= reflect.Int && t.Kind() <= reflect.Complex128 +} + +/* CallerInfo is necessary because the assert functions use the testing object +internally, causing it to print the file:line of the assert method, rather than where +the problem actually occurred in calling code.*/ + +// CallerInfo returns an array of strings containing the file and line number +// of each stack frame leading from the current test to the assert call that +// failed. +func CallerInfo() []string { + var pc uintptr + var file string + var line int + var name string + + const stackFrameBufferSize = 10 + pcs := make([]uintptr, stackFrameBufferSize) + + callers := []string{} + offset := 1 + + for { + n := runtime.Callers(offset, pcs) + + if n == 0 { + break + } + + frames := runtime.CallersFrames(pcs[:n]) + + for { + frame, more := frames.Next() + pc = frame.PC + file = frame.File + line = frame.Line + + // This is a huge edge case, but it will panic if this is the case, see #180 + if file == "" { + break + } + + f := runtime.FuncForPC(pc) + if f == nil { + break + } + name = f.Name() + + // testing.tRunner is the standard library function that calls + // tests. Subtests are called directly by tRunner, without going through + // the Test/Benchmark/Example function that contains the t.Run calls, so + // with subtests we should break when we hit tRunner, without adding it + // to the list of callers. + if name == "testing.tRunner" { + break + } + + parts := strings.Split(file, "/") + if len(parts) > 1 { + filename := parts[len(parts)-1] + dir := parts[len(parts)-2] + if (dir != "assert" && dir != "mock" && dir != "require") || filename == "mock_test.go" { + callers = append(callers, fmt.Sprintf("%s:%d", file, line)) + } + } + + // Drop the package + dotPos := strings.LastIndexByte(name, '.') + name = name[dotPos+1:] + if isTest(name, "Test") || + isTest(name, "Benchmark") || + isTest(name, "Example") { + break + } + + if !more { + break + } + } + + // Next batch + offset += cap(pcs) + } + + return callers +} + +// Stolen from the `go test` tool. +// isTest tells whether name looks like a test (or benchmark, according to prefix). +// It is a Test (say) if there is a character after Test that is not a lower-case letter. +// We don't want TesticularCancer. +func isTest(name, prefix string) bool { + if !strings.HasPrefix(name, prefix) { + return false + } + if len(name) == len(prefix) { // "Test" is ok + return true + } + r, _ := utf8.DecodeRuneInString(name[len(prefix):]) + return !unicode.IsLower(r) +} + +func messageFromMsgAndArgs(msgAndArgs ...interface{}) string { + if len(msgAndArgs) == 0 || msgAndArgs == nil { + return "" + } + if len(msgAndArgs) == 1 { + msg := msgAndArgs[0] + if msgAsStr, ok := msg.(string); ok { + return msgAsStr + } + return fmt.Sprintf("%+v", msg) + } + if len(msgAndArgs) > 1 { + return fmt.Sprintf(msgAndArgs[0].(string), msgAndArgs[1:]...) + } + return "" +} + +// Aligns the provided message so that all lines after the first line start at the same location as the first line. +// Assumes that the first line starts at the correct location (after carriage return, tab, label, spacer and tab). +// The longestLabelLen parameter specifies the length of the longest label in the output (required because this is the +// basis on which the alignment occurs). +func indentMessageLines(message string, longestLabelLen int) string { + outBuf := new(bytes.Buffer) + + for i, scanner := 0, bufio.NewScanner(strings.NewReader(message)); scanner.Scan(); i++ { + // no need to align first line because it starts at the correct location (after the label) + if i != 0 { + // append alignLen+1 spaces to align with "{{longestLabel}}:" before adding tab + outBuf.WriteString("\n\t" + strings.Repeat(" ", longestLabelLen+1) + "\t") + } + outBuf.WriteString(scanner.Text()) + } + + return outBuf.String() +} + +type failNower interface { + FailNow() +} + +// FailNow fails test +func FailNow(t TestingT, failureMessage string, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + Fail(t, failureMessage, msgAndArgs...) + + // We cannot extend TestingT with FailNow() and + // maintain backwards compatibility, so we fallback + // to panicking when FailNow is not available in + // TestingT. + // See issue #263 + + if t, ok := t.(failNower); ok { + t.FailNow() + } else { + panic("test failed and t is missing `FailNow()`") + } + return false +} + +// Fail reports a failure through +func Fail(t TestingT, failureMessage string, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + content := []labeledContent{ + {"Error Trace", strings.Join(CallerInfo(), "\n\t\t\t")}, + {"Error", failureMessage}, + } + + // Add test name if the Go version supports it + if n, ok := t.(interface { + Name() string + }); ok { + content = append(content, labeledContent{"Test", n.Name()}) + } + + message := messageFromMsgAndArgs(msgAndArgs...) + if len(message) > 0 { + content = append(content, labeledContent{"Messages", message}) + } + + t.Errorf("\n%s", ""+labeledOutput(content...)) + + return false +} + +type labeledContent struct { + label string + content string +} + +// labeledOutput returns a string consisting of the provided labeledContent. Each labeled output is appended in the following manner: +// +// \t{{label}}:{{align_spaces}}\t{{content}}\n +// +// The initial carriage return is required to undo/erase any padding added by testing.T.Errorf. The "\t{{label}}:" is for the label. +// If a label is shorter than the longest label provided, padding spaces are added to make all the labels match in length. Once this +// alignment is achieved, "\t{{content}}\n" is added for the output. +// +// If the content of the labeledOutput contains line breaks, the subsequent lines are aligned so that they start at the same location as the first line. +func labeledOutput(content ...labeledContent) string { + longestLabel := 0 + for _, v := range content { + if len(v.label) > longestLabel { + longestLabel = len(v.label) + } + } + var output string + for _, v := range content { + output += "\t" + v.label + ":" + strings.Repeat(" ", longestLabel-len(v.label)) + "\t" + indentMessageLines(v.content, longestLabel) + "\n" + } + return output +} + +// Implements asserts that an object is implemented by the specified interface. +// +// assert.Implements(t, (*MyInterface)(nil), new(MyObject)) +func Implements(t TestingT, interfaceObject interface{}, object interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + interfaceType := reflect.TypeOf(interfaceObject).Elem() + + if object == nil { + return Fail(t, fmt.Sprintf("Cannot check if nil implements %v", interfaceType), msgAndArgs...) + } + if !reflect.TypeOf(object).Implements(interfaceType) { + return Fail(t, fmt.Sprintf("%T must implement %v", object, interfaceType), msgAndArgs...) + } + + return true +} + +// NotImplements asserts that an object does not implement the specified interface. +// +// assert.NotImplements(t, (*MyInterface)(nil), new(MyObject)) +func NotImplements(t TestingT, interfaceObject interface{}, object interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + interfaceType := reflect.TypeOf(interfaceObject).Elem() + + if object == nil { + return Fail(t, fmt.Sprintf("Cannot check if nil does not implement %v", interfaceType), msgAndArgs...) + } + if reflect.TypeOf(object).Implements(interfaceType) { + return Fail(t, fmt.Sprintf("%T implements %v", object, interfaceType), msgAndArgs...) + } + + return true +} + +func isType(expectedType, object interface{}) bool { + return ObjectsAreEqual(reflect.TypeOf(object), reflect.TypeOf(expectedType)) +} + +// IsType asserts that the specified objects are of the same type. +// +// assert.IsType(t, &MyStruct{}, &MyStruct{}) +func IsType(t TestingT, expectedType, object interface{}, msgAndArgs ...interface{}) bool { + if isType(expectedType, object) { + return true + } + if h, ok := t.(tHelper); ok { + h.Helper() + } + return Fail(t, fmt.Sprintf("Object expected to be of type %T, but was %T", expectedType, object), msgAndArgs...) +} + +// IsNotType asserts that the specified objects are not of the same type. +// +// assert.IsNotType(t, &NotMyStruct{}, &MyStruct{}) +func IsNotType(t TestingT, theType, object interface{}, msgAndArgs ...interface{}) bool { + if !isType(theType, object) { + return true + } + if h, ok := t.(tHelper); ok { + h.Helper() + } + return Fail(t, fmt.Sprintf("Object type expected to be different than %T", theType), msgAndArgs...) +} + +// Equal asserts that two objects are equal. +// +// assert.Equal(t, 123, 123) +// +// Pointer variable equality is determined based on the equality of the +// referenced values (as opposed to the memory addresses). Function equality +// cannot be determined and will always fail. +func Equal(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if err := validateEqualArgs(expected, actual); err != nil { + return Fail(t, fmt.Sprintf("Invalid operation: %#v == %#v (%s)", + expected, actual, err), msgAndArgs...) + } + + if !ObjectsAreEqual(expected, actual) { + diff := diff(expected, actual) + expected, actual = formatUnequalValues(expected, actual) + return Fail(t, fmt.Sprintf("Not equal: \n"+ + "expected: %s\n"+ + "actual : %s%s", expected, actual, diff), msgAndArgs...) + } + + return true +} + +// validateEqualArgs checks whether provided arguments can be safely used in the +// Equal/NotEqual functions. +func validateEqualArgs(expected, actual interface{}) error { + if expected == nil && actual == nil { + return nil + } + + if isFunction(expected) || isFunction(actual) { + return errors.New("cannot take func type as argument") + } + return nil +} + +// Same asserts that two pointers reference the same object. +// +// assert.Same(t, ptr1, ptr2) +// +// Both arguments must be pointer variables. Pointer variable sameness is +// determined based on the equality of both type and value. +func Same(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + + same, ok := samePointers(expected, actual) + if !ok { + return Fail(t, "Both arguments must be pointers", msgAndArgs...) + } + + if !same { + // both are pointers but not the same type & pointing to the same address + return Fail(t, fmt.Sprintf("Not same: \n"+ + "expected: %p %#[1]v\n"+ + "actual : %p %#[2]v", + expected, actual), msgAndArgs...) + } + + return true +} + +// NotSame asserts that two pointers do not reference the same object. +// +// assert.NotSame(t, ptr1, ptr2) +// +// Both arguments must be pointer variables. Pointer variable sameness is +// determined based on the equality of both type and value. +func NotSame(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + + same, ok := samePointers(expected, actual) + if !ok { + // fails when the arguments are not pointers + return !(Fail(t, "Both arguments must be pointers", msgAndArgs...)) + } + + if same { + return Fail(t, fmt.Sprintf( + "Expected and actual point to the same object: %p %#[1]v", + expected), msgAndArgs...) + } + return true +} + +// samePointers checks if two generic interface objects are pointers of the same +// type pointing to the same object. It returns two values: same indicating if +// they are the same type and point to the same object, and ok indicating that +// both inputs are pointers. +func samePointers(first, second interface{}) (same bool, ok bool) { + firstPtr, secondPtr := reflect.ValueOf(first), reflect.ValueOf(second) + if firstPtr.Kind() != reflect.Ptr || secondPtr.Kind() != reflect.Ptr { + return false, false // not both are pointers + } + + firstType, secondType := reflect.TypeOf(first), reflect.TypeOf(second) + if firstType != secondType { + return false, true // both are pointers, but of different types + } + + // compare pointer addresses + return first == second, true +} + +// formatUnequalValues takes two values of arbitrary types and returns string +// representations appropriate to be presented to the user. +// +// If the values are not of like type, the returned strings will be prefixed +// with the type name, and the value will be enclosed in parentheses similar +// to a type conversion in the Go grammar. +func formatUnequalValues(expected, actual interface{}) (e string, a string) { + if reflect.TypeOf(expected) != reflect.TypeOf(actual) { + return fmt.Sprintf("%T(%s)", expected, truncatingFormat(expected)), + fmt.Sprintf("%T(%s)", actual, truncatingFormat(actual)) + } + switch expected.(type) { + case time.Duration: + return fmt.Sprintf("%v", expected), fmt.Sprintf("%v", actual) + } + return truncatingFormat(expected), truncatingFormat(actual) +} + +// truncatingFormat formats the data and truncates it if it's too long. +// +// This helps keep formatted error messages lines from exceeding the +// bufio.MaxScanTokenSize max line length that the go testing framework imposes. +func truncatingFormat(data interface{}) string { + value := fmt.Sprintf("%#v", data) + max := bufio.MaxScanTokenSize - 100 // Give us some space the type info too if needed. + if len(value) > max { + value = value[0:max] + "<... truncated>" + } + return value +} + +// EqualValues asserts that two objects are equal or convertible to the larger +// type and equal. +// +// assert.EqualValues(t, uint32(123), int32(123)) +func EqualValues(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + + if !ObjectsAreEqualValues(expected, actual) { + diff := diff(expected, actual) + expected, actual = formatUnequalValues(expected, actual) + return Fail(t, fmt.Sprintf("Not equal: \n"+ + "expected: %s\n"+ + "actual : %s%s", expected, actual, diff), msgAndArgs...) + } + + return true +} + +// EqualExportedValues asserts that the types of two objects are equal and their public +// fields are also equal. This is useful for comparing structs that have private fields +// that could potentially differ. +// +// type S struct { +// Exported int +// notExported int +// } +// assert.EqualExportedValues(t, S{1, 2}, S{1, 3}) => true +// assert.EqualExportedValues(t, S{1, 2}, S{2, 3}) => false +func EqualExportedValues(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + + aType := reflect.TypeOf(expected) + bType := reflect.TypeOf(actual) + + if aType != bType { + return Fail(t, fmt.Sprintf("Types expected to match exactly\n\t%v != %v", aType, bType), msgAndArgs...) + } + + expected = copyExportedFields(expected) + actual = copyExportedFields(actual) + + if !ObjectsAreEqualValues(expected, actual) { + diff := diff(expected, actual) + expected, actual = formatUnequalValues(expected, actual) + return Fail(t, fmt.Sprintf("Not equal (comparing only exported fields): \n"+ + "expected: %s\n"+ + "actual : %s%s", expected, actual, diff), msgAndArgs...) + } + + return true +} + +// Exactly asserts that two objects are equal in value and type. +// +// assert.Exactly(t, int32(123), int64(123)) +func Exactly(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + + aType := reflect.TypeOf(expected) + bType := reflect.TypeOf(actual) + + if aType != bType { + return Fail(t, fmt.Sprintf("Types expected to match exactly\n\t%v != %v", aType, bType), msgAndArgs...) + } + + return Equal(t, expected, actual, msgAndArgs...) +} + +// NotNil asserts that the specified object is not nil. +// +// assert.NotNil(t, err) +func NotNil(t TestingT, object interface{}, msgAndArgs ...interface{}) bool { + if !isNil(object) { + return true + } + if h, ok := t.(tHelper); ok { + h.Helper() + } + return Fail(t, "Expected value not to be nil.", msgAndArgs...) +} + +// isNil checks if a specified object is nil or not, without Failing. +func isNil(object interface{}) bool { + if object == nil { + return true + } + + value := reflect.ValueOf(object) + switch value.Kind() { + case + reflect.Chan, reflect.Func, + reflect.Interface, reflect.Map, + reflect.Ptr, reflect.Slice, reflect.UnsafePointer: + + return value.IsNil() + } + + return false +} + +// Nil asserts that the specified object is nil. +// +// assert.Nil(t, err) +func Nil(t TestingT, object interface{}, msgAndArgs ...interface{}) bool { + if isNil(object) { + return true + } + if h, ok := t.(tHelper); ok { + h.Helper() + } + return Fail(t, fmt.Sprintf("Expected nil, but got: %#v", object), msgAndArgs...) +} + +// isEmpty gets whether the specified object is considered empty or not. +func isEmpty(object interface{}) bool { + // get nil case out of the way + if object == nil { + return true + } + + return isEmptyValue(reflect.ValueOf(object)) +} + +// isEmptyValue gets whether the specified reflect.Value is considered empty or not. +func isEmptyValue(objValue reflect.Value) bool { + if objValue.IsZero() { + return true + } + // Special cases of non-zero values that we consider empty + switch objValue.Kind() { + // collection types are empty when they have no element + // Note: array types are empty when they match their zero-initialized state. + case reflect.Chan, reflect.Map, reflect.Slice: + return objValue.Len() == 0 + // non-nil pointers are empty if the value they point to is empty + case reflect.Ptr: + return isEmptyValue(objValue.Elem()) + } + return false +} + +// Empty asserts that the given value is "empty". +// +// [Zero values] are "empty". +// +// Arrays are "empty" if every element is the zero value of the type (stricter than "empty"). +// +// Slices, maps and channels with zero length are "empty". +// +// Pointer values are "empty" if the pointer is nil or if the pointed value is "empty". +// +// assert.Empty(t, obj) +// +// [Zero values]: https://go.dev/ref/spec#The_zero_value +func Empty(t TestingT, object interface{}, msgAndArgs ...interface{}) bool { + pass := isEmpty(object) + if !pass { + if h, ok := t.(tHelper); ok { + h.Helper() + } + Fail(t, fmt.Sprintf("Should be empty, but was %v", object), msgAndArgs...) + } + + return pass +} + +// NotEmpty asserts that the specified object is NOT [Empty]. +// +// if assert.NotEmpty(t, obj) { +// assert.Equal(t, "two", obj[1]) +// } +func NotEmpty(t TestingT, object interface{}, msgAndArgs ...interface{}) bool { + pass := !isEmpty(object) + if !pass { + if h, ok := t.(tHelper); ok { + h.Helper() + } + Fail(t, fmt.Sprintf("Should NOT be empty, but was %v", object), msgAndArgs...) + } + + return pass +} + +// getLen tries to get the length of an object. +// It returns (0, false) if impossible. +func getLen(x interface{}) (length int, ok bool) { + v := reflect.ValueOf(x) + defer func() { + ok = recover() == nil + }() + return v.Len(), true +} + +// Len asserts that the specified object has specific length. +// Len also fails if the object has a type that len() not accept. +// +// assert.Len(t, mySlice, 3) +func Len(t TestingT, object interface{}, length int, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + l, ok := getLen(object) + if !ok { + return Fail(t, fmt.Sprintf("\"%v\" could not be applied builtin len()", object), msgAndArgs...) + } + + if l != length { + return Fail(t, fmt.Sprintf("\"%v\" should have %d item(s), but has %d", object, length, l), msgAndArgs...) + } + return true +} + +// True asserts that the specified value is true. +// +// assert.True(t, myBool) +func True(t TestingT, value bool, msgAndArgs ...interface{}) bool { + if !value { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return Fail(t, "Should be true", msgAndArgs...) + } + + return true +} + +// False asserts that the specified value is false. +// +// assert.False(t, myBool) +func False(t TestingT, value bool, msgAndArgs ...interface{}) bool { + if value { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return Fail(t, "Should be false", msgAndArgs...) + } + + return true +} + +// NotEqual asserts that the specified values are NOT equal. +// +// assert.NotEqual(t, obj1, obj2) +// +// Pointer variable equality is determined based on the equality of the +// referenced values (as opposed to the memory addresses). +func NotEqual(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if err := validateEqualArgs(expected, actual); err != nil { + return Fail(t, fmt.Sprintf("Invalid operation: %#v != %#v (%s)", + expected, actual, err), msgAndArgs...) + } + + if ObjectsAreEqual(expected, actual) { + return Fail(t, fmt.Sprintf("Should not be: %#v\n", actual), msgAndArgs...) + } + + return true +} + +// NotEqualValues asserts that two objects are not equal even when converted to the same type +// +// assert.NotEqualValues(t, obj1, obj2) +func NotEqualValues(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + + if ObjectsAreEqualValues(expected, actual) { + return Fail(t, fmt.Sprintf("Should not be: %#v\n", actual), msgAndArgs...) + } + + return true +} + +// containsElement try loop over the list check if the list includes the element. +// return (false, false) if impossible. +// return (true, false) if element was not found. +// return (true, true) if element was found. +func containsElement(list interface{}, element interface{}) (ok, found bool) { + listValue := reflect.ValueOf(list) + listType := reflect.TypeOf(list) + if listType == nil { + return false, false + } + listKind := listType.Kind() + defer func() { + if e := recover(); e != nil { + ok = false + found = false + } + }() + + if listKind == reflect.String { + elementValue := reflect.ValueOf(element) + return true, strings.Contains(listValue.String(), elementValue.String()) + } + + if listKind == reflect.Map { + mapKeys := listValue.MapKeys() + for i := 0; i < len(mapKeys); i++ { + if ObjectsAreEqual(mapKeys[i].Interface(), element) { + return true, true + } + } + return true, false + } + + for i := 0; i < listValue.Len(); i++ { + if ObjectsAreEqual(listValue.Index(i).Interface(), element) { + return true, true + } + } + return true, false +} + +// Contains asserts that the specified string, list(array, slice...) or map contains the +// specified substring or element. +// +// assert.Contains(t, "Hello World", "World") +// assert.Contains(t, ["Hello", "World"], "World") +// assert.Contains(t, {"Hello": "World"}, "Hello") +func Contains(t TestingT, s, contains interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + + ok, found := containsElement(s, contains) + if !ok { + return Fail(t, fmt.Sprintf("%#v could not be applied builtin len()", s), msgAndArgs...) + } + if !found { + return Fail(t, fmt.Sprintf("%#v does not contain %#v", s, contains), msgAndArgs...) + } + + return true +} + +// NotContains asserts that the specified string, list(array, slice...) or map does NOT contain the +// specified substring or element. +// +// assert.NotContains(t, "Hello World", "Earth") +// assert.NotContains(t, ["Hello", "World"], "Earth") +// assert.NotContains(t, {"Hello": "World"}, "Earth") +func NotContains(t TestingT, s, contains interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + + ok, found := containsElement(s, contains) + if !ok { + return Fail(t, fmt.Sprintf("%#v could not be applied builtin len()", s), msgAndArgs...) + } + if found { + return Fail(t, fmt.Sprintf("%#v should not contain %#v", s, contains), msgAndArgs...) + } + + return true +} + +// Subset asserts that the list (array, slice, or map) contains all elements +// given in the subset (array, slice, or map). +// Map elements are key-value pairs unless compared with an array or slice where +// only the map key is evaluated. +// +// assert.Subset(t, [1, 2, 3], [1, 2]) +// assert.Subset(t, {"x": 1, "y": 2}, {"x": 1}) +// assert.Subset(t, [1, 2, 3], {1: "one", 2: "two"}) +// assert.Subset(t, {"x": 1, "y": 2}, ["x"]) +func Subset(t TestingT, list, subset interface{}, msgAndArgs ...interface{}) (ok bool) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if subset == nil { + return true // we consider nil to be equal to the nil set + } + + listKind := reflect.TypeOf(list).Kind() + if listKind != reflect.Array && listKind != reflect.Slice && listKind != reflect.Map { + return Fail(t, fmt.Sprintf("%q has an unsupported type %s", list, listKind), msgAndArgs...) + } + + subsetKind := reflect.TypeOf(subset).Kind() + if subsetKind != reflect.Array && subsetKind != reflect.Slice && subsetKind != reflect.Map { + return Fail(t, fmt.Sprintf("%q has an unsupported type %s", subset, subsetKind), msgAndArgs...) + } + + if subsetKind == reflect.Map && listKind == reflect.Map { + subsetMap := reflect.ValueOf(subset) + actualMap := reflect.ValueOf(list) + + for _, k := range subsetMap.MapKeys() { + ev := subsetMap.MapIndex(k) + av := actualMap.MapIndex(k) + + if !av.IsValid() { + return Fail(t, fmt.Sprintf("%#v does not contain %#v", list, subset), msgAndArgs...) + } + if !ObjectsAreEqual(ev.Interface(), av.Interface()) { + return Fail(t, fmt.Sprintf("%#v does not contain %#v", list, subset), msgAndArgs...) + } + } + + return true + } + + subsetList := reflect.ValueOf(subset) + if subsetKind == reflect.Map { + keys := make([]interface{}, subsetList.Len()) + for idx, key := range subsetList.MapKeys() { + keys[idx] = key.Interface() + } + subsetList = reflect.ValueOf(keys) + } + for i := 0; i < subsetList.Len(); i++ { + element := subsetList.Index(i).Interface() + ok, found := containsElement(list, element) + if !ok { + return Fail(t, fmt.Sprintf("%#v could not be applied builtin len()", list), msgAndArgs...) + } + if !found { + return Fail(t, fmt.Sprintf("%#v does not contain %#v", list, element), msgAndArgs...) + } + } + + return true +} + +// NotSubset asserts that the list (array, slice, or map) does NOT contain all +// elements given in the subset (array, slice, or map). +// Map elements are key-value pairs unless compared with an array or slice where +// only the map key is evaluated. +// +// assert.NotSubset(t, [1, 3, 4], [1, 2]) +// assert.NotSubset(t, {"x": 1, "y": 2}, {"z": 3}) +// assert.NotSubset(t, [1, 3, 4], {1: "one", 2: "two"}) +// assert.NotSubset(t, {"x": 1, "y": 2}, ["z"]) +func NotSubset(t TestingT, list, subset interface{}, msgAndArgs ...interface{}) (ok bool) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if subset == nil { + return Fail(t, "nil is the empty set which is a subset of every set", msgAndArgs...) + } + + listKind := reflect.TypeOf(list).Kind() + if listKind != reflect.Array && listKind != reflect.Slice && listKind != reflect.Map { + return Fail(t, fmt.Sprintf("%q has an unsupported type %s", list, listKind), msgAndArgs...) + } + + subsetKind := reflect.TypeOf(subset).Kind() + if subsetKind != reflect.Array && subsetKind != reflect.Slice && subsetKind != reflect.Map { + return Fail(t, fmt.Sprintf("%q has an unsupported type %s", subset, subsetKind), msgAndArgs...) + } + + if subsetKind == reflect.Map && listKind == reflect.Map { + subsetMap := reflect.ValueOf(subset) + actualMap := reflect.ValueOf(list) + + for _, k := range subsetMap.MapKeys() { + ev := subsetMap.MapIndex(k) + av := actualMap.MapIndex(k) + + if !av.IsValid() { + return true + } + if !ObjectsAreEqual(ev.Interface(), av.Interface()) { + return true + } + } + + return Fail(t, fmt.Sprintf("%q is a subset of %q", subset, list), msgAndArgs...) + } + + subsetList := reflect.ValueOf(subset) + if subsetKind == reflect.Map { + keys := make([]interface{}, subsetList.Len()) + for idx, key := range subsetList.MapKeys() { + keys[idx] = key.Interface() + } + subsetList = reflect.ValueOf(keys) + } + for i := 0; i < subsetList.Len(); i++ { + element := subsetList.Index(i).Interface() + ok, found := containsElement(list, element) + if !ok { + return Fail(t, fmt.Sprintf("%q could not be applied builtin len()", list), msgAndArgs...) + } + if !found { + return true + } + } + + return Fail(t, fmt.Sprintf("%q is a subset of %q", subset, list), msgAndArgs...) +} + +// ElementsMatch asserts that the specified listA(array, slice...) is equal to specified +// 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]) +func ElementsMatch(t TestingT, listA, listB interface{}, msgAndArgs ...interface{}) (ok bool) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if isEmpty(listA) && isEmpty(listB) { + return true + } + + if !isList(t, listA, msgAndArgs...) || !isList(t, listB, msgAndArgs...) { + return false + } + + extraA, extraB := diffLists(listA, listB) + + if len(extraA) == 0 && len(extraB) == 0 { + return true + } + + return Fail(t, formatListDiff(listA, listB, extraA, extraB), msgAndArgs...) +} + +// isList checks that the provided value is array or slice. +func isList(t TestingT, list interface{}, msgAndArgs ...interface{}) (ok bool) { + kind := reflect.TypeOf(list).Kind() + if kind != reflect.Array && kind != reflect.Slice { + return Fail(t, fmt.Sprintf("%q has an unsupported type %s, expecting array or slice", list, kind), + msgAndArgs...) + } + return true +} + +// diffLists diffs two arrays/slices and returns slices of elements that are only in A and only in B. +// If some element is present multiple times, each instance is counted separately (e.g. if something is 2x in A and +// 5x in B, it will be 0x in extraA and 3x in extraB). The order of items in both lists is ignored. +func diffLists(listA, listB interface{}) (extraA, extraB []interface{}) { + aValue := reflect.ValueOf(listA) + bValue := reflect.ValueOf(listB) + + aLen := aValue.Len() + bLen := bValue.Len() + + // Mark indexes in bValue that we already used + visited := make([]bool, bLen) + for i := 0; i < aLen; i++ { + element := aValue.Index(i).Interface() + found := false + for j := 0; j < bLen; j++ { + if visited[j] { + continue + } + if ObjectsAreEqual(bValue.Index(j).Interface(), element) { + visited[j] = true + found = true + break + } + } + if !found { + extraA = append(extraA, element) + } + } + + for j := 0; j < bLen; j++ { + if visited[j] { + continue + } + extraB = append(extraB, bValue.Index(j).Interface()) + } + + return +} + +func formatListDiff(listA, listB interface{}, extraA, extraB []interface{}) string { + var msg bytes.Buffer + + msg.WriteString("elements differ") + if len(extraA) > 0 { + msg.WriteString("\n\nextra elements in list A:\n") + msg.WriteString(spewConfig.Sdump(extraA)) + } + if len(extraB) > 0 { + msg.WriteString("\n\nextra elements in list B:\n") + msg.WriteString(spewConfig.Sdump(extraB)) + } + msg.WriteString("\n\nlistA:\n") + msg.WriteString(spewConfig.Sdump(listA)) + msg.WriteString("\n\nlistB:\n") + msg.WriteString(spewConfig.Sdump(listB)) + + return msg.String() +} + +// NotElementsMatch asserts that the specified listA(array, slice...) is NOT equal to specified +// 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 not match. +// This is an inverse of ElementsMatch. +// +// assert.NotElementsMatch(t, [1, 1, 2, 3], [1, 1, 2, 3]) -> false +// +// assert.NotElementsMatch(t, [1, 1, 2, 3], [1, 2, 3]) -> true +// +// assert.NotElementsMatch(t, [1, 2, 3], [1, 2, 4]) -> true +func NotElementsMatch(t TestingT, listA, listB interface{}, msgAndArgs ...interface{}) (ok bool) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if isEmpty(listA) && isEmpty(listB) { + return Fail(t, "listA and listB contain the same elements", msgAndArgs) + } + + if !isList(t, listA, msgAndArgs...) { + return Fail(t, "listA is not a list type", msgAndArgs...) + } + if !isList(t, listB, msgAndArgs...) { + return Fail(t, "listB is not a list type", msgAndArgs...) + } + + extraA, extraB := diffLists(listA, listB) + if len(extraA) == 0 && len(extraB) == 0 { + return Fail(t, "listA and listB contain the same elements", msgAndArgs) + } + + return true +} + +// Condition uses a Comparison to assert a complex condition. +func Condition(t TestingT, comp Comparison, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + result := comp() + if !result { + Fail(t, "Condition failed!", msgAndArgs...) + } + return result +} + +// PanicTestFunc defines a func that should be passed to the assert.Panics and assert.NotPanics +// methods, and represents a simple func that takes no arguments, and returns nothing. +type PanicTestFunc func() + +// didPanic returns true if the function passed to it panics. Otherwise, it returns false. +func didPanic(f PanicTestFunc) (didPanic bool, message interface{}, stack string) { + didPanic = true + + defer func() { + message = recover() + if didPanic { + stack = string(debug.Stack()) + } + }() + + // call the target function + f() + didPanic = false + + return +} + +// Panics asserts that the code inside the specified PanicTestFunc panics. +// +// assert.Panics(t, func(){ GoCrazy() }) +func Panics(t TestingT, f PanicTestFunc, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + + if funcDidPanic, panicValue, _ := didPanic(f); !funcDidPanic { + return Fail(t, fmt.Sprintf("func %#v should panic\n\tPanic value:\t%#v", f, panicValue), msgAndArgs...) + } + + return true +} + +// PanicsWithValue asserts that the code inside the specified PanicTestFunc panics, and that +// the recovered panic value equals the expected panic value. +// +// assert.PanicsWithValue(t, "crazy error", func(){ GoCrazy() }) +func PanicsWithValue(t TestingT, expected interface{}, f PanicTestFunc, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + + funcDidPanic, panicValue, panickedStack := didPanic(f) + if !funcDidPanic { + return Fail(t, fmt.Sprintf("func %#v should panic\n\tPanic value:\t%#v", f, panicValue), msgAndArgs...) + } + if panicValue != expected { + return Fail(t, fmt.Sprintf("func %#v should panic with value:\t%#v\n\tPanic value:\t%#v\n\tPanic stack:\t%s", f, expected, panicValue, panickedStack), msgAndArgs...) + } + + return true +} + +// PanicsWithError asserts that the code inside the specified PanicTestFunc +// panics, and that the recovered panic value is an error that satisfies the +// EqualError comparison. +// +// assert.PanicsWithError(t, "crazy error", func(){ GoCrazy() }) +func PanicsWithError(t TestingT, errString string, f PanicTestFunc, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + + funcDidPanic, panicValue, panickedStack := didPanic(f) + if !funcDidPanic { + return Fail(t, fmt.Sprintf("func %#v should panic\n\tPanic value:\t%#v", f, panicValue), msgAndArgs...) + } + panicErr, ok := panicValue.(error) + if !ok || panicErr.Error() != errString { + return Fail(t, fmt.Sprintf("func %#v should panic with error message:\t%#v\n\tPanic value:\t%#v\n\tPanic stack:\t%s", f, errString, panicValue, panickedStack), msgAndArgs...) + } + + return true +} + +// NotPanics asserts that the code inside the specified PanicTestFunc does NOT panic. +// +// assert.NotPanics(t, func(){ RemainCalm() }) +func NotPanics(t TestingT, f PanicTestFunc, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + + if funcDidPanic, panicValue, panickedStack := didPanic(f); funcDidPanic { + return Fail(t, fmt.Sprintf("func %#v should not panic\n\tPanic value:\t%v\n\tPanic stack:\t%s", f, panicValue, panickedStack), msgAndArgs...) + } + + return true +} + +// WithinDuration asserts that the two times are within duration delta of each other. +// +// assert.WithinDuration(t, time.Now(), time.Now(), 10*time.Second) +func WithinDuration(t TestingT, expected, actual time.Time, delta time.Duration, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + + dt := expected.Sub(actual) + if dt < -delta || dt > delta { + return Fail(t, fmt.Sprintf("Max difference between %v and %v allowed is %v, but difference was %v", expected, actual, delta, dt), msgAndArgs...) + } + + return true +} + +// WithinRange asserts that a time is within a time range (inclusive). +// +// assert.WithinRange(t, time.Now(), time.Now().Add(-time.Second), time.Now().Add(time.Second)) +func WithinRange(t TestingT, actual, start, end time.Time, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + + if end.Before(start) { + return Fail(t, "Start should be before end", msgAndArgs...) + } + + if actual.Before(start) { + return Fail(t, fmt.Sprintf("Time %v expected to be in time range %v to %v, but is before the range", actual, start, end), msgAndArgs...) + } else if actual.After(end) { + return Fail(t, fmt.Sprintf("Time %v expected to be in time range %v to %v, but is after the range", actual, start, end), msgAndArgs...) + } + + return true +} + +func toFloat(x interface{}) (float64, bool) { + var xf float64 + xok := true + + switch xn := x.(type) { + case uint: + xf = float64(xn) + case uint8: + xf = float64(xn) + case uint16: + xf = float64(xn) + case uint32: + xf = float64(xn) + case uint64: + xf = float64(xn) + case int: + xf = float64(xn) + case int8: + xf = float64(xn) + case int16: + xf = float64(xn) + case int32: + xf = float64(xn) + case int64: + xf = float64(xn) + case float32: + xf = float64(xn) + case float64: + xf = xn + case time.Duration: + xf = float64(xn) + default: + xok = false + } + + return xf, xok +} + +// InDelta asserts that the two numerals are within delta of each other. +// +// assert.InDelta(t, math.Pi, 22/7.0, 0.01) +func InDelta(t TestingT, expected, actual interface{}, delta float64, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + + af, aok := toFloat(expected) + bf, bok := toFloat(actual) + + if !aok || !bok { + return Fail(t, "Parameters must be numerical", msgAndArgs...) + } + + if math.IsNaN(af) && math.IsNaN(bf) { + return true + } + + if math.IsNaN(af) { + return Fail(t, "Expected must not be NaN", msgAndArgs...) + } + + if math.IsNaN(bf) { + return Fail(t, fmt.Sprintf("Expected %v with delta %v, but was NaN", expected, delta), msgAndArgs...) + } + + dt := af - bf + if dt < -delta || dt > delta { + return Fail(t, fmt.Sprintf("Max difference between %v and %v allowed is %v, but difference was %v", expected, actual, delta, dt), msgAndArgs...) + } + + return true +} + +// InDeltaSlice is the same as InDelta, except it compares two slices. +func InDeltaSlice(t TestingT, expected, actual interface{}, delta float64, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if expected == nil || actual == nil || + reflect.TypeOf(actual).Kind() != reflect.Slice || + reflect.TypeOf(expected).Kind() != reflect.Slice { + return Fail(t, "Parameters must be slice", msgAndArgs...) + } + + actualSlice := reflect.ValueOf(actual) + expectedSlice := reflect.ValueOf(expected) + + for i := 0; i < actualSlice.Len(); i++ { + result := InDelta(t, actualSlice.Index(i).Interface(), expectedSlice.Index(i).Interface(), delta, msgAndArgs...) + if !result { + return result + } + } + + return true +} + +// InDeltaMapValues is the same as InDelta, but it compares all values between two maps. Both maps must have exactly the same keys. +func InDeltaMapValues(t TestingT, expected, actual interface{}, delta float64, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if expected == nil || actual == nil || + reflect.TypeOf(actual).Kind() != reflect.Map || + reflect.TypeOf(expected).Kind() != reflect.Map { + return Fail(t, "Arguments must be maps", msgAndArgs...) + } + + expectedMap := reflect.ValueOf(expected) + actualMap := reflect.ValueOf(actual) + + if expectedMap.Len() != actualMap.Len() { + return Fail(t, "Arguments must have the same number of keys", msgAndArgs...) + } + + for _, k := range expectedMap.MapKeys() { + ev := expectedMap.MapIndex(k) + av := actualMap.MapIndex(k) + + if !ev.IsValid() { + return Fail(t, fmt.Sprintf("missing key %q in expected map", k), msgAndArgs...) + } + + if !av.IsValid() { + return Fail(t, fmt.Sprintf("missing key %q in actual map", k), msgAndArgs...) + } + + if !InDelta( + t, + ev.Interface(), + av.Interface(), + delta, + msgAndArgs..., + ) { + return false + } + } + + return true +} + +func calcRelativeError(expected, actual interface{}) (float64, error) { + af, aok := toFloat(expected) + bf, bok := toFloat(actual) + if !aok || !bok { + return 0, fmt.Errorf("Parameters must be numerical") + } + if math.IsNaN(af) && math.IsNaN(bf) { + return 0, nil + } + if math.IsNaN(af) { + return 0, errors.New("expected value must not be NaN") + } + if af == 0 { + return 0, fmt.Errorf("expected value must have a value other than zero to calculate the relative error") + } + if math.IsNaN(bf) { + return 0, errors.New("actual value must not be NaN") + } + + return math.Abs(af-bf) / math.Abs(af), nil +} + +// InEpsilon asserts that expected and actual have a relative error less than epsilon +func InEpsilon(t TestingT, expected, actual interface{}, epsilon float64, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if math.IsNaN(epsilon) { + return Fail(t, "epsilon must not be NaN", msgAndArgs...) + } + actualEpsilon, err := calcRelativeError(expected, actual) + if err != nil { + return Fail(t, err.Error(), msgAndArgs...) + } + if math.IsNaN(actualEpsilon) { + return Fail(t, "relative error is NaN", msgAndArgs...) + } + if actualEpsilon > epsilon { + return Fail(t, fmt.Sprintf("Relative error is too high: %#v (expected)\n"+ + " < %#v (actual)", epsilon, actualEpsilon), msgAndArgs...) + } + + return true +} + +// InEpsilonSlice is the same as InEpsilon, except it compares each value from two slices. +func InEpsilonSlice(t TestingT, expected, actual interface{}, epsilon float64, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + + if expected == nil || actual == nil { + return Fail(t, "Parameters must be slice", msgAndArgs...) + } + + expectedSlice := reflect.ValueOf(expected) + actualSlice := reflect.ValueOf(actual) + + if expectedSlice.Type().Kind() != reflect.Slice { + return Fail(t, "Expected value must be slice", msgAndArgs...) + } + + expectedLen := expectedSlice.Len() + if !IsType(t, expected, actual) || !Len(t, actual, expectedLen) { + return false + } + + for i := 0; i < expectedLen; i++ { + if !InEpsilon(t, expectedSlice.Index(i).Interface(), actualSlice.Index(i).Interface(), epsilon, "at index %d", i) { + return false + } + } + + return true +} + +/* + Errors +*/ + +// NoError asserts that a function returned no error (i.e. `nil`). +// +// actualObj, err := SomeFunction() +// if assert.NoError(t, err) { +// assert.Equal(t, expectedObj, actualObj) +// } +func NoError(t TestingT, err error, msgAndArgs ...interface{}) bool { + if err != nil { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return Fail(t, fmt.Sprintf("Received unexpected error:\n%+v", err), msgAndArgs...) + } + + return true +} + +// Error asserts that a function returned an error (i.e. not `nil`). +// +// actualObj, err := SomeFunction() +// assert.Error(t, err) +func Error(t TestingT, err error, msgAndArgs ...interface{}) bool { + if err == nil { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return Fail(t, "An error is expected but got nil.", msgAndArgs...) + } + + return true +} + +// EqualError asserts that a function returned an error (i.e. not `nil`) +// and that it is equal to the provided error. +// +// actualObj, err := SomeFunction() +// assert.EqualError(t, err, expectedErrorString) +func EqualError(t TestingT, theError error, errString string, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if !Error(t, theError, msgAndArgs...) { + return false + } + expected := errString + actual := theError.Error() + // don't need to use deep equals here, we know they are both strings + if expected != actual { + return Fail(t, fmt.Sprintf("Error message not equal:\n"+ + "expected: %q\n"+ + "actual : %q", expected, actual), msgAndArgs...) + } + return true +} + +// ErrorContains asserts that a function returned an error (i.e. not `nil`) +// and that the error contains the specified substring. +// +// actualObj, err := SomeFunction() +// assert.ErrorContains(t, err, expectedErrorSubString) +func ErrorContains(t TestingT, theError error, contains string, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if !Error(t, theError, msgAndArgs...) { + return false + } + + actual := theError.Error() + if !strings.Contains(actual, contains) { + return Fail(t, fmt.Sprintf("Error %#v does not contain %#v", actual, contains), msgAndArgs...) + } + + return true +} + +// matchRegexp return true if a specified regexp matches a string. +func matchRegexp(rx interface{}, str interface{}) bool { + var r *regexp.Regexp + if rr, ok := rx.(*regexp.Regexp); ok { + r = rr + } else { + r = regexp.MustCompile(fmt.Sprint(rx)) + } + + switch v := str.(type) { + case []byte: + return r.Match(v) + case string: + return r.MatchString(v) + default: + return r.MatchString(fmt.Sprint(v)) + } +} + +// Regexp asserts that a specified regexp matches a string. +// +// assert.Regexp(t, regexp.MustCompile("start"), "it's starting") +// assert.Regexp(t, "start...$", "it's not starting") +func Regexp(t TestingT, rx interface{}, str interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + + match := matchRegexp(rx, str) + + if !match { + Fail(t, fmt.Sprintf("Expect \"%v\" to match \"%v\"", str, rx), msgAndArgs...) + } + + return match +} + +// NotRegexp asserts that a specified regexp does not match a string. +// +// assert.NotRegexp(t, regexp.MustCompile("starts"), "it's starting") +// assert.NotRegexp(t, "^start", "it's not starting") +func NotRegexp(t TestingT, rx interface{}, str interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + match := matchRegexp(rx, str) + + if match { + Fail(t, fmt.Sprintf("Expect \"%v\" to NOT match \"%v\"", str, rx), msgAndArgs...) + } + + return !match +} + +// Zero asserts that i is the zero value for its type. +func Zero(t TestingT, i interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if i != nil && !reflect.DeepEqual(i, reflect.Zero(reflect.TypeOf(i)).Interface()) { + return Fail(t, fmt.Sprintf("Should be zero, but was %v", i), msgAndArgs...) + } + return true +} + +// NotZero asserts that i is not the zero value for its type. +func NotZero(t TestingT, i interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if i == nil || reflect.DeepEqual(i, reflect.Zero(reflect.TypeOf(i)).Interface()) { + return Fail(t, fmt.Sprintf("Should not be zero, but was %v", i), msgAndArgs...) + } + return true +} + +// FileExists checks whether a file exists in the given path. It also fails if +// the path points to a directory or there is an error when trying to check the file. +func FileExists(t TestingT, path string, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + info, err := os.Lstat(path) + if err != nil { + if os.IsNotExist(err) { + return Fail(t, fmt.Sprintf("unable to find file %q", path), msgAndArgs...) + } + return Fail(t, fmt.Sprintf("error when running os.Lstat(%q): %s", path, err), msgAndArgs...) + } + if info.IsDir() { + return Fail(t, fmt.Sprintf("%q is a directory", path), msgAndArgs...) + } + return true +} + +// NoFileExists checks whether a file does not exist in a given path. It fails +// if the path points to an existing _file_ only. +func NoFileExists(t TestingT, path string, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + info, err := os.Lstat(path) + if err != nil { + return true + } + if info.IsDir() { + return true + } + return Fail(t, fmt.Sprintf("file %q exists", path), msgAndArgs...) +} + +// DirExists checks whether a directory exists in the given path. It also fails +// if the path is a file rather a directory or there is an error checking whether it exists. +func DirExists(t TestingT, path string, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + info, err := os.Lstat(path) + if err != nil { + if os.IsNotExist(err) { + return Fail(t, fmt.Sprintf("unable to find file %q", path), msgAndArgs...) + } + return Fail(t, fmt.Sprintf("error when running os.Lstat(%q): %s", path, err), msgAndArgs...) + } + if !info.IsDir() { + return Fail(t, fmt.Sprintf("%q is a file", path), msgAndArgs...) + } + return true +} + +// NoDirExists checks whether a directory does not exist in the given path. +// It fails if the path points to an existing _directory_ only. +func NoDirExists(t TestingT, path string, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + info, err := os.Lstat(path) + if err != nil { + if os.IsNotExist(err) { + return true + } + return true + } + if !info.IsDir() { + return true + } + return Fail(t, fmt.Sprintf("directory %q exists", path), msgAndArgs...) +} + +// JSONEq asserts that two JSON strings are equivalent. +// +// assert.JSONEq(t, `{"hello": "world", "foo": "bar"}`, `{"foo": "bar", "hello": "world"}`) +func JSONEq(t TestingT, expected string, actual string, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + var expectedJSONAsInterface, actualJSONAsInterface interface{} + + if err := json.Unmarshal([]byte(expected), &expectedJSONAsInterface); err != nil { + return Fail(t, fmt.Sprintf("Expected value ('%s') is not valid json.\nJSON parsing error: '%s'", expected, err.Error()), msgAndArgs...) + } + + // Shortcut if same bytes + if actual == expected { + return true + } + + if err := json.Unmarshal([]byte(actual), &actualJSONAsInterface); err != nil { + return Fail(t, fmt.Sprintf("Input ('%s') needs to be valid json.\nJSON parsing error: '%s'", actual, err.Error()), msgAndArgs...) + } + + return Equal(t, expectedJSONAsInterface, actualJSONAsInterface, msgAndArgs...) +} + +// YAMLEq asserts that two YAML strings are equivalent. +func YAMLEq(t TestingT, expected string, actual string, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + var expectedYAMLAsInterface, actualYAMLAsInterface interface{} + + if err := yaml.Unmarshal([]byte(expected), &expectedYAMLAsInterface); err != nil { + return Fail(t, fmt.Sprintf("Expected value ('%s') is not valid yaml.\nYAML parsing error: '%s'", expected, err.Error()), msgAndArgs...) + } + + // Shortcut if same bytes + if actual == expected { + return true + } + + if err := yaml.Unmarshal([]byte(actual), &actualYAMLAsInterface); err != nil { + return Fail(t, fmt.Sprintf("Input ('%s') needs to be valid yaml.\nYAML error: '%s'", actual, err.Error()), msgAndArgs...) + } + + return Equal(t, expectedYAMLAsInterface, actualYAMLAsInterface, msgAndArgs...) +} + +func typeAndKind(v interface{}) (reflect.Type, reflect.Kind) { + t := reflect.TypeOf(v) + k := t.Kind() + + if k == reflect.Ptr { + t = t.Elem() + k = t.Kind() + } + return t, k +} + +// diff returns a diff of both values as long as both are of the same type and +// are a struct, map, slice, array or string. Otherwise it returns an empty string. +func diff(expected interface{}, actual interface{}) string { + if expected == nil || actual == nil { + return "" + } + + et, ek := typeAndKind(expected) + at, _ := typeAndKind(actual) + + if et != at { + return "" + } + + if ek != reflect.Struct && ek != reflect.Map && ek != reflect.Slice && ek != reflect.Array && ek != reflect.String { + return "" + } + + var e, a string + + switch et { + case reflect.TypeOf(""): + e = reflect.ValueOf(expected).String() + a = reflect.ValueOf(actual).String() + case reflect.TypeOf(time.Time{}): + e = spewConfigStringerEnabled.Sdump(expected) + a = spewConfigStringerEnabled.Sdump(actual) + default: + e = spewConfig.Sdump(expected) + a = spewConfig.Sdump(actual) + } + + diff, _ := difflib.GetUnifiedDiffString(difflib.UnifiedDiff{ + A: difflib.SplitLines(e), + B: difflib.SplitLines(a), + FromFile: "Expected", + FromDate: "", + ToFile: "Actual", + ToDate: "", + Context: 1, + }) + + return "\n\nDiff:\n" + diff +} + +func isFunction(arg interface{}) bool { + if arg == nil { + return false + } + return reflect.TypeOf(arg).Kind() == reflect.Func +} + +var spewConfig = spew.ConfigState{ + Indent: " ", + DisablePointerAddresses: true, + DisableCapacities: true, + SortKeys: true, + DisableMethods: true, + MaxDepth: 10, +} + +var spewConfigStringerEnabled = spew.ConfigState{ + Indent: " ", + DisablePointerAddresses: true, + DisableCapacities: true, + SortKeys: true, + MaxDepth: 10, +} + +type tHelper = interface { + Helper() +} + +// Eventually asserts that given condition will be met in waitFor time, +// periodically checking target function each tick. +// +// assert.Eventually(t, func() bool { return true; }, time.Second, 10*time.Millisecond) +func Eventually(t TestingT, condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + + ch := make(chan bool, 1) + checkCond := func() { ch <- condition() } + + timer := time.NewTimer(waitFor) + defer timer.Stop() + + ticker := time.NewTicker(tick) + defer ticker.Stop() + + var tickC <-chan time.Time + + // Check the condition once first on the initial call. + go checkCond() + + for { + select { + case <-timer.C: + return Fail(t, "Condition never satisfied", msgAndArgs...) + case <-tickC: + tickC = nil + go checkCond() + case v := <-ch: + if v { + return true + } + tickC = ticker.C + } + } +} + +// CollectT implements the TestingT interface and collects all errors. +type CollectT struct { + // A slice of errors. Non-nil slice denotes a failure. + // If it's non-nil but len(c.errors) == 0, this is also a failure + // obtained by direct c.FailNow() call. + errors []error +} + +// Helper is like [testing.T.Helper] but does nothing. +func (CollectT) Helper() {} + +// Errorf collects the error. +func (c *CollectT) Errorf(format string, args ...interface{}) { + c.errors = append(c.errors, fmt.Errorf(format, args...)) +} + +// FailNow stops execution by calling runtime.Goexit. +func (c *CollectT) FailNow() { + c.fail() + runtime.Goexit() +} + +// Deprecated: That was a method for internal usage that should not have been published. Now just panics. +func (*CollectT) Reset() { + panic("Reset() is deprecated") +} + +// Deprecated: That was a method for internal usage that should not have been published. Now just panics. +func (*CollectT) Copy(TestingT) { + panic("Copy() is deprecated") +} + +func (c *CollectT) fail() { + if !c.failed() { + c.errors = []error{} // Make it non-nil to mark a failure. + } +} + +func (c *CollectT) failed() bool { + return c.errors != nil +} + +// EventuallyWithT asserts that given condition will be met in waitFor time, +// periodically checking target function each tick. In contrast to Eventually, +// it supplies a CollectT to the condition function, so that the condition +// function can use the CollectT to call other assertions. +// The condition is considered "met" if no errors are raised in a tick. +// The supplied CollectT collects all errors from one tick (if there are any). +// If the condition is not met before waitFor, the collected errors of +// the last tick are copied to t. +// +// externalValue := false +// go func() { +// time.Sleep(8*time.Second) +// externalValue = true +// }() +// assert.EventuallyWithT(t, func(c *assert.CollectT) { +// // add assertions as needed; any assertion failure will fail the current tick +// assert.True(c, externalValue, "expected 'externalValue' to be true") +// }, 10*time.Second, 1*time.Second, "external state has not changed to 'true'; still false") +func EventuallyWithT(t TestingT, condition func(collect *CollectT), waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + + var lastFinishedTickErrs []error + ch := make(chan *CollectT, 1) + + checkCond := func() { + collect := new(CollectT) + defer func() { + ch <- collect + }() + condition(collect) + } + + timer := time.NewTimer(waitFor) + defer timer.Stop() + + ticker := time.NewTicker(tick) + defer ticker.Stop() + + var tickC <-chan time.Time + + // Check the condition once first on the initial call. + go checkCond() + + for { + select { + case <-timer.C: + for _, err := range lastFinishedTickErrs { + t.Errorf("%v", err) + } + return Fail(t, "Condition never satisfied", msgAndArgs...) + case <-tickC: + tickC = nil + go checkCond() + case collect := <-ch: + if !collect.failed() { + return true + } + // Keep the errors from the last ended condition, so that they can be copied to t if timeout is reached. + lastFinishedTickErrs = collect.errors + tickC = ticker.C + } + } +} + +// Never asserts that the given condition doesn't satisfy in waitFor time, +// periodically checking the target function each tick. +// +// assert.Never(t, func() bool { return false; }, time.Second, 10*time.Millisecond) +func Never(t TestingT, condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + + ch := make(chan bool, 1) + checkCond := func() { ch <- condition() } + + timer := time.NewTimer(waitFor) + defer timer.Stop() + + ticker := time.NewTicker(tick) + defer ticker.Stop() + + var tickC <-chan time.Time + + // Check the condition once first on the initial call. + go checkCond() + + for { + select { + case <-timer.C: + return true + case <-tickC: + tickC = nil + go checkCond() + case v := <-ch: + if v { + return Fail(t, "Condition satisfied", msgAndArgs...) + } + tickC = ticker.C + } + } +} + +// ErrorIs asserts that at least one of the errors in err's chain matches target. +// This is a wrapper for errors.Is. +func ErrorIs(t TestingT, err, target error, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if errors.Is(err, target) { + return true + } + + var expectedText string + if target != nil { + expectedText = target.Error() + if err == nil { + return Fail(t, fmt.Sprintf("Expected error with %q in chain but got nil.", expectedText), msgAndArgs...) + } + } + + chain := buildErrorChainString(err, false) + + return Fail(t, fmt.Sprintf("Target error should be in err chain:\n"+ + "expected: %q\n"+ + "in chain: %s", expectedText, chain, + ), msgAndArgs...) +} + +// NotErrorIs asserts that none of the errors in err's chain matches target. +// This is a wrapper for errors.Is. +func NotErrorIs(t TestingT, err, target error, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if !errors.Is(err, target) { + return true + } + + var expectedText string + if target != nil { + expectedText = target.Error() + } + + chain := buildErrorChainString(err, false) + + return Fail(t, fmt.Sprintf("Target error should not be in err chain:\n"+ + "found: %q\n"+ + "in chain: %s", expectedText, chain, + ), msgAndArgs...) +} + +// ErrorAs asserts that at least one of the errors in err's chain matches target, and if so, sets target to that error value. +// This is a wrapper for errors.As. +func ErrorAs(t TestingT, err error, target interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if errors.As(err, target) { + return true + } + + expectedType := reflect.TypeOf(target).Elem().String() + if err == nil { + return Fail(t, fmt.Sprintf("An error is expected but got nil.\n"+ + "expected: %s", expectedType), msgAndArgs...) + } + + chain := buildErrorChainString(err, true) + + return Fail(t, fmt.Sprintf("Should be in error chain:\n"+ + "expected: %s\n"+ + "in chain: %s", expectedType, chain, + ), msgAndArgs...) +} + +// NotErrorAs asserts that none of the errors in err's chain matches target, +// but if so, sets target to that error value. +func NotErrorAs(t TestingT, err error, target interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if !errors.As(err, target) { + return true + } + + chain := buildErrorChainString(err, true) + + return Fail(t, fmt.Sprintf("Target error should not be in err chain:\n"+ + "found: %s\n"+ + "in chain: %s", reflect.TypeOf(target).Elem().String(), chain, + ), msgAndArgs...) +} + +func unwrapAll(err error) (errs []error) { + errs = append(errs, err) + switch x := err.(type) { + case interface{ Unwrap() error }: + err = x.Unwrap() + if err == nil { + return + } + errs = append(errs, unwrapAll(err)...) + case interface{ Unwrap() []error }: + for _, err := range x.Unwrap() { + errs = append(errs, unwrapAll(err)...) + } + } + return +} + +func buildErrorChainString(err error, withType bool) string { + if err == nil { + return "" + } + + var chain string + errs := unwrapAll(err) + for i := range errs { + if i != 0 { + chain += "\n\t" + } + chain += fmt.Sprintf("%q", errs[i].Error()) + if withType { + chain += fmt.Sprintf(" (%T)", errs[i]) + } + } + return chain +} diff --git a/pkg/testify/assert/assertions_test.go b/pkg/testify/assert/assertions_test.go new file mode 100644 index 00000000..346a4265 --- /dev/null +++ b/pkg/testify/assert/assertions_test.go @@ -0,0 +1,3917 @@ +package assert + +import ( + "bufio" + "bytes" + "encoding/json" + "errors" + "fmt" + testing "github.com/CodSpeedHQ/codspeed-go/testing/testing" + "io" + "math" + "os" + "path/filepath" + "reflect" + "regexp" + "runtime" + "strings" + "time" +) + +var ( + i interface{} + zeros = []interface{}{ + false, + byte(0), + complex64(0), + complex128(0), + float32(0), + float64(0), + int(0), + int8(0), + int16(0), + int32(0), + int64(0), + rune(0), + uint(0), + uint8(0), + uint16(0), + uint32(0), + uint64(0), + uintptr(0), + "", + [0]interface{}{}, + []interface{}(nil), + struct{ x int }{}, + (*interface{})(nil), + (func())(nil), + nil, + interface{}(nil), + map[interface{}]interface{}(nil), + (chan interface{})(nil), + (<-chan interface{})(nil), + (chan<- interface{})(nil), + } + nonZeros = []interface{}{ + true, + byte(1), + complex64(1), + complex128(1), + float32(1), + float64(1), + int(1), + int8(1), + int16(1), + int32(1), + int64(1), + rune(1), + uint(1), + uint8(1), + uint16(1), + uint32(1), + uint64(1), + uintptr(1), + "s", + [1]interface{}{1}, + []interface{}{}, + struct{ x int }{1}, + (&i), + (func() {}), + interface{}(1), + map[interface{}]interface{}{}, + (make(chan interface{})), + (<-chan interface{})(make(chan interface{})), + (chan<- interface{})(make(chan interface{})), + } +) + +// 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 +type AssertionTesterConformingObject struct{} + +func (a *AssertionTesterConformingObject) TestMethod() { +} + +// AssertionTesterNonConformingObject is an object that does not conform to the AssertionTesterInterface interface +type AssertionTesterNonConformingObject struct{} + +func TestObjectsAreEqual(t *testing.T) { + t.Parallel() + + cases := []struct { + expected interface{} + actual interface{} + result bool + }{ + // cases that are expected to be equal + {"Hello World", "Hello World", true}, + {123, 123, true}, + {123.5, 123.5, true}, + {[]byte("Hello World"), []byte("Hello World"), true}, + {nil, nil, true}, + + // cases that are expected not to be equal + {map[int]int{5: 10}, map[int]int{10: 20}, false}, + {'x', "x", false}, + {"x", 'x', false}, + {0, 0.1, false}, + {0.1, 0, false}, + {time.Now, time.Now, false}, + {func() {}, func() {}, false}, + {uint32(10), int32(10), false}, + } + + for _, c := range cases { + t.Run(fmt.Sprintf("ObjectsAreEqual(%#v, %#v)", c.expected, c.actual), func(t *testing.T) { + res := ObjectsAreEqual(c.expected, c.actual) + + if res != c.result { + t.Errorf("ObjectsAreEqual(%#v, %#v) should return %#v", c.expected, c.actual, c.result) + } + }) + } +} + +func TestObjectsAreEqualValues(t *testing.T) { + t.Parallel() + + now := time.Now() + + cases := []struct { + expected interface{} + actual interface{} + result bool + }{ + {uint32(10), int32(10), true}, + {0, nil, false}, + {nil, 0, false}, + {now, now.In(time.Local), false}, // should not be time zone independent + {int(270), int8(14), false}, // should handle overflow/underflow + {int8(14), int(270), false}, + {[]int{270, 270}, []int8{14, 14}, false}, + {complex128(1e+100 + 1e+100i), complex64(complex(math.Inf(0), math.Inf(0))), false}, + {complex64(complex(math.Inf(0), math.Inf(0))), complex128(1e+100 + 1e+100i), false}, + {complex128(1e+100 + 1e+100i), 270, false}, + {270, complex128(1e+100 + 1e+100i), false}, + {complex128(1e+100 + 1e+100i), 3.14, false}, + {3.14, complex128(1e+100 + 1e+100i), false}, + {complex128(1e+10 + 1e+10i), complex64(1e+10 + 1e+10i), true}, + {complex64(1e+10 + 1e+10i), complex128(1e+10 + 1e+10i), true}, + } + + for _, c := range cases { + t.Run(fmt.Sprintf("ObjectsAreEqualValues(%#v, %#v)", c.expected, c.actual), func(t *testing.T) { + res := ObjectsAreEqualValues(c.expected, c.actual) + + if res != c.result { + t.Errorf("ObjectsAreEqualValues(%#v, %#v) should return %#v", c.expected, c.actual, c.result) + } + }) + } +} + +type Nested struct { + Exported interface{} + notExported interface{} +} + +type S struct { + Exported1 interface{} + Exported2 Nested + notExported1 interface{} + notExported2 Nested +} + +type S2 struct { + foo interface{} +} + +type S3 struct { + Exported1 *Nested + Exported2 *Nested +} + +type S4 struct { + Exported1 []*Nested +} + +type S5 struct { + Exported Nested +} + +type S6 struct { + Exported string + unexported string +} + +func TestObjectsExportedFieldsAreEqual(t *testing.T) { + t.Parallel() + + intValue := 1 + + cases := []struct { + expected interface{} + actual interface{} + result bool + }{ + {S{1, Nested{2, 3}, 4, Nested{5, 6}}, S{1, Nested{2, 3}, 4, Nested{5, 6}}, true}, + {S{1, Nested{2, 3}, 4, Nested{5, 6}}, S{1, Nested{2, 3}, "a", Nested{5, 6}}, true}, + {S{1, Nested{2, 3}, 4, Nested{5, 6}}, S{1, Nested{2, 3}, 4, Nested{5, "a"}}, true}, + {S{1, Nested{2, 3}, 4, Nested{5, 6}}, S{1, Nested{2, 3}, 4, Nested{"a", "a"}}, true}, + {S{1, Nested{2, 3}, 4, Nested{5, 6}}, S{1, Nested{2, "a"}, 4, Nested{5, 6}}, true}, + {S{1, Nested{2, 3}, 4, Nested{5, 6}}, S{"a", Nested{2, 3}, 4, Nested{5, 6}}, false}, + {S{1, Nested{2, 3}, 4, Nested{5, 6}}, S{1, Nested{"a", 3}, 4, Nested{5, 6}}, false}, + {S{1, Nested{2, 3}, 4, Nested{5, 6}}, S2{1}, false}, + {1, S{1, Nested{2, 3}, 4, Nested{5, 6}}, false}, + + {S3{&Nested{1, 2}, &Nested{3, 4}}, S3{&Nested{1, 2}, &Nested{3, 4}}, true}, + {S3{nil, &Nested{3, 4}}, S3{nil, &Nested{3, 4}}, true}, + {S3{&Nested{1, 2}, &Nested{3, 4}}, S3{&Nested{1, 2}, &Nested{3, "b"}}, true}, + {S3{&Nested{1, 2}, &Nested{3, 4}}, S3{&Nested{1, "a"}, &Nested{3, "b"}}, true}, + {S3{&Nested{1, 2}, &Nested{3, 4}}, S3{&Nested{"a", 2}, &Nested{3, 4}}, false}, + {S3{&Nested{1, 2}, &Nested{3, 4}}, S3{}, false}, + {S3{}, S3{}, true}, + + {S4{[]*Nested{{1, 2}}}, S4{[]*Nested{{1, 2}}}, true}, + {S4{[]*Nested{{1, 2}}}, S4{[]*Nested{{1, 3}}}, true}, + {S4{[]*Nested{{1, 2}, {3, 4}}}, S4{[]*Nested{{1, "a"}, {3, "b"}}}, true}, + {S4{[]*Nested{{1, 2}, {3, 4}}}, S4{[]*Nested{{1, "a"}, {2, "b"}}}, false}, + + {Nested{&intValue, 2}, Nested{&intValue, 2}, true}, + {Nested{&Nested{1, 2}, 3}, Nested{&Nested{1, "b"}, 3}, true}, + {Nested{&Nested{1, 2}, 3}, Nested{nil, 3}, false}, + + { + Nested{map[interface{}]*Nested{nil: nil}, 2}, + Nested{map[interface{}]*Nested{nil: nil}, 2}, + true, + }, + { + Nested{map[interface{}]*Nested{"a": nil}, 2}, + Nested{map[interface{}]*Nested{"a": nil}, 2}, + true, + }, + { + Nested{map[interface{}]*Nested{"a": nil}, 2}, + Nested{map[interface{}]*Nested{"a": {1, 2}}, 2}, + false, + }, + { + Nested{map[interface{}]Nested{"a": {1, 2}, "b": {3, 4}}, 2}, + Nested{map[interface{}]Nested{"a": {1, 5}, "b": {3, 7}}, 2}, + true, + }, + { + Nested{map[interface{}]Nested{"a": {1, 2}, "b": {3, 4}}, 2}, + Nested{map[interface{}]Nested{"a": {2, 2}, "b": {3, 4}}, 2}, + false, + }, + } + + for _, c := range cases { + t.Run(fmt.Sprintf("ObjectsExportedFieldsAreEqual(%#v, %#v)", c.expected, c.actual), func(t *testing.T) { + res := ObjectsExportedFieldsAreEqual(c.expected, c.actual) + + if res != c.result { + t.Errorf("ObjectsExportedFieldsAreEqual(%#v, %#v) should return %#v", c.expected, c.actual, c.result) + } + }) + } +} + +func TestCopyExportedFields(t *testing.T) { + t.Parallel() + + intValue := 1 + + cases := []struct { + input interface{} + expected interface{} + }{ + { + input: Nested{"a", "b"}, + expected: Nested{"a", nil}, + }, + { + input: Nested{&intValue, 2}, + expected: Nested{&intValue, nil}, + }, + { + input: Nested{nil, 3}, + expected: Nested{nil, nil}, + }, + { + input: S{1, Nested{2, 3}, 4, Nested{5, 6}}, + expected: S{1, Nested{2, nil}, nil, Nested{}}, + }, + { + input: S3{}, + expected: S3{}, + }, + { + input: S3{&Nested{1, 2}, &Nested{3, 4}}, + expected: S3{&Nested{1, nil}, &Nested{3, nil}}, + }, + { + input: S3{Exported1: &Nested{"a", "b"}}, + expected: S3{Exported1: &Nested{"a", nil}}, + }, + { + input: S4{[]*Nested{ + nil, + {1, 2}, + }}, + expected: S4{[]*Nested{ + nil, + {1, nil}, + }}, + }, + { + input: S4{ + []*Nested{ + {1, 2}, + }, + }, + expected: S4{ + []*Nested{ + {1, nil}, + }, + }, + }, + { + input: S4{[]*Nested{ + {1, 2}, + {3, 4}, + }}, + expected: S4{[]*Nested{ + {1, nil}, + {3, nil}, + }}, + }, + { + input: S5{Exported: Nested{"a", "b"}}, + expected: S5{Exported: Nested{"a", nil}}, + }, + { + input: S6{"a", "b"}, + expected: S6{"a", ""}, + }, + } + + for _, c := range cases { + t.Run("", func(t *testing.T) { + output := copyExportedFields(c.input) + if !ObjectsAreEqualValues(c.expected, output) { + t.Errorf("%#v, %#v should be equal", c.expected, output) + } + }) + } +} + +func TestEqualExportedValues(t *testing.T) { + t.Parallel() + + cases := []struct { + value1 interface{} + value2 interface{} + expectedEqual bool + expectedFail string + }{ + { + value1: S{1, Nested{2, 3}, 4, Nested{5, 6}}, + value2: S{1, Nested{2, nil}, nil, Nested{}}, + expectedEqual: true, + }, + { + value1: S{1, Nested{2, 3}, 4, Nested{5, 6}}, + value2: S{1, Nested{1, nil}, nil, Nested{}}, + expectedEqual: false, + expectedFail: ` + Diff: + --- Expected + +++ Actual + @@ -3,3 +3,3 @@ + Exported2: (assert.Nested) { + - Exported: (int) 2, + + Exported: (int) 1, + notExported: (interface {}) `, + }, + { + value1: S3{&Nested{1, 2}, &Nested{3, 4}}, + value2: S3{&Nested{"a", 2}, &Nested{3, 4}}, + expectedEqual: false, + expectedFail: ` + Diff: + --- Expected + +++ Actual + @@ -2,3 +2,3 @@ + Exported1: (*assert.Nested)({ + - Exported: (int) 1, + + Exported: (string) (len=1) "a", + notExported: (interface {}) `, + }, + { + value1: S4{[]*Nested{ + {1, 2}, + {3, 4}, + }}, + value2: S4{[]*Nested{ + {1, "a"}, + {2, "b"}, + }}, + expectedEqual: false, + expectedFail: ` + Diff: + --- Expected + +++ Actual + @@ -7,3 +7,3 @@ + (*assert.Nested)({ + - Exported: (int) 3, + + Exported: (int) 2, + notExported: (interface {}) `, + }, + { + value1: S{[2]int{1, 2}, Nested{2, 3}, 4, Nested{5, 6}}, + value2: S{[2]int{1, 2}, Nested{2, nil}, nil, Nested{}}, + expectedEqual: true, + }, + { + value1: &S{1, Nested{2, 3}, 4, Nested{5, 6}}, + value2: &S{1, Nested{2, nil}, nil, Nested{}}, + expectedEqual: true, + }, + { + value1: &S{1, Nested{2, 3}, 4, Nested{5, 6}}, + value2: &S{1, Nested{1, nil}, nil, Nested{}}, + expectedEqual: false, + expectedFail: ` + Diff: + --- Expected + +++ Actual + @@ -3,3 +3,3 @@ + Exported2: (assert.Nested) { + - Exported: (int) 2, + + Exported: (int) 1, + notExported: (interface {}) `, + }, + { + value1: []int{1, 2}, + value2: []int{1, 2}, + expectedEqual: true, + }, + { + value1: []int{1, 2}, + value2: []int{1, 3}, + expectedEqual: false, + expectedFail: ` + Diff: + --- Expected + +++ Actual + @@ -2,3 +2,3 @@ + (int) 1, + - (int) 2 + + (int) 3 + }`, + }, + { + value1: []*Nested{ + {1, 2}, + {3, 4}, + }, + value2: []*Nested{ + {1, "a"}, + {3, "b"}, + }, + expectedEqual: true, + }, + { + value1: []*Nested{ + {1, 2}, + {3, 4}, + }, + value2: []*Nested{ + {1, "a"}, + {2, "b"}, + }, + expectedEqual: false, + expectedFail: ` + Diff: + --- Expected + +++ Actual + @@ -6,3 +6,3 @@ + (*assert.Nested)({ + - Exported: (int) 3, + + Exported: (int) 2, + notExported: (interface {}) `, + }, + } + + for _, c := range cases { + t.Run("", func(t *testing.T) { + mockT := new(mockTestingT) + + actual := EqualExportedValues(mockT, c.value1, c.value2) + if actual != c.expectedEqual { + t.Errorf("Expected EqualExportedValues to be %t, but was %t", c.expectedEqual, actual) + } + + actualFail := mockT.errorString() + if !strings.Contains(actualFail, c.expectedFail) { + t.Errorf("Contains failure should include %q but was %q", c.expectedFail, actualFail) + } + }) + } +} + +func TestImplements(t *testing.T) { + t.Parallel() + + mockT := new(testing.T) + + if !Implements(mockT, (*AssertionTesterInterface)(nil), new(AssertionTesterConformingObject)) { + t.Error("Implements method should return true: AssertionTesterConformingObject implements AssertionTesterInterface") + } + if Implements(mockT, (*AssertionTesterInterface)(nil), new(AssertionTesterNonConformingObject)) { + t.Error("Implements method should return false: AssertionTesterNonConformingObject does not implements AssertionTesterInterface") + } + if Implements(mockT, (*AssertionTesterInterface)(nil), nil) { + t.Error("Implements method should return false: nil does not implement AssertionTesterInterface") + } +} + +func TestNotImplements(t *testing.T) { + t.Parallel() + + mockT := new(testing.T) + + if !NotImplements(mockT, (*AssertionTesterInterface)(nil), new(AssertionTesterNonConformingObject)) { + t.Error("NotImplements method should return true: AssertionTesterNonConformingObject does not implement AssertionTesterInterface") + } + if NotImplements(mockT, (*AssertionTesterInterface)(nil), new(AssertionTesterConformingObject)) { + t.Error("NotImplements method should return false: AssertionTesterConformingObject implements AssertionTesterInterface") + } + if NotImplements(mockT, (*AssertionTesterInterface)(nil), nil) { + t.Error("NotImplements method should return false: nil can't be checked to be implementing AssertionTesterInterface or not") + } +} + +func TestIsType(t *testing.T) { + t.Parallel() + + mockT := new(testing.T) + + if !IsType(mockT, new(AssertionTesterConformingObject), new(AssertionTesterConformingObject)) { + t.Error("IsType should return true: AssertionTesterConformingObject is the same type as AssertionTesterConformingObject") + } + if IsType(mockT, new(AssertionTesterConformingObject), new(AssertionTesterNonConformingObject)) { + t.Error("IsType should return false: AssertionTesterConformingObject is not the same type as AssertionTesterNonConformingObject") + } +} + +func TestNotIsType(t *testing.T) { + t.Parallel() + + mockT := new(testing.T) + + if !IsNotType(mockT, new(AssertionTesterConformingObject), new(AssertionTesterNonConformingObject)) { + t.Error("NotIsType should return true: AssertionTesterConformingObject is not the same type as AssertionTesterNonConformingObject") + } + if IsNotType(mockT, new(AssertionTesterConformingObject), new(AssertionTesterConformingObject)) { + t.Error("NotIsType should return false: AssertionTesterConformingObject is the same type as AssertionTesterConformingObject") + } +} + +func TestEqual(t *testing.T) { + t.Parallel() + + type myType string + + mockT := new(testing.T) + var m map[string]interface{} + + cases := []struct { + expected interface{} + actual interface{} + result bool + remark string + }{ + {"Hello World", "Hello World", true, ""}, + {123, 123, true, ""}, + {123.5, 123.5, true, ""}, + {[]byte("Hello World"), []byte("Hello World"), true, ""}, + {nil, nil, true, ""}, + {int32(123), int32(123), true, ""}, + {uint64(123), uint64(123), true, ""}, + {myType("1"), myType("1"), true, ""}, + {&struct{}{}, &struct{}{}, true, "pointer equality is based on equality of underlying value"}, + + // Not expected to be equal + {m["bar"], "something", false, ""}, + {myType("1"), myType("2"), false, ""}, + + // A case that might be confusing, especially with numeric literals + {10, uint(10), false, ""}, + } + + for _, c := range cases { + t.Run(fmt.Sprintf("Equal(%#v, %#v)", c.expected, c.actual), func(t *testing.T) { + res := Equal(mockT, c.expected, c.actual) + + if res != c.result { + t.Errorf("Equal(%#v, %#v) should return %#v: %s", c.expected, c.actual, c.result, c.remark) + } + }) + } +} + +func ptr(i int) *int { + return &i +} + +func TestSame(t *testing.T) { + t.Parallel() + + mockT := new(testing.T) + + if Same(mockT, ptr(1), ptr(1)) { + t.Error("Same should return false") + } + if Same(mockT, 1, 1) { + t.Error("Same should return false") + } + p := ptr(2) + if Same(mockT, p, *p) { + t.Error("Same should return false") + } + if !Same(mockT, p, p) { + t.Error("Same should return true") + } +} + +func TestNotSame(t *testing.T) { + t.Parallel() + + mockT := new(testing.T) + + if !NotSame(mockT, ptr(1), ptr(1)) { + t.Error("NotSame should return true; different pointers") + } + if !NotSame(mockT, 1, 1) { + t.Error("NotSame should return true; constant inputs") + } + p := ptr(2) + if !NotSame(mockT, p, *p) { + t.Error("NotSame should return true; mixed-type inputs") + } + if NotSame(mockT, p, p) { + t.Error("NotSame should return false") + } +} + +func Test_samePointers(t *testing.T) { + t.Parallel() + + p := ptr(2) + + type args struct { + first interface{} + second interface{} + } + tests := []struct { + name string + args args + same BoolAssertionFunc + ok BoolAssertionFunc + }{ + { + name: "1 != 2", + args: args{first: 1, second: 2}, + same: False, + ok: False, + }, + { + name: "1 != 1 (not same ptr)", + args: args{first: 1, second: 1}, + same: False, + ok: False, + }, + { + name: "ptr(1) == ptr(1)", + args: args{first: p, second: p}, + same: True, + ok: True, + }, + { + name: "int(1) != float32(1)", + args: args{first: int(1), second: float32(1)}, + same: False, + ok: False, + }, + { + name: "array != slice", + args: args{first: [2]int{1, 2}, second: []int{1, 2}}, + same: False, + ok: False, + }, + { + name: "non-pointer vs pointer (1 != ptr(2))", + args: args{first: 1, second: p}, + same: False, + ok: False, + }, + { + name: "pointer vs non-pointer (ptr(2) != 1)", + args: args{first: p, second: 1}, + same: False, + ok: False, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + same, ok := samePointers(tt.args.first, tt.args.second) + tt.same(t, same) + tt.ok(t, ok) + }) + } +} + +// bufferT implements TestingT. Its implementation of Errorf writes the output that would be produced by +// testing.T.Errorf to an internal bytes.Buffer. +type bufferT struct { + buf bytes.Buffer +} + +// Helper is like [testing.T.Helper] but does nothing. +func (bufferT) Helper() {} + +func (t *bufferT) Errorf(format string, args ...interface{}) { + // implementation of decorate is copied from testing.T + decorate := func(s string) string { + _, file, line, ok := runtime.Caller(3) // decorate + log + public function. + if ok { + // Truncate file name at last file name separator. + if index := strings.LastIndex(file, "/"); index >= 0 { + file = file[index+1:] + } else if index = strings.LastIndex(file, "\\"); index >= 0 { + file = file[index+1:] + } + } else { + file = "???" + line = 1 + } + buf := new(bytes.Buffer) + // Every line is indented at least one tab. + buf.WriteByte('\t') + fmt.Fprintf(buf, "%s:%d: ", file, line) + lines := strings.Split(s, "\n") + if l := len(lines); l > 1 && lines[l-1] == "" { + lines = lines[:l-1] + } + for i, line := range lines { + if i > 0 { + // Second and subsequent lines are indented an extra tab. + buf.WriteString("\n\t\t") + } + buf.WriteString(line) + } + buf.WriteByte('\n') + return buf.String() + } + t.buf.WriteString(decorate(fmt.Sprintf(format, args...))) +} + +func TestStringEqual(t *testing.T) { + t.Parallel() + + for i, currCase := range []struct { + equalWant string + equalGot string + msgAndArgs []interface{} + 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"}, + } { + mockT := &bufferT{} + Equal(mockT, currCase.equalWant, currCase.equalGot, currCase.msgAndArgs...) + Regexp(t, regexp.MustCompile(currCase.want), mockT.buf.String(), "Case %d", i) + } +} + +func TestEqualFormatting(t *testing.T) { + t.Parallel() + + for i, currCase := range []struct { + equalWant string + equalGot string + msgAndArgs []interface{} + 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: []interface{}{"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: []interface{}{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: []interface{}{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...) + Regexp(t, regexp.MustCompile(currCase.want), mockT.buf.String(), "Case %d", i) + } +} + +func TestFormatUnequalValues(t *testing.T) { + t.Parallel() + + expected, actual := formatUnequalValues("foo", "bar") + Equal(t, `"foo"`, expected, "value should not include type") + Equal(t, `"bar"`, actual, "value should not include type") + + expected, actual = formatUnequalValues(123, 123) + Equal(t, `123`, expected, "value should not include type") + Equal(t, `123`, actual, "value should not include type") + + expected, actual = formatUnequalValues(int64(123), int32(123)) + Equal(t, `int64(123)`, expected, "value should include type") + Equal(t, `int32(123)`, actual, "value should include type") + + expected, actual = formatUnequalValues(int64(123), nil) + Equal(t, `int64(123)`, expected, "value should include type") + Equal(t, `()`, actual, "value should include type") + + type testStructType struct { + Val string + } + + expected, actual = formatUnequalValues(&testStructType{Val: "test"}, &testStructType{Val: "test"}) + Equal(t, `&assert.testStructType{Val:"test"}`, expected, "value should not include type annotation") + Equal(t, `&assert.testStructType{Val:"test"}`, actual, "value should not include type annotation") +} + +func TestNotNil(t *testing.T) { + t.Parallel() + + mockT := new(testing.T) + + if !NotNil(mockT, new(AssertionTesterConformingObject)) { + t.Error("NotNil should return true: object is not nil") + } + if NotNil(mockT, nil) { + t.Error("NotNil should return false: object is nil") + } + if NotNil(mockT, (*struct{})(nil)) { + t.Error("NotNil should return false: object is (*struct{})(nil)") + } +} + +func TestNil(t *testing.T) { + t.Parallel() + + mockT := new(testing.T) + + if !Nil(mockT, nil) { + t.Error("Nil should return true: object is nil") + } + if !Nil(mockT, (*struct{})(nil)) { + t.Error("Nil should return true: object is (*struct{})(nil)") + } + if Nil(mockT, new(AssertionTesterConformingObject)) { + t.Error("Nil should return false: object is not nil") + } +} + +func TestTrue(t *testing.T) { + t.Parallel() + + mockT := new(testing.T) + + if !True(mockT, true) { + t.Error("True should return true") + } + if True(mockT, false) { + t.Error("True should return false") + } +} + +func TestFalse(t *testing.T) { + t.Parallel() + + mockT := new(testing.T) + + if !False(mockT, false) { + t.Error("False should return true") + } + if False(mockT, true) { + t.Error("False should return false") + } +} + +func TestExactly(t *testing.T) { + t.Parallel() + mockT := new(testing.T) + + a := float32(1) + b := float64(1) + c := float32(1) + d := float32(2) + cases := []struct { + expected interface{} + actual interface{} + result bool + }{ + {a, b, false}, + {a, d, false}, + {a, c, true}, + {nil, a, false}, + {a, nil, false}, + } + + for _, c := range cases { + t.Run(fmt.Sprintf("Exactly(%#v, %#v)", c.expected, c.actual), func(t *testing.T) { + res := Exactly(mockT, c.expected, c.actual) + + if res != c.result { + t.Errorf("Exactly(%#v, %#v) should return %#v", c.expected, c.actual, c.result) + } + }) + } +} + +func TestNotEqual(t *testing.T) { + t.Parallel() + mockT := new(testing.T) + + cases := []struct { + expected interface{} + actual interface{} + result bool + }{ + // cases that are expected not to match + {"Hello World", "Hello World!", true}, + {123, 1234, true}, + {123.5, 123.55, true}, + {[]byte("Hello World"), []byte("Hello World!"), true}, + {nil, new(AssertionTesterConformingObject), true}, + + // cases that are expected to match + {nil, nil, false}, + {"Hello World", "Hello World", false}, + {123, 123, false}, + {123.5, 123.5, false}, + {[]byte("Hello World"), []byte("Hello World"), false}, + {new(AssertionTesterConformingObject), new(AssertionTesterConformingObject), false}, + {&struct{}{}, &struct{}{}, false}, + {func() int { return 23 }, func() int { return 24 }, false}, + // A case that might be confusing, especially with numeric literals + {int(10), uint(10), true}, + } + + for _, c := range cases { + t.Run(fmt.Sprintf("NotEqual(%#v, %#v)", c.expected, c.actual), func(t *testing.T) { + res := NotEqual(mockT, c.expected, c.actual) + + if res != c.result { + t.Errorf("NotEqual(%#v, %#v) should return %#v", c.expected, c.actual, c.result) + } + }) + } +} + +func TestEqualValuesAndNotEqualValues(t *testing.T) { + t.Parallel() + + mockT := new(testing.T) + + cases := []struct { + expected interface{} + actual interface{} + notEqualResult bool // result for NotEqualValues + }{ + // cases that are expected not to match + {"Hello World", "Hello World!", true}, + {123, 1234, true}, + {123.5, 123.55, true}, + {[]byte("Hello World"), []byte("Hello World!"), true}, + {nil, new(AssertionTesterConformingObject), true}, + + // cases that are expected to match + {nil, nil, false}, + {"Hello World", "Hello World", false}, + {123, 123, false}, + {123.5, 123.5, false}, + {[]byte("Hello World"), []byte("Hello World"), false}, + {new(AssertionTesterConformingObject), new(AssertionTesterConformingObject), false}, + {&struct{}{}, &struct{}{}, false}, + + // Different behavior from NotEqual() + {func() int { return 23 }, func() int { return 24 }, true}, + {int(10), int(11), true}, + {int(10), uint(10), false}, + + {struct{}{}, struct{}{}, false}, + } + + for _, c := range cases { + // Test NotEqualValues + t.Run(fmt.Sprintf("NotEqualValues(%#v, %#v)", c.expected, c.actual), func(t *testing.T) { + res := NotEqualValues(mockT, c.expected, c.actual) + + if res != c.notEqualResult { + t.Errorf("NotEqualValues(%#v, %#v) should return %#v", c.expected, c.actual, c.notEqualResult) + } + }) + + // Test EqualValues (inverse of NotEqualValues) + t.Run(fmt.Sprintf("EqualValues(%#v, %#v)", c.expected, c.actual), func(t *testing.T) { + expectedEqualResult := !c.notEqualResult // EqualValues should return opposite of NotEqualValues + res := EqualValues(mockT, c.expected, c.actual) + + if res != expectedEqualResult { + t.Errorf("EqualValues(%#v, %#v) should return %#v", c.expected, c.actual, expectedEqualResult) + } + }) + } +} + +func TestContainsNotContains(t *testing.T) { + t.Parallel() + + type A struct { + Name, Value string + } + list := []string{"Foo", "Bar"} + + complexList := []*A{ + {"b", "c"}, + {"d", "e"}, + {"g", "h"}, + {"j", "k"}, + } + simpleMap := map[interface{}]interface{}{"Foo": "Bar"} + var zeroMap map[interface{}]interface{} + + cases := []struct { + expected interface{} + actual interface{} + result bool + }{ + {"Hello World", "Hello", true}, + {"Hello World", "Salut", false}, + {list, "Bar", true}, + {list, "Salut", false}, + {complexList, &A{"g", "h"}, true}, + {complexList, &A{"g", "e"}, false}, + {simpleMap, "Foo", true}, + {simpleMap, "Bar", false}, + {zeroMap, "Bar", false}, + } + + for _, c := range cases { + t.Run(fmt.Sprintf("Contains(%#v, %#v)", c.expected, c.actual), func(t *testing.T) { + mockT := new(testing.T) + res := Contains(mockT, c.expected, c.actual) + + if res != c.result { + if res { + t.Errorf("Contains(%#v, %#v) should return true:\n\t%#v contains %#v", c.expected, c.actual, c.expected, c.actual) + } else { + t.Errorf("Contains(%#v, %#v) should return false:\n\t%#v does not contain %#v", c.expected, c.actual, c.expected, c.actual) + } + } + }) + } + + for _, c := range cases { + t.Run(fmt.Sprintf("NotContains(%#v, %#v)", c.expected, c.actual), func(t *testing.T) { + mockT := new(testing.T) + res := NotContains(mockT, c.expected, c.actual) + + // NotContains should be inverse of Contains. If it's not, something is wrong + if res == Contains(mockT, c.expected, c.actual) { + if res { + t.Errorf("NotContains(%#v, %#v) should return true:\n\t%#v does not contains %#v", c.expected, c.actual, c.expected, c.actual) + } else { + t.Errorf("NotContains(%#v, %#v) should return false:\n\t%#v contains %#v", c.expected, c.actual, c.expected, c.actual) + } + } + }) + } +} + +func TestContainsNotContainsFailMessage(t *testing.T) { + t.Parallel() + + mockT := new(mockTestingT) + + type nonContainer struct { + Value string + } + + cases := []struct { + assertion func(t TestingT, s, contains interface{}, msgAndArgs ...interface{}) bool + container interface{} + instance interface{} + expected string + }{ + { + assertion: Contains, + container: "Hello World", + instance: errors.New("Hello"), + expected: "\"Hello World\" does not contain &errors.errorString{s:\"Hello\"}", + }, + { + assertion: Contains, + container: map[string]int{"one": 1}, + instance: "two", + expected: "map[string]int{\"one\":1} does not contain \"two\"\n", + }, + { + assertion: NotContains, + container: map[string]int{"one": 1}, + instance: "one", + expected: "map[string]int{\"one\":1} should not contain \"one\"", + }, + { + assertion: Contains, + container: nonContainer{Value: "Hello"}, + instance: "Hello", + expected: "assert.nonContainer{Value:\"Hello\"} could not be applied builtin len()\n", + }, + { + assertion: NotContains, + container: nonContainer{Value: "Hello"}, + instance: "Hello", + expected: "assert.nonContainer{Value:\"Hello\"} could not be applied builtin len()\n", + }, + } + for _, c := range cases { + name := filepath.Base(runtime.FuncForPC(reflect.ValueOf(c.assertion).Pointer()).Name()) + t.Run(fmt.Sprintf("%v(%T, %T)", name, c.container, c.instance), func(t *testing.T) { + c.assertion(mockT, c.container, c.instance) + actualFail := mockT.errorString() + if !strings.Contains(actualFail, c.expected) { + t.Errorf("Contains failure should include %q but was %q", c.expected, actualFail) + } + }) + } +} + +func TestContainsNotContainsOnNilValue(t *testing.T) { + t.Parallel() + + mockT := new(mockTestingT) + + Contains(mockT, nil, "key") + expectedFail := " could not be applied builtin len()" + actualFail := mockT.errorString() + if !strings.Contains(actualFail, expectedFail) { + t.Errorf("Contains failure should include %q but was %q", expectedFail, actualFail) + } + + NotContains(mockT, nil, "key") + if !strings.Contains(actualFail, expectedFail) { + t.Errorf("Contains failure should include %q but was %q", expectedFail, actualFail) + } +} + +func TestSubsetNotSubset(t *testing.T) { + t.Parallel() + + cases := []struct { + list interface{} + subset interface{} + result bool + message string + }{ + // cases that are expected to contain + {[]int{1, 2, 3}, nil, true, `nil is the empty set which is a subset of every set`}, + {[]int{1, 2, 3}, []int{}, true, `[] is a subset of ['\x01' '\x02' '\x03']`}, + {[]int{1, 2, 3}, []int{1, 2}, true, `['\x01' '\x02'] is a subset of ['\x01' '\x02' '\x03']`}, + {[]int{1, 2, 3}, []int{1, 2, 3}, true, `['\x01' '\x02' '\x03'] is a subset of ['\x01' '\x02' '\x03']`}, + {[]string{"hello", "world"}, []string{"hello"}, true, `["hello"] is a subset of ["hello" "world"]`}, + {map[string]string{ + "a": "x", + "c": "z", + "b": "y", + }, map[string]string{ + "a": "x", + "b": "y", + }, true, `map["a":"x" "b":"y"] is a subset of map["a":"x" "b":"y" "c":"z"]`}, + {[]string{"a", "b", "c"}, map[string]int{"a": 1, "c": 3}, true, `map["a":'\x01' "c":'\x03'] is a subset of ["a" "b" "c"]`}, + + // cases that are expected not to contain + {[]string{"hello", "world"}, []string{"hello", "testify"}, false, `[]string{"hello", "world"} does not contain "testify"`}, + {[]int{1, 2, 3}, []int{4, 5}, false, `[]int{1, 2, 3} does not contain 4`}, + {[]int{1, 2, 3}, []int{1, 5}, false, `[]int{1, 2, 3} does not contain 5`}, + {map[string]string{ + "a": "x", + "c": "z", + "b": "y", + }, map[string]string{ + "a": "x", + "b": "z", + }, false, `map[string]string{"a":"x", "b":"y", "c":"z"} does not contain map[string]string{"a":"x", "b":"z"}`}, + {map[string]string{ + "a": "x", + "b": "y", + }, map[string]string{ + "a": "x", + "b": "y", + "c": "z", + }, false, `map[string]string{"a":"x", "b":"y"} does not contain map[string]string{"a":"x", "b":"y", "c":"z"}`}, + {[]string{"a", "b", "c"}, map[string]int{"c": 3, "d": 4}, false, `[]string{"a", "b", "c"} does not contain "d"`}, + } + + for _, c := range cases { + t.Run("SubSet: "+c.message, func(t *testing.T) { + mockT := new(mockTestingT) + res := Subset(mockT, c.list, c.subset) + + if res != c.result { + t.Errorf("Subset should return %t: %s", c.result, c.message) + } + if !c.result { + expectedFail := c.message + actualFail := mockT.errorString() + if !strings.Contains(actualFail, expectedFail) { + t.Log(actualFail) + t.Errorf("Subset failure should contain %q but was %q", expectedFail, actualFail) + } + } + }) + } + for _, c := range cases { + t.Run("NotSubSet: "+c.message, func(t *testing.T) { + mockT := new(mockTestingT) + res := NotSubset(mockT, c.list, c.subset) + + // NotSubset should match the inverse of Subset. If it doesn't, something is wrong + if res == Subset(mockT, c.list, c.subset) { + t.Errorf("NotSubset should return %t: %s", !c.result, c.message) + } + if c.result { + expectedFail := c.message + actualFail := mockT.errorString() + if !strings.Contains(actualFail, expectedFail) { + t.Log(actualFail) + t.Errorf("NotSubset failure should contain %q but was %q", expectedFail, actualFail) + } + } + }) + } +} + +func TestNotSubsetNil(t *testing.T) { + t.Parallel() + + mockT := new(testing.T) + NotSubset(mockT, []string{"foo"}, nil) + if !mockT.Failed() { + t.Error("NotSubset on nil set should have failed the test") + } +} + +func Test_containsElement(t *testing.T) { + t.Parallel() + + list1 := []string{"Foo", "Bar"} + list2 := []int{1, 2} + simpleMap := map[interface{}]interface{}{"Foo": "Bar"} + + ok, found := containsElement("Hello World", "World") + True(t, ok) + True(t, found) + + ok, found = containsElement(list1, "Foo") + True(t, ok) + True(t, found) + + ok, found = containsElement(list1, "Bar") + True(t, ok) + True(t, found) + + ok, found = containsElement(list2, 1) + True(t, ok) + True(t, found) + + ok, found = containsElement(list2, 2) + True(t, ok) + True(t, found) + + ok, found = containsElement(list1, "Foo!") + True(t, ok) + False(t, found) + + ok, found = containsElement(list2, 3) + True(t, ok) + False(t, found) + + ok, found = containsElement(list2, "1") + True(t, ok) + False(t, found) + + ok, found = containsElement(simpleMap, "Foo") + True(t, ok) + True(t, found) + + ok, found = containsElement(simpleMap, "Bar") + True(t, ok) + False(t, found) + + ok, found = containsElement(1433, "1") + False(t, ok) + False(t, found) +} + +func TestElementsMatch(t *testing.T) { + t.Parallel() + + mockT := new(testing.T) + + cases := []struct { + expected interface{} + actual interface{} + result bool + }{ + // matching + {nil, nil, true}, + + {nil, nil, true}, + {[]int{}, []int{}, true}, + {[]int{1}, []int{1}, true}, + {[]int{1, 1}, []int{1, 1}, true}, + {[]int{1, 2}, []int{1, 2}, true}, + {[]int{1, 2}, []int{2, 1}, true}, + {[2]int{1, 2}, [2]int{2, 1}, true}, + {[]string{"hello", "world"}, []string{"world", "hello"}, true}, + {[]string{"hello", "hello"}, []string{"hello", "hello"}, true}, + {[]string{"hello", "hello", "world"}, []string{"hello", "world", "hello"}, true}, + {[3]string{"hello", "hello", "world"}, [3]string{"hello", "world", "hello"}, true}, + {[]int{}, nil, true}, + + // not matching + {[]int{1}, []int{1, 1}, false}, + {[]int{1, 2}, []int{2, 2}, false}, + {[]string{"hello", "hello"}, []string{"hello"}, false}, + } + + for _, c := range cases { + t.Run(fmt.Sprintf("ElementsMatch(%#v, %#v)", c.expected, c.actual), func(t *testing.T) { + res := ElementsMatch(mockT, c.actual, c.expected) + + if res != c.result { + t.Errorf("ElementsMatch(%#v, %#v) should return %v", c.actual, c.expected, c.result) + } + }) + } +} + +func TestDiffLists(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + listA interface{} + listB interface{} + extraA []interface{} + extraB []interface{} + }{ + { + name: "equal empty", + listA: []string{}, + listB: []string{}, + extraA: nil, + extraB: nil, + }, + { + name: "equal same order", + listA: []string{"hello", "world"}, + listB: []string{"hello", "world"}, + extraA: nil, + extraB: nil, + }, + { + name: "equal different order", + listA: []string{"hello", "world"}, + listB: []string{"world", "hello"}, + extraA: nil, + extraB: nil, + }, + { + name: "extra A", + listA: []string{"hello", "hello", "world"}, + listB: []string{"hello", "world"}, + extraA: []interface{}{"hello"}, + extraB: nil, + }, + { + name: "extra A twice", + listA: []string{"hello", "hello", "hello", "world"}, + listB: []string{"hello", "world"}, + extraA: []interface{}{"hello", "hello"}, + extraB: nil, + }, + { + name: "extra B", + listA: []string{"hello", "world"}, + listB: []string{"hello", "hello", "world"}, + extraA: nil, + extraB: []interface{}{"hello"}, + }, + { + name: "extra B twice", + listA: []string{"hello", "world"}, + listB: []string{"hello", "hello", "world", "hello"}, + extraA: nil, + extraB: []interface{}{"hello", "hello"}, + }, + { + name: "integers 1", + listA: []int{1, 2, 3, 4, 5}, + listB: []int{5, 4, 3, 2, 1}, + extraA: nil, + extraB: nil, + }, + { + name: "integers 2", + listA: []int{1, 2, 1, 2, 1}, + listB: []int{2, 1, 2, 1, 2}, + extraA: []interface{}{1}, + extraB: []interface{}{2}, + }, + } + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + actualExtraA, actualExtraB := diffLists(test.listA, test.listB) + Equal(t, test.extraA, actualExtraA, "extra A does not match for listA=%v listB=%v", + test.listA, test.listB) + Equal(t, test.extraB, actualExtraB, "extra B does not match for listA=%v listB=%v", + test.listA, test.listB) + }) + } +} + +func TestNotElementsMatch(t *testing.T) { + t.Parallel() + + mockT := new(testing.T) + + cases := []struct { + expected interface{} + actual interface{} + result bool + }{ + // not matching + {[]int{1}, []int{}, true}, + {[]int{}, []int{2}, true}, + {[]int{1}, []int{2}, true}, + {[]int{1}, []int{1, 1}, true}, + {[]int{1, 2}, []int{3, 4}, true}, + {[]int{3, 4}, []int{1, 2}, true}, + {[]int{1, 1, 2, 3}, []int{1, 2, 3}, true}, + {[]string{"hello"}, []string{"world"}, true}, + {[]string{"hello", "hello"}, []string{"world", "world"}, true}, + {[3]string{"hello", "hello", "hello"}, [3]string{"world", "world", "world"}, true}, + + // matching + {nil, nil, false}, + {[]int{}, nil, false}, + {[]int{}, []int{}, false}, + {[]int{1}, []int{1}, false}, + {[]int{1, 1}, []int{1, 1}, false}, + {[]int{1, 2}, []int{2, 1}, false}, + {[2]int{1, 2}, [2]int{2, 1}, false}, + {[]int{1, 1, 2}, []int{1, 2, 1}, false}, + {[]string{"hello", "world"}, []string{"world", "hello"}, false}, + {[]string{"hello", "hello"}, []string{"hello", "hello"}, false}, + {[]string{"hello", "hello", "world"}, []string{"hello", "world", "hello"}, false}, + {[3]string{"hello", "hello", "world"}, [3]string{"hello", "world", "hello"}, false}, + } + + for _, c := range cases { + t.Run(fmt.Sprintf("NotElementsMatch(%#v, %#v)", c.expected, c.actual), func(t *testing.T) { + res := NotElementsMatch(mockT, c.actual, c.expected) + + if res != c.result { + t.Errorf("NotElementsMatch(%#v, %#v) should return %v", c.actual, c.expected, c.result) + } + }) + } +} + +func TestCondition(t *testing.T) { + t.Parallel() + + mockT := new(testing.T) + + if !Condition(mockT, func() bool { return true }, "Truth") { + t.Error("Condition should return true") + } + + if Condition(mockT, func() bool { return false }, "Lie") { + t.Error("Condition should return false") + } +} + +func TestDidPanic(t *testing.T) { + t.Parallel() + + const panicMsg = "Panic!" + + if funcDidPanic, msg, _ := didPanic(func() { + panic(panicMsg) + }); !funcDidPanic || msg != panicMsg { + t.Error("didPanic should return true, panicMsg") + } + + if funcDidPanic, msg, _ := didPanic(func() { + panic(nil) + }); !funcDidPanic || msg != nil { + t.Error("didPanic should return true, nil") + } + + if funcDidPanic, _, _ := didPanic(func() { + }); funcDidPanic { + t.Error("didPanic should return false") + } +} + +func TestPanics(t *testing.T) { + t.Parallel() + + mockT := new(testing.T) + + if !Panics(mockT, func() { + panic("Panic!") + }) { + t.Error("Panics should return true") + } + + if Panics(mockT, func() { + }) { + t.Error("Panics should return false") + } +} + +func TestPanicsWithValue(t *testing.T) { + t.Parallel() + + mockT := new(testing.T) + + if !PanicsWithValue(mockT, "Panic!", func() { + panic("Panic!") + }) { + t.Error("PanicsWithValue should return true") + } + + if !PanicsWithValue(mockT, nil, func() { + panic(nil) + }) { + t.Error("PanicsWithValue should return true") + } + + if PanicsWithValue(mockT, "Panic!", func() { + }) { + t.Error("PanicsWithValue should return false") + } + + if PanicsWithValue(mockT, "at the disco", func() { + panic("Panic!") + }) { + t.Error("PanicsWithValue should return false") + } +} + +func TestPanicsWithError(t *testing.T) { + t.Parallel() + + mockT := new(testing.T) + + if !PanicsWithError(mockT, "panic", func() { + panic(errors.New("panic")) + }) { + t.Error("PanicsWithError should return true") + } + + if PanicsWithError(mockT, "Panic!", func() { + }) { + t.Error("PanicsWithError should return false") + } + + if PanicsWithError(mockT, "at the disco", func() { + panic(errors.New("panic")) + }) { + t.Error("PanicsWithError should return false") + } + + if PanicsWithError(mockT, "Panic!", func() { + panic("panic") + }) { + t.Error("PanicsWithError should return false") + } +} + +func TestNotPanics(t *testing.T) { + t.Parallel() + + mockT := new(testing.T) + + if !NotPanics(mockT, func() { + }) { + t.Error("NotPanics should return true") + } + + if NotPanics(mockT, func() { + panic("Panic!") + }) { + t.Error("NotPanics should return false") + } +} + +func TestNoError(t *testing.T) { + t.Parallel() + + mockT := new(testing.T) + + // start with a nil error + var err error + + True(t, NoError(mockT, err), "NoError should return True for nil arg") + + // now set an error + err = errors.New("some error") + + False(t, NoError(mockT, err), "NoError with error should return False") + + // returning an empty error interface + err = func() error { + var err *customError + return err + }() + + if err == nil { // err is not nil here! + t.Errorf("Error should be nil due to empty interface: %s", err) + } + + False(t, NoError(mockT, err), "NoError should fail with empty error interface") +} + +type customError struct{} + +func (*customError) Error() string { return "fail" } + +func TestError(t *testing.T) { + t.Parallel() + + mockT := new(testing.T) + + // start with a nil error + var err error + + False(t, Error(mockT, err), "Error should return False for nil arg") + + // now set an error + err = errors.New("some error") + + True(t, Error(mockT, err), "Error with error should return True") + + // go vet check + True(t, Errorf(mockT, err, "example with %s", "formatted message"), "Errorf with error should return True") + + // returning an empty error interface + err = func() error { + var err *customError + return err + }() + + if err == nil { // err is not nil here! + t.Errorf("Error should be nil due to empty interface: %s", err) + } + + True(t, Error(mockT, err), "Error should pass with empty error interface") +} + +func TestEqualError(t *testing.T) { + t.Parallel() + + mockT := new(testing.T) + + // start with a nil error + var err error + False(t, EqualError(mockT, err, ""), + "EqualError should return false for nil arg") + + // now set an error + err = errors.New("some error") + False(t, EqualError(mockT, err, "Not some error"), + "EqualError should return false for different error string") + True(t, EqualError(mockT, err, "some error"), + "EqualError should return true") +} + +func TestErrorContains(t *testing.T) { + t.Parallel() + + mockT := new(testing.T) + + // start with a nil error + var err error + False(t, ErrorContains(mockT, err, ""), + "ErrorContains should return false for nil arg") + + // now set an error + err = errors.New("some error: another error") + False(t, ErrorContains(mockT, err, "bad error"), + "ErrorContains should return false for different error string") + True(t, ErrorContains(mockT, err, "some error"), + "ErrorContains should return true") + True(t, ErrorContains(mockT, err, "another error"), + "ErrorContains should return true") +} + +func Test_isEmpty(t *testing.T) { + t.Parallel() + + chWithValue := make(chan struct{}, 1) + chWithValue <- struct{}{} + + True(t, isEmpty("")) + True(t, isEmpty(nil)) + True(t, isEmpty(error(nil))) + True(t, isEmpty((*int)(nil))) + True(t, isEmpty((*string)(nil))) + True(t, isEmpty(new(string))) + True(t, isEmpty([]string{})) + True(t, isEmpty([]string(nil))) + True(t, isEmpty([]byte(nil))) + True(t, isEmpty([]byte{})) + True(t, isEmpty([]byte(""))) + True(t, isEmpty([]bool(nil))) + True(t, isEmpty([]bool{})) + True(t, isEmpty([]interface{}(nil))) + True(t, isEmpty([]interface{}{})) + True(t, isEmpty(struct{}{})) + True(t, isEmpty(&struct{}{})) + True(t, isEmpty(struct{ A int }{A: 0})) + True(t, isEmpty(struct{ a int }{a: 0})) + True(t, isEmpty(struct { + a int + B int + }{a: 0, B: 0})) + True(t, isEmpty(0)) + True(t, isEmpty(int(0))) + True(t, isEmpty(int8(0))) + True(t, isEmpty(int16(0))) + True(t, isEmpty(uint16(0))) + True(t, isEmpty(int32(0))) + True(t, isEmpty(uint32(0))) + True(t, isEmpty(int64(0))) + True(t, isEmpty(uint64(0))) + True(t, isEmpty('\u0000')) // rune => int32 + True(t, isEmpty(float32(0))) + True(t, isEmpty(float64(0))) + True(t, isEmpty(0i)) // complex + True(t, isEmpty(0.0i)) // complex + True(t, isEmpty(false)) + True(t, isEmpty(new(bool))) + True(t, isEmpty(map[string]string{})) + True(t, isEmpty(map[string]string(nil))) + True(t, isEmpty(new(time.Time))) + True(t, isEmpty(time.Time{})) + True(t, isEmpty(make(chan struct{}))) + True(t, isEmpty(chan struct{}(nil))) + True(t, isEmpty(chan<- struct{}(nil))) + True(t, isEmpty(make(chan struct{}))) + True(t, isEmpty(make(chan<- struct{}))) + True(t, isEmpty(make(chan struct{}, 1))) + True(t, isEmpty(make(chan<- struct{}, 1))) + True(t, isEmpty([1]int{0})) + True(t, isEmpty([2]int{0, 0})) + True(t, isEmpty([8]int{})) + True(t, isEmpty([...]int{7: 0})) + True(t, isEmpty([...]bool{false, false})) + True(t, isEmpty(errors.New(""))) // BEWARE + True(t, isEmpty([]error{})) + True(t, isEmpty([]error(nil))) + True(t, isEmpty(&[1]int{0})) + True(t, isEmpty(&[2]int{0, 0})) + False(t, isEmpty("something")) + False(t, isEmpty(errors.New("something"))) + False(t, isEmpty([]string{"something"})) + False(t, isEmpty(1)) + False(t, isEmpty(int(1))) + False(t, isEmpty(uint(1))) + False(t, isEmpty(byte(1))) + False(t, isEmpty(int8(1))) + False(t, isEmpty(uint8(1))) + False(t, isEmpty(int16(1))) + False(t, isEmpty(uint16(1))) + False(t, isEmpty(int32(1))) + False(t, isEmpty(uint32(1))) + False(t, isEmpty(int64(1))) + False(t, isEmpty(uint64(1))) + False(t, isEmpty('A')) // rune => int32 + False(t, isEmpty(true)) + False(t, isEmpty(1.0)) + False(t, isEmpty(1i)) // complex + False(t, isEmpty([]byte{0})) // elements values are ignored for slices + False(t, isEmpty([]byte{0, 0})) // elements values are ignored for slices + False(t, isEmpty([]string{""})) // elements values are ignored for slices + False(t, isEmpty([]string{"a"})) // elements values are ignored for slices + False(t, isEmpty([]bool{false})) // elements values are ignored for slices + False(t, isEmpty([]bool{true})) // elements values are ignored for slices + False(t, isEmpty([]error{errors.New("xxx")})) + False(t, isEmpty([]error{nil})) // BEWARE + False(t, isEmpty([]error{errors.New("")})) // BEWARE + False(t, isEmpty(map[string]string{"Hello": "World"})) + False(t, isEmpty(map[string]string{"": ""})) + False(t, isEmpty(map[string]string{"foo": ""})) + False(t, isEmpty(map[string]string{"": "foo"})) + False(t, isEmpty(chWithValue)) + False(t, isEmpty([1]bool{true})) + False(t, isEmpty([2]bool{false, true})) + False(t, isEmpty([...]bool{10: true})) + False(t, isEmpty([]int{0})) + False(t, isEmpty([]int{42})) + False(t, isEmpty([1]int{42})) + False(t, isEmpty([2]int{0, 42})) + False(t, isEmpty(&[1]int{42})) + False(t, isEmpty(&[2]int{0, 42})) + False(t, isEmpty([1]*int{new(int)})) // array elements must be the zero value, not any Empty value + False(t, isEmpty(struct{ A int }{A: 42})) + False(t, isEmpty(struct{ a int }{a: 42})) + False(t, isEmpty(struct{ a *int }{a: new(int)})) // fields must be the zero value, not any Empty value + False(t, isEmpty(struct{ a []int }{a: []int{}})) // fields must be the zero value, not any Empty value + False(t, isEmpty(struct { + a int + B int + }{a: 0, B: 42})) + False(t, isEmpty(struct { + a int + B int + }{a: 42, B: 0})) +} + +func Benchmark_isEmpty(b *testing.B) { + b.ReportAllocs() + + v := new(int) + b.ResetTimer() + + for i := 0; i < b.N; i++ { + isEmpty("") + isEmpty(42) + isEmpty(v) + } +} + +func TestEmpty(t *testing.T) { + t.Parallel() + + mockT := new(testing.T) + chWithValue := make(chan struct{}, 1) + chWithValue <- struct{}{} + var tiP *time.Time + var tiNP time.Time + var s *string + var f *os.File + sP := &s + x := 1 + xP := &x + + type TString string + type TStruct struct { + x int + } + + True(t, Empty(mockT, ""), "Empty string is empty") + True(t, Empty(mockT, nil), "Nil is empty") + True(t, Empty(mockT, []string{}), "Empty string array is empty") + True(t, Empty(mockT, 0), "Zero int value is empty") + True(t, Empty(mockT, false), "False value is empty") + True(t, Empty(mockT, make(chan struct{})), "Channel without values is empty") + True(t, Empty(mockT, s), "Nil string pointer is empty") + True(t, Empty(mockT, f), "Nil os.File pointer is empty") + True(t, Empty(mockT, tiP), "Nil time.Time pointer is empty") + True(t, Empty(mockT, tiNP), "time.Time is empty") + True(t, Empty(mockT, TStruct{}), "struct with zero values is empty") + True(t, Empty(mockT, TString("")), "empty aliased string is empty") + True(t, Empty(mockT, sP), "ptr to nil value is empty") + True(t, Empty(mockT, [1]int{}), "array is state") + + False(t, Empty(mockT, "something"), "Non Empty string is not empty") + False(t, Empty(mockT, errors.New("something")), "Non nil object is not empty") + False(t, Empty(mockT, []string{"something"}), "Non empty string array is not empty") + False(t, Empty(mockT, 1), "Non-zero int value is not empty") + False(t, Empty(mockT, true), "True value is not empty") + False(t, Empty(mockT, chWithValue), "Channel with values is not empty") + False(t, Empty(mockT, TStruct{x: 1}), "struct with initialized values is empty") + False(t, Empty(mockT, TString("abc")), "non-empty aliased string is empty") + False(t, Empty(mockT, xP), "ptr to non-nil value is not empty") + False(t, Empty(mockT, [1]int{42}), "array is not state") + + // error messages validation + tests := []struct { + name string + value interface{} + expectedResult bool + expectedErrMsg string + }{ + { + name: "Non Empty string is not empty", + value: "something", + expectedResult: false, + expectedErrMsg: "Should be empty, but was something\n", + }, + { + name: "Non nil object is not empty", + value: errors.New("something"), + expectedResult: false, + expectedErrMsg: "Should be empty, but was something\n", + }, + { + name: "Non empty string array is not empty", + value: []string{"something"}, + expectedResult: false, + expectedErrMsg: "Should be empty, but was [something]\n", + }, + { + name: "Non-zero int value is not empty", + value: 1, + expectedResult: false, + expectedErrMsg: "Should be empty, but was 1\n", + }, + { + name: "True value is not empty", + value: true, + expectedResult: false, + expectedErrMsg: "Should be empty, but was true\n", + }, + { + name: "Channel with values is not empty", + value: chWithValue, + expectedResult: false, + expectedErrMsg: fmt.Sprintf("Should be empty, but was %v\n", chWithValue), + }, + { + name: "struct with initialized values is empty", + value: TStruct{x: 1}, + expectedResult: false, + expectedErrMsg: "Should be empty, but was {1}\n", + }, + { + name: "non-empty aliased string is empty", + value: TString("abc"), + expectedResult: false, + expectedErrMsg: "Should be empty, but was abc\n", + }, + { + name: "ptr to non-nil value is not empty", + value: xP, + expectedResult: false, + expectedErrMsg: fmt.Sprintf("Should be empty, but was %p\n", xP), + }, + { + name: "array is not state", + value: [1]int{42}, + expectedResult: false, + expectedErrMsg: "Should be empty, but was [42]\n", + }, + + // Here are some edge cases + { + name: "string with only spaces is not empty", + value: " ", + expectedResult: false, + expectedErrMsg: "Should be empty, but was \n", // TODO FIX THIS strange error message + }, + { + name: "string with a line feed is not empty", + value: "\n", + expectedResult: false, + // TODO This is the exact same error message as for an empty string + expectedErrMsg: "Should be empty, but was \n", // TODO FIX THIS strange error message + }, + { + name: "string with only tabulation and lines feed is not empty", + value: "\n\t\n", + expectedResult: false, + // TODO The line feeds and tab are not helping to spot what is expected + expectedErrMsg: "" + // this syntax is used to show how errors are reported. + "Should be empty, but was \n" + + "\t\n", + }, + { + name: "string with trailing lines feed is not empty", + value: "foo\n\n", + expectedResult: false, + // TODO it's not clear if one or two lines feed are expected + expectedErrMsg: "Should be empty, but was foo\n\n", + }, + { + name: "string with leading and trailing tabulation and lines feed is not empty", + value: "\n\nfoo\t\n\t\n", + expectedResult: false, + // TODO The line feeds and tab are not helping to figure what is expected + expectedErrMsg: "" + + "Should be empty, but was \n" + + "\n" + + "foo\t\n" + + "\t\n", + }, + + { + name: "non-printable character is not empty", + value: "\u00a0", // NO-BREAK SPACE UNICODE CHARACTER + expectedResult: false, + // TODO here you cannot figure out what is expected + expectedErrMsg: "Should be empty, but was \u00a0\n", + }, + + // Here we are testing there is no error message on success + { + name: "Empty string is empty", + value: "", + expectedResult: true, + expectedErrMsg: "", + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + mockCT := new(captureTestingT) + res := Empty(mockCT, tt.value) + mockCT.checkResultAndErrMsg(t, res, tt.expectedResult, tt.expectedErrMsg) + }) + } +} + +func TestNotEmpty(t *testing.T) { + t.Parallel() + + mockT := new(testing.T) + chWithValue := make(chan struct{}, 1) + chWithValue <- struct{}{} + + False(t, NotEmpty(mockT, ""), "Empty string is empty") + False(t, NotEmpty(mockT, nil), "Nil is empty") + False(t, NotEmpty(mockT, []string{}), "Empty string array is empty") + False(t, NotEmpty(mockT, 0), "Zero int value is empty") + False(t, NotEmpty(mockT, false), "False value is empty") + False(t, NotEmpty(mockT, make(chan struct{})), "Channel without values is empty") + False(t, NotEmpty(mockT, [1]int{}), "array is state") + + True(t, NotEmpty(mockT, "something"), "Non Empty string is not empty") + True(t, NotEmpty(mockT, errors.New("something")), "Non nil object is not empty") + True(t, NotEmpty(mockT, []string{"something"}), "Non empty string array is not empty") + True(t, NotEmpty(mockT, 1), "Non-zero int value is not empty") + True(t, NotEmpty(mockT, true), "True value is not empty") + True(t, NotEmpty(mockT, chWithValue), "Channel with values is not empty") + True(t, NotEmpty(mockT, [1]int{42}), "array is not state") + + // error messages validation + tests := []struct { + name string + value interface{} + expectedResult bool + expectedErrMsg string + }{ + { + name: "Empty string is empty", + value: "", + expectedResult: false, + expectedErrMsg: `Should NOT be empty, but was ` + "\n", // TODO FIX THIS strange error message + }, + { + name: "Nil is empty", + value: nil, + expectedResult: false, + expectedErrMsg: "Should NOT be empty, but was \n", + }, + { + name: "Empty string array is empty", + value: []string{}, + expectedResult: false, + expectedErrMsg: "Should NOT be empty, but was []\n", + }, + { + name: "Zero int value is empty", + value: 0, + expectedResult: false, + expectedErrMsg: "Should NOT be empty, but was 0\n", + }, + { + name: "False value is empty", + value: false, + expectedResult: false, + expectedErrMsg: "Should NOT be empty, but was false\n", + }, + { + name: "array is state", + value: [1]int{}, + expectedResult: false, + expectedErrMsg: "Should NOT be empty, but was [0]\n", + }, + + // Here we are testing there is no error message on success + { + name: "Non Empty string is not empty", + value: "something", + expectedResult: true, + expectedErrMsg: "", + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + mockCT := new(captureTestingT) + res := NotEmpty(mockCT, tt.value) + mockCT.checkResultAndErrMsg(t, tt.expectedResult, res, tt.expectedErrMsg) + }) + } +} + +func Test_getLen(t *testing.T) { + t.Parallel() + + falseCases := []interface{}{ + nil, + 0, + true, + false, + 'A', + struct{}{}, + } + for _, v := range falseCases { + l, ok := getLen(v) + False(t, ok, "Expected getLen fail to get length of %#v", v) + Equal(t, 0, l, "getLen should return 0 for %#v", v) + } + + ch := make(chan int, 5) + ch <- 1 + ch <- 2 + ch <- 3 + trueCases := []struct { + v interface{} + l int + }{ + {[]int{1, 2, 3}, 3}, + {[...]int{1, 2, 3}, 3}, + {"ABC", 3}, + {map[int]int{1: 2, 2: 4, 3: 6}, 3}, + {ch, 3}, + + {[]int{}, 0}, + {map[int]int{}, 0}, + {make(chan int), 0}, + + {[]int(nil), 0}, + {map[int]int(nil), 0}, + {(chan int)(nil), 0}, + } + + for _, c := range trueCases { + l, ok := getLen(c.v) + True(t, ok, "Expected getLen success to get length of %#v", c.v) + Equal(t, c.l, l) + } +} + +func TestLen(t *testing.T) { + t.Parallel() + + mockT := new(testing.T) + + False(t, Len(mockT, nil, 0), "nil does not have length") + False(t, Len(mockT, 0, 0), "int does not have length") + False(t, Len(mockT, true, 0), "true does not have length") + False(t, Len(mockT, false, 0), "false does not have length") + False(t, Len(mockT, 'A', 0), "Rune does not have length") + False(t, Len(mockT, struct{}{}, 0), "Struct does not have length") + + ch := make(chan int, 5) + ch <- 1 + ch <- 2 + ch <- 3 + + cases := []struct { + v interface{} + l int + expected1234567 string // message when expecting 1234567 items + }{ + {[]int{1, 2, 3}, 3, `"[1 2 3]" should have 1234567 item(s), but has 3`}, + {[...]int{1, 2, 3}, 3, `"[1 2 3]" should have 1234567 item(s), but has 3`}, + {"ABC", 3, `"ABC" should have 1234567 item(s), but has 3`}, + {map[int]int{1: 2, 2: 4, 3: 6}, 3, `"map[1:2 2:4 3:6]" should have 1234567 item(s), but has 3`}, + {ch, 3, ""}, + + {[]int{}, 0, `"[]" should have 1234567 item(s), but has 0`}, + {map[int]int{}, 0, `"map[]" should have 1234567 item(s), but has 0`}, + {make(chan int), 0, ""}, + + {[]int(nil), 0, `"[]" should have 1234567 item(s), but has 0`}, + {map[int]int(nil), 0, `"map[]" should have 1234567 item(s), but has 0`}, + {(chan int)(nil), 0, `"" should have 1234567 item(s), but has 0`}, + } + + for _, c := range cases { + True(t, Len(mockT, c.v, c.l), "%#v have %d items", c.v, c.l) + False(t, Len(mockT, c.v, c.l+1), "%#v have %d items", c.v, c.l) + if c.expected1234567 != "" { + msgMock := new(mockTestingT) + Len(msgMock, c.v, 1234567) + Contains(t, msgMock.errorString(), c.expected1234567) + } + } +} + +func TestWithinDuration(t *testing.T) { + t.Parallel() + + mockT := new(testing.T) + a := time.Now() + b := a.Add(10 * time.Second) + + True(t, WithinDuration(mockT, a, b, 10*time.Second), "A 10s difference is within a 10s time difference") + True(t, WithinDuration(mockT, b, a, 10*time.Second), "A 10s difference is within a 10s time difference") + + False(t, WithinDuration(mockT, a, b, 9*time.Second), "A 10s difference is not within a 9s time difference") + False(t, WithinDuration(mockT, b, a, 9*time.Second), "A 10s difference is not within a 9s time difference") + + False(t, WithinDuration(mockT, a, b, -9*time.Second), "A 10s difference is not within a 9s time difference") + False(t, WithinDuration(mockT, b, a, -9*time.Second), "A 10s difference is not within a 9s time difference") + + False(t, WithinDuration(mockT, a, b, -11*time.Second), "A 10s difference is not within a 9s time difference") + False(t, WithinDuration(mockT, b, a, -11*time.Second), "A 10s difference is not within a 9s time difference") +} + +func TestWithinRange(t *testing.T) { + t.Parallel() + + mockT := new(testing.T) + n := time.Now() + s := n.Add(-time.Second) + e := n.Add(time.Second) + + True(t, WithinRange(mockT, n, n, n), "Exact same actual, start, and end values return true") + + True(t, WithinRange(mockT, n, s, e), "Time in range is within the time range") + True(t, WithinRange(mockT, s, s, e), "The start time is within the time range") + True(t, WithinRange(mockT, e, s, e), "The end time is within the time range") + + False(t, WithinRange(mockT, s.Add(-time.Nanosecond), s, e, "Just before the start time is not within the time range")) + False(t, WithinRange(mockT, e.Add(time.Nanosecond), s, e, "Just after the end time is not within the time range")) + + False(t, WithinRange(mockT, n, e, s, "Just after the end time is not within the time range")) +} + +func TestInDelta(t *testing.T) { + t.Parallel() + + mockT := new(testing.T) + + True(t, InDelta(mockT, 1.001, 1, 0.01), "|1.001 - 1| <= 0.01") + True(t, InDelta(mockT, 1, 1.001, 0.01), "|1 - 1.001| <= 0.01") + True(t, InDelta(mockT, 1, 2, 1), "|1 - 2| <= 1") + False(t, InDelta(mockT, 1, 2, 0.5), "Expected |1 - 2| <= 0.5 to fail") + False(t, InDelta(mockT, 2, 1, 0.5), "Expected |2 - 1| <= 0.5 to fail") + False(t, InDelta(mockT, "", nil, 1), "Expected non numerals to fail") + False(t, InDelta(mockT, 42, math.NaN(), 0.01), "Expected NaN for actual to fail") + False(t, InDelta(mockT, math.NaN(), 42, 0.01), "Expected NaN for expected to fail") + True(t, InDelta(mockT, math.NaN(), math.NaN(), 0.01), "Expected NaN for both to pass") + + cases := []struct { + a, b interface{} + delta float64 + }{ + {uint(2), uint(1), 1}, + {uint8(2), uint8(1), 1}, + {uint16(2), uint16(1), 1}, + {uint32(2), uint32(1), 1}, + {uint64(2), uint64(1), 1}, + + {int(2), int(1), 1}, + {int8(2), int8(1), 1}, + {int16(2), int16(1), 1}, + {int32(2), int32(1), 1}, + {int64(2), int64(1), 1}, + + {float32(2), float32(1), 1}, + {float64(2), float64(1), 1}, + } + + for _, tc := range cases { + True(t, InDelta(mockT, tc.a, tc.b, tc.delta), "Expected |%V - %V| <= %v", tc.a, tc.b, tc.delta) + } +} + +func TestInDeltaSlice(t *testing.T) { + t.Parallel() + + mockT := new(testing.T) + + True(t, InDeltaSlice(mockT, + []float64{1.001, math.NaN(), 0.999}, + []float64{1, math.NaN(), 1}, + 0.1), "{1.001, NaN, 0.009} is element-wise close to {1, NaN, 1} in delta=0.1") + + True(t, InDeltaSlice(mockT, + []float64{1, math.NaN(), 2}, + []float64{0, math.NaN(), 3}, + 1), "{1, NaN, 2} is element-wise close to {0, NaN, 3} in delta=1") + + False(t, InDeltaSlice(mockT, + []float64{1, math.NaN(), 2}, + []float64{0, math.NaN(), 3}, + 0.1), "{1, NaN, 2} is not element-wise close to {0, NaN, 3} in delta=0.1") + + False(t, InDeltaSlice(mockT, "", nil, 1), "Expected non numeral slices to fail") +} + +func TestInDeltaMapValues(t *testing.T) { + t.Parallel() + + mockT := new(testing.T) + + for _, tc := range []struct { + title string + expect interface{} + actual interface{} + f func(TestingT, bool, ...interface{}) bool + delta float64 + }{ + { + title: "Within delta", + expect: map[string]float64{ + "foo": 1.0, + "bar": 2.0, + "baz": math.NaN(), + }, + actual: map[string]float64{ + "foo": 1.01, + "bar": 1.99, + "baz": math.NaN(), + }, + delta: 0.1, + f: True, + }, + { + title: "Within delta", + expect: map[int]float64{ + 1: 1.0, + 2: 2.0, + }, + actual: map[int]float64{ + 1: 1.0, + 2: 1.99, + }, + delta: 0.1, + f: True, + }, + { + title: "Different number of keys", + expect: map[int]float64{ + 1: 1.0, + 2: 2.0, + }, + actual: map[int]float64{ + 1: 1.0, + }, + delta: 0.1, + f: False, + }, + { + title: "Within delta with zero value", + expect: map[string]float64{ + "zero": 0, + }, + actual: map[string]float64{ + "zero": 0, + }, + delta: 0.1, + f: True, + }, + { + title: "With missing key with zero value", + expect: map[string]float64{ + "zero": 0, + "foo": 0, + }, + actual: map[string]float64{ + "zero": 0, + "bar": 0, + }, + f: False, + }, + } { + tc.f(t, InDeltaMapValues(mockT, tc.expect, tc.actual, tc.delta), tc.title+"\n"+diff(tc.expect, tc.actual)) + } +} + +func TestInEpsilon(t *testing.T) { + t.Parallel() + + mockT := new(testing.T) + + cases := []struct { + a, b interface{} + epsilon float64 + }{ + {uint8(2), uint16(2), .001}, + {2.1, 2.2, 0.1}, + {2.2, 2.1, 0.1}, + {-2.1, -2.2, 0.1}, + {-2.2, -2.1, 0.1}, + {uint64(100), uint8(101), 0.01}, + {0.1, -0.1, 2}, + {0.1, 0, 2}, + {math.NaN(), math.NaN(), 1}, + {time.Second, time.Second + time.Millisecond, 0.002}, + } + + for _, tc := range cases { + True(t, InEpsilon(t, tc.a, tc.b, tc.epsilon, "Expected %V and %V to have a relative difference of %v", tc.a, tc.b, tc.epsilon), "test: %q", tc) + } + + cases = []struct { + a, b interface{} + epsilon float64 + }{ + {uint8(2), int16(-2), .001}, + {uint64(100), uint8(102), 0.01}, + {2.1, 2.2, 0.001}, + {2.2, 2.1, 0.001}, + {2.1, -2.2, 1}, + {2.1, "bla-bla", 0}, + {0.1, -0.1, 1.99}, + {0, 0.1, 2}, // expected must be different to zero + {time.Second, time.Second + 10*time.Millisecond, 0.002}, + {math.NaN(), 0, 1}, + {0, math.NaN(), 1}, + {0, 0, math.NaN()}, + {math.Inf(1), 1, 1}, + {math.Inf(-1), 1, 1}, + {1, math.Inf(1), 1}, + {1, math.Inf(-1), 1}, + {math.Inf(1), math.Inf(1), 1}, + {math.Inf(1), math.Inf(-1), 1}, + {math.Inf(-1), math.Inf(1), 1}, + {math.Inf(-1), math.Inf(-1), 1}, + } + + for _, tc := range cases { + False(t, InEpsilon(mockT, tc.a, tc.b, tc.epsilon, "Expected %V and %V to have a relative difference of %v", tc.a, tc.b, tc.epsilon)) + } +} + +func TestInEpsilonSlice(t *testing.T) { + t.Parallel() + + mockT := new(testing.T) + + True(t, InEpsilonSlice(mockT, + []float64{2.2, math.NaN(), 2.0}, + []float64{2.1, math.NaN(), 2.1}, + 0.06), "{2.2, NaN, 2.0} is element-wise close to {2.1, NaN, 2.1} in epsilon=0.06") + + False(t, InEpsilonSlice(mockT, + []float64{2.2, 2.0}, + []float64{2.1, 2.1}, + 0.04), "{2.2, 2.0} is not element-wise close to {2.1, 2.1} in epsilon=0.04") + + False(t, InEpsilonSlice(mockT, "", nil, 1), "Expected non numeral slices to fail") +} + +func TestRegexp(t *testing.T) { + t.Parallel() + + mockT := new(testing.T) + + cases := []struct { + rx, str string + }{ + {"^start", "start of the line"}, + {"end$", "in the end"}, + {"end$", "in the end"}, + {"[0-9]{3}[.-]?[0-9]{2}[.-]?[0-9]{2}", "My phone number is 650.12.34"}, + } + + for _, tc := range cases { + True(t, Regexp(mockT, tc.rx, tc.str)) + True(t, Regexp(mockT, regexp.MustCompile(tc.rx), tc.str)) + True(t, Regexp(mockT, regexp.MustCompile(tc.rx), []byte(tc.str))) + False(t, NotRegexp(mockT, tc.rx, tc.str)) + False(t, NotRegexp(mockT, tc.rx, []byte(tc.str))) + False(t, NotRegexp(mockT, regexp.MustCompile(tc.rx), tc.str)) + } + + cases = []struct { + rx, str string + }{ + {"^asdfastart", "Not the start of the line"}, + {"end$", "in the end."}, + {"[0-9]{3}[.-]?[0-9]{2}[.-]?[0-9]{2}", "My phone number is 650.12a.34"}, + } + + for _, tc := range cases { + False(t, Regexp(mockT, tc.rx, tc.str), "Expected %q to not match %q", tc.rx, tc.str) + False(t, Regexp(mockT, regexp.MustCompile(tc.rx), tc.str)) + False(t, Regexp(mockT, regexp.MustCompile(tc.rx), []byte(tc.str))) + True(t, NotRegexp(mockT, tc.rx, tc.str)) + True(t, NotRegexp(mockT, tc.rx, []byte(tc.str))) + True(t, NotRegexp(mockT, regexp.MustCompile(tc.rx), tc.str)) + } +} + +func testAutogeneratedFunction() { + defer func() { + if err := recover(); err == nil { + panic("did not panic") + } + CallerInfo() + }() + t := struct { + io.Closer + }{} + c := t + c.Close() +} + +func TestCallerInfoWithAutogeneratedFunctions(t *testing.T) { + t.Parallel() + + NotPanics(t, func() { + testAutogeneratedFunction() + }) +} + +func TestZero(t *testing.T) { + t.Parallel() + + mockT := new(testing.T) + + for _, test := range zeros { + True(t, Zero(mockT, test, "%#v is not the %T zero value", test, test)) + } + + for _, test := range nonZeros { + False(t, Zero(mockT, test, "%#v is not the %T zero value", test, test)) + } +} + +func TestNotZero(t *testing.T) { + t.Parallel() + + mockT := new(testing.T) + + for _, test := range zeros { + False(t, NotZero(mockT, test, "%#v is not the %T zero value", test, test)) + } + + for _, test := range nonZeros { + True(t, NotZero(mockT, test, "%#v is not the %T zero value", test, test)) + } +} + +func TestFileExists(t *testing.T) { + t.Parallel() + + mockT := new(testing.T) + True(t, FileExists(mockT, "assertions.go")) + + mockT = new(testing.T) + False(t, FileExists(mockT, "random_file")) + + mockT = new(testing.T) + False(t, FileExists(mockT, "../_codegen")) + + link := getTempSymlinkPath(t, "assertions.go") + mockT = new(testing.T) + True(t, FileExists(mockT, link)) + + link = getTempSymlinkPath(t, "non_existent_file") + mockT = new(testing.T) + True(t, FileExists(mockT, link)) +} + +func TestNoFileExists(t *testing.T) { + t.Parallel() + + mockT := new(testing.T) + False(t, NoFileExists(mockT, "assertions.go")) + + mockT = new(testing.T) + True(t, NoFileExists(mockT, "non_existent_file")) + + mockT = new(testing.T) + True(t, NoFileExists(mockT, "../_codegen")) + + link := getTempSymlinkPath(t, "assertions.go") + mockT = new(testing.T) + False(t, NoFileExists(mockT, link)) + + link = getTempSymlinkPath(t, "non_existent_file") + mockT = new(testing.T) + False(t, NoFileExists(mockT, link)) +} + +func getTempSymlinkPath(t *testing.T, file string) string { + t.Helper() + + tempDir := t.TempDir() + link := filepath.Join(tempDir, file+"_symlink") + if err := os.Symlink(file, link); err != nil { + t.Fatalf("could not create temp symlink %q pointing to %q: %v", link, file, err) + } + return link +} + +func TestDirExists(t *testing.T) { + t.Parallel() + + mockT := new(testing.T) + False(t, DirExists(mockT, "assertions.go")) + + mockT = new(testing.T) + False(t, DirExists(mockT, "non_existent_dir")) + + mockT = new(testing.T) + True(t, DirExists(mockT, "../_codegen")) + + link := getTempSymlinkPath(t, "assertions.go") + mockT = new(testing.T) + False(t, DirExists(mockT, link)) + + link = getTempSymlinkPath(t, "non_existent_dir") + mockT = new(testing.T) + False(t, DirExists(mockT, link)) +} + +func TestNoDirExists(t *testing.T) { + t.Parallel() + + mockT := new(testing.T) + True(t, NoDirExists(mockT, "assertions.go")) + + mockT = new(testing.T) + True(t, NoDirExists(mockT, "non_existent_dir")) + + mockT = new(testing.T) + False(t, NoDirExists(mockT, "../_codegen")) + + link := getTempSymlinkPath(t, "assertions.go") + mockT = new(testing.T) + True(t, NoDirExists(mockT, link)) + + link = getTempSymlinkPath(t, "non_existent_dir") + mockT = new(testing.T) + True(t, NoDirExists(mockT, link)) +} + +func TestJSONEq_EqualSONString(t *testing.T) { + t.Parallel() + + mockT := new(testing.T) + True(t, JSONEq(mockT, `{"hello": "world", "foo": "bar"}`, `{"hello": "world", "foo": "bar"}`)) +} + +func TestJSONEq_EquivalentButNotEqual(t *testing.T) { + t.Parallel() + + mockT := new(testing.T) + True(t, JSONEq(mockT, `{"hello": "world", "foo": "bar"}`, `{"foo": "bar", "hello": "world"}`)) +} + +func TestJSONEq_HashOfArraysAndHashes(t *testing.T) { + t.Parallel() + + mockT := new(testing.T) + True(t, 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}")) +} + +func TestJSONEq_Array(t *testing.T) { + t.Parallel() + + mockT := new(testing.T) + True(t, JSONEq(mockT, `["foo", {"hello": "world", "nested": "hash"}]`, `["foo", {"nested": "hash", "hello": "world"}]`)) +} + +func TestJSONEq_HashAndArrayNotEquivalent(t *testing.T) { + t.Parallel() + + mockT := new(testing.T) + False(t, JSONEq(mockT, `["foo", {"hello": "world", "nested": "hash"}]`, `{"foo": "bar", {"nested": "hash", "hello": "world"}}`)) +} + +func TestJSONEq_HashesNotEquivalent(t *testing.T) { + t.Parallel() + + mockT := new(testing.T) + False(t, JSONEq(mockT, `{"foo": "bar"}`, `{"foo": "bar", "hello": "world"}`)) +} + +func TestJSONEq_ActualIsNotJSON(t *testing.T) { + t.Parallel() + + mockT := new(testing.T) + False(t, JSONEq(mockT, `{"foo": "bar"}`, "Not JSON")) +} + +func TestJSONEq_ExpectedIsNotJSON(t *testing.T) { + t.Parallel() + + mockT := new(testing.T) + False(t, JSONEq(mockT, "Not JSON", `{"foo": "bar", "hello": "world"}`)) +} + +func TestJSONEq_ExpectedAndActualNotJSON(t *testing.T) { + t.Parallel() + + mockT := new(testing.T) + False(t, JSONEq(mockT, "Not JSON", "Not JSON")) +} + +func TestJSONEq_ArraysOfDifferentOrder(t *testing.T) { + t.Parallel() + + mockT := new(testing.T) + False(t, JSONEq(mockT, `["foo", {"hello": "world", "nested": "hash"}]`, `[{ "hello": "world", "nested": "hash"}, "foo"]`)) +} + +func TestYAMLEq_EqualYAMLString(t *testing.T) { + t.Parallel() + + mockT := new(testing.T) + True(t, YAMLEq(mockT, `{"hello": "world", "foo": "bar"}`, `{"hello": "world", "foo": "bar"}`)) +} + +func TestYAMLEq_EquivalentButNotEqual(t *testing.T) { + t.Parallel() + + mockT := new(testing.T) + True(t, YAMLEq(mockT, `{"hello": "world", "foo": "bar"}`, `{"foo": "bar", "hello": "world"}`)) +} + +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] +` + True(t, YAMLEq(mockT, expected, actual)) +} + +func TestYAMLEq_Array(t *testing.T) { + t.Parallel() + + mockT := new(testing.T) + True(t, YAMLEq(mockT, `["foo", {"hello": "world", "nested": "hash"}]`, `["foo", {"nested": "hash", "hello": "world"}]`)) +} + +func TestYAMLEq_HashAndArrayNotEquivalent(t *testing.T) { + t.Parallel() + + mockT := new(testing.T) + False(t, YAMLEq(mockT, `["foo", {"hello": "world", "nested": "hash"}]`, `{"foo": "bar", {"nested": "hash", "hello": "world"}}`)) +} + +func TestYAMLEq_HashesNotEquivalent(t *testing.T) { + t.Parallel() + + mockT := new(testing.T) + False(t, YAMLEq(mockT, `{"foo": "bar"}`, `{"foo": "bar", "hello": "world"}`)) +} + +func TestYAMLEq_ActualIsSimpleString(t *testing.T) { + t.Parallel() + + mockT := new(testing.T) + False(t, YAMLEq(mockT, `{"foo": "bar"}`, "Simple String")) +} + +func TestYAMLEq_ExpectedIsSimpleString(t *testing.T) { + t.Parallel() + + mockT := new(testing.T) + False(t, YAMLEq(mockT, "Simple String", `{"foo": "bar", "hello": "world"}`)) +} + +func TestYAMLEq_ExpectedAndActualSimpleString(t *testing.T) { + t.Parallel() + + mockT := new(testing.T) + True(t, YAMLEq(mockT, "Simple String", "Simple String")) +} + +func TestYAMLEq_ArraysOfDifferentOrder(t *testing.T) { + t.Parallel() + + mockT := new(testing.T) + False(t, YAMLEq(mockT, `["foo", {"hello": "world", "nested": "hash"}]`, `[{ "hello": "world", "nested": "hash"}, "foo"]`)) +} + +type diffTestingStruct struct { + A string + B int +} + +func (d *diffTestingStruct) String() string { + return d.A +} + +func TestDiff(t *testing.T) { + t.Parallel() + + expected := ` + +Diff: +--- Expected ++++ Actual +@@ -1,3 +1,3 @@ + (struct { foo string }) { +- foo: (string) (len=5) "hello" ++ foo: (string) (len=3) "bar" + } +` + actual := diff( + struct{ foo string }{"hello"}, + struct{ foo string }{"bar"}, + ) + Equal(t, expected, actual) + + expected = ` + +Diff: +--- Expected ++++ Actual +@@ -2,5 +2,5 @@ + (int) 1, +- (int) 2, + (int) 3, +- (int) 4 ++ (int) 5, ++ (int) 7 + } +` + actual = diff( + []int{1, 2, 3, 4}, + []int{1, 3, 5, 7}, + ) + Equal(t, expected, actual) + + expected = ` + +Diff: +--- Expected ++++ Actual +@@ -2,4 +2,4 @@ + (int) 1, +- (int) 2, +- (int) 3 ++ (int) 3, ++ (int) 5 + } +` + actual = diff( + []int{1, 2, 3, 4}[0:3], + []int{1, 3, 5, 7}[0:3], + ) + Equal(t, expected, actual) + + expected = ` + +Diff: +--- Expected ++++ Actual +@@ -1,6 +1,6 @@ + (map[string]int) (len=4) { +- (string) (len=4) "four": (int) 4, ++ (string) (len=4) "five": (int) 5, + (string) (len=3) "one": (int) 1, +- (string) (len=5) "three": (int) 3, +- (string) (len=3) "two": (int) 2 ++ (string) (len=5) "seven": (int) 7, ++ (string) (len=5) "three": (int) 3 + } +` + + actual = diff( + map[string]int{"one": 1, "two": 2, "three": 3, "four": 4}, + map[string]int{"one": 1, "three": 3, "five": 5, "seven": 7}, + ) + Equal(t, expected, actual) + + expected = ` + +Diff: +--- Expected ++++ Actual +@@ -1,3 +1,3 @@ + (*errors.errorString)({ +- s: (string) (len=19) "some expected error" ++ s: (string) (len=12) "actual error" + }) +` + + actual = diff( + errors.New("some expected error"), + errors.New("actual error"), + ) + Equal(t, expected, actual) + + expected = ` + +Diff: +--- Expected ++++ Actual +@@ -2,3 +2,3 @@ + A: (string) (len=11) "some string", +- B: (int) 10 ++ B: (int) 15 + } +` + + actual = diff( + diffTestingStruct{A: "some string", B: 10}, + diffTestingStruct{A: "some string", B: 15}, + ) + Equal(t, expected, actual) + + expected = ` + +Diff: +--- Expected ++++ Actual +@@ -1,2 +1,2 @@ +-(time.Time) 2020-09-24 00:00:00 +0000 UTC ++(time.Time) 2020-09-25 00:00:00 +0000 UTC + +` + + actual = diff( + time.Date(2020, 9, 24, 0, 0, 0, 0, time.UTC), + time.Date(2020, 9, 25, 0, 0, 0, 0, time.UTC), + ) + Equal(t, expected, actual) +} + +func TestTimeEqualityErrorFormatting(t *testing.T) { + t.Parallel() + + mockT := new(mockTestingT) + + Equal(mockT, time.Second*2, time.Millisecond) + + expectedErr := "\\s+Error Trace:\\s+Error:\\s+Not equal:\\s+\n\\s+expected: 2s\n\\s+actual\\s+: 1ms\n" + Regexp(t, regexp.MustCompile(expectedErr), mockT.errorString()) +} + +func TestDiffEmptyCases(t *testing.T) { + t.Parallel() + + Equal(t, "", diff(nil, nil)) + Equal(t, "", diff(struct{ foo string }{}, nil)) + Equal(t, "", diff(nil, struct{ foo string }{})) + Equal(t, "", diff(1, 2)) + Equal(t, "", diff(1, 2)) + Equal(t, "", diff([]int{1}, []bool{true})) +} + +// Ensure there are no data races +func TestDiffRace(t *testing.T) { + t.Parallel() + + expected := map[string]string{ + "a": "A", + "b": "B", + "c": "C", + } + + actual := map[string]string{ + "d": "D", + "e": "E", + "f": "F", + } + + // run diffs in parallel simulating tests with t.Parallel() + numRoutines := 10 + rChans := make([]chan string, numRoutines) + for idx := range rChans { + rChans[idx] = make(chan string) + go func(ch chan string) { + defer close(ch) + ch <- diff(expected, actual) + }(rChans[idx]) + } + + for _, ch := range rChans { + for msg := range ch { + NotZero(t, msg) // dummy assert + } + } +} + +type mockTestingT struct { + errorFmt string + args []interface{} +} + +// Helper is like [testing.T.Helper] but does nothing. +func (mockTestingT) Helper() {} + +func (m *mockTestingT) errorString() string { + return fmt.Sprintf(m.errorFmt, m.args...) +} + +func (m *mockTestingT) Errorf(format string, args ...interface{}) { + m.errorFmt = format + m.args = args +} + +func (m *mockTestingT) Failed() bool { + return m.errorFmt != "" +} + +func TestFailNowWithPlainTestingT(t *testing.T) { + t.Parallel() + + mockT := &mockTestingT{} + + Panics(t, func() { + FailNow(mockT, "failed") + }, "should panic since mockT is missing FailNow()") +} + +type mockFailNowTestingT struct{} + +// Helper is like [testing.T.Helper] but does nothing. +func (mockFailNowTestingT) Helper() {} + +func (m *mockFailNowTestingT) Errorf(format string, args ...interface{}) {} + +func (m *mockFailNowTestingT) FailNow() {} + +func TestFailNowWithFullTestingT(t *testing.T) { + t.Parallel() + + mockT := &mockFailNowTestingT{} + + NotPanics(t, func() { + FailNow(mockT, "failed") + }, "should call mockT.FailNow() rather than panicking") +} + +func TestBytesEqual(t *testing.T) { + t.Parallel() + + cases := []struct { + a, b []byte + }{ + {make([]byte, 2), make([]byte, 2)}, + {make([]byte, 2), make([]byte, 2, 3)}, + {nil, make([]byte, 0)}, + } + for i, c := range cases { + Equal(t, reflect.DeepEqual(c.a, c.b), ObjectsAreEqual(c.a, c.b), "case %d failed", i+1) + } +} + +func BenchmarkBytesEqual(b *testing.B) { + const size = 1024 * 8 + s := make([]byte, size) + for i := range s { + s[i] = byte(i % 255) + } + s2 := make([]byte, size) + copy(s2, s) + + mockT := &mockFailNowTestingT{} + b.ResetTimer() + for i := 0; i < b.N; i++ { + Equal(mockT, s, s2) + } +} + +func BenchmarkNotNil(b *testing.B) { + for i := 0; i < b.N; i++ { + 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() + + type iface interface { + Name() string + } + + tests := []struct { + name string + expect interface{} + got interface{} + assertion ComparisonAssertionFunc + }{ + {"implements", (*iface)(nil), t, Implements}, + {"isType", (*testing.T)(nil), t, IsType}, + {"equal", t, t, Equal}, + {"equalValues", t, t, EqualValues}, + {"notEqualValues", t, nil, NotEqualValues}, + {"exactly", t, t, Exactly}, + {"notEqual", t, nil, NotEqual}, + {"notContains", []int{1, 2, 3}, 4, NotContains}, + {"subset", []int{1, 2, 3, 4}, []int{2, 3}, Subset}, + {"notSubset", []int{1, 2, 3, 4}, []int{0, 3}, NotSubset}, + {"elementsMatch", []byte("abc"), []byte("bac"), ElementsMatch}, + {"regexp", "^t.*y$", "testify", Regexp}, + {"notRegexp", "^t.*y$", "Testify", NotRegexp}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.assertion(t, tt.expect, tt.got) + }) + } +} + +func ExampleValueAssertionFunc() { + t := &testing.T{} // provided by test + + dumbParse := func(input string) interface{} { + var x interface{} + _ = 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() + + tests := []struct { + name string + value interface{} + assertion ValueAssertionFunc + }{ + {"notNil", true, NotNil}, + {"nil", nil, Nil}, + {"empty", []int{}, Empty}, + {"notEmpty", []int{1}, NotEmpty}, + {"zero", false, Zero}, + {"notZero", 42, NotZero}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.assertion(t, tt.value) + }) + } +} + +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() + + tests := []struct { + name string + value bool + assertion BoolAssertionFunc + }{ + {"true", true, True}, + {"false", false, False}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.assertion(t, tt.value) + }) + } +} + +func ExampleErrorAssertionFunc() { + t := &testing.T{} // provided by test + + dumbParseNum := func(input string, v interface{}) 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() + + tests := []struct { + name string + err error + assertion ErrorAssertionFunc + }{ + {"noError", nil, NoError}, + {"error", errors.New("whoops"), Error}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.assertion(t, tt.err) + }) + } +} + +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() + + tests := []struct { + name string + panicFn PanicTestFunc + assertion PanicAssertionFunc + }{ + {"not panic", func() {}, NotPanics}, + {"panic", func() { panic(nil) }, Panics}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.assertion(t, tt.panicFn) + }) + } +} + +func TestEventuallyFalse(t *testing.T) { + t.Parallel() + + mockT := new(testing.T) + + condition := func() bool { + return false + } + + False(t, Eventually(mockT, condition, 100*time.Millisecond, 20*time.Millisecond)) +} + +func TestEventuallyTrue(t *testing.T) { + t.Parallel() + + state := 0 + condition := func() bool { + defer func() { + state += 1 + }() + return state == 2 + } + + True(t, Eventually(t, condition, 100*time.Millisecond, 20*time.Millisecond)) +} + +// errorsCapturingT is a mock implementation of TestingT that captures errors reported with Errorf. +type errorsCapturingT struct { + errors []error +} + +// Helper is like [testing.T.Helper] but does nothing. +func (errorsCapturingT) Helper() {} + +func (t *errorsCapturingT) Errorf(format string, args ...interface{}) { + t.errors = append(t.errors, fmt.Errorf(format, args...)) +} + +func TestEventuallyWithTFalse(t *testing.T) { + t.Parallel() + + mockT := new(errorsCapturingT) + + condition := func(collect *CollectT) { + Fail(collect, "condition fixed failure") + } + + False(t, EventuallyWithT(mockT, condition, 100*time.Millisecond, 20*time.Millisecond)) + Len(t, mockT.errors, 2) +} + +func TestEventuallyWithTTrue(t *testing.T) { + t.Parallel() + + mockT := new(errorsCapturingT) + + counter := 0 + condition := func(collect *CollectT) { + counter += 1 + True(collect, counter == 2) + } + + True(t, EventuallyWithT(mockT, condition, 100*time.Millisecond, 20*time.Millisecond)) + Len(t, mockT.errors, 0) + Equal(t, 2, counter, "Condition is expected to be called 2 times") +} + +func TestEventuallyWithT_ConcurrencySafe(t *testing.T) { + t.Parallel() + + mockT := new(errorsCapturingT) + + condition := func(collect *CollectT) { + Fail(collect, "condition fixed failure") + } + + // To trigger race conditions, we run EventuallyWithT with a nanosecond tick. + False(t, EventuallyWithT(mockT, condition, 100*time.Millisecond, time.Nanosecond)) + Len(t, mockT.errors, 2) +} + +func TestEventuallyWithT_ReturnsTheLatestFinishedConditionErrors(t *testing.T) { + t.Parallel() + + // We'll use a channel to control whether a condition should sleep or not. + mustSleep := make(chan bool, 2) + mustSleep <- false + mustSleep <- true + close(mustSleep) + + condition := func(collect *CollectT) { + if <-mustSleep { + // Sleep to ensure that the second condition runs longer than timeout. + time.Sleep(time.Second) + return + } + + // The first condition will fail. We expect to get this error as a result. + Fail(collect, "condition fixed failure") + } + + mockT := new(errorsCapturingT) + False(t, EventuallyWithT(mockT, condition, 100*time.Millisecond, 20*time.Millisecond)) + Len(t, mockT.errors, 2) +} + +func TestEventuallyWithTFailNow(t *testing.T) { + t.Parallel() + + mockT := new(CollectT) + + condition := func(collect *CollectT) { + collect.FailNow() + } + + False(t, EventuallyWithT(mockT, condition, 100*time.Millisecond, 20*time.Millisecond)) + Len(t, mockT.errors, 1) +} + +// Check that a long running condition doesn't block Eventually. +// See issue 805 (and its long tail of following issues) +func TestEventuallyTimeout(t *testing.T) { + t.Parallel() + + mockT := new(testing.T) + + NotPanics(t, func() { + done, done2 := make(chan struct{}), make(chan struct{}) + + // A condition function that returns after the Eventually timeout + condition := func() bool { + // Wait until Eventually times out and terminates + <-done + close(done2) + return true + } + + False(t, Eventually(mockT, condition, time.Millisecond, time.Microsecond)) + + close(done) + <-done2 + }) +} + +func TestEventuallySucceedQuickly(t *testing.T) { + t.Parallel() + + mockT := new(testing.T) + + condition := func() bool { return true } + + // By making the tick longer than the total duration, we expect that this test would fail if + // we didn't check the condition before the first tick elapses. + True(t, Eventually(mockT, condition, 100*time.Millisecond, time.Second)) +} + +func TestEventuallyWithTSucceedQuickly(t *testing.T) { + t.Parallel() + + mockT := new(testing.T) + + condition := func(t *CollectT) {} + + // By making the tick longer than the total duration, we expect that this test would fail if + // we didn't check the condition before the first tick elapses. + True(t, EventuallyWithT(mockT, condition, 100*time.Millisecond, time.Second)) +} + +func TestNeverFalse(t *testing.T) { + t.Parallel() + + condition := func() bool { + return false + } + + True(t, Never(t, condition, 100*time.Millisecond, 20*time.Millisecond)) +} + +// TestNeverTrue checks Never with a condition that returns true on second call. +func TestNeverTrue(t *testing.T) { + t.Parallel() + + mockT := new(testing.T) + + // A list of values returned by condition. + // Channel protects against concurrent access. + returns := make(chan bool, 2) + returns <- false + returns <- true + defer close(returns) + + // Will return true on second call. + condition := func() bool { + return <-returns + } + + False(t, Never(mockT, condition, 100*time.Millisecond, 20*time.Millisecond)) +} + +func TestNeverFailQuickly(t *testing.T) { + t.Parallel() + + mockT := new(testing.T) + + // By making the tick longer than the total duration, we expect that this test would fail if + // we didn't check the condition before the first tick elapses. + condition := func() bool { return true } + False(t, Never(mockT, condition, 100*time.Millisecond, time.Second)) +} + +func Test_validateEqualArgs(t *testing.T) { + t.Parallel() + + if validateEqualArgs(func() {}, func() {}) == nil { + t.Error("non-nil functions should error") + } + + if validateEqualArgs(func() {}, func() {}) == nil { + t.Error("non-nil functions should error") + } + + if validateEqualArgs(nil, nil) != nil { + t.Error("nil functions are equal") + } +} + +func Test_truncatingFormat(t *testing.T) { + t.Parallel() + + original := strings.Repeat("a", bufio.MaxScanTokenSize-102) + result := truncatingFormat(original) + Equal(t, fmt.Sprintf("%#v", original), result, "string should not be truncated") + + original = original + "x" + result = truncatingFormat(original) + NotEqual(t, fmt.Sprintf("%#v", original), result, "string should have been truncated.") + + if !strings.HasSuffix(result, "<... truncated>") { + t.Error("truncated string should have <... truncated> suffix") + } +} + +// parseLabeledOutput does the inverse of labeledOutput - it takes a formatted +// output string and turns it back into a slice of labeledContent. +func parseLabeledOutput(output string) []labeledContent { + labelPattern := regexp.MustCompile(`^\t([^\t]*): *\t(.*)$`) + contentPattern := regexp.MustCompile(`^\t *\t(.*)$`) + var contents []labeledContent + lines := strings.Split(output, "\n") + i := -1 + for _, line := range lines { + if line == "" { + // skip blank lines + continue + } + matches := labelPattern.FindStringSubmatch(line) + if len(matches) == 3 { + // a label + contents = append(contents, labeledContent{ + label: matches[1], + content: matches[2] + "\n", + }) + i++ + continue + } + matches = contentPattern.FindStringSubmatch(line) + if len(matches) == 2 { + // just content + if i >= 0 { + contents[i].content += matches[1] + "\n" + continue + } + } + // Couldn't parse output + return nil + } + return contents +} + +type captureTestingT struct { + failed bool + msg string +} + +// Helper is like [testing.T.Helper] but does nothing. +func (captureTestingT) Helper() {} + +func (ctt *captureTestingT) Errorf(format string, args ...interface{}) { + ctt.msg = fmt.Sprintf(format, args...) + ctt.failed = true +} + +func (ctt *captureTestingT) checkResultAndErrMsg(t *testing.T, expectedRes, res bool, expectedErrMsg string) { + t.Helper() + if res != expectedRes { + t.Errorf("Should return %t", expectedRes) + return + } + if res == ctt.failed { + t.Errorf("The test result (%t) should be reflected in the testing.T type (%t)", res, !ctt.failed) + return + } + contents := parseLabeledOutput(ctt.msg) + if res == true { + if contents != nil { + t.Errorf("Should not log an error. Log output: %q", ctt.msg) + } + return + } + if contents == nil { + t.Errorf("Should log an error. Log output: %q", ctt.msg) + return + } + for _, content := range contents { + if content.label == "Error" { + if expectedErrMsg == content.content { + return + } + t.Errorf("Recorded Error: %q", content.content) + } + } + t.Errorf("Expected Error: %q", expectedErrMsg) +} + +func TestErrorIs(t *testing.T) { + t.Parallel() + + tests := []struct { + err error + target error + result bool + resultErrMsg string + }{ + { + err: io.EOF, + target: io.EOF, + result: true, + }, + { + err: fmt.Errorf("wrap: %w", io.EOF), + target: io.EOF, + result: true, + }, + { + err: io.EOF, + target: io.ErrClosedPipe, + result: false, + resultErrMsg: "" + + "Target error should be in err chain:\n" + + "expected: \"io: read/write on closed pipe\"\n" + + "in chain: \"EOF\"\n", + }, + { + err: nil, + target: io.EOF, + result: false, + resultErrMsg: "Expected error with \"EOF\" in chain but got nil.\n", + }, + { + err: io.EOF, + target: nil, + result: false, + resultErrMsg: "" + + "Target error should be in err chain:\n" + + "expected: \"\"\n" + + "in chain: \"EOF\"\n", + }, + { + err: nil, + target: nil, + result: true, + }, + { + err: fmt.Errorf("abc: %w", errors.New("def")), + target: io.EOF, + result: false, + resultErrMsg: "" + + "Target error should be in err chain:\n" + + "expected: \"EOF\"\n" + + "in chain: \"abc: def\"\n" + + "\t\"def\"\n", + }, + } + for _, tt := range tests { + tt := tt + t.Run(fmt.Sprintf("ErrorIs(%#v,%#v)", tt.err, tt.target), func(t *testing.T) { + mockT := new(captureTestingT) + res := ErrorIs(mockT, tt.err, tt.target) + mockT.checkResultAndErrMsg(t, tt.result, res, tt.resultErrMsg) + }) + } +} + +func TestNotErrorIs(t *testing.T) { + t.Parallel() + + tests := []struct { + err error + target error + result bool + resultErrMsg string + }{ + { + err: io.EOF, + target: io.EOF, + result: false, + resultErrMsg: "" + + "Target error should not be in err chain:\n" + + "found: \"EOF\"\n" + + "in chain: \"EOF\"\n", + }, + { + err: fmt.Errorf("wrap: %w", io.EOF), + target: io.EOF, + result: false, + resultErrMsg: "" + + "Target error should not be in err chain:\n" + + "found: \"EOF\"\n" + + "in chain: \"wrap: EOF\"\n" + + "\t\"EOF\"\n", + }, + { + err: io.EOF, + target: io.ErrClosedPipe, + result: true, + }, + { + err: nil, + target: io.EOF, + result: true, + }, + { + err: io.EOF, + target: nil, + result: true, + }, + { + err: nil, + target: nil, + result: false, + resultErrMsg: "" + + "Target error should not be in err chain:\n" + + "found: \"\"\n" + + "in chain: \n", + }, + { + err: fmt.Errorf("abc: %w", errors.New("def")), + target: io.EOF, + result: true, + }, + } + for _, tt := range tests { + tt := tt + t.Run(fmt.Sprintf("NotErrorIs(%#v,%#v)", tt.err, tt.target), func(t *testing.T) { + mockT := new(captureTestingT) + res := NotErrorIs(mockT, tt.err, tt.target) + mockT.checkResultAndErrMsg(t, tt.result, res, tt.resultErrMsg) + }) + } +} + +func TestErrorAs(t *testing.T) { + t.Parallel() + + tests := []struct { + err error + result bool + resultErrMsg string + }{ + { + err: fmt.Errorf("wrap: %w", &customError{}), + result: true, + }, + { + err: io.EOF, + result: false, + resultErrMsg: "" + + "Should be in error chain:\n" + + "expected: *assert.customError\n" + + "in chain: \"EOF\" (*errors.errorString)\n", + }, + { + err: nil, + result: false, + resultErrMsg: "" + + "An error is expected but got nil.\n" + + `expected: *assert.customError` + "\n", + }, + { + err: fmt.Errorf("abc: %w", errors.New("def")), + result: false, + resultErrMsg: "" + + "Should be in error chain:\n" + + "expected: *assert.customError\n" + + "in chain: \"abc: def\" (*fmt.wrapError)\n" + + "\t\"def\" (*errors.errorString)\n", + }, + } + for _, tt := range tests { + tt := tt + var target *customError + t.Run(fmt.Sprintf("ErrorAs(%#v,%#v)", tt.err, target), func(t *testing.T) { + mockT := new(captureTestingT) + res := ErrorAs(mockT, tt.err, &target) + mockT.checkResultAndErrMsg(t, tt.result, res, tt.resultErrMsg) + }) + } +} + +func TestNotErrorAs(t *testing.T) { + t.Parallel() + + tests := []struct { + err error + result bool + resultErrMsg string + }{ + { + err: fmt.Errorf("wrap: %w", &customError{}), + result: false, + resultErrMsg: "" + + "Target error should not be in err chain:\n" + + "found: *assert.customError\n" + + "in chain: \"wrap: fail\" (*fmt.wrapError)\n" + + "\t\"fail\" (*assert.customError)\n", + }, + { + err: io.EOF, + result: true, + }, + { + err: nil, + result: true, + }, + } + for _, tt := range tests { + tt := tt + var target *customError + t.Run(fmt.Sprintf("NotErrorAs(%#v,%#v)", tt.err, target), func(t *testing.T) { + mockT := new(captureTestingT) + res := NotErrorAs(mockT, tt.err, &target) + mockT.checkResultAndErrMsg(t, tt.result, res, tt.resultErrMsg) + }) + } +} diff --git a/pkg/testify/assert/doc.go b/pkg/testify/assert/doc.go new file mode 100644 index 00000000..e11bc9ca --- /dev/null +++ b/pkg/testify/assert/doc.go @@ -0,0 +1,50 @@ +// Package assert provides a set of comprehensive testing tools for use with the normal Go testing system. +// +// # Note +// +// All functions in this package return a bool value indicating whether the assertion has passed. +// +// # Example Usage +// +// The following is a complete example using assert in a standard test function: +// +// import ( +// "testing" +// "github.com/CodSpeedHQ/codspeed-go/pkg/testify/assert" +// ) +// +// func TestSomething(t *testing.T) { +// +// var a string = "Hello" +// var b string = "Hello" +// +// assert.Equal(t, a, b, "The two words should be the same.") +// +// } +// +// if you assert many times, use the format below: +// +// import ( +// "testing" +// "github.com/CodSpeedHQ/codspeed-go/pkg/testify/assert" +// ) +// +// func TestSomething(t *testing.T) { +// assert := assert.New(t) +// +// var a string = "Hello" +// var b string = "Hello" +// +// assert.Equal(a, b, "The two words should be the same.") +// } +// +// # Assertions +// +// Assertions allow you to easily write test code, and are global funcs in the `assert` package. +// All assertion functions take, as the first argument, the `*testing.T` object provided by the +// testing framework. This allows the assertion funcs to write the failings and other details to +// the correct place. +// +// Every assertion function also takes an optional string message as the final argument, +// allowing custom error messages to be appended to the message the assertion method outputs. +package assert diff --git a/pkg/testify/assert/errors.go b/pkg/testify/assert/errors.go new file mode 100644 index 00000000..ac9dc9d1 --- /dev/null +++ b/pkg/testify/assert/errors.go @@ -0,0 +1,10 @@ +package assert + +import ( + "errors" +) + +// AnError is an error instance useful for testing. If the code does not care +// about error specifics, and only needs to return the error for example, this +// error should be used to make the test code more readable. +var AnError = errors.New("assert.AnError general error for testing") diff --git a/pkg/testify/assert/forward_assertions.go b/pkg/testify/assert/forward_assertions.go new file mode 100644 index 00000000..df189d23 --- /dev/null +++ b/pkg/testify/assert/forward_assertions.go @@ -0,0 +1,16 @@ +package assert + +// Assertions provides assertion methods around the +// TestingT interface. +type Assertions struct { + t TestingT +} + +// New makes a new Assertions object for the specified TestingT. +func New(t TestingT) *Assertions { + return &Assertions{ + t: t, + } +} + +//go:generate sh -c "cd ../_codegen && go build && cd - && ../_codegen/_codegen -output-package=assert -template=assertion_forward.go.tmpl -include-format-funcs" diff --git a/pkg/testify/assert/forward_assertions_test.go b/pkg/testify/assert/forward_assertions_test.go new file mode 100644 index 00000000..874ee7b1 --- /dev/null +++ b/pkg/testify/assert/forward_assertions_test.go @@ -0,0 +1,843 @@ +package assert + +import ( + "errors" + testing "github.com/CodSpeedHQ/codspeed-go/testing/testing" + "regexp" + "time" +) + +func TestImplementsWrapper(t *testing.T) { + t.Parallel() + + assert := New(new(testing.T)) + + if !assert.Implements((*AssertionTesterInterface)(nil), new(AssertionTesterConformingObject)) { + t.Error("Implements method should return true: AssertionTesterConformingObject implements AssertionTesterInterface") + } + if assert.Implements((*AssertionTesterInterface)(nil), new(AssertionTesterNonConformingObject)) { + t.Error("Implements method should return false: AssertionTesterNonConformingObject does not implements AssertionTesterInterface") + } +} + +func TestIsTypeWrapper(t *testing.T) { + t.Parallel() + + assert := New(new(testing.T)) + + if !assert.IsType(new(AssertionTesterConformingObject), new(AssertionTesterConformingObject)) { + t.Error("IsType should return true: AssertionTesterConformingObject is the same type as AssertionTesterConformingObject") + } + if assert.IsType(new(AssertionTesterConformingObject), new(AssertionTesterNonConformingObject)) { + t.Error("IsType should return false: AssertionTesterConformingObject is not the same type as AssertionTesterNonConformingObject") + } + +} + +func TestEqualWrapper(t *testing.T) { + t.Parallel() + + assert := New(new(testing.T)) + + if !assert.Equal("Hello World", "Hello World") { + t.Error("Equal should return true") + } + if !assert.Equal(123, 123) { + t.Error("Equal should return true") + } + if !assert.Equal(123.5, 123.5) { + t.Error("Equal should return true") + } + if !assert.Equal([]byte("Hello World"), []byte("Hello World")) { + t.Error("Equal should return true") + } + if !assert.Equal(nil, nil) { + t.Error("Equal should return true") + } +} + +func TestEqualValuesWrapper(t *testing.T) { + t.Parallel() + + assert := New(new(testing.T)) + + if !assert.EqualValues(uint32(10), int32(10)) { + t.Error("EqualValues should return true") + } +} + +func TestNotNilWrapper(t *testing.T) { + t.Parallel() + + assert := New(new(testing.T)) + + if !assert.NotNil(new(AssertionTesterConformingObject)) { + t.Error("NotNil should return true: object is not nil") + } + if assert.NotNil(nil) { + t.Error("NotNil should return false: object is nil") + } + +} + +func TestNilWrapper(t *testing.T) { + t.Parallel() + + assert := New(new(testing.T)) + + if !assert.Nil(nil) { + t.Error("Nil should return true: object is nil") + } + if assert.Nil(new(AssertionTesterConformingObject)) { + t.Error("Nil should return false: object is not nil") + } + +} + +func TestTrueWrapper(t *testing.T) { + t.Parallel() + + assert := New(new(testing.T)) + + if !assert.True(true) { + t.Error("True should return true") + } + if assert.True(false) { + t.Error("True should return false") + } + +} + +func TestFalseWrapper(t *testing.T) { + t.Parallel() + + assert := New(new(testing.T)) + + if !assert.False(false) { + t.Error("False should return true") + } + if assert.False(true) { + t.Error("False should return false") + } + +} + +func TestExactlyWrapper(t *testing.T) { + t.Parallel() + + assert := New(new(testing.T)) + + a := float32(1) + b := float64(1) + c := float32(1) + d := float32(2) + + if assert.Exactly(a, b) { + t.Error("Exactly should return false") + } + if assert.Exactly(a, d) { + t.Error("Exactly should return false") + } + if !assert.Exactly(a, c) { + t.Error("Exactly should return true") + } + + if assert.Exactly(nil, a) { + t.Error("Exactly should return false") + } + if assert.Exactly(a, nil) { + t.Error("Exactly should return false") + } + +} + +func TestNotEqualWrapper(t *testing.T) { + t.Parallel() + + assert := New(new(testing.T)) + + if !assert.NotEqual("Hello World", "Hello World!") { + t.Error("NotEqual should return true") + } + if !assert.NotEqual(123, 1234) { + t.Error("NotEqual should return true") + } + if !assert.NotEqual(123.5, 123.55) { + t.Error("NotEqual should return true") + } + if !assert.NotEqual([]byte("Hello World"), []byte("Hello World!")) { + t.Error("NotEqual should return true") + } + if !assert.NotEqual(nil, new(AssertionTesterConformingObject)) { + t.Error("NotEqual should return true") + } +} + +func TestNotEqualValuesWrapper(t *testing.T) { + t.Parallel() + + assert := New(new(testing.T)) + + if !assert.NotEqualValues("Hello World", "Hello World!") { + t.Error("NotEqualValues should return true") + } + if !assert.NotEqualValues(123, 1234) { + t.Error("NotEqualValues should return true") + } + if !assert.NotEqualValues(123.5, 123.55) { + t.Error("NotEqualValues should return true") + } + if !assert.NotEqualValues([]byte("Hello World"), []byte("Hello World!")) { + t.Error("NotEqualValues should return true") + } + if !assert.NotEqualValues(nil, new(AssertionTesterConformingObject)) { + t.Error("NotEqualValues should return true") + } + if assert.NotEqualValues(10, uint(10)) { + t.Error("NotEqualValues should return false") + } +} + +func TestContainsWrapper(t *testing.T) { + t.Parallel() + + assert := New(new(testing.T)) + list := []string{"Foo", "Bar"} + + if !assert.Contains("Hello World", "Hello") { + t.Error("Contains should return true: \"Hello World\" contains \"Hello\"") + } + if assert.Contains("Hello World", "Salut") { + t.Error("Contains should return false: \"Hello World\" does not contain \"Salut\"") + } + + if !assert.Contains(list, "Foo") { + t.Error("Contains should return true: \"[\"Foo\", \"Bar\"]\" contains \"Foo\"") + } + if assert.Contains(list, "Salut") { + t.Error("Contains should return false: \"[\"Foo\", \"Bar\"]\" does not contain \"Salut\"") + } + +} + +func TestNotContainsWrapper(t *testing.T) { + t.Parallel() + + assert := New(new(testing.T)) + list := []string{"Foo", "Bar"} + + if !assert.NotContains("Hello World", "Hello!") { + t.Error("NotContains should return true: \"Hello World\" does not contain \"Hello!\"") + } + if assert.NotContains("Hello World", "Hello") { + t.Error("NotContains should return false: \"Hello World\" contains \"Hello\"") + } + + if !assert.NotContains(list, "Foo!") { + t.Error("NotContains should return true: \"[\"Foo\", \"Bar\"]\" does not contain \"Foo!\"") + } + if assert.NotContains(list, "Foo") { + t.Error("NotContains should return false: \"[\"Foo\", \"Bar\"]\" contains \"Foo\"") + } + +} + +func TestConditionWrapper(t *testing.T) { + t.Parallel() + + assert := New(new(testing.T)) + + if !assert.Condition(func() bool { return true }, "Truth") { + t.Error("Condition should return true") + } + + if assert.Condition(func() bool { return false }, "Lie") { + t.Error("Condition should return false") + } + +} + +func TestDidPanicWrapper(t *testing.T) { + t.Parallel() + + if funcDidPanic, _, _ := didPanic(func() { + panic("Panic!") + }); !funcDidPanic { + t.Error("didPanic should return true") + } + + if funcDidPanic, _, _ := didPanic(func() { + }); funcDidPanic { + t.Error("didPanic should return false") + } + +} + +func TestPanicsWrapper(t *testing.T) { + t.Parallel() + + assert := New(new(testing.T)) + + if !assert.Panics(func() { + panic("Panic!") + }) { + t.Error("Panics should return true") + } + + if assert.Panics(func() { + }) { + t.Error("Panics should return false") + } + +} + +func TestNotPanicsWrapper(t *testing.T) { + t.Parallel() + + assert := New(new(testing.T)) + + if !assert.NotPanics(func() { + }) { + t.Error("NotPanics should return true") + } + + if assert.NotPanics(func() { + panic("Panic!") + }) { + t.Error("NotPanics should return false") + } + +} + +func TestNoErrorWrapper(t *testing.T) { + t.Parallel() + + assert := New(t) + mockAssert := New(new(testing.T)) + + // start with a nil error + var err error + + assert.True(mockAssert.NoError(err), "NoError should return True for nil arg") + + // now set an error + err = errors.New("Some error") + + assert.False(mockAssert.NoError(err), "NoError with error should return False") + +} + +func TestErrorWrapper(t *testing.T) { + t.Parallel() + + assert := New(t) + mockAssert := New(new(testing.T)) + + // start with a nil error + var err error + + assert.False(mockAssert.Error(err), "Error should return False for nil arg") + + // now set an error + err = errors.New("Some error") + + assert.True(mockAssert.Error(err), "Error with error should return True") + +} + +func TestErrorContainsWrapper(t *testing.T) { + t.Parallel() + + assert := New(t) + mockAssert := New(new(testing.T)) + + // start with a nil error + var err error + assert.False(mockAssert.ErrorContains(err, ""), + "ErrorContains should return false for nil arg") + + // now set an error + err = errors.New("some error: another error") + assert.False(mockAssert.ErrorContains(err, "different error"), + "ErrorContains should return false for different error string") + assert.True(mockAssert.ErrorContains(err, "some error"), + "ErrorContains should return true") + assert.True(mockAssert.ErrorContains(err, "another error"), + "ErrorContains should return true") +} + +func TestEqualErrorWrapper(t *testing.T) { + t.Parallel() + + assert := New(t) + mockAssert := New(new(testing.T)) + + // start with a nil error + var err error + assert.False(mockAssert.EqualError(err, ""), + "EqualError should return false for nil arg") + + // now set an error + err = errors.New("some error") + assert.False(mockAssert.EqualError(err, "Not some error"), + "EqualError should return false for different error string") + assert.True(mockAssert.EqualError(err, "some error"), + "EqualError should return true") +} + +func TestEmptyWrapper(t *testing.T) { + t.Parallel() + + assert := New(t) + mockAssert := New(new(testing.T)) + + assert.True(mockAssert.Empty(""), "Empty string is empty") + assert.True(mockAssert.Empty(nil), "Nil is empty") + assert.True(mockAssert.Empty([]string{}), "Empty string array is empty") + assert.True(mockAssert.Empty(0), "Zero int value is empty") + assert.True(mockAssert.Empty(false), "False value is empty") + + assert.False(mockAssert.Empty("something"), "Non Empty string is not empty") + assert.False(mockAssert.Empty(errors.New("something")), "Non nil object is not empty") + assert.False(mockAssert.Empty([]string{"something"}), "Non empty string array is not empty") + assert.False(mockAssert.Empty(1), "Non-zero int value is not empty") + assert.False(mockAssert.Empty(true), "True value is not empty") + +} + +func TestNotEmptyWrapper(t *testing.T) { + t.Parallel() + + assert := New(t) + mockAssert := New(new(testing.T)) + + assert.False(mockAssert.NotEmpty(""), "Empty string is empty") + assert.False(mockAssert.NotEmpty(nil), "Nil is empty") + assert.False(mockAssert.NotEmpty([]string{}), "Empty string array is empty") + assert.False(mockAssert.NotEmpty(0), "Zero int value is empty") + assert.False(mockAssert.NotEmpty(false), "False value is empty") + + assert.True(mockAssert.NotEmpty("something"), "Non Empty string is not empty") + assert.True(mockAssert.NotEmpty(errors.New("something")), "Non nil object is not empty") + assert.True(mockAssert.NotEmpty([]string{"something"}), "Non empty string array is not empty") + assert.True(mockAssert.NotEmpty(1), "Non-zero int value is not empty") + assert.True(mockAssert.NotEmpty(true), "True value is not empty") + +} + +func TestLenWrapper(t *testing.T) { + t.Parallel() + + assert := New(t) + mockAssert := New(new(testing.T)) + + assert.False(mockAssert.Len(nil, 0), "nil does not have length") + assert.False(mockAssert.Len(0, 0), "int does not have length") + assert.False(mockAssert.Len(true, 0), "true does not have length") + assert.False(mockAssert.Len(false, 0), "false does not have length") + assert.False(mockAssert.Len('A', 0), "Rune does not have length") + assert.False(mockAssert.Len(struct{}{}, 0), "Struct does not have length") + + ch := make(chan int, 5) + ch <- 1 + ch <- 2 + ch <- 3 + + cases := []struct { + v interface{} + l int + }{ + {[]int{1, 2, 3}, 3}, + {[...]int{1, 2, 3}, 3}, + {"ABC", 3}, + {map[int]int{1: 2, 2: 4, 3: 6}, 3}, + {ch, 3}, + + {[]int{}, 0}, + {map[int]int{}, 0}, + {make(chan int), 0}, + + {[]int(nil), 0}, + {map[int]int(nil), 0}, + {(chan int)(nil), 0}, + } + + for _, c := range cases { + assert.True(mockAssert.Len(c.v, c.l), "%#v have %d items", c.v, c.l) + } +} + +func TestWithinDurationWrapper(t *testing.T) { + t.Parallel() + + assert := New(t) + mockAssert := New(new(testing.T)) + a := time.Now() + b := a.Add(10 * time.Second) + + assert.True(mockAssert.WithinDuration(a, b, 10*time.Second), "A 10s difference is within a 10s time difference") + assert.True(mockAssert.WithinDuration(b, a, 10*time.Second), "A 10s difference is within a 10s time difference") + + assert.False(mockAssert.WithinDuration(a, b, 9*time.Second), "A 10s difference is not within a 9s time difference") + assert.False(mockAssert.WithinDuration(b, a, 9*time.Second), "A 10s difference is not within a 9s time difference") + + assert.False(mockAssert.WithinDuration(a, b, -9*time.Second), "A 10s difference is not within a 9s time difference") + assert.False(mockAssert.WithinDuration(b, a, -9*time.Second), "A 10s difference is not within a 9s time difference") + + assert.False(mockAssert.WithinDuration(a, b, -11*time.Second), "A 10s difference is not within a 9s time difference") + assert.False(mockAssert.WithinDuration(b, a, -11*time.Second), "A 10s difference is not within a 9s time difference") +} + +func TestInDeltaWrapper(t *testing.T) { + t.Parallel() + + assert := New(new(testing.T)) + + True(t, assert.InDelta(1.001, 1, 0.01), "|1.001 - 1| <= 0.01") + True(t, assert.InDelta(1, 1.001, 0.01), "|1 - 1.001| <= 0.01") + True(t, assert.InDelta(1, 2, 1), "|1 - 2| <= 1") + False(t, assert.InDelta(1, 2, 0.5), "Expected |1 - 2| <= 0.5 to fail") + False(t, assert.InDelta(2, 1, 0.5), "Expected |2 - 1| <= 0.5 to fail") + False(t, assert.InDelta("", nil, 1), "Expected non numerals to fail") + + cases := []struct { + a, b interface{} + delta float64 + }{ + {uint8(2), uint8(1), 1}, + {uint16(2), uint16(1), 1}, + {uint32(2), uint32(1), 1}, + {uint64(2), uint64(1), 1}, + + {int(2), int(1), 1}, + {int8(2), int8(1), 1}, + {int16(2), int16(1), 1}, + {int32(2), int32(1), 1}, + {int64(2), int64(1), 1}, + + {float32(2), float32(1), 1}, + {float64(2), float64(1), 1}, + } + + for _, tc := range cases { + True(t, assert.InDelta(tc.a, tc.b, tc.delta), "Expected |%V - %V| <= %v", tc.a, tc.b, tc.delta) + } +} + +func TestInEpsilonWrapper(t *testing.T) { + t.Parallel() + + assert := New(new(testing.T)) + + cases := []struct { + a, b interface{} + epsilon float64 + }{ + {uint8(2), uint16(2), .001}, + {2.1, 2.2, 0.1}, + {2.2, 2.1, 0.1}, + {-2.1, -2.2, 0.1}, + {-2.2, -2.1, 0.1}, + {uint64(100), uint8(101), 0.01}, + {0.1, -0.1, 2}, + } + + for _, tc := range cases { + True(t, assert.InEpsilon(tc.a, tc.b, tc.epsilon, "Expected %V and %V to have a relative difference of %v", tc.a, tc.b, tc.epsilon)) + } + + cases = []struct { + a, b interface{} + epsilon float64 + }{ + {uint8(2), int16(-2), .001}, + {uint64(100), uint8(102), 0.01}, + {2.1, 2.2, 0.001}, + {2.2, 2.1, 0.001}, + {2.1, -2.2, 1}, + {2.1, "bla-bla", 0}, + {0.1, -0.1, 1.99}, + } + + for _, tc := range cases { + False(t, assert.InEpsilon(tc.a, tc.b, tc.epsilon, "Expected %V and %V to have a relative difference of %v", tc.a, tc.b, tc.epsilon)) + } +} + +func TestRegexpWrapper(t *testing.T) { + t.Parallel() + + assert := New(new(testing.T)) + + cases := []struct { + rx, str string + }{ + {"^start", "start of the line"}, + {"end$", "in the end"}, + {"[0-9]{3}[.-]?[0-9]{2}[.-]?[0-9]{2}", "My phone number is 650.12.34"}, + } + + for _, tc := range cases { + True(t, assert.Regexp(tc.rx, tc.str)) + True(t, assert.Regexp(regexp.MustCompile(tc.rx), tc.str)) + False(t, assert.NotRegexp(tc.rx, tc.str)) + False(t, assert.NotRegexp(regexp.MustCompile(tc.rx), tc.str)) + } + + cases = []struct { + rx, str string + }{ + {"^asdfastart", "Not the start of the line"}, + {"end$", "in the end."}, + {"[0-9]{3}[.-]?[0-9]{2}[.-]?[0-9]{2}", "My phone number is 650.12a.34"}, + } + + for _, tc := range cases { + False(t, assert.Regexp(tc.rx, tc.str), "Expected %q to not match %q", tc.rx, tc.str) + False(t, assert.Regexp(regexp.MustCompile(tc.rx), tc.str)) + True(t, assert.NotRegexp(tc.rx, tc.str)) + True(t, assert.NotRegexp(regexp.MustCompile(tc.rx), tc.str)) + } +} + +func TestZeroWrapper(t *testing.T) { + t.Parallel() + + assert := New(t) + mockAssert := New(new(testing.T)) + + for _, test := range zeros { + assert.True(mockAssert.Zero(test), "Zero should return true for %v", test) + } + + for _, test := range nonZeros { + assert.False(mockAssert.Zero(test), "Zero should return false for %v", test) + } +} + +func TestNotZeroWrapper(t *testing.T) { + t.Parallel() + + assert := New(t) + mockAssert := New(new(testing.T)) + + for _, test := range zeros { + assert.False(mockAssert.NotZero(test), "Zero should return true for %v", test) + } + + for _, test := range nonZeros { + assert.True(mockAssert.NotZero(test), "Zero should return false for %v", test) + } +} + +func TestJSONEqWrapper_EqualSONString(t *testing.T) { + t.Parallel() + + assert := New(new(testing.T)) + if !assert.JSONEq(`{"hello": "world", "foo": "bar"}`, `{"hello": "world", "foo": "bar"}`) { + t.Error("JSONEq should return true") + } + +} + +func TestJSONEqWrapper_EquivalentButNotEqual(t *testing.T) { + t.Parallel() + + assert := New(new(testing.T)) + if !assert.JSONEq(`{"hello": "world", "foo": "bar"}`, `{"foo": "bar", "hello": "world"}`) { + t.Error("JSONEq should return true") + } + +} + +func TestJSONEqWrapper_HashOfArraysAndHashes(t *testing.T) { + t.Parallel() + + assert := New(new(testing.T)) + if !assert.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}") { + t.Error("JSONEq should return true") + } +} + +func TestJSONEqWrapper_Array(t *testing.T) { + t.Parallel() + + assert := New(new(testing.T)) + if !assert.JSONEq(`["foo", {"hello": "world", "nested": "hash"}]`, `["foo", {"nested": "hash", "hello": "world"}]`) { + t.Error("JSONEq should return true") + } + +} + +func TestJSONEqWrapper_HashAndArrayNotEquivalent(t *testing.T) { + t.Parallel() + + assert := New(new(testing.T)) + if assert.JSONEq(`["foo", {"hello": "world", "nested": "hash"}]`, `{"foo": "bar", {"nested": "hash", "hello": "world"}}`) { + t.Error("JSONEq should return false") + } +} + +func TestJSONEqWrapper_HashesNotEquivalent(t *testing.T) { + t.Parallel() + + assert := New(new(testing.T)) + if assert.JSONEq(`{"foo": "bar"}`, `{"foo": "bar", "hello": "world"}`) { + t.Error("JSONEq should return false") + } +} + +func TestJSONEqWrapper_ActualIsNotJSON(t *testing.T) { + t.Parallel() + + assert := New(new(testing.T)) + if assert.JSONEq(`{"foo": "bar"}`, "Not JSON") { + t.Error("JSONEq should return false") + } +} + +func TestJSONEqWrapper_ExpectedIsNotJSON(t *testing.T) { + t.Parallel() + + assert := New(new(testing.T)) + if assert.JSONEq("Not JSON", `{"foo": "bar", "hello": "world"}`) { + t.Error("JSONEq should return false") + } +} + +func TestJSONEqWrapper_ExpectedAndActualNotJSON(t *testing.T) { + t.Parallel() + + assert := New(new(testing.T)) + if assert.JSONEq("Not JSON", "Not JSON") { + t.Error("JSONEq should return false") + } +} + +func TestJSONEqWrapper_ArraysOfDifferentOrder(t *testing.T) { + t.Parallel() + + assert := New(new(testing.T)) + if assert.JSONEq(`["foo", {"hello": "world", "nested": "hash"}]`, `[{ "hello": "world", "nested": "hash"}, "foo"]`) { + t.Error("JSONEq should return false") + } +} + +func TestYAMLEqWrapper_EqualYAMLString(t *testing.T) { + t.Parallel() + + assert := New(new(testing.T)) + if !assert.YAMLEq(`{"hello": "world", "foo": "bar"}`, `{"hello": "world", "foo": "bar"}`) { + t.Error("YAMLEq should return true") + } + +} + +func TestYAMLEqWrapper_EquivalentButNotEqual(t *testing.T) { + t.Parallel() + + assert := New(new(testing.T)) + if !assert.YAMLEq(`{"hello": "world", "foo": "bar"}`, `{"foo": "bar", "hello": "world"}`) { + t.Error("YAMLEq should return true") + } + +} + +func TestYAMLEqWrapper_HashOfArraysAndHashes(t *testing.T) { + t.Parallel() + + assert := New(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] +` + if !assert.YAMLEq(expected, actual) { + t.Error("YAMLEq should return true") + } +} + +func TestYAMLEqWrapper_Array(t *testing.T) { + t.Parallel() + + assert := New(new(testing.T)) + if !assert.YAMLEq(`["foo", {"hello": "world", "nested": "hash"}]`, `["foo", {"nested": "hash", "hello": "world"}]`) { + t.Error("YAMLEq should return true") + } + +} + +func TestYAMLEqWrapper_HashAndArrayNotEquivalent(t *testing.T) { + t.Parallel() + + assert := New(new(testing.T)) + if assert.YAMLEq(`["foo", {"hello": "world", "nested": "hash"}]`, `{"foo": "bar", {"nested": "hash", "hello": "world"}}`) { + t.Error("YAMLEq should return false") + } +} + +func TestYAMLEqWrapper_HashesNotEquivalent(t *testing.T) { + t.Parallel() + + assert := New(new(testing.T)) + if assert.YAMLEq(`{"foo": "bar"}`, `{"foo": "bar", "hello": "world"}`) { + t.Error("YAMLEq should return false") + } +} + +func TestYAMLEqWrapper_ActualIsSimpleString(t *testing.T) { + t.Parallel() + + assert := New(new(testing.T)) + if assert.YAMLEq(`{"foo": "bar"}`, "Simple String") { + t.Error("YAMLEq should return false") + } +} + +func TestYAMLEqWrapper_ExpectedIsSimpleString(t *testing.T) { + t.Parallel() + + assert := New(new(testing.T)) + if assert.YAMLEq("Simple String", `{"foo": "bar", "hello": "world"}`) { + t.Error("YAMLEq should return false") + } +} + +func TestYAMLEqWrapper_ExpectedAndActualSimpleString(t *testing.T) { + t.Parallel() + + assert := New(new(testing.T)) + if !assert.YAMLEq("Simple String", "Simple String") { + t.Error("YAMLEq should return true") + } +} + +func TestYAMLEqWrapper_ArraysOfDifferentOrder(t *testing.T) { + t.Parallel() + + assert := New(new(testing.T)) + if assert.YAMLEq(`["foo", {"hello": "world", "nested": "hash"}]`, `[{ "hello": "world", "nested": "hash"}, "foo"]`) { + t.Error("YAMLEq should return false") + } +} diff --git a/pkg/testify/assert/http_assertions.go b/pkg/testify/assert/http_assertions.go new file mode 100644 index 00000000..5a6bb75f --- /dev/null +++ b/pkg/testify/assert/http_assertions.go @@ -0,0 +1,165 @@ +package assert + +import ( + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "strings" +) + +// httpCode is a helper that returns HTTP code of the response. It returns -1 and +// an error if building a new request fails. +func httpCode(handler http.HandlerFunc, method, url string, values url.Values) (int, error) { + w := httptest.NewRecorder() + req, err := http.NewRequest(method, url, http.NoBody) + if err != nil { + return -1, err + } + req.URL.RawQuery = values.Encode() + handler(w, req) + return w.Code, nil +} + +// HTTPSuccess asserts that a specified handler returns a success status code. +// +// assert.HTTPSuccess(t, myHandler, "POST", "http://www.google.com", nil) +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPSuccess(t TestingT, handler http.HandlerFunc, method, url string, values url.Values, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + code, err := httpCode(handler, method, url, values) + if err != nil { + Fail(t, fmt.Sprintf("Failed to build test request, got error: %s", err), msgAndArgs...) + } + + isSuccessCode := code >= http.StatusOK && code <= http.StatusPartialContent + if !isSuccessCode { + Fail(t, fmt.Sprintf("Expected HTTP success status code for %q but received %d", url+"?"+values.Encode(), code), msgAndArgs...) + } + + return isSuccessCode +} + +// HTTPRedirect asserts that a specified handler returns a redirect status code. +// +// assert.HTTPRedirect(t, myHandler, "GET", "/a/b/c", url.Values{"a": []string{"b", "c"}} +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPRedirect(t TestingT, handler http.HandlerFunc, method, url string, values url.Values, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + code, err := httpCode(handler, method, url, values) + if err != nil { + Fail(t, fmt.Sprintf("Failed to build test request, got error: %s", err), msgAndArgs...) + } + + isRedirectCode := code >= http.StatusMultipleChoices && code <= http.StatusTemporaryRedirect + if !isRedirectCode { + Fail(t, fmt.Sprintf("Expected HTTP redirect status code for %q but received %d", url+"?"+values.Encode(), code), msgAndArgs...) + } + + return isRedirectCode +} + +// HTTPError asserts that a specified handler returns an error status code. +// +// assert.HTTPError(t, myHandler, "POST", "/a/b/c", url.Values{"a": []string{"b", "c"}} +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPError(t TestingT, handler http.HandlerFunc, method, url string, values url.Values, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + code, err := httpCode(handler, method, url, values) + if err != nil { + Fail(t, fmt.Sprintf("Failed to build test request, got error: %s", err), msgAndArgs...) + } + + isErrorCode := code >= http.StatusBadRequest + if !isErrorCode { + Fail(t, fmt.Sprintf("Expected HTTP error status code for %q but received %d", url+"?"+values.Encode(), code), msgAndArgs...) + } + + return isErrorCode +} + +// HTTPStatusCode asserts that a specified handler returns a specified status code. +// +// assert.HTTPStatusCode(t, myHandler, "GET", "/notImplemented", nil, 501) +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPStatusCode(t TestingT, handler http.HandlerFunc, method, url string, values url.Values, statuscode int, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + code, err := httpCode(handler, method, url, values) + if err != nil { + Fail(t, fmt.Sprintf("Failed to build test request, got error: %s", err), msgAndArgs...) + } + + successful := code == statuscode + if !successful { + Fail(t, fmt.Sprintf("Expected HTTP status code %d for %q but received %d", statuscode, url+"?"+values.Encode(), code), msgAndArgs...) + } + + return successful +} + +// HTTPBody is a helper that returns HTTP body of the response. It returns +// empty string if building a new request fails. +func HTTPBody(handler http.HandlerFunc, method, url string, values url.Values) string { + w := httptest.NewRecorder() + if len(values) > 0 { + url += "?" + values.Encode() + } + req, err := http.NewRequest(method, url, http.NoBody) + if err != nil { + return "" + } + handler(w, req) + return w.Body.String() +} + +// HTTPBodyContains asserts that a specified handler returns a +// body that contains a string. +// +// assert.HTTPBodyContains(t, myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky") +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPBodyContains(t TestingT, handler http.HandlerFunc, method, url string, values url.Values, str interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + body := HTTPBody(handler, method, url, values) + + contains := strings.Contains(body, fmt.Sprint(str)) + if !contains { + Fail(t, fmt.Sprintf("Expected response body for %q to contain %q but found %q", url+"?"+values.Encode(), str, body), msgAndArgs...) + } + + return contains +} + +// HTTPBodyNotContains asserts that a specified handler returns a +// body that does not contain a string. +// +// assert.HTTPBodyNotContains(t, myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky") +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPBodyNotContains(t TestingT, handler http.HandlerFunc, method, url string, values url.Values, str interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + body := HTTPBody(handler, method, url, values) + + contains := strings.Contains(body, fmt.Sprint(str)) + if contains { + Fail(t, fmt.Sprintf("Expected response body for %q to NOT contain %q but found %q", url+"?"+values.Encode(), str, body), msgAndArgs...) + } + + return !contains +} diff --git a/pkg/testify/assert/http_assertions_test.go b/pkg/testify/assert/http_assertions_test.go new file mode 100644 index 00000000..17805935 --- /dev/null +++ b/pkg/testify/assert/http_assertions_test.go @@ -0,0 +1,232 @@ +package assert + +import ( + "fmt" + testing "github.com/CodSpeedHQ/codspeed-go/testing/testing" + "io" + "net/http" + "net/url" +) + +func httpOK(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) +} + +func httpReadBody(w http.ResponseWriter, r *http.Request) { + _, _ = io.Copy(io.Discard, r.Body) + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("hello")) +} + +func httpRedirect(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusTemporaryRedirect) +} + +func httpError(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) +} + +func httpStatusCode(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusSwitchingProtocols) +} + +func TestHTTPSuccess(t *testing.T) { + t.Parallel() + + assert := New(t) + + mockT1 := new(testing.T) + assert.Equal(HTTPSuccess(mockT1, httpOK, "GET", "/", nil), true) + assert.False(mockT1.Failed()) + + mockT2 := new(testing.T) + assert.Equal(HTTPSuccess(mockT2, httpRedirect, "GET", "/", nil), false) + assert.True(mockT2.Failed()) + + mockT3 := new(mockTestingT) + assert.Equal(HTTPSuccess( + mockT3, httpError, "GET", "/", nil, + "was not expecting a failure here", + ), false) + assert.True(mockT3.Failed()) + assert.Contains(mockT3.errorString(), "was not expecting a failure here") + + mockT4 := new(testing.T) + assert.Equal(HTTPSuccess(mockT4, httpStatusCode, "GET", "/", nil), false) + assert.True(mockT4.Failed()) + + mockT5 := new(testing.T) + assert.Equal(HTTPSuccess(mockT5, httpReadBody, "POST", "/", nil), true) + assert.False(mockT5.Failed()) +} + +func TestHTTPRedirect(t *testing.T) { + t.Parallel() + + assert := New(t) + + mockT1 := new(mockTestingT) + assert.Equal(HTTPRedirect( + mockT1, httpOK, "GET", "/", nil, + "was expecting a 3xx status code. Got 200.", + ), false) + assert.True(mockT1.Failed()) + assert.Contains(mockT1.errorString(), "was expecting a 3xx status code. Got 200.") + + mockT2 := new(testing.T) + assert.Equal(HTTPRedirect(mockT2, httpRedirect, "GET", "/", nil), true) + assert.False(mockT2.Failed()) + + mockT3 := new(testing.T) + assert.Equal(HTTPRedirect(mockT3, httpError, "GET", "/", nil), false) + assert.True(mockT3.Failed()) + + mockT4 := new(testing.T) + assert.Equal(HTTPRedirect(mockT4, httpStatusCode, "GET", "/", nil), false) + assert.True(mockT4.Failed()) +} + +func TestHTTPError(t *testing.T) { + t.Parallel() + + assert := New(t) + + mockT1 := new(testing.T) + assert.Equal(HTTPError(mockT1, httpOK, "GET", "/", nil), false) + assert.True(mockT1.Failed()) + + mockT2 := new(mockTestingT) + assert.Equal(HTTPError( + mockT2, httpRedirect, "GET", "/", nil, + "Expected this request to error out. But it didn't", + ), false) + assert.True(mockT2.Failed()) + assert.Contains(mockT2.errorString(), "Expected this request to error out. But it didn't") + + mockT3 := new(testing.T) + assert.Equal(HTTPError(mockT3, httpError, "GET", "/", nil), true) + assert.False(mockT3.Failed()) + + mockT4 := new(testing.T) + assert.Equal(HTTPError(mockT4, httpStatusCode, "GET", "/", nil), false) + assert.True(mockT4.Failed()) +} + +func TestHTTPStatusCode(t *testing.T) { + t.Parallel() + + assert := New(t) + + mockT1 := new(testing.T) + assert.Equal(HTTPStatusCode(mockT1, httpOK, "GET", "/", nil, http.StatusSwitchingProtocols), false) + assert.True(mockT1.Failed()) + + mockT2 := new(testing.T) + assert.Equal(HTTPStatusCode(mockT2, httpRedirect, "GET", "/", nil, http.StatusSwitchingProtocols), false) + assert.True(mockT2.Failed()) + + mockT3 := new(mockTestingT) + assert.Equal(HTTPStatusCode( + mockT3, httpError, "GET", "/", nil, http.StatusSwitchingProtocols, + "Expected the status code to be %d", http.StatusSwitchingProtocols, + ), false) + assert.True(mockT3.Failed()) + assert.Contains(mockT3.errorString(), "Expected the status code to be 101") + + mockT4 := new(testing.T) + assert.Equal(HTTPStatusCode(mockT4, httpStatusCode, "GET", "/", nil, http.StatusSwitchingProtocols), true) + assert.False(mockT4.Failed()) +} + +func TestHTTPStatusesWrapper(t *testing.T) { + t.Parallel() + + assert := New(t) + mockAssert := New(new(testing.T)) + + assert.Equal(mockAssert.HTTPSuccess(httpOK, "GET", "/", nil), true) + assert.Equal(mockAssert.HTTPSuccess(httpRedirect, "GET", "/", nil), false) + assert.Equal(mockAssert.HTTPSuccess(httpError, "GET", "/", nil), false) + + assert.Equal(mockAssert.HTTPRedirect(httpOK, "GET", "/", nil), false) + assert.Equal(mockAssert.HTTPRedirect(httpRedirect, "GET", "/", nil), true) + assert.Equal(mockAssert.HTTPRedirect(httpError, "GET", "/", nil), false) + + assert.Equal(mockAssert.HTTPError(httpOK, "GET", "/", nil), false) + assert.Equal(mockAssert.HTTPError(httpRedirect, "GET", "/", nil), false) + assert.Equal(mockAssert.HTTPError(httpError, "GET", "/", nil), true) +} + +func httpHelloName(w http.ResponseWriter, r *http.Request) { + name := r.FormValue("name") + _, _ = fmt.Fprintf(w, "Hello, %s!", name) +} + +func TestHTTPRequestWithNoParams(t *testing.T) { + t.Parallel() + + var got *http.Request + handler := func(w http.ResponseWriter, r *http.Request) { + got = r + w.WriteHeader(http.StatusOK) + } + + True(t, HTTPSuccess(t, handler, "GET", "/url", nil)) + + Empty(t, got.URL.Query()) + Equal(t, "/url", got.URL.RequestURI()) +} + +func TestHTTPRequestWithParams(t *testing.T) { + t.Parallel() + + var got *http.Request + handler := func(w http.ResponseWriter, r *http.Request) { + got = r + w.WriteHeader(http.StatusOK) + } + params := url.Values{} + params.Add("id", "12345") + + True(t, HTTPSuccess(t, handler, "GET", "/url", params)) + + Equal(t, url.Values{"id": []string{"12345"}}, got.URL.Query()) + Equal(t, "/url?id=12345", got.URL.String()) + Equal(t, "/url?id=12345", got.URL.RequestURI()) +} + +func TestHttpBody(t *testing.T) { + t.Parallel() + + assert := New(t) + mockT := new(mockTestingT) + + assert.True(HTTPBodyContains(mockT, httpHelloName, "GET", "/", url.Values{"name": []string{"World"}}, "Hello, World!")) + assert.True(HTTPBodyContains(mockT, httpHelloName, "GET", "/", url.Values{"name": []string{"World"}}, "World")) + assert.False(HTTPBodyContains(mockT, httpHelloName, "GET", "/", url.Values{"name": []string{"World"}}, "world")) + + assert.False(HTTPBodyNotContains(mockT, httpHelloName, "GET", "/", url.Values{"name": []string{"World"}}, "Hello, World!")) + assert.False(HTTPBodyNotContains( + mockT, httpHelloName, "GET", "/", url.Values{"name": []string{"World"}}, "World", + "Expected the request body to not contain 'World'. But it did.", + )) + assert.True(HTTPBodyNotContains(mockT, httpHelloName, "GET", "/", url.Values{"name": []string{"World"}}, "world")) + assert.Contains(mockT.errorString(), "Expected the request body to not contain 'World'. But it did.") + + assert.True(HTTPBodyContains(mockT, httpReadBody, "GET", "/", nil, "hello")) +} + +func TestHttpBodyWrappers(t *testing.T) { + t.Parallel() + + assert := New(t) + mockAssert := New(new(testing.T)) + + assert.True(mockAssert.HTTPBodyContains(httpHelloName, "GET", "/", url.Values{"name": []string{"World"}}, "Hello, World!")) + assert.True(mockAssert.HTTPBodyContains(httpHelloName, "GET", "/", url.Values{"name": []string{"World"}}, "World")) + assert.False(mockAssert.HTTPBodyContains(httpHelloName, "GET", "/", url.Values{"name": []string{"World"}}, "world")) + + assert.False(mockAssert.HTTPBodyNotContains(httpHelloName, "GET", "/", url.Values{"name": []string{"World"}}, "Hello, World!")) + assert.False(mockAssert.HTTPBodyNotContains(httpHelloName, "GET", "/", url.Values{"name": []string{"World"}}, "World")) + assert.True(mockAssert.HTTPBodyNotContains(httpHelloName, "GET", "/", url.Values{"name": []string{"World"}}, "world")) +} diff --git a/pkg/testify/assert/internal/unsafetests/doc.go b/pkg/testify/assert/internal/unsafetests/doc.go new file mode 100644 index 00000000..08172d51 --- /dev/null +++ b/pkg/testify/assert/internal/unsafetests/doc.go @@ -0,0 +1,4 @@ +// This package exists just to isolate tests that reference the [unsafe] package. +// +// The tests in this package are totally safe. +package unsafetests diff --git a/pkg/testify/assert/internal/unsafetests/unsafetests_test.go b/pkg/testify/assert/internal/unsafetests/unsafetests_test.go new file mode 100644 index 00000000..34834757 --- /dev/null +++ b/pkg/testify/assert/internal/unsafetests/unsafetests_test.go @@ -0,0 +1,34 @@ +package unsafetests_test + +import ( + "fmt" + testing "github.com/CodSpeedHQ/codspeed-go/testing/testing" + "unsafe" + + "github.com/CodSpeedHQ/codspeed-go/pkg/testify/assert" +) + +type ignoreTestingT struct{} + +var _ assert.TestingT = ignoreTestingT{} + +func (ignoreTestingT) Helper() {} + +func (ignoreTestingT) Errorf(format string, args ...interface{}) { + // Run the formatting, but ignore the result + msg := fmt.Sprintf(format, args...) + _ = msg +} + +func TestUnsafePointers(t *testing.T) { + var ignore ignoreTestingT + + assert.True(t, assert.Nil(t, unsafe.Pointer(nil), "unsafe.Pointer(nil) is nil")) + assert.False(t, assert.NotNil(ignore, unsafe.Pointer(nil), "unsafe.Pointer(nil) is nil")) + + assert.True(t, assert.Nil(t, unsafe.Pointer((*int)(nil)), "unsafe.Pointer((*int)(nil)) is nil")) + assert.False(t, assert.NotNil(ignore, unsafe.Pointer((*int)(nil)), "unsafe.Pointer((*int)(nil)) is nil")) + + assert.False(t, assert.Nil(ignore, unsafe.Pointer(new(int)), "unsafe.Pointer(new(int)) is NOT nil")) + assert.True(t, assert.NotNil(t, unsafe.Pointer(new(int)), "unsafe.Pointer(new(int)) is NOT nil")) +} diff --git a/pkg/testify/assert/yaml/yaml_custom.go b/pkg/testify/assert/yaml/yaml_custom.go new file mode 100644 index 00000000..d0d1c6db --- /dev/null +++ b/pkg/testify/assert/yaml/yaml_custom.go @@ -0,0 +1,24 @@ +//go:build testify_yaml_custom && !testify_yaml_fail && !testify_yaml_default + +// Package yaml is an implementation of YAML functions that calls a pluggable implementation. +// +// This implementation is selected with the testify_yaml_custom build tag. +// +// go test -tags testify_yaml_custom +// +// This implementation can be used at build time to replace the default implementation +// to avoid linking with [gopkg.in/yaml.v3]. +// +// In your test package: +// +// import assertYaml "github.com/CodSpeedHQ/codspeed-go/pkg/testify/assert/yaml" +// +// func init() { +// assertYaml.Unmarshal = func (in []byte, out interface{}) error { +// // ... +// return nil +// } +// } +package yaml + +var Unmarshal func(in []byte, out interface{}) error diff --git a/pkg/testify/assert/yaml/yaml_default.go b/pkg/testify/assert/yaml/yaml_default.go new file mode 100644 index 00000000..0bae80e3 --- /dev/null +++ b/pkg/testify/assert/yaml/yaml_default.go @@ -0,0 +1,36 @@ +//go:build !testify_yaml_fail && !testify_yaml_custom + +// Package yaml is just an indirection to handle YAML deserialization. +// +// This package is just an indirection that allows the builder to override the +// indirection with an alternative implementation of this package that uses +// another implementation of YAML deserialization. This allows to not either not +// use YAML deserialization at all, or to use another implementation than +// [gopkg.in/yaml.v3] (for example for license compatibility reasons, see [PR #1120]). +// +// Alternative implementations are selected using build tags: +// +// - testify_yaml_fail: [Unmarshal] always fails with an error +// - testify_yaml_custom: [Unmarshal] is a variable. Caller must initialize it +// before calling any of [github.com/stretchr/testify/assert.YAMLEq] or +// [github.com/stretchr/testify/assert.YAMLEqf]. +// +// Usage: +// +// go test -tags testify_yaml_fail +// +// You can check with "go list" which implementation is linked: +// +// go list -f '{{.Imports}}' github.com/stretchr/testify/assert/yaml +// go list -tags testify_yaml_fail -f '{{.Imports}}' github.com/stretchr/testify/assert/yaml +// go list -tags testify_yaml_custom -f '{{.Imports}}' github.com/stretchr/testify/assert/yaml +// +// [PR #1120]: https://github.com/stretchr/testify/pull/1120 +package yaml + +import goyaml "gopkg.in/yaml.v3" + +// Unmarshal is just a wrapper of [gopkg.in/yaml.v3.Unmarshal]. +func Unmarshal(in []byte, out interface{}) error { + return goyaml.Unmarshal(in, out) +} diff --git a/pkg/testify/assert/yaml/yaml_fail.go b/pkg/testify/assert/yaml/yaml_fail.go new file mode 100644 index 00000000..8041803f --- /dev/null +++ b/pkg/testify/assert/yaml/yaml_fail.go @@ -0,0 +1,17 @@ +//go:build testify_yaml_fail && !testify_yaml_custom && !testify_yaml_default + +// Package yaml is an implementation of YAML functions that always fail. +// +// This implementation can be used at build time to replace the default implementation +// to avoid linking with [gopkg.in/yaml.v3]: +// +// go test -tags testify_yaml_fail +package yaml + +import "errors" + +var errNotImplemented = errors.New("YAML functions are not available (see https://pkg.go.dev/github.com/stretchr/testify/assert/yaml)") + +func Unmarshal([]byte, interface{}) error { + return errNotImplemented +} diff --git a/pkg/testify/doc.go b/pkg/testify/doc.go new file mode 100644 index 00000000..2076714c --- /dev/null +++ b/pkg/testify/doc.go @@ -0,0 +1,17 @@ +// Module 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: +// +// The [github.com/stretchr/testify/assert] package provides a comprehensive set of assertion functions that tie in to [the Go testing system]. +// The [github.com/stretchr/testify/require] package provides the same assertions but as fatal checks. +// +// The [github.com/stretchr/testify/mock] package provides a system by which it is possible to mock your objects and verify calls are happening as expected. +// +// The [github.com/stretchr/testify/suite] package provides a basic structure for using structs as testing suites, and methods on those structs as tests. It includes setup/teardown functionality in the way of interfaces. +// +// A [golangci-lint] compatible linter for testify is available called [testifylint]. +// +// [the Go testing system]: https://go.dev/doc/code#Testing +// [golangci-lint]: https://golangci-lint.run/ +// [testifylint]: https://github.com/Antonboom/testifylint +package testify diff --git a/pkg/testify/http/doc.go b/pkg/testify/http/doc.go new file mode 100644 index 00000000..7c800aa2 --- /dev/null +++ b/pkg/testify/http/doc.go @@ -0,0 +1,2 @@ +// Deprecated: Use [net/http/httptest] instead. +package http diff --git a/pkg/testify/http/test_response_writer.go b/pkg/testify/http/test_response_writer.go new file mode 100644 index 00000000..6744e1c7 --- /dev/null +++ b/pkg/testify/http/test_response_writer.go @@ -0,0 +1,49 @@ +package http + +import ( + "net/http" +) + +// Deprecated: Use [net/http/httptest] instead. +type TestResponseWriter struct { + + // StatusCode is the last int written by the call to WriteHeader(int) + StatusCode int + + // Output is a string containing the written bytes using the Write([]byte) func. + Output string + + // header is the internal storage of the http.Header object + header http.Header +} + +// Deprecated: Use [net/http/httptest] instead. +func (rw *TestResponseWriter) Header() http.Header { + + if rw.header == nil { + rw.header = make(http.Header) + } + + return rw.header +} + +// Deprecated: Use [net/http/httptest] instead. +func (rw *TestResponseWriter) Write(bytes []byte) (int, error) { + + // assume 200 success if no header has been set + if rw.StatusCode == 0 { + rw.WriteHeader(200) + } + + // add these bytes to the output string + rw.Output += string(bytes) + + // return normal values + return 0, nil + +} + +// Deprecated: Use [net/http/httptest] instead. +func (rw *TestResponseWriter) WriteHeader(i int) { + rw.StatusCode = i +} diff --git a/pkg/testify/http/test_round_tripper.go b/pkg/testify/http/test_round_tripper.go new file mode 100644 index 00000000..acd8b38c --- /dev/null +++ b/pkg/testify/http/test_round_tripper.go @@ -0,0 +1,18 @@ +package http + +import ( + "net/http" + + "github.com/CodSpeedHQ/codspeed-go/pkg/testify/mock" +) + +// Deprecated: Use [net/http/httptest] instead. +type TestRoundTripper struct { + mock.Mock +} + +// Deprecated: Use [net/http/httptest] instead. +func (t *TestRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + args := t.Called(req) + return args.Get(0).(*http.Response), args.Error(1) +} diff --git a/pkg/testify/mock/doc.go b/pkg/testify/mock/doc.go new file mode 100644 index 00000000..d6b3c844 --- /dev/null +++ b/pkg/testify/mock/doc.go @@ -0,0 +1,44 @@ +// Package mock provides a system by which it is possible to mock your objects +// and verify calls are happening as expected. +// +// # Example Usage +// +// The mock package provides an object, Mock, that tracks activity on another object. It is usually +// embedded into a test object as shown below: +// +// type MyTestObject struct { +// // add a Mock object instance +// mock.Mock +// +// // other fields go here as normal +// } +// +// When implementing the methods of an interface, you wire your functions up +// to call the Mock.Called(args...) method, and return the appropriate values. +// +// For example, to mock a method that saves the name and age of a person and returns +// the year of their birth or an error, you might write this: +// +// func (o *MyTestObject) SavePersonDetails(firstname, lastname string, age int) (int, error) { +// args := o.Called(firstname, lastname, age) +// return args.Int(0), args.Error(1) +// } +// +// The Int, Error and Bool methods are examples of strongly typed getters that take the argument +// index position. Given this argument list: +// +// (12, true, "Something") +// +// You could read them out strongly typed like this: +// +// args.Int(0) +// args.Bool(1) +// args.String(2) +// +// For objects of your own type, use the generic Arguments.Get(index) method and make a type assertion: +// +// return args.Get(0).(*MyObject), args.Get(1).(*AnotherObjectOfMine) +// +// This may cause a panic if the object you are getting is nil (the type assertion will fail), in those +// cases you should check for nil first. +package mock diff --git a/pkg/testify/mock/mock.go b/pkg/testify/mock/mock.go new file mode 100644 index 00000000..38c3e9bf --- /dev/null +++ b/pkg/testify/mock/mock.go @@ -0,0 +1,1298 @@ +package mock + +import ( + "errors" + "fmt" + "path" + "reflect" + "regexp" + "runtime" + "strings" + "sync" + "time" + + "github.com/davecgh/go-spew/spew" + "github.com/pmezard/go-difflib/difflib" + "github.com/stretchr/objx" + + "github.com/CodSpeedHQ/codspeed-go/pkg/testify/assert" +) + +// regex for GCCGO functions +var gccgoRE = regexp.MustCompile(`\.pN\d+_`) + +// TestingT is an interface wrapper around *testing.T +type TestingT interface { + Logf(format string, args ...interface{}) + Errorf(format string, args ...interface{}) + FailNow() +} + +/* + Call +*/ + +// Call represents a method call and is used for setting expectations, +// as well as recording activity. +type Call struct { + Parent *Mock + + // The name of the method that was or will be called. + Method string + + // Holds the arguments of the method. + Arguments Arguments + + // Holds the arguments that should be returned when + // this method is called. + ReturnArguments Arguments + + // Holds the caller info for the On() call + callerInfo []string + + // The number of times to return the return arguments when setting + // expectations. 0 means to always return the value. + Repeatability int + + // Amount of times this call has been called + totalCalls int + + // Call to this method can be optional + optional bool + + // Holds a channel that will be used to block the Return until it either + // receives a message or is closed. nil means it returns immediately. + WaitFor <-chan time.Time + + waitTime time.Duration + + // Holds a handler used to manipulate arguments content that are passed by + // reference. It's useful when mocking methods such as unmarshalers or + // decoders. + RunFn func(Arguments) + + // PanicMsg holds msg to be used to mock panic on the function call + // if the PanicMsg is set to a non nil string the function call will panic + // irrespective of other settings + PanicMsg *string + + // Calls which must be satisfied before this call can be + requires []*Call +} + +func newCall(parent *Mock, methodName string, callerInfo []string, methodArguments Arguments, returnArguments Arguments) *Call { + return &Call{ + Parent: parent, + Method: methodName, + Arguments: methodArguments, + ReturnArguments: returnArguments, + callerInfo: callerInfo, + Repeatability: 0, + WaitFor: nil, + RunFn: nil, + PanicMsg: nil, + } +} + +func (c *Call) lock() { + c.Parent.mutex.Lock() +} + +func (c *Call) unlock() { + c.Parent.mutex.Unlock() +} + +// Return specifies the return arguments for the expectation. +// +// Mock.On("DoSomething").Return(errors.New("failed")) +func (c *Call) Return(returnArguments ...interface{}) *Call { + c.lock() + defer c.unlock() + + c.ReturnArguments = returnArguments + + return c +} + +// Panic specifies if the function call should fail and the panic message +// +// Mock.On("DoSomething").Panic("test panic") +func (c *Call) Panic(msg string) *Call { + c.lock() + defer c.unlock() + + c.PanicMsg = &msg + + return c +} + +// Once indicates that the mock should only return the value once. +// +// Mock.On("MyMethod", arg1, arg2).Return(returnArg1, returnArg2).Once() +func (c *Call) Once() *Call { + return c.Times(1) +} + +// Twice indicates that the mock should only return the value twice. +// +// Mock.On("MyMethod", arg1, arg2).Return(returnArg1, returnArg2).Twice() +func (c *Call) Twice() *Call { + return c.Times(2) +} + +// Times indicates that the mock should only return the indicated number +// of times. +// +// Mock.On("MyMethod", arg1, arg2).Return(returnArg1, returnArg2).Times(5) +func (c *Call) Times(i int) *Call { + c.lock() + defer c.unlock() + c.Repeatability = i + return c +} + +// WaitUntil sets the channel that will block the mock's return until its closed +// or a message is received. +// +// Mock.On("MyMethod", arg1, arg2).WaitUntil(time.After(time.Second)) +func (c *Call) WaitUntil(w <-chan time.Time) *Call { + c.lock() + defer c.unlock() + c.WaitFor = w + return c +} + +// After sets how long to block until the call returns +// +// Mock.On("MyMethod", arg1, arg2).After(time.Second) +func (c *Call) After(d time.Duration) *Call { + c.lock() + defer c.unlock() + c.waitTime = d + return c +} + +// Run sets a handler to be called before returning. It can be used when +// mocking a method (such as an unmarshaler) that takes a pointer to a struct and +// sets properties in such struct +// +// Mock.On("Unmarshal", AnythingOfType("*map[string]interface{}")).Return().Run(func(args Arguments) { +// arg := args.Get(0).(*map[string]interface{}) +// arg["foo"] = "bar" +// }) +func (c *Call) Run(fn func(args Arguments)) *Call { + c.lock() + defer c.unlock() + c.RunFn = fn + return c +} + +// Maybe allows the method call to be optional. Not calling an optional method +// will not cause an error while asserting expectations +func (c *Call) Maybe() *Call { + c.lock() + defer c.unlock() + c.optional = true + return c +} + +// On chains a new expectation description onto the mocked interface. This +// allows syntax like. +// +// Mock. +// On("MyMethod", 1).Return(nil). +// On("MyOtherMethod", 'a', 'b', 'c').Return(errors.New("Some Error")) +// +//go:noinline +func (c *Call) On(methodName string, arguments ...interface{}) *Call { + return c.Parent.On(methodName, arguments...) +} + +// Unset removes all mock handlers that satisfy the call instance arguments from being +// called. Only supported on call instances with static input arguments. +// +// For example, the only handler remaining after the following would be "MyMethod(2, 2)": +// +// Mock. +// On("MyMethod", 2, 2).Return(0). +// On("MyMethod", 3, 3).Return(0). +// On("MyMethod", Anything, Anything).Return(0) +// Mock.On("MyMethod", 3, 3).Unset() +func (c *Call) Unset() *Call { + var unlockOnce sync.Once + + for _, arg := range c.Arguments { + if v := reflect.ValueOf(arg); v.Kind() == reflect.Func { + panic(fmt.Sprintf("cannot use Func in expectations. Use mock.AnythingOfType(\"%T\")", arg)) + } + } + + c.lock() + defer unlockOnce.Do(c.unlock) + + foundMatchingCall := false + + // in-place filter slice for calls to be removed - iterate from 0'th to last skipping unnecessary ones + var index int // write index + for _, call := range c.Parent.ExpectedCalls { + if call.Method == c.Method { + _, diffCount := call.Arguments.Diff(c.Arguments) + if diffCount == 0 { + foundMatchingCall = true + // Remove from ExpectedCalls - just skip it + continue + } + } + c.Parent.ExpectedCalls[index] = call + index++ + } + // trim slice up to last copied index + c.Parent.ExpectedCalls = c.Parent.ExpectedCalls[:index] + + if !foundMatchingCall { + unlockOnce.Do(c.unlock) + c.Parent.fail("\n\nmock: Could not find expected call\n-----------------------------\n\n%s\n\n", + callString(c.Method, c.Arguments, true), + ) + } + + return c +} + +// NotBefore indicates that the mock should only be called after the referenced +// calls have been called as expected. The referenced calls may be from the +// same mock instance and/or other mock instances. +// +// Mock.On("Do").Return(nil).NotBefore( +// Mock.On("Init").Return(nil) +// ) +func (c *Call) NotBefore(calls ...*Call) *Call { + c.lock() + defer c.unlock() + + for _, call := range calls { + if call.Parent == nil { + panic("not before calls must be created with Mock.On()") + } + } + + c.requires = append(c.requires, calls...) + return c +} + +// InOrder defines the order in which the calls should be made +// +// For example: +// +// InOrder( +// Mock.On("init").Return(nil), +// Mock.On("Do").Return(nil), +// ) +func InOrder(calls ...*Call) { + for i := 1; i < len(calls); i++ { + calls[i].NotBefore(calls[i-1]) + } +} + +// Mock is the workhorse used to track activity on another object. +// For an example of its usage, refer to the "Example Usage" section at the top +// of this document. +type Mock struct { + // Represents the calls that are expected of + // an object. + ExpectedCalls []*Call + + // Holds the calls that were made to this mocked object. + Calls []Call + + // test is An optional variable that holds the test struct, to be used when an + // invalid mock call was made. + test TestingT + + // TestData holds any data that might be useful for testing. Testify ignores + // this data completely allowing you to do whatever you like with it. + testData objx.Map + + mutex sync.Mutex +} + +// String provides a %v format string for Mock. +// Note: this is used implicitly by Arguments.Diff if a Mock is passed. +// It exists because go's default %v formatting traverses the struct +// without acquiring the mutex, which is detected by go test -race. +func (m *Mock) String() string { + return fmt.Sprintf("%[1]T<%[1]p>", m) +} + +// TestData holds any data that might be useful for testing. Testify ignores +// this data completely allowing you to do whatever you like with it. +func (m *Mock) TestData() objx.Map { + if m.testData == nil { + m.testData = make(objx.Map) + } + + return m.testData +} + +/* + Setting expectations +*/ + +// Test sets the [TestingT] on which errors will be reported, otherwise errors +// will cause a panic. +// Test should not be called on an object that is going to be used in a +// goroutine other than the one running the test function. +func (m *Mock) Test(t TestingT) { + m.mutex.Lock() + defer m.mutex.Unlock() + m.test = t +} + +// fail fails the current test with the given formatted format and args. +// In case that a test was defined, it uses the test APIs for failing a test, +// otherwise it uses panic. +func (m *Mock) fail(format string, args ...interface{}) { + m.mutex.Lock() + defer m.mutex.Unlock() + + if m.test == nil { + panic(fmt.Sprintf(format, args...)) + } + m.test.Errorf(format, args...) + m.test.FailNow() +} + +// On starts a description of an expectation of the specified method +// being called. +// +// Mock.On("MyMethod", arg1, arg2) +func (m *Mock) On(methodName string, arguments ...interface{}) *Call { + for _, arg := range arguments { + if v := reflect.ValueOf(arg); v.Kind() == reflect.Func { + panic(fmt.Sprintf("cannot use Func in expectations. Use mock.AnythingOfType(\"%T\")", arg)) + } + } + + m.mutex.Lock() + defer m.mutex.Unlock() + + c := newCall(m, methodName, assert.CallerInfo(), arguments, make([]interface{}, 0)) + m.ExpectedCalls = append(m.ExpectedCalls, c) + return c +} + +// /* +// Recording and responding to activity +// */ + +func (m *Mock) findExpectedCall(method string, arguments ...interface{}) (int, *Call) { + var expectedCall *Call + + for i, call := range m.ExpectedCalls { + if call.Method == method { + _, diffCount := call.Arguments.Diff(arguments) + if diffCount == 0 { + expectedCall = call + if call.Repeatability > -1 { + return i, call + } + } + } + } + + return -1, expectedCall +} + +type matchCandidate struct { + call *Call + mismatch string + diffCount int +} + +func (c matchCandidate) isBetterMatchThan(other matchCandidate) bool { + if c.call == nil { + return false + } + if other.call == nil { + return true + } + + if c.diffCount > other.diffCount { + return false + } + if c.diffCount < other.diffCount { + return true + } + + if c.call.Repeatability > 0 && other.call.Repeatability <= 0 { + return true + } + return false +} + +func (m *Mock) findClosestCall(method string, arguments ...interface{}) (*Call, string) { + var bestMatch matchCandidate + + for _, call := range m.expectedCalls() { + if call.Method == method { + + errInfo, tempDiffCount := call.Arguments.Diff(arguments) + tempCandidate := matchCandidate{ + call: call, + mismatch: errInfo, + diffCount: tempDiffCount, + } + if tempCandidate.isBetterMatchThan(bestMatch) { + bestMatch = tempCandidate + } + } + } + + return bestMatch.call, bestMatch.mismatch +} + +func callString(method string, arguments Arguments, includeArgumentValues bool) string { + var argValsString string + if includeArgumentValues { + var argVals []string + for argIndex, arg := range arguments { + if _, ok := arg.(*FunctionalOptionsArgument); ok { + argVals = append(argVals, fmt.Sprintf("%d: %s", argIndex, arg)) + continue + } + argVals = append(argVals, fmt.Sprintf("%d: %#v", argIndex, arg)) + } + argValsString = fmt.Sprintf("\n\t\t%s", strings.Join(argVals, "\n\t\t")) + } + + return fmt.Sprintf("%s(%s)%s", method, arguments.String(), argValsString) +} + +// Called tells the mock object that a method has been called, and gets an array +// of arguments to return. Panics if the call is unexpected (i.e. not preceded by +// appropriate .On .Return() calls) +// If Call.WaitFor is set, blocks until the channel is closed or receives a message. +func (m *Mock) Called(arguments ...interface{}) Arguments { + // get the calling function's name + pc, _, _, ok := runtime.Caller(1) + if !ok { + panic("Couldn't get the caller information") + } + functionPath := runtime.FuncForPC(pc).Name() + // Next four lines are required to use GCCGO function naming conventions. + // For Ex: github_com_docker_libkv_store_mock.WatchTree.pN39_github_com_docker_libkv_store_mock.Mock + // uses interface information unlike golang github.com/docker/libkv/store/mock.(*Mock).WatchTree + // With GCCGO we need to remove interface information starting from pN
. + if gccgoRE.MatchString(functionPath) { + functionPath = gccgoRE.Split(functionPath, -1)[0] + } + parts := strings.Split(functionPath, ".") + functionName := parts[len(parts)-1] + return m.MethodCalled(functionName, arguments...) +} + +// MethodCalled tells the mock object that the given method has been called, and gets +// an array of arguments to return. Panics if the call is unexpected (i.e. not preceded +// by appropriate .On .Return() calls) +// If Call.WaitFor is set, blocks until the channel is closed or receives a message. +func (m *Mock) MethodCalled(methodName string, arguments ...interface{}) Arguments { + m.mutex.Lock() + // TODO: could combine expected and closes in single loop + found, call := m.findExpectedCall(methodName, arguments...) + + if found < 0 { + // expected call found, but it has already been called with repeatable times + if call != nil { + m.mutex.Unlock() + m.fail("\nassert: mock: The method has been called over %d times.\n\tEither do one more Mock.On(%#v).Return(...), or remove extra call.\n\tThis call was unexpected:\n\t\t%s\n\tat: %s", call.totalCalls, methodName, callString(methodName, arguments, true), assert.CallerInfo()) + } + // we have to fail here - because we don't know what to do + // as the return arguments. This is because: + // + // a) this is a totally unexpected call to this method, + // b) the arguments are not what was expected, or + // c) the developer has forgotten to add an accompanying On...Return pair. + closestCall, mismatch := m.findClosestCall(methodName, arguments...) + m.mutex.Unlock() + + if closestCall != nil { + m.fail("\n\nmock: Unexpected Method Call\n-----------------------------\n\n%s\n\nThe closest call I have is: \n\n%s\n\n%s\nDiff: %s\nat: %s\n", + callString(methodName, arguments, true), + callString(methodName, closestCall.Arguments, true), + diffArguments(closestCall.Arguments, arguments), + strings.TrimSpace(mismatch), + assert.CallerInfo(), + ) + } else { + m.fail("\nassert: mock: I don't know what to return because the method call was unexpected.\n\tEither do Mock.On(%#v).Return(...) first, or remove the %s() call.\n\tThis method was unexpected:\n\t\t%s\n\tat: %s", methodName, methodName, callString(methodName, arguments, true), assert.CallerInfo()) + } + } + + for _, requirement := range call.requires { + if satisfied, _ := requirement.Parent.checkExpectation(requirement); !satisfied { + m.mutex.Unlock() + m.fail("mock: Unexpected Method Call\n-----------------------------\n\n%s\n\nMust not be called before%s:\n\n%s", + callString(call.Method, call.Arguments, true), + func() (s string) { + if requirement.totalCalls > 0 { + s = " another call of" + } + if call.Parent != requirement.Parent { + s += " method from another mock instance" + } + return + }(), + callString(requirement.Method, requirement.Arguments, true), + ) + } + } + + if call.Repeatability == 1 { + call.Repeatability = -1 + } else if call.Repeatability > 1 { + call.Repeatability-- + } + call.totalCalls++ + + // add the call + m.Calls = append(m.Calls, *newCall(m, methodName, assert.CallerInfo(), arguments, call.ReturnArguments)) + m.mutex.Unlock() + + // block if specified + if call.WaitFor != nil { + <-call.WaitFor + } else { + time.Sleep(call.waitTime) + } + + m.mutex.Lock() + panicMsg := call.PanicMsg + m.mutex.Unlock() + if panicMsg != nil { + panic(*panicMsg) + } + + m.mutex.Lock() + runFn := call.RunFn + m.mutex.Unlock() + + if runFn != nil { + runFn(arguments) + } + + m.mutex.Lock() + returnArgs := call.ReturnArguments + m.mutex.Unlock() + + return returnArgs +} + +/* + Assertions +*/ + +type assertExpectationiser interface { + AssertExpectations(TestingT) bool +} + +// AssertExpectationsForObjects asserts that everything specified with On and Return +// of the specified objects was in fact called as expected. +// +// Calls may have occurred in any order. +func AssertExpectationsForObjects(t TestingT, testObjects ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + for _, obj := range testObjects { + if m, ok := obj.(*Mock); ok { + t.Logf("Deprecated mock.AssertExpectationsForObjects(myMock.Mock) use mock.AssertExpectationsForObjects(myMock)") + obj = m + } + m := obj.(assertExpectationiser) + if !m.AssertExpectations(t) { + t.Logf("Expectations didn't match for Mock: %+v", reflect.TypeOf(m)) + return false + } + } + return true +} + +// AssertExpectations asserts that everything specified with On and Return was +// in fact called as expected. Calls may have occurred in any order. +func (m *Mock) AssertExpectations(t TestingT) bool { + if s, ok := t.(interface{ Skipped() bool }); ok && s.Skipped() { + return true + } + if h, ok := t.(tHelper); ok { + h.Helper() + } + + m.mutex.Lock() + defer m.mutex.Unlock() + var failedExpectations int + + // iterate through each expectation + expectedCalls := m.expectedCalls() + for _, expectedCall := range expectedCalls { + satisfied, reason := m.checkExpectation(expectedCall) + if !satisfied { + failedExpectations++ + t.Logf(reason) + } + } + + if failedExpectations != 0 { + t.Errorf("FAIL: %d out of %d expectation(s) were met.\n\tThe code you are testing needs to make %d more call(s).\n\tat: %s", len(expectedCalls)-failedExpectations, len(expectedCalls), failedExpectations, assert.CallerInfo()) + } + + return failedExpectations == 0 +} + +func (m *Mock) checkExpectation(call *Call) (bool, string) { + if !call.optional && !m.methodWasCalled(call.Method, call.Arguments) && call.totalCalls == 0 { + return false, fmt.Sprintf("FAIL:\t%s(%s)\n\t\tat: %s", call.Method, call.Arguments.String(), call.callerInfo) + } + if call.Repeatability > 0 { + return false, fmt.Sprintf("FAIL:\t%s(%s)\n\t\tat: %s", call.Method, call.Arguments.String(), call.callerInfo) + } + return true, fmt.Sprintf("PASS:\t%s(%s)", call.Method, call.Arguments.String()) +} + +// AssertNumberOfCalls asserts that the method was called expectedCalls times. +func (m *Mock) AssertNumberOfCalls(t TestingT, methodName string, expectedCalls int) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + m.mutex.Lock() + defer m.mutex.Unlock() + var actualCalls int + for _, call := range m.calls() { + if call.Method == methodName { + actualCalls++ + } + } + return assert.Equal(t, expectedCalls, actualCalls, fmt.Sprintf("Expected number of calls (%d) of method %s does not match the actual number of calls (%d).", expectedCalls, methodName, actualCalls)) +} + +// AssertCalled asserts that the method was called. +// It can produce a false result when an argument is a pointer type and the underlying value changed after calling the mocked method. +func (m *Mock) AssertCalled(t TestingT, methodName string, arguments ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + m.mutex.Lock() + defer m.mutex.Unlock() + if !m.methodWasCalled(methodName, arguments) { + var calledWithArgs []string + for _, call := range m.calls() { + calledWithArgs = append(calledWithArgs, fmt.Sprintf("%v", call.Arguments)) + } + if len(calledWithArgs) == 0 { + return assert.Fail(t, "Should have called with given arguments", + fmt.Sprintf("Expected %q to have been called with:\n%v\nbut no actual calls happened", methodName, arguments)) + } + return assert.Fail(t, "Should have called with given arguments", + fmt.Sprintf("Expected %q to have been called with:\n%v\nbut actual calls were:\n %v", methodName, arguments, strings.Join(calledWithArgs, "\n"))) + } + return true +} + +// AssertNotCalled asserts that the method was not called. +// It can produce a false result when an argument is a pointer type and the underlying value changed after calling the mocked method. +func (m *Mock) AssertNotCalled(t TestingT, methodName string, arguments ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + m.mutex.Lock() + defer m.mutex.Unlock() + if m.methodWasCalled(methodName, arguments) { + return assert.Fail(t, "Should not have called with given arguments", + fmt.Sprintf("Expected %q to not have been called with:\n%v\nbut actually it was.", methodName, arguments)) + } + return true +} + +// IsMethodCallable checking that the method can be called +// If the method was called more than `Repeatability` return false +func (m *Mock) IsMethodCallable(t TestingT, methodName string, arguments ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + m.mutex.Lock() + defer m.mutex.Unlock() + + for _, v := range m.ExpectedCalls { + if v.Method != methodName { + continue + } + if len(arguments) != len(v.Arguments) { + continue + } + if v.Repeatability < v.totalCalls { + continue + } + if isArgsEqual(v.Arguments, arguments) { + return true + } + } + return false +} + +// isArgsEqual compares arguments +func isArgsEqual(expected Arguments, args []interface{}) bool { + if len(expected) != len(args) { + return false + } + for i, v := range args { + if !reflect.DeepEqual(expected[i], v) { + return false + } + } + return true +} + +func (m *Mock) methodWasCalled(methodName string, expected []interface{}) bool { + for _, call := range m.calls() { + if call.Method == methodName { + + _, differences := Arguments(expected).Diff(call.Arguments) + + if differences == 0 { + // found the expected call + return true + } + + } + } + // we didn't find the expected call + return false +} + +func (m *Mock) expectedCalls() []*Call { + return append([]*Call{}, m.ExpectedCalls...) +} + +func (m *Mock) calls() []Call { + return append([]Call{}, m.Calls...) +} + +/* + Arguments +*/ + +// Arguments holds an array of method arguments or return values. +type Arguments []interface{} + +const ( + // Anything is used in Diff and Assert when the argument being tested + // shouldn't be taken into consideration. + Anything = "mock.Anything" +) + +// AnythingOfTypeArgument contains the type of an argument +// for use when type checking. Used in [Arguments.Diff] and [Arguments.Assert]. +// +// Deprecated: this is an implementation detail that must not be used. Use the [AnythingOfType] constructor instead, example: +// +// m.On("Do", mock.AnythingOfType("string")) +// +// All explicit type declarations can be replaced with interface{} as is expected by [Mock.On], example: +// +// func anyString interface{} { +// return mock.AnythingOfType("string") +// } +type AnythingOfTypeArgument = anythingOfTypeArgument + +// anythingOfTypeArgument is a string that contains the type of an argument +// for use when type checking. Used in Diff and Assert. +type anythingOfTypeArgument string + +// AnythingOfType returns a special value containing the +// name of the type to check for. The type name will be matched against the type name returned by [reflect.Type.String]. +// +// Used in Diff and Assert. +// +// For example: +// +// args.Assert(t, AnythingOfType("string"), AnythingOfType("int")) +func AnythingOfType(t string) AnythingOfTypeArgument { + return anythingOfTypeArgument(t) +} + +// IsTypeArgument is a struct that contains the type of an argument +// for use when type checking. This is an alternative to [AnythingOfType]. +// Used in [Arguments.Diff] and [Arguments.Assert]. +type IsTypeArgument struct { + t reflect.Type +} + +// IsType returns an IsTypeArgument object containing the type to check for. +// You can provide a zero-value of the type to check. This is an +// alternative to [AnythingOfType]. Used in [Arguments.Diff] and [Arguments.Assert]. +// +// For example: +// +// args.Assert(t, IsType(""), IsType(0)) +func IsType(t interface{}) *IsTypeArgument { + return &IsTypeArgument{t: reflect.TypeOf(t)} +} + +// FunctionalOptionsArgument contains a list of functional options arguments +// expected for use when matching a list of arguments. +type FunctionalOptionsArgument struct { + values []interface{} +} + +// String returns the string representation of FunctionalOptionsArgument +func (f *FunctionalOptionsArgument) String() string { + var name string + if len(f.values) > 0 { + name = "[]" + reflect.TypeOf(f.values[0]).String() + } + + return strings.Replace(fmt.Sprintf("%#v", f.values), "[]interface {}", name, 1) +} + +// FunctionalOptions returns an [FunctionalOptionsArgument] object containing +// the expected functional-options to check for. +// +// For example: +// +// args.Assert(t, FunctionalOptions(foo.Opt1("strValue"), foo.Opt2(613))) +func FunctionalOptions(values ...interface{}) *FunctionalOptionsArgument { + return &FunctionalOptionsArgument{ + values: values, + } +} + +// argumentMatcher performs custom argument matching, returning whether or +// not the argument is matched by the expectation fixture function. +type argumentMatcher struct { + // fn is a function which accepts one argument, and returns a bool. + fn reflect.Value +} + +func (f argumentMatcher) Matches(argument interface{}) bool { + expectType := f.fn.Type().In(0) + expectTypeNilSupported := false + switch expectType.Kind() { + case reflect.Interface, reflect.Chan, reflect.Func, reflect.Map, reflect.Slice, reflect.Ptr: + expectTypeNilSupported = true + } + + argType := reflect.TypeOf(argument) + var arg reflect.Value + if argType == nil { + arg = reflect.New(expectType).Elem() + } else { + arg = reflect.ValueOf(argument) + } + + if argType == nil && !expectTypeNilSupported { + panic(errors.New("attempting to call matcher with nil for non-nil expected type")) + } + if argType == nil || argType.AssignableTo(expectType) { + result := f.fn.Call([]reflect.Value{arg}) + return result[0].Bool() + } + return false +} + +func (f argumentMatcher) String() string { + return fmt.Sprintf("func(%s) bool", f.fn.Type().In(0).String()) +} + +// MatchedBy can be used to match a mock call based on only certain properties +// from a complex struct or some calculation. It takes a function that will be +// evaluated with the called argument and will return true when there's a match +// and false otherwise. +// +// Example: +// +// m.On("Do", MatchedBy(func(req *http.Request) bool { return req.Host == "example.com" })) +// +// fn must be a function accepting a single argument (of the expected type) +// which returns a bool. If fn doesn't match the required signature, +// MatchedBy() panics. +func MatchedBy(fn interface{}) argumentMatcher { + fnType := reflect.TypeOf(fn) + + if fnType.Kind() != reflect.Func { + panic(fmt.Sprintf("assert: arguments: %s is not a func", fn)) + } + if fnType.NumIn() != 1 { + panic(fmt.Sprintf("assert: arguments: %s does not take exactly one argument", fn)) + } + if fnType.NumOut() != 1 || fnType.Out(0).Kind() != reflect.Bool { + panic(fmt.Sprintf("assert: arguments: %s does not return a bool", fn)) + } + + return argumentMatcher{fn: reflect.ValueOf(fn)} +} + +// Get Returns the argument at the specified index. +func (args Arguments) Get(index int) interface{} { + if index+1 > len(args) { + panic(fmt.Sprintf("assert: arguments: Cannot call Get(%d) because there are %d argument(s).", index, len(args))) + } + return args[index] +} + +// Is gets whether the objects match the arguments specified. +func (args Arguments) Is(objects ...interface{}) bool { + for i, obj := range args { + if obj != objects[i] { + return false + } + } + return true +} + +// Diff gets a string describing the differences between the arguments +// and the specified objects. +// +// Returns the diff string and number of differences found. +func (args Arguments) Diff(objects []interface{}) (string, int) { + // TODO: could return string as error and nil for No difference + + output := "\n" + var differences int + + maxArgCount := len(args) + if len(objects) > maxArgCount { + maxArgCount = len(objects) + } + + for i := 0; i < maxArgCount; i++ { + var actual, expected interface{} + var actualFmt, expectedFmt string + + if len(objects) <= i { + actual = "(Missing)" + actualFmt = "(Missing)" + } else { + actual = objects[i] + actualFmt = fmt.Sprintf("(%[1]T=%[1]v)", actual) + } + + if len(args) <= i { + expected = "(Missing)" + expectedFmt = "(Missing)" + } else { + expected = args[i] + expectedFmt = fmt.Sprintf("(%[1]T=%[1]v)", expected) + } + + if matcher, ok := expected.(argumentMatcher); ok { + var matches bool + func() { + defer func() { + if r := recover(); r != nil { + actualFmt = fmt.Sprintf("panic in argument matcher: %v", r) + } + }() + matches = matcher.Matches(actual) + }() + if matches { + output = fmt.Sprintf("%s\t%d: PASS: %s matched by %s\n", output, i, actualFmt, matcher) + } else { + differences++ + output = fmt.Sprintf("%s\t%d: FAIL: %s not matched by %s\n", output, i, actualFmt, matcher) + } + } else { + switch expected := expected.(type) { + case anythingOfTypeArgument: + // type checking + if reflect.TypeOf(actual).Name() != string(expected) && reflect.TypeOf(actual).String() != string(expected) { + // not match + differences++ + output = fmt.Sprintf("%s\t%d: FAIL: type %s != type %s - %s\n", output, i, expected, reflect.TypeOf(actual).Name(), actualFmt) + } + case *IsTypeArgument: + actualT := reflect.TypeOf(actual) + if actualT != expected.t { + differences++ + output = fmt.Sprintf("%s\t%d: FAIL: type %s != type %s - %s\n", output, i, expected.t.Name(), actualT.Name(), actualFmt) + } + case *FunctionalOptionsArgument: + var name string + if len(expected.values) > 0 { + name = "[]" + reflect.TypeOf(expected.values[0]).String() + } + + const tName = "[]interface{}" + if name != reflect.TypeOf(actual).String() && len(expected.values) != 0 { + differences++ + output = fmt.Sprintf("%s\t%d: FAIL: type %s != type %s - %s\n", output, i, tName, reflect.TypeOf(actual).Name(), actualFmt) + } else { + if ef, af := assertOpts(expected.values, actual); ef == "" && af == "" { + // match + output = fmt.Sprintf("%s\t%d: PASS: %s == %s\n", output, i, tName, tName) + } else { + // not match + differences++ + output = fmt.Sprintf("%s\t%d: FAIL: %s != %s\n", output, i, af, ef) + } + } + + default: + if assert.ObjectsAreEqual(expected, Anything) || assert.ObjectsAreEqual(actual, Anything) || assert.ObjectsAreEqual(actual, expected) { + // match + output = fmt.Sprintf("%s\t%d: PASS: %s == %s\n", output, i, actualFmt, expectedFmt) + } else { + // not match + differences++ + output = fmt.Sprintf("%s\t%d: FAIL: %s != %s\n", output, i, actualFmt, expectedFmt) + } + } + } + + } + + if differences == 0 { + return "No differences.", differences + } + + return output, differences +} + +// Assert compares the arguments with the specified objects and fails if +// they do not exactly match. +func (args Arguments) Assert(t TestingT, objects ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + + // get the differences + diff, diffCount := args.Diff(objects) + + if diffCount == 0 { + return true + } + + // there are differences... report them... + t.Logf(diff) + t.Errorf("%sArguments do not match.", assert.CallerInfo()) + + return false +} + +// String gets the argument at the specified index. Panics if there is no argument, or +// if the argument is of the wrong type. +// +// If no index is provided, String() returns a complete string representation +// of the arguments. +func (args Arguments) String(indexOrNil ...int) string { + if len(indexOrNil) == 0 { + // normal String() method - return a string representation of the args + var argsStr []string + for _, arg := range args { + argsStr = append(argsStr, fmt.Sprintf("%T", arg)) // handles nil nicely + } + return strings.Join(argsStr, ",") + } else if len(indexOrNil) == 1 { + // Index has been specified - get the argument at that index + index := indexOrNil[0] + var s string + var ok bool + if s, ok = args.Get(index).(string); !ok { + panic(fmt.Sprintf("assert: arguments: String(%d) failed because object wasn't correct type: %s", index, args.Get(index))) + } + return s + } + + panic(fmt.Sprintf("assert: arguments: Wrong number of arguments passed to String. Must be 0 or 1, not %d", len(indexOrNil))) +} + +// Int gets the argument at the specified index. Panics if there is no argument, or +// if the argument is of the wrong type. +func (args Arguments) Int(index int) int { + var s int + var ok bool + if s, ok = args.Get(index).(int); !ok { + panic(fmt.Sprintf("assert: arguments: Int(%d) failed because object wasn't correct type: %v", index, args.Get(index))) + } + return s +} + +// Error gets the argument at the specified index. Panics if there is no argument, or +// if the argument is of the wrong type. +func (args Arguments) Error(index int) error { + obj := args.Get(index) + var s error + var ok bool + if obj == nil { + return nil + } + if s, ok = obj.(error); !ok { + panic(fmt.Sprintf("assert: arguments: Error(%d) failed because object wasn't correct type: %v", index, obj)) + } + return s +} + +// Bool gets the argument at the specified index. Panics if there is no argument, or +// if the argument is of the wrong type. +func (args Arguments) Bool(index int) bool { + var s bool + var ok bool + if s, ok = args.Get(index).(bool); !ok { + panic(fmt.Sprintf("assert: arguments: Bool(%d) failed because object wasn't correct type: %v", index, args.Get(index))) + } + return s +} + +func typeAndKind(v interface{}) (reflect.Type, reflect.Kind) { + t := reflect.TypeOf(v) + k := t.Kind() + + if k == reflect.Ptr { + t = t.Elem() + k = t.Kind() + } + return t, k +} + +func diffArguments(expected Arguments, actual Arguments) string { + if len(expected) != len(actual) { + return fmt.Sprintf("Provided %v arguments, mocked for %v arguments", len(expected), len(actual)) + } + + for x := range expected { + if diffString := diff(expected[x], actual[x]); diffString != "" { + return fmt.Sprintf("Difference found in argument %v:\n\n%s", x, diffString) + } + } + + return "" +} + +// diff returns a diff of both values as long as both are of the same type and +// are a struct, map, slice or array. Otherwise it returns an empty string. +func diff(expected interface{}, actual interface{}) string { + if expected == nil || actual == nil { + return "" + } + + et, ek := typeAndKind(expected) + at, _ := typeAndKind(actual) + + if et != at { + return "" + } + + if ek != reflect.Struct && ek != reflect.Map && ek != reflect.Slice && ek != reflect.Array { + return "" + } + + e := spewConfig.Sdump(expected) + a := spewConfig.Sdump(actual) + + diff, _ := difflib.GetUnifiedDiffString(difflib.UnifiedDiff{ + A: difflib.SplitLines(e), + B: difflib.SplitLines(a), + FromFile: "Expected", + FromDate: "", + ToFile: "Actual", + ToDate: "", + Context: 1, + }) + + return diff +} + +var spewConfig = spew.ConfigState{ + Indent: " ", + DisablePointerAddresses: true, + DisableCapacities: true, + SortKeys: true, +} + +type tHelper interface { + Helper() +} + +func assertOpts(expected, actual interface{}) (expectedFmt, actualFmt string) { + expectedOpts := reflect.ValueOf(expected) + actualOpts := reflect.ValueOf(actual) + + var expectedFuncs []*runtime.Func + var expectedNames []string + for i := 0; i < expectedOpts.Len(); i++ { + f := runtimeFunc(expectedOpts.Index(i).Interface()) + expectedFuncs = append(expectedFuncs, f) + expectedNames = append(expectedNames, funcName(f)) + } + var actualFuncs []*runtime.Func + var actualNames []string + for i := 0; i < actualOpts.Len(); i++ { + f := runtimeFunc(actualOpts.Index(i).Interface()) + actualFuncs = append(actualFuncs, f) + actualNames = append(actualNames, funcName(f)) + } + + if expectedOpts.Len() != actualOpts.Len() { + expectedFmt = fmt.Sprintf("%v", expectedNames) + actualFmt = fmt.Sprintf("%v", actualNames) + return + } + + for i := 0; i < expectedOpts.Len(); i++ { + if !isFuncSame(expectedFuncs[i], actualFuncs[i]) { + expectedFmt = expectedNames[i] + actualFmt = actualNames[i] + return + } + + expectedOpt := expectedOpts.Index(i).Interface() + actualOpt := actualOpts.Index(i).Interface() + + ot := reflect.TypeOf(expectedOpt) + var expectedValues []reflect.Value + var actualValues []reflect.Value + if ot.NumIn() == 0 { + return + } + + for i := 0; i < ot.NumIn(); i++ { + vt := ot.In(i).Elem() + expectedValues = append(expectedValues, reflect.New(vt)) + actualValues = append(actualValues, reflect.New(vt)) + } + + reflect.ValueOf(expectedOpt).Call(expectedValues) + reflect.ValueOf(actualOpt).Call(actualValues) + + for i := 0; i < ot.NumIn(); i++ { + if expectedArg, actualArg := expectedValues[i].Interface(), actualValues[i].Interface(); !assert.ObjectsAreEqual(expectedArg, actualArg) { + expectedFmt = fmt.Sprintf("%s(%T) -> %#v", expectedNames[i], expectedArg, expectedArg) + actualFmt = fmt.Sprintf("%s(%T) -> %#v", expectedNames[i], actualArg, actualArg) + return + } + } + } + + return "", "" +} + +func runtimeFunc(opt interface{}) *runtime.Func { + return runtime.FuncForPC(reflect.ValueOf(opt).Pointer()) +} + +func funcName(f *runtime.Func) string { + name := f.Name() + trimmed := strings.TrimSuffix(path.Base(name), path.Ext(name)) + splitted := strings.Split(trimmed, ".") + + if len(splitted) == 0 { + return trimmed + } + + return splitted[len(splitted)-1] +} + +func isFuncSame(f1, f2 *runtime.Func) bool { + f1File, f1Loc := f1.FileLine(f1.Entry()) + f2File, f2Loc := f2.FileLine(f2.Entry()) + + return f1File == f2File && f1Loc == f2Loc +} diff --git a/pkg/testify/mock/mock_test.go b/pkg/testify/mock/mock_test.go new file mode 100644 index 00000000..fa1c305e --- /dev/null +++ b/pkg/testify/mock/mock_test.go @@ -0,0 +1,2443 @@ +package mock + +import ( + "errors" + "fmt" + testing "github.com/CodSpeedHQ/codspeed-go/testing/testing" + "regexp" + "runtime" + "strconv" + "sync" + "time" + + "github.com/CodSpeedHQ/codspeed-go/pkg/testify/assert" + "github.com/CodSpeedHQ/codspeed-go/pkg/testify/require" +) + +/* + Test objects +*/ + +// ExampleInterface represents an example interface. +type ExampleInterface interface { + TheExampleMethod(a, b, c int) (int, error) +} + +// TestExampleImplementation is a test implementation of ExampleInterface +type TestExampleImplementation struct { + Mock +} + +func (i *TestExampleImplementation) TheExampleMethod(a, b, c int) (int, error) { + args := i.Called(a, b, c) + return args.Int(0), errors.New("Whoops") +} + +type options struct { + num int + str string +} + +type OptionFn func(*options) + +func OpNum(n int) OptionFn { + return func(o *options) { + o.num = n + } +} + +func OpStr(s string) OptionFn { + return func(o *options) { + o.str = s + } +} + +func OpBytes(b []byte) OptionFn { + return func(m *options) { + m.str = string(b) + } +} + +func (i *TestExampleImplementation) TheExampleMethodFunctionalOptions(x string, opts ...OptionFn) error { + args := i.Called(x, opts) + return args.Error(0) +} + +func TheExampleMethodFunctionalOptionsIndirect(i *TestExampleImplementation) { + i.TheExampleMethodFunctionalOptions("test", OpNum(1), OpStr("foo")) +} + +//go:noinline +func (i *TestExampleImplementation) TheExampleMethod2(yesorno bool) { + i.Called(yesorno) +} + +type ExampleType struct { + ran bool +} + +func (i *TestExampleImplementation) TheExampleMethod3(et *ExampleType) error { + args := i.Called(et) + return args.Error(0) +} + +func (i *TestExampleImplementation) TheExampleMethod4(v ExampleInterface) error { + args := i.Called(v) + return args.Error(0) +} + +func (i *TestExampleImplementation) TheExampleMethod5(ch chan struct{}) error { + args := i.Called(ch) + return args.Error(0) +} + +func (i *TestExampleImplementation) TheExampleMethod6(m map[string]bool) error { + args := i.Called(m) + return args.Error(0) +} + +func (i *TestExampleImplementation) TheExampleMethod7(slice []bool) error { + args := i.Called(slice) + return args.Error(0) +} + +func (i *TestExampleImplementation) TheExampleMethodFunc(fn func(string) error) error { + args := i.Called(fn) + return args.Error(0) +} + +func (i *TestExampleImplementation) TheExampleMethodVariadic(a ...int) error { + args := i.Called(a) + return args.Error(0) +} + +func (i *TestExampleImplementation) TheExampleMethodVariadicInterface(a ...interface{}) error { + args := i.Called(a) + return args.Error(0) +} + +func (i *TestExampleImplementation) TheExampleMethodMixedVariadic(a int, b ...int) error { + args := i.Called(a, b) + return args.Error(0) +} + +type ExampleFuncType func(string) error + +func (i *TestExampleImplementation) TheExampleMethodFuncType(fn ExampleFuncType) error { + args := i.Called(fn) + return args.Error(0) +} + +// MockTestingT mocks a test struct +type MockTestingT struct { + logfCount, errorfCount, failNowCount int +} + +// Helper is like [testing.T.Helper] but does nothing. +func (MockTestingT) Helper() {} + +const mockTestingTFailNowCalled = "FailNow was called" + +func (m *MockTestingT) Logf(string, ...interface{}) { + m.logfCount++ +} + +func (m *MockTestingT) Errorf(string, ...interface{}) { + m.errorfCount++ +} + +// FailNow mocks the FailNow call. +// It panics in order to mimic the FailNow behavior in the sense that +// the execution stops. +// When expecting this method, the call that invokes it should use the following code: +// +// assert.PanicsWithValue(t, mockTestingTFailNowCalled, func() {...}) +func (m *MockTestingT) FailNow() { + m.failNowCount++ + + // this function should panic now to stop the execution as expected + panic(mockTestingTFailNowCalled) +} + +/* + Mock +*/ + +func Test_Mock_TestData(t *testing.T) { + t.Parallel() + + var mockedService = new(TestExampleImplementation) + + if assert.NotNil(t, mockedService.TestData()) { + + mockedService.TestData().Set("something", 123) + assert.Equal(t, 123, mockedService.TestData().Get("something").Data()) + } +} + +func Test_Mock_On(t *testing.T) { + t.Parallel() + + // make a test impl object + var mockedService = new(TestExampleImplementation) + + c := mockedService.On("TheExampleMethod") + assert.Equal(t, []*Call{c}, mockedService.ExpectedCalls) + assert.Equal(t, "TheExampleMethod", c.Method) +} + +func Test_Mock_Chained_On(t *testing.T) { + t.Parallel() + + // make a test impl object + var mockedService = new(TestExampleImplementation) + + // determine our current line number so we can assert the expected calls callerInfo properly + _, filename, line, _ := runtime.Caller(0) + mockedService. + On("TheExampleMethod", 1, 2, 3). + Return(0). + On("TheExampleMethod3", AnythingOfType("*mock.ExampleType")). + Return(nil) + + expectedCalls := []*Call{ + { + Parent: &mockedService.Mock, + Method: "TheExampleMethod", + Arguments: []interface{}{1, 2, 3}, + ReturnArguments: []interface{}{0}, + callerInfo: []string{fmt.Sprintf("%s:%d", filename, line+2)}, + }, + { + Parent: &mockedService.Mock, + Method: "TheExampleMethod3", + Arguments: []interface{}{AnythingOfType("*mock.ExampleType")}, + ReturnArguments: []interface{}{nil}, + callerInfo: []string{fmt.Sprintf("%s:%d", filename, line+4)}, + }, + } + assert.Equal(t, expectedCalls, mockedService.ExpectedCalls) +} + +func Test_Mock_On_WithArgs(t *testing.T) { + t.Parallel() + + // make a test impl object + var mockedService = new(TestExampleImplementation) + + c := mockedService.On("TheExampleMethod", 1, 2, 3, 4) + + assert.Equal(t, []*Call{c}, mockedService.ExpectedCalls) + assert.Equal(t, "TheExampleMethod", c.Method) + assert.Equal(t, Arguments{1, 2, 3, 4}, c.Arguments) +} + +func Test_Mock_On_WithFuncArg(t *testing.T) { + t.Parallel() + + // make a test impl object + var mockedService = new(TestExampleImplementation) + + c := mockedService. + On("TheExampleMethodFunc", AnythingOfType("func(string) error")). + Return(nil) + + assert.Equal(t, []*Call{c}, mockedService.ExpectedCalls) + assert.Equal(t, "TheExampleMethodFunc", c.Method) + assert.Equal(t, 1, len(c.Arguments)) + assert.Equal(t, AnythingOfType("func(string) error"), c.Arguments[0]) + + fn := func(string) error { return nil } + + assert.NotPanics(t, func() { + mockedService.TheExampleMethodFunc(fn) + }) +} + +func Test_Mock_On_WithIntArgMatcher(t *testing.T) { + t.Parallel() + + var mockedService TestExampleImplementation + + mockedService.On("TheExampleMethod", + MatchedBy(func(a int) bool { + return a == 1 + }), MatchedBy(func(b int) bool { + return b == 2 + }), MatchedBy(func(c int) bool { + return c == 3 + })).Return(0, nil) + + assert.Panics(t, func() { + mockedService.TheExampleMethod(1, 2, 4) + }) + assert.Panics(t, func() { + mockedService.TheExampleMethod(2, 2, 3) + }) + assert.NotPanics(t, func() { + mockedService.TheExampleMethod(1, 2, 3) + }) +} + +func Test_Mock_On_WithArgMatcherThatPanics(t *testing.T) { + t.Parallel() + + var mockedService TestExampleImplementation + + mockedService.On("TheExampleMethod2", MatchedBy(func(_ interface{}) bool { + panic("try to lock mockedService") + })).Return() + + defer func() { + assertedExpectations := make(chan struct{}) + go func() { + tt := new(testing.T) + mockedService.AssertExpectations(tt) + close(assertedExpectations) + }() + select { + case <-assertedExpectations: + case <-time.After(time.Second): + t.Fatal("AssertExpectations() deadlocked, did the panic leave mockedService locked?") + } + }() + + assert.Panics(t, func() { + mockedService.TheExampleMethod2(false) + }) +} + +func TestMock_WithTest(t *testing.T) { + t.Parallel() + + var ( + mockedService TestExampleImplementation + mockedTest MockTestingT + ) + + mockedService.Test(&mockedTest) + mockedService.On("TheExampleMethod", 1, 2, 3).Return(0, nil) + + // Test that on an expected call, the test was not failed + + mockedService.TheExampleMethod(1, 2, 3) + + // Assert that Errorf and FailNow were not called + assert.Equal(t, 0, mockedTest.errorfCount) + assert.Equal(t, 0, mockedTest.failNowCount) + + // Test that on unexpected call, the mocked test was called to fail the test + + assert.PanicsWithValue(t, mockTestingTFailNowCalled, func() { + mockedService.TheExampleMethod(1, 1, 1) + }) + + // Assert that Errorf and FailNow were called once + assert.Equal(t, 1, mockedTest.errorfCount) + assert.Equal(t, 1, mockedTest.failNowCount) +} + +func Test_Mock_On_WithPtrArgMatcher(t *testing.T) { + t.Parallel() + + var mockedService TestExampleImplementation + + mockedService.On("TheExampleMethod3", + MatchedBy(func(a *ExampleType) bool { return a != nil && a.ran == true }), + ).Return(nil) + + mockedService.On("TheExampleMethod3", + MatchedBy(func(a *ExampleType) bool { return a != nil && a.ran == false }), + ).Return(errors.New("error")) + + mockedService.On("TheExampleMethod3", + MatchedBy(func(a *ExampleType) bool { return a == nil }), + ).Return(errors.New("error2")) + + assert.Equal(t, mockedService.TheExampleMethod3(&ExampleType{true}), nil) + assert.EqualError(t, mockedService.TheExampleMethod3(&ExampleType{false}), "error") + assert.EqualError(t, mockedService.TheExampleMethod3(nil), "error2") +} + +func Test_Mock_On_WithFuncArgMatcher(t *testing.T) { + t.Parallel() + + var mockedService TestExampleImplementation + + fixture1, fixture2 := errors.New("fixture1"), errors.New("fixture2") + + mockedService.On("TheExampleMethodFunc", + MatchedBy(func(a func(string) error) bool { return a != nil && a("string") == fixture1 }), + ).Return(errors.New("fixture1")) + + mockedService.On("TheExampleMethodFunc", + MatchedBy(func(a func(string) error) bool { return a != nil && a("string") == fixture2 }), + ).Return(errors.New("fixture2")) + + mockedService.On("TheExampleMethodFunc", + MatchedBy(func(a func(string) error) bool { return a == nil }), + ).Return(errors.New("fixture3")) + + assert.EqualError(t, mockedService.TheExampleMethodFunc( + func(string) error { return fixture1 }), "fixture1") + assert.EqualError(t, mockedService.TheExampleMethodFunc( + func(string) error { return fixture2 }), "fixture2") + assert.EqualError(t, mockedService.TheExampleMethodFunc(nil), "fixture3") +} + +func Test_Mock_On_WithInterfaceArgMatcher(t *testing.T) { + t.Parallel() + + var mockedService TestExampleImplementation + + mockedService.On("TheExampleMethod4", + MatchedBy(func(a ExampleInterface) bool { return a == nil }), + ).Return(errors.New("fixture1")) + + assert.EqualError(t, mockedService.TheExampleMethod4(nil), "fixture1") +} + +func Test_Mock_On_WithChannelArgMatcher(t *testing.T) { + t.Parallel() + + var mockedService TestExampleImplementation + + mockedService.On("TheExampleMethod5", + MatchedBy(func(ch chan struct{}) bool { return ch == nil }), + ).Return(errors.New("fixture1")) + + assert.EqualError(t, mockedService.TheExampleMethod5(nil), "fixture1") +} + +func Test_Mock_On_WithMapArgMatcher(t *testing.T) { + t.Parallel() + + var mockedService TestExampleImplementation + + mockedService.On("TheExampleMethod6", + MatchedBy(func(m map[string]bool) bool { return m == nil }), + ).Return(errors.New("fixture1")) + + assert.EqualError(t, mockedService.TheExampleMethod6(nil), "fixture1") +} + +func Test_Mock_On_WithSliceArgMatcher(t *testing.T) { + t.Parallel() + + var mockedService TestExampleImplementation + + mockedService.On("TheExampleMethod7", + MatchedBy(func(slice []bool) bool { return slice == nil }), + ).Return(errors.New("fixture1")) + + assert.EqualError(t, mockedService.TheExampleMethod7(nil), "fixture1") +} + +func Test_Mock_On_WithVariadicFunc(t *testing.T) { + t.Parallel() + + // make a test impl object + var mockedService = new(TestExampleImplementation) + + c := mockedService. + On("TheExampleMethodVariadic", []int{1, 2, 3}). + Return(nil) + + assert.Equal(t, []*Call{c}, mockedService.ExpectedCalls) + assert.Equal(t, 1, len(c.Arguments)) + assert.Equal(t, []int{1, 2, 3}, c.Arguments[0]) + + assert.NotPanics(t, func() { + mockedService.TheExampleMethodVariadic(1, 2, 3) + }) + assert.Panics(t, func() { + mockedService.TheExampleMethodVariadic(1, 2) + }) + +} + +func Test_Mock_On_WithMixedVariadicFunc(t *testing.T) { + t.Parallel() + + // make a test impl object + var mockedService = new(TestExampleImplementation) + + c := mockedService. + On("TheExampleMethodMixedVariadic", 1, []int{2, 3, 4}). + Return(nil) + + assert.Equal(t, []*Call{c}, mockedService.ExpectedCalls) + assert.Equal(t, 2, len(c.Arguments)) + assert.Equal(t, 1, c.Arguments[0]) + assert.Equal(t, []int{2, 3, 4}, c.Arguments[1]) + + assert.NotPanics(t, func() { + mockedService.TheExampleMethodMixedVariadic(1, 2, 3, 4) + }) + assert.Panics(t, func() { + mockedService.TheExampleMethodMixedVariadic(1, 2, 3, 5) + }) + +} + +func Test_Mock_On_WithVariadicFuncWithInterface(t *testing.T) { + t.Parallel() + + // make a test impl object + var mockedService = new(TestExampleImplementation) + + c := mockedService.On("TheExampleMethodVariadicInterface", []interface{}{1, 2, 3}). + Return(nil) + + assert.Equal(t, []*Call{c}, mockedService.ExpectedCalls) + assert.Equal(t, 1, len(c.Arguments)) + assert.Equal(t, []interface{}{1, 2, 3}, c.Arguments[0]) + + assert.NotPanics(t, func() { + mockedService.TheExampleMethodVariadicInterface(1, 2, 3) + }) + assert.Panics(t, func() { + mockedService.TheExampleMethodVariadicInterface(1, 2) + }) + +} + +func Test_Mock_On_WithVariadicFuncWithEmptyInterfaceArray(t *testing.T) { + t.Parallel() + + // make a test impl object + var mockedService = new(TestExampleImplementation) + + var expected []interface{} + c := mockedService. + On("TheExampleMethodVariadicInterface", expected). + Return(nil) + + assert.Equal(t, []*Call{c}, mockedService.ExpectedCalls) + assert.Equal(t, 1, len(c.Arguments)) + assert.Equal(t, expected, c.Arguments[0]) + + assert.NotPanics(t, func() { + mockedService.TheExampleMethodVariadicInterface() + }) + assert.Panics(t, func() { + mockedService.TheExampleMethodVariadicInterface(1, 2) + }) + +} + +func Test_Mock_On_WithFuncPanics(t *testing.T) { + t.Parallel() + + // make a test impl object + var mockedService = new(TestExampleImplementation) + + assert.Panics(t, func() { + mockedService.On("TheExampleMethodFunc", func(string) error { return nil }) + }) +} + +func Test_Mock_On_WithFuncTypeArg(t *testing.T) { + t.Parallel() + + // make a test impl object + var mockedService = new(TestExampleImplementation) + + c := mockedService. + On("TheExampleMethodFuncType", AnythingOfType("mock.ExampleFuncType")). + Return(nil) + + assert.Equal(t, []*Call{c}, mockedService.ExpectedCalls) + assert.Equal(t, 1, len(c.Arguments)) + assert.Equal(t, AnythingOfType("mock.ExampleFuncType"), c.Arguments[0]) + + fn := func(string) error { return nil } + assert.NotPanics(t, func() { + mockedService.TheExampleMethodFuncType(fn) + }) +} + +func Test_Mock_Unset(t *testing.T) { + t.Parallel() + + // make a test impl object + var mockedService = new(TestExampleImplementation) + + call := mockedService. + On("TheExampleMethodFuncType", "argA"). + Return("blah") + + found, foundCall := mockedService.findExpectedCall("TheExampleMethodFuncType", "argA") + require.NotEqual(t, -1, found) + require.Equal(t, foundCall, call) + + call.Unset() + + found, foundCall = mockedService.findExpectedCall("TheExampleMethodFuncType", "argA") + require.Equal(t, -1, found) + + var expectedCall *Call + require.Equal(t, expectedCall, foundCall) + + fn := func(string) error { return nil } + assert.Panics(t, func() { + mockedService.TheExampleMethodFuncType(fn) + }) +} + +// Since every time you call On it creates a new object +// the last time you call Unset it will only unset the last call +func Test_Mock_Chained_UnsetOnlyUnsetsLastCall(t *testing.T) { + t.Parallel() + + // make a test impl object + var mockedService = new(TestExampleImplementation) + + // determine our current line number so we can assert the expected calls callerInfo properly + _, filename, line, _ := runtime.Caller(0) + mockedService. + On("TheExampleMethod1", 1, 1). + Return(0). + On("TheExampleMethod2", 2, 2). + On("TheExampleMethod3", 3, 3, 3). + Return(nil). + Unset() + + expectedCalls := []*Call{ + { + Parent: &mockedService.Mock, + Method: "TheExampleMethod1", + Arguments: []interface{}{1, 1}, + ReturnArguments: []interface{}{0}, + callerInfo: []string{fmt.Sprintf("%s:%d", filename, line+2)}, + }, + { + Parent: &mockedService.Mock, + Method: "TheExampleMethod2", + Arguments: []interface{}{2, 2}, + ReturnArguments: []interface{}{}, + callerInfo: []string{fmt.Sprintf("%s:%d", filename, line+4)}, + }, + } + assert.Equal(t, 2, len(expectedCalls)) + assert.Equal(t, expectedCalls, mockedService.ExpectedCalls) +} + +func Test_Mock_UnsetIfAlreadyUnsetFails(t *testing.T) { + t.Parallel() + + // make a test impl object + var mockedService = new(TestExampleImplementation) + + mock1 := mockedService. + On("TheExampleMethod1", 1, 1). + Return(1) + + assert.Equal(t, 1, len(mockedService.ExpectedCalls)) + mock1.Unset() + assert.Equal(t, 0, len(mockedService.ExpectedCalls)) + + assert.Panics(t, func() { + mock1.Unset() + }) + + assert.Equal(t, 0, len(mockedService.ExpectedCalls)) +} + +func Test_Mock_UnsetByOnMethodSpec(t *testing.T) { + t.Parallel() + + // make a test impl object + var mockedService = new(TestExampleImplementation) + + mock1 := mockedService. + On("TheExampleMethod", 1, 2, 3). + Return(0, nil) + + assert.Equal(t, 1, len(mockedService.ExpectedCalls)) + mock1.On("TheExampleMethod", 1, 2, 3). + Return(0, nil).Unset() + + assert.Equal(t, 0, len(mockedService.ExpectedCalls)) + + assert.Panics(t, func() { + mock1.Unset() + }) + + assert.Equal(t, 0, len(mockedService.ExpectedCalls)) +} + +func Test_Mock_UnsetByOnMethodSpecAmongOthers(t *testing.T) { + t.Parallel() + + // make a test impl object + var mockedService = new(TestExampleImplementation) + + _, filename, line, _ := runtime.Caller(0) + mock1 := mockedService. + On("TheExampleMethod", 1, 2, 3). + Return(0, nil). + On("TheExampleMethodVariadic", 1, 2, 3, 4, 5).Once(). + Return(nil) + mock1. + On("TheExampleMethodFuncType", Anything). + Return(nil) + + assert.Equal(t, 3, len(mockedService.ExpectedCalls)) + mock1.On("TheExampleMethod", 1, 2, 3). + Return(0, nil).Unset() + + assert.Equal(t, 2, len(mockedService.ExpectedCalls)) + + expectedCalls := []*Call{ + { + Parent: &mockedService.Mock, + Method: "TheExampleMethodVariadic", + Repeatability: 1, + Arguments: []interface{}{1, 2, 3, 4, 5}, + ReturnArguments: []interface{}{nil}, + callerInfo: []string{fmt.Sprintf("%s:%d", filename, line+4)}, + }, + { + Parent: &mockedService.Mock, + Method: "TheExampleMethodFuncType", + Arguments: []interface{}{Anything}, + ReturnArguments: []interface{}{nil}, + callerInfo: []string{fmt.Sprintf("%s:%d", filename, line+7)}, + }, + } + + assert.Equal(t, 2, len(mockedService.ExpectedCalls)) + assert.Equal(t, expectedCalls, mockedService.ExpectedCalls) +} + +func Test_Mock_Unset_WithFuncPanics(t *testing.T) { + t.Parallel() + + // make a test impl object + var mockedService = new(TestExampleImplementation) + mock1 := mockedService.On("TheExampleMethod", 1) + mock1.Arguments = append(mock1.Arguments, func(string) error { return nil }) + + assert.Panics(t, func() { + mock1.Unset() + }) +} + +func Test_Mock_Return(t *testing.T) { + t.Parallel() + + // make a test impl object + var mockedService = new(TestExampleImplementation) + + c := mockedService. + On("TheExampleMethod", "A", "B", true). + Return(1, "two", true) + + require.Equal(t, []*Call{c}, mockedService.ExpectedCalls) + + call := mockedService.ExpectedCalls[0] + + assert.Equal(t, "TheExampleMethod", call.Method) + assert.Equal(t, "A", call.Arguments[0]) + assert.Equal(t, "B", call.Arguments[1]) + assert.Equal(t, true, call.Arguments[2]) + assert.Equal(t, 1, call.ReturnArguments[0]) + assert.Equal(t, "two", call.ReturnArguments[1]) + assert.Equal(t, true, call.ReturnArguments[2]) + assert.Equal(t, 0, call.Repeatability) + assert.Nil(t, call.WaitFor) +} + +func Test_Mock_Panic(t *testing.T) { + t.Parallel() + + // make a test impl object + var mockedService = new(TestExampleImplementation) + + c := mockedService. + On("TheExampleMethod", "A", "B", true). + Panic("panic message for example method") + + require.Equal(t, []*Call{c}, mockedService.ExpectedCalls) + + call := mockedService.ExpectedCalls[0] + + assert.Equal(t, "TheExampleMethod", call.Method) + assert.Equal(t, "A", call.Arguments[0]) + assert.Equal(t, "B", call.Arguments[1]) + assert.Equal(t, true, call.Arguments[2]) + assert.Equal(t, 0, call.Repeatability) + assert.Equal(t, 0, call.Repeatability) + assert.Equal(t, "panic message for example method", *call.PanicMsg) + assert.Nil(t, call.WaitFor) +} + +func Test_Mock_Return_WaitUntil(t *testing.T) { + t.Parallel() + + // make a test impl object + var mockedService = new(TestExampleImplementation) + ch := time.After(time.Second) + + c := mockedService.Mock. + On("TheExampleMethod", "A", "B", true). + WaitUntil(ch). + Return(1, "two", true) + + // assert that the call was created + require.Equal(t, []*Call{c}, mockedService.ExpectedCalls) + + call := mockedService.ExpectedCalls[0] + + assert.Equal(t, "TheExampleMethod", call.Method) + assert.Equal(t, "A", call.Arguments[0]) + assert.Equal(t, "B", call.Arguments[1]) + assert.Equal(t, true, call.Arguments[2]) + assert.Equal(t, 1, call.ReturnArguments[0]) + assert.Equal(t, "two", call.ReturnArguments[1]) + assert.Equal(t, true, call.ReturnArguments[2]) + assert.Equal(t, 0, call.Repeatability) + assert.Equal(t, ch, call.WaitFor) +} + +func Test_Mock_Return_After(t *testing.T) { + t.Parallel() + + // make a test impl object + var mockedService = new(TestExampleImplementation) + + c := mockedService.Mock. + On("TheExampleMethod", "A", "B", true). + Return(1, "two", true). + After(time.Second) + + require.Equal(t, []*Call{c}, mockedService.ExpectedCalls) + + call := mockedService.Mock.ExpectedCalls[0] + + assert.Equal(t, "TheExampleMethod", call.Method) + assert.Equal(t, "A", call.Arguments[0]) + assert.Equal(t, "B", call.Arguments[1]) + assert.Equal(t, true, call.Arguments[2]) + assert.Equal(t, 1, call.ReturnArguments[0]) + assert.Equal(t, "two", call.ReturnArguments[1]) + assert.Equal(t, true, call.ReturnArguments[2]) + assert.Equal(t, 0, call.Repeatability) + assert.NotEqual(t, nil, call.WaitFor) + +} + +func Test_Mock_Return_Run(t *testing.T) { + t.Parallel() + + // make a test impl object + var mockedService = new(TestExampleImplementation) + + fn := func(args Arguments) { + arg := args.Get(0).(*ExampleType) + arg.ran = true + } + + c := mockedService.Mock. + On("TheExampleMethod3", AnythingOfType("*mock.ExampleType")). + Return(nil). + Run(fn) + + require.Equal(t, []*Call{c}, mockedService.ExpectedCalls) + + call := mockedService.Mock.ExpectedCalls[0] + + assert.Equal(t, "TheExampleMethod3", call.Method) + assert.Equal(t, AnythingOfType("*mock.ExampleType"), call.Arguments[0]) + assert.Equal(t, nil, call.ReturnArguments[0]) + assert.Equal(t, 0, call.Repeatability) + assert.NotEqual(t, nil, call.WaitFor) + assert.NotNil(t, call.Run) + + et := ExampleType{} + assert.Equal(t, false, et.ran) + mockedService.TheExampleMethod3(&et) + assert.Equal(t, true, et.ran) +} + +func Test_Mock_Return_Run_Out_Of_Order(t *testing.T) { + t.Parallel() + + // make a test impl object + var mockedService = new(TestExampleImplementation) + f := func(args Arguments) { + arg := args.Get(0).(*ExampleType) + arg.ran = true + } + + c := mockedService.Mock. + On("TheExampleMethod3", AnythingOfType("*mock.ExampleType")). + Run(f). + Return(nil) + + require.Equal(t, []*Call{c}, mockedService.ExpectedCalls) + + call := mockedService.Mock.ExpectedCalls[0] + + assert.Equal(t, "TheExampleMethod3", call.Method) + assert.Equal(t, AnythingOfType("*mock.ExampleType"), call.Arguments[0]) + assert.Equal(t, nil, call.ReturnArguments[0]) + assert.Equal(t, 0, call.Repeatability) + assert.NotEqual(t, nil, call.WaitFor) + assert.NotNil(t, call.Run) +} + +func Test_Mock_Return_Once(t *testing.T) { + t.Parallel() + + // make a test impl object + var mockedService = new(TestExampleImplementation) + + c := mockedService.On("TheExampleMethod", "A", "B", true). + Return(1, "two", true). + Once() + + require.Equal(t, []*Call{c}, mockedService.ExpectedCalls) + + call := mockedService.ExpectedCalls[0] + + assert.Equal(t, "TheExampleMethod", call.Method) + assert.Equal(t, "A", call.Arguments[0]) + assert.Equal(t, "B", call.Arguments[1]) + assert.Equal(t, true, call.Arguments[2]) + assert.Equal(t, 1, call.ReturnArguments[0]) + assert.Equal(t, "two", call.ReturnArguments[1]) + assert.Equal(t, true, call.ReturnArguments[2]) + assert.Equal(t, 1, call.Repeatability) + assert.Nil(t, call.WaitFor) +} + +func Test_Mock_Return_Twice(t *testing.T) { + t.Parallel() + + // make a test impl object + var mockedService = new(TestExampleImplementation) + + c := mockedService. + On("TheExampleMethod", "A", "B", true). + Return(1, "two", true). + Twice() + + require.Equal(t, []*Call{c}, mockedService.ExpectedCalls) + + call := mockedService.ExpectedCalls[0] + + assert.Equal(t, "TheExampleMethod", call.Method) + assert.Equal(t, "A", call.Arguments[0]) + assert.Equal(t, "B", call.Arguments[1]) + assert.Equal(t, true, call.Arguments[2]) + assert.Equal(t, 1, call.ReturnArguments[0]) + assert.Equal(t, "two", call.ReturnArguments[1]) + assert.Equal(t, true, call.ReturnArguments[2]) + assert.Equal(t, 2, call.Repeatability) + assert.Nil(t, call.WaitFor) +} + +func Test_Mock_Return_Times(t *testing.T) { + t.Parallel() + + // make a test impl object + var mockedService = new(TestExampleImplementation) + + c := mockedService. + On("TheExampleMethod", "A", "B", true). + Return(1, "two", true). + Times(5) + + require.Equal(t, []*Call{c}, mockedService.ExpectedCalls) + + call := mockedService.ExpectedCalls[0] + + assert.Equal(t, "TheExampleMethod", call.Method) + assert.Equal(t, "A", call.Arguments[0]) + assert.Equal(t, "B", call.Arguments[1]) + assert.Equal(t, true, call.Arguments[2]) + assert.Equal(t, 1, call.ReturnArguments[0]) + assert.Equal(t, "two", call.ReturnArguments[1]) + assert.Equal(t, true, call.ReturnArguments[2]) + assert.Equal(t, 5, call.Repeatability) + assert.Nil(t, call.WaitFor) +} + +func Test_Mock_Return_Nothing(t *testing.T) { + t.Parallel() + + // make a test impl object + var mockedService = new(TestExampleImplementation) + + c := mockedService. + On("TheExampleMethod", "A", "B", true). + Return() + + require.Equal(t, []*Call{c}, mockedService.ExpectedCalls) + + call := mockedService.ExpectedCalls[0] + + assert.Equal(t, "TheExampleMethod", call.Method) + assert.Equal(t, "A", call.Arguments[0]) + assert.Equal(t, "B", call.Arguments[1]) + assert.Equal(t, true, call.Arguments[2]) + assert.Equal(t, 0, len(call.ReturnArguments)) +} + +func Test_Mock_Return_NotBefore_In_Order(t *testing.T) { + t.Parallel() + + var mockedService = new(TestExampleImplementation) + + b := mockedService. + On("TheExampleMethod", 1, 2, 3). + Return(4, nil) + c := mockedService. + On("TheExampleMethod2", true). + Return(). + NotBefore(b) + + require.Equal(t, []*Call{b, c}, mockedService.ExpectedCalls) + require.NotPanics(t, func() { + mockedService.TheExampleMethod(1, 2, 3) + }) + require.NotPanics(t, func() { + mockedService.TheExampleMethod2(true) + }) +} + +func Test_Mock_Return_InOrder_Uses_NotBefore(t *testing.T) { + t.Parallel() + + var mockedService = new(TestExampleImplementation) + + InOrder( + mockedService. + On("TheExampleMethod", 1, 2, 3). + Return(4, nil), + mockedService. + On("TheExampleMethod2", true). + Return(), + ) + + require.NotPanics(t, func() { + mockedService.TheExampleMethod(1, 2, 3) + }) + require.NotPanics(t, func() { + mockedService.TheExampleMethod2(true) + }) +} + +func Test_Mock_Return_NotBefore_Out_Of_Order(t *testing.T) { + t.Parallel() + + var mockedService = new(TestExampleImplementation) + + b := mockedService. + On("TheExampleMethod", 1, 2, 3). + Return(4, nil).Twice() + c := mockedService. + On("TheExampleMethod2", true). + Return(). + NotBefore(b) + + require.Equal(t, []*Call{b, c}, mockedService.ExpectedCalls) + + expectedPanicString := `mock: Unexpected Method Call +----------------------------- + +TheExampleMethod2(bool) + 0: true + +Must not be called before: + +TheExampleMethod(int,int,int) + 0: 1 + 1: 2 + 2: 3` + require.PanicsWithValue(t, expectedPanicString, func() { + mockedService.TheExampleMethod2(true) + }) +} + +func Test_Mock_Return_InOrder_Uses_NotBefore_Out_Of_Order(t *testing.T) { + t.Parallel() + + var mockedService = new(TestExampleImplementation) + + InOrder( + mockedService. + On("TheExampleMethod", 1, 2, 3). + Return(4, nil).Twice(), + mockedService. + On("TheExampleMethod2", true). + Return(), + ) + + expectedPanicString := `mock: Unexpected Method Call +----------------------------- + +TheExampleMethod2(bool) + 0: true + +Must not be called before: + +TheExampleMethod(int,int,int) + 0: 1 + 1: 2 + 2: 3` + require.PanicsWithValue(t, expectedPanicString, func() { + mockedService.TheExampleMethod2(true) + }) +} + +func Test_Mock_Return_NotBefore_Not_Enough_Times(t *testing.T) { + t.Parallel() + + var mockedService = new(TestExampleImplementation) + + b := mockedService. + On("TheExampleMethod", 1, 2, 3). + Return(4, nil).Twice() + c := mockedService. + On("TheExampleMethod2", true). + Return(). + NotBefore(b) + + require.Equal(t, []*Call{b, c}, mockedService.ExpectedCalls) + + require.NotPanics(t, func() { + mockedService.TheExampleMethod(1, 2, 3) + }) + expectedPanicString := `mock: Unexpected Method Call +----------------------------- + +TheExampleMethod2(bool) + 0: true + +Must not be called before another call of: + +TheExampleMethod(int,int,int) + 0: 1 + 1: 2 + 2: 3` + require.PanicsWithValue(t, expectedPanicString, func() { + mockedService.TheExampleMethod2(true) + }) +} + +func Test_Mock_Return_NotBefore_Different_Mock_In_Order(t *testing.T) { + t.Parallel() + + var ( + mockedService1 = new(TestExampleImplementation) + mockedService2 = new(TestExampleImplementation) + ) + + b := mockedService1. + On("TheExampleMethod", 1, 2, 3). + Return(4, nil) + c := mockedService2. + On("TheExampleMethod2", true). + Return(). + NotBefore(b) + + require.Equal(t, []*Call{c}, mockedService2.ExpectedCalls) + require.NotPanics(t, func() { + mockedService1.TheExampleMethod(1, 2, 3) + }) + require.NotPanics(t, func() { + mockedService2.TheExampleMethod2(true) + }) +} + +func Test_Mock_Return_NotBefore_Different_Mock_Out_Of_Order(t *testing.T) { + t.Parallel() + + var ( + mockedService1 = new(TestExampleImplementation) + mockedService2 = new(TestExampleImplementation) + ) + + b := mockedService1. + On("TheExampleMethod", 1, 2, 3). + Return(4, nil) + c := mockedService2. + On("TheExampleMethod2", true). + Return(). + NotBefore(b) + + require.Equal(t, []*Call{c}, mockedService2.ExpectedCalls) + + expectedPanicString := `mock: Unexpected Method Call +----------------------------- + +TheExampleMethod2(bool) + 0: true + +Must not be called before method from another mock instance: + +TheExampleMethod(int,int,int) + 0: 1 + 1: 2 + 2: 3` + require.PanicsWithValue(t, expectedPanicString, func() { + mockedService2.TheExampleMethod2(true) + }) +} + +func Test_Mock_Return_NotBefore_In_Order_With_Non_Dependant(t *testing.T) { + t.Parallel() + + var mockedService = new(TestExampleImplementation) + + a := mockedService. + On("TheExampleMethod", 1, 2, 3). + Return(4, nil) + b := mockedService. + On("TheExampleMethod", 4, 5, 6). + Return(4, nil) + c := mockedService. + On("TheExampleMethod2", true). + Return(). + NotBefore(a, b) + d := mockedService. + On("TheExampleMethod7", []bool{}).Return(nil) + + require.Equal(t, []*Call{a, b, c, d}, mockedService.ExpectedCalls) + require.NotPanics(t, func() { + mockedService.TheExampleMethod7([]bool{}) + }) + require.NotPanics(t, func() { + mockedService.TheExampleMethod(1, 2, 3) + }) + require.NotPanics(t, func() { + mockedService.TheExampleMethod7([]bool{}) + }) + require.NotPanics(t, func() { + mockedService.TheExampleMethod(4, 5, 6) + }) + require.NotPanics(t, func() { + mockedService.TheExampleMethod7([]bool{}) + }) + require.NotPanics(t, func() { + mockedService.TheExampleMethod2(true) + }) + require.NotPanics(t, func() { + mockedService.TheExampleMethod7([]bool{}) + }) +} + +func Test_Mock_Return_NotBefore_Orphan_Call(t *testing.T) { + t.Parallel() + + var mockedService = new(TestExampleImplementation) + + require.PanicsWithValue(t, "not before calls must be created with Mock.On()", func() { + mockedService. + On("TheExampleMethod2", true). + Return(). + NotBefore(&Call{Method: "Not", Arguments: Arguments{"how", "it's"}, ReturnArguments: Arguments{"done"}}) + }) +} + +func Test_Mock_findExpectedCall(t *testing.T) { + t.Parallel() + + m := new(Mock) + m.On("One", 1).Return("one") + m.On("Two", 2).Return("two") + m.On("Two", 3).Return("three") + + f, c := m.findExpectedCall("Two", 3) + + if assert.Equal(t, 2, f) { + if assert.NotNil(t, c) { + assert.Equal(t, "Two", c.Method) + assert.Equal(t, 3, c.Arguments[0]) + assert.Equal(t, "three", c.ReturnArguments[0]) + } + } + +} + +func Test_Mock_findExpectedCall_For_Unknown_Method(t *testing.T) { + t.Parallel() + + m := new(Mock) + m.On("One", 1).Return("one") + m.On("Two", 2).Return("two") + m.On("Two", 3).Return("three") + + f, _ := m.findExpectedCall("Two") + + assert.Equal(t, -1, f) + +} + +func Test_Mock_findExpectedCall_Respects_Repeatability(t *testing.T) { + t.Parallel() + + m := new(Mock) + m.On("One", 1).Return("one") + m.On("Two", 2).Return("two").Once() + m.On("Two", 3).Return("three").Twice() + m.On("Two", 3).Return("three").Times(8) + + f, c := m.findExpectedCall("Two", 3) + + if assert.Equal(t, 2, f) { + if assert.NotNil(t, c) { + assert.Equal(t, "Two", c.Method) + assert.Equal(t, 3, c.Arguments[0]) + assert.Equal(t, "three", c.ReturnArguments[0]) + } + } + + c = m.On("Once", 1).Return("one").Once() + c.Repeatability = -1 + f, c = m.findExpectedCall("Once", 1) + if assert.Equal(t, -1, f) { + if assert.NotNil(t, c) { + assert.Equal(t, "Once", c.Method) + assert.Equal(t, 1, c.Arguments[0]) + assert.Equal(t, "one", c.ReturnArguments[0]) + } + } +} + +func Test_callString(t *testing.T) { + t.Parallel() + + assert.Equal(t, `Method(int,bool,string)`, callString("Method", []interface{}{1, true, "something"}, false)) + assert.Equal(t, `Method()`, callString("Method", []interface{}{nil}, false)) + +} + +func Test_Mock_Called(t *testing.T) { + t.Parallel() + + var mockedService = new(TestExampleImplementation) + + mockedService.On("Test_Mock_Called", 1, 2, 3).Return(5, "6", true) + + returnArguments := mockedService.Called(1, 2, 3) + + if assert.Equal(t, 1, len(mockedService.Calls)) { + assert.Equal(t, "Test_Mock_Called", mockedService.Calls[0].Method) + assert.Equal(t, 1, mockedService.Calls[0].Arguments[0]) + assert.Equal(t, 2, mockedService.Calls[0].Arguments[1]) + assert.Equal(t, 3, mockedService.Calls[0].Arguments[2]) + } + + if assert.Equal(t, 3, len(returnArguments)) { + assert.Equal(t, 5, returnArguments[0]) + assert.Equal(t, "6", returnArguments[1]) + assert.Equal(t, true, returnArguments[2]) + } + +} + +func asyncCall(m *Mock, ch chan Arguments) { + ch <- m.Called(1, 2, 3) +} + +func Test_Mock_Called_blocks(t *testing.T) { + t.Parallel() + + var mockedService = new(TestExampleImplementation) + + mockedService.Mock.On("asyncCall", 1, 2, 3).Return(5, "6", true).After(20 * time.Millisecond) + + ch := make(chan Arguments) + + go asyncCall(&mockedService.Mock, ch) + + select { + case <-ch: + t.Fatal("should have waited") + case <-time.After(10 * time.Millisecond): + } + + returnArguments := <-ch + + if assert.Equal(t, 1, len(mockedService.Mock.Calls)) { + assert.Equal(t, "asyncCall", mockedService.Mock.Calls[0].Method) + assert.Equal(t, 1, mockedService.Mock.Calls[0].Arguments[0]) + assert.Equal(t, 2, mockedService.Mock.Calls[0].Arguments[1]) + assert.Equal(t, 3, mockedService.Mock.Calls[0].Arguments[2]) + } + + if assert.Equal(t, 3, len(returnArguments)) { + assert.Equal(t, 5, returnArguments[0]) + assert.Equal(t, "6", returnArguments[1]) + assert.Equal(t, true, returnArguments[2]) + } + +} + +func Test_Mock_Called_For_Bounded_Repeatability(t *testing.T) { + t.Parallel() + + var mockedService = new(TestExampleImplementation) + + mockedService. + On("Test_Mock_Called_For_Bounded_Repeatability", 1, 2, 3). + Return(5, "6", true). + Once() + mockedService. + On("Test_Mock_Called_For_Bounded_Repeatability", 1, 2, 3). + Return(-1, "hi", false) + + returnArguments1 := mockedService.Called(1, 2, 3) + returnArguments2 := mockedService.Called(1, 2, 3) + + if assert.Equal(t, 2, len(mockedService.Calls)) { + assert.Equal(t, "Test_Mock_Called_For_Bounded_Repeatability", mockedService.Calls[0].Method) + assert.Equal(t, 1, mockedService.Calls[0].Arguments[0]) + assert.Equal(t, 2, mockedService.Calls[0].Arguments[1]) + assert.Equal(t, 3, mockedService.Calls[0].Arguments[2]) + + assert.Equal(t, "Test_Mock_Called_For_Bounded_Repeatability", mockedService.Calls[1].Method) + assert.Equal(t, 1, mockedService.Calls[1].Arguments[0]) + assert.Equal(t, 2, mockedService.Calls[1].Arguments[1]) + assert.Equal(t, 3, mockedService.Calls[1].Arguments[2]) + } + + if assert.Equal(t, 3, len(returnArguments1)) { + assert.Equal(t, 5, returnArguments1[0]) + assert.Equal(t, "6", returnArguments1[1]) + assert.Equal(t, true, returnArguments1[2]) + } + + if assert.Equal(t, 3, len(returnArguments2)) { + assert.Equal(t, -1, returnArguments2[0]) + assert.Equal(t, "hi", returnArguments2[1]) + assert.Equal(t, false, returnArguments2[2]) + } + +} + +func Test_Mock_Called_For_SetTime_Expectation(t *testing.T) { + t.Parallel() + + var mockedService = new(TestExampleImplementation) + + mockedService.On("TheExampleMethod", 1, 2, 3).Return(5, "6", true).Times(4) + + mockedService.TheExampleMethod(1, 2, 3) + mockedService.TheExampleMethod(1, 2, 3) + mockedService.TheExampleMethod(1, 2, 3) + mockedService.TheExampleMethod(1, 2, 3) + assert.Panics(t, func() { + mockedService.TheExampleMethod(1, 2, 3) + }) + +} + +func Test_Mock_Called_Unexpected(t *testing.T) { + t.Parallel() + + var mockedService = new(TestExampleImplementation) + + // make sure it panics if no expectation was made + assert.Panics(t, func() { + mockedService.Called(1, 2, 3) + }, "Calling unexpected method should panic") + +} + +func Test_AssertExpectationsForObjects_Helper(t *testing.T) { + t.Parallel() + + var mockedService1 = new(TestExampleImplementation) + var mockedService2 = new(TestExampleImplementation) + var mockedService3 = new(TestExampleImplementation) + var mockedService4 = new(TestExampleImplementation) // No expectations does not cause a panic + + mockedService1.On("Test_AssertExpectationsForObjects_Helper", 1).Return() + mockedService2.On("Test_AssertExpectationsForObjects_Helper", 2).Return() + mockedService3.On("Test_AssertExpectationsForObjects_Helper", 3).Return() + + mockedService1.Called(1) + mockedService2.Called(2) + mockedService3.Called(3) + + assert.True(t, AssertExpectationsForObjects(t, &mockedService1.Mock, &mockedService2.Mock, &mockedService3.Mock, &mockedService4.Mock)) + assert.True(t, AssertExpectationsForObjects(t, mockedService1, mockedService2, mockedService3, mockedService4)) + +} + +func Test_AssertExpectationsForObjects_Helper_Failed(t *testing.T) { + t.Parallel() + + var mockedService1 = new(TestExampleImplementation) + var mockedService2 = new(TestExampleImplementation) + var mockedService3 = new(TestExampleImplementation) + + mockedService1.On("Test_AssertExpectationsForObjects_Helper_Failed", 1).Return() + mockedService2.On("Test_AssertExpectationsForObjects_Helper_Failed", 2).Return() + mockedService3.On("Test_AssertExpectationsForObjects_Helper_Failed", 3).Return() + + mockedService1.Called(1) + mockedService3.Called(3) + + tt := new(testing.T) + assert.False(t, AssertExpectationsForObjects(tt, &mockedService1.Mock, &mockedService2.Mock, &mockedService3.Mock)) + assert.False(t, AssertExpectationsForObjects(tt, mockedService1, mockedService2, mockedService3)) + +} + +func Test_Mock_AssertExpectations(t *testing.T) { + t.Parallel() + + var mockedService = new(TestExampleImplementation) + + mockedService.On("Test_Mock_AssertExpectations", 1, 2, 3).Return(5, 6, 7) + + tt := new(testing.T) + assert.False(t, mockedService.AssertExpectations(tt)) + + // make the call now + mockedService.Called(1, 2, 3) + + // now assert expectations + assert.True(t, mockedService.AssertExpectations(tt)) + +} + +func Test_Mock_AssertExpectations_Placeholder_NoArgs(t *testing.T) { + t.Parallel() + + var mockedService = new(TestExampleImplementation) + + mockedService.On("Test_Mock_AssertExpectations_Placeholder_NoArgs").Return(5, 6, 7).Once() + mockedService.On("Test_Mock_AssertExpectations_Placeholder_NoArgs").Return(7, 6, 5) + + tt := new(testing.T) + assert.False(t, mockedService.AssertExpectations(tt)) + + // make the call now + mockedService.Called() + + // now assert expectations + assert.True(t, mockedService.AssertExpectations(tt)) + +} + +func Test_Mock_AssertExpectations_Placeholder(t *testing.T) { + t.Parallel() + + var mockedService = new(TestExampleImplementation) + + mockedService.On("Test_Mock_AssertExpectations_Placeholder", 1, 2, 3).Return(5, 6, 7).Once() + mockedService.On("Test_Mock_AssertExpectations_Placeholder", 3, 2, 1).Return(7, 6, 5) + + tt := new(testing.T) + assert.False(t, mockedService.AssertExpectations(tt)) + + // make the call now + mockedService.Called(1, 2, 3) + + // now assert expectations + assert.False(t, mockedService.AssertExpectations(tt)) + + // make call to the second expectation + mockedService.Called(3, 2, 1) + + // now assert expectations again + assert.True(t, mockedService.AssertExpectations(tt)) +} + +func Test_Mock_AssertExpectations_With_Pointers(t *testing.T) { + t.Parallel() + + var mockedService = new(TestExampleImplementation) + + mockedService.On("Test_Mock_AssertExpectations_With_Pointers", &struct{ Foo int }{1}).Return(1) + mockedService.On("Test_Mock_AssertExpectations_With_Pointers", &struct{ Foo int }{2}).Return(2) + + tt := new(testing.T) + assert.False(t, mockedService.AssertExpectations(tt)) + + s := struct{ Foo int }{1} + // make the calls now + mockedService.Called(&s) + s.Foo = 2 + mockedService.Called(&s) + + // now assert expectations + assert.True(t, mockedService.AssertExpectations(tt)) + +} + +func Test_Mock_AssertExpectationsCustomType(t *testing.T) { + t.Parallel() + + var mockedService = new(TestExampleImplementation) + + mockedService.On("TheExampleMethod3", AnythingOfType("*mock.ExampleType")).Return(nil).Once() + + tt := new(testing.T) + assert.False(t, mockedService.AssertExpectations(tt)) + + // make the call now + mockedService.TheExampleMethod3(&ExampleType{}) + + // now assert expectations + assert.True(t, mockedService.AssertExpectations(tt)) + +} + +func Test_Mock_AssertExpectationsFunctionalOptionsType(t *testing.T) { + t.Parallel() + + var mockedService = new(TestExampleImplementation) + + mockedService.On("TheExampleMethodFunctionalOptions", "test", FunctionalOptions(OpNum(1), OpStr("foo"))).Return(nil).Once() + + tt := new(testing.T) + assert.False(t, mockedService.AssertExpectations(tt)) + + // make the call now + mockedService.TheExampleMethodFunctionalOptions("test", OpNum(1), OpStr("foo")) + + // now assert expectations + assert.True(t, mockedService.AssertExpectations(tt)) + +} + +func Test_Mock_AssertExpectationsFunctionalOptionsType_Empty(t *testing.T) { + t.Parallel() + + var mockedService = new(TestExampleImplementation) + + mockedService.On("TheExampleMethodFunctionalOptions", "test", FunctionalOptions()).Return(nil).Once() + + tt := new(testing.T) + assert.False(t, mockedService.AssertExpectations(tt)) + + // make the call now + mockedService.TheExampleMethodFunctionalOptions("test") + + // now assert expectations + assert.True(t, mockedService.AssertExpectations(tt)) + +} + +func Test_Mock_AssertExpectationsFunctionalOptionsType_Indirectly(t *testing.T) { + t.Parallel() + + var mockedService = new(TestExampleImplementation) + + mockedService.On("TheExampleMethodFunctionalOptions", "test", FunctionalOptions(OpNum(1), OpStr("foo"))).Return(nil).Once() + + tt := new(testing.T) + assert.False(t, mockedService.AssertExpectations(tt)) + + // make the call now + TheExampleMethodFunctionalOptionsIndirect(mockedService) + + // now assert expectations + assert.True(t, mockedService.AssertExpectations(tt)) + +} + +func Test_Mock_AssertExpectationsFunctionalOptionsType_Diff_Func(t *testing.T) { + t.Parallel() + + var mockedService = new(TestExampleImplementation) + + mockedService.On("TheExampleMethodFunctionalOptions", "test", FunctionalOptions(OpStr("this"))).Return(nil).Once() + + tt := new(testing.T) + assert.False(t, mockedService.AssertExpectations(tt)) + + assert.Panics(t, func() { + mockedService.TheExampleMethodFunctionalOptions("test", OpBytes([]byte("this"))) + }) +} + +func Test_Mock_AssertExpectationsFunctionalOptionsType_Diff_Arg(t *testing.T) { + t.Parallel() + + var mockedService = new(TestExampleImplementation) + + mockedService.On("TheExampleMethodFunctionalOptions", "test", FunctionalOptions(OpStr("this"))).Return(nil).Once() + + tt := new(testing.T) + assert.False(t, mockedService.AssertExpectations(tt)) + + assert.Panics(t, func() { + mockedService.TheExampleMethodFunctionalOptions("test", OpStr("that")) + }) +} + +func Test_Mock_AssertExpectations_With_Repeatability(t *testing.T) { + t.Parallel() + + var mockedService = new(TestExampleImplementation) + + mockedService.On("Test_Mock_AssertExpectations_With_Repeatability", 1, 2, 3).Return(5, 6, 7).Twice() + + tt := new(testing.T) + assert.False(t, mockedService.AssertExpectations(tt)) + + // make the call now + mockedService.Called(1, 2, 3) + + assert.False(t, mockedService.AssertExpectations(tt)) + + mockedService.Called(1, 2, 3) + + // now assert expectations + assert.True(t, mockedService.AssertExpectations(tt)) + +} + +func Test_Mock_AssertExpectations_Skipped_Test(t *testing.T) { + t.Parallel() + + var mockedService = new(TestExampleImplementation) + + mockedService.On("Test_Mock_AssertExpectations_Skipped_Test", 1, 2, 3).Return(5, 6, 7) + defer mockedService.AssertExpectations(t) + + t.Skip("skipping test to ensure AssertExpectations does not fail") +} + +func Test_Mock_TwoCallsWithDifferentArguments(t *testing.T) { + t.Parallel() + + var mockedService = new(TestExampleImplementation) + + mockedService.On("Test_Mock_TwoCallsWithDifferentArguments", 1, 2, 3).Return(5, 6, 7) + mockedService.On("Test_Mock_TwoCallsWithDifferentArguments", 4, 5, 6).Return(5, 6, 7) + + args1 := mockedService.Called(1, 2, 3) + assert.Equal(t, 5, args1.Int(0)) + assert.Equal(t, 6, args1.Int(1)) + assert.Equal(t, 7, args1.Int(2)) + + args2 := mockedService.Called(4, 5, 6) + assert.Equal(t, 5, args2.Int(0)) + assert.Equal(t, 6, args2.Int(1)) + assert.Equal(t, 7, args2.Int(2)) + +} + +func Test_Mock_AssertNumberOfCalls(t *testing.T) { + t.Parallel() + + var mockedService = new(TestExampleImplementation) + + mockedService.On("Test_Mock_AssertNumberOfCalls", 1, 2, 3).Return(5, 6, 7) + + mockedService.Called(1, 2, 3) + assert.True(t, mockedService.AssertNumberOfCalls(t, "Test_Mock_AssertNumberOfCalls", 1)) + + mockedService.Called(1, 2, 3) + assert.True(t, mockedService.AssertNumberOfCalls(t, "Test_Mock_AssertNumberOfCalls", 2)) + +} + +func Test_Mock_AssertCalled(t *testing.T) { + t.Parallel() + + var mockedService = new(TestExampleImplementation) + + mockedService.On("Test_Mock_AssertCalled", 1, 2, 3).Return(5, 6, 7) + + mockedService.Called(1, 2, 3) + + assert.True(t, mockedService.AssertCalled(t, "Test_Mock_AssertCalled", 1, 2, 3)) + +} + +func Test_Mock_AssertCalled_WithAnythingOfTypeArgument(t *testing.T) { + t.Parallel() + + var mockedService = new(TestExampleImplementation) + + mockedService. + On("Test_Mock_AssertCalled_WithAnythingOfTypeArgument", Anything, Anything, Anything). + Return() + + mockedService.Called(1, "two", []uint8("three")) + + assert.True(t, mockedService.AssertCalled(t, "Test_Mock_AssertCalled_WithAnythingOfTypeArgument", AnythingOfType("int"), AnythingOfType("string"), AnythingOfType("[]uint8"))) + +} + +func Test_Mock_AssertCalled_WithArguments(t *testing.T) { + t.Parallel() + + var mockedService = new(TestExampleImplementation) + + mockedService.On("Test_Mock_AssertCalled_WithArguments", 1, 2, 3).Return(5, 6, 7) + + mockedService.Called(1, 2, 3) + + tt := new(testing.T) + assert.True(t, mockedService.AssertCalled(tt, "Test_Mock_AssertCalled_WithArguments", 1, 2, 3)) + assert.False(t, mockedService.AssertCalled(tt, "Test_Mock_AssertCalled_WithArguments", 2, 3, 4)) + +} + +func Test_Mock_AssertCalled_WithArguments_With_Repeatability(t *testing.T) { + t.Parallel() + + var mockedService = new(TestExampleImplementation) + + mockedService.On("Test_Mock_AssertCalled_WithArguments_With_Repeatability", 1, 2, 3).Return(5, 6, 7).Once() + mockedService.On("Test_Mock_AssertCalled_WithArguments_With_Repeatability", 2, 3, 4).Return(5, 6, 7).Once() + + mockedService.Called(1, 2, 3) + mockedService.Called(2, 3, 4) + + tt := new(testing.T) + assert.True(t, mockedService.AssertCalled(tt, "Test_Mock_AssertCalled_WithArguments_With_Repeatability", 1, 2, 3)) + assert.True(t, mockedService.AssertCalled(tt, "Test_Mock_AssertCalled_WithArguments_With_Repeatability", 2, 3, 4)) + assert.False(t, mockedService.AssertCalled(tt, "Test_Mock_AssertCalled_WithArguments_With_Repeatability", 3, 4, 5)) + +} + +func Test_Mock_AssertNotCalled(t *testing.T) { + t.Parallel() + + var mockedService = new(TestExampleImplementation) + + mockedService.On("Test_Mock_AssertNotCalled", 1, 2, 3).Return(5, 6, 7) + + mockedService.Called(1, 2, 3) + + assert.True(t, mockedService.AssertNotCalled(t, "Test_Mock_NotCalled")) + +} + +func Test_Mock_IsMethodCallable(t *testing.T) { + t.Parallel() + + var mockedService = new(TestExampleImplementation) + + arg := []Call{{Repeatability: 1}, {Repeatability: 2}} + arg2 := []Call{{Repeatability: 1}, {Repeatability: 1}} + arg3 := []Call{{Repeatability: 1}, {Repeatability: 1}} + + mockedService.On("Test_Mock_IsMethodCallable", arg2).Return(true).Twice() + + assert.False(t, mockedService.IsMethodCallable(t, "Test_Mock_IsMethodCallable", arg)) + assert.True(t, mockedService.IsMethodCallable(t, "Test_Mock_IsMethodCallable", arg2)) + assert.True(t, mockedService.IsMethodCallable(t, "Test_Mock_IsMethodCallable", arg3)) + + mockedService.MethodCalled("Test_Mock_IsMethodCallable", arg2) + mockedService.MethodCalled("Test_Mock_IsMethodCallable", arg2) + + assert.False(t, mockedService.IsMethodCallable(t, "Test_Mock_IsMethodCallable", arg2)) +} + +func TestIsArgsEqual(t *testing.T) { + t.Parallel() + + var expected = Arguments{5, 3, 4, 6, 7, 2} + + // Copy elements 1 to 5 + args := append(([]interface{})(nil), expected[1:]...) + args[2] = expected[1] + assert.False(t, isArgsEqual(expected, args)) + + // Clone + arr := append(([]interface{})(nil), expected...) + assert.True(t, isArgsEqual(expected, arr)) +} + +func Test_Mock_AssertOptional(t *testing.T) { + t.Parallel() + + // Optional called + var ms1 = new(TestExampleImplementation) + ms1.On("TheExampleMethod", 1, 2, 3).Maybe().Return(4, nil) + ms1.TheExampleMethod(1, 2, 3) + + tt1 := new(testing.T) + assert.Equal(t, true, ms1.AssertExpectations(tt1)) + + // Optional not called + var ms2 = new(TestExampleImplementation) + ms2.On("TheExampleMethod", 1, 2, 3).Maybe().Return(4, nil) + + tt2 := new(testing.T) + assert.Equal(t, true, ms2.AssertExpectations(tt2)) + + // Non-optional called + var ms3 = new(TestExampleImplementation) + ms3.On("TheExampleMethod", 1, 2, 3).Return(4, nil) + ms3.TheExampleMethod(1, 2, 3) + + tt3 := new(testing.T) + assert.Equal(t, true, ms3.AssertExpectations(tt3)) +} + +/* +Arguments helper methods +*/ +func Test_Arguments_Get(t *testing.T) { + t.Parallel() + + var args = Arguments([]interface{}{"string", 123, true}) + + assert.Equal(t, "string", args.Get(0).(string)) + assert.Equal(t, 123, args.Get(1).(int)) + assert.Equal(t, true, args.Get(2).(bool)) + +} + +func Test_Arguments_Is(t *testing.T) { + t.Parallel() + + var args = Arguments([]interface{}{"string", 123, true}) + + assert.True(t, args.Is("string", 123, true)) + assert.False(t, args.Is("wrong", 456, false)) + +} + +func Test_Arguments_Diff(t *testing.T) { + t.Parallel() + + var args = Arguments([]interface{}{"Hello World", 123, true}) + var diff string + var count int + diff, count = args.Diff([]interface{}{"Hello World", 456, "false"}) + + assert.Equal(t, 2, count) + assert.Contains(t, diff, `(int=456) != (int=123)`) + assert.Contains(t, diff, `(string=false) != (bool=true)`) + +} + +func Test_Arguments_Diff_DifferentNumberOfArgs(t *testing.T) { + t.Parallel() + + var args = Arguments([]interface{}{"string", 123, true}) + var diff string + var count int + diff, count = args.Diff([]interface{}{"string", 456, "false", "extra"}) + + assert.Equal(t, 3, count) + assert.Contains(t, diff, `(string=extra) != (Missing)`) + +} + +func Test_Arguments_Diff_WithAnythingArgument(t *testing.T) { + t.Parallel() + + var args = Arguments([]interface{}{"string", 123, true}) + var count int + _, count = args.Diff([]interface{}{"string", Anything, true}) + + assert.Equal(t, 0, count) + +} + +func Test_Arguments_Diff_WithAnythingArgument_InActualToo(t *testing.T) { + t.Parallel() + + var args = Arguments([]interface{}{"string", Anything, true}) + var count int + _, count = args.Diff([]interface{}{"string", 123, true}) + + assert.Equal(t, 0, count) + +} + +func Test_Arguments_Diff_WithAnythingOfTypeArgument(t *testing.T) { + t.Parallel() + + var args = Arguments([]interface{}{"string", AnythingOfType("int"), true}) + var count int + _, count = args.Diff([]interface{}{"string", 123, true}) + + assert.Equal(t, 0, count) + +} + +func Test_Arguments_Diff_WithAnythingOfTypeArgument_Failing(t *testing.T) { + t.Parallel() + + var args = Arguments([]interface{}{"string", AnythingOfType("string"), true}) + var count int + var diff string + diff, count = args.Diff([]interface{}{"string", 123, true}) + + assert.Equal(t, 1, count) + assert.Contains(t, diff, `string != type int - (int=123)`) + +} + +func Test_Arguments_Diff_WithIsTypeArgument(t *testing.T) { + t.Parallel() + + var args = Arguments([]interface{}{"string", IsType(0), true}) + var count int + _, count = args.Diff([]interface{}{"string", 123, true}) + + assert.Equal(t, 0, count) +} + +func Test_Arguments_Diff_WithIsTypeArgument_Failing(t *testing.T) { + t.Parallel() + + var args = Arguments([]interface{}{"string", IsType(""), true}) + var count int + var diff string + diff, count = args.Diff([]interface{}{"string", 123, true}) + + assert.Equal(t, 1, count) + assert.Contains(t, diff, `string != type int - (int=123)`) +} + +func Test_Arguments_Diff_WithArgMatcher(t *testing.T) { + t.Parallel() + + matchFn := func(a int) bool { + return a == 123 + } + var args = Arguments([]interface{}{"string", MatchedBy(matchFn), true}) + + diff, count := args.Diff([]interface{}{"string", 124, true}) + assert.Equal(t, 1, count) + assert.Contains(t, diff, `(int=124) not matched by func(int) bool`) + + diff, count = args.Diff([]interface{}{"string", false, true}) + assert.Equal(t, 1, count) + assert.Contains(t, diff, `(bool=false) not matched by func(int) bool`) + + diff, count = args.Diff([]interface{}{"string", 123, false}) + assert.Equal(t, 1, count) + assert.Contains(t, diff, `(int=123) matched by func(int) bool`) + + diff, count = args.Diff([]interface{}{"string", 123, true}) + assert.Equal(t, 0, count) + assert.Contains(t, diff, `No differences.`) +} + +func Test_Arguments_Assert(t *testing.T) { + t.Parallel() + + var args = Arguments([]interface{}{"string", 123, true}) + + assert.True(t, args.Assert(t, "string", 123, true)) + +} + +func Test_Arguments_String_Representation(t *testing.T) { + t.Parallel() + + var args = Arguments([]interface{}{"string", 123, true}) + assert.Equal(t, `string,int,bool`, args.String()) + +} + +func Test_Arguments_String(t *testing.T) { + t.Parallel() + + var args = Arguments([]interface{}{"string", 123, true}) + assert.Equal(t, "string", args.String(0)) + +} + +func Test_Arguments_Error(t *testing.T) { + t.Parallel() + + var err = errors.New("An Error") + var args = Arguments([]interface{}{"string", 123, true, err}) + assert.Equal(t, err, args.Error(3)) + +} + +func Test_Arguments_Error_Nil(t *testing.T) { + t.Parallel() + + var args = Arguments([]interface{}{"string", 123, true, nil}) + assert.Equal(t, nil, args.Error(3)) + +} + +func Test_Arguments_Int(t *testing.T) { + t.Parallel() + + var args = Arguments([]interface{}{"string", 123, true}) + assert.Equal(t, 123, args.Int(1)) + +} + +func Test_Arguments_Bool(t *testing.T) { + t.Parallel() + + var args = Arguments([]interface{}{"string", 123, true}) + assert.Equal(t, true, args.Bool(2)) + +} + +func Test_WaitUntil_Parallel(t *testing.T) { + t.Parallel() + + // make a test impl object + var mockedService = new(TestExampleImplementation) + + ch1 := make(chan time.Time) + ch2 := make(chan time.Time) + + mockedService.Mock.On("TheExampleMethod2", true).Return().WaitUntil(ch2).Run(func(args Arguments) { + ch1 <- time.Now() + }) + + mockedService.Mock.On("TheExampleMethod2", false).Return().WaitUntil(ch1) + + // Lock both goroutines on the .WaitUntil method + go func() { + mockedService.TheExampleMethod2(false) + }() + go func() { + mockedService.TheExampleMethod2(true) + }() + + // Allow the first call to execute, so the second one executes afterwards + ch2 <- time.Now() +} + +func Test_MockMethodCalled(t *testing.T) { + t.Parallel() + + m := new(Mock) + m.On("foo", "hello").Return("world") + + retArgs := m.MethodCalled("foo", "hello") + require.True(t, len(retArgs) == 1) + require.Equal(t, "world", retArgs[0]) + m.AssertExpectations(t) +} + +func Test_MockMethodCalled_Panic(t *testing.T) { + t.Parallel() + + m := new(Mock) + m.On("foo", "hello").Panic("world panics") + + require.PanicsWithValue(t, "world panics", func() { m.MethodCalled("foo", "hello") }) + m.AssertExpectations(t) +} + +// Test to validate fix for racy concurrent call access in MethodCalled() +func Test_MockReturnAndCalledConcurrent(t *testing.T) { + t.Parallel() + + iterations := 1000 + m := &Mock{} + call := m.On("ConcurrencyTestMethod") + + wg := sync.WaitGroup{} + wg.Add(2) + + go func() { + for i := 0; i < iterations; i++ { + call.Return(10) + } + wg.Done() + }() + go func() { + for i := 0; i < iterations; i++ { + ConcurrencyTestMethod(m) + } + wg.Done() + }() + wg.Wait() +} + +type timer struct{ Mock } + +func (s *timer) GetTime(i int) string { + return s.Called(i).Get(0).(string) +} + +func (s *timer) GetTimes(times []int) string { + return s.Called(times).Get(0).(string) +} + +type tCustomLogger struct { + *testing.T + logs []string + errs []string +} + +func (tc *tCustomLogger) Logf(format string, args ...interface{}) { + tc.T.Logf(format, args...) + tc.logs = append(tc.logs, fmt.Sprintf(format, args...)) +} + +func (tc *tCustomLogger) Errorf(format string, args ...interface{}) { + tc.errs = append(tc.errs, fmt.Sprintf(format, args...)) +} + +func (tc *tCustomLogger) FailNow() {} + +func TestLoggingAssertExpectations(t *testing.T) { + t.Parallel() + + m := new(timer) + m.On("GetTime", 0).Return("") + tcl := &tCustomLogger{t, []string{}, []string{}} + + AssertExpectationsForObjects(tcl, m, new(TestExampleImplementation)) + + require.Equal(t, 1, len(tcl.errs)) + assert.Regexp(t, regexp.MustCompile("(?s)FAIL: 0 out of 1 expectation\\(s\\) were met.*The code you are testing needs to make 1 more call\\(s\\).*"), tcl.errs[0]) + require.Equal(t, 2, len(tcl.logs)) + assert.Regexp(t, regexp.MustCompile("(?s)FAIL:\tGetTime\\(int\\).*"), tcl.logs[0]) + require.Equal(t, "Expectations didn't match for Mock: *mock.timer", tcl.logs[1]) +} + +func TestAfterTotalWaitTimeWhileExecution(t *testing.T) { + t.Parallel() + + waitDuration := 1 + total, waitMs := 5, time.Millisecond*time.Duration(waitDuration) + aTimer := new(timer) + for i := 0; i < total; i++ { + aTimer.On("GetTime", i).After(waitMs).Return(fmt.Sprintf("Time%d", i)).Once() + } + time.Sleep(waitMs) + start := time.Now() + var results []string + + for i := 0; i < total; i++ { + results = append(results, aTimer.GetTime(i)) + } + + end := time.Now() + elapsedTime := end.Sub(start) + assert.True(t, elapsedTime > waitMs, fmt.Sprintf("Total elapsed time:%v should be at least greater than %v", elapsedTime, waitMs)) + assert.Equal(t, total, len(results)) + for i := range results { + assert.Equal(t, fmt.Sprintf("Time%d", i), results[i], "Return value of method should be same") + } +} + +func TestArgumentMatcherToPrintMismatch(t *testing.T) { + t.Parallel() + + defer func() { + if r := recover(); r != nil { + matchingExp := regexp.MustCompile( + `\s+mock: Unexpected Method Call\s+-*\s+GetTime\(int\)\s+0: 1\s+The closest call I have is:\s+GetTime\(mock.argumentMatcher\)\s+0: mock.argumentMatcher\{.*?\}\s+Diff:.*\(int=1\) not matched by func\(int\) bool\nat: \[[^\]]+mock\/mock_test.go`) + assert.Regexp(t, matchingExp, r) + } + }() + + m := new(timer) + m.On("GetTime", MatchedBy(func(i int) bool { return false })).Return("SomeTime").Once() + + res := m.GetTime(1) + require.Equal(t, "SomeTime", res) + m.AssertExpectations(t) +} + +func TestArgumentMatcherToPrintMismatchWithReferenceType(t *testing.T) { + t.Parallel() + + defer func() { + if r := recover(); r != nil { + matchingExp := regexp.MustCompile( + `\s+mock: Unexpected Method Call\s+-*\s+GetTimes\(\[\]int\)\s+0: \[\]int\{1\}\s+The closest call I have is:\s+GetTimes\(mock.argumentMatcher\)\s+0: mock.argumentMatcher\{.*?\}\s+Diff:.*\(\[\]int=\[1\]\) not matched by func\(\[\]int\) bool\nat: \[[^\]]+mock\/mock_test.go`) + assert.Regexp(t, matchingExp, r) + } + }() + + m := new(timer) + m.On("GetTimes", MatchedBy(func(_ []int) bool { return false })).Return("SomeTime").Once() + + res := m.GetTimes([]int{1}) + require.Equal(t, "SomeTime", res) + m.AssertExpectations(t) +} + +func TestClosestCallMismatchedArgumentInformationShowsTheClosest(t *testing.T) { + t.Parallel() + + defer func() { + if r := recover(); r != nil { + matchingExp := regexp.MustCompile(unexpectedCallRegex(`TheExampleMethod(int,int,int)`, `0: 1\s+1: 1\s+2: 2`, `0: 1\s+1: 1\s+2: 1`, `Diff: 0: PASS: \(int=1\) == \(int=1\)\s+1: PASS: \(int=1\) == \(int=1\)\s+2: FAIL: \(int=2\) != \(int=1\)`)) + assert.Regexp(t, matchingExp, r) + } + }() + + m := new(TestExampleImplementation) + m.On("TheExampleMethod", 1, 1, 1).Return(1, nil).Once() + m.On("TheExampleMethod", 2, 2, 2).Return(2, nil).Once() + + m.TheExampleMethod(1, 1, 2) +} + +func TestClosestCallFavorsFirstMock(t *testing.T) { + t.Parallel() + + defer func() { + if r := recover(); r != nil { + diffRegExp := `Difference found in argument 0:\s+--- Expected\s+\+\+\+ Actual\s+@@ -2,4 \+2,4 @@\s+\(bool\) true,\s+- \(bool\) true,\s+- \(bool\) true\s+\+ \(bool\) false,\s+\+ \(bool\) false\s+}\s+Diff: 0: FAIL: \(\[\]bool=\[(true\s?|false\s?){3}]\) != \(\[\]bool=\[(true\s?|false\s?){3}\]\)` + matchingExp := regexp.MustCompile(unexpectedCallRegex(`TheExampleMethod7([]bool)`, `0: \[\]bool{true, false, false}`, `0: \[\]bool{true, true, true}`, diffRegExp)) + assert.Regexp(t, matchingExp, r) + } + }() + + m := new(TestExampleImplementation) + m.On("TheExampleMethod7", []bool{true, true, true}).Return(nil).Once() + m.On("TheExampleMethod7", []bool{false, false, false}).Return(nil).Once() + + m.TheExampleMethod7([]bool{true, false, false}) +} + +func TestClosestCallUsesRepeatabilityToFindClosest(t *testing.T) { + t.Parallel() + + defer func() { + if r := recover(); r != nil { + diffRegExp := `Difference found in argument 0:\s+--- Expected\s+\+\+\+ Actual\s+@@ -1,4 \+1,4 @@\s+\(\[\]bool\) \(len=3\) {\s+- \(bool\) false,\s+- \(bool\) false,\s+\+ \(bool\) true,\s+\+ \(bool\) true,\s+\(bool\) false\s+Diff: 0: FAIL: \(\[\]bool=\[(true\s?|false\s?){3}]\) != \(\[\]bool=\[(true\s?|false\s?){3}\]\)` + matchingExp := regexp.MustCompile(unexpectedCallRegex(`TheExampleMethod7([]bool)`, `0: \[\]bool{true, true, false}`, `0: \[\]bool{false, false, false}`, diffRegExp)) + assert.Regexp(t, matchingExp, r) + } + }() + + m := new(TestExampleImplementation) + m.On("TheExampleMethod7", []bool{true, true, true}).Return(nil).Once() + m.On("TheExampleMethod7", []bool{false, false, false}).Return(nil).Once() + + m.TheExampleMethod7([]bool{true, true, true}) + + // Since the first mocked call has already been used, it now has no repeatability, + // thus the second mock should be shown as the closest match + m.TheExampleMethod7([]bool{true, true, false}) +} + +func TestClosestCallMismatchedArgumentValueInformation(t *testing.T) { + t.Parallel() + + defer func() { + if r := recover(); r != nil { + matchingExp := regexp.MustCompile(unexpectedCallRegex(`GetTime(int)`, "0: 1", "0: 999", `Diff: 0: FAIL: \(int=1\) != \(int=999\)`)) + assert.Regexp(t, matchingExp, r) + } + }() + + m := new(timer) + m.On("GetTime", 999).Return("SomeTime").Once() + + _ = m.GetTime(1) +} + +func Test_isBetterMatchThanReturnsFalseIfCandidateCallIsNil(t *testing.T) { + t.Parallel() + + assert.False(t, matchCandidate{}.isBetterMatchThan(matchCandidate{})) +} + +func Test_isBetterMatchThanReturnsTrueIfOtherCandidateCallIsNil(t *testing.T) { + t.Parallel() + + assert.True(t, matchCandidate{call: &Call{}}.isBetterMatchThan(matchCandidate{})) +} + +func Test_isBetterMatchThanReturnsFalseIfDiffCountIsGreaterThanOther(t *testing.T) { + t.Parallel() + + assert.False(t, matchCandidate{call: &Call{}, diffCount: 2}.isBetterMatchThan(matchCandidate{call: &Call{}, diffCount: 1})) +} + +func Test_isBetterMatchThanReturnsTrueIfDiffCountIsLessThanOther(t *testing.T) { + t.Parallel() + + assert.True(t, matchCandidate{call: &Call{}, diffCount: 1}.isBetterMatchThan(matchCandidate{call: &Call{}, diffCount: 2})) +} + +func Test_isBetterMatchThanReturnsTrueIfRepeatabilityIsGreaterThanOther(t *testing.T) { + t.Parallel() + + assert.True(t, matchCandidate{call: &Call{Repeatability: 1}, diffCount: 1}.isBetterMatchThan(matchCandidate{call: &Call{Repeatability: -1}, diffCount: 1})) +} + +func Test_isBetterMatchThanReturnsFalseIfRepeatabilityIsLessThanOrEqualToOther(t *testing.T) { + t.Parallel() + + assert.False(t, matchCandidate{call: &Call{Repeatability: 1}, diffCount: 1}.isBetterMatchThan(matchCandidate{call: &Call{Repeatability: 1}, diffCount: 1})) +} + +func unexpectedCallRegex(method, calledArg, expectedArg, diff string) string { + rMethod := regexp.QuoteMeta(method) + return fmt.Sprintf(`\s+mock: Unexpected Method Call\s+-*\s+%s\s+%s\s+The closest call I have is:\s+%s\s+%s\s+%s\nat: \[[^\]]+mock\/mock_test.go`, + rMethod, calledArg, rMethod, expectedArg, diff) +} + +//go:noinline +func ConcurrencyTestMethod(m *Mock) { + m.Called() +} + +func TestConcurrentArgumentRead(t *testing.T) { + t.Parallel() + + methodUnderTest := func(c caller, u user) { + go u.Use(c) + c.Call() + } + + c := &mockCaller{} + defer c.AssertExpectations(t) + + u := &mockUser{} + defer u.AssertExpectations(t) + + done := make(chan struct{}) + + c.On("Call").Return().Once() + u.On("Use", c).Return().Once().Run(func(args Arguments) { close(done) }) + + methodUnderTest(c, u) + <-done // wait until Use is called or assertions will fail +} + +type caller interface { + Call() +} + +type mockCaller struct{ Mock } + +func (m *mockCaller) Call() { m.Called() } + +type user interface { + Use(caller) +} + +type mockUser struct{ Mock } + +func (m *mockUser) Use(c caller) { m.Called(c) } + +type mutatingStringer struct { + N int + s string +} + +func (m *mutatingStringer) String() string { + m.s = strconv.Itoa(m.N) + return m.s +} + +func TestIssue1785ArgumentWithMutatingStringer(t *testing.T) { + m := &Mock{} + m.On("Method", &mutatingStringer{N: 2}) + m.On("Method", &mutatingStringer{N: 1}) + m.MethodCalled("Method", &mutatingStringer{N: 1}) + m.MethodCalled("Method", &mutatingStringer{N: 2}) + m.AssertExpectations(t) +} diff --git a/pkg/testify/package_test.go b/pkg/testify/package_test.go new file mode 100644 index 00000000..44504ccc --- /dev/null +++ b/pkg/testify/package_test.go @@ -0,0 +1,13 @@ +package testify + +import ( + testing "github.com/CodSpeedHQ/codspeed-go/testing/testing" + + "github.com/CodSpeedHQ/codspeed-go/pkg/testify/assert" +) + +func TestImports(t *testing.T) { + if assert.Equal(t, 1, 1) != true { + t.Error("Something is wrong.") + } +} diff --git a/pkg/testify/require/doc.go b/pkg/testify/require/doc.go new file mode 100644 index 00000000..d8eb275c --- /dev/null +++ b/pkg/testify/require/doc.go @@ -0,0 +1,31 @@ +// Package require implements the same assertions as the `assert` package but +// stops test execution when a test fails. +// +// # Example Usage +// +// The following is a complete example using require in a standard test function: +// +// import ( +// "testing" +// "github.com/CodSpeedHQ/codspeed-go/pkg/testify/require" +// ) +// +// func TestSomething(t *testing.T) { +// +// var a string = "Hello" +// var b string = "Hello" +// +// require.Equal(t, a, b, "The two words should be the same.") +// +// } +// +// # Assertions +// +// The `require` package have same global functions as in the `assert` package, +// but instead of returning a boolean result they call `t.FailNow()`. +// A consequence of this is that it must be called from the goroutine running +// the test function, not from other goroutines created during the test. +// +// Every assertion function also takes an optional string message as the final argument, +// allowing custom error messages to be appended to the message the assertion method outputs. +package require diff --git a/pkg/testify/require/forward_requirements.go b/pkg/testify/require/forward_requirements.go new file mode 100644 index 00000000..1dcb2338 --- /dev/null +++ b/pkg/testify/require/forward_requirements.go @@ -0,0 +1,16 @@ +package require + +// Assertions provides assertion methods around the +// TestingT interface. +type Assertions struct { + t TestingT +} + +// New makes a new Assertions object for the specified TestingT. +func New(t TestingT) *Assertions { + return &Assertions{ + t: t, + } +} + +//go:generate sh -c "cd ../_codegen && go build && cd - && ../_codegen/_codegen -output-package=require -template=require_forward.go.tmpl -include-format-funcs" diff --git a/pkg/testify/require/forward_requirements_test.go b/pkg/testify/require/forward_requirements_test.go new file mode 100644 index 00000000..f62e19d0 --- /dev/null +++ b/pkg/testify/require/forward_requirements_test.go @@ -0,0 +1,623 @@ +package require + +import ( + "errors" + testing "github.com/CodSpeedHQ/codspeed-go/testing/testing" + "time" +) + +func TestImplementsWrapper(t *testing.T) { + t.Parallel() + + require := New(t) + + require.Implements((*AssertionTesterInterface)(nil), new(AssertionTesterConformingObject)) + + mockT := new(MockT) + mockRequire := New(mockT) + mockRequire.Implements((*AssertionTesterInterface)(nil), new(AssertionTesterNonConformingObject)) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestIsNotTypeWrapper(t *testing.T) { + t.Parallel() + + require := New(t) + require.IsNotType(new(AssertionTesterNonConformingObject), new(AssertionTesterConformingObject)) + + mockT := new(MockT) + mockRequire := New(mockT) + mockRequire.IsNotType(new(AssertionTesterConformingObject), new(AssertionTesterConformingObject)) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestIsTypeWrapper(t *testing.T) { + t.Parallel() + + require := New(t) + require.IsType(new(AssertionTesterConformingObject), new(AssertionTesterConformingObject)) + + mockT := new(MockT) + mockRequire := New(mockT) + mockRequire.IsType(new(AssertionTesterConformingObject), new(AssertionTesterNonConformingObject)) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestEqualWrapper(t *testing.T) { + t.Parallel() + + require := New(t) + require.Equal(1, 1) + + mockT := new(MockT) + mockRequire := New(mockT) + mockRequire.Equal(1, 2) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestNotEqualWrapper(t *testing.T) { + t.Parallel() + + require := New(t) + require.NotEqual(1, 2) + + mockT := new(MockT) + mockRequire := New(mockT) + mockRequire.NotEqual(2, 2) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestExactlyWrapper(t *testing.T) { + t.Parallel() + + require := New(t) + + a := float32(1) + b := float32(1) + c := float64(1) + + require.Exactly(a, b) + + mockT := new(MockT) + mockRequire := New(mockT) + mockRequire.Exactly(a, c) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestNotNilWrapper(t *testing.T) { + t.Parallel() + + require := New(t) + require.NotNil(t, new(AssertionTesterConformingObject)) + + mockT := new(MockT) + mockRequire := New(mockT) + mockRequire.NotNil(nil) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestNilWrapper(t *testing.T) { + t.Parallel() + + require := New(t) + require.Nil(nil) + + mockT := new(MockT) + mockRequire := New(mockT) + mockRequire.Nil(new(AssertionTesterConformingObject)) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestTrueWrapper(t *testing.T) { + t.Parallel() + + require := New(t) + require.True(true) + + mockT := new(MockT) + mockRequire := New(mockT) + mockRequire.True(false) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestFalseWrapper(t *testing.T) { + t.Parallel() + + require := New(t) + require.False(false) + + mockT := new(MockT) + mockRequire := New(mockT) + mockRequire.False(true) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestContainsWrapper(t *testing.T) { + t.Parallel() + + require := New(t) + require.Contains("Hello World", "Hello") + + mockT := new(MockT) + mockRequire := New(mockT) + mockRequire.Contains("Hello World", "Salut") + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestNotContainsWrapper(t *testing.T) { + t.Parallel() + + require := New(t) + require.NotContains("Hello World", "Hello!") + + mockT := new(MockT) + mockRequire := New(mockT) + mockRequire.NotContains("Hello World", "Hello") + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestPanicsWrapper(t *testing.T) { + t.Parallel() + + require := New(t) + require.Panics(func() { + panic("Panic!") + }) + + mockT := new(MockT) + mockRequire := New(mockT) + mockRequire.Panics(func() {}) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestNotPanicsWrapper(t *testing.T) { + t.Parallel() + + require := New(t) + require.NotPanics(func() {}) + + mockT := new(MockT) + mockRequire := New(mockT) + mockRequire.NotPanics(func() { + panic("Panic!") + }) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestNoErrorWrapper(t *testing.T) { + t.Parallel() + + require := New(t) + require.NoError(nil) + + mockT := new(MockT) + mockRequire := New(mockT) + mockRequire.NoError(errors.New("some error")) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestErrorWrapper(t *testing.T) { + t.Parallel() + + require := New(t) + require.Error(errors.New("some error")) + + mockT := new(MockT) + mockRequire := New(mockT) + mockRequire.Error(nil) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestErrorContainsWrapper(t *testing.T) { + t.Parallel() + + require := New(t) + require.ErrorContains(errors.New("some error: another error"), "some error") + + mockT := new(MockT) + mockRequire := New(mockT) + mockRequire.ErrorContains(errors.New("some error: another error"), "different error") + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestEqualErrorWrapper(t *testing.T) { + t.Parallel() + + require := New(t) + require.EqualError(errors.New("some error"), "some error") + + mockT := new(MockT) + mockRequire := New(mockT) + mockRequire.EqualError(errors.New("some error"), "Not some error") + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestEmptyWrapper(t *testing.T) { + t.Parallel() + + require := New(t) + require.Empty("") + + mockT := new(MockT) + mockRequire := New(mockT) + mockRequire.Empty("x") + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestNotEmptyWrapper(t *testing.T) { + t.Parallel() + + require := New(t) + require.NotEmpty("x") + + mockT := new(MockT) + mockRequire := New(mockT) + mockRequire.NotEmpty("") + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestWithinDurationWrapper(t *testing.T) { + t.Parallel() + + require := New(t) + a := time.Now() + b := a.Add(10 * time.Second) + + require.WithinDuration(a, b, 15*time.Second) + + mockT := new(MockT) + mockRequire := New(mockT) + mockRequire.WithinDuration(a, b, 5*time.Second) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestInDeltaWrapper(t *testing.T) { + t.Parallel() + + require := New(t) + require.InDelta(1.001, 1, 0.01) + + mockT := new(MockT) + mockRequire := New(mockT) + mockRequire.InDelta(1, 2, 0.5) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestZeroWrapper(t *testing.T) { + t.Parallel() + + require := New(t) + require.Zero(0) + + mockT := new(MockT) + mockRequire := New(mockT) + mockRequire.Zero(1) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestNotZeroWrapper(t *testing.T) { + t.Parallel() + + require := New(t) + require.NotZero(1) + + mockT := new(MockT) + mockRequire := New(mockT) + mockRequire.NotZero(0) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestJSONEqWrapper_EqualSONString(t *testing.T) { + t.Parallel() + + mockT := new(MockT) + mockRequire := New(mockT) + + mockRequire.JSONEq(`{"hello": "world", "foo": "bar"}`, `{"hello": "world", "foo": "bar"}`) + if mockT.Failed { + t.Error("Check should pass") + } +} + +func TestJSONEqWrapper_EquivalentButNotEqual(t *testing.T) { + t.Parallel() + + mockT := new(MockT) + mockRequire := New(mockT) + + mockRequire.JSONEq(`{"hello": "world", "foo": "bar"}`, `{"foo": "bar", "hello": "world"}`) + if mockT.Failed { + t.Error("Check should pass") + } +} + +func TestJSONEqWrapper_HashOfArraysAndHashes(t *testing.T) { + t.Parallel() + + 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}") + if mockT.Failed { + t.Error("Check should pass") + } +} + +func TestJSONEqWrapper_Array(t *testing.T) { + t.Parallel() + + mockT := new(MockT) + mockRequire := New(mockT) + + mockRequire.JSONEq(`["foo", {"hello": "world", "nested": "hash"}]`, `["foo", {"nested": "hash", "hello": "world"}]`) + if mockT.Failed { + t.Error("Check should pass") + } +} + +func TestJSONEqWrapper_HashAndArrayNotEquivalent(t *testing.T) { + t.Parallel() + + mockT := new(MockT) + mockRequire := New(mockT) + + mockRequire.JSONEq(`["foo", {"hello": "world", "nested": "hash"}]`, `{"foo": "bar", {"nested": "hash", "hello": "world"}}`) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestJSONEqWrapper_HashesNotEquivalent(t *testing.T) { + t.Parallel() + + mockT := new(MockT) + mockRequire := New(mockT) + + mockRequire.JSONEq(`{"foo": "bar"}`, `{"foo": "bar", "hello": "world"}`) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestJSONEqWrapper_ActualIsNotJSON(t *testing.T) { + t.Parallel() + + mockT := new(MockT) + mockRequire := New(mockT) + + mockRequire.JSONEq(`{"foo": "bar"}`, "Not JSON") + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestJSONEqWrapper_ExpectedIsNotJSON(t *testing.T) { + t.Parallel() + + mockT := new(MockT) + mockRequire := New(mockT) + + mockRequire.JSONEq("Not JSON", `{"foo": "bar", "hello": "world"}`) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestJSONEqWrapper_ExpectedAndActualNotJSON(t *testing.T) { + t.Parallel() + + mockT := new(MockT) + mockRequire := New(mockT) + + mockRequire.JSONEq("Not JSON", "Not JSON") + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestJSONEqWrapper_ArraysOfDifferentOrder(t *testing.T) { + t.Parallel() + + mockT := new(MockT) + mockRequire := New(mockT) + + mockRequire.JSONEq(`["foo", {"hello": "world", "nested": "hash"}]`, `[{ "hello": "world", "nested": "hash"}, "foo"]`) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestYAMLEqWrapper_EqualYAMLString(t *testing.T) { + t.Parallel() + + mockT := new(MockT) + mockRequire := New(mockT) + + mockRequire.YAMLEq(`{"hello": "world", "foo": "bar"}`, `{"hello": "world", "foo": "bar"}`) + if mockT.Failed { + t.Error("Check should pass") + } +} + +func TestYAMLEqWrapper_EquivalentButNotEqual(t *testing.T) { + t.Parallel() + + mockT := new(MockT) + mockRequire := New(mockT) + + mockRequire.YAMLEq(`{"hello": "world", "foo": "bar"}`, `{"foo": "bar", "hello": "world"}`) + if mockT.Failed { + t.Error("Check should pass") + } +} + +func TestYAMLEqWrapper_HashOfArraysAndHashes(t *testing.T) { + t.Parallel() + + mockT := new(MockT) + mockRequire := 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] +` + + mockRequire.YAMLEq(expected, actual) + if mockT.Failed { + t.Error("Check should pass") + } +} + +func TestYAMLEqWrapper_Array(t *testing.T) { + t.Parallel() + + mockT := new(MockT) + mockRequire := New(mockT) + + mockRequire.YAMLEq(`["foo", {"hello": "world", "nested": "hash"}]`, `["foo", {"nested": "hash", "hello": "world"}]`) + if mockT.Failed { + t.Error("Check should pass") + } +} + +func TestYAMLEqWrapper_HashAndArrayNotEquivalent(t *testing.T) { + t.Parallel() + + mockT := new(MockT) + mockRequire := New(mockT) + + mockRequire.YAMLEq(`["foo", {"hello": "world", "nested": "hash"}]`, `{"foo": "bar", {"nested": "hash", "hello": "world"}}`) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestYAMLEqWrapper_HashesNotEquivalent(t *testing.T) { + t.Parallel() + + mockT := new(MockT) + mockRequire := New(mockT) + + mockRequire.YAMLEq(`{"foo": "bar"}`, `{"foo": "bar", "hello": "world"}`) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestYAMLEqWrapper_ActualIsSimpleString(t *testing.T) { + t.Parallel() + + mockT := new(MockT) + mockRequire := New(mockT) + + mockRequire.YAMLEq(`{"foo": "bar"}`, "Simple String") + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestYAMLEqWrapper_ExpectedIsSimpleString(t *testing.T) { + t.Parallel() + + mockT := new(MockT) + mockRequire := New(mockT) + + mockRequire.YAMLEq("Simple String", `{"foo": "bar", "hello": "world"}`) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestYAMLEqWrapper_ExpectedAndActualSimpleString(t *testing.T) { + t.Parallel() + + mockT := new(MockT) + mockRequire := New(mockT) + + mockRequire.YAMLEq("Simple String", "Simple String") + if mockT.Failed { + t.Error("Check should pass") + } +} + +func TestYAMLEqWrapper_ArraysOfDifferentOrder(t *testing.T) { + t.Parallel() + + mockT := new(MockT) + mockRequire := New(mockT) + + mockRequire.YAMLEq(`["foo", {"hello": "world", "nested": "hash"}]`, `[{ "hello": "world", "nested": "hash"}, "foo"]`) + if !mockT.Failed { + t.Error("Check should fail") + } +} diff --git a/pkg/testify/require/require.go b/pkg/testify/require/require.go new file mode 100644 index 00000000..607f075f --- /dev/null +++ b/pkg/testify/require/require.go @@ -0,0 +1,2180 @@ +// Code generated with github.com/stretchr/testify/_codegen; DO NOT EDIT. + +package require + +import ( + assert "github.com/CodSpeedHQ/codspeed-go/pkg/testify/assert" + http "net/http" + url "net/url" + time "time" +) + +// Condition uses a Comparison to assert a complex condition. +func Condition(t TestingT, comp assert.Comparison, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Condition(t, comp, msgAndArgs...) { + return + } + t.FailNow() +} + +// Conditionf uses a Comparison to assert a complex condition. +func Conditionf(t TestingT, comp assert.Comparison, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Conditionf(t, comp, msg, args...) { + return + } + t.FailNow() +} + +// Contains asserts that the specified string, list(array, slice...) or map contains the +// specified substring or element. +// +// require.Contains(t, "Hello World", "World") +// require.Contains(t, ["Hello", "World"], "World") +// require.Contains(t, {"Hello": "World"}, "Hello") +func Contains(t TestingT, s interface{}, contains interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Contains(t, s, contains, msgAndArgs...) { + return + } + t.FailNow() +} + +// Containsf asserts that the specified string, list(array, slice...) or map contains the +// specified substring or element. +// +// require.Containsf(t, "Hello World", "World", "error message %s", "formatted") +// require.Containsf(t, ["Hello", "World"], "World", "error message %s", "formatted") +// require.Containsf(t, {"Hello": "World"}, "Hello", "error message %s", "formatted") +func Containsf(t TestingT, s interface{}, contains interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Containsf(t, s, contains, msg, args...) { + return + } + t.FailNow() +} + +// DirExists checks whether a directory exists in the given path. It also fails +// if the path is a file rather a directory or there is an error checking whether it exists. +func DirExists(t TestingT, path string, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.DirExists(t, path, msgAndArgs...) { + return + } + t.FailNow() +} + +// DirExistsf checks whether a directory exists in the given path. It also fails +// if the path is a file rather a directory or there is an error checking whether it exists. +func DirExistsf(t TestingT, path string, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.DirExistsf(t, path, msg, args...) { + return + } + t.FailNow() +} + +// ElementsMatch asserts that the specified listA(array, slice...) is equal to specified +// 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]) +func ElementsMatch(t TestingT, listA interface{}, listB interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.ElementsMatch(t, listA, listB, msgAndArgs...) { + return + } + t.FailNow() +} + +// ElementsMatchf asserts that the specified listA(array, slice...) is equal to specified +// 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.ElementsMatchf(t, [1, 3, 2, 3], [1, 3, 3, 2], "error message %s", "formatted") +func ElementsMatchf(t TestingT, listA interface{}, listB interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.ElementsMatchf(t, listA, listB, msg, args...) { + return + } + t.FailNow() +} + +// Empty asserts that the given value is "empty". +// +// [Zero values] are "empty". +// +// Arrays are "empty" if every element is the zero value of the type (stricter than "empty"). +// +// Slices, maps and channels with zero length are "empty". +// +// Pointer values are "empty" if the pointer is nil or if the pointed value is "empty". +// +// require.Empty(t, obj) +// +// [Zero values]: https://go.dev/ref/spec#The_zero_value +func Empty(t TestingT, object interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Empty(t, object, msgAndArgs...) { + return + } + t.FailNow() +} + +// Emptyf asserts that the given value is "empty". +// +// [Zero values] are "empty". +// +// Arrays are "empty" if every element is the zero value of the type (stricter than "empty"). +// +// Slices, maps and channels with zero length are "empty". +// +// Pointer values are "empty" if the pointer is nil or if the pointed value is "empty". +// +// require.Emptyf(t, obj, "error message %s", "formatted") +// +// [Zero values]: https://go.dev/ref/spec#The_zero_value +func Emptyf(t TestingT, object interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Emptyf(t, object, msg, args...) { + return + } + t.FailNow() +} + +// Equal asserts that two objects are equal. +// +// require.Equal(t, 123, 123) +// +// Pointer variable equality is determined based on the equality of the +// referenced values (as opposed to the memory addresses). Function equality +// cannot be determined and will always fail. +func Equal(t TestingT, expected interface{}, actual interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Equal(t, expected, actual, msgAndArgs...) { + return + } + t.FailNow() +} + +// EqualError asserts that a function returned an error (i.e. not `nil`) +// and that it is equal to the provided error. +// +// actualObj, err := SomeFunction() +// require.EqualError(t, err, expectedErrorString) +func EqualError(t TestingT, theError error, errString string, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.EqualError(t, theError, errString, msgAndArgs...) { + return + } + t.FailNow() +} + +// EqualErrorf asserts that a function returned an error (i.e. not `nil`) +// and that it is equal to the provided error. +// +// actualObj, err := SomeFunction() +// require.EqualErrorf(t, err, expectedErrorString, "error message %s", "formatted") +func EqualErrorf(t TestingT, theError error, errString string, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.EqualErrorf(t, theError, errString, msg, args...) { + return + } + t.FailNow() +} + +// EqualExportedValues asserts that the types of two objects are equal and their public +// fields are also equal. This is useful for comparing structs that have private fields +// that could potentially differ. +// +// type S struct { +// Exported int +// notExported int +// } +// require.EqualExportedValues(t, S{1, 2}, S{1, 3}) => true +// require.EqualExportedValues(t, S{1, 2}, S{2, 3}) => false +func EqualExportedValues(t TestingT, expected interface{}, actual interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.EqualExportedValues(t, expected, actual, msgAndArgs...) { + return + } + t.FailNow() +} + +// EqualExportedValuesf asserts that the types of two objects are equal and their public +// fields are also equal. This is useful for comparing structs that have private fields +// that could potentially differ. +// +// type S struct { +// Exported int +// notExported int +// } +// require.EqualExportedValuesf(t, S{1, 2}, S{1, 3}, "error message %s", "formatted") => true +// require.EqualExportedValuesf(t, S{1, 2}, S{2, 3}, "error message %s", "formatted") => false +func EqualExportedValuesf(t TestingT, expected interface{}, actual interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.EqualExportedValuesf(t, expected, actual, msg, args...) { + return + } + t.FailNow() +} + +// EqualValues asserts that two objects are equal or convertible to the larger +// type and equal. +// +// require.EqualValues(t, uint32(123), int32(123)) +func EqualValues(t TestingT, expected interface{}, actual interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.EqualValues(t, expected, actual, msgAndArgs...) { + return + } + t.FailNow() +} + +// EqualValuesf asserts that two objects are equal or convertible to the larger +// type and equal. +// +// require.EqualValuesf(t, uint32(123), int32(123), "error message %s", "formatted") +func EqualValuesf(t TestingT, expected interface{}, actual interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.EqualValuesf(t, expected, actual, msg, args...) { + return + } + t.FailNow() +} + +// Equalf asserts that two objects are equal. +// +// require.Equalf(t, 123, 123, "error message %s", "formatted") +// +// Pointer variable equality is determined based on the equality of the +// referenced values (as opposed to the memory addresses). Function equality +// cannot be determined and will always fail. +func Equalf(t TestingT, expected interface{}, actual interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Equalf(t, expected, actual, msg, args...) { + return + } + t.FailNow() +} + +// Error asserts that a function returned an error (i.e. not `nil`). +// +// actualObj, err := SomeFunction() +// require.Error(t, err) +func Error(t TestingT, err error, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Error(t, err, msgAndArgs...) { + return + } + t.FailNow() +} + +// ErrorAs asserts that at least one of the errors in err's chain matches target, and if so, sets target to that error value. +// This is a wrapper for errors.As. +func ErrorAs(t TestingT, err error, target interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.ErrorAs(t, err, target, msgAndArgs...) { + return + } + t.FailNow() +} + +// ErrorAsf asserts that at least one of the errors in err's chain matches target, and if so, sets target to that error value. +// This is a wrapper for errors.As. +func ErrorAsf(t TestingT, err error, target interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.ErrorAsf(t, err, target, msg, args...) { + return + } + t.FailNow() +} + +// ErrorContains asserts that a function returned an error (i.e. not `nil`) +// and that the error contains the specified substring. +// +// actualObj, err := SomeFunction() +// require.ErrorContains(t, err, expectedErrorSubString) +func ErrorContains(t TestingT, theError error, contains string, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.ErrorContains(t, theError, contains, msgAndArgs...) { + return + } + t.FailNow() +} + +// ErrorContainsf asserts that a function returned an error (i.e. not `nil`) +// and that the error contains the specified substring. +// +// actualObj, err := SomeFunction() +// require.ErrorContainsf(t, err, expectedErrorSubString, "error message %s", "formatted") +func ErrorContainsf(t TestingT, theError error, contains string, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.ErrorContainsf(t, theError, contains, msg, args...) { + return + } + t.FailNow() +} + +// ErrorIs asserts that at least one of the errors in err's chain matches target. +// This is a wrapper for errors.Is. +func ErrorIs(t TestingT, err error, target error, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.ErrorIs(t, err, target, msgAndArgs...) { + return + } + t.FailNow() +} + +// ErrorIsf asserts that at least one of the errors in err's chain matches target. +// This is a wrapper for errors.Is. +func ErrorIsf(t TestingT, err error, target error, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.ErrorIsf(t, err, target, msg, args...) { + return + } + t.FailNow() +} + +// Errorf asserts that a function returned an error (i.e. not `nil`). +// +// actualObj, err := SomeFunction() +// require.Errorf(t, err, "error message %s", "formatted") +func Errorf(t TestingT, err error, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Errorf(t, err, msg, args...) { + return + } + t.FailNow() +} + +// Eventually asserts that given condition will be met in waitFor time, +// periodically checking target function each tick. +// +// require.Eventually(t, func() bool { return true; }, time.Second, 10*time.Millisecond) +func Eventually(t TestingT, condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Eventually(t, condition, waitFor, tick, msgAndArgs...) { + return + } + t.FailNow() +} + +// EventuallyWithT asserts that given condition will be met in waitFor time, +// periodically checking target function each tick. In contrast to Eventually, +// it supplies a CollectT to the condition function, so that the condition +// function can use the CollectT to call other assertions. +// The condition is considered "met" if no errors are raised in a tick. +// The supplied CollectT collects all errors from one tick (if there are any). +// If the condition is not met before waitFor, the collected errors of +// the last tick are copied to t. +// +// externalValue := false +// go func() { +// time.Sleep(8*time.Second) +// externalValue = true +// }() +// require.EventuallyWithT(t, func(c *require.CollectT) { +// // add assertions as needed; any assertion failure will fail the current tick +// require.True(c, externalValue, "expected 'externalValue' to be true") +// }, 10*time.Second, 1*time.Second, "external state has not changed to 'true'; still false") +func EventuallyWithT(t TestingT, condition func(collect *assert.CollectT), waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.EventuallyWithT(t, condition, waitFor, tick, msgAndArgs...) { + return + } + t.FailNow() +} + +// EventuallyWithTf asserts that given condition will be met in waitFor time, +// periodically checking target function each tick. In contrast to Eventually, +// it supplies a CollectT to the condition function, so that the condition +// function can use the CollectT to call other assertions. +// The condition is considered "met" if no errors are raised in a tick. +// The supplied CollectT collects all errors from one tick (if there are any). +// If the condition is not met before waitFor, the collected errors of +// the last tick are copied to t. +// +// externalValue := false +// go func() { +// time.Sleep(8*time.Second) +// externalValue = true +// }() +// require.EventuallyWithTf(t, func(c *require.CollectT, "error message %s", "formatted") { +// // add assertions as needed; any assertion failure will fail the current tick +// require.True(c, externalValue, "expected 'externalValue' to be true") +// }, 10*time.Second, 1*time.Second, "external state has not changed to 'true'; still false") +func EventuallyWithTf(t TestingT, condition func(collect *assert.CollectT), waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.EventuallyWithTf(t, condition, waitFor, tick, msg, args...) { + return + } + t.FailNow() +} + +// Eventuallyf asserts that given condition will be met in waitFor time, +// periodically checking target function each tick. +// +// require.Eventuallyf(t, func() bool { return true; }, time.Second, 10*time.Millisecond, "error message %s", "formatted") +func Eventuallyf(t TestingT, condition func() bool, waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Eventuallyf(t, condition, waitFor, tick, msg, args...) { + return + } + t.FailNow() +} + +// Exactly asserts that two objects are equal in value and type. +// +// require.Exactly(t, int32(123), int64(123)) +func Exactly(t TestingT, expected interface{}, actual interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Exactly(t, expected, actual, msgAndArgs...) { + return + } + t.FailNow() +} + +// Exactlyf asserts that two objects are equal in value and type. +// +// require.Exactlyf(t, int32(123), int64(123), "error message %s", "formatted") +func Exactlyf(t TestingT, expected interface{}, actual interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Exactlyf(t, expected, actual, msg, args...) { + return + } + t.FailNow() +} + +// Fail reports a failure through +func Fail(t TestingT, failureMessage string, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Fail(t, failureMessage, msgAndArgs...) { + return + } + t.FailNow() +} + +// FailNow fails test +func FailNow(t TestingT, failureMessage string, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.FailNow(t, failureMessage, msgAndArgs...) { + return + } + t.FailNow() +} + +// FailNowf fails test +func FailNowf(t TestingT, failureMessage string, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.FailNowf(t, failureMessage, msg, args...) { + return + } + t.FailNow() +} + +// Failf reports a failure through +func Failf(t TestingT, failureMessage string, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Failf(t, failureMessage, msg, args...) { + return + } + t.FailNow() +} + +// False asserts that the specified value is false. +// +// require.False(t, myBool) +func False(t TestingT, value bool, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.False(t, value, msgAndArgs...) { + return + } + t.FailNow() +} + +// Falsef asserts that the specified value is false. +// +// require.Falsef(t, myBool, "error message %s", "formatted") +func Falsef(t TestingT, value bool, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Falsef(t, value, msg, args...) { + return + } + t.FailNow() +} + +// FileExists checks whether a file exists in the given path. It also fails if +// the path points to a directory or there is an error when trying to check the file. +func FileExists(t TestingT, path string, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.FileExists(t, path, msgAndArgs...) { + return + } + t.FailNow() +} + +// FileExistsf checks whether a file exists in the given path. It also fails if +// the path points to a directory or there is an error when trying to check the file. +func FileExistsf(t TestingT, path string, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.FileExistsf(t, path, msg, args...) { + return + } + t.FailNow() +} + +// Greater asserts that the first element is greater than the second +// +// require.Greater(t, 2, 1) +// require.Greater(t, float64(2), float64(1)) +// require.Greater(t, "b", "a") +func Greater(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Greater(t, e1, e2, msgAndArgs...) { + return + } + t.FailNow() +} + +// GreaterOrEqual asserts that the first element is greater than or equal to the second +// +// require.GreaterOrEqual(t, 2, 1) +// require.GreaterOrEqual(t, 2, 2) +// require.GreaterOrEqual(t, "b", "a") +// require.GreaterOrEqual(t, "b", "b") +func GreaterOrEqual(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.GreaterOrEqual(t, e1, e2, msgAndArgs...) { + return + } + t.FailNow() +} + +// GreaterOrEqualf asserts that the first element is greater than or equal to the second +// +// require.GreaterOrEqualf(t, 2, 1, "error message %s", "formatted") +// require.GreaterOrEqualf(t, 2, 2, "error message %s", "formatted") +// require.GreaterOrEqualf(t, "b", "a", "error message %s", "formatted") +// require.GreaterOrEqualf(t, "b", "b", "error message %s", "formatted") +func GreaterOrEqualf(t TestingT, e1 interface{}, e2 interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.GreaterOrEqualf(t, e1, e2, msg, args...) { + return + } + t.FailNow() +} + +// Greaterf asserts that the first element is greater than the second +// +// require.Greaterf(t, 2, 1, "error message %s", "formatted") +// require.Greaterf(t, float64(2), float64(1), "error message %s", "formatted") +// require.Greaterf(t, "b", "a", "error message %s", "formatted") +func Greaterf(t TestingT, e1 interface{}, e2 interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Greaterf(t, e1, e2, msg, args...) { + return + } + t.FailNow() +} + +// HTTPBodyContains asserts that a specified handler returns a +// body that contains a string. +// +// require.HTTPBodyContains(t, myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky") +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPBodyContains(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.HTTPBodyContains(t, handler, method, url, values, str, msgAndArgs...) { + return + } + t.FailNow() +} + +// HTTPBodyContainsf asserts that a specified handler returns a +// body that contains a string. +// +// require.HTTPBodyContainsf(t, myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky", "error message %s", "formatted") +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPBodyContainsf(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.HTTPBodyContainsf(t, handler, method, url, values, str, msg, args...) { + return + } + t.FailNow() +} + +// HTTPBodyNotContains asserts that a specified handler returns a +// body that does not contain a string. +// +// require.HTTPBodyNotContains(t, myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky") +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPBodyNotContains(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.HTTPBodyNotContains(t, handler, method, url, values, str, msgAndArgs...) { + return + } + t.FailNow() +} + +// HTTPBodyNotContainsf asserts that a specified handler returns a +// body that does not contain a string. +// +// require.HTTPBodyNotContainsf(t, myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky", "error message %s", "formatted") +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPBodyNotContainsf(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.HTTPBodyNotContainsf(t, handler, method, url, values, str, msg, args...) { + return + } + t.FailNow() +} + +// HTTPError asserts that a specified handler returns an error status code. +// +// require.HTTPError(t, myHandler, "POST", "/a/b/c", url.Values{"a": []string{"b", "c"}} +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPError(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.HTTPError(t, handler, method, url, values, msgAndArgs...) { + return + } + t.FailNow() +} + +// HTTPErrorf asserts that a specified handler returns an error status code. +// +// require.HTTPErrorf(t, myHandler, "POST", "/a/b/c", url.Values{"a": []string{"b", "c"}} +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPErrorf(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.HTTPErrorf(t, handler, method, url, values, msg, args...) { + return + } + t.FailNow() +} + +// HTTPRedirect asserts that a specified handler returns a redirect status code. +// +// require.HTTPRedirect(t, myHandler, "GET", "/a/b/c", url.Values{"a": []string{"b", "c"}} +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPRedirect(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.HTTPRedirect(t, handler, method, url, values, msgAndArgs...) { + return + } + t.FailNow() +} + +// HTTPRedirectf asserts that a specified handler returns a redirect status code. +// +// require.HTTPRedirectf(t, myHandler, "GET", "/a/b/c", url.Values{"a": []string{"b", "c"}} +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPRedirectf(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.HTTPRedirectf(t, handler, method, url, values, msg, args...) { + return + } + t.FailNow() +} + +// HTTPStatusCode asserts that a specified handler returns a specified status code. +// +// require.HTTPStatusCode(t, myHandler, "GET", "/notImplemented", nil, 501) +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPStatusCode(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, statuscode int, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.HTTPStatusCode(t, handler, method, url, values, statuscode, msgAndArgs...) { + return + } + t.FailNow() +} + +// HTTPStatusCodef asserts that a specified handler returns a specified status code. +// +// require.HTTPStatusCodef(t, myHandler, "GET", "/notImplemented", nil, 501, "error message %s", "formatted") +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPStatusCodef(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, statuscode int, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.HTTPStatusCodef(t, handler, method, url, values, statuscode, msg, args...) { + return + } + t.FailNow() +} + +// HTTPSuccess asserts that a specified handler returns a success status code. +// +// require.HTTPSuccess(t, myHandler, "POST", "http://www.google.com", nil) +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPSuccess(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.HTTPSuccess(t, handler, method, url, values, msgAndArgs...) { + return + } + t.FailNow() +} + +// HTTPSuccessf asserts that a specified handler returns a success status code. +// +// require.HTTPSuccessf(t, myHandler, "POST", "http://www.google.com", nil, "error message %s", "formatted") +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPSuccessf(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.HTTPSuccessf(t, handler, method, url, values, msg, args...) { + return + } + t.FailNow() +} + +// Implements asserts that an object is implemented by the specified interface. +// +// require.Implements(t, (*MyInterface)(nil), new(MyObject)) +func Implements(t TestingT, interfaceObject interface{}, object interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Implements(t, interfaceObject, object, msgAndArgs...) { + return + } + t.FailNow() +} + +// Implementsf asserts that an object is implemented by the specified interface. +// +// require.Implementsf(t, (*MyInterface)(nil), new(MyObject), "error message %s", "formatted") +func Implementsf(t TestingT, interfaceObject interface{}, object interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Implementsf(t, interfaceObject, object, msg, args...) { + return + } + t.FailNow() +} + +// InDelta asserts that the two numerals are within delta of each other. +// +// require.InDelta(t, math.Pi, 22/7.0, 0.01) +func InDelta(t TestingT, expected interface{}, actual interface{}, delta float64, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.InDelta(t, expected, actual, delta, msgAndArgs...) { + return + } + t.FailNow() +} + +// InDeltaMapValues is the same as InDelta, but it compares all values between two maps. Both maps must have exactly the same keys. +func InDeltaMapValues(t TestingT, expected interface{}, actual interface{}, delta float64, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.InDeltaMapValues(t, expected, actual, delta, msgAndArgs...) { + return + } + t.FailNow() +} + +// InDeltaMapValuesf is the same as InDelta, but it compares all values between two maps. Both maps must have exactly the same keys. +func InDeltaMapValuesf(t TestingT, expected interface{}, actual interface{}, delta float64, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.InDeltaMapValuesf(t, expected, actual, delta, msg, args...) { + return + } + t.FailNow() +} + +// InDeltaSlice is the same as InDelta, except it compares two slices. +func InDeltaSlice(t TestingT, expected interface{}, actual interface{}, delta float64, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.InDeltaSlice(t, expected, actual, delta, msgAndArgs...) { + return + } + t.FailNow() +} + +// InDeltaSlicef is the same as InDelta, except it compares two slices. +func InDeltaSlicef(t TestingT, expected interface{}, actual interface{}, delta float64, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.InDeltaSlicef(t, expected, actual, delta, msg, args...) { + return + } + t.FailNow() +} + +// InDeltaf asserts that the two numerals are within delta of each other. +// +// require.InDeltaf(t, math.Pi, 22/7.0, 0.01, "error message %s", "formatted") +func InDeltaf(t TestingT, expected interface{}, actual interface{}, delta float64, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.InDeltaf(t, expected, actual, delta, msg, args...) { + return + } + t.FailNow() +} + +// InEpsilon asserts that expected and actual have a relative error less than epsilon +func InEpsilon(t TestingT, expected interface{}, actual interface{}, epsilon float64, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.InEpsilon(t, expected, actual, epsilon, msgAndArgs...) { + return + } + t.FailNow() +} + +// InEpsilonSlice is the same as InEpsilon, except it compares each value from two slices. +func InEpsilonSlice(t TestingT, expected interface{}, actual interface{}, epsilon float64, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.InEpsilonSlice(t, expected, actual, epsilon, msgAndArgs...) { + return + } + t.FailNow() +} + +// InEpsilonSlicef is the same as InEpsilon, except it compares each value from two slices. +func InEpsilonSlicef(t TestingT, expected interface{}, actual interface{}, epsilon float64, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.InEpsilonSlicef(t, expected, actual, epsilon, msg, args...) { + return + } + t.FailNow() +} + +// InEpsilonf asserts that expected and actual have a relative error less than epsilon +func InEpsilonf(t TestingT, expected interface{}, actual interface{}, epsilon float64, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.InEpsilonf(t, expected, actual, epsilon, msg, args...) { + return + } + t.FailNow() +} + +// IsDecreasing asserts that the collection is decreasing +// +// require.IsDecreasing(t, []int{2, 1, 0}) +// require.IsDecreasing(t, []float{2, 1}) +// require.IsDecreasing(t, []string{"b", "a"}) +func IsDecreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.IsDecreasing(t, object, msgAndArgs...) { + return + } + t.FailNow() +} + +// IsDecreasingf asserts that the collection is decreasing +// +// require.IsDecreasingf(t, []int{2, 1, 0}, "error message %s", "formatted") +// require.IsDecreasingf(t, []float{2, 1}, "error message %s", "formatted") +// require.IsDecreasingf(t, []string{"b", "a"}, "error message %s", "formatted") +func IsDecreasingf(t TestingT, object interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.IsDecreasingf(t, object, msg, args...) { + return + } + t.FailNow() +} + +// IsIncreasing asserts that the collection is increasing +// +// require.IsIncreasing(t, []int{1, 2, 3}) +// require.IsIncreasing(t, []float{1, 2}) +// require.IsIncreasing(t, []string{"a", "b"}) +func IsIncreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.IsIncreasing(t, object, msgAndArgs...) { + return + } + t.FailNow() +} + +// IsIncreasingf asserts that the collection is increasing +// +// require.IsIncreasingf(t, []int{1, 2, 3}, "error message %s", "formatted") +// require.IsIncreasingf(t, []float{1, 2}, "error message %s", "formatted") +// require.IsIncreasingf(t, []string{"a", "b"}, "error message %s", "formatted") +func IsIncreasingf(t TestingT, object interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.IsIncreasingf(t, object, msg, args...) { + return + } + t.FailNow() +} + +// IsNonDecreasing asserts that the collection is not decreasing +// +// require.IsNonDecreasing(t, []int{1, 1, 2}) +// require.IsNonDecreasing(t, []float{1, 2}) +// require.IsNonDecreasing(t, []string{"a", "b"}) +func IsNonDecreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.IsNonDecreasing(t, object, msgAndArgs...) { + return + } + t.FailNow() +} + +// IsNonDecreasingf asserts that the collection is not decreasing +// +// require.IsNonDecreasingf(t, []int{1, 1, 2}, "error message %s", "formatted") +// require.IsNonDecreasingf(t, []float{1, 2}, "error message %s", "formatted") +// require.IsNonDecreasingf(t, []string{"a", "b"}, "error message %s", "formatted") +func IsNonDecreasingf(t TestingT, object interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.IsNonDecreasingf(t, object, msg, args...) { + return + } + t.FailNow() +} + +// IsNonIncreasing asserts that the collection is not increasing +// +// require.IsNonIncreasing(t, []int{2, 1, 1}) +// require.IsNonIncreasing(t, []float{2, 1}) +// require.IsNonIncreasing(t, []string{"b", "a"}) +func IsNonIncreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.IsNonIncreasing(t, object, msgAndArgs...) { + return + } + t.FailNow() +} + +// IsNonIncreasingf asserts that the collection is not increasing +// +// require.IsNonIncreasingf(t, []int{2, 1, 1}, "error message %s", "formatted") +// require.IsNonIncreasingf(t, []float{2, 1}, "error message %s", "formatted") +// require.IsNonIncreasingf(t, []string{"b", "a"}, "error message %s", "formatted") +func IsNonIncreasingf(t TestingT, object interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.IsNonIncreasingf(t, object, msg, args...) { + return + } + t.FailNow() +} + +// IsNotType asserts that the specified objects are not of the same type. +// +// require.IsNotType(t, &NotMyStruct{}, &MyStruct{}) +func IsNotType(t TestingT, theType interface{}, object interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.IsNotType(t, theType, object, msgAndArgs...) { + return + } + t.FailNow() +} + +// IsNotTypef asserts that the specified objects are not of the same type. +// +// require.IsNotTypef(t, &NotMyStruct{}, &MyStruct{}, "error message %s", "formatted") +func IsNotTypef(t TestingT, theType interface{}, object interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.IsNotTypef(t, theType, object, msg, args...) { + return + } + t.FailNow() +} + +// IsType asserts that the specified objects are of the same type. +// +// require.IsType(t, &MyStruct{}, &MyStruct{}) +func IsType(t TestingT, expectedType interface{}, object interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.IsType(t, expectedType, object, msgAndArgs...) { + return + } + t.FailNow() +} + +// IsTypef asserts that the specified objects are of the same type. +// +// require.IsTypef(t, &MyStruct{}, &MyStruct{}, "error message %s", "formatted") +func IsTypef(t TestingT, expectedType interface{}, object interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.IsTypef(t, expectedType, object, msg, args...) { + return + } + t.FailNow() +} + +// JSONEq asserts that two JSON strings are equivalent. +// +// require.JSONEq(t, `{"hello": "world", "foo": "bar"}`, `{"foo": "bar", "hello": "world"}`) +func JSONEq(t TestingT, expected string, actual string, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.JSONEq(t, expected, actual, msgAndArgs...) { + return + } + t.FailNow() +} + +// JSONEqf asserts that two JSON strings are equivalent. +// +// require.JSONEqf(t, `{"hello": "world", "foo": "bar"}`, `{"foo": "bar", "hello": "world"}`, "error message %s", "formatted") +func JSONEqf(t TestingT, expected string, actual string, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.JSONEqf(t, expected, actual, msg, args...) { + return + } + t.FailNow() +} + +// Len asserts that the specified object has specific length. +// Len also fails if the object has a type that len() not accept. +// +// require.Len(t, mySlice, 3) +func Len(t TestingT, object interface{}, length int, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Len(t, object, length, msgAndArgs...) { + return + } + t.FailNow() +} + +// Lenf asserts that the specified object has specific length. +// Lenf also fails if the object has a type that len() not accept. +// +// require.Lenf(t, mySlice, 3, "error message %s", "formatted") +func Lenf(t TestingT, object interface{}, length int, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Lenf(t, object, length, msg, args...) { + return + } + t.FailNow() +} + +// Less asserts that the first element is less than the second +// +// require.Less(t, 1, 2) +// require.Less(t, float64(1), float64(2)) +// require.Less(t, "a", "b") +func Less(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Less(t, e1, e2, msgAndArgs...) { + return + } + t.FailNow() +} + +// LessOrEqual asserts that the first element is less than or equal to the second +// +// require.LessOrEqual(t, 1, 2) +// require.LessOrEqual(t, 2, 2) +// require.LessOrEqual(t, "a", "b") +// require.LessOrEqual(t, "b", "b") +func LessOrEqual(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.LessOrEqual(t, e1, e2, msgAndArgs...) { + return + } + t.FailNow() +} + +// LessOrEqualf asserts that the first element is less than or equal to the second +// +// require.LessOrEqualf(t, 1, 2, "error message %s", "formatted") +// require.LessOrEqualf(t, 2, 2, "error message %s", "formatted") +// require.LessOrEqualf(t, "a", "b", "error message %s", "formatted") +// require.LessOrEqualf(t, "b", "b", "error message %s", "formatted") +func LessOrEqualf(t TestingT, e1 interface{}, e2 interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.LessOrEqualf(t, e1, e2, msg, args...) { + return + } + t.FailNow() +} + +// Lessf asserts that the first element is less than the second +// +// require.Lessf(t, 1, 2, "error message %s", "formatted") +// require.Lessf(t, float64(1), float64(2), "error message %s", "formatted") +// require.Lessf(t, "a", "b", "error message %s", "formatted") +func Lessf(t TestingT, e1 interface{}, e2 interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Lessf(t, e1, e2, msg, args...) { + return + } + t.FailNow() +} + +// Negative asserts that the specified element is negative +// +// require.Negative(t, -1) +// require.Negative(t, -1.23) +func Negative(t TestingT, e interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Negative(t, e, msgAndArgs...) { + return + } + t.FailNow() +} + +// Negativef asserts that the specified element is negative +// +// require.Negativef(t, -1, "error message %s", "formatted") +// require.Negativef(t, -1.23, "error message %s", "formatted") +func Negativef(t TestingT, e interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Negativef(t, e, msg, args...) { + return + } + t.FailNow() +} + +// Never asserts that the given condition doesn't satisfy in waitFor time, +// periodically checking the target function each tick. +// +// require.Never(t, func() bool { return false; }, time.Second, 10*time.Millisecond) +func Never(t TestingT, condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Never(t, condition, waitFor, tick, msgAndArgs...) { + return + } + t.FailNow() +} + +// Neverf asserts that the given condition doesn't satisfy in waitFor time, +// periodically checking the target function each tick. +// +// require.Neverf(t, func() bool { return false; }, time.Second, 10*time.Millisecond, "error message %s", "formatted") +func Neverf(t TestingT, condition func() bool, waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Neverf(t, condition, waitFor, tick, msg, args...) { + return + } + t.FailNow() +} + +// Nil asserts that the specified object is nil. +// +// require.Nil(t, err) +func Nil(t TestingT, object interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Nil(t, object, msgAndArgs...) { + return + } + t.FailNow() +} + +// Nilf asserts that the specified object is nil. +// +// require.Nilf(t, err, "error message %s", "formatted") +func Nilf(t TestingT, object interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Nilf(t, object, msg, args...) { + return + } + t.FailNow() +} + +// NoDirExists checks whether a directory does not exist in the given path. +// It fails if the path points to an existing _directory_ only. +func NoDirExists(t TestingT, path string, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NoDirExists(t, path, msgAndArgs...) { + return + } + t.FailNow() +} + +// NoDirExistsf checks whether a directory does not exist in the given path. +// It fails if the path points to an existing _directory_ only. +func NoDirExistsf(t TestingT, path string, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NoDirExistsf(t, path, msg, args...) { + return + } + t.FailNow() +} + +// NoError asserts that a function returned no error (i.e. `nil`). +// +// actualObj, err := SomeFunction() +// if require.NoError(t, err) { +// require.Equal(t, expectedObj, actualObj) +// } +func NoError(t TestingT, err error, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NoError(t, err, msgAndArgs...) { + return + } + t.FailNow() +} + +// NoErrorf asserts that a function returned no error (i.e. `nil`). +// +// actualObj, err := SomeFunction() +// if require.NoErrorf(t, err, "error message %s", "formatted") { +// require.Equal(t, expectedObj, actualObj) +// } +func NoErrorf(t TestingT, err error, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NoErrorf(t, err, msg, args...) { + return + } + t.FailNow() +} + +// NoFileExists checks whether a file does not exist in a given path. It fails +// if the path points to an existing _file_ only. +func NoFileExists(t TestingT, path string, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NoFileExists(t, path, msgAndArgs...) { + return + } + t.FailNow() +} + +// NoFileExistsf checks whether a file does not exist in a given path. It fails +// if the path points to an existing _file_ only. +func NoFileExistsf(t TestingT, path string, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NoFileExistsf(t, path, msg, args...) { + return + } + t.FailNow() +} + +// NotContains asserts that the specified string, list(array, slice...) or map does NOT contain the +// specified substring or element. +// +// require.NotContains(t, "Hello World", "Earth") +// require.NotContains(t, ["Hello", "World"], "Earth") +// require.NotContains(t, {"Hello": "World"}, "Earth") +func NotContains(t TestingT, s interface{}, contains interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotContains(t, s, contains, msgAndArgs...) { + return + } + t.FailNow() +} + +// NotContainsf asserts that the specified string, list(array, slice...) or map does NOT contain the +// specified substring or element. +// +// require.NotContainsf(t, "Hello World", "Earth", "error message %s", "formatted") +// require.NotContainsf(t, ["Hello", "World"], "Earth", "error message %s", "formatted") +// require.NotContainsf(t, {"Hello": "World"}, "Earth", "error message %s", "formatted") +func NotContainsf(t TestingT, s interface{}, contains interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotContainsf(t, s, contains, msg, args...) { + return + } + t.FailNow() +} + +// NotElementsMatch asserts that the specified listA(array, slice...) is NOT equal to specified +// 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 not match. +// This is an inverse of ElementsMatch. +// +// require.NotElementsMatch(t, [1, 1, 2, 3], [1, 1, 2, 3]) -> false +// +// require.NotElementsMatch(t, [1, 1, 2, 3], [1, 2, 3]) -> true +// +// require.NotElementsMatch(t, [1, 2, 3], [1, 2, 4]) -> true +func NotElementsMatch(t TestingT, listA interface{}, listB interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotElementsMatch(t, listA, listB, msgAndArgs...) { + return + } + t.FailNow() +} + +// NotElementsMatchf asserts that the specified listA(array, slice...) is NOT equal to specified +// 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 not match. +// This is an inverse of ElementsMatch. +// +// require.NotElementsMatchf(t, [1, 1, 2, 3], [1, 1, 2, 3], "error message %s", "formatted") -> false +// +// require.NotElementsMatchf(t, [1, 1, 2, 3], [1, 2, 3], "error message %s", "formatted") -> true +// +// require.NotElementsMatchf(t, [1, 2, 3], [1, 2, 4], "error message %s", "formatted") -> true +func NotElementsMatchf(t TestingT, listA interface{}, listB interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotElementsMatchf(t, listA, listB, msg, args...) { + return + } + t.FailNow() +} + +// NotEmpty asserts that the specified object is NOT [Empty]. +// +// if require.NotEmpty(t, obj) { +// require.Equal(t, "two", obj[1]) +// } +func NotEmpty(t TestingT, object interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotEmpty(t, object, msgAndArgs...) { + return + } + t.FailNow() +} + +// NotEmptyf asserts that the specified object is NOT [Empty]. +// +// if require.NotEmptyf(t, obj, "error message %s", "formatted") { +// require.Equal(t, "two", obj[1]) +// } +func NotEmptyf(t TestingT, object interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotEmptyf(t, object, msg, args...) { + return + } + t.FailNow() +} + +// NotEqual asserts that the specified values are NOT equal. +// +// require.NotEqual(t, obj1, obj2) +// +// Pointer variable equality is determined based on the equality of the +// referenced values (as opposed to the memory addresses). +func NotEqual(t TestingT, expected interface{}, actual interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotEqual(t, expected, actual, msgAndArgs...) { + return + } + t.FailNow() +} + +// NotEqualValues asserts that two objects are not equal even when converted to the same type +// +// require.NotEqualValues(t, obj1, obj2) +func NotEqualValues(t TestingT, expected interface{}, actual interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotEqualValues(t, expected, actual, msgAndArgs...) { + return + } + t.FailNow() +} + +// NotEqualValuesf asserts that two objects are not equal even when converted to the same type +// +// require.NotEqualValuesf(t, obj1, obj2, "error message %s", "formatted") +func NotEqualValuesf(t TestingT, expected interface{}, actual interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotEqualValuesf(t, expected, actual, msg, args...) { + return + } + t.FailNow() +} + +// NotEqualf asserts that the specified values are NOT equal. +// +// require.NotEqualf(t, obj1, obj2, "error message %s", "formatted") +// +// Pointer variable equality is determined based on the equality of the +// referenced values (as opposed to the memory addresses). +func NotEqualf(t TestingT, expected interface{}, actual interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotEqualf(t, expected, actual, msg, args...) { + return + } + t.FailNow() +} + +// NotErrorAs asserts that none of the errors in err's chain matches target, +// but if so, sets target to that error value. +func NotErrorAs(t TestingT, err error, target interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotErrorAs(t, err, target, msgAndArgs...) { + return + } + t.FailNow() +} + +// NotErrorAsf asserts that none of the errors in err's chain matches target, +// but if so, sets target to that error value. +func NotErrorAsf(t TestingT, err error, target interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotErrorAsf(t, err, target, msg, args...) { + return + } + t.FailNow() +} + +// NotErrorIs asserts that none of the errors in err's chain matches target. +// This is a wrapper for errors.Is. +func NotErrorIs(t TestingT, err error, target error, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotErrorIs(t, err, target, msgAndArgs...) { + return + } + t.FailNow() +} + +// NotErrorIsf asserts that none of the errors in err's chain matches target. +// This is a wrapper for errors.Is. +func NotErrorIsf(t TestingT, err error, target error, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotErrorIsf(t, err, target, msg, args...) { + return + } + t.FailNow() +} + +// NotImplements asserts that an object does not implement the specified interface. +// +// require.NotImplements(t, (*MyInterface)(nil), new(MyObject)) +func NotImplements(t TestingT, interfaceObject interface{}, object interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotImplements(t, interfaceObject, object, msgAndArgs...) { + return + } + t.FailNow() +} + +// NotImplementsf asserts that an object does not implement the specified interface. +// +// require.NotImplementsf(t, (*MyInterface)(nil), new(MyObject), "error message %s", "formatted") +func NotImplementsf(t TestingT, interfaceObject interface{}, object interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotImplementsf(t, interfaceObject, object, msg, args...) { + return + } + t.FailNow() +} + +// NotNil asserts that the specified object is not nil. +// +// require.NotNil(t, err) +func NotNil(t TestingT, object interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotNil(t, object, msgAndArgs...) { + return + } + t.FailNow() +} + +// NotNilf asserts that the specified object is not nil. +// +// require.NotNilf(t, err, "error message %s", "formatted") +func NotNilf(t TestingT, object interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotNilf(t, object, msg, args...) { + return + } + t.FailNow() +} + +// NotPanics asserts that the code inside the specified PanicTestFunc does NOT panic. +// +// require.NotPanics(t, func(){ RemainCalm() }) +func NotPanics(t TestingT, f assert.PanicTestFunc, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotPanics(t, f, msgAndArgs...) { + return + } + t.FailNow() +} + +// NotPanicsf asserts that the code inside the specified PanicTestFunc does NOT panic. +// +// require.NotPanicsf(t, func(){ RemainCalm() }, "error message %s", "formatted") +func NotPanicsf(t TestingT, f assert.PanicTestFunc, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotPanicsf(t, f, msg, args...) { + return + } + t.FailNow() +} + +// NotRegexp asserts that a specified regexp does not match a string. +// +// require.NotRegexp(t, regexp.MustCompile("starts"), "it's starting") +// require.NotRegexp(t, "^start", "it's not starting") +func NotRegexp(t TestingT, rx interface{}, str interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotRegexp(t, rx, str, msgAndArgs...) { + return + } + t.FailNow() +} + +// NotRegexpf asserts that a specified regexp does not match a string. +// +// require.NotRegexpf(t, regexp.MustCompile("starts"), "it's starting", "error message %s", "formatted") +// require.NotRegexpf(t, "^start", "it's not starting", "error message %s", "formatted") +func NotRegexpf(t TestingT, rx interface{}, str interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotRegexpf(t, rx, str, msg, args...) { + return + } + t.FailNow() +} + +// NotSame asserts that two pointers do not reference the same object. +// +// require.NotSame(t, ptr1, ptr2) +// +// Both arguments must be pointer variables. Pointer variable sameness is +// determined based on the equality of both type and value. +func NotSame(t TestingT, expected interface{}, actual interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotSame(t, expected, actual, msgAndArgs...) { + return + } + t.FailNow() +} + +// NotSamef asserts that two pointers do not reference the same object. +// +// require.NotSamef(t, ptr1, ptr2, "error message %s", "formatted") +// +// Both arguments must be pointer variables. Pointer variable sameness is +// determined based on the equality of both type and value. +func NotSamef(t TestingT, expected interface{}, actual interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotSamef(t, expected, actual, msg, args...) { + return + } + t.FailNow() +} + +// NotSubset asserts that the list (array, slice, or map) does NOT contain all +// elements given in the subset (array, slice, or map). +// Map elements are key-value pairs unless compared with an array or slice where +// only the map key is evaluated. +// +// require.NotSubset(t, [1, 3, 4], [1, 2]) +// require.NotSubset(t, {"x": 1, "y": 2}, {"z": 3}) +// require.NotSubset(t, [1, 3, 4], {1: "one", 2: "two"}) +// require.NotSubset(t, {"x": 1, "y": 2}, ["z"]) +func NotSubset(t TestingT, list interface{}, subset interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotSubset(t, list, subset, msgAndArgs...) { + return + } + t.FailNow() +} + +// NotSubsetf asserts that the list (array, slice, or map) does NOT contain all +// elements given in the subset (array, slice, or map). +// Map elements are key-value pairs unless compared with an array or slice where +// only the map key is evaluated. +// +// require.NotSubsetf(t, [1, 3, 4], [1, 2], "error message %s", "formatted") +// require.NotSubsetf(t, {"x": 1, "y": 2}, {"z": 3}, "error message %s", "formatted") +// require.NotSubsetf(t, [1, 3, 4], {1: "one", 2: "two"}, "error message %s", "formatted") +// require.NotSubsetf(t, {"x": 1, "y": 2}, ["z"], "error message %s", "formatted") +func NotSubsetf(t TestingT, list interface{}, subset interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotSubsetf(t, list, subset, msg, args...) { + return + } + t.FailNow() +} + +// NotZero asserts that i is not the zero value for its type. +func NotZero(t TestingT, i interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotZero(t, i, msgAndArgs...) { + return + } + t.FailNow() +} + +// NotZerof asserts that i is not the zero value for its type. +func NotZerof(t TestingT, i interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotZerof(t, i, msg, args...) { + return + } + t.FailNow() +} + +// Panics asserts that the code inside the specified PanicTestFunc panics. +// +// require.Panics(t, func(){ GoCrazy() }) +func Panics(t TestingT, f assert.PanicTestFunc, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Panics(t, f, msgAndArgs...) { + return + } + t.FailNow() +} + +// PanicsWithError asserts that the code inside the specified PanicTestFunc +// panics, and that the recovered panic value is an error that satisfies the +// EqualError comparison. +// +// require.PanicsWithError(t, "crazy error", func(){ GoCrazy() }) +func PanicsWithError(t TestingT, errString string, f assert.PanicTestFunc, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.PanicsWithError(t, errString, f, msgAndArgs...) { + return + } + t.FailNow() +} + +// PanicsWithErrorf asserts that the code inside the specified PanicTestFunc +// panics, and that the recovered panic value is an error that satisfies the +// EqualError comparison. +// +// require.PanicsWithErrorf(t, "crazy error", func(){ GoCrazy() }, "error message %s", "formatted") +func PanicsWithErrorf(t TestingT, errString string, f assert.PanicTestFunc, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.PanicsWithErrorf(t, errString, f, msg, args...) { + return + } + t.FailNow() +} + +// PanicsWithValue asserts that the code inside the specified PanicTestFunc panics, and that +// the recovered panic value equals the expected panic value. +// +// require.PanicsWithValue(t, "crazy error", func(){ GoCrazy() }) +func PanicsWithValue(t TestingT, expected interface{}, f assert.PanicTestFunc, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.PanicsWithValue(t, expected, f, msgAndArgs...) { + return + } + t.FailNow() +} + +// PanicsWithValuef asserts that the code inside the specified PanicTestFunc panics, and that +// the recovered panic value equals the expected panic value. +// +// require.PanicsWithValuef(t, "crazy error", func(){ GoCrazy() }, "error message %s", "formatted") +func PanicsWithValuef(t TestingT, expected interface{}, f assert.PanicTestFunc, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.PanicsWithValuef(t, expected, f, msg, args...) { + return + } + t.FailNow() +} + +// Panicsf asserts that the code inside the specified PanicTestFunc panics. +// +// require.Panicsf(t, func(){ GoCrazy() }, "error message %s", "formatted") +func Panicsf(t TestingT, f assert.PanicTestFunc, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Panicsf(t, f, msg, args...) { + return + } + t.FailNow() +} + +// Positive asserts that the specified element is positive +// +// require.Positive(t, 1) +// require.Positive(t, 1.23) +func Positive(t TestingT, e interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Positive(t, e, msgAndArgs...) { + return + } + t.FailNow() +} + +// Positivef asserts that the specified element is positive +// +// require.Positivef(t, 1, "error message %s", "formatted") +// require.Positivef(t, 1.23, "error message %s", "formatted") +func Positivef(t TestingT, e interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Positivef(t, e, msg, args...) { + return + } + t.FailNow() +} + +// Regexp asserts that a specified regexp matches a string. +// +// require.Regexp(t, regexp.MustCompile("start"), "it's starting") +// require.Regexp(t, "start...$", "it's not starting") +func Regexp(t TestingT, rx interface{}, str interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Regexp(t, rx, str, msgAndArgs...) { + return + } + t.FailNow() +} + +// Regexpf asserts that a specified regexp matches a string. +// +// require.Regexpf(t, regexp.MustCompile("start"), "it's starting", "error message %s", "formatted") +// require.Regexpf(t, "start...$", "it's not starting", "error message %s", "formatted") +func Regexpf(t TestingT, rx interface{}, str interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Regexpf(t, rx, str, msg, args...) { + return + } + t.FailNow() +} + +// Same asserts that two pointers reference the same object. +// +// require.Same(t, ptr1, ptr2) +// +// Both arguments must be pointer variables. Pointer variable sameness is +// determined based on the equality of both type and value. +func Same(t TestingT, expected interface{}, actual interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Same(t, expected, actual, msgAndArgs...) { + return + } + t.FailNow() +} + +// Samef asserts that two pointers reference the same object. +// +// require.Samef(t, ptr1, ptr2, "error message %s", "formatted") +// +// Both arguments must be pointer variables. Pointer variable sameness is +// determined based on the equality of both type and value. +func Samef(t TestingT, expected interface{}, actual interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Samef(t, expected, actual, msg, args...) { + return + } + t.FailNow() +} + +// Subset asserts that the list (array, slice, or map) contains all elements +// given in the subset (array, slice, or map). +// Map elements are key-value pairs unless compared with an array or slice where +// only the map key is evaluated. +// +// require.Subset(t, [1, 2, 3], [1, 2]) +// require.Subset(t, {"x": 1, "y": 2}, {"x": 1}) +// require.Subset(t, [1, 2, 3], {1: "one", 2: "two"}) +// require.Subset(t, {"x": 1, "y": 2}, ["x"]) +func Subset(t TestingT, list interface{}, subset interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Subset(t, list, subset, msgAndArgs...) { + return + } + t.FailNow() +} + +// Subsetf asserts that the list (array, slice, or map) contains all elements +// given in the subset (array, slice, or map). +// Map elements are key-value pairs unless compared with an array or slice where +// only the map key is evaluated. +// +// require.Subsetf(t, [1, 2, 3], [1, 2], "error message %s", "formatted") +// require.Subsetf(t, {"x": 1, "y": 2}, {"x": 1}, "error message %s", "formatted") +// require.Subsetf(t, [1, 2, 3], {1: "one", 2: "two"}, "error message %s", "formatted") +// require.Subsetf(t, {"x": 1, "y": 2}, ["x"], "error message %s", "formatted") +func Subsetf(t TestingT, list interface{}, subset interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Subsetf(t, list, subset, msg, args...) { + return + } + t.FailNow() +} + +// True asserts that the specified value is true. +// +// require.True(t, myBool) +func True(t TestingT, value bool, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.True(t, value, msgAndArgs...) { + return + } + t.FailNow() +} + +// Truef asserts that the specified value is true. +// +// require.Truef(t, myBool, "error message %s", "formatted") +func Truef(t TestingT, value bool, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Truef(t, value, msg, args...) { + return + } + t.FailNow() +} + +// WithinDuration asserts that the two times are within duration delta of each other. +// +// require.WithinDuration(t, time.Now(), time.Now(), 10*time.Second) +func WithinDuration(t TestingT, expected time.Time, actual time.Time, delta time.Duration, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.WithinDuration(t, expected, actual, delta, msgAndArgs...) { + return + } + t.FailNow() +} + +// WithinDurationf asserts that the two times are within duration delta of each other. +// +// require.WithinDurationf(t, time.Now(), time.Now(), 10*time.Second, "error message %s", "formatted") +func WithinDurationf(t TestingT, expected time.Time, actual time.Time, delta time.Duration, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.WithinDurationf(t, expected, actual, delta, msg, args...) { + return + } + t.FailNow() +} + +// WithinRange asserts that a time is within a time range (inclusive). +// +// require.WithinRange(t, time.Now(), time.Now().Add(-time.Second), time.Now().Add(time.Second)) +func WithinRange(t TestingT, actual time.Time, start time.Time, end time.Time, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.WithinRange(t, actual, start, end, msgAndArgs...) { + return + } + t.FailNow() +} + +// WithinRangef asserts that a time is within a time range (inclusive). +// +// require.WithinRangef(t, time.Now(), time.Now().Add(-time.Second), time.Now().Add(time.Second), "error message %s", "formatted") +func WithinRangef(t TestingT, actual time.Time, start time.Time, end time.Time, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.WithinRangef(t, actual, start, end, msg, args...) { + return + } + t.FailNow() +} + +// YAMLEq asserts that two YAML strings are equivalent. +func YAMLEq(t TestingT, expected string, actual string, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.YAMLEq(t, expected, actual, msgAndArgs...) { + return + } + t.FailNow() +} + +// YAMLEqf asserts that two YAML strings are equivalent. +func YAMLEqf(t TestingT, expected string, actual string, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.YAMLEqf(t, expected, actual, msg, args...) { + return + } + t.FailNow() +} + +// Zero asserts that i is the zero value for its type. +func Zero(t TestingT, i interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Zero(t, i, msgAndArgs...) { + return + } + t.FailNow() +} + +// Zerof asserts that i is the zero value for its type. +func Zerof(t TestingT, i interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Zerof(t, i, msg, args...) { + return + } + t.FailNow() +} diff --git a/pkg/testify/require/require.go.tmpl b/pkg/testify/require/require.go.tmpl new file mode 100644 index 00000000..8b328368 --- /dev/null +++ b/pkg/testify/require/require.go.tmpl @@ -0,0 +1,6 @@ +{{ replace .Comment "assert." "require."}} +func {{.DocInfo.Name}}(t TestingT, {{.Params}}) { + if h, ok := t.(tHelper); ok { h.Helper() } + if assert.{{.DocInfo.Name}}(t, {{.ForwardedParams}}) { return } + t.FailNow() +} diff --git a/pkg/testify/require/require_forward.go b/pkg/testify/require/require_forward.go new file mode 100644 index 00000000..a13165ef --- /dev/null +++ b/pkg/testify/require/require_forward.go @@ -0,0 +1,1724 @@ +// Code generated with github.com/stretchr/testify/_codegen; DO NOT EDIT. + +package require + +import ( + assert "github.com/CodSpeedHQ/codspeed-go/pkg/testify/assert" + http "net/http" + url "net/url" + time "time" +) + +// Condition uses a Comparison to assert a complex condition. +func (a *Assertions) Condition(comp assert.Comparison, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Condition(a.t, comp, msgAndArgs...) +} + +// Conditionf uses a Comparison to assert a complex condition. +func (a *Assertions) Conditionf(comp assert.Comparison, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Conditionf(a.t, comp, msg, args...) +} + +// Contains asserts that the specified string, list(array, slice...) or map contains the +// specified substring or element. +// +// a.Contains("Hello World", "World") +// a.Contains(["Hello", "World"], "World") +// a.Contains({"Hello": "World"}, "Hello") +func (a *Assertions) Contains(s interface{}, contains interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Contains(a.t, s, contains, msgAndArgs...) +} + +// Containsf asserts that the specified string, list(array, slice...) or map contains the +// specified substring or element. +// +// a.Containsf("Hello World", "World", "error message %s", "formatted") +// a.Containsf(["Hello", "World"], "World", "error message %s", "formatted") +// a.Containsf({"Hello": "World"}, "Hello", "error message %s", "formatted") +func (a *Assertions) Containsf(s interface{}, contains interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Containsf(a.t, s, contains, msg, args...) +} + +// DirExists checks whether a directory exists in the given path. It also fails +// if the path is a file rather a directory or there is an error checking whether it exists. +func (a *Assertions) DirExists(path string, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + DirExists(a.t, path, msgAndArgs...) +} + +// DirExistsf checks whether a directory exists in the given path. It also fails +// if the path is a file rather a directory or there is an error checking whether it exists. +func (a *Assertions) DirExistsf(path string, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + DirExistsf(a.t, path, msg, args...) +} + +// ElementsMatch asserts that the specified listA(array, slice...) is equal to specified +// 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]) +func (a *Assertions) ElementsMatch(listA interface{}, listB interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + ElementsMatch(a.t, listA, listB, msgAndArgs...) +} + +// ElementsMatchf asserts that the specified listA(array, slice...) is equal to specified +// 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.ElementsMatchf([1, 3, 2, 3], [1, 3, 3, 2], "error message %s", "formatted") +func (a *Assertions) ElementsMatchf(listA interface{}, listB interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + ElementsMatchf(a.t, listA, listB, msg, args...) +} + +// Empty asserts that the given value is "empty". +// +// [Zero values] are "empty". +// +// Arrays are "empty" if every element is the zero value of the type (stricter than "empty"). +// +// Slices, maps and channels with zero length are "empty". +// +// Pointer values are "empty" if the pointer is nil or if the pointed value is "empty". +// +// a.Empty(obj) +// +// [Zero values]: https://go.dev/ref/spec#The_zero_value +func (a *Assertions) Empty(object interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Empty(a.t, object, msgAndArgs...) +} + +// Emptyf asserts that the given value is "empty". +// +// [Zero values] are "empty". +// +// Arrays are "empty" if every element is the zero value of the type (stricter than "empty"). +// +// Slices, maps and channels with zero length are "empty". +// +// Pointer values are "empty" if the pointer is nil or if the pointed value is "empty". +// +// a.Emptyf(obj, "error message %s", "formatted") +// +// [Zero values]: https://go.dev/ref/spec#The_zero_value +func (a *Assertions) Emptyf(object interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Emptyf(a.t, object, msg, args...) +} + +// Equal asserts that two objects are equal. +// +// a.Equal(123, 123) +// +// Pointer variable equality is determined based on the equality of the +// referenced values (as opposed to the memory addresses). Function equality +// cannot be determined and will always fail. +func (a *Assertions) Equal(expected interface{}, actual interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Equal(a.t, expected, actual, msgAndArgs...) +} + +// EqualError asserts that a function returned an error (i.e. not `nil`) +// and that it is equal to the provided error. +// +// actualObj, err := SomeFunction() +// a.EqualError(err, expectedErrorString) +func (a *Assertions) EqualError(theError error, errString string, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + EqualError(a.t, theError, errString, msgAndArgs...) +} + +// EqualErrorf asserts that a function returned an error (i.e. not `nil`) +// and that it is equal to the provided error. +// +// actualObj, err := SomeFunction() +// a.EqualErrorf(err, expectedErrorString, "error message %s", "formatted") +func (a *Assertions) EqualErrorf(theError error, errString string, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + EqualErrorf(a.t, theError, errString, msg, args...) +} + +// EqualExportedValues asserts that the types of two objects are equal and their public +// fields are also equal. This is useful for comparing structs that have private fields +// that could potentially differ. +// +// type S struct { +// Exported int +// notExported int +// } +// a.EqualExportedValues(S{1, 2}, S{1, 3}) => true +// a.EqualExportedValues(S{1, 2}, S{2, 3}) => false +func (a *Assertions) EqualExportedValues(expected interface{}, actual interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + EqualExportedValues(a.t, expected, actual, msgAndArgs...) +} + +// EqualExportedValuesf asserts that the types of two objects are equal and their public +// fields are also equal. This is useful for comparing structs that have private fields +// that could potentially differ. +// +// type S struct { +// Exported int +// notExported int +// } +// a.EqualExportedValuesf(S{1, 2}, S{1, 3}, "error message %s", "formatted") => true +// a.EqualExportedValuesf(S{1, 2}, S{2, 3}, "error message %s", "formatted") => false +func (a *Assertions) EqualExportedValuesf(expected interface{}, actual interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + EqualExportedValuesf(a.t, expected, actual, msg, args...) +} + +// EqualValues asserts that two objects are equal or convertible to the larger +// type and equal. +// +// a.EqualValues(uint32(123), int32(123)) +func (a *Assertions) EqualValues(expected interface{}, actual interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + EqualValues(a.t, expected, actual, msgAndArgs...) +} + +// EqualValuesf asserts that two objects are equal or convertible to the larger +// type and equal. +// +// a.EqualValuesf(uint32(123), int32(123), "error message %s", "formatted") +func (a *Assertions) EqualValuesf(expected interface{}, actual interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + EqualValuesf(a.t, expected, actual, msg, args...) +} + +// Equalf asserts that two objects are equal. +// +// a.Equalf(123, 123, "error message %s", "formatted") +// +// Pointer variable equality is determined based on the equality of the +// referenced values (as opposed to the memory addresses). Function equality +// cannot be determined and will always fail. +func (a *Assertions) Equalf(expected interface{}, actual interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Equalf(a.t, expected, actual, msg, args...) +} + +// Error asserts that a function returned an error (i.e. not `nil`). +// +// actualObj, err := SomeFunction() +// a.Error(err) +func (a *Assertions) Error(err error, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Error(a.t, err, msgAndArgs...) +} + +// ErrorAs asserts that at least one of the errors in err's chain matches target, and if so, sets target to that error value. +// This is a wrapper for errors.As. +func (a *Assertions) ErrorAs(err error, target interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + ErrorAs(a.t, err, target, msgAndArgs...) +} + +// ErrorAsf asserts that at least one of the errors in err's chain matches target, and if so, sets target to that error value. +// This is a wrapper for errors.As. +func (a *Assertions) ErrorAsf(err error, target interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + ErrorAsf(a.t, err, target, msg, args...) +} + +// ErrorContains asserts that a function returned an error (i.e. not `nil`) +// and that the error contains the specified substring. +// +// actualObj, err := SomeFunction() +// a.ErrorContains(err, expectedErrorSubString) +func (a *Assertions) ErrorContains(theError error, contains string, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + ErrorContains(a.t, theError, contains, msgAndArgs...) +} + +// ErrorContainsf asserts that a function returned an error (i.e. not `nil`) +// and that the error contains the specified substring. +// +// actualObj, err := SomeFunction() +// a.ErrorContainsf(err, expectedErrorSubString, "error message %s", "formatted") +func (a *Assertions) ErrorContainsf(theError error, contains string, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + ErrorContainsf(a.t, theError, contains, msg, args...) +} + +// ErrorIs asserts that at least one of the errors in err's chain matches target. +// This is a wrapper for errors.Is. +func (a *Assertions) ErrorIs(err error, target error, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + ErrorIs(a.t, err, target, msgAndArgs...) +} + +// ErrorIsf asserts that at least one of the errors in err's chain matches target. +// This is a wrapper for errors.Is. +func (a *Assertions) ErrorIsf(err error, target error, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + ErrorIsf(a.t, err, target, msg, args...) +} + +// Errorf asserts that a function returned an error (i.e. not `nil`). +// +// actualObj, err := SomeFunction() +// a.Errorf(err, "error message %s", "formatted") +func (a *Assertions) Errorf(err error, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Errorf(a.t, err, msg, args...) +} + +// Eventually asserts that given condition will be met in waitFor time, +// periodically checking target function each tick. +// +// a.Eventually(func() bool { return true; }, time.Second, 10*time.Millisecond) +func (a *Assertions) Eventually(condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Eventually(a.t, condition, waitFor, tick, msgAndArgs...) +} + +// EventuallyWithT asserts that given condition will be met in waitFor time, +// periodically checking target function each tick. In contrast to Eventually, +// it supplies a CollectT to the condition function, so that the condition +// function can use the CollectT to call other assertions. +// The condition is considered "met" if no errors are raised in a tick. +// The supplied CollectT collects all errors from one tick (if there are any). +// If the condition is not met before waitFor, the collected errors of +// the last tick are copied to t. +// +// externalValue := false +// go func() { +// time.Sleep(8*time.Second) +// externalValue = true +// }() +// a.EventuallyWithT(func(c *assert.CollectT) { +// // add assertions as needed; any assertion failure will fail the current tick +// assert.True(c, externalValue, "expected 'externalValue' to be true") +// }, 10*time.Second, 1*time.Second, "external state has not changed to 'true'; still false") +func (a *Assertions) EventuallyWithT(condition func(collect *assert.CollectT), waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + EventuallyWithT(a.t, condition, waitFor, tick, msgAndArgs...) +} + +// EventuallyWithTf asserts that given condition will be met in waitFor time, +// periodically checking target function each tick. In contrast to Eventually, +// it supplies a CollectT to the condition function, so that the condition +// function can use the CollectT to call other assertions. +// The condition is considered "met" if no errors are raised in a tick. +// The supplied CollectT collects all errors from one tick (if there are any). +// If the condition is not met before waitFor, the collected errors of +// the last tick are copied to t. +// +// externalValue := false +// go func() { +// time.Sleep(8*time.Second) +// externalValue = true +// }() +// a.EventuallyWithTf(func(c *assert.CollectT, "error message %s", "formatted") { +// // add assertions as needed; any assertion failure will fail the current tick +// assert.True(c, externalValue, "expected 'externalValue' to be true") +// }, 10*time.Second, 1*time.Second, "external state has not changed to 'true'; still false") +func (a *Assertions) EventuallyWithTf(condition func(collect *assert.CollectT), waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + EventuallyWithTf(a.t, condition, waitFor, tick, msg, args...) +} + +// Eventuallyf asserts that given condition will be met in waitFor time, +// periodically checking target function each tick. +// +// a.Eventuallyf(func() bool { return true; }, time.Second, 10*time.Millisecond, "error message %s", "formatted") +func (a *Assertions) Eventuallyf(condition func() bool, waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Eventuallyf(a.t, condition, waitFor, tick, msg, args...) +} + +// Exactly asserts that two objects are equal in value and type. +// +// a.Exactly(int32(123), int64(123)) +func (a *Assertions) Exactly(expected interface{}, actual interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Exactly(a.t, expected, actual, msgAndArgs...) +} + +// Exactlyf asserts that two objects are equal in value and type. +// +// a.Exactlyf(int32(123), int64(123), "error message %s", "formatted") +func (a *Assertions) Exactlyf(expected interface{}, actual interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Exactlyf(a.t, expected, actual, msg, args...) +} + +// Fail reports a failure through +func (a *Assertions) Fail(failureMessage string, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Fail(a.t, failureMessage, msgAndArgs...) +} + +// FailNow fails test +func (a *Assertions) FailNow(failureMessage string, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + FailNow(a.t, failureMessage, msgAndArgs...) +} + +// FailNowf fails test +func (a *Assertions) FailNowf(failureMessage string, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + FailNowf(a.t, failureMessage, msg, args...) +} + +// Failf reports a failure through +func (a *Assertions) Failf(failureMessage string, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Failf(a.t, failureMessage, msg, args...) +} + +// False asserts that the specified value is false. +// +// a.False(myBool) +func (a *Assertions) False(value bool, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + False(a.t, value, msgAndArgs...) +} + +// Falsef asserts that the specified value is false. +// +// a.Falsef(myBool, "error message %s", "formatted") +func (a *Assertions) Falsef(value bool, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Falsef(a.t, value, msg, args...) +} + +// FileExists checks whether a file exists in the given path. It also fails if +// the path points to a directory or there is an error when trying to check the file. +func (a *Assertions) FileExists(path string, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + FileExists(a.t, path, msgAndArgs...) +} + +// FileExistsf checks whether a file exists in the given path. It also fails if +// the path points to a directory or there is an error when trying to check the file. +func (a *Assertions) FileExistsf(path string, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + FileExistsf(a.t, path, msg, args...) +} + +// Greater asserts that the first element is greater than the second +// +// a.Greater(2, 1) +// a.Greater(float64(2), float64(1)) +// a.Greater("b", "a") +func (a *Assertions) Greater(e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Greater(a.t, e1, e2, msgAndArgs...) +} + +// GreaterOrEqual asserts that the first element is greater than or equal to the second +// +// a.GreaterOrEqual(2, 1) +// a.GreaterOrEqual(2, 2) +// a.GreaterOrEqual("b", "a") +// a.GreaterOrEqual("b", "b") +func (a *Assertions) GreaterOrEqual(e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + GreaterOrEqual(a.t, e1, e2, msgAndArgs...) +} + +// GreaterOrEqualf asserts that the first element is greater than or equal to the second +// +// a.GreaterOrEqualf(2, 1, "error message %s", "formatted") +// a.GreaterOrEqualf(2, 2, "error message %s", "formatted") +// a.GreaterOrEqualf("b", "a", "error message %s", "formatted") +// a.GreaterOrEqualf("b", "b", "error message %s", "formatted") +func (a *Assertions) GreaterOrEqualf(e1 interface{}, e2 interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + GreaterOrEqualf(a.t, e1, e2, msg, args...) +} + +// Greaterf asserts that the first element is greater than the second +// +// a.Greaterf(2, 1, "error message %s", "formatted") +// a.Greaterf(float64(2), float64(1), "error message %s", "formatted") +// a.Greaterf("b", "a", "error message %s", "formatted") +func (a *Assertions) Greaterf(e1 interface{}, e2 interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Greaterf(a.t, e1, e2, msg, args...) +} + +// HTTPBodyContains asserts that a specified handler returns a +// body that contains a string. +// +// a.HTTPBodyContains(myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky") +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPBodyContains(handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + HTTPBodyContains(a.t, handler, method, url, values, str, msgAndArgs...) +} + +// HTTPBodyContainsf asserts that a specified handler returns a +// body that contains a string. +// +// a.HTTPBodyContainsf(myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky", "error message %s", "formatted") +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPBodyContainsf(handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + HTTPBodyContainsf(a.t, handler, method, url, values, str, msg, args...) +} + +// HTTPBodyNotContains asserts that a specified handler returns a +// body that does not contain a string. +// +// a.HTTPBodyNotContains(myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky") +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPBodyNotContains(handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + HTTPBodyNotContains(a.t, handler, method, url, values, str, msgAndArgs...) +} + +// HTTPBodyNotContainsf asserts that a specified handler returns a +// body that does not contain a string. +// +// a.HTTPBodyNotContainsf(myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky", "error message %s", "formatted") +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPBodyNotContainsf(handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + HTTPBodyNotContainsf(a.t, handler, method, url, values, str, msg, args...) +} + +// HTTPError asserts that a specified handler returns an error status code. +// +// a.HTTPError(myHandler, "POST", "/a/b/c", url.Values{"a": []string{"b", "c"}} +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPError(handler http.HandlerFunc, method string, url string, values url.Values, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + HTTPError(a.t, handler, method, url, values, msgAndArgs...) +} + +// HTTPErrorf asserts that a specified handler returns an error status code. +// +// a.HTTPErrorf(myHandler, "POST", "/a/b/c", url.Values{"a": []string{"b", "c"}} +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPErrorf(handler http.HandlerFunc, method string, url string, values url.Values, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + HTTPErrorf(a.t, handler, method, url, values, msg, args...) +} + +// HTTPRedirect asserts that a specified handler returns a redirect status code. +// +// a.HTTPRedirect(myHandler, "GET", "/a/b/c", url.Values{"a": []string{"b", "c"}} +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPRedirect(handler http.HandlerFunc, method string, url string, values url.Values, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + HTTPRedirect(a.t, handler, method, url, values, msgAndArgs...) +} + +// HTTPRedirectf asserts that a specified handler returns a redirect status code. +// +// a.HTTPRedirectf(myHandler, "GET", "/a/b/c", url.Values{"a": []string{"b", "c"}} +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPRedirectf(handler http.HandlerFunc, method string, url string, values url.Values, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + HTTPRedirectf(a.t, handler, method, url, values, msg, args...) +} + +// HTTPStatusCode asserts that a specified handler returns a specified status code. +// +// a.HTTPStatusCode(myHandler, "GET", "/notImplemented", nil, 501) +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPStatusCode(handler http.HandlerFunc, method string, url string, values url.Values, statuscode int, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + HTTPStatusCode(a.t, handler, method, url, values, statuscode, msgAndArgs...) +} + +// HTTPStatusCodef asserts that a specified handler returns a specified status code. +// +// a.HTTPStatusCodef(myHandler, "GET", "/notImplemented", nil, 501, "error message %s", "formatted") +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPStatusCodef(handler http.HandlerFunc, method string, url string, values url.Values, statuscode int, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + HTTPStatusCodef(a.t, handler, method, url, values, statuscode, msg, args...) +} + +// HTTPSuccess asserts that a specified handler returns a success status code. +// +// a.HTTPSuccess(myHandler, "POST", "http://www.google.com", nil) +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPSuccess(handler http.HandlerFunc, method string, url string, values url.Values, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + HTTPSuccess(a.t, handler, method, url, values, msgAndArgs...) +} + +// HTTPSuccessf asserts that a specified handler returns a success status code. +// +// a.HTTPSuccessf(myHandler, "POST", "http://www.google.com", nil, "error message %s", "formatted") +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPSuccessf(handler http.HandlerFunc, method string, url string, values url.Values, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + HTTPSuccessf(a.t, handler, method, url, values, msg, args...) +} + +// Implements asserts that an object is implemented by the specified interface. +// +// a.Implements((*MyInterface)(nil), new(MyObject)) +func (a *Assertions) Implements(interfaceObject interface{}, object interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Implements(a.t, interfaceObject, object, msgAndArgs...) +} + +// Implementsf asserts that an object is implemented by the specified interface. +// +// a.Implementsf((*MyInterface)(nil), new(MyObject), "error message %s", "formatted") +func (a *Assertions) Implementsf(interfaceObject interface{}, object interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Implementsf(a.t, interfaceObject, object, msg, args...) +} + +// InDelta asserts that the two numerals are within delta of each other. +// +// a.InDelta(math.Pi, 22/7.0, 0.01) +func (a *Assertions) InDelta(expected interface{}, actual interface{}, delta float64, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + InDelta(a.t, expected, actual, delta, msgAndArgs...) +} + +// InDeltaMapValues is the same as InDelta, but it compares all values between two maps. Both maps must have exactly the same keys. +func (a *Assertions) InDeltaMapValues(expected interface{}, actual interface{}, delta float64, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + InDeltaMapValues(a.t, expected, actual, delta, msgAndArgs...) +} + +// InDeltaMapValuesf is the same as InDelta, but it compares all values between two maps. Both maps must have exactly the same keys. +func (a *Assertions) InDeltaMapValuesf(expected interface{}, actual interface{}, delta float64, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + InDeltaMapValuesf(a.t, expected, actual, delta, msg, args...) +} + +// InDeltaSlice is the same as InDelta, except it compares two slices. +func (a *Assertions) InDeltaSlice(expected interface{}, actual interface{}, delta float64, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + InDeltaSlice(a.t, expected, actual, delta, msgAndArgs...) +} + +// InDeltaSlicef is the same as InDelta, except it compares two slices. +func (a *Assertions) InDeltaSlicef(expected interface{}, actual interface{}, delta float64, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + InDeltaSlicef(a.t, expected, actual, delta, msg, args...) +} + +// InDeltaf asserts that the two numerals are within delta of each other. +// +// a.InDeltaf(math.Pi, 22/7.0, 0.01, "error message %s", "formatted") +func (a *Assertions) InDeltaf(expected interface{}, actual interface{}, delta float64, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + InDeltaf(a.t, expected, actual, delta, msg, args...) +} + +// InEpsilon asserts that expected and actual have a relative error less than epsilon +func (a *Assertions) InEpsilon(expected interface{}, actual interface{}, epsilon float64, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + InEpsilon(a.t, expected, actual, epsilon, msgAndArgs...) +} + +// InEpsilonSlice is the same as InEpsilon, except it compares each value from two slices. +func (a *Assertions) InEpsilonSlice(expected interface{}, actual interface{}, epsilon float64, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + InEpsilonSlice(a.t, expected, actual, epsilon, msgAndArgs...) +} + +// InEpsilonSlicef is the same as InEpsilon, except it compares each value from two slices. +func (a *Assertions) InEpsilonSlicef(expected interface{}, actual interface{}, epsilon float64, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + InEpsilonSlicef(a.t, expected, actual, epsilon, msg, args...) +} + +// InEpsilonf asserts that expected and actual have a relative error less than epsilon +func (a *Assertions) InEpsilonf(expected interface{}, actual interface{}, epsilon float64, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + InEpsilonf(a.t, expected, actual, epsilon, msg, args...) +} + +// IsDecreasing asserts that the collection is decreasing +// +// a.IsDecreasing([]int{2, 1, 0}) +// a.IsDecreasing([]float{2, 1}) +// a.IsDecreasing([]string{"b", "a"}) +func (a *Assertions) IsDecreasing(object interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + IsDecreasing(a.t, object, msgAndArgs...) +} + +// IsDecreasingf asserts that the collection is decreasing +// +// a.IsDecreasingf([]int{2, 1, 0}, "error message %s", "formatted") +// a.IsDecreasingf([]float{2, 1}, "error message %s", "formatted") +// a.IsDecreasingf([]string{"b", "a"}, "error message %s", "formatted") +func (a *Assertions) IsDecreasingf(object interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + IsDecreasingf(a.t, object, msg, args...) +} + +// IsIncreasing asserts that the collection is increasing +// +// a.IsIncreasing([]int{1, 2, 3}) +// a.IsIncreasing([]float{1, 2}) +// a.IsIncreasing([]string{"a", "b"}) +func (a *Assertions) IsIncreasing(object interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + IsIncreasing(a.t, object, msgAndArgs...) +} + +// IsIncreasingf asserts that the collection is increasing +// +// a.IsIncreasingf([]int{1, 2, 3}, "error message %s", "formatted") +// a.IsIncreasingf([]float{1, 2}, "error message %s", "formatted") +// a.IsIncreasingf([]string{"a", "b"}, "error message %s", "formatted") +func (a *Assertions) IsIncreasingf(object interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + IsIncreasingf(a.t, object, msg, args...) +} + +// IsNonDecreasing asserts that the collection is not decreasing +// +// a.IsNonDecreasing([]int{1, 1, 2}) +// a.IsNonDecreasing([]float{1, 2}) +// a.IsNonDecreasing([]string{"a", "b"}) +func (a *Assertions) IsNonDecreasing(object interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + IsNonDecreasing(a.t, object, msgAndArgs...) +} + +// IsNonDecreasingf asserts that the collection is not decreasing +// +// a.IsNonDecreasingf([]int{1, 1, 2}, "error message %s", "formatted") +// a.IsNonDecreasingf([]float{1, 2}, "error message %s", "formatted") +// a.IsNonDecreasingf([]string{"a", "b"}, "error message %s", "formatted") +func (a *Assertions) IsNonDecreasingf(object interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + IsNonDecreasingf(a.t, object, msg, args...) +} + +// IsNonIncreasing asserts that the collection is not increasing +// +// a.IsNonIncreasing([]int{2, 1, 1}) +// a.IsNonIncreasing([]float{2, 1}) +// a.IsNonIncreasing([]string{"b", "a"}) +func (a *Assertions) IsNonIncreasing(object interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + IsNonIncreasing(a.t, object, msgAndArgs...) +} + +// IsNonIncreasingf asserts that the collection is not increasing +// +// a.IsNonIncreasingf([]int{2, 1, 1}, "error message %s", "formatted") +// a.IsNonIncreasingf([]float{2, 1}, "error message %s", "formatted") +// a.IsNonIncreasingf([]string{"b", "a"}, "error message %s", "formatted") +func (a *Assertions) IsNonIncreasingf(object interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + IsNonIncreasingf(a.t, object, msg, args...) +} + +// IsNotType asserts that the specified objects are not of the same type. +// +// a.IsNotType(&NotMyStruct{}, &MyStruct{}) +func (a *Assertions) IsNotType(theType interface{}, object interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + IsNotType(a.t, theType, object, msgAndArgs...) +} + +// IsNotTypef asserts that the specified objects are not of the same type. +// +// a.IsNotTypef(&NotMyStruct{}, &MyStruct{}, "error message %s", "formatted") +func (a *Assertions) IsNotTypef(theType interface{}, object interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + IsNotTypef(a.t, theType, object, msg, args...) +} + +// IsType asserts that the specified objects are of the same type. +// +// a.IsType(&MyStruct{}, &MyStruct{}) +func (a *Assertions) IsType(expectedType interface{}, object interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + IsType(a.t, expectedType, object, msgAndArgs...) +} + +// IsTypef asserts that the specified objects are of the same type. +// +// a.IsTypef(&MyStruct{}, &MyStruct{}, "error message %s", "formatted") +func (a *Assertions) IsTypef(expectedType interface{}, object interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + IsTypef(a.t, expectedType, object, msg, args...) +} + +// JSONEq asserts that two JSON strings are equivalent. +// +// a.JSONEq(`{"hello": "world", "foo": "bar"}`, `{"foo": "bar", "hello": "world"}`) +func (a *Assertions) JSONEq(expected string, actual string, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + JSONEq(a.t, expected, actual, msgAndArgs...) +} + +// JSONEqf asserts that two JSON strings are equivalent. +// +// a.JSONEqf(`{"hello": "world", "foo": "bar"}`, `{"foo": "bar", "hello": "world"}`, "error message %s", "formatted") +func (a *Assertions) JSONEqf(expected string, actual string, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + JSONEqf(a.t, expected, actual, msg, args...) +} + +// Len asserts that the specified object has specific length. +// Len also fails if the object has a type that len() not accept. +// +// a.Len(mySlice, 3) +func (a *Assertions) Len(object interface{}, length int, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Len(a.t, object, length, msgAndArgs...) +} + +// Lenf asserts that the specified object has specific length. +// Lenf also fails if the object has a type that len() not accept. +// +// a.Lenf(mySlice, 3, "error message %s", "formatted") +func (a *Assertions) Lenf(object interface{}, length int, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Lenf(a.t, object, length, msg, args...) +} + +// Less asserts that the first element is less than the second +// +// a.Less(1, 2) +// a.Less(float64(1), float64(2)) +// a.Less("a", "b") +func (a *Assertions) Less(e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Less(a.t, e1, e2, msgAndArgs...) +} + +// LessOrEqual asserts that the first element is less than or equal to the second +// +// a.LessOrEqual(1, 2) +// a.LessOrEqual(2, 2) +// a.LessOrEqual("a", "b") +// a.LessOrEqual("b", "b") +func (a *Assertions) LessOrEqual(e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + LessOrEqual(a.t, e1, e2, msgAndArgs...) +} + +// LessOrEqualf asserts that the first element is less than or equal to the second +// +// a.LessOrEqualf(1, 2, "error message %s", "formatted") +// a.LessOrEqualf(2, 2, "error message %s", "formatted") +// a.LessOrEqualf("a", "b", "error message %s", "formatted") +// a.LessOrEqualf("b", "b", "error message %s", "formatted") +func (a *Assertions) LessOrEqualf(e1 interface{}, e2 interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + LessOrEqualf(a.t, e1, e2, msg, args...) +} + +// Lessf asserts that the first element is less than the second +// +// a.Lessf(1, 2, "error message %s", "formatted") +// a.Lessf(float64(1), float64(2), "error message %s", "formatted") +// a.Lessf("a", "b", "error message %s", "formatted") +func (a *Assertions) Lessf(e1 interface{}, e2 interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Lessf(a.t, e1, e2, msg, args...) +} + +// Negative asserts that the specified element is negative +// +// a.Negative(-1) +// a.Negative(-1.23) +func (a *Assertions) Negative(e interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Negative(a.t, e, msgAndArgs...) +} + +// Negativef asserts that the specified element is negative +// +// a.Negativef(-1, "error message %s", "formatted") +// a.Negativef(-1.23, "error message %s", "formatted") +func (a *Assertions) Negativef(e interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Negativef(a.t, e, msg, args...) +} + +// Never asserts that the given condition doesn't satisfy in waitFor time, +// periodically checking the target function each tick. +// +// a.Never(func() bool { return false; }, time.Second, 10*time.Millisecond) +func (a *Assertions) Never(condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Never(a.t, condition, waitFor, tick, msgAndArgs...) +} + +// Neverf asserts that the given condition doesn't satisfy in waitFor time, +// periodically checking the target function each tick. +// +// a.Neverf(func() bool { return false; }, time.Second, 10*time.Millisecond, "error message %s", "formatted") +func (a *Assertions) Neverf(condition func() bool, waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Neverf(a.t, condition, waitFor, tick, msg, args...) +} + +// Nil asserts that the specified object is nil. +// +// a.Nil(err) +func (a *Assertions) Nil(object interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Nil(a.t, object, msgAndArgs...) +} + +// Nilf asserts that the specified object is nil. +// +// a.Nilf(err, "error message %s", "formatted") +func (a *Assertions) Nilf(object interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Nilf(a.t, object, msg, args...) +} + +// NoDirExists checks whether a directory does not exist in the given path. +// It fails if the path points to an existing _directory_ only. +func (a *Assertions) NoDirExists(path string, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NoDirExists(a.t, path, msgAndArgs...) +} + +// NoDirExistsf checks whether a directory does not exist in the given path. +// It fails if the path points to an existing _directory_ only. +func (a *Assertions) NoDirExistsf(path string, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NoDirExistsf(a.t, path, msg, args...) +} + +// NoError asserts that a function returned no error (i.e. `nil`). +// +// actualObj, err := SomeFunction() +// if a.NoError(err) { +// assert.Equal(t, expectedObj, actualObj) +// } +func (a *Assertions) NoError(err error, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NoError(a.t, err, msgAndArgs...) +} + +// NoErrorf asserts that a function returned no error (i.e. `nil`). +// +// actualObj, err := SomeFunction() +// if a.NoErrorf(err, "error message %s", "formatted") { +// assert.Equal(t, expectedObj, actualObj) +// } +func (a *Assertions) NoErrorf(err error, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NoErrorf(a.t, err, msg, args...) +} + +// NoFileExists checks whether a file does not exist in a given path. It fails +// if the path points to an existing _file_ only. +func (a *Assertions) NoFileExists(path string, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NoFileExists(a.t, path, msgAndArgs...) +} + +// NoFileExistsf checks whether a file does not exist in a given path. It fails +// if the path points to an existing _file_ only. +func (a *Assertions) NoFileExistsf(path string, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NoFileExistsf(a.t, path, msg, args...) +} + +// NotContains asserts that the specified string, list(array, slice...) or map does NOT contain the +// specified substring or element. +// +// a.NotContains("Hello World", "Earth") +// a.NotContains(["Hello", "World"], "Earth") +// a.NotContains({"Hello": "World"}, "Earth") +func (a *Assertions) NotContains(s interface{}, contains interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotContains(a.t, s, contains, msgAndArgs...) +} + +// NotContainsf asserts that the specified string, list(array, slice...) or map does NOT contain the +// specified substring or element. +// +// a.NotContainsf("Hello World", "Earth", "error message %s", "formatted") +// a.NotContainsf(["Hello", "World"], "Earth", "error message %s", "formatted") +// a.NotContainsf({"Hello": "World"}, "Earth", "error message %s", "formatted") +func (a *Assertions) NotContainsf(s interface{}, contains interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotContainsf(a.t, s, contains, msg, args...) +} + +// NotElementsMatch asserts that the specified listA(array, slice...) is NOT equal to specified +// 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 not match. +// This is an inverse of ElementsMatch. +// +// a.NotElementsMatch([1, 1, 2, 3], [1, 1, 2, 3]) -> false +// +// a.NotElementsMatch([1, 1, 2, 3], [1, 2, 3]) -> true +// +// a.NotElementsMatch([1, 2, 3], [1, 2, 4]) -> true +func (a *Assertions) NotElementsMatch(listA interface{}, listB interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotElementsMatch(a.t, listA, listB, msgAndArgs...) +} + +// NotElementsMatchf asserts that the specified listA(array, slice...) is NOT equal to specified +// 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 not match. +// This is an inverse of ElementsMatch. +// +// a.NotElementsMatchf([1, 1, 2, 3], [1, 1, 2, 3], "error message %s", "formatted") -> false +// +// a.NotElementsMatchf([1, 1, 2, 3], [1, 2, 3], "error message %s", "formatted") -> true +// +// a.NotElementsMatchf([1, 2, 3], [1, 2, 4], "error message %s", "formatted") -> true +func (a *Assertions) NotElementsMatchf(listA interface{}, listB interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotElementsMatchf(a.t, listA, listB, msg, args...) +} + +// NotEmpty asserts that the specified object is NOT [Empty]. +// +// if a.NotEmpty(obj) { +// assert.Equal(t, "two", obj[1]) +// } +func (a *Assertions) NotEmpty(object interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotEmpty(a.t, object, msgAndArgs...) +} + +// NotEmptyf asserts that the specified object is NOT [Empty]. +// +// if a.NotEmptyf(obj, "error message %s", "formatted") { +// assert.Equal(t, "two", obj[1]) +// } +func (a *Assertions) NotEmptyf(object interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotEmptyf(a.t, object, msg, args...) +} + +// NotEqual asserts that the specified values are NOT equal. +// +// a.NotEqual(obj1, obj2) +// +// Pointer variable equality is determined based on the equality of the +// referenced values (as opposed to the memory addresses). +func (a *Assertions) NotEqual(expected interface{}, actual interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotEqual(a.t, expected, actual, msgAndArgs...) +} + +// NotEqualValues asserts that two objects are not equal even when converted to the same type +// +// a.NotEqualValues(obj1, obj2) +func (a *Assertions) NotEqualValues(expected interface{}, actual interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotEqualValues(a.t, expected, actual, msgAndArgs...) +} + +// NotEqualValuesf asserts that two objects are not equal even when converted to the same type +// +// a.NotEqualValuesf(obj1, obj2, "error message %s", "formatted") +func (a *Assertions) NotEqualValuesf(expected interface{}, actual interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotEqualValuesf(a.t, expected, actual, msg, args...) +} + +// NotEqualf asserts that the specified values are NOT equal. +// +// a.NotEqualf(obj1, obj2, "error message %s", "formatted") +// +// Pointer variable equality is determined based on the equality of the +// referenced values (as opposed to the memory addresses). +func (a *Assertions) NotEqualf(expected interface{}, actual interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotEqualf(a.t, expected, actual, msg, args...) +} + +// NotErrorAs asserts that none of the errors in err's chain matches target, +// but if so, sets target to that error value. +func (a *Assertions) NotErrorAs(err error, target interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotErrorAs(a.t, err, target, msgAndArgs...) +} + +// NotErrorAsf asserts that none of the errors in err's chain matches target, +// but if so, sets target to that error value. +func (a *Assertions) NotErrorAsf(err error, target interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotErrorAsf(a.t, err, target, msg, args...) +} + +// NotErrorIs asserts that none of the errors in err's chain matches target. +// This is a wrapper for errors.Is. +func (a *Assertions) NotErrorIs(err error, target error, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotErrorIs(a.t, err, target, msgAndArgs...) +} + +// NotErrorIsf asserts that none of the errors in err's chain matches target. +// This is a wrapper for errors.Is. +func (a *Assertions) NotErrorIsf(err error, target error, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotErrorIsf(a.t, err, target, msg, args...) +} + +// NotImplements asserts that an object does not implement the specified interface. +// +// a.NotImplements((*MyInterface)(nil), new(MyObject)) +func (a *Assertions) NotImplements(interfaceObject interface{}, object interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotImplements(a.t, interfaceObject, object, msgAndArgs...) +} + +// NotImplementsf asserts that an object does not implement the specified interface. +// +// a.NotImplementsf((*MyInterface)(nil), new(MyObject), "error message %s", "formatted") +func (a *Assertions) NotImplementsf(interfaceObject interface{}, object interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotImplementsf(a.t, interfaceObject, object, msg, args...) +} + +// NotNil asserts that the specified object is not nil. +// +// a.NotNil(err) +func (a *Assertions) NotNil(object interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotNil(a.t, object, msgAndArgs...) +} + +// NotNilf asserts that the specified object is not nil. +// +// a.NotNilf(err, "error message %s", "formatted") +func (a *Assertions) NotNilf(object interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotNilf(a.t, object, msg, args...) +} + +// NotPanics asserts that the code inside the specified PanicTestFunc does NOT panic. +// +// a.NotPanics(func(){ RemainCalm() }) +func (a *Assertions) NotPanics(f assert.PanicTestFunc, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotPanics(a.t, f, msgAndArgs...) +} + +// NotPanicsf asserts that the code inside the specified PanicTestFunc does NOT panic. +// +// a.NotPanicsf(func(){ RemainCalm() }, "error message %s", "formatted") +func (a *Assertions) NotPanicsf(f assert.PanicTestFunc, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotPanicsf(a.t, f, msg, args...) +} + +// NotRegexp asserts that a specified regexp does not match a string. +// +// a.NotRegexp(regexp.MustCompile("starts"), "it's starting") +// a.NotRegexp("^start", "it's not starting") +func (a *Assertions) NotRegexp(rx interface{}, str interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotRegexp(a.t, rx, str, msgAndArgs...) +} + +// NotRegexpf asserts that a specified regexp does not match a string. +// +// a.NotRegexpf(regexp.MustCompile("starts"), "it's starting", "error message %s", "formatted") +// a.NotRegexpf("^start", "it's not starting", "error message %s", "formatted") +func (a *Assertions) NotRegexpf(rx interface{}, str interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotRegexpf(a.t, rx, str, msg, args...) +} + +// NotSame asserts that two pointers do not reference the same object. +// +// a.NotSame(ptr1, ptr2) +// +// Both arguments must be pointer variables. Pointer variable sameness is +// determined based on the equality of both type and value. +func (a *Assertions) NotSame(expected interface{}, actual interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotSame(a.t, expected, actual, msgAndArgs...) +} + +// NotSamef asserts that two pointers do not reference the same object. +// +// a.NotSamef(ptr1, ptr2, "error message %s", "formatted") +// +// Both arguments must be pointer variables. Pointer variable sameness is +// determined based on the equality of both type and value. +func (a *Assertions) NotSamef(expected interface{}, actual interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotSamef(a.t, expected, actual, msg, args...) +} + +// NotSubset asserts that the list (array, slice, or map) does NOT contain all +// elements given in the subset (array, slice, or map). +// Map elements are key-value pairs unless compared with an array or slice where +// only the map key is evaluated. +// +// a.NotSubset([1, 3, 4], [1, 2]) +// a.NotSubset({"x": 1, "y": 2}, {"z": 3}) +// a.NotSubset([1, 3, 4], {1: "one", 2: "two"}) +// a.NotSubset({"x": 1, "y": 2}, ["z"]) +func (a *Assertions) NotSubset(list interface{}, subset interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotSubset(a.t, list, subset, msgAndArgs...) +} + +// NotSubsetf asserts that the list (array, slice, or map) does NOT contain all +// elements given in the subset (array, slice, or map). +// Map elements are key-value pairs unless compared with an array or slice where +// only the map key is evaluated. +// +// a.NotSubsetf([1, 3, 4], [1, 2], "error message %s", "formatted") +// a.NotSubsetf({"x": 1, "y": 2}, {"z": 3}, "error message %s", "formatted") +// a.NotSubsetf([1, 3, 4], {1: "one", 2: "two"}, "error message %s", "formatted") +// a.NotSubsetf({"x": 1, "y": 2}, ["z"], "error message %s", "formatted") +func (a *Assertions) NotSubsetf(list interface{}, subset interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotSubsetf(a.t, list, subset, msg, args...) +} + +// NotZero asserts that i is not the zero value for its type. +func (a *Assertions) NotZero(i interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotZero(a.t, i, msgAndArgs...) +} + +// NotZerof asserts that i is not the zero value for its type. +func (a *Assertions) NotZerof(i interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotZerof(a.t, i, msg, args...) +} + +// Panics asserts that the code inside the specified PanicTestFunc panics. +// +// a.Panics(func(){ GoCrazy() }) +func (a *Assertions) Panics(f assert.PanicTestFunc, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Panics(a.t, f, msgAndArgs...) +} + +// PanicsWithError asserts that the code inside the specified PanicTestFunc +// panics, and that the recovered panic value is an error that satisfies the +// EqualError comparison. +// +// a.PanicsWithError("crazy error", func(){ GoCrazy() }) +func (a *Assertions) PanicsWithError(errString string, f assert.PanicTestFunc, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + PanicsWithError(a.t, errString, f, msgAndArgs...) +} + +// PanicsWithErrorf asserts that the code inside the specified PanicTestFunc +// panics, and that the recovered panic value is an error that satisfies the +// EqualError comparison. +// +// a.PanicsWithErrorf("crazy error", func(){ GoCrazy() }, "error message %s", "formatted") +func (a *Assertions) PanicsWithErrorf(errString string, f assert.PanicTestFunc, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + PanicsWithErrorf(a.t, errString, f, msg, args...) +} + +// PanicsWithValue asserts that the code inside the specified PanicTestFunc panics, and that +// the recovered panic value equals the expected panic value. +// +// a.PanicsWithValue("crazy error", func(){ GoCrazy() }) +func (a *Assertions) PanicsWithValue(expected interface{}, f assert.PanicTestFunc, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + PanicsWithValue(a.t, expected, f, msgAndArgs...) +} + +// PanicsWithValuef asserts that the code inside the specified PanicTestFunc panics, and that +// the recovered panic value equals the expected panic value. +// +// a.PanicsWithValuef("crazy error", func(){ GoCrazy() }, "error message %s", "formatted") +func (a *Assertions) PanicsWithValuef(expected interface{}, f assert.PanicTestFunc, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + PanicsWithValuef(a.t, expected, f, msg, args...) +} + +// Panicsf asserts that the code inside the specified PanicTestFunc panics. +// +// a.Panicsf(func(){ GoCrazy() }, "error message %s", "formatted") +func (a *Assertions) Panicsf(f assert.PanicTestFunc, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Panicsf(a.t, f, msg, args...) +} + +// Positive asserts that the specified element is positive +// +// a.Positive(1) +// a.Positive(1.23) +func (a *Assertions) Positive(e interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Positive(a.t, e, msgAndArgs...) +} + +// Positivef asserts that the specified element is positive +// +// a.Positivef(1, "error message %s", "formatted") +// a.Positivef(1.23, "error message %s", "formatted") +func (a *Assertions) Positivef(e interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Positivef(a.t, e, msg, args...) +} + +// Regexp asserts that a specified regexp matches a string. +// +// a.Regexp(regexp.MustCompile("start"), "it's starting") +// a.Regexp("start...$", "it's not starting") +func (a *Assertions) Regexp(rx interface{}, str interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Regexp(a.t, rx, str, msgAndArgs...) +} + +// Regexpf asserts that a specified regexp matches a string. +// +// a.Regexpf(regexp.MustCompile("start"), "it's starting", "error message %s", "formatted") +// a.Regexpf("start...$", "it's not starting", "error message %s", "formatted") +func (a *Assertions) Regexpf(rx interface{}, str interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Regexpf(a.t, rx, str, msg, args...) +} + +// Same asserts that two pointers reference the same object. +// +// a.Same(ptr1, ptr2) +// +// Both arguments must be pointer variables. Pointer variable sameness is +// determined based on the equality of both type and value. +func (a *Assertions) Same(expected interface{}, actual interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Same(a.t, expected, actual, msgAndArgs...) +} + +// Samef asserts that two pointers reference the same object. +// +// a.Samef(ptr1, ptr2, "error message %s", "formatted") +// +// Both arguments must be pointer variables. Pointer variable sameness is +// determined based on the equality of both type and value. +func (a *Assertions) Samef(expected interface{}, actual interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Samef(a.t, expected, actual, msg, args...) +} + +// Subset asserts that the list (array, slice, or map) contains all elements +// given in the subset (array, slice, or map). +// Map elements are key-value pairs unless compared with an array or slice where +// only the map key is evaluated. +// +// a.Subset([1, 2, 3], [1, 2]) +// a.Subset({"x": 1, "y": 2}, {"x": 1}) +// a.Subset([1, 2, 3], {1: "one", 2: "two"}) +// a.Subset({"x": 1, "y": 2}, ["x"]) +func (a *Assertions) Subset(list interface{}, subset interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Subset(a.t, list, subset, msgAndArgs...) +} + +// Subsetf asserts that the list (array, slice, or map) contains all elements +// given in the subset (array, slice, or map). +// Map elements are key-value pairs unless compared with an array or slice where +// only the map key is evaluated. +// +// a.Subsetf([1, 2, 3], [1, 2], "error message %s", "formatted") +// a.Subsetf({"x": 1, "y": 2}, {"x": 1}, "error message %s", "formatted") +// a.Subsetf([1, 2, 3], {1: "one", 2: "two"}, "error message %s", "formatted") +// a.Subsetf({"x": 1, "y": 2}, ["x"], "error message %s", "formatted") +func (a *Assertions) Subsetf(list interface{}, subset interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Subsetf(a.t, list, subset, msg, args...) +} + +// True asserts that the specified value is true. +// +// a.True(myBool) +func (a *Assertions) True(value bool, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + True(a.t, value, msgAndArgs...) +} + +// Truef asserts that the specified value is true. +// +// a.Truef(myBool, "error message %s", "formatted") +func (a *Assertions) Truef(value bool, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Truef(a.t, value, msg, args...) +} + +// WithinDuration asserts that the two times are within duration delta of each other. +// +// a.WithinDuration(time.Now(), time.Now(), 10*time.Second) +func (a *Assertions) WithinDuration(expected time.Time, actual time.Time, delta time.Duration, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + WithinDuration(a.t, expected, actual, delta, msgAndArgs...) +} + +// WithinDurationf asserts that the two times are within duration delta of each other. +// +// a.WithinDurationf(time.Now(), time.Now(), 10*time.Second, "error message %s", "formatted") +func (a *Assertions) WithinDurationf(expected time.Time, actual time.Time, delta time.Duration, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + WithinDurationf(a.t, expected, actual, delta, msg, args...) +} + +// WithinRange asserts that a time is within a time range (inclusive). +// +// a.WithinRange(time.Now(), time.Now().Add(-time.Second), time.Now().Add(time.Second)) +func (a *Assertions) WithinRange(actual time.Time, start time.Time, end time.Time, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + WithinRange(a.t, actual, start, end, msgAndArgs...) +} + +// WithinRangef asserts that a time is within a time range (inclusive). +// +// a.WithinRangef(time.Now(), time.Now().Add(-time.Second), time.Now().Add(time.Second), "error message %s", "formatted") +func (a *Assertions) WithinRangef(actual time.Time, start time.Time, end time.Time, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + WithinRangef(a.t, actual, start, end, msg, args...) +} + +// YAMLEq asserts that two YAML strings are equivalent. +func (a *Assertions) YAMLEq(expected string, actual string, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + YAMLEq(a.t, expected, actual, msgAndArgs...) +} + +// YAMLEqf asserts that two YAML strings are equivalent. +func (a *Assertions) YAMLEqf(expected string, actual string, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + YAMLEqf(a.t, expected, actual, msg, args...) +} + +// Zero asserts that i is the zero value for its type. +func (a *Assertions) Zero(i interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Zero(a.t, i, msgAndArgs...) +} + +// Zerof asserts that i is the zero value for its type. +func (a *Assertions) Zerof(i interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Zerof(a.t, i, msg, args...) +} diff --git a/pkg/testify/require/require_forward.go.tmpl b/pkg/testify/require/require_forward.go.tmpl new file mode 100644 index 00000000..54124df1 --- /dev/null +++ b/pkg/testify/require/require_forward.go.tmpl @@ -0,0 +1,5 @@ +{{.CommentWithoutT "a"}} +func (a *Assertions) {{.DocInfo.Name}}({{.Params}}) { + if h, ok := a.t.(tHelper); ok { h.Helper() } + {{.DocInfo.Name}}(a.t, {{.ForwardedParams}}) +} diff --git a/pkg/testify/require/requirements.go b/pkg/testify/require/requirements.go new file mode 100644 index 00000000..6b7ce929 --- /dev/null +++ b/pkg/testify/require/requirements.go @@ -0,0 +1,29 @@ +package require + +// TestingT is an interface wrapper around *testing.T +type TestingT interface { + Errorf(format string, args ...interface{}) + FailNow() +} + +type tHelper = interface { + Helper() +} + +// ComparisonAssertionFunc is a common function prototype when comparing two values. Can be useful +// for table driven tests. +type ComparisonAssertionFunc func(TestingT, interface{}, interface{}, ...interface{}) + +// ValueAssertionFunc is a common function prototype when validating a single value. Can be useful +// for table driven tests. +type ValueAssertionFunc func(TestingT, interface{}, ...interface{}) + +// BoolAssertionFunc is a common function prototype when validating a bool value. Can be useful +// for table driven tests. +type BoolAssertionFunc func(TestingT, bool, ...interface{}) + +// ErrorAssertionFunc is a common function prototype when validating an error value. Can be useful +// for table driven tests. +type ErrorAssertionFunc func(TestingT, error, ...interface{}) + +//go:generate sh -c "cd ../_codegen && go build && cd - && ../_codegen/_codegen -output-package=require -template=require.go.tmpl -include-format-funcs" diff --git a/pkg/testify/require/requirements_test.go b/pkg/testify/require/requirements_test.go new file mode 100644 index 00000000..efcef209 --- /dev/null +++ b/pkg/testify/require/requirements_test.go @@ -0,0 +1,790 @@ +package require + +import ( + "encoding/json" + "errors" + testing "github.com/CodSpeedHQ/codspeed-go/testing/testing" + "time" + + "github.com/CodSpeedHQ/codspeed-go/pkg/testify/assert" +) + +// 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 +type AssertionTesterConformingObject struct { +} + +func (a *AssertionTesterConformingObject) TestMethod() { +} + +// AssertionTesterNonConformingObject is an object that does not conform to the AssertionTesterInterface interface +type AssertionTesterNonConformingObject struct { +} + +type MockT struct { + Failed bool +} + +// Helper is like [testing.T.Helper] but does nothing. +func (MockT) Helper() {} + +func (t *MockT) FailNow() { + t.Failed = true +} + +func (t *MockT) Errorf(format string, args ...interface{}) { + _, _ = format, args +} + +func TestImplements(t *testing.T) { + t.Parallel() + + Implements(t, (*AssertionTesterInterface)(nil), new(AssertionTesterConformingObject)) + + mockT := new(MockT) + Implements(mockT, (*AssertionTesterInterface)(nil), new(AssertionTesterNonConformingObject)) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestIsType(t *testing.T) { + t.Parallel() + + IsType(t, new(AssertionTesterConformingObject), new(AssertionTesterConformingObject)) + + mockT := new(MockT) + IsType(mockT, new(AssertionTesterConformingObject), new(AssertionTesterNonConformingObject)) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestEqual(t *testing.T) { + t.Parallel() + + Equal(t, 1, 1) + + mockT := new(MockT) + Equal(mockT, 1, 2) + if !mockT.Failed { + t.Error("Check should fail") + } + +} + +func TestNotEqual(t *testing.T) { + t.Parallel() + + NotEqual(t, 1, 2) + mockT := new(MockT) + NotEqual(mockT, 2, 2) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestExactly(t *testing.T) { + t.Parallel() + + a := float32(1) + b := float32(1) + c := float64(1) + + Exactly(t, a, b) + + mockT := new(MockT) + Exactly(mockT, a, c) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestNotNil(t *testing.T) { + t.Parallel() + + NotNil(t, new(AssertionTesterConformingObject)) + + mockT := new(MockT) + NotNil(mockT, nil) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestNil(t *testing.T) { + t.Parallel() + + Nil(t, nil) + + mockT := new(MockT) + Nil(mockT, new(AssertionTesterConformingObject)) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestTrue(t *testing.T) { + t.Parallel() + + True(t, true) + + mockT := new(MockT) + True(mockT, false) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestFalse(t *testing.T) { + t.Parallel() + + False(t, false) + + mockT := new(MockT) + False(mockT, true) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestContains(t *testing.T) { + t.Parallel() + + Contains(t, "Hello World", "Hello") + + mockT := new(MockT) + Contains(mockT, "Hello World", "Salut") + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestNotContains(t *testing.T) { + t.Parallel() + + NotContains(t, "Hello World", "Hello!") + + mockT := new(MockT) + NotContains(mockT, "Hello World", "Hello") + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestPanics(t *testing.T) { + t.Parallel() + + Panics(t, func() { + panic("Panic!") + }) + + mockT := new(MockT) + Panics(mockT, func() {}) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestNotPanics(t *testing.T) { + t.Parallel() + + NotPanics(t, func() {}) + + mockT := new(MockT) + NotPanics(mockT, func() { + panic("Panic!") + }) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestNoError(t *testing.T) { + t.Parallel() + + NoError(t, nil) + + mockT := new(MockT) + NoError(mockT, errors.New("some error")) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestError(t *testing.T) { + t.Parallel() + + Error(t, errors.New("some error")) + + mockT := new(MockT) + Error(mockT, nil) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestErrorContains(t *testing.T) { + t.Parallel() + + ErrorContains(t, errors.New("some error: another error"), "some error") + + mockT := new(MockT) + ErrorContains(mockT, errors.New("some error"), "different error") + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestEqualError(t *testing.T) { + t.Parallel() + + EqualError(t, errors.New("some error"), "some error") + + mockT := new(MockT) + EqualError(mockT, errors.New("some error"), "Not some error") + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestEmpty(t *testing.T) { + t.Parallel() + + Empty(t, "") + + mockT := new(MockT) + Empty(mockT, "x") + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestNotEmpty(t *testing.T) { + t.Parallel() + + NotEmpty(t, "x") + + mockT := new(MockT) + NotEmpty(mockT, "") + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestWithinDuration(t *testing.T) { + t.Parallel() + + a := time.Now() + b := a.Add(10 * time.Second) + + WithinDuration(t, a, b, 15*time.Second) + + mockT := new(MockT) + WithinDuration(mockT, a, b, 5*time.Second) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestInDelta(t *testing.T) { + t.Parallel() + + InDelta(t, 1.001, 1, 0.01) + + mockT := new(MockT) + InDelta(mockT, 1, 2, 0.5) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestZero(t *testing.T) { + t.Parallel() + + Zero(t, "") + + mockT := new(MockT) + Zero(mockT, "x") + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestNotZero(t *testing.T) { + t.Parallel() + + NotZero(t, "x") + + mockT := new(MockT) + NotZero(mockT, "") + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestJSONEq_EqualSONString(t *testing.T) { + t.Parallel() + + mockT := new(MockT) + JSONEq(mockT, `{"hello": "world", "foo": "bar"}`, `{"hello": "world", "foo": "bar"}`) + if mockT.Failed { + t.Error("Check should pass") + } +} + +func TestJSONEq_EquivalentButNotEqual(t *testing.T) { + t.Parallel() + + mockT := new(MockT) + JSONEq(mockT, `{"hello": "world", "foo": "bar"}`, `{"foo": "bar", "hello": "world"}`) + if mockT.Failed { + t.Error("Check should pass") + } +} + +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}") + if mockT.Failed { + t.Error("Check should pass") + } +} + +func TestJSONEq_Array(t *testing.T) { + t.Parallel() + + mockT := new(MockT) + JSONEq(mockT, `["foo", {"hello": "world", "nested": "hash"}]`, `["foo", {"nested": "hash", "hello": "world"}]`) + if mockT.Failed { + t.Error("Check should pass") + } +} + +func TestJSONEq_HashAndArrayNotEquivalent(t *testing.T) { + t.Parallel() + + mockT := new(MockT) + JSONEq(mockT, `["foo", {"hello": "world", "nested": "hash"}]`, `{"foo": "bar", {"nested": "hash", "hello": "world"}}`) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestJSONEq_HashesNotEquivalent(t *testing.T) { + t.Parallel() + + mockT := new(MockT) + JSONEq(mockT, `{"foo": "bar"}`, `{"foo": "bar", "hello": "world"}`) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestJSONEq_ActualIsNotJSON(t *testing.T) { + t.Parallel() + + mockT := new(MockT) + JSONEq(mockT, `{"foo": "bar"}`, "Not JSON") + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestJSONEq_ExpectedIsNotJSON(t *testing.T) { + t.Parallel() + + mockT := new(MockT) + JSONEq(mockT, "Not JSON", `{"foo": "bar", "hello": "world"}`) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestJSONEq_ExpectedAndActualNotJSON(t *testing.T) { + t.Parallel() + + mockT := new(MockT) + JSONEq(mockT, "Not JSON", "Not JSON") + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestJSONEq_ArraysOfDifferentOrder(t *testing.T) { + t.Parallel() + + mockT := new(MockT) + JSONEq(mockT, `["foo", {"hello": "world", "nested": "hash"}]`, `[{ "hello": "world", "nested": "hash"}, "foo"]`) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestYAMLEq_EqualYAMLString(t *testing.T) { + t.Parallel() + + mockT := new(MockT) + YAMLEq(mockT, `{"hello": "world", "foo": "bar"}`, `{"hello": "world", "foo": "bar"}`) + if mockT.Failed { + t.Error("Check should pass") + } +} + +func TestYAMLEq_EquivalentButNotEqual(t *testing.T) { + t.Parallel() + + mockT := new(MockT) + YAMLEq(mockT, `{"hello": "world", "foo": "bar"}`, `{"foo": "bar", "hello": "world"}`) + if mockT.Failed { + t.Error("Check should pass") + } +} + +func TestYAMLEq_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] +` + YAMLEq(mockT, expected, actual) + if mockT.Failed { + t.Error("Check should pass") + } +} + +func TestYAMLEq_Array(t *testing.T) { + t.Parallel() + + mockT := new(MockT) + YAMLEq(mockT, `["foo", {"hello": "world", "nested": "hash"}]`, `["foo", {"nested": "hash", "hello": "world"}]`) + if mockT.Failed { + t.Error("Check should pass") + } +} + +func TestYAMLEq_HashAndArrayNotEquivalent(t *testing.T) { + t.Parallel() + + mockT := new(MockT) + YAMLEq(mockT, `["foo", {"hello": "world", "nested": "hash"}]`, `{"foo": "bar", {"nested": "hash", "hello": "world"}}`) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestYAMLEq_HashesNotEquivalent(t *testing.T) { + t.Parallel() + + mockT := new(MockT) + YAMLEq(mockT, `{"foo": "bar"}`, `{"foo": "bar", "hello": "world"}`) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestYAMLEq_ActualIsSimpleString(t *testing.T) { + t.Parallel() + + mockT := new(MockT) + YAMLEq(mockT, `{"foo": "bar"}`, "Simple String") + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestYAMLEq_ExpectedIsSimpleString(t *testing.T) { + t.Parallel() + + mockT := new(MockT) + YAMLEq(mockT, "Simple String", `{"foo": "bar", "hello": "world"}`) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestYAMLEq_ExpectedAndActualSimpleString(t *testing.T) { + t.Parallel() + + mockT := new(MockT) + YAMLEq(mockT, "Simple String", "Simple String") + if mockT.Failed { + t.Error("Check should pass") + } +} + +func TestYAMLEq_ArraysOfDifferentOrder(t *testing.T) { + t.Parallel() + + mockT := new(MockT) + YAMLEq(mockT, `["foo", {"hello": "world", "nested": "hash"}]`, `[{ "hello": "world", "nested": "hash"}, "foo"]`) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +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() + + type iface interface { + Name() string + } + + tests := []struct { + name string + expect interface{} + got interface{} + assertion ComparisonAssertionFunc + }{ + {"implements", (*iface)(nil), t, Implements}, + {"isType", (*testing.T)(nil), t, IsType}, + {"equal", t, t, Equal}, + {"equalValues", t, t, EqualValues}, + {"exactly", t, t, Exactly}, + {"notEqual", t, nil, NotEqual}, + {"NotEqualValues", t, nil, NotEqualValues}, + {"notContains", []int{1, 2, 3}, 4, NotContains}, + {"subset", []int{1, 2, 3, 4}, []int{2, 3}, Subset}, + {"notSubset", []int{1, 2, 3, 4}, []int{0, 3}, NotSubset}, + {"elementsMatch", []byte("abc"), []byte("bac"), ElementsMatch}, + {"regexp", "^t.*y$", "testify", Regexp}, + {"notRegexp", "^t.*y$", "Testify", NotRegexp}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.assertion(t, tt.expect, tt.got) + }) + } +} + +func ExampleValueAssertionFunc() { + t := &testing.T{} // provided by test + + dumbParse := func(input string) interface{} { + var x interface{} + 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() + + tests := []struct { + name string + value interface{} + assertion ValueAssertionFunc + }{ + {"notNil", true, NotNil}, + {"nil", nil, Nil}, + {"empty", []int{}, Empty}, + {"notEmpty", []int{1}, NotEmpty}, + {"zero", false, Zero}, + {"notZero", 42, NotZero}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.assertion(t, tt.value) + }) + } +} + +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() + + tests := []struct { + name string + value bool + assertion BoolAssertionFunc + }{ + {"true", true, True}, + {"false", false, False}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.assertion(t, tt.value) + }) + } +} + +func ExampleErrorAssertionFunc() { + t := &testing.T{} // provided by test + + dumbParseNum := func(input string, v interface{}) 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() + + tests := []struct { + name string + err error + assertion ErrorAssertionFunc + }{ + {"noError", nil, NoError}, + {"error", errors.New("whoops"), Error}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.assertion(t, tt.err) + }) + } +} + +func TestEventuallyWithTFalse(t *testing.T) { + t.Parallel() + + mockT := new(MockT) + + condition := func(collect *assert.CollectT) { + True(collect, false) + } + + EventuallyWithT(mockT, condition, 100*time.Millisecond, 20*time.Millisecond) + True(t, mockT.Failed, "Check should fail") +} + +func TestEventuallyWithTTrue(t *testing.T) { + t.Parallel() + + mockT := new(MockT) + + counter := 0 + condition := func(collect *assert.CollectT) { + defer func() { + counter += 1 + }() + True(collect, counter == 1) + } + + EventuallyWithT(mockT, condition, 100*time.Millisecond, 20*time.Millisecond) + False(t, mockT.Failed, "Check should pass") + Equal(t, 2, counter, "Condition is expected to be called 2 times") +} diff --git a/pkg/testify/suite/doc.go b/pkg/testify/suite/doc.go new file mode 100644 index 00000000..ce94883a --- /dev/null +++ b/pkg/testify/suite/doc.go @@ -0,0 +1,70 @@ +// Package suite contains logic for creating testing suite structs +// and running the methods on those structs as tests. The most useful +// piece of this package is that you can create setup/teardown methods +// on your testing suites, which will run before/after the whole suite +// or individual tests (depending on which interface(s) you +// implement). +// +// The suite package does not support parallel tests. See [issue 934]. +// +// A testing suite is usually built by first extending the built-in +// suite functionality from suite.Suite in testify. Alternatively, +// you could reproduce that logic on your own if you wanted (you +// just need to implement the TestingSuite interface from +// suite/interfaces.go). +// +// After that, you can implement any of the interfaces in +// suite/interfaces.go to add setup/teardown functionality to your +// suite, and add any methods that start with "Test" to add tests. +// Methods that do not match any suite interfaces and do not begin +// with "Test" will not be run by testify, and can safely be used as +// helper methods. +// +// Once you've built your testing suite, you need to run the suite +// (using suite.Run from testify) inside any function that matches the +// identity that "go test" is already looking for (i.e. +// func(*testing.T)). +// +// Regular expression to select test suites specified command-line +// argument "-run". Regular expression to select the methods +// of test suites specified command-line argument "-m". +// Suite object has assertion methods. +// +// A crude example: +// +// // Basic imports +// import ( +// "testing" +// "github.com/CodSpeedHQ/codspeed-go/pkg/testify/assert" +// "github.com/CodSpeedHQ/codspeed-go/pkg/testify/suite" +// ) +// +// // Define the suite, and absorb the built-in basic suite +// // functionality from testify - including a T() method which +// // returns the current testing context +// type ExampleTestSuite struct { +// suite.Suite +// VariableThatShouldStartAtFive int +// } +// +// // Make sure that VariableThatShouldStartAtFive is set to five +// // before each test +// func (suite *ExampleTestSuite) SetupTest() { +// suite.VariableThatShouldStartAtFive = 5 +// } +// +// // All methods that begin with "Test" are run as tests within a +// // suite. +// func (suite *ExampleTestSuite) TestExample() { +// assert.Equal(suite.T(), 5, suite.VariableThatShouldStartAtFive) +// suite.Equal(5, suite.VariableThatShouldStartAtFive) +// } +// +// // In order for 'go test' to run this suite, we need to create +// // a normal test function and pass our suite to suite.Run +// func TestExampleTestSuite(t *testing.T) { +// suite.Run(t, new(ExampleTestSuite)) +// } +// +// [issue 934]: https://github.com/stretchr/testify/issues/934 +package suite diff --git a/pkg/testify/suite/interfaces.go b/pkg/testify/suite/interfaces.go new file mode 100644 index 00000000..b0edc01b --- /dev/null +++ b/pkg/testify/suite/interfaces.go @@ -0,0 +1,66 @@ +package suite + +import testing "github.com/CodSpeedHQ/codspeed-go/testing/testing" + +// TestingSuite can store and return the current *testing.T context +// generated by 'go test'. +type TestingSuite interface { + T() *testing.T + SetT(*testing.T) + SetS(suite TestingSuite) +} + +// SetupAllSuite has a SetupSuite method, which will run before the +// tests in the suite are run. +type SetupAllSuite interface { + SetupSuite() +} + +// SetupTestSuite has a SetupTest method, which will run before each +// test in the suite. +type SetupTestSuite interface { + SetupTest() +} + +// TearDownAllSuite has a TearDownSuite method, which will run after +// all the tests in the suite have been run. +type TearDownAllSuite interface { + TearDownSuite() +} + +// TearDownTestSuite has a TearDownTest method, which will run after +// each test in the suite. +type TearDownTestSuite interface { + TearDownTest() +} + +// BeforeTest has a function to be executed right before the test +// starts and receives the suite and test names as input +type BeforeTest interface { + BeforeTest(suiteName, testName string) +} + +// AfterTest has a function to be executed right after the test +// finishes and receives the suite and test names as input +type AfterTest interface { + AfterTest(suiteName, testName string) +} + +// WithStats implements HandleStats, a function that will be executed +// when a test suite is finished. The stats contain information about +// the execution of that suite and its tests. +type WithStats interface { + HandleStats(suiteName string, stats *SuiteInformation) +} + +// SetupSubTest has a SetupSubTest method, which will run before each +// subtest in the suite. +type SetupSubTest interface { + SetupSubTest() +} + +// TearDownSubTest has a TearDownSubTest method, which will run after +// each subtest in the suite have been run. +type TearDownSubTest interface { + TearDownSubTest() +} diff --git a/pkg/testify/suite/stats.go b/pkg/testify/suite/stats.go new file mode 100644 index 00000000..be4ccd67 --- /dev/null +++ b/pkg/testify/suite/stats.go @@ -0,0 +1,50 @@ +package suite + +import "time" + +// SuiteInformation stats stores stats for the whole suite execution. +type SuiteInformation struct { + Start, End time.Time + TestStats map[string]*TestInformation +} + +// TestInformation stores information about the execution of each test. +type TestInformation struct { + TestName string + Start, End time.Time + Passed bool +} + +func newSuiteInformation() *SuiteInformation { + return &SuiteInformation{ + TestStats: make(map[string]*TestInformation), + } +} + +func (s *SuiteInformation) start(testName string) { + if s == nil { + return + } + s.TestStats[testName] = &TestInformation{ + TestName: testName, + Start: time.Now(), + } +} + +func (s *SuiteInformation) end(testName string, passed bool) { + if s == nil { + return + } + s.TestStats[testName].End = time.Now() + s.TestStats[testName].Passed = passed +} + +func (s *SuiteInformation) Passed() bool { + for _, stats := range s.TestStats { + if !stats.Passed { + return false + } + } + + return true +} diff --git a/pkg/testify/suite/stats_test.go b/pkg/testify/suite/stats_test.go new file mode 100644 index 00000000..a88d3712 --- /dev/null +++ b/pkg/testify/suite/stats_test.go @@ -0,0 +1,40 @@ +package suite + +import ( + testing "github.com/CodSpeedHQ/codspeed-go/testing/testing" + + "github.com/CodSpeedHQ/codspeed-go/pkg/testify/assert" +) + +func TestPassedReturnsTrueWhenAllTestsPass(t *testing.T) { + sinfo := newSuiteInformation() + sinfo.TestStats = map[string]*TestInformation{ + "Test1": {TestName: "Test1", Passed: true}, + "Test2": {TestName: "Test2", Passed: true}, + "Test3": {TestName: "Test3", Passed: true}, + } + + assert.True(t, sinfo.Passed()) +} + +func TestPassedReturnsFalseWhenSomeTestFails(t *testing.T) { + sinfo := newSuiteInformation() + sinfo.TestStats = map[string]*TestInformation{ + "Test1": {TestName: "Test1", Passed: true}, + "Test2": {TestName: "Test2", Passed: false}, + "Test3": {TestName: "Test3", Passed: true}, + } + + assert.False(t, sinfo.Passed()) +} + +func TestPassedReturnsFalseWhenAllTestsFail(t *testing.T) { + sinfo := newSuiteInformation() + sinfo.TestStats = map[string]*TestInformation{ + "Test1": {TestName: "Test1", Passed: false}, + "Test2": {TestName: "Test2", Passed: false}, + "Test3": {TestName: "Test3", Passed: false}, + } + + assert.False(t, sinfo.Passed()) +} diff --git a/pkg/testify/suite/suite.go b/pkg/testify/suite/suite.go new file mode 100644 index 00000000..447140f9 --- /dev/null +++ b/pkg/testify/suite/suite.go @@ -0,0 +1,237 @@ +package suite + +import ( + "flag" + "fmt" + testing "github.com/CodSpeedHQ/codspeed-go/testing/testing" + "os" + "reflect" + "regexp" + "runtime/debug" + "strings" + "sync" + "time" + + "github.com/CodSpeedHQ/codspeed-go/pkg/testify/assert" + "github.com/CodSpeedHQ/codspeed-go/pkg/testify/require" +) + +var matchMethod = flag.String("testify.m", "", "regular expression to select tests of the testify suite to run") + +// Suite is a basic testing suite with methods for storing and +// retrieving the current *testing.T context. +type Suite struct { + *assert.Assertions + + mu sync.RWMutex + require *require.Assertions + t *testing.T + + // Parent suite to have access to the implemented methods of parent struct + s TestingSuite +} + +// T retrieves the current *testing.T context. +func (suite *Suite) T() *testing.T { + suite.mu.RLock() + defer suite.mu.RUnlock() + return suite.t +} + +// SetT sets the current *testing.T context. +func (suite *Suite) SetT(t *testing.T) { + suite.mu.Lock() + defer suite.mu.Unlock() + suite.t = t + suite.Assertions = assert.New(t) + suite.require = require.New(t) +} + +// SetS needs to set the current test suite as parent +// to get access to the parent methods +func (suite *Suite) SetS(s TestingSuite) { + suite.s = s +} + +// Require returns a require context for suite. +func (suite *Suite) Require() *require.Assertions { + suite.mu.Lock() + defer suite.mu.Unlock() + if suite.require == nil { + panic("'Require' must not be called before 'Run' or 'SetT'") + } + return suite.require +} + +// Assert returns an assert context for suite. Normally, you can call +// `suite.NoError(expected, actual)`, but for situations where the embedded +// methods are overridden (for example, you might want to override +// assert.Assertions with require.Assertions), this method is provided so you +// can call `suite.Assert().NoError()`. +func (suite *Suite) Assert() *assert.Assertions { + suite.mu.Lock() + defer suite.mu.Unlock() + if suite.Assertions == nil { + panic("'Assert' must not be called before 'Run' or 'SetT'") + } + return suite.Assertions +} + +func recoverAndFailOnPanic(t *testing.T) { + t.Helper() + r := recover() + failOnPanic(t, r) +} + +func failOnPanic(t *testing.T, r interface{}) { + t.Helper() + if r != nil { + t.Errorf("test panicked: %v\n%s", r, debug.Stack()) + t.FailNow() + } +} + +// Run provides suite functionality around golang subtests. It should be +// called in place of t.Run(name, func(t *testing.T)) in test suite code. +// The passed-in func will be executed as a subtest with a fresh instance of t. +// Provides compatibility with go test pkg -run TestSuite/TestName/SubTestName. +func (suite *Suite) Run(name string, subtest func()) bool { + oldT := suite.T() + + return oldT.Run(name, func(t *testing.T) { + suite.SetT(t) + defer suite.SetT(oldT) + + defer recoverAndFailOnPanic(t) + + if setupSubTest, ok := suite.s.(SetupSubTest); ok { + setupSubTest.SetupSubTest() + } + + if tearDownSubTest, ok := suite.s.(TearDownSubTest); ok { + defer tearDownSubTest.TearDownSubTest() + } + + subtest() + }) +} + +type test = struct { + name string + run func(t *testing.T) +} + +// Run takes a testing suite and runs all of the tests attached +// to it. +func Run(t *testing.T, suite TestingSuite) { + defer recoverAndFailOnPanic(t) + + suite.SetT(t) + suite.SetS(suite) + + var stats *SuiteInformation + if _, ok := suite.(WithStats); ok { + stats = newSuiteInformation() + } + + var tests []test + methodFinder := reflect.TypeOf(suite) + suiteName := methodFinder.Elem().Name() + + var matchMethodRE *regexp.Regexp + if *matchMethod != "" { + var err error + matchMethodRE, err = regexp.Compile(*matchMethod) + if err != nil { + fmt.Fprintf(os.Stderr, "testify: invalid regexp for -m: %s\n", err) + os.Exit(1) + } + } + + for i := 0; i < methodFinder.NumMethod(); i++ { + method := methodFinder.Method(i) + + if !strings.HasPrefix(method.Name, "Test") { + continue + } + // Apply -testify.m filter + if matchMethodRE != nil && !matchMethodRE.MatchString(method.Name) { + continue + } + + test := test{ + name: method.Name, + run: func(t *testing.T) { + parentT := suite.T() + suite.SetT(t) + defer recoverAndFailOnPanic(t) + defer func() { + t.Helper() + + r := recover() + + stats.end(method.Name, !t.Failed() && r == nil) + + if afterTestSuite, ok := suite.(AfterTest); ok { + afterTestSuite.AfterTest(suiteName, method.Name) + } + + if tearDownTestSuite, ok := suite.(TearDownTestSuite); ok { + tearDownTestSuite.TearDownTest() + } + + suite.SetT(parentT) + failOnPanic(t, r) + }() + + if setupTestSuite, ok := suite.(SetupTestSuite); ok { + setupTestSuite.SetupTest() + } + if beforeTestSuite, ok := suite.(BeforeTest); ok { + beforeTestSuite.BeforeTest(methodFinder.Elem().Name(), method.Name) + } + + stats.start(method.Name) + + method.Func.Call([]reflect.Value{reflect.ValueOf(suite)}) + }, + } + tests = append(tests, test) + } + + if len(tests) == 0 { + return + } + + if stats != nil { + stats.Start = time.Now() + } + + if setupAllSuite, ok := suite.(SetupAllSuite); ok { + setupAllSuite.SetupSuite() + } + + defer func() { + if tearDownAllSuite, ok := suite.(TearDownAllSuite); ok { + tearDownAllSuite.TearDownSuite() + } + + if suiteWithStats, measureStats := suite.(WithStats); measureStats { + stats.End = time.Now() + suiteWithStats.HandleStats(suiteName, stats) + } + }() + + runTests(t, tests) +} + +func runTests(t *testing.T, tests []test) { + if len(tests) == 0 { + t.Log("warning: no tests to run") + return + } + + for _, test := range tests { + t.Run(test.name, test.run) + } +} diff --git a/pkg/testify/suite/suite_test.go b/pkg/testify/suite/suite_test.go new file mode 100644 index 00000000..b47847b5 --- /dev/null +++ b/pkg/testify/suite/suite_test.go @@ -0,0 +1,753 @@ +package suite + +import ( + "bytes" + "errors" + "flag" + testing "github.com/CodSpeedHQ/codspeed-go/testing/testing" + "io" + "math/rand" + "os" + "os/exec" + "strings" + "time" + + "github.com/CodSpeedHQ/codspeed-go/pkg/testify/assert" + "github.com/CodSpeedHQ/codspeed-go/pkg/testify/require" +) + +// allTestsFilter is a yes filter for testing.RunTests +func allTestsFilter(pat, str string) (bool, error) { + return true, nil +} + +// SuiteRequireTwice is intended to test the usage of suite.Require in two +// different tests +type SuiteRequireTwice struct{ Suite } + +// TestSuiteRequireTwice checks for regressions of issue #149 where +// suite.requirements was not initialized in suite.SetT() +// A regression would result on these tests panicking rather than failing. +func TestSuiteRequireTwice(t *testing.T) { + ok := testing.RunTests( + allTestsFilter, + []testing.InternalTest{{ + Name: t.Name() + "/SuiteRequireTwice", + F: func(t *testing.T) { + suite := new(SuiteRequireTwice) + Run(t, suite) + }, + }}, + ) + assert.False(t, ok) +} + +func (s *SuiteRequireTwice) TestRequireOne() { + r := s.Require() + r.Equal(1, 2) +} + +func (s *SuiteRequireTwice) TestRequireTwo() { + r := s.Require() + r.Equal(1, 2) +} + +type panickingSuite struct { + Suite + panicInSetupSuite bool + panicInSetupTest bool + panicInBeforeTest bool + panicInTest bool + panicInAfterTest bool + panicInTearDownTest bool + panicInTearDownSuite bool +} + +func (s *panickingSuite) SetupSuite() { + if s.panicInSetupSuite { + panic("oops in setup suite") + } +} + +func (s *panickingSuite) SetupTest() { + if s.panicInSetupTest { + panic("oops in setup test") + } +} + +func (s *panickingSuite) BeforeTest(_, _ string) { + if s.panicInBeforeTest { + panic("oops in before test") + } +} + +func (s *panickingSuite) Test() { + if s.panicInTest { + panic("oops in test") + } +} + +func (s *panickingSuite) AfterTest(_, _ string) { + if s.panicInAfterTest { + panic("oops in after test") + } +} + +func (s *panickingSuite) TearDownTest() { + if s.panicInTearDownTest { + panic("oops in tear down test") + } +} + +func (s *panickingSuite) TearDownSuite() { + if s.panicInTearDownSuite { + panic("oops in tear down suite") + } +} + +func TestSuiteRecoverPanic(t *testing.T) { + ok := true + panickingTests := []testing.InternalTest{ + { + Name: t.Name() + "/InSetupSuite", + F: func(t *testing.T) { Run(t, &panickingSuite{panicInSetupSuite: true}) }, + }, + { + Name: t.Name() + "/InSetupTest", + F: func(t *testing.T) { Run(t, &panickingSuite{panicInSetupTest: true}) }, + }, + { + Name: t.Name() + "InBeforeTest", + F: func(t *testing.T) { Run(t, &panickingSuite{panicInBeforeTest: true}) }, + }, + { + Name: t.Name() + "/InTest", + F: func(t *testing.T) { Run(t, &panickingSuite{panicInTest: true}) }, + }, + { + Name: t.Name() + "/InAfterTest", + F: func(t *testing.T) { Run(t, &panickingSuite{panicInAfterTest: true}) }, + }, + { + Name: t.Name() + "/InTearDownTest", + F: func(t *testing.T) { Run(t, &panickingSuite{panicInTearDownTest: true}) }, + }, + { + Name: t.Name() + "/InTearDownSuite", + F: func(t *testing.T) { Run(t, &panickingSuite{panicInTearDownSuite: true}) }, + }, + } + + require.NotPanics(t, func() { + ok = testing.RunTests(allTestsFilter, panickingTests) + }) + + assert.False(t, ok) +} + +// This suite is intended to store values to make sure that only +// testing-suite-related methods are run. It's also a fully +// functional example of a testing suite, using setup/teardown methods +// and a helper method that is ignored by testify. To make this look +// more like a real world example, all tests in the suite perform some +// type of assertion. +type SuiteTester struct { + // Include our basic suite logic. + Suite + + // Keep counts of how many times each method is run. + SetupSuiteRunCount int + TearDownSuiteRunCount int + SetupTestRunCount int + TearDownTestRunCount int + TestOneRunCount int + TestTwoRunCount int + TestSubtestRunCount int + NonTestMethodRunCount int + SetupSubTestRunCount int + TearDownSubTestRunCount int + + SetupSubTestNames []string + TearDownSubTestNames []string + + SuiteNameBefore []string + TestNameBefore []string + + SuiteNameAfter []string + TestNameAfter []string + + TimeBefore []time.Time + TimeAfter []time.Time +} + +// The SetupSuite method will be run by testify once, at the very +// start of the testing suite, before any tests are run. +func (suite *SuiteTester) SetupSuite() { + suite.SetupSuiteRunCount++ +} + +func (suite *SuiteTester) BeforeTest(suiteName, testName string) { + suite.SuiteNameBefore = append(suite.SuiteNameBefore, suiteName) + suite.TestNameBefore = append(suite.TestNameBefore, testName) + suite.TimeBefore = append(suite.TimeBefore, time.Now()) +} + +func (suite *SuiteTester) AfterTest(suiteName, testName string) { + suite.SuiteNameAfter = append(suite.SuiteNameAfter, suiteName) + suite.TestNameAfter = append(suite.TestNameAfter, testName) + suite.TimeAfter = append(suite.TimeAfter, time.Now()) +} + +// The TearDownSuite method will be run by testify once, at the very +// end of the testing suite, after all tests have been run. +func (suite *SuiteTester) TearDownSuite() { + suite.TearDownSuiteRunCount++ +} + +// The SetupTest method will be run before every test in the suite. +func (suite *SuiteTester) SetupTest() { + suite.SetupTestRunCount++ +} + +// The TearDownTest method will be run after every test in the suite. +func (suite *SuiteTester) TearDownTest() { + suite.TearDownTestRunCount++ +} + +// Every method in a testing suite that begins with "Test" will be run +// as a test. TestOne is an example of a test. For the purposes of +// this example, we've included assertions in the tests, since most +// tests will issue assertions. +func (suite *SuiteTester) TestOne() { + beforeCount := suite.TestOneRunCount + suite.TestOneRunCount++ + assert.Equal(suite.T(), suite.TestOneRunCount, beforeCount+1) + suite.Equal(suite.TestOneRunCount, beforeCount+1) +} + +// TestTwo is another example of a test. +func (suite *SuiteTester) TestTwo() { + beforeCount := suite.TestTwoRunCount + suite.TestTwoRunCount++ + assert.NotEqual(suite.T(), suite.TestTwoRunCount, beforeCount) + suite.NotEqual(suite.TestTwoRunCount, beforeCount) +} + +func (suite *SuiteTester) TestSkip() { + suite.T().Skip() +} + +// NonTestMethod does not begin with "Test", so it will not be run by +// testify as a test in the suite. This is useful for creating helper +// methods for your tests. +func (suite *SuiteTester) NonTestMethod() { + suite.NonTestMethodRunCount++ +} + +func (suite *SuiteTester) TestSubtest() { + suite.TestSubtestRunCount++ + + for _, t := range []struct { + testName string + }{ + {"first"}, + {"second"}, + } { + suiteT := suite.T() + suite.Run(t.testName, func() { + // We should get a different *testing.T for subtests, so that + // go test recognizes them as proper subtests for output formatting + // and running individual subtests + subTestT := suite.T() + suite.NotEqual(subTestT, suiteT) + }) + suite.Equal(suiteT, suite.T()) + } +} + +func (suite *SuiteTester) TearDownSubTest() { + suite.TearDownSubTestNames = append(suite.TearDownSubTestNames, suite.T().Name()) + suite.TearDownSubTestRunCount++ +} + +func (suite *SuiteTester) SetupSubTest() { + suite.SetupSubTestNames = append(suite.SetupSubTestNames, suite.T().Name()) + suite.SetupSubTestRunCount++ +} + +type SuiteSkipTester struct { + // Include our basic suite logic. + Suite + + // Keep counts of how many times each method is run. + SetupSuiteRunCount int + TearDownSuiteRunCount int +} + +func (suite *SuiteSkipTester) SetupSuite() { + suite.SetupSuiteRunCount++ + suite.T().Skip() +} + +func (suite *SuiteSkipTester) TestNothing() { + // SetupSuite is only called when at least one test satisfies + // test filter. For this suite to be set up (and then tore down) + // it is necessary to add at least one test method. +} + +func (suite *SuiteSkipTester) TearDownSuite() { + suite.TearDownSuiteRunCount++ +} + +// TestRunSuite will be run by the 'go test' command, so within it, we +// can run our suite using the Run(*testing.T, TestingSuite) function. +func TestRunSuite(t *testing.T) { + suiteTester := new(SuiteTester) + Run(t, suiteTester) + + // Normally, the test would end here. The following are simply + // some assertions to ensure that the Run function is working as + // intended - they are not part of the example. + + // The suite was only run once, so the SetupSuite and TearDownSuite + // methods should have each been run only once. + assert.Equal(t, 1, suiteTester.SetupSuiteRunCount) + assert.Equal(t, 1, suiteTester.TearDownSuiteRunCount) + + assert.Len(t, suiteTester.SuiteNameAfter, 4) + assert.Len(t, suiteTester.SuiteNameBefore, 4) + assert.Len(t, suiteTester.TestNameAfter, 4) + assert.Len(t, suiteTester.TestNameBefore, 4) + + assert.Contains(t, suiteTester.TestNameAfter, "TestOne") + assert.Contains(t, suiteTester.TestNameAfter, "TestTwo") + assert.Contains(t, suiteTester.TestNameAfter, "TestSkip") + assert.Contains(t, suiteTester.TestNameAfter, "TestSubtest") + + assert.Contains(t, suiteTester.TestNameBefore, "TestOne") + assert.Contains(t, suiteTester.TestNameBefore, "TestTwo") + assert.Contains(t, suiteTester.TestNameBefore, "TestSkip") + assert.Contains(t, suiteTester.TestNameBefore, "TestSubtest") + + assert.Contains(t, suiteTester.SetupSubTestNames, "TestRunSuite/TestSubtest/first") + assert.Contains(t, suiteTester.SetupSubTestNames, "TestRunSuite/TestSubtest/second") + + assert.Contains(t, suiteTester.TearDownSubTestNames, "TestRunSuite/TestSubtest/first") + assert.Contains(t, suiteTester.TearDownSubTestNames, "TestRunSuite/TestSubtest/second") + + for _, suiteName := range suiteTester.SuiteNameAfter { + assert.Equal(t, "SuiteTester", suiteName) + } + + for _, suiteName := range suiteTester.SuiteNameBefore { + assert.Equal(t, "SuiteTester", suiteName) + } + + for _, when := range suiteTester.TimeAfter { + assert.False(t, when.IsZero()) + } + + for _, when := range suiteTester.TimeBefore { + assert.False(t, when.IsZero()) + } + + // There are four test methods (TestOne, TestTwo, TestSkip, and TestSubtest), so + // the SetupTest and TearDownTest methods (which should be run once for + // each test) should have been run four times. + assert.Equal(t, 4, suiteTester.SetupTestRunCount) + assert.Equal(t, 4, suiteTester.TearDownTestRunCount) + + // Each test should have been run once. + assert.Equal(t, 1, suiteTester.TestOneRunCount) + assert.Equal(t, 1, suiteTester.TestTwoRunCount) + assert.Equal(t, 1, suiteTester.TestSubtestRunCount) + + assert.Equal(t, 2, suiteTester.TearDownSubTestRunCount) + assert.Equal(t, 2, suiteTester.SetupSubTestRunCount) + + // Methods that don't match the test method identifier shouldn't + // have been run at all. + assert.Equal(t, 0, suiteTester.NonTestMethodRunCount) + + suiteSkipTester := new(SuiteSkipTester) + Run(t, suiteSkipTester) + + // The suite was only run once, so the SetupSuite and TearDownSuite + // methods should have each been run only once, even though SetupSuite + // called Skip() + assert.Equal(t, 1, suiteSkipTester.SetupSuiteRunCount) + assert.Equal(t, 1, suiteSkipTester.TearDownSuiteRunCount) + +} + +// This suite has no Test... methods. It's setup and teardown must be skipped. +type SuiteSetupSkipTester struct { + Suite + + setUp bool + toreDown bool +} + +func (s *SuiteSetupSkipTester) SetupSuite() { + s.setUp = true +} + +func (s *SuiteSetupSkipTester) NonTestMethod() { + +} + +func (s *SuiteSetupSkipTester) TearDownSuite() { + s.toreDown = true +} + +func TestSkippingSuiteSetup(t *testing.T) { + suiteTester := new(SuiteSetupSkipTester) + Run(t, suiteTester) + assert.False(t, suiteTester.setUp) + assert.False(t, suiteTester.toreDown) +} + +func TestSuiteGetters(t *testing.T) { + suite := new(SuiteTester) + suite.SetT(t) + assert.NotNil(t, suite.Assert()) + assert.Equal(t, suite.Assertions, suite.Assert()) + assert.NotNil(t, suite.Require()) + assert.Equal(t, suite.require, suite.Require()) +} + +type SuiteLoggingTester struct { + Suite +} + +func (s *SuiteLoggingTester) TestLoggingPass() { + s.T().Log("TESTLOGPASS") +} + +func (s *SuiteLoggingTester) TestLoggingFail() { + s.T().Log("TESTLOGFAIL") + assert.NotNil(s.T(), nil) // expected to fail +} + +type StdoutCapture struct { + oldStdout *os.File + readPipe *os.File +} + +func (sc *StdoutCapture) StartCapture() { + sc.oldStdout = os.Stdout + sc.readPipe, os.Stdout, _ = os.Pipe() +} + +func (sc *StdoutCapture) StopCapture() (string, error) { + if sc.oldStdout == nil || sc.readPipe == nil { + return "", errors.New("StartCapture not called before StopCapture") + } + os.Stdout.Close() + os.Stdout = sc.oldStdout + bytes, err := io.ReadAll(sc.readPipe) + if err != nil { + return "", err + } + return string(bytes), nil +} + +func TestSuiteLogging(t *testing.T) { + suiteLoggingTester := new(SuiteLoggingTester) + capture := StdoutCapture{} + internalTest := testing.InternalTest{ + Name: t.Name() + "/SuiteLoggingTester", + F: func(subT *testing.T) { + Run(subT, suiteLoggingTester) + }, + } + capture.StartCapture() + testing.RunTests(allTestsFilter, []testing.InternalTest{internalTest}) + output, err := capture.StopCapture() + require.NoError(t, err, "Got an error trying to capture stdout and stderr!") + require.NotEmpty(t, output, "output content must not be empty") + + // Failed tests' output is always printed + assert.Contains(t, output, "TESTLOGFAIL") + + if testing.Verbose() { + // In verbose mode, output from successful tests is also printed + assert.Contains(t, output, "TESTLOGPASS") + } else { + assert.NotContains(t, output, "TESTLOGPASS") + } +} + +type CallOrderSuite struct { + Suite + callOrder []string +} + +func (s *CallOrderSuite) call(method string) { + time.Sleep(time.Duration(rand.Intn(300)) * time.Millisecond) + s.callOrder = append(s.callOrder, method) +} + +func TestSuiteCallOrder(t *testing.T) { + Run(t, new(CallOrderSuite)) +} +func (s *CallOrderSuite) SetupSuite() { + s.call("SetupSuite") +} + +func (s *CallOrderSuite) TearDownSuite() { + s.call("TearDownSuite") + assert.Equal(s.T(), "SetupSuite;SetupTest;Test A;SetupSubTest;SubTest A1;TearDownSubTest;SetupSubTest;SubTest A2;TearDownSubTest;TearDownTest;SetupTest;Test B;SetupSubTest;SubTest B1;TearDownSubTest;SetupSubTest;SubTest B2;TearDownSubTest;TearDownTest;TearDownSuite", strings.Join(s.callOrder, ";")) +} +func (s *CallOrderSuite) SetupTest() { + s.call("SetupTest") +} + +func (s *CallOrderSuite) TearDownTest() { + s.call("TearDownTest") +} + +func (s *CallOrderSuite) SetupSubTest() { + s.call("SetupSubTest") +} + +func (s *CallOrderSuite) TearDownSubTest() { + s.call("TearDownSubTest") +} + +func (s *CallOrderSuite) Test_A() { + s.call("Test A") + s.Run("SubTest A1", func() { + s.call("SubTest A1") + }) + s.Run("SubTest A2", func() { + s.call("SubTest A2") + }) +} + +func (s *CallOrderSuite) Test_B() { + s.call("Test B") + s.Run("SubTest B1", func() { + s.call("SubTest B1") + }) + s.Run("SubTest B2", func() { + s.call("SubTest B2") + }) +} + +type suiteWithStats struct { + Suite + wasCalled bool + stats *SuiteInformation +} + +func (s *suiteWithStats) HandleStats(suiteName string, stats *SuiteInformation) { + s.wasCalled = true + s.stats = stats +} + +func (s *suiteWithStats) TestSomething() { + s.Equal(1, 1) +} + +func (s *suiteWithStats) TestPanic() { + panic("oops") +} + +func TestSuiteWithStats(t *testing.T) { + suiteWithStats := new(suiteWithStats) + + suiteSuccess := testing.RunTests(allTestsFilter, []testing.InternalTest{ + { + Name: t.Name() + "/suiteWithStats", + F: func(t *testing.T) { + Run(t, suiteWithStats) + }, + }, + }) + require.False(t, suiteSuccess, "suiteWithStats should report test failure because of panic in TestPanic") + + assert.True(t, suiteWithStats.wasCalled) + assert.NotZero(t, suiteWithStats.stats.Start) + assert.NotZero(t, suiteWithStats.stats.End) + assert.False(t, suiteWithStats.stats.Passed()) + + testStats := suiteWithStats.stats.TestStats + + assert.NotZero(t, testStats["TestSomething"].Start) + assert.NotZero(t, testStats["TestSomething"].End) + assert.True(t, testStats["TestSomething"].Passed) + + assert.NotZero(t, testStats["TestPanic"].Start) + assert.NotZero(t, testStats["TestPanic"].End) + assert.False(t, testStats["TestPanic"].Passed) +} + +// FailfastSuite will test the behavior when running with the failfast flag +// It logs calls in the callOrder slice which we then use to assert the correct calls were made +type FailfastSuite struct { + Suite + callOrder []string +} + +func (s *FailfastSuite) call(method string) { + s.callOrder = append(s.callOrder, method) +} + +func TestFailfastSuite(t *testing.T) { + // This test suite is run twice. Once normally and once with the -failfast flag by TestFailfastSuiteFailFastOn + // If you need to debug it run this test directly with the failfast flag set on/off as you need + failFast := flag.Lookup("test.failfast").Value.(flag.Getter).Get().(bool) + s := new(FailfastSuite) + ok := testing.RunTests( + allTestsFilter, + []testing.InternalTest{{ + Name: t.Name() + "/FailfastSuite", + F: func(t *testing.T) { + Run(t, s) + }, + }}, + ) + assert.False(t, ok) + var expect []string + if failFast { + // Test A Fails and because we are running with failfast Test B never runs and we proceed straight to TearDownSuite + expect = []string{"SetupSuite", "SetupTest", "Test A Fails", "TearDownTest", "TearDownSuite"} + } else { + // Test A Fails and because we are running without failfast we continue and run Test B and then proceed to TearDownSuite + expect = []string{"SetupSuite", "SetupTest", "Test A Fails", "TearDownTest", "SetupTest", "Test B Passes", "TearDownTest", "TearDownSuite"} + } + callOrderAssert(t, expect, s.callOrder) +} + +type tHelper interface { + Helper() +} + +// callOrderAssert is a help with confirms that asserts that expect +// matches one or more times in callOrder. This makes it compatible +// with go test flag -count=X where X > 1. +func callOrderAssert(t *testing.T, expect, callOrder []string) { + var ti interface{} = t + if h, ok := ti.(tHelper); ok { + h.Helper() + } + + callCount := len(callOrder) + expectCount := len(expect) + if callCount > expectCount && callCount%expectCount == 0 { + // Command line flag -count=X where X > 1. + for len(callOrder) >= expectCount { + assert.Equal(t, expect, callOrder[:expectCount]) + callOrder = callOrder[expectCount:] + } + return + } + + assert.Equal(t, expect, callOrder) +} + +func TestFailfastSuiteFailFastOn(t *testing.T) { + // To test this with failfast on (and isolated from other intended test failures in our test suite) we launch it in its own process + cmd := exec.Command("go", "test", "-v", "-race", "-run", "TestFailfastSuite", "-failfast") + var out bytes.Buffer + cmd.Stdout = &out + t.Log("Running go test -v -race -run TestFailfastSuite -failfast") + err := cmd.Run() + t.Log(out.String()) + if err != nil { + t.Log(err) + t.Fail() + } +} +func (s *FailfastSuite) SetupSuite() { + s.call("SetupSuite") +} + +func (s *FailfastSuite) TearDownSuite() { + s.call("TearDownSuite") +} +func (s *FailfastSuite) SetupTest() { + s.call("SetupTest") +} + +func (s *FailfastSuite) TearDownTest() { + s.call("TearDownTest") +} + +func (s *FailfastSuite) Test_A_Fails() { + s.call("Test A Fails") + s.T().Error("Test A meant to fail") +} + +func (s *FailfastSuite) Test_B_Passes() { + s.call("Test B Passes") + s.Require().True(true) +} + +type subtestPanicSuite struct { + Suite + inTearDownSuite bool + inTearDownTest bool + inTearDownSubTest bool +} + +func (s *subtestPanicSuite) TearDownSuite() { + s.inTearDownSuite = true +} + +func (s *subtestPanicSuite) TearDownTest() { + s.inTearDownTest = true +} + +func (s *subtestPanicSuite) TearDownSubTest() { + s.inTearDownSubTest = true +} + +func (s *subtestPanicSuite) TestSubtestPanic() { + ok := s.Run("subtest", func() { + panic("panic") + }) + s.False(ok, "subtest failure is expected") +} + +func TestSubtestPanic(t *testing.T) { + suite := new(subtestPanicSuite) + ok := testing.RunTests( + allTestsFilter, + []testing.InternalTest{{ + Name: t.Name() + "/subtestPanicSuite", + F: func(t *testing.T) { + Run(t, suite) + }, + }}, + ) + assert.False(t, ok, "TestSubtestPanic/subtest should make the testsuite fail") + assert.True(t, suite.inTearDownSubTest) + assert.True(t, suite.inTearDownTest) + assert.True(t, suite.inTearDownSuite) +} + +type unInitializedSuite struct { + Suite +} + +// TestUnInitializedSuites asserts the behavior of the suite methods when the +// suite is not initialized +func TestUnInitializedSuites(t *testing.T) { + t.Run("should panic on Require", func(t *testing.T) { + suite := new(unInitializedSuite) + + assert.Panics(t, func() { + suite.Require().True(true) + }) + }) + + t.Run("should panic on Assert", func(t *testing.T) { + suite := new(unInitializedSuite) + + assert.Panics(t, func() { + suite.Assert().True(true) + }) + }) +} From 297f5a4deffc0084807788d2bf5c0ed6783e1a8b Mon Sep 17 00:00:00 2001 From: not-matthias Date: Wed, 7 Jan 2026 16:29:56 +0100 Subject: [PATCH 09/11] feat(go-runner): patch testify imports --- go-runner/src/builder/patcher.rs | 22 +++++++++++++++++++ ...ilder__patcher__tests__import_testify.snap | 11 ++++++++++ 2 files changed, 33 insertions(+) create mode 100644 go-runner/src/builder/snapshots/codspeed_go_runner__builder__patcher__tests__import_testify.snap diff --git a/go-runner/src/builder/patcher.rs b/go-runner/src/builder/patcher.rs index 8eb56f61..911866f4 100644 --- a/go-runner/src/builder/patcher.rs +++ b/go-runner/src/builder/patcher.rs @@ -161,6 +161,18 @@ impl Patcher { "\"github.com/CodSpeedHQ/codspeed-go/pkg/stdr\"", ); + // Replace testify + subpackages + for testify_pkg in &["assert", "require", "mock", "suite", "http"] { + find_replace_range( + &format!("github.com/stretchr/testify/{}", testify_pkg), + &format!("\"github.com/CodSpeedHQ/codspeed-go/pkg/testify/{testify_pkg}\""), + ); + } + find_replace_range( + "github.com/stretchr/testify", + "\"github.com/CodSpeedHQ/codspeed-go/pkg/testify\"", + ); + // Apply replacements in reverse order to avoid shifting positions for (range, replacement) in replacements .into_iter() @@ -481,6 +493,15 @@ import ( "testing/iotest" "testing/synctest" ) +"#; + + const IMPORT_TESTIFY: &str = r#"package main +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) "#; #[rstest] @@ -500,6 +521,7 @@ import ( )] #[case("package_main", PACKAGE_MAIN)] #[case("many_testing_imports", MANY_TESTING_IMPORTS)] + #[case("import_testify", IMPORT_TESTIFY)] fn test_patch_go_source(#[case] test_name: &str, #[case] source: &str) { let mut result = source.to_string(); diff --git a/go-runner/src/builder/snapshots/codspeed_go_runner__builder__patcher__tests__import_testify.snap b/go-runner/src/builder/snapshots/codspeed_go_runner__builder__patcher__tests__import_testify.snap new file mode 100644 index 00000000..7100fbb4 --- /dev/null +++ b/go-runner/src/builder/snapshots/codspeed_go_runner__builder__patcher__tests__import_testify.snap @@ -0,0 +1,11 @@ +--- +source: go-runner/src/builder/patcher.rs +expression: result +--- +package main_compat +import ( + testing "github.com/CodSpeedHQ/codspeed-go/testing/testing" + + "github.com/CodSpeedHQ/codspeed-go/pkg/testify/assert" + "github.com/CodSpeedHQ/codspeed-go/pkg/testify/require" +) From 3b55a5efe65fa6d5b26e48ff69b840cc1c7c723f Mon Sep 17 00:00:00 2001 From: not-matthias Date: Wed, 7 Jan 2026 16:34:54 +0100 Subject: [PATCH 10/11] chore(example): add testify example --- go-runner/src/builder/discovery.rs | 1 + ...cover_benchmarks@example-with-testify.snap | 19 ++++ go-runner/src/integration_tests.rs | 1 + ...esults_snapshots@example-with-testify.snap | 29 ++++++ .../example-type-mismatch-external/go.mod | 3 + .../example-type-mismatch-external/suite.go | 15 +++ .../projects/example-with-testify/README.md | 96 +++++++++++++++++++ .../example-with-testify/external/suite.go | 20 ++++ .../projects/example-with-testify/go.mod | 11 +++ .../projects/example-with-testify/go.sum | 10 ++ .../metric/benchmark_test.go | 26 +++++ 11 files changed, 231 insertions(+) create mode 100644 go-runner/src/builder/snapshots/codspeed_go_runner__builder__discovery__tests__discover_benchmarks@example-with-testify.snap create mode 100644 go-runner/src/snapshots/codspeed_go_runner__integration_tests__assert_results_snapshots@example-with-testify.snap create mode 100644 go-runner/testdata/projects/example-type-mismatch-external/go.mod create mode 100644 go-runner/testdata/projects/example-type-mismatch-external/suite.go create mode 100644 go-runner/testdata/projects/example-with-testify/README.md create mode 100644 go-runner/testdata/projects/example-with-testify/external/suite.go create mode 100644 go-runner/testdata/projects/example-with-testify/go.mod create mode 100644 go-runner/testdata/projects/example-with-testify/go.sum create mode 100644 go-runner/testdata/projects/example-with-testify/metric/benchmark_test.go diff --git a/go-runner/src/builder/discovery.rs b/go-runner/src/builder/discovery.rs index 9ca4a15b..0100b415 100644 --- a/go-runner/src/builder/discovery.rs +++ b/go-runner/src/builder/discovery.rs @@ -453,6 +453,7 @@ mod tests { #[case::example_with_test_package("example-with-test-package")] #[case::example_with_excluded_names("example-with-excluded-names")] #[case::example_external_test_unexported_access("example-external-test-unexported-access")] + #[case::example_with_testify("example-with-testify")] #[test_log::test] fn test_discover_benchmarks(#[case] project_name: &str) { let project_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")) diff --git a/go-runner/src/builder/snapshots/codspeed_go_runner__builder__discovery__tests__discover_benchmarks@example-with-testify.snap b/go-runner/src/builder/snapshots/codspeed_go_runner__builder__discovery__tests__discover_benchmarks@example-with-testify.snap new file mode 100644 index 00000000..f33c2d71 --- /dev/null +++ b/go-runner/src/builder/snapshots/codspeed_go_runner__builder__discovery__tests__discover_benchmarks@example-with-testify.snap @@ -0,0 +1,19 @@ +--- +source: go-runner/src/builder/discovery.rs +expression: packages +--- +[ + { + "raw_package": "[raw_package]", + "benchmarks": [ + { + "name": "BenchmarkWithTestifySuite", + "module_path": "example-with-testify/metric", + "import_alias": "benchmarkwithtestifysuite_14340558256714831592", + "qualified_name": "benchmarkwithtestifysuite_14340558256714831592.BenchmarkWithTestifySuite", + "file_path": "metric/benchmark_test.go", + "is_external": false + } + ] + } +] diff --git a/go-runner/src/integration_tests.rs b/go-runner/src/integration_tests.rs index f0b357cf..18c10840 100644 --- a/go-runner/src/integration_tests.rs +++ b/go-runner/src/integration_tests.rs @@ -85,6 +85,7 @@ fn assert_results_snapshots(profile_dir: &Path, project_name: &str) { #[case::example_with_excluded_names("example-with-excluded-names")] #[case::example_logr_type_mismatch("example-logr-type-mismatch")] #[case::example_external_test_unexported_access("example-external-test-unexported-access")] +#[case::example_with_testify("example-with-testify")] #[test_log::test] fn test_build_and_run(#[case] project_name: &str) { let project_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")) diff --git a/go-runner/src/snapshots/codspeed_go_runner__integration_tests__assert_results_snapshots@example-with-testify.snap b/go-runner/src/snapshots/codspeed_go_runner__integration_tests__assert_results_snapshots@example-with-testify.snap new file mode 100644 index 00000000..1116687b --- /dev/null +++ b/go-runner/src/snapshots/codspeed_go_runner__integration_tests__assert_results_snapshots@example-with-testify.snap @@ -0,0 +1,29 @@ +--- +source: go-runner/src/integration_tests.rs +expression: results +--- +[ + { + "creator": { + "name": "codspeed-go", + "version": "[version]", + "pid": "[pid]" + }, + "instrument": { + "type": "walltime" + }, + "benchmarks": [ + { + "name": "BenchmarkWithTestifySuite", + "uri": "go-runner/testdata/projects/example-with-testify/metric/benchmark_test.go::BenchmarkWithTestifySuite", + "config": { + "warmup_time_ns": null, + "min_round_time_ns": null, + "max_time_ns": null, + "max_rounds": null + }, + "stats": "[stats]" + } + ] + } +] diff --git a/go-runner/testdata/projects/example-type-mismatch-external/go.mod b/go-runner/testdata/projects/example-type-mismatch-external/go.mod new file mode 100644 index 00000000..c7a1ef32 --- /dev/null +++ b/go-runner/testdata/projects/example-type-mismatch-external/go.mod @@ -0,0 +1,3 @@ +module example.com/external + +go 1.24.3 diff --git a/go-runner/testdata/projects/example-type-mismatch-external/suite.go b/go-runner/testdata/projects/example-type-mismatch-external/suite.go new file mode 100644 index 00000000..e3e3d46b --- /dev/null +++ b/go-runner/testdata/projects/example-type-mismatch-external/suite.go @@ -0,0 +1,15 @@ +package external + +import "testing" + +// Suite represents a test suite (similar to testify/suite) +type Suite struct { + T *testing.T +} + +// Run runs the suite with the given testing.T from standard library +// This simulates external libraries like testify that expect *testing.T +func (s *Suite) Run(t *testing.T) { + s.T = t + s.T.Log("Running test suite") +} diff --git a/go-runner/testdata/projects/example-with-testify/README.md b/go-runner/testdata/projects/example-with-testify/README.md new file mode 100644 index 00000000..7489ab3f --- /dev/null +++ b/go-runner/testdata/projects/example-with-testify/README.md @@ -0,0 +1,96 @@ +# Example: Testing.T Type Mismatch with testify/suite + +This example demonstrates the type mismatch issue that occurs when CodSpeed's import patching conflicts with external libraries like testify that expect the standard library's `*testing.T` type. + +## The Issue + +This reproduces the exact error seen in the opentelemetry-go project: +``` +./benchmark_codspeed.go:19:12: cannot use t (variable of type *"github.com/CodSpeedHQ/codspeed-go/testing/testing".T) + as *"testing".T value in argument to suite.Run +./benchmark_codspeed.go:25:12: cannot use &testing.T{} (value of type *"github.com/CodSpeedHQ/codspeed-go/testing/testing".T) + as *"testing".T value in argument to suite.Run +``` + +Seen in production at: https://github.com/CodSpeedHQ/codspeed-go (opentelemetry-go customer project) + +### Root Cause + +1. CodSpeed patches test files: `import "testing"` → `import testing "github.com/CodSpeedHQ/codspeed-go/compat/testing"` +2. This makes all `*testing.T` references in the test file use the CodSpeed version +3. However, external packages like `github.com/stretchr/testify/suite` are compiled against the standard library `testing` package +4. When test code calls `suite.Run(t)`, it passes CodSpeed's `*testing.T`, but `suite.Run` expects standard library's `*testing.T` +5. Go's type system treats these as incompatible types → compile error + +### Why testify Specifically? + +CodSpeed already patches some testing-related imports (see `patcher.rs`): +- `github.com/go-quicktest/qt` → patched +- `github.com/go-logr/logr/slogassert` → patched +- `github.com/go-logr/logr/testr` → patched +- `github.com/go-logr/stdr` → patched + +But **NOT** `github.com/stretchr/testify`, which is extremely popular and used in opentelemetry-go. + +### Real-World Impact + +This pattern appears in many projects using: +- **testify/suite**: `suite.Run(t *testing.T)` expects standard library type +- **testify/require**: Various assertion methods that take `testing.TB` +- **testify/assert**: Various assertion methods that take `testing.TB` +- Any external library that accepts `*testing.T` or `*testing.B` as parameters + +## Project Structure + +``` +example-with-testify/ +├── go.mod # Requires github.com/stretchr/testify v1.10.0 +└── metric/ + └── benchmark_test.go # Uses testify/suite.Run() +``` + +## How to Test + +### Standard Go (works correctly): +```bash +cd /home/not-matthias/Documents/work/wgit/codspeed-go/go-runner/testdata/projects/example-with-testify +go test -v ./metric +go test -bench=. ./metric +``` + +### CodSpeed Go Runner (fails with type mismatch): +```bash +cd /home/not-matthias/Documents/work/wgit/codspeed-go/go-runner +cargo run -- test -bench=. ./metric +``` + +**Expected behavior**: Benchmark should compile and run + +**Actual behavior**: +``` +error: cannot use t (variable of type *"github.com/CodSpeedHQ/codspeed-go/testing/testing".T) + as *"testing".T value in argument to suite.Run +``` + +## Potential Solutions + +1. **Patch testify imports** (similar to logr/testr): + - Add testify-compatible wrappers in codspeed-go + - Patch `github.com/stretchr/testify/suite` imports in test files + - Requires maintaining compatibility with testify API + +2. **Type wrapper/adapter**: + - Create conversion functions between types + - Inject at call sites (complex AST manipulation) + +3. **Skip patching for testify tests**: + - Detect testify usage and skip those test files + - Tests run without CodSpeed instrumentation (loses metrics) + +4. **Upstream collaboration**: + - Work with testify to accept interface instead of concrete type + - Long-term solution but requires ecosystem changes + +## Notes + +This is a fundamental limitation of Go's type system - two types with the same structure but different package paths are incompatible. The patched `*testing.T` from `github.com/CodSpeedHQ/codspeed-go/testing/testing` and standard library `*testing.T` are distinct, incompatible types. diff --git a/go-runner/testdata/projects/example-with-testify/external/suite.go b/go-runner/testdata/projects/example-with-testify/external/suite.go new file mode 100644 index 00000000..aa48773d --- /dev/null +++ b/go-runner/testdata/projects/example-with-testify/external/suite.go @@ -0,0 +1,20 @@ +package external + +// Explicitly using standard library testing package +// This simulates an external library that is already compiled +// and was NOT patched by CodSpeed (like github.com/stretchr/testify) +import stdtesting "testing" + +// Suite represents a test suite (similar to testify/suite) +type Suite struct { + T *stdtesting.T +} + +// Run runs the suite with the given testing.T from standard library +// This simulates external libraries like testify that expect *testing.T +// The key is that this function signature uses the STANDARD LIBRARY testing.T +// not the CodSpeed version, because external packages are already compiled +func (s *Suite) Run(t *stdtesting.T) { + s.T = t + s.T.Log("Running test suite") +} diff --git a/go-runner/testdata/projects/example-with-testify/go.mod b/go-runner/testdata/projects/example-with-testify/go.mod new file mode 100644 index 00000000..8dd6a0d7 --- /dev/null +++ b/go-runner/testdata/projects/example-with-testify/go.mod @@ -0,0 +1,11 @@ +module example-with-testify + +go 1.24.3 + +require github.com/stretchr/testify v1.10.0 + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go-runner/testdata/projects/example-with-testify/go.sum b/go-runner/testdata/projects/example-with-testify/go.sum new file mode 100644 index 00000000..713a0b4f --- /dev/null +++ b/go-runner/testdata/projects/example-with-testify/go.sum @@ -0,0 +1,10 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/go-runner/testdata/projects/example-with-testify/metric/benchmark_test.go b/go-runner/testdata/projects/example-with-testify/metric/benchmark_test.go new file mode 100644 index 00000000..40d990c1 --- /dev/null +++ b/go-runner/testdata/projects/example-with-testify/metric/benchmark_test.go @@ -0,0 +1,26 @@ +package metric + +import ( + "testing" + + "github.com/stretchr/testify/suite" +) + +// MetricTestSuite demonstrates the type mismatch issue with testify +type MetricTestSuite struct { + suite.Suite +} + +// TestWithSuite demonstrates using testify/suite in a test +func TestWithSuite(t *testing.T) { + // This will fail when imports are patched because: + // - t is *"github.com/CodSpeedHQ/codspeed-go/testing/testing".T (after patching) + // - suite.Run expects *"testing".T from standard library (unpatched) + suite.Run(t, new(MetricTestSuite)) +} + +func BenchmarkWithTestifySuite(b *testing.B) { + // This reproduces the exact error from opentelemetry-go + // where testify/suite.Run expects *testing.T but receives CodSpeed's version + suite.Run(&testing.T{}, new(MetricTestSuite)) +} From 5a11ae497d2fabfbffb9fbbf3582b55acb1fc812 Mon Sep 17 00:00:00 2001 From: not-matthias Date: Wed, 7 Jan 2026 17:32:03 +0100 Subject: [PATCH 11/11] fix(ci): shard integration tests --- .github/workflows/ci.yml | 48 ++++++++++++++++++++++++++++++++++------ 1 file changed, 41 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d33f852c..076145c1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,13 +18,12 @@ jobs: with: extra_args: --all-files - tests: + tests-unit: runs-on: ubuntu-latest strategy: matrix: go-version: ['1.24.x', '1.25.x'] env: - # Needed for the quic-go test GOEXPERIMENT: ${{ matrix.go-version == '1.24.x' && 'synctest' || '' }} steps: - uses: actions/checkout@v4 @@ -35,14 +34,48 @@ jobs: with: go-version: ${{ matrix.go-version }} - uses: taiki-e/install-action@cargo-nextest - - uses: moonrepo/setup-rust@v1 - - run: | - cd go-runner - cargo nextest run --all + - name: Run unit and discovery tests + run: cargo nextest run --lib -E 'not test(~integration_tests)' + working-directory: go-runner env: CODSPEED_GO_PKG_VERSION: ${{ github.head_ref || github.ref_name }} + tests-integration: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + go-version: ['1.24.x', '1.25.x'] + shard: + - name: hugo + filter: "test(~hugo)" + - name: caddy + filter: "test(~caddy)" + - name: large + filter: "test(~fzf) | test(~opentelemetry) | test(~golang_benchmarks)" + - name: medium + filter: "test(~zerolog) | test(~zap) | test(~fuego) | test(~cli_runtime) | test(~quic)" + - name: examples + filter: "test(~example)" + env: + GOEXPERIMENT: ${{ matrix.go-version == '1.24.x' && 'synctest' || '' }} + name: integration (${{ matrix.go-version }}, ${{ matrix.shard.name }}) + steps: + - uses: actions/checkout@v4 + with: + lfs: true + submodules: true + - uses: actions/setup-go@v6 + with: + go-version: ${{ matrix.go-version }} + - uses: taiki-e/install-action@cargo-nextest + - uses: moonrepo/setup-rust@v1 + - name: Run integration tests (${{ matrix.shard.name }}) + run: cargo nextest run -E '${{ matrix.shard.filter }}' + working-directory: go-runner + env: + CODSPEED_GO_PKG_VERSION: ${{ github.head_ref || github.ref_name }} verify-fork-scripts: runs-on: ubuntu-latest @@ -118,7 +151,8 @@ jobs: if: always() needs: - lint - - tests + - tests-unit + - tests-integration - verify-fork-scripts - compat-integration-test-walltime - go-runner-benchmarks