From 92f5c11029a156ce8357d4a1c2ee309e2d2f1be7 Mon Sep 17 00:00:00 2001 From: Frederic BIDON Date: Sat, 27 Dec 2025 19:51:24 +0100 Subject: [PATCH] doc: added generated documentation and doc-site * refactored scanner to cope with doc-specific details * removed deprecated methods and types Signed-off-by: Frederic BIDON --- .claude/CLAUDE.md | 303 ++++++- .github/CONTRIBUTING.md | 2 +- .golangci.yml | 7 +- assert/assert_assertions.go | 572 +++++++----- assert/assert_assertions_test.go | 5 +- assert/assert_examples_test.go | 34 +- assert/assert_format.go | 3 +- assert/assert_format_test.go | 5 +- assert/assert_forward.go | 3 +- assert/assert_forward_test.go | 7 +- assert/assert_helpers.go | 14 +- assert/assert_helpers_test.go | 7 +- assert/assert_types.go | 8 +- codegen/go.mod | 1 + codegen/go.sum | 2 + codegen/internal/generator/doc_generator.go | 235 +++++ codegen/internal/generator/domains/doc.go | 9 + codegen/internal/generator/domains/domains.go | 463 ++++++++++ .../generator/domains/domains_test.go | 596 +++++++++++++ .../generator/domains/sorting_test.go | 232 +++++ codegen/internal/generator/funcmap.go | 201 ----- codegen/internal/generator/funcmaps/doc.go | 11 + .../internal/generator/funcmaps/funcmaps.go | 346 ++++++++ .../generator/funcmaps/funcmaps_test.go | 815 ++++++++++++++++++ .../internal/generator/funcmaps/markdown.go | 144 ++++ .../generator/funcmaps/markdown_test.go | 159 ++++ codegen/internal/generator/generator.go | 146 ++-- codegen/internal/generator/generator_test.go | 3 +- codegen/internal/generator/options.go | 23 +- codegen/internal/generator/render.go | 36 + codegen/internal/generator/templates.go | 98 +++ .../templates/assertion_assertions.gotmpl | 1 + .../assertion_assertions_test.gotmpl | 7 +- .../templates/assertion_examples_test.gotmpl | 7 +- .../templates/assertion_format.gotmpl | 1 + .../templates/assertion_format_test.gotmpl | 7 +- .../templates/assertion_forward.gotmpl | 1 + .../templates/assertion_forward_test.gotmpl | 13 +- .../templates/assertion_helpers.gotmpl | 1 + .../templates/assertion_helpers_test.gotmpl | 1 + .../templates/assertion_types.gotmpl | 1 + .../generator/templates/doc_index.md.gotmpl | 52 ++ .../generator/templates/doc_page.md.gotmpl | 175 ++++ .../templates/requirement_assertions.gotmpl | 1 + .../templates/requirement_format.gotmpl | 1 + .../templates/requirement_forward.gotmpl | 1 + codegen/internal/model/documentation.go | 131 +++ codegen/internal/model/model.go | 107 ++- .../internal/scanner/comments-parser/doc.go | 14 + .../scanner/comments-parser/examples.go | 118 +++ .../scanner/comments-parser/examples_test.go | 350 ++++++++ .../scanner/comments-parser/expressions.go | 144 ++++ .../comments-parser/expressions_test.go | 254 ++++++ .../scanner/comments-parser/matchers.go | 53 ++ .../scanner/comments-parser/matchers_test.go | 171 ++++ .../scanner/comments-parser/relocation.go | 235 +++++ .../comments-parser/relocation_test.go | 219 +++++ .../internal/scanner/comments-parser/tags.go | 147 ++++ .../scanner/comments-parser/tags_test.go | 407 +++++++++ codegen/internal/scanner/comments/bridge.go | 72 ++ codegen/internal/scanner/comments/doc.go | 16 + .../internal/scanner/comments/extractor.go | 299 +++++++ .../scanner/comments/extractor_test.go | 519 +++++++++++ .../comments/testdata/assertions/boolean.go | 50 ++ .../comments/testdata/assertions/condition.go | 234 +++++ .../comments/testdata/assertions/doc.go | 32 + .../comments/testdata/assertions/helpers.go | 27 + .../comments/testdata/assertions/vars.go | 15 + codegen/internal/scanner/imports.go | 55 ++ codegen/internal/scanner/module.go | 88 ++ codegen/internal/scanner/module_test.go | 25 + codegen/internal/scanner/options.go | 16 +- codegen/internal/scanner/scanner.go | 554 ++---------- codegen/internal/scanner/scanner_test.go | 2 +- codegen/internal/scanner/signature/doc.go | 14 + .../internal/scanner/signature/extractor.go | 138 +++ .../scanner/signature/extractor_test.go | 512 +++++++++++ codegen/main.go | 37 + docs/EXAMPLES_STATUS.md | 148 ---- docs/MAINTAINERS.md | 18 + docs/doc-site/_index.md | 128 +++ docs/doc-site/api/_index.md | 70 ++ docs/doc-site/api/boolean.md | 137 +++ docs/doc-site/api/collection.md | 423 +++++++++ docs/doc-site/api/common.md | 161 ++++ docs/doc-site/api/comparison.md | 345 ++++++++ docs/doc-site/api/condition.md | 252 ++++++ docs/doc-site/api/equality.md | 660 ++++++++++++++ docs/doc-site/api/error.md | 446 ++++++++++ docs/doc-site/api/file.md | 339 ++++++++ docs/doc-site/api/http.md | 377 ++++++++ docs/doc-site/api/json.md | 137 +++ docs/doc-site/api/number.md | 284 ++++++ docs/doc-site/api/ordering.md | 243 ++++++ docs/doc-site/api/panic.md | 238 +++++ docs/doc-site/api/string.md | 139 +++ docs/doc-site/api/testing.md | 135 +++ docs/doc-site/api/time.md | 137 +++ docs/doc-site/api/type.md | 333 +++++++ docs/doc-site/api/yaml.md | 98 +++ docs/doc-site/examples/EXAMPLES.md | 481 +++++++++++ docs/doc-site/examples/TUTORIAL.md | 645 ++++++++++++++ docs/doc-site/examples/_index.md | 7 + docs/doc-site/keywords/_index.md | 9 + docs/doc-site/project/LICENSE.md | 208 +++++ docs/doc-site/project/NOTICE.md | 104 +++ docs/doc-site/project/README.md | 161 ++++ docs/doc-site/project/SECURITY.md | 43 + docs/doc-site/project/_index.md | 12 + .../project/contributing/CODE_OF_CONDUCT.md | 78 ++ .../project/contributing/CONTRIBUTING.md | 287 ++++++ .../project/contributing/CONTRIBUTORS.md | 18 + docs/doc-site/project/contributing/DCO.md | 72 ++ docs/doc-site/project/contributing/STYLE.md | 96 +++ docs/doc-site/project/contributing/_index.md | 11 + .../project/maintainers/ARCHITECTURE.md | 49 ++ docs/doc-site/project/maintainers/CODEGEN.md | 285 ++++++ .../project/maintainers/MAINTAINERS.md | 185 ++++ docs/doc-site/project/maintainers/ORIGINAL.md | 374 ++++++++ docs/doc-site/project/maintainers/ROADMAP.md | 117 +++ docs/doc-site/project/maintainers/_index.md | 7 + hack/doc-site/hugo/README.md | 99 +++ hack/doc-site/hugo/gendoc.sh | 60 ++ hack/doc-site/hugo/hugo.yaml | 137 +++ hack/doc-site/hugo/layouts/partials/.gitkeep | 2 + .../hugo/layouts/partials/bodys/single.html | 7 + .../hugo/layouts/partials/content-footer.html | 37 + .../hugo/layouts/partials/custom-header.html | 9 + .../doc-site/hugo/layouts/shortcodes/.gitkeep | 2 + hack/doc-site/hugo/testify.yaml | 16 + hack/doc-site/hugo/testify.yaml.template | 16 + hack/gogenerate.sh | 16 - hack/readme.fmt.sh | 12 - internal/assertions/boolean.go | 10 +- internal/assertions/collection.go | 133 +-- internal/assertions/compare.go | 327 +++---- internal/assertions/condition.go | 48 +- internal/assertions/doc.go | 20 + .../assertions/enable/yaml/enable_yaml.go | 1 - internal/assertions/equal.go | 241 +++--- internal/assertions/equal_test.go | 23 +- internal/assertions/error.go | 73 +- internal/assertions/error_test.go | 5 - internal/assertions/file.go | 43 +- internal/assertions/http.go | 82 +- internal/assertions/http_test.go | 2 +- internal/assertions/json.go | 14 +- internal/assertions/json_test.go | 2 +- internal/assertions/number.go | 79 +- internal/assertions/object.go | 13 - internal/assertions/object_test.go | 77 -- internal/assertions/order.go | 122 +-- internal/assertions/panic.go | 78 +- internal/assertions/string.go | 18 +- internal/assertions/testing.go | 23 +- internal/assertions/time.go | 14 +- internal/assertions/type.go | 50 +- internal/assertions/yaml.go | 9 +- package_test.go | 7 +- require/require_assertions.go | 572 +++++++----- require/require_assertions_test.go | 5 +- require/require_examples_test.go | 35 +- require/require_format.go | 3 +- require/require_format_test.go | 5 +- require/require_forward.go | 3 +- require/require_forward_test.go | 7 +- require/require_helpers.go | 14 +- require/require_helpers_test.go | 7 +- require/require_types.go | 8 +- 169 files changed, 18786 insertions(+), 2112 deletions(-) create mode 100644 codegen/internal/generator/doc_generator.go create mode 100644 codegen/internal/generator/domains/doc.go create mode 100644 codegen/internal/generator/domains/domains.go create mode 100644 codegen/internal/generator/domains/domains_test.go create mode 100644 codegen/internal/generator/domains/sorting_test.go delete mode 100644 codegen/internal/generator/funcmap.go create mode 100644 codegen/internal/generator/funcmaps/doc.go create mode 100644 codegen/internal/generator/funcmaps/funcmaps.go create mode 100644 codegen/internal/generator/funcmaps/funcmaps_test.go create mode 100644 codegen/internal/generator/funcmaps/markdown.go create mode 100644 codegen/internal/generator/funcmaps/markdown_test.go create mode 100644 codegen/internal/generator/render.go create mode 100644 codegen/internal/generator/templates.go create mode 100644 codegen/internal/generator/templates/doc_index.md.gotmpl create mode 100644 codegen/internal/generator/templates/doc_page.md.gotmpl create mode 100644 codegen/internal/model/documentation.go create mode 100644 codegen/internal/scanner/comments-parser/doc.go create mode 100644 codegen/internal/scanner/comments-parser/examples.go create mode 100644 codegen/internal/scanner/comments-parser/examples_test.go create mode 100644 codegen/internal/scanner/comments-parser/expressions.go create mode 100644 codegen/internal/scanner/comments-parser/expressions_test.go create mode 100644 codegen/internal/scanner/comments-parser/matchers.go create mode 100644 codegen/internal/scanner/comments-parser/matchers_test.go create mode 100644 codegen/internal/scanner/comments-parser/relocation.go create mode 100644 codegen/internal/scanner/comments-parser/relocation_test.go create mode 100644 codegen/internal/scanner/comments-parser/tags.go create mode 100644 codegen/internal/scanner/comments-parser/tags_test.go create mode 100644 codegen/internal/scanner/comments/bridge.go create mode 100644 codegen/internal/scanner/comments/doc.go create mode 100644 codegen/internal/scanner/comments/extractor.go create mode 100644 codegen/internal/scanner/comments/extractor_test.go create mode 100644 codegen/internal/scanner/comments/testdata/assertions/boolean.go create mode 100644 codegen/internal/scanner/comments/testdata/assertions/condition.go create mode 100644 codegen/internal/scanner/comments/testdata/assertions/doc.go create mode 100644 codegen/internal/scanner/comments/testdata/assertions/helpers.go create mode 100644 codegen/internal/scanner/comments/testdata/assertions/vars.go create mode 100644 codegen/internal/scanner/imports.go create mode 100644 codegen/internal/scanner/module.go create mode 100644 codegen/internal/scanner/module_test.go create mode 100644 codegen/internal/scanner/signature/doc.go create mode 100644 codegen/internal/scanner/signature/extractor.go create mode 100644 codegen/internal/scanner/signature/extractor_test.go delete mode 100644 docs/EXAMPLES_STATUS.md create mode 100644 docs/doc-site/_index.md create mode 100644 docs/doc-site/api/_index.md create mode 100644 docs/doc-site/api/boolean.md create mode 100644 docs/doc-site/api/collection.md create mode 100644 docs/doc-site/api/common.md create mode 100644 docs/doc-site/api/comparison.md create mode 100644 docs/doc-site/api/condition.md create mode 100644 docs/doc-site/api/equality.md create mode 100644 docs/doc-site/api/error.md create mode 100644 docs/doc-site/api/file.md create mode 100644 docs/doc-site/api/http.md create mode 100644 docs/doc-site/api/json.md create mode 100644 docs/doc-site/api/number.md create mode 100644 docs/doc-site/api/ordering.md create mode 100644 docs/doc-site/api/panic.md create mode 100644 docs/doc-site/api/string.md create mode 100644 docs/doc-site/api/testing.md create mode 100644 docs/doc-site/api/time.md create mode 100644 docs/doc-site/api/type.md create mode 100644 docs/doc-site/api/yaml.md create mode 100644 docs/doc-site/examples/EXAMPLES.md create mode 100644 docs/doc-site/examples/TUTORIAL.md create mode 100644 docs/doc-site/examples/_index.md create mode 100644 docs/doc-site/keywords/_index.md create mode 100644 docs/doc-site/project/LICENSE.md create mode 100644 docs/doc-site/project/NOTICE.md create mode 100644 docs/doc-site/project/README.md create mode 100644 docs/doc-site/project/SECURITY.md create mode 100644 docs/doc-site/project/_index.md create mode 100644 docs/doc-site/project/contributing/CODE_OF_CONDUCT.md create mode 100644 docs/doc-site/project/contributing/CONTRIBUTING.md create mode 100644 docs/doc-site/project/contributing/CONTRIBUTORS.md create mode 100644 docs/doc-site/project/contributing/DCO.md create mode 100644 docs/doc-site/project/contributing/STYLE.md create mode 100644 docs/doc-site/project/contributing/_index.md create mode 100644 docs/doc-site/project/maintainers/ARCHITECTURE.md create mode 100644 docs/doc-site/project/maintainers/CODEGEN.md create mode 100644 docs/doc-site/project/maintainers/MAINTAINERS.md create mode 100644 docs/doc-site/project/maintainers/ORIGINAL.md create mode 100644 docs/doc-site/project/maintainers/ROADMAP.md create mode 100644 docs/doc-site/project/maintainers/_index.md create mode 100644 hack/doc-site/hugo/README.md create mode 100755 hack/doc-site/hugo/gendoc.sh create mode 100644 hack/doc-site/hugo/hugo.yaml create mode 100644 hack/doc-site/hugo/layouts/partials/.gitkeep create mode 100644 hack/doc-site/hugo/layouts/partials/bodys/single.html create mode 100644 hack/doc-site/hugo/layouts/partials/content-footer.html create mode 100644 hack/doc-site/hugo/layouts/partials/custom-header.html create mode 100644 hack/doc-site/hugo/layouts/shortcodes/.gitkeep create mode 100644 hack/doc-site/hugo/testify.yaml create mode 100644 hack/doc-site/hugo/testify.yaml.template delete mode 100755 hack/gogenerate.sh delete mode 100755 hack/readme.fmt.sh diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index a34b3e8a5..9ae028f72 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -6,6 +6,11 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co This is the go-openapi fork of the testify testing package. The main goal is to remove external dependencies while maintaining a clean, focused API for testing in Go. This fork strips out unnecessary features (mocks, suite) and internalizes dependencies (go-spew, difflib) to ensure `github.com/go-openapi/testify/v2` is the only import needed. +The project uses sophisticated code generation to: +1. Generate assert and require packages from a single source (`internal/assertions/`) +2. Generate comprehensive tests for all assertion variants +3. Generate domain-organized documentation for a Hugo-based documentation site + ## Key Architecture ### Single Source of Truth: `internal/assertions/` @@ -46,14 +51,22 @@ The codebase uses sophisticated code generation via the `codegen/` directory: ``` codegen/ ├── internal/ -│ ├── scanner/ # Parses internal/assertions using go/packages and go/types -│ ├── generator/ # Template-based code generation engine -│ ├── model/ # Data model for assertions -├── main.go # CLI orchestration +│ ├── scanner/ # Parses internal/assertions using go/packages and go/types +│ │ ├── comments/ # Doc comment extraction +│ │ ├── comments-parser/ # Domain, examples, and tag parsing +│ │ └── signature/ # Function signature analysis +│ ├── generator/ # Template-based code generation engine +│ │ ├── doc_generator.go # Documentation generator (domain-organized) +│ │ ├── render.go # Markdown rendering utilities +│ │ └── templates/ # Go templates for code and docs +│ ├── model/ # Data model for assertions +│ │ └── documentation.go # Documentation structures +├── main.go # CLI orchestration +├── docs/ # Generated documentation output └── (generated outputs in assert/ and require/) ``` -**Generated files include:** +**Generated code files include:** - **assert/assertion_assertions.go** - Package-level assertion functions - **assert/assertion_format.go** - Format string variants (Equalf, Truef, etc.) - **assert/assertion_forward.go** - Forwarded assertion methods for chaining @@ -63,6 +76,12 @@ codegen/ - **require/requirement_forward.go** - Fatal forwarded methods - **require/requirement_*_test.go** - Generated tests for all require variants +**Generated documentation files include:** +- **docs/doc-site/api/_index.md** - API index page listing all domains +- **docs/doc-site/api/{domain}.md** - Domain-specific pages (boolean.md, collection.md, etc.) +- Documentation is organized by domain (boolean, string, error, etc.) rather than by package +- Each domain page shows both assert and require variants together + **Each assertion function generates 8 variants:** 1. `assert.Equal(t, ...)` - package-level function 2. `assert.Equalf(t, ..., "msg")` - format variant @@ -94,27 +113,48 @@ go test ./... go test ./internal/assertions # Source of truth with exhaustive tests go test ./assert # Generated package tests go test ./require # Generated package tests +go test ./codegen/internal/... # Scanner and generator tests # Run a single test go test ./internal/assertions -run TestEqual # Run with coverage -go test -cover ./internal/assertions # Should be 90%+ -go test -cover ./assert # Should be ~100% -go test -cover ./require # Should be ~100% +go test -cover ./internal/assertions # Should be 90%+ +go test -cover ./assert # Should be ~100% +go test -cover ./require # Should be ~100% +go test -cover ./codegen/internal/scanner/... # Scanner tests # Run tests with verbose output go test -v ./... ``` +### Working with Documentation + +```bash +# Generate documentation (included in go generate) +go generate ./... + +# Or generate documentation explicitly +cd codegen && go run . -output-packages assert,require -include-doc + +# Preview documentation site locally +cd hack/doc-site/hugo +./gendoc.sh +# Visit http://localhost:1313/testify/ + +# The Hugo site auto-reloads on changes to docs/doc-site/ +# To see changes, re-run: go generate ./... +``` + ### Adding a New Assertion **The entire workflow:** 1. Add function to appropriate file in `internal/assertions/` 2. Add "Examples:" section to doc comment -3. Add tests to corresponding `*_test.go` file -4. Run `go generate ./...` -5. Done - all 8 variants generated with tests +3. Add "domain:" tag to doc comment for documentation organization +4. Add tests to corresponding `*_test.go` file +5. Run `go generate ./...` +6. Done - all 8 variants generated with tests + documentation **Example - Adding a new assertion:** ```go @@ -122,6 +162,8 @@ go test -v ./... // StartsWith asserts that the string starts with the given prefix. // +// domain: string +// // Examples: // // success: "hello world", "hello" @@ -137,6 +179,15 @@ func StartsWith(t T, str, prefix string, msgAndArgs ...any) bool { } ``` +**Note on placeholder values in Examples:** +- For complex values that can't be easily represented (pointers, structs, etc.), use `// NOT IMPLEMENTED` marker: + ```go + // Examples: + // success: &customStruct{Field: "value"}, // NOT IMPLEMENTED + // failure: complexType{}, // NOT IMPLEMENTED + ``` +- **Never use `// TODO`** - it triggers false positives in code quality analyzers and project management tools + Then add tests in `internal/assertions/string_test.go` and run `go generate ./...`. This generates: @@ -149,22 +200,26 @@ This generates: - `r.StartsWith(str, prefix)` (forward method) - `r.StartsWithf(str, prefix, "msg")` - Tests for all 8 variants +- Documentation entry in `docs/doc-site/api/string.md` ### Code Generation ```bash -# Generate all code from internal/assertions +# Generate all code and documentation from internal/assertions go generate ./... -# Or run the generator directly -cd codegen && go run . -target assert -cd codegen && go run . -target require +# Or run the generator directly with all outputs +cd codegen && go run . -output-packages assert,require -include-doc -# The generator: -# 1. Scans internal/assertions/ for exported functions -# 2. Extracts "Examples:" from doc comments +# Run code generation only (skip documentation) +cd codegen && go run . -output-packages assert,require -include-doc=false + +# The generator workflow: +# 1. Scans internal/assertions/ for exported functions and types +# 2. Extracts doc comments, "Examples:", and domain tags # 3. Generates assert/ package with all variants + tests # 4. Generates require/ package with all variants + tests -# 5. Ensures 100% test coverage via example-driven tests +# 5. Reorganizes by domain and generates docs/doc-site/ markdown +# 6. Ensures 100% test coverage via example-driven tests ``` ### Example-Driven Test Generation @@ -195,6 +250,65 @@ From this, it generates tests that verify: - `panic: ` - Test should panic (followed by assertion message on next line) `` +### Documentation Generation + +The codegen also generates domain-organized documentation for a Hugo static site: + +**Documentation workflow:** +1. Scanner extracts domain tags from doc comments (e.g., `// domain: string`) +2. Scanner collects domain descriptions from `internal/assertions/doc.go` +3. Generator merges documentation from assert and require packages +4. DocGenerator reorganizes by domain instead of package +5. Markdown files generated in `docs/doc-site/api/` (19 domain pages) + +**Domain organization:** +- Functions are grouped by domain (boolean, collection, comparison, equality, error, etc.) +- 19 domains total covering all assertion categories +- Each domain page shows assert and require variants together +- Index page lists all domains with descriptions +- Uses Hugo front matter for metadata + +**Hugo static site setup:** +Located in `hack/doc-site/hugo/`: +- **hugo.yaml** - Main Hugo configuration +- **gendoc.sh** - Development server script +- **themes/hugo-relearn** - Documentation theme +- Mounts generated content from `docs/doc-site/` + +**Running the documentation site locally:** +```bash +# First generate the documentation +go generate ./... + +# Then start the Hugo dev server +cd hack/doc-site/hugo +./gendoc.sh + +# Visit http://localhost:1313/testify/ +``` + +**Domain tagging in source code:** +To assign a function to a domain, add a domain tag in its doc comment: +```go +// Equal asserts that two objects are equal. +// +// domain: equality +// +// Examples: +// success: 123, 123 +// failure: 123, 456 +func Equal(t T, expected, actual any, msgAndArgs ...any) bool { + // implementation +} +``` + +Add domain descriptions in `internal/assertions/doc.go`: +```go +// domain-description: equality +// Assertions for comparing values for equality, including deep equality, +// value equality, and pointer equality. +``` + ### Build and Verify ```bash # Tidy dependencies @@ -275,6 +389,69 @@ This layered approach ensures: - Simple, maintainable test generation - No duplication of complex test logic +### Iterator Pattern for Table-Driven Tests + +**This repository uses a signature testing pattern** based on Go 1.23's `iter.Seq` for all table-driven tests: + +```go +// Define test case struct +type parseTestExamplesCase struct { + name string + input string + expected []model.Test +} + +// Create iterator function returning iter.Seq[caseType] +func parseTestExamplesCases() iter.Seq[parseTestExamplesCase] { + return slices.Values([]parseTestExamplesCase{ + { + name: "success and failure examples", + input: `Examples: + success: 123, 456 + failure: 789, 012`, + expected: []model.Test{ + {TestedValue: "123, 456", ExpectedOutcome: model.TestSuccess}, + {TestedValue: "789, 012", ExpectedOutcome: model.TestFailure}, + }, + }, + // More cases... + }) +} + +// Test function iterates over cases +func TestParseTestExamples(t *testing.T) { + t.Parallel() + + for c := range parseTestExamplesCases() { + t.Run(c.name, func(t *testing.T) { + t.Parallel() + + result := ParseTestExamples(c.input) + // Assertions... + }) + } +} +``` + +**Benefits of this pattern:** +- Clean separation between test data (iterator functions) and test logic (test functions) +- Easy to add new test cases by appending to the slice +- Test case structs can include helper functions or complex setup logic +- Subtests run in parallel automatically with `t.Parallel()` +- Iterator functions can be reused across multiple tests +- Type-safe test case definitions + +**When to use this pattern:** +- Any test with 2+ test cases +- Tests that need complex setup or helper functions +- Tests that benefit from parallel execution +- Any table-driven test scenario + +**Examples in codebase:** +- `codegen/internal/scanner/comments-parser/examples_test.go` +- `codegen/internal/generator/domains/domains_test.go` +- `internal/assertions/*_test.go` (most assertion tests) + ## Architecture Benefits ### Why This Design Wins @@ -311,6 +488,15 @@ This layered approach ensures: - Import alias resolution for accurate code generation - Handles complex Go constructs (generics, interfaces, variadic args) +**Scanner Architecture (Refactored):** +- Modular sub-packages for different extraction concerns: + - `comments/` - Doc comment extraction with integration tests + - `comments-parser/` - Domain tags, examples, and metadata parsing + - `signature/` - Function signature analysis +- Comprehensive test coverage (~1,435 lines across 4 test files) +- Extracts domain tags, domain descriptions, examples, and other metadata +- Integration tests validate end-to-end comment extraction + **Example-Driven Testing:** - "Examples:" sections in doc comments drive test generation - success/failure/panic cases extracted automatically @@ -318,18 +504,87 @@ This layered approach ensures: - Achieves 100% coverage with minimal test complexity **Template Architecture:** -- Separate templates for assert vs require packages -- Conditional logic handles return values vs void functions +- Separate templates for code (assert/require) and documentation +- Code templates handle return values vs void functions +- Doc templates (doc_index.md.gotmpl, doc_page.md.gotmpl) generate Hugo markdown - Mock selection based on FailNow requirements - Consistent formatting and structure across all output +**Documentation Generator:** +- Merges package-based documentation into domain-based organization +- Reorganizes functions from assert/require packages by domain +- Generates Hugo-compatible markdown with front matter +- Creates navigable API reference organized by concern (boolean, string, error, etc.) + +## Documentation Site + +A Hugo-based documentation site is automatically generated from the source code: +- **Generated content**: `docs/doc-site/api/` - Domain-organized markdown files (19 domain pages) +- **Hugo site**: `hack/doc-site/hugo/` - Hugo configuration and theme (temporary location) +- **Target URL**: `https://go-openapi.github.io/testify/` + +**Site workflow:** +1. `codegen` generates markdown in `docs/doc-site/api/` +2. Hugo builds static site from the generated markdown +3. Hugo mounts the `docs/doc-site/` directory as content + +**Generated domain pages in `docs/doc-site/api/`:** +- `_index.md` - API index page +- `boolean.md`, `collection.md`, `common.md`, `comparison.md`, `condition.md` +- `equality.md`, `error.md`, `file.md`, `http.md`, `json.md` +- `number.md`, `ordering.md`, `panic.md`, `string.md`, `testing.md` +- `time.md`, `type.md`, `yaml.md` + +**Hugo configuration in `hack/doc-site/hugo/`:** +``` +hack/doc-site/hugo/ # Note: Temporary location +├── hugo.yaml # Main Hugo configuration +├── testify.yaml # Generated config with version info +├── testify.yaml.template # Template for testify.yaml +├── gendoc.sh # Development server launcher +├── README.md, TODO.md # Documentation and planning +├── themes/ +│ └── hugo-relearn/ # Documentation theme +├── layouts/ # Custom layout overrides +└── content/ # Mounted from docs/doc-site/ +``` + ## Example Coverage Status -Most assertion functions now have "Examples:" sections in their doc comments. The generator extracts these to create both tests and testable examples. +Most assertion functions now have "Examples:" sections in their doc comments. The generator extracts these to create both tests and documentation examples. **Coverage notes:** - Basic assertions (Equal, Error, Contains, Len, True, False) have complete examples -- Some complex assertions use TODO placeholders for pointer/struct values -- All new assertions should include Examples before merging +- Complex values that can't be easily represented should use `// NOT IMPLEMENTED` as placeholder marker + - **Never use `// TODO`** - it triggers false positives in code quality analyzers + - Use: `success: &structValue{}, // NOT IMPLEMENTED` + - Not: `success: &structValue{}, // TODO` +- All new assertions should include Examples and domain tags before merging +- Domain tags organize assertions into logical groups for documentation For the complete guide on adding examples, see `docs/MAINTAINERS.md` section "Maintaining Generated Code". + +## Scanner Testing and Architecture + +The scanner package has comprehensive test coverage to ensure reliable code generation: + +**Test files (~1,435 lines total):** +- `comments-parser/examples_test.go` (350 lines) - Tests for example extraction from doc comments +- `comments-parser/matchers_test.go` (171 lines) - Tests for pattern matching in comments +- `comments-parser/tags_test.go` (407 lines) - Tests for domain and metadata tag extraction +- `comments/extractor_integration_test.go` (507 lines) - End-to-end comment extraction tests + +**Scanner responsibilities:** +1. Parse Go packages using `go/packages` and `go/types` +2. Extract doc comments and parse structured metadata +3. Identify assertion functions and their signatures +4. Extract domain tags for documentation organization +5. Parse "Examples:" sections for test generation +6. Collect domain descriptions from special comment tags +7. Build the model used by code and doc generators + +**Testing strategy:** +- Unit tests for individual parsers (examples, tags, matchers) +- Integration tests validate complete extraction pipeline +- Tests use real Go code samples from `testdata/` +- Ensures generated code and docs stay in sync with source diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 85707f7ef..a421e53df 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -138,7 +138,7 @@ Most documentation for this library is directly found in code as comments for go The documentation for the go-openapi packages is published on the public go docs site: - + Check your documentation changes for clarity, concision, and correctness. diff --git a/.golangci.yml b/.golangci.yml index 776b1154c..e479680d7 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -48,8 +48,13 @@ linters: - examples$ formatters: enable: - - gofmt + - gofumpt - goimports + settings: + # local prefixes regroup imports from these packages + goimports: + local-prefixes: + - github.com/go-openapi exclusions: generated: lax paths: diff --git a/assert/assert_assertions.go b/assert/assert_assertions.go index b16424664..ff5120051 100644 --- a/assert/assert_assertions.go +++ b/assert/assert_assertions.go @@ -1,7 +1,8 @@ // SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 -// Code generated with github.com/go-openapi/testify/v2/codegen@master [sha: bb2c19fba6c03f46cb643b3bcdc1d647ea1453ab]; DO NOT EDIT. +// Code generated with github.com/go-openapi/testify/v2/codegen; DO NOT EDIT. +// Generated on 2026-01-02 (version v1.2.2-760-g97c29e3) using codegen version master [sha: 97c29e3dbfc40800a080863ceea81db0cfd6e858] package assert @@ -15,7 +16,11 @@ import ( // Condition uses a Comparison to assert a complex condition. // -// Examples: +// # Usage +// +// assertions.Condition(t, func() bool { return myCondition }) +// +// # Examples // // success: func() bool { return true } // failure: func() bool { return false } @@ -31,13 +36,13 @@ func Condition(t T, comp Comparison, msgAndArgs ...any) bool { // Contains asserts that the specified string, list(array, slice...) or map contains the // specified substring or element. // -// Usage: +// # Usage // // assertions.Contains(t, "Hello World", "World") -// assertions.Contains(t, ["Hello", "World"], "World") -// assertions.Contains(t, {"Hello": "World"}, "Hello") +// assertions.Contains(t, []string{"Hello", "World"}, "World") +// assertions.Contains(t, map[string]string{"Hello": "World"}, "Hello") // -// Examples: +// # Examples // // success: []string{"A","B"}, "A" // failure: []string{"A","B"}, "C" @@ -53,7 +58,11 @@ func Contains(t T, s any, contains any, msgAndArgs ...any) bool { // 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. // -// Examples: +// # Usage +// +// assertions.DirExists(t, "path/to/directory") +// +// # Examples // // success: filepath.Join(testDataPath(),"existing_dir") // failure: filepath.Join(testDataPath(),"non_existing_dir") @@ -70,9 +79,11 @@ func DirExists(t T, path string, msgAndArgs ...any) bool { // listB(array, slice...) ignoring the order of the elements. If there are duplicate elements, // the number of appearances of each of them in both lists should match. // -// assert.ElementsMatch(t, [1, 3, 2, 3], [1, 3, 3, 2]). +// # Usage // -// Examples: +// assertions.ElementsMatch(t, [1, 3, 2, 3], [1, 3, 3, 2]) +// +// # Examples // // success: []int{1, 3, 2, 3}, []int{1, 3, 3, 2} // failure: []int{1, 2, 3}, []int{1, 2, 4} @@ -87,7 +98,7 @@ func ElementsMatch(t T, listA any, listB any, msgAndArgs ...any) (ok bool) { // Empty asserts that the given value is "empty". // -// [Zero values] are "empty". +// Zero values are "empty". // // Arrays are "empty" if every element is the zero value of the type (stricter than "empty"). // @@ -95,9 +106,11 @@ func ElementsMatch(t T, listA any, listB any, msgAndArgs ...any) (ok bool) { // // Pointer values are "empty" if the pointer is nil or if the pointed value is "empty". // -// assert.Empty(t, obj) +// # Usage +// +// assertions.Empty(t, obj) // -// Examples: +// # Examples // // success: "" // failure: "not empty" @@ -114,13 +127,16 @@ func Empty(t T, object any, msgAndArgs ...any) bool { // 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. +// referenced values (as opposed to the memory addresses). // -// Examples: +// Function equality cannot be determined and will always fail. +// +// # Usage +// +// assertions.Equal(t, 123, 123) +// +// # Examples // // success: 123, 123 // failure: 123, 456 @@ -136,10 +152,12 @@ func Equal(t T, expected any, actual any, msgAndArgs ...any) bool { // EqualError asserts that a function returned a non-nil error (i.e. an error) // and that it is equal to the provided error. // +// # Usage +// // actualObj, err := SomeFunction() -// assert.EqualError(t, err, expectedErrorString) +// assertions.EqualError(t, err, expectedErrorString) // -// Examples: +// # Examples // // success: ErrTest, "assert.ErrTest general error for testing" // failure: ErrTest, "wrong error message" @@ -156,14 +174,16 @@ func EqualError(t T, theError error, errString string, msgAndArgs ...any) bool { // fields are also equal. This is useful for comparing structs that have private fields // that could potentially differ. // +// # Usage +// // 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 +// assertions.EqualExportedValues(t, S{1, 2}, S{1, 3}) => true +// assertions.EqualExportedValues(t, S{1, 2}, S{2, 3}) => false // -// Examples: +// # Examples // // success: &dummyStruct{A: "a", b: 1}, &dummyStruct{A: "a", b: 2} // failure: &dummyStruct{A: "a", b: 1}, &dummyStruct{A: "b", b: 1} @@ -179,9 +199,11 @@ func EqualExportedValues(t T, expected any, actual any, msgAndArgs ...any) bool // EqualValues asserts that two objects are equal or convertible to the larger // type and equal. // -// assert.EqualValues(t, uint32(123), int32(123)) +// # Usage +// +// assertions.EqualValues(t, uint32(123), int32(123)) // -// Examples: +// # Examples // // success: uint32(123), int32(123) // failure: uint32(123), int32(456) @@ -196,10 +218,12 @@ func EqualValues(t T, expected any, actual any, msgAndArgs ...any) bool { // Error asserts that a function returned a non-nil error (ie. an error). // +// # Usage +// // actualObj, err := SomeFunction() -// assert.Error(t, err) +// assertions.Error(t, err) // -// Examples: +// # Examples // // success: ErrTest // failure: nil @@ -213,9 +237,14 @@ func Error(t T, err error, msgAndArgs ...any) bool { } // 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. // -// Examples: +// This is a wrapper for [errors.As]. +// +// # Usage +// +// assertions.ErrorAs(t, err, &target) +// +// # Examples // // success: fmt.Errorf("wrap: %w", &dummyError{}), new(*dummyError) // failure: ErrTest, new(*dummyError) @@ -231,10 +260,12 @@ func ErrorAs(t T, err error, target any, msgAndArgs ...any) bool { // ErrorContains asserts that a function returned a non-nil error (i.e. an // error) and that the error contains the specified substring. // +// # Usage +// // actualObj, err := SomeFunction() -// assert.ErrorContains(t, err, expectedErrorSubString) +// assertions.ErrorContains(t, err, expectedErrorSubString) // -// Examples: +// # Examples // // success: ErrTest, "general error" // failure: ErrTest, "not in message" @@ -248,9 +279,14 @@ func ErrorContains(t T, theError error, contains string, msgAndArgs ...any) bool } // ErrorIs asserts that at least one of the errors in err's chain matches target. -// This is a wrapper for errors.Is. // -// Examples: +// This is a wrapper for [errors.Is]. +// +// # Usage +// +// assertions.ErrorIs(t, err, io.EOF) +// +// # Examples // // success: fmt.Errorf("wrap: %w", io.EOF), io.EOF // failure: ErrTest, io.EOF @@ -266,9 +302,11 @@ func ErrorIs(t T, err error, target error, msgAndArgs ...any) bool { // 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) +// # Usage +// +// assertions.Eventually(t, func() bool { return true; }, time.Second, 10*time.Millisecond) // -// Examples: +// # Examples // // success: func() bool { return true }, 100*time.Millisecond, 20*time.Millisecond // failure: func() bool { return false }, 100*time.Millisecond, 20*time.Millisecond @@ -290,17 +328,19 @@ func Eventually(t T, condition func() bool, waitFor time.Duration, tick time.Dur // If the condition is not met before waitFor, the collected errors of // the last tick are copied to t. // +// # Usage +// // externalValue := false // go func() { // time.Sleep(8*time.Second) // externalValue = true // }() -// assert.EventuallyWithT(t, func(c *assert.CollectT) { +// assertions.EventuallyWithT(t, func(c *assertions.CollectT) { // // add assertions as needed; any assertion failure will fail the current tick -// assert.True(c, externalValue, "expected 'externalValue' to be true") +// assertions.True(c, externalValue, "expected 'externalValue' to be true") // }, 10*time.Second, 1*time.Second, "external state has not changed to 'true'; still false") // -// Examples: +// # Examples // // success: func(c *CollectT) { True(c,true) }, 100*time.Millisecond, 20*time.Millisecond // failure: func(c *CollectT) { False(c,true) }, 100*time.Millisecond, 20*time.Millisecond @@ -315,9 +355,11 @@ func EventuallyWithT(t T, condition func(collect *CollectT), waitFor time.Durati // Exactly asserts that two objects are equal in value and type. // -// assert.Exactly(t, int32(123), int64(123)) +// # Usage +// +// assertions.Exactly(t, int32(123), int64(123)) // -// Examples: +// # Examples // // success: int32(123), int32(123) // failure: int32(123), int64(123) @@ -332,7 +374,11 @@ func Exactly(t T, expected any, actual any, msgAndArgs ...any) bool { // Fail reports a failure through. // -// Example: +// # Usage +// +// assertions.Fail(t, "failed") +// +// # Examples // // failure: "failed" // @@ -346,7 +392,11 @@ func Fail(t T, failureMessage string, msgAndArgs ...any) bool { // FailNow fails test. // -// Example: +// # Usage +// +// assertions.FailNow(t, "failed") +// +// # Examples // // failure: "failed" // @@ -360,11 +410,11 @@ func FailNow(t T, failureMessage string, msgAndArgs ...any) bool { // False asserts that the specified value is false. // -// Usage: +// # Usage // // assertions.False(t, myBool) // -// Examples: +// # Examples // // success: 1 == 0 // failure: 1 == 1 @@ -380,7 +430,11 @@ func False(t T, value bool, msgAndArgs ...any) bool { // FileEmpty checks whether a file exists in the given path and is empty. // It fails if the file is not empty, if the path points to a directory or there is an error when trying to check the file. // -// Examples: +// # Usage +// +// assertions.FileEmpty(t, "path/to/file") +// +// # Examples // // success: filepath.Join(testDataPath(),"empty_file") // failure: filepath.Join(testDataPath(),"existing_file") @@ -396,7 +450,11 @@ func FileEmpty(t T, path string, msgAndArgs ...any) bool { // 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. // -// Examples: +// # Usage +// +// assertions.FileExists(t, "path/to/file") +// +// # Examples // // success: filepath.Join(testDataPath(),"existing_file") // failure: filepath.Join(testDataPath(),"non_existing_file") @@ -412,7 +470,11 @@ func FileExists(t T, path string, msgAndArgs ...any) bool { // FileNotEmpty checks whether a file exists in the given path and is not empty. // It fails if the file is empty, if the path points to a directory or there is an error when trying to check the file. // -// Examples: +// # Usage +// +// assertions.FileNotEmpty(t, "path/to/file") +// +// # Examples // // success: filepath.Join(testDataPath(),"existing_file") // failure: filepath.Join(testDataPath(),"empty_file") @@ -427,13 +489,13 @@ func FileNotEmpty(t T, path string, msgAndArgs ...any) bool { // Greater asserts that the first element is strictly greater than the second. // -// Usage: +// # Usage // // assertions.Greater(t, 2, 1) // assertions.Greater(t, float64(2), float64(1)) // assertions.Greater(t, "b", "a") // -// Examples: +// # Examples // // success: 2, 1 // failure: 1, 2 @@ -448,12 +510,14 @@ func Greater(t T, e1 any, e2 any, msgAndArgs ...any) bool { // 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") +// # Usage +// +// assertions.GreaterOrEqual(t, 2, 1) +// assertions.GreaterOrEqual(t, 2, 2) +// assertions.GreaterOrEqual(t, "b", "a") +// assertions.GreaterOrEqual(t, "b", "b") // -// Examples: +// # Examples // // success: 2, 1 // failure: 1, 2 @@ -468,11 +532,13 @@ func GreaterOrEqual(t T, e1 any, e2 any, msgAndArgs ...any) bool { // 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). // -// Examples: +// # Usage +// +// assertions.HTTPBodyContains(t, myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky") +// +// # Examples // // success: httpBody, "GET", "/", url.Values{"name": []string{"World"}}, "Hello, World!" // failure: httpBody, "GET", "/", url.Values{"name": []string{"Bob"}}, "Hello, World!" @@ -488,11 +554,13 @@ func HTTPBodyContains(t T, handler http.HandlerFunc, method string, url string, // 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). // -// Examples: +// # Usage +// +// assertions.HTTPBodyNotContains(t, myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky") +// +// # Examples // // success: httpBody, "GET", "/", url.Values{"name": []string{"World"}}, "Hello, Bob!" // failure: httpBody, "GET", "/", url.Values{"name": []string{"Bob"}}, "Hello, Bob!" @@ -507,11 +575,13 @@ func HTTPBodyNotContains(t T, handler http.HandlerFunc, method string, url strin // 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). // -// Examples: +// # Usage +// +// assertions.HTTPError(t, myHandler, "POST", "/a/b/c", url.Values{"a": []string{"b", "c"}} +// +// # Examples // // success: httpError, "GET", "/", nil // failure: httpOK, "GET", "/", nil @@ -526,11 +596,13 @@ func HTTPError(t T, handler http.HandlerFunc, method string, url string, values // 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). // -// Examples: +// # Usage +// +// assertions.HTTPRedirect(t, myHandler, "GET", "/a/b/c", url.Values{"a": []string{"b", "c"}} +// +// # Examples // // success: httpRedirect, "GET", "/", nil // failure: httpError, "GET", "/", nil @@ -545,11 +617,13 @@ func HTTPRedirect(t T, handler http.HandlerFunc, method string, url string, valu // 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). // -// Examples: +// # Usage +// +// assertions.HTTPStatusCode(t, myHandler, "GET", "/notImplemented", nil, 501) +// +// # Examples // // success: httpOK, "GET", "/", nil, http.StatusOK // failure: httpError, "GET", "/", nil, http.StatusOK @@ -564,11 +638,13 @@ func HTTPStatusCode(t T, handler http.HandlerFunc, method string, url string, va // 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). // -// Examples: +// # Usage +// +// assertions.HTTPSuccess(t, myHandler, "POST", "http://www.google.com", nil) +// +// # Examples // // success: httpOK, "GET", "/", nil // failure: httpError, "GET", "/", nil @@ -583,9 +659,11 @@ func HTTPSuccess(t T, handler http.HandlerFunc, method string, url string, value // Implements asserts that an object is implemented by the specified interface. // -// assert.Implements(t, (*MyInterface)(nil), new(MyObject)) +// # Usage // -// Examples: +// assertions.Implements(t, (*MyInterface)(nil), new(MyObject)) +// +// # Examples // // success: ptr(dummyInterface), new(testing.T) // failure: (*error)(nil), new(testing.T) @@ -600,9 +678,11 @@ func Implements(t T, interfaceObject any, object any, msgAndArgs ...any) bool { // InDelta asserts that the two numerals are within delta of each other. // -// assert.InDelta(t, math.Pi, 22/7.0, 0.01) +// # Usage +// +// assertions.InDelta(t, math.Pi, 22/7.0, 0.01) // -// Examples: +// # Examples // // success: 1.0, 1.01, 0.02 // failure: 1.0, 1.1, 0.05 @@ -617,7 +697,11 @@ func InDelta(t T, expected any, actual any, delta float64, msgAndArgs ...any) bo // InDeltaMapValues is the same as InDelta, but it compares all values between two maps. Both maps must have exactly the same keys. // -// Examples: +// # Usage +// +// assertions.InDeltaMapValues(t, map[string]float64{"a": 1.0}, map[string]float64{"a": 1.01}, 0.02) +// +// # Examples // // success: map[string]float64{"a": 1.0}, map[string]float64{"a": 1.01}, 0.02 // failure: map[string]float64{"a": 1.0}, map[string]float64{"a": 1.1}, 0.05 @@ -632,7 +716,11 @@ func InDeltaMapValues(t T, expected any, actual any, delta float64, msgAndArgs . // InDeltaSlice is the same as InDelta, except it compares two slices. // -// Examples: +// # Usage +// +// assertions.InDeltaSlice(t, []float64{1.0, 2.0}, []float64{1.01, 2.01}, 0.02) +// +// # Examples // // success: []float64{1.0, 2.0}, []float64{1.01, 2.01}, 0.02 // failure: []float64{1.0, 2.0}, []float64{1.1, 2.1}, 0.05 @@ -647,7 +735,11 @@ func InDeltaSlice(t T, expected any, actual any, delta float64, msgAndArgs ...an // InEpsilon asserts that expected and actual have a relative error less than epsilon. // -// Examples: +// # Usage +// +// assertions.InEpsilon(t, 100.0, 101.0, 0.02) +// +// # Examples // // success: 100.0, 101.0, 0.02 // failure: 100.0, 110.0, 0.05 @@ -662,7 +754,11 @@ func InEpsilon(t T, expected any, actual any, epsilon float64, msgAndArgs ...any // InEpsilonSlice is the same as InEpsilon, except it compares each value from two slices. // -// Examples: +// # Usage +// +// assertions.InEpsilonSlice(t, []float64{100.0, 200.0}, []float64{101.0, 202.0}, 0.02) +// +// # Examples // // success: []float64{100.0, 200.0}, []float64{101.0, 202.0}, 0.02 // failure: []float64{100.0, 200.0}, []float64{110.0, 220.0}, 0.05 @@ -677,11 +773,13 @@ func InEpsilonSlice(t T, expected any, actual any, epsilon float64, 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"}) +// # Usage +// +// assertions.IsDecreasing(t, []int{2, 1, 0}) +// assertions.IsDecreasing(t, []float{2, 1}) +// assertions.IsDecreasing(t, []string{"b", "a"}) // -// Examples: +// # Examples // // success: []int{3, 2, 1} // failure: []int{1, 2, 3} @@ -696,11 +794,13 @@ func IsDecreasing(t T, object any, msgAndArgs ...any) bool { // 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"}) +// # Usage // -// Examples: +// assertions.IsIncreasing(t, []int{1, 2, 3}) +// assertions.IsIncreasing(t, []float{1, 2}) +// assertions.IsIncreasing(t, []string{"a", "b"}) +// +// # Examples // // success: []int{1, 2, 3} // failure: []int{1, 1, 2} @@ -715,11 +815,13 @@ func IsIncreasing(t T, object any, msgAndArgs ...any) bool { // 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"}) +// # Usage +// +// assertions.IsNonDecreasing(t, []int{1, 1, 2}) +// assertions.IsNonDecreasing(t, []float{1, 2}) +// assertions.IsNonDecreasing(t, []string{"a", "b"}) // -// Examples: +// # Examples // // success: []int{1, 1, 2} // failure: []int{2, 1, 1} @@ -734,11 +836,13 @@ func IsNonDecreasing(t T, object any, msgAndArgs ...any) bool { // 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"}) +// # Usage // -// Examples: +// assertions.IsNonIncreasing(t, []int{2, 1, 1}) +// assertions.IsNonIncreasing(t, []float{2, 1}) +// assertions.IsNonIncreasing(t, []string{"b", "a"}) +// +// # Examples // // success: []int{2, 1, 1} // failure: []int{1, 2, 3} @@ -753,9 +857,11 @@ func IsNonIncreasing(t T, object any, msgAndArgs ...any) bool { // IsNotType asserts that the specified objects are not of the same type. // -// assert.IsNotType(t, &NotMyStruct{}, &MyStruct{}) +// # Usage +// +// assertions.IsNotType(t, &NotMyStruct{}, &MyStruct{}) // -// Examples: +// # Examples // // success: int32(123), int64(456) // failure: 123, 456 @@ -770,9 +876,11 @@ func IsNotType(t T, theType any, object any, msgAndArgs ...any) bool { // IsType asserts that the specified objects are of the same type. // -// assert.IsType(t, &MyStruct{}, &MyStruct{}) +// # Usage // -// Examples: +// assertions.IsType(t, &MyStruct{}, &MyStruct{}) +// +// # Examples // // success: 123, 456 // failure: int32(123), int64(456) @@ -787,9 +895,11 @@ func IsType(t T, expectedType any, object any, msgAndArgs ...any) bool { // JSONEq asserts that two JSON strings are equivalent. // -// assert.JSONEq(t, `{"hello": "world", "foo": "bar"}`, `{"foo": "bar", "hello": "world"}`) +// # Usage +// +// assertions.JSONEq(t, `{"hello": "world", "foo": "bar"}`, `{"foo": "bar", "hello": "world"}`) // -// Examples: +// # Examples // // success: `{"hello": "world", "foo": "bar"}`, `{"foo": "bar", "hello": "world"}` // failure: `{"hello": "world", "foo": "bar"}`, `[{"foo": "bar"}, {"hello": "world"}]` @@ -804,9 +914,11 @@ func JSONEq(t T, expected string, actual string, msgAndArgs ...any) bool { // JSONEqBytes asserts that two JSON byte slices are equivalent. // -// assert.JSONEqBytes(t, []byte(`{"hello": "world", "foo": "bar"}`), []byte(`{"foo": "bar", "hello": "world"}`)) +// # Usage // -// Examples: +// assertions.JSONEqBytes(t, []byte(`{"hello": "world", "foo": "bar"}`), []byte(`{"foo": "bar", "hello": "world"}`)) +// +// # Examples // // success: []byte(`{"hello": "world", "foo": "bar"}`), []byte(`{"foo": "bar", "hello": "world"}`) // failure: []byte(`{"hello": "world", "foo": "bar"}`), []byte(`[{"foo": "bar"}, {"hello": "world"}]`) @@ -827,13 +939,13 @@ func JSONEqBytes(t T, expected []byte, actual []byte, msgAndArgs ...any) bool { // // See also [reflect.Len]. // -// Usage: +// # Usage // // assertions.Len(t, mySlice, 3) // assertions.Len(t, myString, 4) // assertions.Len(t, myMap, 5) // -// Examples: +// # Examples // // success: []string{"A","B"}, 2 // failure: []string{"A","B"}, 1 @@ -848,11 +960,13 @@ func Len(t T, object any, length int, msgAndArgs ...any) bool { // Less asserts that the first element is strictly less than the second. // -// assert.Less(t, 1, 2) -// assert.Less(t, float64(1), float64(2)) -// assert.Less(t, "a", "b") +// # Usage +// +// assertions.Less(t, 1, 2) +// assertions.Less(t, float64(1), float64(2)) +// assertions.Less(t, "a", "b") // -// Examples: +// # Examples // // success: 1, 2 // failure: 2, 1 @@ -867,12 +981,14 @@ func Less(t T, e1 any, e2 any, msgAndArgs ...any) bool { // 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") +// # Usage +// +// assertions.LessOrEqual(t, 1, 2) +// assertions.LessOrEqual(t, 2, 2) +// assertions.LessOrEqual(t, "a", "b") +// assertions.LessOrEqual(t, "b", "b") // -// Examples: +// # Examples // // success: 1, 2 // failure: 2, 1 @@ -887,10 +1003,12 @@ func LessOrEqual(t T, e1 any, e2 any, msgAndArgs ...any) bool { // Negative asserts that the specified element is strictly negative. // -// assert.Negative(t, -1) -// assert.Negative(t, -1.23) +// # Usage // -// Examples: +// assertions.Negative(t, -1) +// assertions.Negative(t, -1.23) +// +// # Examples // // success: -1 // failure: 1 @@ -906,9 +1024,11 @@ func Negative(t T, e any, msgAndArgs ...any) bool { // 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) +// # Usage +// +// assertions.Never(t, func() bool { return false; }, time.Second, 10*time.Millisecond) // -// Examples: +// # Examples // // success: func() bool { return false }, 100*time.Millisecond, 20*time.Millisecond // failure: func() bool { return true }, 100*time.Millisecond, 20*time.Millisecond @@ -923,9 +1043,11 @@ func Never(t T, condition func() bool, waitFor time.Duration, tick time.Duration // Nil asserts that the specified object is nil. // -// assert.Nil(t, err) +// # Usage // -// Examples: +// assertions.Nil(t, err) +// +// # Examples // // success: nil // failure: "not nil" @@ -940,7 +1062,12 @@ func Nil(t T, object any, msgAndArgs ...any) bool { // NoDirExists checks whether a directory does not exist in the given path. // It fails if the path points to an existing _directory_ only. -// Examples: +// +// # Usage +// +// assertions.NoDirExists(t, "path/to/directory") +// +// # Examples // // success: filepath.Join(testDataPath(),"non_existing_dir") // failure: filepath.Join(testDataPath(),"existing_dir") @@ -955,12 +1082,14 @@ func NoDirExists(t T, path string, msgAndArgs ...any) bool { // NoError asserts that a function returned a nil error (ie. no error). // -// actualObj, err := SomeFunction() -// if assert.NoError(t, err) { -// assert.Equal(t, expectedObj, actualObj) -// } +// # Usage +// +// actualObj, err := SomeFunction() +// if assert.NoError(t, err) { +// assertions.Equal(t, expectedObj, actualObj) +// } // -// Examples: +// # Examples // // success: nil // failure: ErrTest @@ -976,7 +1105,11 @@ func NoError(t T, err error, msgAndArgs ...any) bool { // NoFileExists checks whether a file does not exist in a given path. It fails // if the path points to an existing _file_ only. // -// Examples: +// # Usage +// +// assertions.NoFileExists(t, "path/to/file") +// +// # Examples // // success: filepath.Join(testDataPath(),"non_existing_file") // failure: filepath.Join(testDataPath(),"existing_file") @@ -992,13 +1125,13 @@ func NoFileExists(t T, path string, msgAndArgs ...any) bool { // NotContains asserts that the specified string, list(array, slice...) or map does NOT contain the // specified substring or element. // -// Usage: +// # Usage // // assertions.NotContains(t, "Hello World", "Earth") // assertions.NotContains(t, ["Hello", "World"], "Earth") // assertions.NotContains(t, {"Hello": "World"}, "Earth") // -// Examples: +// # Examples // // success: []string{"A","B"}, "C" // failure: []string{"A","B"}, "B" @@ -1016,13 +1149,13 @@ func NotContains(t T, s any, contains any, msgAndArgs ...any) bool { // 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 +// # Usage // -// assert.NotElementsMatch(t, [1, 1, 2, 3], [1, 2, 3]) -> true +// assertions.NotElementsMatch(t, [1, 1, 2, 3], [1, 1, 2, 3]) -> false +// assertions.NotElementsMatch(t, [1, 1, 2, 3], [1, 2, 3]) -> true +// assertions.NotElementsMatch(t, [1, 2, 3], [1, 2, 4]) -> true // -// assert.NotElementsMatch(t, [1, 2, 3], [1, 2, 4]) -> true. -// -// Examples: +// # Examples // // success: []int{1, 2, 3}, []int{1, 2, 4} // failure: []int{1, 3, 2, 3}, []int{1, 3, 3, 2} @@ -1037,11 +1170,13 @@ func NotElementsMatch(t T, listA any, listB any, msgAndArgs ...any) (ok bool) { // NotEmpty asserts that the specified object is NOT [Empty]. // +// # Usage +// // if assert.NotEmpty(t, obj) { -// assert.Equal(t, "two", obj[1]) +// assertions.Equal(t, "two", obj[1]) // } // -// Examples: +// # Examples // // success: "not empty" // failure: "" @@ -1056,12 +1191,14 @@ func NotEmpty(t T, object any, msgAndArgs ...any) bool { // NotEqual asserts that the specified values are NOT equal. // -// assert.NotEqual(t, obj1, obj2) +// # Usage +// +// assertions.NotEqual(t, obj1, obj2) // // Pointer variable equality is determined based on the equality of the // referenced values (as opposed to the memory addresses). // -// Examples: +// # Examples // // success: 123, 456 // failure: 123, 123 @@ -1076,9 +1213,11 @@ func NotEqual(t T, expected any, actual any, msgAndArgs ...any) bool { // NotEqualValues asserts that two objects are not equal even when converted to the same type. // -// assert.NotEqualValues(t, obj1, obj2) +// # Usage // -// Examples: +// assertions.NotEqualValues(t, obj1, obj2) +// +// # Examples // // success: uint32(123), int32(456) // failure: uint32(123), int32(123) @@ -1094,7 +1233,11 @@ func NotEqualValues(t T, expected any, actual any, msgAndArgs ...any) bool { // NotErrorAs asserts that none of the errors in err's chain matches target, // but if so, sets target to that error value. // -// Examples: +// # Usage +// +// assertions.NotErrorAs(t, err, &target) +// +// # Examples // // success: ErrTest, new(*dummyError) // failure: fmt.Errorf("wrap: %w", &dummyError{}), new(*dummyError) @@ -1108,9 +1251,14 @@ func NotErrorAs(t T, err error, target any, msgAndArgs ...any) bool { } // NotErrorIs asserts that none of the errors in err's chain matches target. -// This is a wrapper for errors.Is. // -// Examples: +// This is a wrapper for [errors.Is]. +// +// # Usage +// +// assertions.NotErrorIs(t, err, io.EOF) +// +// # Examples // // success: ErrTest, io.EOF // failure: fmt.Errorf("wrap: %w", io.EOF), io.EOF @@ -1125,9 +1273,11 @@ func NotErrorIs(t T, err error, target error, msgAndArgs ...any) bool { // NotImplements asserts that an object does not implement the specified interface. // -// assert.NotImplements(t, (*MyInterface)(nil), new(MyObject)) +// # Usage // -// Examples: +// assertions.NotImplements(t, (*MyInterface)(nil), new(MyObject)) +// +// # Examples // // success: (*error)(nil), new(testing.T) // failure: ptr(dummyInterface), new(testing.T) @@ -1142,9 +1292,11 @@ func NotImplements(t T, interfaceObject any, object any, msgAndArgs ...any) bool // NotNil asserts that the specified object is not nil. // -// assert.NotNil(t, err) +// # Usage +// +// assertions.NotNil(t, err) // -// Examples: +// # Examples // // success: "not nil" // failure: nil @@ -1159,9 +1311,11 @@ func NotNil(t T, object any, msgAndArgs ...any) bool { // NotPanics asserts that the code inside the specified PanicTestFunc does NOT panic. // -// assert.NotPanics(t, func(){ RemainCalm() }) +// # Usage // -// Examples: +// assertions.NotPanics(t, func(){ RemainCalm() }) +// +// # Examples // // success: func() { } // failure: func() { panic("panicking") } @@ -1176,10 +1330,12 @@ func NotPanics(t T, f assertions.PanicTestFunc, msgAndArgs ...any) bool { // 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") +// # Usage +// +// assertions.NotRegexp(t, regexp.MustCompile("starts"), "it's starting") +// assertions.NotRegexp(t, "^start", "it's not starting") // -// Examples: +// # Examples // // success: "^start", "not starting" // failure: "^start", "starting" @@ -1194,12 +1350,14 @@ func NotRegexp(t T, rx any, str any, msgAndArgs ...any) bool { // 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. // -// Examples: +// # Usage +// +// assertions.NotSame(t, ptr1, ptr2) +// +// # Examples // // success: &staticVar, ptr("static string") // failure: &staticVar, staticVarPtr @@ -1217,12 +1375,14 @@ func NotSame(t T, expected any, actual any, msgAndArgs ...any) bool { // 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"]) +// # Usage +// +// assertions.NotSubset(t, [1, 3, 4], [1, 2]) +// assertions.NotSubset(t, {"x": 1, "y": 2}, {"z": 3}) +// assertions.NotSubset(t, [1, 3, 4], {1: "one", 2: "two"}) +// assertions.NotSubset(t, {"x": 1, "y": 2}, ["z"]) // -// Examples: +// # Examples // // success: []int{1, 2, 3}, []int{4, 5} // failure: []int{1, 2, 3}, []int{1, 2} @@ -1237,7 +1397,11 @@ func NotSubset(t T, list any, subset any, msgAndArgs ...any) (ok bool) { // NotZero asserts that i is not the zero value for its type. // -// Examples: +// # Usage +// +// assertions.NotZero(t, obj) +// +// # Examples // // success: 1 // failure: 0 @@ -1252,9 +1416,11 @@ func NotZero(t T, i any, msgAndArgs ...any) bool { // Panics asserts that the code inside the specified PanicTestFunc panics. // -// assert.Panics(t, func(){ GoCrazy() }) +// # Usage +// +// assertions.Panics(t, func(){ GoCrazy() }) // -// Examples: +// # Examples // // success: func() { panic("panicking") } // failure: func() { } @@ -1271,9 +1437,11 @@ func Panics(t T, f assertions.PanicTestFunc, msgAndArgs ...any) bool { // panics, and that the recovered panic value is an error that satisfies the // EqualError comparison. // -// assert.PanicsWithError(t, "crazy error", func(){ GoCrazy() }) +// # Usage // -// Examples: +// assertions.PanicsWithError(t, "crazy error", func(){ GoCrazy() }) +// +// # Examples // // success: ErrTest.Error(), func() { panic(ErrTest) } // failure: ErrTest.Error(), func() { } @@ -1289,9 +1457,11 @@ func PanicsWithError(t T, errString string, f assertions.PanicTestFunc, msgAndAr // 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() }) +// # Usage +// +// assertions.PanicsWithValue(t, "crazy error", func(){ GoCrazy() }) // -// Examples: +// # Examples // // success: "panicking", func() { panic("panicking") } // failure: "panicking", func() { } @@ -1306,10 +1476,12 @@ func PanicsWithValue(t T, expected any, f assertions.PanicTestFunc, msgAndArgs . // Positive asserts that the specified element is strictly positive. // -// assert.Positive(t, 1) -// assert.Positive(t, 1.23) +// # Usage // -// Examples: +// assertions.Positive(t, 1) +// assertions.Positive(t, 1.23) +// +// # Examples // // success: 1 // failure: -1 @@ -1324,10 +1496,12 @@ func Positive(t T, e any, msgAndArgs ...any) bool { // 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") +// # Usage +// +// assertions.Regexp(t, regexp.MustCompile("start"), "it's starting") +// assertions.Regexp(t, "start...$", "it's not starting") // -// Examples: +// # Examples // // success: "^start", "starting" // failure: "^start", "not starting" @@ -1342,12 +1516,14 @@ func Regexp(t T, rx any, str any, msgAndArgs ...any) bool { // 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. // -// Examples: +// # Usage +// +// assertions.Same(t, ptr1, ptr2) +// +// # Examples // // success: &staticVar, staticVarPtr // failure: &staticVar, ptr("static string") @@ -1366,12 +1542,14 @@ func Same(t T, expected any, actual any, msgAndArgs ...any) bool { // 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"]) +// # Usage // -// Examples: +// assertions.Subset(t, [1, 2, 3], [1, 2]) +// assertions.Subset(t, {"x": 1, "y": 2}, {"x": 1}) +// assertions.Subset(t, [1, 2, 3], {1: "one", 2: "two"}) +// assertions.Subset(t, {"x": 1, "y": 2}, ["x"]) +// +// # Examples // // success: []int{1, 2, 3}, []int{1, 2} // failure: []int{1, 2, 3}, []int{4, 5} @@ -1386,11 +1564,11 @@ func Subset(t T, list any, subset any, msgAndArgs ...any) (ok bool) { // True asserts that the specified value is true. // -// Usage: +// # Usage // // assertions.True(t, myBool) // -// Examples: +// # Examples // // success: 1 == 1 // failure: 1 == 0 @@ -1405,9 +1583,11 @@ func True(t T, value bool, msgAndArgs ...any) bool { // WithinDuration asserts that the two times are within duration delta of each other. // -// assert.WithinDuration(t, time.Now(), 10*time.Second) +// # Usage +// +// assertions.WithinDuration(t, time.Now(), 10*time.Second) // -// Examples: +// # Examples // // success: time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC), time.Date(2024, 1, 1, 12, 0, 1, 0, time.UTC), 2*time.Second // failure: time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC), time.Date(2024, 1, 1, 12, 0, 10, 0, time.UTC), 1*time.Second @@ -1422,9 +1602,11 @@ func WithinDuration(t T, expected time.Time, actual time.Time, delta time.Durati // 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)) +// # Usage // -// Examples: +// assertions.WithinRange(t, time.Now(), time.Now().Add(-time.Second), time.Now().Add(time.Second)) +// +// # Examples // // success: time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC), time.Date(2024, 1, 1, 11, 0, 0, 0, time.UTC), time.Date(2024, 1, 1, 13, 0, 0, 0, time.UTC) // failure: time.Date(2024, 1, 1, 14, 0, 0, 0, time.UTC), time.Date(2024, 1, 1, 11, 0, 0, 0, time.UTC), time.Date(2024, 1, 1, 13, 0, 0, 0, time.UTC) @@ -1439,7 +1621,7 @@ func WithinRange(t T, actual time.Time, start time.Time, end time.Time, msgAndAr // YAMLEq asserts that the first documents in the two YAML strings are equivalent. // -// Usage: +// # Usage // // expected := `--- // key: value @@ -1453,10 +1635,10 @@ func WithinRange(t T, actual time.Time, start time.Time, end time.Time, msgAndAr // ` // assertions.YAMLEq(t, expected, actual) // -// Example: +// # Examples // -// panic: "key: value", "key: value" -// should panic without the yaml feature enabled +// panic: "key: value", "key: value" +// should panic without the yaml feature enabled. // // Upon failure, the test [T] is marked as failed and continues execution. func YAMLEq(t T, expected string, actual string, msgAndArgs ...any) bool { @@ -1468,7 +1650,11 @@ func YAMLEq(t T, expected string, actual string, msgAndArgs ...any) bool { // Zero asserts that i is the zero value for its type. // -// Examples: +// # Usage +// +// assertions.Zero(t, obj) +// +// # Examples // // success: 0 // failure: 1 diff --git a/assert/assert_assertions_test.go b/assert/assert_assertions_test.go index 63ec808c8..68cc33de2 100644 --- a/assert/assert_assertions_test.go +++ b/assert/assert_assertions_test.go @@ -1,7 +1,8 @@ // SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 -// Code generated with github.com/go-openapi/testify/v2/codegen@master [sha: bb2c19fba6c03f46cb643b3bcdc1d647ea1453ab]; DO NOT EDIT. +// Code generated with github.com/go-openapi/testify/v2/codegen; DO NOT EDIT. +// Generated on 2026-01-02 (version v1.2.2-760-g97c29e3) using codegen version master [sha: 97c29e3dbfc40800a080863ceea81db0cfd6e858] package assert @@ -1854,7 +1855,7 @@ func TestYAMLEq(t *testing.T) { Panics(t, func() { YAMLEq(t, "key: value", "key: value") - }, "should panic without the yaml feature enabled") + }, "should panic without the yaml feature enabled.") }) } diff --git a/assert/assert_examples_test.go b/assert/assert_examples_test.go index 4cda71b3e..82cb71f11 100644 --- a/assert/assert_examples_test.go +++ b/assert/assert_examples_test.go @@ -1,7 +1,8 @@ // SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 -// Code generated with github.com/go-openapi/testify/v2/codegen@master [sha: bb2c19fba6c03f46cb643b3bcdc1d647ea1453ab]; DO NOT EDIT. +// Code generated with github.com/go-openapi/testify/v2/codegen; DO NOT EDIT. +// Generated on 2026-01-02 (version v1.2.2-760-g97c29e3) using codegen version master [sha: 97c29e3dbfc40800a080863ceea81db0cfd6e858] package assert_test @@ -19,7 +20,9 @@ import ( func ExampleCondition() { t := new(testing.T) - success := assert.Condition(t, func() bool { return true }) + success := assert.Condition(t, func() bool { + return true + }) fmt.Printf("success: %t\n", success) // Output: success: true @@ -123,7 +126,9 @@ func ExampleErrorIs() { func ExampleEventually() { t := new(testing.T) - success := assert.Eventually(t, func() bool { return true }, 100*time.Millisecond, 20*time.Millisecond) + success := assert.Eventually(t, func() bool { + return true + }, 100*time.Millisecond, 20*time.Millisecond) fmt.Printf("success: %t\n", success) // Output: success: true @@ -131,7 +136,9 @@ func ExampleEventually() { func ExampleEventuallyWithT() { t := new(testing.T) - success := assert.EventuallyWithT(t, func(c *assert.CollectT) { assert.True(c, true) }, 100*time.Millisecond, 20*time.Millisecond) + success := assert.EventuallyWithT(t, func(c *assert.CollectT) { + assert.True(c, true) + }, 100*time.Millisecond, 20*time.Millisecond) fmt.Printf("success: %t\n", success) // Output: success: true @@ -395,7 +402,9 @@ func ExampleNegative() { func ExampleNever() { t := new(testing.T) - success := assert.Never(t, func() bool { return false }, 100*time.Millisecond, 20*time.Millisecond) + success := assert.Never(t, func() bool { + return false + }, 100*time.Millisecond, 20*time.Millisecond) fmt.Printf("success: %t\n", success) // Output: success: true @@ -507,7 +516,8 @@ func ExampleNotNil() { func ExampleNotPanics() { t := new(testing.T) - success := assert.NotPanics(t, func() {}) + success := assert.NotPanics(t, func() { + }) fmt.Printf("success: %t\n", success) // Output: success: true @@ -547,7 +557,9 @@ func ExampleNotZero() { func ExamplePanics() { t := new(testing.T) - success := assert.Panics(t, func() { panic("panicking") }) + success := assert.Panics(t, func() { + panic("panicking") + }) fmt.Printf("success: %t\n", success) // Output: success: true @@ -555,7 +567,9 @@ func ExamplePanics() { func ExamplePanicsWithError() { t := new(testing.T) - success := assert.PanicsWithError(t, assert.ErrTest.Error(), func() { panic(assert.ErrTest) }) + success := assert.PanicsWithError(t, assert.ErrTest.Error(), func() { + panic(assert.ErrTest) + }) fmt.Printf("success: %t\n", success) // Output: success: true @@ -563,7 +577,9 @@ func ExamplePanicsWithError() { func ExamplePanicsWithValue() { t := new(testing.T) - success := assert.PanicsWithValue(t, "panicking", func() { panic("panicking") }) + success := assert.PanicsWithValue(t, "panicking", func() { + panic("panicking") + }) fmt.Printf("success: %t\n", success) // Output: success: true diff --git a/assert/assert_format.go b/assert/assert_format.go index 597352781..b6b901ed8 100644 --- a/assert/assert_format.go +++ b/assert/assert_format.go @@ -1,7 +1,8 @@ // SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 -// Code generated with github.com/go-openapi/testify/v2/codegen@master [sha: bb2c19fba6c03f46cb643b3bcdc1d647ea1453ab]; DO NOT EDIT. +// Code generated with github.com/go-openapi/testify/v2/codegen; DO NOT EDIT. +// Generated on 2026-01-02 (version v1.2.2-760-g97c29e3) using codegen version master [sha: 97c29e3dbfc40800a080863ceea81db0cfd6e858] package assert diff --git a/assert/assert_format_test.go b/assert/assert_format_test.go index 8f35675d0..e5e318a87 100644 --- a/assert/assert_format_test.go +++ b/assert/assert_format_test.go @@ -1,7 +1,8 @@ // SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 -// Code generated with github.com/go-openapi/testify/v2/codegen@master [sha: bb2c19fba6c03f46cb643b3bcdc1d647ea1453ab]; DO NOT EDIT. +// Code generated with github.com/go-openapi/testify/v2/codegen; DO NOT EDIT. +// Generated on 2026-01-02 (version v1.2.2-760-g97c29e3) using codegen version master [sha: 97c29e3dbfc40800a080863ceea81db0cfd6e858] package assert @@ -1854,7 +1855,7 @@ func TestYAMLEqf(t *testing.T) { Panicsf(t, func() { YAMLEqf(t, "key: value", "key: value", "test message") - }, "should panic without the yaml feature enabled") + }, "should panic without the yaml feature enabled.") }) } diff --git a/assert/assert_forward.go b/assert/assert_forward.go index 384202c0b..3861c5cbb 100644 --- a/assert/assert_forward.go +++ b/assert/assert_forward.go @@ -1,7 +1,8 @@ // SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 -// Code generated with github.com/go-openapi/testify/v2/codegen@master [sha: bb2c19fba6c03f46cb643b3bcdc1d647ea1453ab]; DO NOT EDIT. +// Code generated with github.com/go-openapi/testify/v2/codegen; DO NOT EDIT. +// Generated on 2026-01-02 (version v1.2.2-760-g97c29e3) using codegen version master [sha: 97c29e3dbfc40800a080863ceea81db0cfd6e858] package assert diff --git a/assert/assert_forward_test.go b/assert/assert_forward_test.go index a4a3d0b2e..839495b1e 100644 --- a/assert/assert_forward_test.go +++ b/assert/assert_forward_test.go @@ -1,7 +1,8 @@ // SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 -// Code generated with github.com/go-openapi/testify/v2/codegen@master [sha: bb2c19fba6c03f46cb643b3bcdc1d647ea1453ab]; DO NOT EDIT. +// Code generated with github.com/go-openapi/testify/v2/codegen; DO NOT EDIT. +// Generated on 2026-01-02 (version v1.2.2-760-g97c29e3) using codegen version master [sha: 97c29e3dbfc40800a080863ceea81db0cfd6e858] package assert @@ -4144,7 +4145,7 @@ func TestAssertionsYAMLEq(t *testing.T) { a := New(t) a.Panics(func() { a.YAMLEq("key: value", "key: value") - }, "should panic without the yaml feature enabled") + }, "should panic without the yaml feature enabled.") }) } @@ -4156,7 +4157,7 @@ func TestAssertionsYAMLEqf(t *testing.T) { a := New(t) a.Panics(func() { a.YAMLEqf("key: value", "key: value", "test message") - }, "should panic without the yaml feature enabled") + }, "should panic without the yaml feature enabled.") }) } diff --git a/assert/assert_helpers.go b/assert/assert_helpers.go index 9abad028e..ea9205e9e 100644 --- a/assert/assert_helpers.go +++ b/assert/assert_helpers.go @@ -1,7 +1,8 @@ // SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 -// Code generated with github.com/go-openapi/testify/v2/codegen@master [sha: bb2c19fba6c03f46cb643b3bcdc1d647ea1453ab]; DO NOT EDIT. +// Code generated with github.com/go-openapi/testify/v2/codegen; DO NOT EDIT. +// Generated on 2026-01-02 (version v1.2.2-760-g97c29e3) using codegen version master [sha: 97c29e3dbfc40800a080863ceea81db0cfd6e858] package assert @@ -37,14 +38,3 @@ func ObjectsAreEqual(expected any, actual any) bool { func ObjectsAreEqualValues(expected any, actual any) bool { return assertions.ObjectsAreEqualValues(expected, actual) } - -// 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 any, actual any) bool { - return assertions.ObjectsExportedFieldsAreEqual(expected, actual) -} diff --git a/assert/assert_helpers_test.go b/assert/assert_helpers_test.go index 2462d7469..c86490fe1 100644 --- a/assert/assert_helpers_test.go +++ b/assert/assert_helpers_test.go @@ -1,7 +1,8 @@ // SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 -// Code generated with github.com/go-openapi/testify/v2/codegen@master [sha: bb2c19fba6c03f46cb643b3bcdc1d647ea1453ab]; DO NOT EDIT. +// Code generated with github.com/go-openapi/testify/v2/codegen; DO NOT EDIT. +// Generated on 2026-01-02 (version v1.2.2-760-g97c29e3) using codegen version master [sha: 97c29e3dbfc40800a080863ceea81db0cfd6e858] package assert @@ -24,7 +25,3 @@ func TestObjectsAreEqualf(t *testing.T) { func TestObjectsAreEqualValuesf(t *testing.T) { t.Skip() // this function doesn't have tests yet } - -func TestObjectsExportedFieldsAreEqualf(t *testing.T) { - t.Skip() // this function doesn't have tests yet -} diff --git a/assert/assert_types.go b/assert/assert_types.go index 3d872082f..0bb0068e9 100644 --- a/assert/assert_types.go +++ b/assert/assert_types.go @@ -1,7 +1,8 @@ // SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 -// Code generated with github.com/go-openapi/testify/v2/codegen@master [sha: bb2c19fba6c03f46cb643b3bcdc1d647ea1453ab]; DO NOT EDIT. +// Code generated with github.com/go-openapi/testify/v2/codegen; DO NOT EDIT. +// Generated on 2026-01-02 (version v1.2.2-760-g97c29e3) using codegen version master [sha: 97c29e3dbfc40800a080863ceea81db0cfd6e858] package assert @@ -23,12 +24,9 @@ type ( // for table driven tests. BoolAssertionFunc = assertions.BoolAssertionFunc - // CollectT implements the T interface and collects all errors. + // CollectT implements the [T] interface and collects all errors. CollectT = assertions.CollectT - // Deprecated: CompareType has only ever been for internal use and has accidentally been published since v1.6.0. Do not use it. - CompareType = assertions.CompareType - // Comparison is a custom function that returns true on success and false on failure. Comparison = assertions.Comparison diff --git a/codegen/go.mod b/codegen/go.mod index ba63fc337..ccc5797a9 100644 --- a/codegen/go.mod +++ b/codegen/go.mod @@ -6,6 +6,7 @@ toolchain go1.25.0 require ( github.com/go-openapi/testify/v2 v2.0.2 + golang.org/x/text v0.32.0 golang.org/x/tools v0.40.0 ) diff --git a/codegen/go.sum b/codegen/go.sum index 26ca4bf3b..20135bd44 100644 --- a/codegen/go.sum +++ b/codegen/go.sum @@ -6,5 +6,7 @@ golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= diff --git a/codegen/internal/generator/doc_generator.go b/codegen/internal/generator/doc_generator.go new file mode 100644 index 000000000..67c3cbaab --- /dev/null +++ b/codegen/internal/generator/doc_generator.go @@ -0,0 +1,235 @@ +// SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + +package generator + +import ( + "errors" + "fmt" + "iter" + "os" + "path" + "path/filepath" + + "github.com/go-openapi/testify/v2/codegen/internal/generator/domains" + "github.com/go-openapi/testify/v2/codegen/internal/generator/funcmaps" + "github.com/go-openapi/testify/v2/codegen/internal/model" +) + +const ( + // index page metadata. + indexTitle = "Assertions index" + indexDescription = "Index of assertion domains" + indexFile = "_index.md" + + // sensible default preallocated slots. + allocatedEntries = 15 +) + +type DocGenerator struct { + options + + ctx *genCtx + doc model.Documentation +} + +func NewDocGenerator(doc model.Documentation, opts ...Option) *DocGenerator { + return &DocGenerator{ + options: optionsWithDefaults(opts), + doc: doc, + } +} + +func (d *DocGenerator) Generate(opts ...GenerateOption) error { + // prepare options + d.ctx = &genCtx{ + generateOptions: generateOptionsWithDefaults(opts), + } + if d.ctx.targetDoc == "" { + return errors.New("a target directory is required for docs") + } + + if err := d.loadTemplates(); err != nil { + return err + } + + // Proposal for enhancement: other fun stuff + // - capture testable examples and render their source code + + // reorganize accumulated package-based docs into domain-based docs + // + // This iterator renders all domains in the desired order. + domainDocs, extraUniqueValues := d.reorganizeByDomain() + + // generate an index page with all domains + indexDoc := d.buildIndexDocument(domainDocs, extraUniqueValues) + if err := d.generateDomainIndex(indexDoc); err != nil { + return err + } + + // generate one page per domain. Each document knows about the domain. + for _, document := range domainDocs { + if err := d.generateDomainPage(document); err != nil { + return err + } + } + + return nil +} + +type uniqueValues struct { + tool string + receiver string + copyright string + header string + githubURL string + pkggodevURL string +} + +func (d *DocGenerator) reorganizeByDomain() (iter.Seq2[string, model.Document], uniqueValues) { + docs := domains.FlattenDocumentation(d.doc) + discoveredDomains := domains.MakeDomainIndex(docs) + githubURL := "https://" + path.Dir(discoveredDomains.RootPackage()) + pkggodevURL := "https://pkg.go.dev/" + discoveredDomains.RootPackage() + + return func(yield func(string, model.Document) bool) { + weight := 1 + for domain, entry := range discoveredDomains.Entries() { + doc := model.Document{ + Title: funcmaps.Titleize(domain), + Domain: domain, + Description: entry.Description(), + Kind: model.KindPage, + File: domain + ".md", + Package: &model.AssertionPackage{ + Package: assertions, // package that is the single source of truth + Tool: discoveredDomains.Tool(), + Copyright: discoveredDomains.Copyright(), + Receiver: discoveredDomains.Receiver(), + Header: discoveredDomains.Header(), + EnableFormat: d.ctx.enableFormat, + EnableForward: d.ctx.enableForward, + EnableGenerics: d.ctx.enableGenerics, + EnableExamples: d.ctx.generateExamples, + RunnableExamples: d.ctx.runnableExamples, + // skip package-level docstring + // skip other package-level extra comments + // filtered functions and types for this domain across all packages + Functions: entry.Functions(), + Types: entry.Types(), + Vars: entry.Vars(), + Consts: entry.Consts(), + }, + ExtraPackages: entry.ExtraPackages(), + GitHubURL: githubURL, + PkgGoDevURL: pkggodevURL, + RefCount: entry.Len(), + Weight: weight, + } + weight++ + + if !yield(doc.Domain, doc) { + return + } + } + }, uniqueValues{ + // metadata that are unique + tool: discoveredDomains.Tool(), + receiver: discoveredDomains.Receiver(), + copyright: discoveredDomains.Copyright(), + header: discoveredDomains.Header(), + githubURL: githubURL, + pkggodevURL: pkggodevURL, + } +} + +func (d *DocGenerator) buildIndexDocument(docsByDomain iter.Seq2[string, model.Document], extras uniqueValues) model.Document { + doc := model.Document{ + Title: indexTitle, + Description: indexDescription, + Kind: model.KindIndex, + File: indexFile, + Index: buildIndexEntries(docsByDomain), + Package: &model.AssertionPackage{ + Tool: extras.tool, + Copyright: extras.copyright, + Receiver: extras.receiver, + Header: extras.header, + // skip everything else + }, + GitHubURL: extras.githubURL, + PkgGoDevURL: extras.pkggodevURL, + } + + doc.RefCount = len(doc.Index) + + return doc +} + +func buildIndexEntries(docsByDomain iter.Seq2[string, model.Document]) []model.IndexEntry { + entries := make([]model.IndexEntry, 0, allocatedEntries) + + weight := 1 + for domain, doc := range docsByDomain { + entries = append(entries, model.IndexEntry{ + Name: domain, + Title: doc.Title, + Description: doc.Description, + Link: "./" + doc.File, + RefCount: len(doc.Package.Functions), + Weight: weight, + }) + weight++ + } + + return entries +} + +func (d *DocGenerator) generateDomainIndex(document model.Document) error { + base := filepath.Join(d.ctx.targetRoot, d.ctx.targetDoc, document.Path) + if err := os.MkdirAll(base, dirPermissions); err != nil { + return fmt.Errorf("can't make target folder: %w", err) + } + return d.render("doc_index", filepath.Join(base, document.File), document) +} + +func (d *DocGenerator) generateDomainPage(document model.Document) error { + base := filepath.Join(d.ctx.targetRoot, d.ctx.targetDoc, document.Path) + if err := os.MkdirAll(base, dirPermissions); err != nil { + return fmt.Errorf("can't make target folder: %w", err) + } + + return d.render("doc_page", filepath.Join(base, document.File), document) +} + +func (d *DocGenerator) loadTemplates() error { + const ( + tplExt = ".md.gotmpl" + expectedTemplates = 10 + ) + + index := make(map[string]string, expectedTemplates) + index["doc_index"] = "doc_index" + index["doc_page"] = "doc_page" + + templates, err := loadTemplatesFromIndex(index, tplExt, templatesFS) + if err != nil { + return err + } + + d.ctx.index = index + d.ctx.templates = templates + + return nil +} + +func (d *DocGenerator) render(name string, target string, data any) error { + return renderTemplate( + d.ctx.index, + d.ctx.templates, + name, + target, + data, + renderMD, + ) +} diff --git a/codegen/internal/generator/domains/doc.go b/codegen/internal/generator/domains/doc.go new file mode 100644 index 000000000..39a79b654 --- /dev/null +++ b/codegen/internal/generator/domains/doc.go @@ -0,0 +1,9 @@ +// SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + +// Package domains provides domain-based organization of assertions for documentation generation. +// +// This package handles the reorganization of assertion functions, types, variables, and constants +// from package-based organization (assert, require, internal/assertions) to domain-based organization +// (boolean, collection, error, etc.) for documentation purposes. +package domains diff --git a/codegen/internal/generator/domains/domains.go b/codegen/internal/generator/domains/domains.go new file mode 100644 index 000000000..6e60b8f07 --- /dev/null +++ b/codegen/internal/generator/domains/domains.go @@ -0,0 +1,463 @@ +// SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + +package domains + +import ( + "iter" + "path" + "slices" + "strings" + + "github.com/go-openapi/testify/v2/codegen/internal/model" +) + +const ( + nodomain = "common" + assertions = "assertions" +) + +// FlattenDocumentation flattens a nested documentation structure into a map of packages. +func FlattenDocumentation(documentation model.Documentation) map[string]model.Document { + index := make(map[string]model.Document, len(documentation.Documents)) + + flattenDocuments(index, documentation.Documents) + + return index +} + +func flattenDocuments(index map[string]model.Document, docs []model.Document) { + for _, doc := range docs { + key := doc.Package.Package + if _, ok := index[key]; !ok { + index[key] = doc + } + + flattenDocuments(index, doc.Documents) + } +} + +// Index represents a domain-based index of assertions organized by domain. +type Index struct { + index map[string]Entry + tool string + copyright string + receiver string + rootPackage string + header string +} + +// mapEntry is used for sorting domain entries. +type mapEntry struct { + key string + value Entry +} + +// Entries returns an iterator over domain entries, sorted alphabetically with "common" domain last. +func (idx Index) Entries() iter.Seq2[string, Entry] { + list := make([]mapEntry, 0, len(idx.index)) + for k, v := range idx.index { + list = append(list, mapEntry{key: k, value: v}) + } + + slices.SortFunc(list, compareMapEntries) + + return func(yield func(string, Entry) bool) { + for _, entry := range list { + if !yield(entry.key, entry.value) { + return + } + } + } +} + +// compareMapEntries compares two mapEntry items by their key field. +// +// The "common" domain (nodomain) always sorts last, all others sort alphabetically. +func compareMapEntries(a, b mapEntry) int { + if a.key == nodomain { + if b.key == nodomain { + return 0 + } + + return 1 + } + if b.key == nodomain { + return -1 + } + + return strings.Compare(a.key, b.key) +} + +// Tool returns the tool name. +func (idx Index) Tool() string { + return idx.tool +} + +// Copyright returns the copyright string. +func (idx Index) Copyright() string { + return idx.copyright +} + +// Receiver returns the receiver name. +func (idx Index) Receiver() string { + return idx.receiver +} + +// RootPackage returns the root package path. +func (idx Index) RootPackage() string { + return idx.rootPackage +} + +// Header returns the header prepared by the Scanner. +func (idx Index) Header() string { + return idx.header +} + +// MakeDomainIndex creates a domain-based index from flattened documentation. +func MakeDomainIndex(docs map[string]model.Document) Index { + // collect all functions from all packages (assert, require) + describedDomains := make(map[string]string) + discoveredDomains := make(map[string]Entry) + + var tool, receiver, copyright, rootPackage, header string + + for _, doc := range docs { + data := doc.Package + findDescribedDomains(data, describedDomains) + pkg := data.Package // the generated package + + // unique values resolved once + if tool == "" { + tool = data.Tool + } + if receiver == "" { + receiver = data.Receiver + } + if copyright == "" { + copyright = data.Copyright + } + if rootPackage == "" { + rootPackage = path.Dir(path.Dir(data.Imports[assertions])) + } + if header == "" { + header = data.Header + } + + discoverDomainsInFunctions(pkg, data, discoveredDomains) + discoverDomainsInTypes(pkg, data, discoveredDomains) + discoverDomainsInVariables(pkg, data, discoveredDomains) + discoverDomainsInConstants(pkg, data, discoveredDomains) + } + + // now add descriptions to domains + for domain, description := range describedDomains { + entry, ok := discoveredDomains[domain] + if !ok { + continue + } + entry.description = description + discoveredDomains[domain] = entry + } + + entry, ok := discoveredDomains[nodomain] + if ok { + entry.description = "Other uncategorized helpers" + discoveredDomains[nodomain] = entry + } + + return Index{ + index: discoveredDomains, + tool: tool, + receiver: receiver, + copyright: copyright, + header: header, + rootPackage: rootPackage, + } +} + +// findDescribedDomains look for annotations in the package comments +// that describe assertion domains. +// +// Example: github.com/go-openapi/testify/v2/internal/assertions/doc.go +// +// Comment format: +// +// domain: description +func findDescribedDomains(data *model.AssertionPackage, describedDomains map[string]string) { + for _, tagged := range data.ExtraComments { + if tagged.Tag != model.CommentTagDomainDescription { + continue + } + description := describedDomains[tagged.Key] + if description == "" { + describedDomains[tagged.Key] = tagged.Text + } + } +} + +func discoverDomainsInFunctions(pkg string, data *model.AssertionPackage, discoveredDomains map[string]Entry) { + for _, fn := range data.Functions { + domain := fn.Domain + if domain == "" { + entry := discoveredDomains[nodomain] + entry.AddPackage(pkg, data) + fn.Domain = nodomain + entry.AddFunction(pkg, fn) + discoveredDomains[nodomain] = entry + + continue + } + + entry := discoveredDomains[domain] + entry.AddPackage(pkg, data) + entry.AddFunction(pkg, fn) + discoveredDomains[domain] = entry + } +} + +func discoverDomainsInTypes(pkg string, data *model.AssertionPackage, discoveredDomains map[string]Entry) { + for _, ty := range data.Types { + domain := ty.Domain + if domain == "" { + entry := discoveredDomains[nodomain] + entry.AddPackage(pkg, data) + ty.Domain = nodomain + entry.AddType(pkg, ty) + discoveredDomains[nodomain] = entry + + continue + } + + entry := discoveredDomains[domain] + entry.AddPackage(pkg, data) + entry.AddType(pkg, ty) + discoveredDomains[domain] = entry + } +} + +func discoverDomainsInVariables(pkg string, data *model.AssertionPackage, discoveredDomains map[string]Entry) { + for _, va := range data.Vars { + domain := va.Domain + if domain == "" { + entry := discoveredDomains[nodomain] + entry.AddPackage(pkg, data) + va.Domain = nodomain + entry.AddVariable(pkg, va) + discoveredDomains[nodomain] = entry + + continue + } + + entry := discoveredDomains[domain] + entry.AddPackage(pkg, data) + entry.AddVariable(pkg, va) + discoveredDomains[domain] = entry + } +} + +func discoverDomainsInConstants(pkg string, data *model.AssertionPackage, discoveredDomains map[string]Entry) { + for _, co := range data.Consts { + domain := co.Domain + if domain == "" { + entry := discoveredDomains[nodomain] + entry.AddPackage(pkg, data) + co.Domain = nodomain + entry.AddConst(pkg, co) + discoveredDomains[nodomain] = entry + + continue + } + + entry := discoveredDomains[domain] + entry.AddPackage(pkg, data) + entry.AddConst(pkg, co) + discoveredDomains[domain] = entry + } +} + +type key struct { + pkg string + name string +} + +func makeKey(pkg, name string) key { + return key{ + pkg: pkg, + name: name, + } +} + +// Entry represents a discovered domain entry with associated functions, types, variables, and constants. +type Entry struct { + description string + packages map[key]*model.AssertionPackage + funcs map[key]*model.Function + typeDecls map[key]*model.Ident + varDecls map[key]*model.Ident + constDecls map[key]*model.Ident +} + +func makeEntry() Entry { + return Entry{ + packages: make(map[key]*model.AssertionPackage), + funcs: make(map[key]*model.Function), + typeDecls: make(map[key]*model.Ident), + varDecls: make(map[key]*model.Ident), + constDecls: make(map[key]*model.Ident), + } +} + +// AddPackage adds a package to the entry. +func (e *Entry) AddPackage(pkg string, apkg *model.AssertionPackage) { + if e.packages == nil { + *e = makeEntry() + } + e.packages[makeKey(pkg, apkg.Package)] = apkg +} + +// AddFunction adds a function to the entry. +func (e *Entry) AddFunction(pkg string, fn model.Function) { + val := fn + if e.packages == nil { + *e = makeEntry() + } + e.funcs[makeKey(pkg, fn.Name)] = &val +} + +// AddType adds a type declaration to the entry. +func (e *Entry) AddType(pkg string, id model.Ident) { + val := id + if e.packages == nil { + *e = makeEntry() + } + e.typeDecls[makeKey(pkg, id.Name)] = &val +} + +// AddVariable adds a variable declaration to the entry. +func (e *Entry) AddVariable(pkg string, id model.Ident) { + val := id + if e.packages == nil { + *e = makeEntry() + } + e.varDecls[makeKey(pkg, id.Name)] = &val +} + +// AddConst adds a constant declaration to the entry. +func (e *Entry) AddConst(pkg string, id model.Ident) { + val := id + if e.packages == nil { + *e = makeEntry() + } + e.constDecls[makeKey(pkg, id.Name)] = &val +} + +// ExtraPackages returns the list of extra packages (excluding the assertions package). +// +// This should correspond to the generated packages, e.g. "assert" and "require". +func (e Entry) ExtraPackages() model.ExtraPackages { + if len(e.packages) == 0 { + return nil + } + + result := make(model.ExtraPackages, 0, len(e.packages)-1) + for _, pkg := range e.packages { + if pkg == nil { + // safeguard + continue + } + if path.Base(pkg.Package) == assertions { + continue + } + result = append(result, pkg) + } + slices.SortFunc(result, comparePackages) + + return result +} + +// comparePackages compares two AssertionPackages by their Package field. +func comparePackages(a, b *model.AssertionPackage) int { + return strings.Compare(a.Package, b.Package) +} + +// Functions returns the list of functions in this domain from the assertions package. +func (e Entry) Functions() []model.Function { + result := make([]model.Function, 0, len(e.funcs)) + for key, fn := range e.funcs { + if fn == nil { + // safeguard + continue + } + if path.Base(key.pkg) != assertions { + continue + } + result = append(result, *fn) + } + slices.SortFunc(result, compareFunctions) + + return slices.Clip(result) +} + +// Len returns the number of (filtered) functions for this entry. +func (e Entry) Len() (l int) { + for key, fn := range e.funcs { + if fn == nil { + // safeguard + continue + } + if path.Base(key.pkg) != assertions { + continue + } + l++ + } + + return l +} + +// compareFunctions compares two Functions by their Name field. +func compareFunctions(a, b model.Function) int { + return strings.Compare(a.Name, b.Name) +} + +// Types returns the list of type declarations in this domain. +func (e Entry) Types() []model.Ident { + return e.sortedIdents(e.typeDecls) +} + +// Vars returns the list of variable declarations in this domain. +func (e Entry) Vars() []model.Ident { + return e.sortedIdents(e.varDecls) +} + +// Consts returns the list of constant declarations in this domain. +func (e Entry) Consts() []model.Ident { + return e.sortedIdents(e.constDecls) +} + +// Description returns the description of this domain. +func (e Entry) Description() string { + return e.description +} + +func (e Entry) sortedIdents(idents map[key]*model.Ident) []model.Ident { + result := make([]model.Ident, 0, len(idents)) + for key, id := range idents { + if id == nil { + // safeguard + continue + } + if path.Base(key.pkg) != assertions { + continue + } + result = append(result, *id) + } + slices.SortFunc(result, compareIdents) + + return result +} + +// compareIdents compares two Idents by their Name field. +func compareIdents(a, b model.Ident) int { + return strings.Compare(a.Name, b.Name) +} diff --git a/codegen/internal/generator/domains/domains_test.go b/codegen/internal/generator/domains/domains_test.go new file mode 100644 index 000000000..6f491b186 --- /dev/null +++ b/codegen/internal/generator/domains/domains_test.go @@ -0,0 +1,596 @@ +// SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + +package domains + +import ( + "iter" + "slices" + "testing" + + "github.com/go-openapi/testify/v2/codegen/internal/model" +) + +const ( + testPackage = "github.com/go-openapi/testify/v2/internal/assertions" + testRepo = "github.com/go-openapi/testify/v2" + testAssertPackage = "github.com/go-openapi/testify/v2/assert" + testRequirePackage = "github.com/go-openapi/testify/v2/require" +) + +// TestFlattenDocumentation verifies documentation flattening. +func TestFlattenDocumentation(t *testing.T) { + t.Parallel() + + for c := range flattenDocumentationCases() { + t.Run(c.name, func(t *testing.T) { + t.Parallel() + + flattened := FlattenDocumentation(c.doc) + + if len(flattened) != c.expectedCount { + t.Errorf("Expected %d packages, got %d", c.expectedCount, len(flattened)) + } + + for _, pkg := range c.expectedPackages { + if _, ok := flattened[pkg]; !ok { + t.Errorf("Missing package %s", pkg) + } + } + }) + } +} + +// TestMakeDomainIndex verifies domain index creation. +func TestMakeDomainIndex(t *testing.T) { + t.Parallel() + + for c := range makeDomainIndexCases() { + t.Run(c.name, func(t *testing.T) { + t.Parallel() + + index := MakeDomainIndex(c.docs) + + // Verify metadata + if c.checkMetadata != nil { + c.checkMetadata(t, index) + } + + // Verify domains + if c.checkDomains != nil { + c.checkDomains(t, index) + } + }) + } +} + +// TestDomainIndexSorting verifies that domains are sorted correctly. +func TestDomainIndexSorting(t *testing.T) { + t.Parallel() + + for c := range domainIndexSortingCases() { + t.Run(c.name, func(t *testing.T) { + t.Parallel() + + index := MakeDomainIndex(c.docs) + + // Collect domain names in order + var domains []string + for domain := range index.Entries() { + domains = append(domains, domain) + } + + // Verify order + if len(domains) != len(c.expectedOrder) { + t.Fatalf("Expected %d domains, got %d: %v", len(c.expectedOrder), len(domains), domains) + } + + for i, expected := range c.expectedOrder { + if domains[i] != expected { + t.Errorf("Position %d: expected %q, got %q", i, expected, domains[i]) + } + } + }) + } +} + +// TestEntry_AddMethods verifies entry mutation methods. +func TestEntry_AddMethods(t *testing.T) { + t.Parallel() + + for c := range entryAddMethodsCases() { + t.Run(c.name, func(t *testing.T) { + t.Parallel() + + var entry Entry + + // Execute the add operation + c.addFunc(&entry) + + // Verify the result + c.checkFunc(t, &entry) + }) + } +} + +// TestEntry_ExtraPackages verifies extra package filtering. +func TestEntry_ExtraPackages(t *testing.T) { + t.Parallel() + + for c := range entryExtraPackagesCases() { + t.Run(c.name, func(t *testing.T) { + t.Parallel() + + var entry Entry + + // Add packages + for _, pkg := range c.packages { + entry.AddPackage(pkg.name, pkg.pkg) + } + + extras := entry.ExtraPackages() + + // Verify count + if len(extras) != c.expectedCount { + t.Errorf("Expected %d extra packages, got %d", c.expectedCount, len(extras)) + } + + // Verify order and content + for i, expectedPkg := range c.expectedPackages { + if i >= len(extras) { + break + } + if extras[i].Package != expectedPkg { + t.Errorf("Expected package[%d] to be %s, got %s", i, expectedPkg, extras[i].Package) + } + } + }) + } +} + +// TestEntry_Functions verifies function filtering and sorting. +func TestEntry_Functions(t *testing.T) { + t.Parallel() + + for c := range entryFunctionsCases() { + t.Run(c.name, func(t *testing.T) { + t.Parallel() + + var entry Entry + + // Add functions + for _, fn := range c.functions { + entry.AddFunction(fn.pkg, fn.fn) + } + + funcs := entry.Functions() + + // Verify count + if len(funcs) != c.expectedCount { + t.Errorf("Expected %d functions, got %d", c.expectedCount, len(funcs)) + } + + // Verify order + for i, expectedName := range c.expectedNames { + if i >= len(funcs) { + break + } + if funcs[i].Name != expectedName { + t.Errorf("Expected function[%d] to be %s, got %s", i, expectedName, funcs[i].Name) + } + } + }) + } +} + +/* Test case iterators */ + +type flattenDocumentationCase struct { + name string + doc model.Documentation + expectedCount int + expectedPackages []string +} + +func flattenDocumentationCases() iter.Seq[flattenDocumentationCase] { + return slices.Values([]flattenDocumentationCase{ + { + name: "nested documentation structure", + doc: func() model.Documentation { + pkg1 := model.New() + pkg1.Package = "pkg1" + + pkg2 := model.New() + pkg2.Package = "pkg2" + + pkg3 := model.New() + pkg3.Package = "pkg3" + + doc := model.NewDocumentation() + doc.Documents = []model.Document{ + { + Package: pkg1, + Documents: []model.Document{ + {Package: pkg2}, + }, + }, + {Package: pkg3}, + } + + return *doc + }(), + expectedCount: 3, + expectedPackages: []string{"pkg1", "pkg2", "pkg3"}, + }, + }) +} + +type makeDomainIndexCase struct { + name string + docs map[string]model.Document + checkMetadata func(*testing.T, Index) + checkDomains func(*testing.T, Index) +} + +//nolint:gocognit,gocyclo,cyclop // this is temporary accepted extra complexity. Should refactor with externalized asserting functions +func makeDomainIndexCases() iter.Seq[makeDomainIndexCase] { + return slices.Values([]makeDomainIndexCase{ + { + name: "complete domain index with metadata and domains", + docs: func() map[string]model.Document { + assertPkg := model.New() + assertPkg.Package = testPackage + assertPkg.Tool = "testify-codegen" + assertPkg.Copyright = "Copyright 2025" + assertPkg.Receiver = "Assertions" + assertPkg.Imports = model.ImportMap{ + "assertions": testPackage, + } + + assertPkg.Functions = []model.Function{ + {Name: "Equal", Domain: "equal"}, + {Name: "True", Domain: "boolean"}, + {Name: "Helper"}, // no domain + } + + assertPkg.Types = []model.Ident{ + {Name: "TestingT", Domain: "testing"}, + {Name: "H"}, // no domain + } + + assertPkg.Vars = []model.Ident{ + {Name: "SomeVar", Domain: "boolean"}, + {Name: "SomeOtherVar"}, // no domain + } + + assertPkg.Consts = []model.Ident{ + {Name: "SomeConst", Domain: "equal"}, + {Name: "SomeOtherConst"}, // no domain + } + + assertPkg.ExtraComments = []model.ExtraComment{ + { + Tag: model.CommentTagDomainDescription, + Key: "equal", + Text: "Equality assertions", + }, + { + Tag: model.CommentTagDomainDescription, + Key: "boolean", + Text: "Boolean assertions", + }, + { + Tag: model.CommentTagMaintainer, + Key: "author", + Text: "Some Author", // Should be ignored by domain indexer + }, + } + + return map[string]model.Document{ + testPackage: {Package: assertPkg}, + } + }(), + checkMetadata: func(t *testing.T, index Index) { + t.Helper() + + if index.Tool() != "testify-codegen" { + t.Errorf("Expected tool 'testify-codegen', got %q", index.Tool()) + } + if index.Copyright() != "Copyright 2025" { + t.Errorf("Expected copyright 'Copyright 2025', got %q", index.Copyright()) + } + if index.Receiver() != "Assertions" { + t.Errorf("Expected receiver 'Assertions', got %q", index.Receiver()) + } + if index.RootPackage() != testRepo { + t.Errorf("Expected root package '%s', got %q", testRepo, index.RootPackage()) + } + }, + checkDomains: func(t *testing.T, index Index) { + t.Helper() + + domainCount := 0 + for domain, entry := range index.Entries() { + domainCount++ + + switch domain { + case "equal": + if entry.Description() != "Equality assertions" { + t.Errorf("Expected description 'Equality assertions', got %q", entry.Description()) + } + if len(entry.Functions()) != 1 || entry.Functions()[0].Name != "Equal" { + t.Error("Expected Equal function in equal domain") + } + if len(entry.Consts()) != 1 || entry.Consts()[0].Name != "SomeConst" { + t.Error("Expected SomeConst in equal domain") + } + + case "boolean": + if entry.Description() != "Boolean assertions" { + t.Errorf("Expected description 'Boolean assertions', got %q", entry.Description()) + } + if len(entry.Functions()) != 1 || entry.Functions()[0].Name != "True" { + t.Error("Expected True function in boolean domain") + } + if len(entry.Vars()) != 1 || entry.Vars()[0].Name != "SomeVar" { + t.Error("Expected SomeVar in boolean domain") + } + + case "testing": + if len(entry.Types()) != 1 || entry.Types()[0].Name != "TestingT" { + t.Error("Expected TestingT type in testing domain") + } + + case "common": + if entry.Description() != "Other uncategorized helpers" { + t.Errorf("Expected description 'Other uncategorized helpers', got %q", entry.Description()) + } + if len(entry.Functions()) != 1 || entry.Functions()[0].Name != "Helper" { + t.Error("Expected Helper function in common domain") + } + if len(entry.Types()) != 1 || entry.Types()[0].Name != "H" { + t.Error("Expected H type in common domain") + } + if len(entry.Vars()) != 1 || entry.Vars()[0].Name != "SomeOtherVar" { + t.Error("Expected SomeOtherVar in common domain") + } + if len(entry.Consts()) != 1 || entry.Consts()[0].Name != "SomeOtherConst" { + t.Error("Expected SomeOtherConst in common domain") + } + + default: + t.Errorf("Unexpected domain: %s", domain) + } + } + + if domainCount != 4 { + t.Errorf("Expected 4 domains, got %d", domainCount) + } + }, + }, + { + name: "dangling domain description without declarations", + docs: func() map[string]model.Document { + pkg := model.New() + pkg.Package = testPackage + pkg.Imports = model.ImportMap{ + "assertions": testPackage, + } + + pkg.Functions = []model.Function{ + {Name: "Equal", Domain: "equal"}, + } + + pkg.ExtraComments = []model.ExtraComment{ + { + Tag: model.CommentTagDomainDescription, + Key: "equal", + Text: "Equality assertions", + }, + { + Tag: model.CommentTagDomainDescription, + Key: "phantom", + Text: "This domain has no associated declarations", // Dangling description + }, + } + + return map[string]model.Document{ + testPackage: {Package: pkg}, + } + }(), + checkDomains: func(t *testing.T, index Index) { + t.Helper() + + domainCount := 0 + for domain := range index.Entries() { + domainCount++ + if domain == "phantom" { + t.Error("Dangling domain 'phantom' should not appear in entries") + } + } + + if domainCount != 1 { + t.Errorf("Expected 1 domain (equal), got %d", domainCount) + } + }, + }, + }) +} + +type domainIndexSortingCase struct { + name string + docs map[string]model.Document + expectedOrder []string +} + +func domainIndexSortingCases() iter.Seq[domainIndexSortingCase] { + return slices.Values([]domainIndexSortingCase{ + { + name: "alphabetical with common last", + docs: func() map[string]model.Document { + pkg := model.New() + pkg.Package = testPackage + pkg.Imports = model.ImportMap{ + "assertions": testPackage, + } + + pkg.Functions = []model.Function{ + {Name: "ZFunc", Domain: "zebra"}, + {Name: "AFunc", Domain: "alpha"}, + {Name: "MFunc", Domain: "middle"}, + {Name: "NoFunc"}, // no domain -> common + } + + return map[string]model.Document{ + testPackage: {Package: pkg}, + } + }(), + expectedOrder: []string{"alpha", "middle", "zebra", "common"}, + }, + }) +} + +type entryAddMethodsCase struct { + name string + addFunc func(*Entry) + checkFunc func(*testing.T, *Entry) +} + +func entryAddMethodsCases() iter.Seq[entryAddMethodsCase] { + return slices.Values([]entryAddMethodsCase{ + { + name: "add package", + addFunc: func(e *Entry) { + pkg := model.New() + pkg.Package = "test" + e.AddPackage("pkg", pkg) + }, + checkFunc: func(t *testing.T, e *Entry) { + t.Helper() + if len(e.packages) != 1 { + t.Error("Package not added") + } + }, + }, + { + name: "add function", + addFunc: func(e *Entry) { + fn := model.Function{Name: "TestFunc"} + e.AddFunction("pkg", fn) + }, + checkFunc: func(t *testing.T, e *Entry) { + t.Helper() + if len(e.funcs) != 1 { + t.Error("Function not added") + } + }, + }, + { + name: "add type", + addFunc: func(e *Entry) { + ty := model.Ident{Name: "TestType"} + e.AddType("pkg", ty) + }, + checkFunc: func(t *testing.T, e *Entry) { + t.Helper() + if len(e.typeDecls) != 1 { + t.Error("Type not added") + } + }, + }, + { + name: "add variable", + addFunc: func(e *Entry) { + vr := model.Ident{Name: "TestVar"} + e.AddVariable("pkg", vr) + }, + checkFunc: func(t *testing.T, e *Entry) { + t.Helper() + if len(e.varDecls) != 1 { + t.Error("Variable not added") + } + }, + }, + { + name: "add const", + addFunc: func(e *Entry) { + co := model.Ident{Name: "TestConst"} + e.AddConst("pkg", co) + }, + checkFunc: func(t *testing.T, e *Entry) { + t.Helper() + if len(e.constDecls) != 1 { + t.Error("Const not added") + } + }, + }, + }) +} + +type packageToAdd struct { + name string + pkg *model.AssertionPackage +} + +type entryExtraPackagesCase struct { + name string + packages []packageToAdd + expectedCount int + expectedPackages []string +} + +func entryExtraPackagesCases() iter.Seq[entryExtraPackagesCase] { + return slices.Values([]entryExtraPackagesCase{ + { + name: "filter assertions package and sort", + packages: func() []packageToAdd { + assertionsPkg := model.New() + assertionsPkg.Package = testPackage + + assertPkg := model.New() + assertPkg.Package = testAssertPackage + + requirePkg := model.New() + requirePkg.Package = testRequirePackage + + return []packageToAdd{ + {name: "assertions", pkg: assertionsPkg}, + {name: "assert", pkg: assertPkg}, + {name: "require", pkg: requirePkg}, + } + }(), + expectedCount: 2, + expectedPackages: []string{ + testAssertPackage, + testRequirePackage, + }, + }, + }) +} + +type functionToAdd struct { + pkg string + fn model.Function +} + +type entryFunctionsCase struct { + name string + functions []functionToAdd + expectedCount int + expectedNames []string +} + +func entryFunctionsCases() iter.Seq[entryFunctionsCase] { + return slices.Values([]entryFunctionsCase{ + { + name: "filter by assertions package and sort alphabetically", + functions: []functionToAdd{ + {pkg: "assertions", fn: model.Function{Name: "Zebra"}}, + {pkg: "assertions", fn: model.Function{Name: "Alpha"}}, + {pkg: "assert", fn: model.Function{Name: "Other"}}, // filtered out + }, + expectedCount: 2, + expectedNames: []string{"Alpha", "Zebra"}, + }, + }) +} diff --git a/codegen/internal/generator/domains/sorting_test.go b/codegen/internal/generator/domains/sorting_test.go new file mode 100644 index 000000000..28a2dbbd2 --- /dev/null +++ b/codegen/internal/generator/domains/sorting_test.go @@ -0,0 +1,232 @@ +// SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + +package domains + +import ( + "iter" + "slices" + "testing" + + "github.com/go-openapi/testify/v2/codegen/internal/model" +) + +// TestCompareIdents verifies Ident comparison by Name. +func TestCompareIdents(t *testing.T) { + t.Parallel() + + for c := range compareIdentsCase() { + t.Run(c.name, func(t *testing.T) { + t.Parallel() + + result := compareIdents(c.a, c.b) + + if (result < 0 && !c.aLessThanB) || (result > 0 && c.aLessThanB) || (result == 0 && c.aLessThanB) { + t.Errorf("compareIdents(%q, %q) = %d, expected a < b: %v", c.a.Name, c.b.Name, result, c.aLessThanB) + } + }) + } +} + +// TestCompareFunctions verifies Function comparison by Name. +func TestCompareFunctions(t *testing.T) { + t.Parallel() + + for c := range compareFunctionsCase() { + t.Run(c.name, func(t *testing.T) { + t.Parallel() + + result := compareFunctions(c.a, c.b) + + if (result < 0 && !c.aLessThanB) || (result > 0 && c.aLessThanB) || (result == 0 && c.aLessThanB) { + t.Errorf("compareFunctions(%q, %q) = %d, expected a < b: %v", c.a.Name, c.b.Name, result, c.aLessThanB) + } + }) + } +} + +// TestComparePackages verifies AssertionPackage comparison by Package field. +func TestComparePackages(t *testing.T) { + t.Parallel() + + for c := range comparePackagesCase() { + t.Run(c.name, func(t *testing.T) { + t.Parallel() + + result := comparePackages(c.a, c.b) + + if (result < 0 && !c.aLessThanB) || (result > 0 && c.aLessThanB) || (result == 0 && c.aLessThanB) { + t.Errorf("comparePackages(%q, %q) = %d, expected a < b: %v", c.a.Package, c.b.Package, result, c.aLessThanB) + } + }) + } +} + +// TestCompareMapEntries verifies mapEntry comparison with special "common" domain handling. +func TestCompareMapEntries(t *testing.T) { + t.Parallel() + + for c := range compareMapEntriesCase() { + t.Run(c.name, func(t *testing.T) { + t.Parallel() + + result := compareMapEntries(c.a, c.b) + + if (result < 0 && !c.aLessThanB) || (result > 0 && c.aLessThanB) || (result == 0 && c.aLessThanB) { + t.Errorf("compareMapEntries(%q, %q) = %d, expected a < b: %v", c.a.key, c.b.key, result, c.aLessThanB) + } + }) + } +} + +/* Test case iterators */ + +type compareIdentsTestCase struct { + name string + a model.Ident + b model.Ident + aLessThanB bool +} + +func compareIdentsCase() iter.Seq[compareIdentsTestCase] { + return slices.Values([]compareIdentsTestCase{ + { + name: "a < b alphabetically", + a: model.Ident{Name: "Alpha"}, + b: model.Ident{Name: "Beta"}, + aLessThanB: true, + }, + { + name: "a > b alphabetically", + a: model.Ident{Name: "Zebra"}, + b: model.Ident{Name: "Alpha"}, + aLessThanB: false, + }, + { + name: "a == b", + a: model.Ident{Name: "Equal"}, + b: model.Ident{Name: "Equal"}, + aLessThanB: false, + }, + }) +} + +type compareFunctionsTestCase struct { + name string + a model.Function + b model.Function + aLessThanB bool +} + +func compareFunctionsCase() iter.Seq[compareFunctionsTestCase] { + return slices.Values([]compareFunctionsTestCase{ + { + name: "a < b alphabetically", + a: model.Function{Name: "Contains"}, + b: model.Function{Name: "Equal"}, + aLessThanB: true, + }, + { + name: "a > b alphabetically", + a: model.Function{Name: "True"}, + b: model.Function{Name: "False"}, + aLessThanB: false, + }, + { + name: "a == b", + a: model.Function{Name: "Empty"}, + b: model.Function{Name: "Empty"}, + aLessThanB: false, + }, + }) +} + +type comparePackagesTestCase struct { + name string + a *model.AssertionPackage + b *model.AssertionPackage + aLessThanB bool +} + +func comparePackagesCase() iter.Seq[comparePackagesTestCase] { + return slices.Values([]comparePackagesTestCase{ + { + name: "a < b alphabetically", + a: &model.AssertionPackage{ + Package: "github.com/go-openapi/testify/v2/assert", + }, + b: &model.AssertionPackage{ + Package: "github.com/go-openapi/testify/v2/require", + }, + aLessThanB: true, + }, + { + name: "a > b alphabetically", + a: &model.AssertionPackage{ + Package: "github.com/some/pkg/z", + }, + b: &model.AssertionPackage{ + Package: "github.com/some/pkg/a", + }, + aLessThanB: false, + }, + { + name: "a == b", + a: &model.AssertionPackage{ + Package: "github.com/go-openapi/testify/v2/assert", + }, + b: &model.AssertionPackage{ + Package: "github.com/go-openapi/testify/v2/assert", + }, + aLessThanB: false, + }, + }) +} + +type compareMapEntriesTestCase struct { + name string + a mapEntry + b mapEntry + aLessThanB bool +} + +func compareMapEntriesCase() iter.Seq[compareMapEntriesTestCase] { + return slices.Values([]compareMapEntriesTestCase{ + { + name: "both regular domains, a < b", + a: mapEntry{key: "alpha"}, + b: mapEntry{key: "beta"}, + aLessThanB: true, + }, + { + name: "both regular domains, a > b", + a: mapEntry{key: "zebra"}, + b: mapEntry{key: "alpha"}, + aLessThanB: false, + }, + { + name: "both regular domains, a == b", + a: mapEntry{key: "equal"}, + b: mapEntry{key: "equal"}, + aLessThanB: false, + }, + { + name: "a is common domain, b is regular (common sorts last)", + a: mapEntry{key: "common"}, + b: mapEntry{key: "alpha"}, + aLessThanB: false, // common > alpha (sorts after) + }, + { + name: "a is regular, b is common domain (common sorts last)", + a: mapEntry{key: "zebra"}, + b: mapEntry{key: "common"}, + aLessThanB: true, // zebra < common (common sorts after) + }, + { + name: "both are common domain", + a: mapEntry{key: "common"}, + b: mapEntry{key: "common"}, + aLessThanB: false, + }, + }) +} diff --git a/codegen/internal/generator/funcmap.go b/codegen/internal/generator/funcmap.go deleted file mode 100644 index e780c1831..000000000 --- a/codegen/internal/generator/funcmap.go +++ /dev/null @@ -1,201 +0,0 @@ -package generator - -import ( - "fmt" - "path" - "path/filepath" - "regexp" - "sort" - "strings" - "text/template" - - "github.com/go-openapi/testify/v2/codegen/internal/model" -) - -func funcMap() template.FuncMap { - return map[string]any{ - "imports": printImports, - "comment": comment, - "params": params, - "forward": forward, - "docStringFor": docStringFor, - "docStringPackage": docStringPackage, - "returns": printReturns, - "concat": concatStrings, - "pathparts": pathParts, - "relocate": relocate, - "hasSuffix": strings.HasSuffix, - } -} - -func printImports(in model.ImportMap) string { - list := make([]string, 0, len(in)) - - for k, v := range in { - if k == path.Base(v) { - // no alias - list = append(list, "\t\""+v+"\"") - - continue - } - - list = append(list, "\t"+k+"\t\""+v+"\"") - } - - sort.Strings(list) - - return strings.Join(list, "\n") -} - -func comment(str string) string { - if str == "" { - return "" - } - - lines := rTrimEmpty(strings.Split(str, "\n")) - - return "// " + strings.Join(lines, "\n// ") -} - -func rTrimEmpty(lines []string) []string { - var i int - for i = len(lines) - 1; i >= 0; i-- { - if lines[i] != "" { - break - } - } - - return lines[:i+1] -} - -func params(args model.Parameters) string { - var b strings.Builder - l := len(args) - - if l == 0 { - return "" - } - - b.WriteString(args[0].Name) - b.WriteByte(' ') - if strings.HasSuffix(args[0].GoType, "Func") && args[0].Selector == assertions { - b.WriteString(args[0].Selector + ".") // for xxxFunc types, backward-compatibility imposes to use the "assert-like" type definition - } - b.WriteString(args[0].GoType) - - for _, v := range args[1:] { - b.WriteString(", ") - b.WriteString(v.Name) - b.WriteByte(' ') - if strings.HasSuffix(v.GoType, "Func") && v.Selector == assertions { - b.WriteString(v.Selector + ".") // for xxxFunc types, backward-compatibility imposes to use the "assert-like" type definition - } - b.WriteString(v.GoType) - } - - return b.String() -} - -func forward(args model.Parameters) string { - var b strings.Builder - l := len(args) - - if l == 0 { - return "" - } - - b.WriteString(args[0].Name) - if args[0].IsVariadic { - b.WriteString("...") - } - - for _, v := range args[1:] { - b.WriteString(", ") - b.WriteString(v.Name) - if v.IsVariadic { - b.WriteString("...") - } - } - - return b.String() -} - -// docStringFor adds an extra comment for context. -func docStringFor(usage, name string) string { - parts := strings.Split(name, ".") - basename := parts[len(parts)-1] - - switch usage { - case "format": - return comment( - fmt.Sprintf( - "%sf is the same as [%s], but accepts a format msg string to format arguments like [fmt.Printf].", - basename, - name, - ), - ) - case "forward": - return comment( - fmt.Sprintf( - "%s is the same as [%s], as a method rather than a package-level function.", - basename, - name), - ) - default: - return "" - } -} - -// docStringPackage adds an additional comment specific to the target package. -func docStringPackage(pkg string) string { - switch pkg { - case pkgAssert: - return `// Upon failure, the test [T] is marked as failed and continues execution.` - case pkgRequire: - return `// Upon failure, the test [T] is marked as failed and stops execution.` - default: - return "" - } -} - -func printReturns(in model.Parameters) string { - return fmt.Sprintf("%v", in) -} - -func concatStrings(in ...string) string { - return strings.Join(in, "") -} - -func pathParts(in string) string { - parts := strings.FieldsFunc(filepath.Clean(in), func(r rune) bool { return r == filepath.Separator }) - - for i := range parts { - parts[i] = fmt.Sprintf("%q", parts[i]) - } - - return strings.Join(parts, ",") -} - -// relocate handles special cases in testable examples. -// -// It rewrites specific values to be run from a {{ .Package }}_test external package. -// -// Since there are only a few such edge cases, we hard-code this is this funcmap -// rather than indulging into a full-fleged go value parsing and package relocating. -func relocate(in, pkg string) string { - if pkg == "" { - return in - } - - replaced := errTestRex.ReplaceAllString(in, "${1}"+pkg+".${2}") - replaced = collectTRex.ReplaceAllString(replaced, "${1}"+pkg+".${2}") - replaced = trueRex.ReplaceAllString(replaced, "${1}"+pkg+".${2}") - - return replaced -} - -var ( - errTestRex = regexp.MustCompile(`(^|[^\.])(ErrTest)`) - collectTRex = regexp.MustCompile(`(^|[^\.])(CollectT)`) - trueRex = regexp.MustCompile(`(^|[^\.])(True\(c)`) -) diff --git a/codegen/internal/generator/funcmaps/doc.go b/codegen/internal/generator/funcmaps/doc.go new file mode 100644 index 000000000..420a75df6 --- /dev/null +++ b/codegen/internal/generator/funcmaps/doc.go @@ -0,0 +1,11 @@ +// SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + +// Package funcmaps provides template functions for code and documentation generation. +// +// This package contains all template helper functions used by both the code generator +// (for assert/require packages) and the documentation generator (for markdown docs). +// +// The main entry point is FuncMap(), which returns a complete template.FuncMap +// with all available functions registered. +package funcmaps diff --git a/codegen/internal/generator/funcmaps/funcmaps.go b/codegen/internal/generator/funcmaps/funcmaps.go new file mode 100644 index 000000000..f56e6cc95 --- /dev/null +++ b/codegen/internal/generator/funcmaps/funcmaps.go @@ -0,0 +1,346 @@ +// SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + +package funcmaps + +import ( + "fmt" + "go/token" + "net/url" + "path" + "path/filepath" + "sort" + "strings" + "text/template" + "time" + + "golang.org/x/text/cases" + "golang.org/x/text/language" + + "github.com/go-openapi/testify/v2/codegen/internal/model" + parser "github.com/go-openapi/testify/v2/codegen/internal/scanner/comments-parser" + "github.com/go-openapi/testify/v2/internal/spew" +) + +const ( + pkgRequire = "require" + pkgAssert = "assert" + assertions = "assertions" +) + +// Titleize transform a string into a title, upper-casing the first letter. +func Titleize(in string) string { + return titleize(in) +} + +// PrintReturns print a collection of [model.Parameters] as values returned by a function. +func PrintReturns(vars model.Parameters) string { + return printReturns(vars) +} + +// FuncMap returns the complete template function map for code and documentation generation. +func FuncMap() template.FuncMap { + return map[string]any{ + "comment": comment, + "concat": concatStrings, + "date": printDate, + "debug": printDebug, + "docStringFor": docStringFor, + "docStringPackage": docStringPackage, + "forward": forward, + "godocbadge": godocbadge, + "hasSuffix": strings.HasSuffix, + "imports": printImports, + "mdformat": FormatMarkdown, // From markdown.go + "params": params, + "pathparts": pathParts, + "quote": quote, + "relocate": relocate, + "returns": PrintReturns, + "sourceLink": sourceLink, + "titleize": titleize, + } +} + +func printImports(in model.ImportMap) string { + list := make([]string, 0, len(in)) + + for k, v := range in { + if k == path.Base(v) { + // no alias + list = append(list, "\t\""+v+"\"") + + continue + } + + list = append(list, "\t"+k+"\t\""+v+"\"") + } + + sort.Strings(list) + + return strings.Join(list, "\n") +} + +func comment(str string) string { + if str == "" { + return "" + } + + lines := rTrimEmpty(strings.Split(str, "\n")) + + return "// " + strings.Join(lines, "\n// ") +} + +func rTrimEmpty(lines []string) []string { + var i int + for i = len(lines) - 1; i >= 0; i-- { + if lines[i] != "" { + break + } + } + + return lines[:i+1] +} + +func params(args model.Parameters) string { + var b strings.Builder + l := len(args) + + if l == 0 { + return "" + } + + b.WriteString(args[0].Name) + b.WriteByte(' ') + if strings.HasSuffix(args[0].GoType, "Func") && args[0].Selector == assertions { + b.WriteString(args[0].Selector + ".") // for xxxFunc types, backward-compatibility imposes to use the "assert-like" type definition + } + b.WriteString(args[0].GoType) + + for _, v := range args[1:] { + b.WriteString(", ") + b.WriteString(v.Name) + b.WriteByte(' ') + if strings.HasSuffix(v.GoType, "Func") && v.Selector == assertions { + b.WriteString(v.Selector + ".") // for xxxFunc types, backward-compatibility imposes to use the "assert-like" type definition + } + b.WriteString(v.GoType) + } + + return b.String() +} + +func forward(args model.Parameters) string { + var b strings.Builder + l := len(args) + + if l == 0 { + return "" + } + + b.WriteString(args[0].Name) + if args[0].IsVariadic { + b.WriteString("...") + } + + for _, v := range args[1:] { + b.WriteString(", ") + b.WriteString(v.Name) + if v.IsVariadic { + b.WriteString("...") + } + } + + return b.String() +} + +func printReturns(vars model.Parameters) string { + var b strings.Builder + l := len(vars) + + if l == 0 { + return "" + } + + if l > 1 || vars[0].Name != "" { + b.WriteByte('(') + } + + if vars[0].Name != "" { + b.WriteString(vars[0].Name) + b.WriteByte(' ') + } + b.WriteString(vars[0].GoType) + + for _, v := range vars[1:] { + b.WriteByte(',') + b.WriteByte(' ') + if v.Name != "" { + b.WriteString(v.Name) + b.WriteByte(' ') + } + b.WriteString(v.GoType) + + } + + if l > 1 || vars[0].Name != "" { + b.WriteByte(')') + } + + return b.String() +} + +// docStringFor adds an extra comment with usage-specific context. +// +// Supported usage strings are: +// +// - format: usage with a format argument +// - forward: usage as a method. +func docStringFor(usage, name string) string { + parts := strings.Split(name, ".") + basename := parts[len(parts)-1] + + switch usage { + case "format": + return comment( + fmt.Sprintf( + "%sf is the same as [%s], but accepts a format msg string to format arguments like [fmt.Printf].", + basename, + name, + ), + ) + case "forward": + return comment( + fmt.Sprintf( + "%s is the same as [%s], as a method rather than a package-level function.", + basename, + name), + ) + default: + return "" + } +} + +// docStringPackage adds an additional comment specific to the target package. +// +// Supported pkg strings are: +// +// - assert +// - require. +func docStringPackage(pkg string) string { + switch pkg { + case pkgAssert: + return `// Upon failure, the test [T] is marked as failed and continues execution.` + case pkgRequire: + return `// Upon failure, the test [T] is marked as failed and stops execution.` + // NOTE(fredbi): + // Proposal for enhancement: add more packages, e.g. for the generics-only API + default: + return "" + } +} + +func concatStrings(in ...string) string { + return strings.Join(in, "") +} + +func pathParts(in string) string { + parts := strings.FieldsFunc(filepath.Clean(in), func(r rune) bool { return r == filepath.Separator }) + + for i := range parts { + parts[i] = fmt.Sprintf("%q", parts[i]) + } + + return strings.Join(parts, ",") +} + +// relocate relocates already-parsed test values from the "assertions" package to the target package. +// +// It uses AST-based relocation to properly handle all Go expressions, replacing +// package selectors and qualifying unqualified identifiers as needed. +// +// Examples: +// - assertions.ErrTest → assert.ErrTest +// - ErrTest → assert.ErrTest (adds qualifier) +// - assertions.PanicTestFunc → assertions.PanicTestFunc (exception) +func relocate(values []model.TestValue, pkg string) string { + if len(values) == 0 { + return "" + } + + if pkg == "" { + // Return original values as string + parts := make([]string, 0, len(values)) + for _, v := range values { + parts = append(parts, v.Raw) + } + return strings.Join(parts, ", ") + } + + // Relocate each value + relocated := make([]string, 0, len(values)) + for _, tv := range values { + // If parse failed, use original (fallback) + if tv.Error != nil { + relocated = append(relocated, tv.Raw) + continue + } + + // Relocate from "assertions" to target package + relocatedTV := parser.RelocateTestValue(tv, assertions, pkg) + + // If relocation failed, use original (fallback) + if relocatedTV.Error != nil { + relocated = append(relocated, tv.Raw) + } else { + relocated = append(relocated, relocatedTV.Raw) + } + } + + // Join back with comma-space + return strings.Join(relocated, ", ") +} + +func sourceLink(baseGitHubURL string, pos *token.Position) string { + if pos == nil { + return "" + } + + dir, file := filepath.Split(pos.Filename) + l1 := filepath.Base(dir) + l2 := filepath.Base(filepath.Dir(filepath.Dir(dir))) + filename := path.Join(l2, l1, file) + return fmt.Sprintf("%s/blob/master/%s#L%d", + baseGitHubURL, + filename, + pos.Line, + ) +} + +func quote(in string) string { + return fmt.Sprintf("%q", in) +} + +func titleize(in string) string { + caser := cases.Title(language.English, cases.NoLower) // the case is stateful: cannot declare it globally + + return caser.String(in) +} + +func godocbadge(pkggodevURL string) (string, error) { + u, err := url.Parse(pkggodevURL) + if err != nil { + return "", err + } + u.Path = path.Join("badge", u.Path) + + return u.String(), nil +} + +func printDebug(in any) string { + return spew.Sdump(in) +} + +func printDate() string { + return time.Now().Format(time.DateOnly) +} diff --git a/codegen/internal/generator/funcmaps/funcmaps_test.go b/codegen/internal/generator/funcmaps/funcmaps_test.go new file mode 100644 index 000000000..46ce06f89 --- /dev/null +++ b/codegen/internal/generator/funcmaps/funcmaps_test.go @@ -0,0 +1,815 @@ +// SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + +package funcmaps + +import ( + "fmt" + "go/token" + "iter" + "slices" + "testing" + + "github.com/go-openapi/testify/v2/codegen/internal/model" + parser "github.com/go-openapi/testify/v2/codegen/internal/scanner/comments-parser" +) + +const ( + testPackage = "github.com/go-openapi/testify/v2/internal/assertions" + testRepo = "github.com/go-openapi/testify/v2" + testAssertPackage = "github.com/go-openapi/testify/v2/assert" + testRequirePackage = "github.com/go-openapi/testify/v2/require" + githubRepo = "https://github.com/go-openapi/testify" +) + +// TestFuncMap verifies that FuncMap returns all expected functions. +func TestFuncMap(t *testing.T) { + t.Parallel() + + fm := FuncMap() + + expectedFuncs := []string{ + "imports", "comment", "date", "params", "forward", "docStringFor", "docStringPackage", + "returns", "concat", "pathparts", "relocate", "hasSuffix", "sourceLink", + "titleize", "quote", "mdformat", "godocbadge", "debug", + } + + for _, name := range expectedFuncs { + if _, ok := fm[name]; !ok { + t.Errorf("FuncMap missing expected function: %q", name) + } + } +} + +// TestPrintImports verifies import formatting. +func TestPrintImports(t *testing.T) { + t.Parallel() + + for c := range printImportsCases() { + t.Run(c.name, func(t *testing.T) { + t.Parallel() + + result := printImports(c.input) + + if result != c.expected { + t.Errorf("Expected:\n%s\n\nGot:\n%s", c.expected, result) + } + }) + } +} + +// TestComment verifies comment formatting. +func TestComment(t *testing.T) { + t.Parallel() + + for c := range commentCases() { + t.Run(c.name, func(t *testing.T) { + t.Parallel() + + result := comment(c.input) + + if result != c.expected { + t.Errorf("Expected: %q, Got: %q", c.expected, result) + } + }) + } +} + +// TestParams verifies parameter formatting. +func TestParams(t *testing.T) { + t.Parallel() + + for c := range paramsCases() { + t.Run(c.name, func(t *testing.T) { + t.Parallel() + + result := params(c.input) + + if result != c.expected { + t.Errorf("Expected: %q, Got: %q", c.expected, result) + } + }) + } +} + +// TestForward verifies forward parameter formatting. +func TestForward(t *testing.T) { + t.Parallel() + + for c := range forwardCases() { + t.Run(c.name, func(t *testing.T) { + t.Parallel() + + result := forward(c.input) + + if result != c.expected { + t.Errorf("Expected: %q, Got: %q", c.expected, result) + } + }) + } +} + +// TestPrintReturns verifies return value formatting. +func TestPrintReturns(t *testing.T) { + t.Parallel() + + for c := range printReturnsCases() { + t.Run(c.name, func(t *testing.T) { + t.Parallel() + + result := PrintReturns(c.input) + + if result != c.expected { + t.Errorf("Expected: %q, Got: %q", c.expected, result) + } + }) + } +} + +// TestDocStringFor verifies usage-specific doc string generation. +func TestDocStringFor(t *testing.T) { + t.Parallel() + + for c := range docStringForCases() { + t.Run(c.name, func(t *testing.T) { + t.Parallel() + + result := docStringFor(c.usage, c.funcName) + + if result != c.expected { + t.Errorf("Expected: %q, Got: %q", c.expected, result) + } + }) + } +} + +// TestDocStringPackage verifies package-specific doc string generation. +func TestDocStringPackage(t *testing.T) { + t.Parallel() + + for c := range docStringPackageCases() { + t.Run(c.name, func(t *testing.T) { + t.Parallel() + + result := docStringPackage(c.pkg) + + if result != c.expected { + t.Errorf("Expected: %q, Got: %q", c.expected, result) + } + }) + } +} + +// TestConcatStrings verifies string concatenation. +func TestConcatStrings(t *testing.T) { + t.Parallel() + + for c := range concatStringsCases() { + t.Run(c.name, func(t *testing.T) { + t.Parallel() + + result := concatStrings(c.input...) + + if result != c.expected { + t.Errorf("Expected: %q, Got: %q", c.expected, result) + } + }) + } +} + +// TestPathParts verifies path splitting and quoting. +func TestPathParts(t *testing.T) { + t.Parallel() + + for c := range pathPartsCases() { + t.Run(c.name, func(t *testing.T) { + t.Parallel() + + result := pathParts(c.input) + + if result != c.expected { + t.Errorf("Expected: %q, Got: %q", c.expected, result) + } + }) + } +} + +// TestRelocate verifies special value relocation for testable examples. +func TestRelocate(t *testing.T) { + t.Parallel() + + for c := range relocateCases() { + t.Run(c.name, func(t *testing.T) { + t.Parallel() + + // Parse input string into TestValue slice + values := parser.ParseTestValues(c.input) + result := relocate(values, c.pkg) + + if result != c.expected { + t.Errorf("Expected: %q, Got: %q", c.expected, result) + } + }) + } +} + +// TestSourceLink verifies GitHub source link generation. +func TestSourceLink(t *testing.T) { + t.Parallel() + + for c := range sourceLinkCases() { + t.Run(c.name, func(t *testing.T) { + t.Parallel() + + result := sourceLink(c.baseURL, c.pos) + + if result != c.expected { + t.Errorf("Expected: %q, Got: %q", c.expected, result) + } + }) + } +} + +// TestQuote verifies string quoting. +func TestQuote(t *testing.T) { + t.Parallel() + + for c := range quoteCases() { + t.Run(c.name, func(t *testing.T) { + t.Parallel() + + result := quote(c.input) + + if result != c.expected { + t.Errorf("Expected: %q, Got: %q", c.expected, result) + } + }) + } +} + +// TestTitleize verifies title casing. +func TestTitleize(t *testing.T) { + t.Parallel() + + for c := range titleizeCases() { + t.Run(c.name, func(t *testing.T) { + t.Parallel() + + result := Titleize(c.input) + + if result != c.expected { + t.Errorf("Expected: %q, Got: %q", c.expected, result) + } + }) + } +} + +// TestGodocbadge verifies pkg.go.dev badge URL generation. +func TestGodocbadge(t *testing.T) { + t.Parallel() + + for c := range godocbadgeCases() { + t.Run(c.name, func(t *testing.T) { + t.Parallel() + + result, err := godocbadge(c.input) + + if c.expectError { + if err == nil { + t.Error("Expected error but got nil") + } + } else { + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + if result != c.expected { + t.Errorf("Expected: %q, Got: %q", c.expected, result) + } + } + }) + } +} + +func TestDebug(t *testing.T) { + t.Parallel() + + result := printDebug(1) + const expected = "(int) 1\n" + if result != expected { + t.Errorf("Expected: %q, Got: %q", expected, result) + } +} + +func TestData(t *testing.T) { + t.Parallel() + + result := printDate() + if result == "" { + t.Errorf("Expected a non-empty string, Got: %q", result) + } +} + +/* Test case iterators */ + +type printImportsCase struct { + name string + input model.ImportMap + expected string +} + +func printImportsCases() iter.Seq[printImportsCase] { + return slices.Values([]printImportsCase{ + { + name: "empty imports", + input: model.ImportMap{}, + expected: "", + }, + { + name: "single import without alias", + input: model.ImportMap{ + "testing": "testing", + }, + expected: "\t\"testing\"", + }, + { + name: "single import with alias", + input: model.ImportMap{ + "req": testRequirePackage, + }, + expected: fmt.Sprintf("\treq\t%q", testRequirePackage), + }, + { + name: "multiple imports sorted alphabetically", + input: model.ImportMap{ + "testing": "testing", + "assertions": testPackage, + "fmt": "fmt", + }, + // Sorted by the full formatted string, not package name + // No alias for assertions since it matches path.Base + expected: fmt.Sprintf("\t\"fmt\"\n\t%q\n\t\"testing\"", testPackage), + }, + }) +} + +type commentCase struct { + name string + input string + expected string +} + +func commentCases() iter.Seq[commentCase] { + return slices.Values([]commentCase{ + { + name: "empty string", + input: "", + expected: "", + }, + { + name: "single line", + input: "This is a comment", + expected: "// This is a comment", + }, + { + name: "multiline", + input: "Line 1\nLine 2\nLine 3", + expected: "// Line 1\n// Line 2\n// Line 3", + }, + { + name: "trailing empty lines trimmed", + input: "Line 1\nLine 2\n\n\n", + expected: "// Line 1\n// Line 2", + }, + }) +} + +type paramsCase struct { + name string + input model.Parameters + expected string +} + +func paramsCases() iter.Seq[paramsCase] { + return slices.Values([]paramsCase{ + { + name: "empty parameters", + input: model.Parameters{}, + expected: "", + }, + { + name: "single parameter", + input: model.Parameters{ + {Name: "t", GoType: "TestingT"}, + }, + expected: "t TestingT", + }, + { + name: "multiple parameters", + input: model.Parameters{ + {Name: "expected", GoType: "any"}, + {Name: "actual", GoType: "any"}, + }, + expected: "expected any, actual any", + }, + { + name: "parameter with Func type and assertions selector", + input: model.Parameters{ + {Name: "fn", GoType: "ComparisonAssertionFunc", Selector: "assertions"}, + }, + expected: "fn assertions.ComparisonAssertionFunc", + }, + { + name: "variadic parameter as last argument", + input: model.Parameters{ + {Name: "t", GoType: "TestingT"}, + {Name: "expected", GoType: "any"}, + {Name: "msgAndArgs", GoType: "...any", IsVariadic: true}, + }, + expected: "t TestingT, expected any, msgAndArgs ...any", + }, + { + name: "variadic parameter as unique argument", + input: model.Parameters{ + {Name: "msgAndArgs", GoType: "...any", IsVariadic: true}, + }, + expected: "msgAndArgs ...any", + }, + { + name: "special handling of xxxFunc type as unique argument", + input: model.Parameters{ + {Name: "fn", GoType: "PanicFunc", Selector: "assertions"}, + }, + expected: "fn assertions.PanicFunc", + }, + { + name: "special handling of xxxFunc type with multiple arguments", + input: model.Parameters{ + {Name: "t", GoType: "TestingT"}, + {Name: "fn", GoType: "PanicFunc", Selector: "assertions"}, + }, + expected: "t TestingT, fn assertions.PanicFunc", + }, + }) +} + +type forwardCase struct { + name string + input model.Parameters + expected string +} + +func forwardCases() iter.Seq[forwardCase] { + return slices.Values([]forwardCase{ + { + name: "empty parameters", + input: model.Parameters{}, + expected: "", + }, + { + name: "single parameter", + input: model.Parameters{ + {Name: "expected"}, + }, + expected: "expected", + }, + { + name: "multiple parameters", + input: model.Parameters{ + {Name: "expected"}, + {Name: "actual"}, + {Name: "msgAndArgs"}, + }, + expected: "expected, actual, msgAndArgs", + }, + { + name: "variadic parameter as last argument", + input: model.Parameters{ + {Name: "expected"}, + {Name: "msgAndArgs", IsVariadic: true}, + }, + expected: "expected, msgAndArgs...", + }, + { + name: "variadic parameter as unique argument", + input: model.Parameters{ + {Name: "msgAndArgs", GoType: "...any", IsVariadic: true}, + }, + expected: "msgAndArgs...", + }, + }) +} + +type printReturnsCase struct { + name string + input model.Parameters + expected string +} + +func printReturnsCases() iter.Seq[printReturnsCase] { + return slices.Values([]printReturnsCase{ + { + name: "no returns", + input: model.Parameters{}, + expected: "", + }, + { + name: "single unnamed return", + input: model.Parameters{ + {GoType: "bool"}, + }, + expected: "bool", + }, + { + name: "single named return", + input: model.Parameters{ + {Name: "success", GoType: "bool"}, + }, + expected: "(success bool)", + }, + { + name: "multiple unnamed returns", + input: model.Parameters{ + {GoType: "string"}, + {GoType: "error"}, + }, + expected: "(string, error)", + }, + { + name: "multiple named returns", + input: model.Parameters{ + {Name: "result", GoType: "string"}, + {Name: "err", GoType: "error"}, + }, + expected: "(result string, err error)", + }, + }) +} + +type docStringForCase struct { + name string + usage string + funcName string + expected string +} + +func docStringForCases() iter.Seq[docStringForCase] { + return slices.Values([]docStringForCase{ + { + name: "format usage", + usage: "format", + funcName: "assert.Equal", + expected: "// Equalf is the same as [assert.Equal], but accepts a format msg string to format arguments like [fmt.Printf].", + }, + { + name: "forward usage", + usage: "forward", + funcName: "assert.Equal", + expected: "// Equal is the same as [assert.Equal], as a method rather than a package-level function.", + }, + { + name: "unknown usage", + usage: "unknown", + funcName: "assert.Equal", + expected: "", + }, + }) +} + +type docStringPackageCase struct { + name string + pkg string + expected string +} + +func docStringPackageCases() iter.Seq[docStringPackageCase] { + return slices.Values([]docStringPackageCase{ + { + name: "assert package", + pkg: "assert", + expected: `// Upon failure, the test [T] is marked as failed and continues execution.`, + }, + { + name: "require package", + pkg: "require", + expected: `// Upon failure, the test [T] is marked as failed and stops execution.`, + }, + { + name: "unknown package", + pkg: "unknown", + expected: "", + }, + }) +} + +type concatStringsCase struct { + name string + input []string + expected string +} + +func concatStringsCases() iter.Seq[concatStringsCase] { + return slices.Values([]concatStringsCase{ + { + name: "empty strings", + input: []string{}, + expected: "", + }, + { + name: "single string", + input: []string{"hello"}, + expected: "hello", + }, + { + name: "multiple strings", + input: []string{"hello", " ", "world"}, + expected: "hello world", + }, + }) +} + +type pathPartsCase struct { + name string + input string + expected string +} + +func pathPartsCases() iter.Seq[pathPartsCase] { + return slices.Values([]pathPartsCase{ + { + name: "simple path", + input: "foo/bar/baz", + expected: `"foo","bar","baz"`, + }, + { + name: "absolute path", + input: "/usr/local/bin", + expected: `"usr","local","bin"`, + }, + { + name: "single component", + input: "test", + expected: `"test"`, + }, + }) +} + +type relocateCase struct { + name string + input string + pkg string + expected string +} + +func relocateCases() iter.Seq[relocateCase] { + return slices.Values([]relocateCase{ + { + name: "empty package does nothing", + input: "ErrTest{}, CollectT{}, True(c)", + pkg: "", + expected: "ErrTest{}, CollectT{}, True(c)", + }, + { + name: "relocates ErrTest", + input: "ErrTest{}", + pkg: "assert", + expected: "assert.ErrTest{}", + }, + { + name: "relocates CollectT", + input: "CollectT{}", + pkg: "assert", + expected: "assert.CollectT{}", + }, + { + name: "relocates True(c)", + input: "True(c)", + pkg: "assert", + expected: "assert.True(c)", + }, + { + name: "does not relocate with dot prefix", + input: "t.ErrTest{}", + pkg: "assert", + expected: "t.ErrTest{}", + }, + { + name: "relocates all three", + input: "ErrTest{}, CollectT{}, True(c)", + pkg: "assert", + expected: "assert.ErrTest{}, assert.CollectT{}, assert.True(c)", + }, + }) +} + +type sourceLinkCase struct { + name string + baseURL string + pos *token.Position + expected string +} + +func sourceLinkCases() iter.Seq[sourceLinkCase] { + return slices.Values([]sourceLinkCase{ + { + name: "nil position", + baseURL: githubRepo, + pos: nil, + expected: "", + }, + { + name: "valid position", + baseURL: githubRepo, + pos: &token.Position{ + Filename: "/home/user/go/src/github.com/go-openapi/testify/internal/assertions/equal.go", + Line: 42, + }, + expected: githubRepo + "/blob/master/internal/assertions/equal.go#L42", + }, + }) +} + +type quoteCase struct { + name string + input string + expected string +} + +func quoteCases() iter.Seq[quoteCase] { + return slices.Values([]quoteCase{ + { + name: "simple string", + input: "hello", + expected: `"hello"`, + }, + { + name: "string with quotes", + input: `he said "hello"`, + expected: `"he said \"hello\""`, + }, + { + name: "empty string", + input: "", + expected: `""`, + }, + }) +} + +type titleizeCase struct { + name string + input string + expected string +} + +func titleizeCases() iter.Seq[titleizeCase] { + return slices.Values([]titleizeCase{ + { + name: "lowercase", + input: "hello world", + expected: "Hello World", + }, + { + name: "mixed case keeps original case", + input: "hELLo WoRLD", + expected: "HELLo WoRLD", + }, + { + name: "upper-cased keeps original case", + input: "HTTP", + expected: "HTTP", + }, + { + name: "already titlecase", + input: "Hello World", + expected: "Hello World", + }, + { + name: "empty string", + input: "", + expected: "", + }, + }) +} + +type godocbadgeCase struct { + name string + input string + expected string + expectError bool +} + +func godocbadgeCases() iter.Seq[godocbadgeCase] { + return slices.Values([]godocbadgeCase{ + { + name: "valid URL", + input: "https://pkg.go.dev/github.com/go-openapi/testify/v2", + expected: "https://pkg.go.dev/badge/github.com/go-openapi/testify/v2", + expectError: false, + }, + { + name: "invalid URL", + input: "://invalid", + expected: "", + expectError: true, + }, + }) +} diff --git a/codegen/internal/generator/funcmaps/markdown.go b/codegen/internal/generator/funcmaps/markdown.go new file mode 100644 index 000000000..c6fa9c96c --- /dev/null +++ b/codegen/internal/generator/funcmaps/markdown.go @@ -0,0 +1,144 @@ +// SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + +package funcmaps + +import ( + "fmt" + "regexp" + "strings" +) + +var refLinkPattern = regexp.MustCompile(`(?m)^\[([^\]]+)\]:\s+(.+)$`) + +// FormatMarkdown carries out a formatting of inline markdown in comments that handles: +// +// 1. Reference-style markdown links: [text]: url +// 2. Godoc-style links: [errors.Is], [testing.T], etc. +// +//nolint:gocognit,gocognit // this is temporary accepted extra complexity. Should refactor with steps as functions +func FormatMarkdown(in string) string { + const sensiblePrealloc = 20 + + // Step 1: Extract reference-style link definitions + // Pattern: [text]: url (at start of line or after whitespace) + refLinks := make(map[string]string) + + // Extract all reference links + matches := refLinkPattern.FindAllStringSubmatch(in, -1) + const expectedGroups = 2 + for _, match := range matches { + if len(match) == expectedGroups+1 { + refLinks[match[1]] = match[2] + } + } + + // Remove reference link definitions from input + processed := refLinkPattern.ReplaceAllString(in, "") + + // Step 2: Convert reference-style links to inline links + // Replace [text] with [text](url) where we have the reference + usedRefs := make(map[string]bool) + for refText, refURL := range refLinks { + // Pattern: [text] not followed by ( or : + pattern := regexp.MustCompile(`\[` + regexp.QuoteMeta(refText) + `\]([^(\[]|$)`) + if pattern.MatchString(processed) { + processed = pattern.ReplaceAllString(processed, fmt.Sprintf("[%s](%s)$1", refText, refURL)) + usedRefs[refText] = true + } + } + + // Collect unused reference links to append later (dangling references) + var danglingRefs []string + for refText, refURL := range refLinks { + if !usedRefs[refText] { + danglingRefs = append(danglingRefs, fmt.Sprintf("[%s]: %s", refText, refURL)) + } + } + + // Step 3: Convert godoc-style links to pkg.go.dev URLs + // Pattern: [package.Type] - must contain a dot and only valid identifier chars + // We process line by line to avoid matching already-converted links + lines := strings.Split(processed, "\n") + for i, line := range lines { + // Skip if line already has inline links (contains ](http) + if strings.Contains(line, "](") { + continue + } + + // Find godoc-style links: [word.word] + godocPattern := regexp.MustCompile(`\[([a-zA-Z0-9_]+\.[a-zA-Z0-9_.]+)\]`) + lines[i] = godocPattern.ReplaceAllStringFunc(line, func(match string) string { + identifier := strings.Trim(match, "[]") + + // Has package qualifier - link to pkg.go.dev + const expectedParts = 2 + parts := strings.SplitN(identifier, ".", expectedParts) + if len(parts) != expectedParts { + // Defensive code: This should never happen with the current regex pattern, + // which requires at least one dot. If this triggers, it indicates a bug in + // the regex pattern that needs to be fixed. + panic(fmt.Errorf("internal error: godoc pattern matched %q but split into %d parts instead of %d", + identifier, len(parts), expectedParts)) + } + + pkgPath := parts[0] + symbol := parts[1] + + // Assume standard library for simple package names + return fmt.Sprintf("[%s](https://pkg.go.dev/%s#%s)", identifier, pkgPath, symbol) + }) + } + processed = strings.Join(lines, "\n") + + // Step 4: Do existing processing (Hugo shortcodes) + result := make([]string, 0, sensiblePrealloc) + expanded := false + tab := false + + for line := range strings.SplitSeq(processed, "\n") { + if expanded && len(strings.TrimSpace(line)) == 0 { + continue + } + + if strings.HasPrefix(line, "#") { + if !expanded { + result = append(result, `{{% expand title="Examples" %}}`) + result = append(result, `{{< tabs >}}`) + expanded = true + } + + title := Titleize(strings.TrimLeft(line, "# \t")) + if tab { + result = append(result, "```") + result = append(result, `{{< /tab >}}`) + } + + result = append(result, fmt.Sprintf(`{{%% tab title="%s" %%}}`, title)) + result = append(result, "```go") + tab = true + + continue + } + + result = append(result, line) + } + + if tab { + result = append(result, "```") + result = append(result, `{{< /tab >}}`) + } + + if expanded { + result = append(result, `{{< /tabs >}}`) + result = append(result, `{{% /expand %}}`) + + // Append dangling reference links as additional context + if len(danglingRefs) > 0 { + result = append(result, "") + result = append(result, danglingRefs...) + } + } + + return strings.Join(result, "\n") +} diff --git a/codegen/internal/generator/funcmaps/markdown_test.go b/codegen/internal/generator/funcmaps/markdown_test.go new file mode 100644 index 000000000..258de28fa --- /dev/null +++ b/codegen/internal/generator/funcmaps/markdown_test.go @@ -0,0 +1,159 @@ +// SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + +package funcmaps + +import ( + "strings" + "testing" +) + +func TestMarkdownFormatEnhanced(t *testing.T) { + tests := []struct { + name string + input string + contains []string // strings that should appear in output + notContains []string // strings that should NOT appear + }{ + { + name: "reference-style markdown link (dangling)", + input: `Empty asserts that the given value is "empty". + +# Usage + + assertions.Empty(t, obj) + +# Examples + + success: "" + failure: "not empty" +[Zero values]: https://go.dev/ref/spec#The_zero_value`, + contains: []string{ + `[Zero values]: https://go.dev/ref/spec#The_zero_value`, // Dangling ref appears after expand + `{{% expand title="Examples" %}}`, + `{{< tabs >}}`, + `{{% /expand %}}`, + }, + notContains: []string{ + // The reference shouldn't appear inside code blocks + }, + }, + { + name: "godoc-style link", + input: `EqualError asserts that a function returned an error equal to the provided error. + +This is a wrapper for [errors.Is]. + +# Usage + + assertions.EqualError(t, err, expectedErrorString)`, + contains: []string{ + `[errors.Is](https://pkg.go.dev/errors#Is)`, + }, + notContains: []string{ + `[errors.Is].`, // Should be converted + }, + }, + { + name: "multiple godoc links", + input: `See [testing.T] and [fmt.Printf] for details. + +# Usage + + assertions.Something(t)`, + contains: []string{ + `[testing.T](https://pkg.go.dev/testing#T)`, + `[fmt.Printf](https://pkg.go.dev/fmt#Printf)`, + }, + }, + { + name: "mixed markdown and godoc links (dangling ref)", + input: `Uses [errors.Is] to check errors. + +# Examples + + success: nil + failure: ErrTest +[error wrapping]: https://go.dev/blog/go1.13-errors`, + contains: []string{ + `[errors.Is](https://pkg.go.dev/errors#Is)`, + `[error wrapping]: https://go.dev/blog/go1.13-errors`, // Dangling ref + }, + notContains: []string{ + // Nothing + }, + }, + { + name: "used reference link", + input: `Empty asserts values are empty. + +See [Zero values] for definition. + +# Examples + + success: "" +[Zero values]: https://go.dev/ref/spec#The_zero_value`, + contains: []string{ + `[Zero values](https://go.dev/ref/spec#The_zero_value)`, // Converted to inline + }, + notContains: []string{ + `[Zero values]:`, // Definition removed since it was used + }, + }, + { + name: "identifier without package qualifier unchanged", + input: `This function uses [T] as a type parameter. + +It also references [TestingT] interface. + +# Usage + + assertions.Something[T TestingT](t)`, + contains: []string{ + `[T]`, // No package qualifier, left unchanged + `[TestingT]`, // No package qualifier, left unchanged + }, + notContains: []string{ + `[T](https://`, // Should NOT be converted to a link + `[TestingT](https://`, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := FormatMarkdown(tt.input) + + for _, want := range tt.contains { + if !strings.Contains(result, want) { + t.Errorf("FormatMarkdown() missing expected string:\nwant: %q\ngot: %s", want, result) + } + } + + for _, notWant := range tt.notContains { + if strings.Contains(result, notWant) { + t.Errorf("FormatMarkdown() contains unexpected string:\ndon't want: %q\ngot: %s", notWant, result) + } + } + }) + } +} + +func TestMarkdownFormatEnhanced_Output(t *testing.T) { + input := `Empty asserts that the given value is "empty". + +Zero values are "empty". + +# Usage + + assertions.Empty(t, obj) + +# Examples + + success: "" + failure: "not empty" +[Zero values]: https://go.dev/ref/spec#The_zero_value` + + result := FormatMarkdown(input) + t.Logf("Output:\n%s", result) +} diff --git a/codegen/internal/generator/generator.go b/codegen/internal/generator/generator.go index bd63989d2..d11e002d0 100644 --- a/codegen/internal/generator/generator.go +++ b/codegen/internal/generator/generator.go @@ -4,18 +4,15 @@ package generator import ( - "bytes" "embed" "errors" "fmt" "os" "path" "path/filepath" - "sort" "text/template" "github.com/go-openapi/testify/v2/codegen/internal/model" - "golang.org/x/tools/imports" ) const ( @@ -25,14 +22,12 @@ const ( ) const ( - dirPermissions = 0750 - filePermissions = 0600 + dirPermissions = 0o750 + filePermissions = 0o600 ) -var ( - //go:embed templates/*.gotmpl - templatesFS embed.FS -) +//go:embed templates/*.gotmpl +var templatesFS embed.FS // Generator generates testify packages (assert, require) from the internal assertions package. type Generator struct { @@ -40,6 +35,7 @@ type Generator struct { source *model.AssertionPackage ctx *genCtx + docs model.Documentation } type genCtx struct { @@ -48,6 +44,7 @@ type genCtx struct { index map[string]string templates map[string]*template.Template target *model.AssertionPackage + docs *model.Documentation targetBase string } @@ -63,15 +60,24 @@ func (g *Generator) Generate(opts ...GenerateOption) error { return err } defer func() { + if g.ctx.docs != nil { + g.docs = *g.ctx.docs // before we leave, stash the transformed documentation for the current package + } g.ctx = nil }() - if err := g.loadTemplates(); err != nil { - return err - } + { + // prepare steps - if err := g.transformModel(); err != nil { - return err + if err := g.loadTemplates(); err != nil { + return err + } + + if err := g.transformModel(); err != nil { + return err + } + + g.buildDocs() } { @@ -122,8 +128,18 @@ func (g *Generator) Generate(opts ...GenerateOption) error { return err } - return g.generateHelpersTests() + if err := g.generateHelpersTests(); err != nil { + return err + } } + + return nil +} + +// Documentation yields the transformed package model as a [model.Documentation] +// usable by a generation step by the [DocGenerator]. +func (g *Generator) Documentation() model.Documentation { + return g.docs } func (g *Generator) initContext(opts []GenerateOption) error { @@ -148,7 +164,11 @@ func (g *Generator) initContext(opts []GenerateOption) error { } func (g *Generator) loadTemplates() error { - const expectedTemplates = 10 + const ( + tplExt = ".gotmpl" + expectedTemplates = 10 + ) + index := make(map[string]string, expectedTemplates) switch g.ctx.targetBase { @@ -160,24 +180,14 @@ func (g *Generator) loadTemplates() error { panic(fmt.Errorf("internal error: invalid targetBase: %q", g.ctx.targetBase)) } - g.ctx.index = index - needed := make([]string, 0, len(index)) - for _, v := range index { - needed = append(needed, v) - } - sort.Strings(needed) - - g.ctx.templates = make(map[string]*template.Template, len(needed)) - for _, name := range needed { - file := name + ".gotmpl" - tpl, err := template.New(file).Funcs(funcMap()).ParseFS(templatesFS, path.Join("templates", file)) - if err != nil { - return fmt.Errorf("failed to load template %q from %q: %w", name, file, err) - } - - g.ctx.templates[name] = tpl + templates, err := loadTemplatesFromIndex(index, tplExt, templatesFS) + if err != nil { + return err } + g.ctx.index = index + g.ctx.templates = templates + return nil } @@ -268,6 +278,9 @@ func (g *Generator) transformModel() error { return err } + // NOTE: the use of [filepath.Rel] here imposes a constraint that the target resides on the same + // drive as the source. This may cause issues, e.g. on windows (currently, our CI ensures any temp file + // resides on the same drive as the source). testdata, err := filepath.Rel(filepath.Join(absRoot, g.ctx.targetBase), g.source.TestDataPath) if err != nil { return err @@ -408,28 +421,16 @@ func (g *Generator) generateHelpersTests() error { } func (g *Generator) render(name string, target string, data any) error { - tplName, ok := g.ctx.index[name] - if !ok { - panic(fmt.Errorf("internal error: expect template name %q", name)) - } - - tpl, ok := g.ctx.templates[tplName] - if !ok { - panic(fmt.Errorf("internal error: expect template %q", name)) - } - - var buffer bytes.Buffer - if err := tpl.Execute(&buffer, data); err != nil { - return fmt.Errorf("error executing template %q (%s): %w", name, tplName, err) - } - - formatted, err := imports.Process(target, buffer.Bytes(), g.ctx.formatOptions) - if err != nil { - _ = os.WriteFile(target, buffer.Bytes(), filePermissions) - return fmt.Errorf("error formatting go code: %w:%w", err, fmt.Errorf("details available at: %v", target)) - } - - return os.WriteFile(target, formatted, filePermissions) + return renderTemplate( + g.ctx.index, + g.ctx.templates, + name, + target, + data, + func(tpl *template.Template, target string, data any) error { + return render(tpl, target, data, g.ctx.formatOptions) + }, + ) } func (g *Generator) transformArgs(in model.Parameters) model.Parameters { @@ -442,3 +443,40 @@ func (g *Generator) transformArgs(in model.Parameters) model.Parameters { return in } + +// buildDocs builds a hierarchy of documents for the current package. +// +// Documents are NOT rendered by the [Generator]. +func (g *Generator) buildDocs() { + if !g.ctx.generateDoc { + return + } + + docs := model.NewDocumentation() + docs.Package = g.ctx.target + + // create a single document representing this package's contribution. + // + // This document will be reorganized by domain by the [DocGenerator] later on. + // The [DocGenerator] carries out the heavy lifting when building a layout-ready Document. + doc := model.Document{ + Title: g.ctx.target.Package, + Path: g.ctx.targetBase, // "assert" or "require" + Kind: model.KindFolder, + Package: g.ctx.target, + } + + source := model.Document{ + Title: g.source.Package, + Path: assertions, + Kind: model.KindFolder, + Package: g.source, + } + + docs.Documents = []model.Document{ + doc, + source, + } + + g.ctx.docs = docs +} diff --git a/codegen/internal/generator/generator_test.go b/codegen/internal/generator/generator_test.go index d6dda42e7..170d3d1db 100644 --- a/codegen/internal/generator/generator_test.go +++ b/codegen/internal/generator/generator_test.go @@ -8,6 +8,7 @@ import ( "slices" "testing" + "github.com/go-openapi/testify/v2/codegen/internal/generator/funcmaps" "github.com/go-openapi/testify/v2/codegen/internal/model" ) @@ -76,7 +77,7 @@ func TestTransformArgs(t *testing.T) { t.Parallel() params := gen.transformArgs(tt.input) - result := params.String() + result := funcmaps.PrintReturns(params) if result != tt.expected { t.Errorf("Expected %q, got %q", tt.expected, result) diff --git a/codegen/internal/generator/options.go b/codegen/internal/generator/options.go index 3a5d3686c..84b8d06aa 100644 --- a/codegen/internal/generator/options.go +++ b/codegen/internal/generator/options.go @@ -13,9 +13,9 @@ type GenerateOption func(*generateOptions) // Currently unused (reserved for future use). type Option func(*options) -func WithTargetRoot(root string) GenerateOption { +func WithTargetRoot(dir string) GenerateOption { return func(o *generateOptions) { - o.targetRoot = root + o.targetRoot = dir } } @@ -25,6 +25,12 @@ func WithTargetPackage(pkg string) GenerateOption { } } +func WithTargetDoc(dir string) GenerateOption { + return func(o *generateOptions) { + o.targetDoc = dir + } +} + func WithIncludeFormatFuncs(enabled bool) GenerateOption { return func(o *generateOptions) { o.enableFormat = enabled @@ -67,9 +73,16 @@ func WithRunnableExamples(enabled bool) GenerateOption { } } +func WithIncludeDoc(enabled bool) GenerateOption { + return func(o *generateOptions) { + o.generateDoc = enabled + } +} + type generateOptions struct { targetPkg string targetRoot string + targetDoc string enableForward bool enableFormat bool enableGenerics bool @@ -77,21 +90,23 @@ type generateOptions struct { generateTests bool generateExamples bool runnableExamples bool + generateDoc bool formatOptions *imports.Options } -type options struct { -} +type options struct{} func generateOptionsWithDefaults(opts []GenerateOption) generateOptions { o := generateOptions{ targetRoot: ".", + targetDoc: ".", enableForward: true, enableFormat: true, enableGenerics: false, generateHelpers: true, generateTests: false, generateExamples: false, + generateDoc: false, runnableExamples: false, } diff --git a/codegen/internal/generator/render.go b/codegen/internal/generator/render.go new file mode 100644 index 000000000..9c2411e90 --- /dev/null +++ b/codegen/internal/generator/render.go @@ -0,0 +1,36 @@ +package generator + +import ( + "bytes" + "fmt" + "os" + "text/template" + + "golang.org/x/tools/imports" +) + +func render(tpl *template.Template, target string, data any, o *imports.Options) error { + var buffer bytes.Buffer + + if err := tpl.Execute(&buffer, data); err != nil { + return fmt.Errorf("error executing template %q: %w", tpl.Name(), err) + } + + formatted, err := imports.Process(target, buffer.Bytes(), o) + if err != nil { + _ = os.WriteFile(target, buffer.Bytes(), filePermissions) + return fmt.Errorf("error formatting go code: %w:%w", err, fmt.Errorf("details available at: %v", target)) + } + + return os.WriteFile(target, formatted, filePermissions) +} + +func renderMD(tpl *template.Template, target string, data any) error { + var buffer bytes.Buffer + + if err := tpl.Execute(&buffer, data); err != nil { + return fmt.Errorf("error executing template %q: %w", tpl.Name(), err) + } + + return os.WriteFile(target, buffer.Bytes(), filePermissions) +} diff --git a/codegen/internal/generator/templates.go b/codegen/internal/generator/templates.go new file mode 100644 index 000000000..3a5d7cbdb --- /dev/null +++ b/codegen/internal/generator/templates.go @@ -0,0 +1,98 @@ +// SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + +package generator + +import ( + "embed" + "fmt" + "path" + "sort" + "text/template" + + "github.com/go-openapi/testify/v2/codegen/internal/generator/funcmaps" +) + +// buildTemplateIndex extracts template names from the index and returns them sorted. +// +// This helper reduces duplication between Generator and DocGenerator template loading. +func buildTemplateIndex(index map[string]string) []string { + needed := make([]string, 0, len(index)) + for _, v := range index { + needed = append(needed, v) + } + sort.Strings(needed) + + return needed +} + +// loadTemplatesFromIndex loads templates from an embedded filesystem using a name-to-file index. +// +// Parameters: +// - index: map from logical template name to template file name (without extension) +// - tplExt: template file extension (e.g., ".gotmpl", ".md.gotmpl") +// - fs: embedded filesystem containing template files in "templates/" directory +// +// Returns a map from template name to parsed template, or an error if any template fails to load. +// +// This function consolidates the common template loading logic shared between Generator +// and DocGenerator, eliminating ~25 lines of duplicate code. +func loadTemplatesFromIndex( + index map[string]string, + tplExt string, + fs embed.FS, +) (map[string]*template.Template, error) { + needed := buildTemplateIndex(index) + + templates := make(map[string]*template.Template, len(needed)) + for _, name := range needed { + file := name + tplExt + tpl, err := template.New(file).Funcs(funcmaps.FuncMap()).ParseFS(fs, path.Join("templates", file)) + if err != nil { + return nil, fmt.Errorf("failed to load template %q from %q: %w", name, file, err) + } + templates[name] = tpl + } + + return templates, nil +} + +// renderFunc is a function that executes a template and writes the result to a file. +type renderFunc func(*template.Template, string, any) error + +// renderTemplate executes a template by name and writes the result using the provided render function. +// +// Parameters: +// - index: map from logical template name to template file name +// - templates: map from template file name to parsed template +// - name: logical template name to render +// - target: output file path +// - data: data to pass to the template +// - renderFn: function that executes the template and writes output (e.g., render or renderMD) +// +// Returns an error if the template name is not found in the index, the template is not loaded, +// or the render function fails. +// +// This function consolidates the common rendering logic shared between Generator and DocGenerator, +// eliminating ~15 lines of duplicate code. The only difference between the two generators is +// the final render function (render vs renderMD), which is passed as a parameter. +func renderTemplate( + index map[string]string, + templates map[string]*template.Template, + name string, + target string, + data any, + renderFn renderFunc, +) error { + tplName, ok := index[name] + if !ok { + return fmt.Errorf("internal error: expect template name %q", name) + } + + tpl, ok := templates[tplName] + if !ok { + return fmt.Errorf("internal error: expect template %q", name) + } + + return renderFn(tpl, target, data) +} diff --git a/codegen/internal/generator/templates/assertion_assertions.gotmpl b/codegen/internal/generator/templates/assertion_assertions.gotmpl index 1ff267d77..082001550 100644 --- a/codegen/internal/generator/templates/assertion_assertions.gotmpl +++ b/codegen/internal/generator/templates/assertion_assertions.gotmpl @@ -1,6 +1,7 @@ {{ comment .Copyright }} // Code generated with {{ .Tool }}; DO NOT EDIT. +// {{ .Header }} package {{ .Package }} diff --git a/codegen/internal/generator/templates/assertion_assertions_test.gotmpl b/codegen/internal/generator/templates/assertion_assertions_test.gotmpl index bff7995f0..390ba742c 100644 --- a/codegen/internal/generator/templates/assertion_assertions_test.gotmpl +++ b/codegen/internal/generator/templates/assertion_assertions_test.gotmpl @@ -1,6 +1,7 @@ {{ comment .Copyright }} // Code generated with {{ .Tool }}; DO NOT EDIT. +// {{ .Header }} package {{ .Package }} @@ -25,7 +26,7 @@ func Test{{ .Name }}(t *testing.T) { {{- $isRequire := (eq $.Package "require") }} {{- $first := true }} {{- range .Tests }} - {{- if eq .ExpectedOutcome 0 }}{{/* TestSuccess */}} + {{- if eq .ExpectedOutcome 1 }}{{/* TestSuccess */}} {{- if (not $first) }}{{ println "" }}{{- end }}{{ $first = false }} t.Run("success", func(t *testing.T) { t.Parallel() @@ -40,7 +41,7 @@ func Test{{ .Name }}(t *testing.T) { } {{- end }} }) - {{- else if eq .ExpectedOutcome 1 }}{{/* TestFailure */}} + {{- else if eq .ExpectedOutcome 2 }}{{/* TestFailure */}} {{- if (not $first) }}{{ println "" }}{{- end }}{{ $first = false }} t.Run("failure", func(t *testing.T) { t.Parallel() @@ -59,7 +60,7 @@ func Test{{ .Name }}(t *testing.T) { t.Error("{{ $fn.Name }} {{ if eq $fn.UseMock "mockFailNowT" }}should call FailNow(){{ else }}should mark test as failed{{ end }}") } }) - {{- else if eq .ExpectedOutcome 2 }}{{/* TestPanic */}} + {{- else if eq .ExpectedOutcome 3 }}{{/* TestPanic */}} {{- if (not $first) }}{{ println "" }}{{- end }}{{ $first = false }} t.Run("panic", func(t *testing.T) { t.Parallel() diff --git a/codegen/internal/generator/templates/assertion_examples_test.gotmpl b/codegen/internal/generator/templates/assertion_examples_test.gotmpl index df4acb2d3..2760085d2 100644 --- a/codegen/internal/generator/templates/assertion_examples_test.gotmpl +++ b/codegen/internal/generator/templates/assertion_examples_test.gotmpl @@ -1,6 +1,7 @@ {{ comment .Copyright }} // Code generated with {{ .Tool }}; DO NOT EDIT. +// {{ .Header }} package {{ .Package }}_test @@ -30,18 +31,18 @@ func Example{{ .Name }}() { {{- $isRequire := (eq $.Package "require") }} {{- $first := true }} {{- range .Tests }} - {{- if eq .ExpectedOutcome 0 }}{{/* TestSuccess */}} + {{- if eq .ExpectedOutcome 1 }}{{/* TestSuccess */}} {{- if (not $first) }}{{ println "" }}{{- end }}{{ $first = false }} t := new(testing.T) {{- if $isRequire }} - {{ $pkg }}.{{ $fn.Name }}(t, {{ relocate .TestedValue $pkg }}) + {{ $pkg }}.{{ $fn.Name }}(t, {{ relocate .TestedValues $pkg }}) fmt.Println("passed") {{- if $runnable }} // Output: passed {{- end }} {{- else }} - success := {{ $pkg }}.{{ $fn.Name }}(t, {{ relocate .TestedValue $pkg }}) + success := {{ $pkg }}.{{ $fn.Name }}(t, {{ relocate .TestedValues $pkg }}) fmt.Printf("success: %t\n", success) {{- if $runnable }} diff --git a/codegen/internal/generator/templates/assertion_format.gotmpl b/codegen/internal/generator/templates/assertion_format.gotmpl index d0f57e604..6030b7d65 100644 --- a/codegen/internal/generator/templates/assertion_format.gotmpl +++ b/codegen/internal/generator/templates/assertion_format.gotmpl @@ -1,6 +1,7 @@ {{ comment .Copyright }} // Code generated with {{ .Tool }}; DO NOT EDIT. +// {{ .Header }} package {{ .Package }} diff --git a/codegen/internal/generator/templates/assertion_format_test.gotmpl b/codegen/internal/generator/templates/assertion_format_test.gotmpl index 9af9c3bd9..78b82e47d 100644 --- a/codegen/internal/generator/templates/assertion_format_test.gotmpl +++ b/codegen/internal/generator/templates/assertion_format_test.gotmpl @@ -1,6 +1,7 @@ {{ comment .Copyright }} // Code generated with {{ .Tool }}; DO NOT EDIT. +// {{ .Header }} package {{ .Package }} @@ -26,7 +27,7 @@ func Test{{ .Name }}f(t *testing.T) { {{- $isRequire := (eq $.Package "require") }} {{- $first := true }} {{- range .Tests }} - {{- if eq .ExpectedOutcome 0 }}{{/* TestSuccess */}} + {{- if eq .ExpectedOutcome 1 }}{{/* TestSuccess */}} {{- if (not $first) }}{{ println "" }}{{- end }}{{ $first = false }} t.Run("success", func(t *testing.T) { t.Parallel() @@ -41,7 +42,7 @@ func Test{{ .Name }}f(t *testing.T) { } {{- end }} }) - {{- else if eq .ExpectedOutcome 1 }}{{/* TestFailure */}} + {{- else if eq .ExpectedOutcome 2 }}{{/* TestFailure */}} {{- if (not $first) }}{{ println "" }}{{- end }}{{ $first = false }} t.Run("failure", func(t *testing.T) { t.Parallel() @@ -60,7 +61,7 @@ func Test{{ .Name }}f(t *testing.T) { t.Error("{{ $fn.Name }} {{ if eq $fn.UseMock "mockFailNowT" }}should call FailNow(){{ else }}should mark test as failed{{ end }}") } }) - {{- else if eq .ExpectedOutcome 2 }}{{/* TestPanic */}} + {{- else if eq .ExpectedOutcome 3 }}{{/* TestPanic */}} {{- if (not $first) }}{{ println "" }}{{- end }}{{ $first = false }} t.Run("panic", func(t *testing.T) { t.Parallel() diff --git a/codegen/internal/generator/templates/assertion_forward.gotmpl b/codegen/internal/generator/templates/assertion_forward.gotmpl index 0a7fb23a9..42ecab630 100644 --- a/codegen/internal/generator/templates/assertion_forward.gotmpl +++ b/codegen/internal/generator/templates/assertion_forward.gotmpl @@ -1,6 +1,7 @@ {{ comment .Copyright }} // Code generated with {{ .Tool }}; DO NOT EDIT. +// {{ .Header }} package {{ .Package }} diff --git a/codegen/internal/generator/templates/assertion_forward_test.gotmpl b/codegen/internal/generator/templates/assertion_forward_test.gotmpl index 548e81e49..0759aca2e 100644 --- a/codegen/internal/generator/templates/assertion_forward_test.gotmpl +++ b/codegen/internal/generator/templates/assertion_forward_test.gotmpl @@ -1,6 +1,7 @@ {{ comment .Copyright }} // Code generated with {{ .Tool }}; DO NOT EDIT. +// {{ .Header }} package {{ .Package }} @@ -25,7 +26,7 @@ func Test{{ $.Receiver }}{{ .Name }}(t *testing.T) { {{- $isRequire := (eq $.Package "require") }} {{- $first := true }} {{- range .Tests }} - {{- if eq .ExpectedOutcome 0 }}{{/* TestSuccess */}} + {{- if eq .ExpectedOutcome 1 }}{{/* TestSuccess */}} {{- if (not $first) }}{{ println "" }}{{- end }}{{ $first = false }} t.Run("success", func(t *testing.T) { t.Parallel() @@ -41,7 +42,7 @@ func Test{{ $.Receiver }}{{ .Name }}(t *testing.T) { } {{- end }} }) - {{- else if eq .ExpectedOutcome 1 }}{{/* TestFailure */}} + {{- else if eq .ExpectedOutcome 2 }}{{/* TestFailure */}} {{- if (not $first) }}{{ println "" }}{{- end }}{{ $first = false }} t.Run("failure", func(t *testing.T) { t.Parallel() @@ -61,7 +62,7 @@ func Test{{ $.Receiver }}{{ .Name }}(t *testing.T) { t.Error("{{ $fn.Name }} {{ if eq $fn.UseMock "mockFailNowT" }}should call FailNow(){{ else }}should mark test as failed{{ end }}") } }) - {{- else if eq .ExpectedOutcome 2 }}{{/* TestPanic */}} + {{- else if eq .ExpectedOutcome 3 }}{{/* TestPanic */}} {{- if (not $first) }}{{ println "" }}{{- end }}{{ $first = false }} t.Run("panic", func(t *testing.T) { t.Parallel() @@ -87,7 +88,7 @@ func Test{{ $.Receiver }}{{ .Name }}f(t *testing.T) { {{- $isRequire := (eq $.Package "require") }} {{- $first := true }} {{- range .Tests }} - {{- if eq .ExpectedOutcome 0 }}{{/* TestSuccess */}} + {{- if eq .ExpectedOutcome 1 }}{{/* TestSuccess */}} {{- if (not $first) }}{{ println "" }}{{- end }}{{ $first = false }} t.Run("success", func(t *testing.T) { t.Parallel() @@ -103,7 +104,7 @@ func Test{{ $.Receiver }}{{ .Name }}f(t *testing.T) { } {{- end }} }) - {{- else if eq .ExpectedOutcome 1 }}{{/* TestFailure */}} + {{- else if eq .ExpectedOutcome 2 }}{{/* TestFailure */}} {{- if (not $first) }}{{ println "" }}{{- end }}{{ $first = false }} t.Run("failure", func(t *testing.T) { t.Parallel() @@ -128,7 +129,7 @@ func Test{{ $.Receiver }}{{ .Name }}f(t *testing.T) { } {{- end }} }) - {{- else if eq .ExpectedOutcome 2 }}{{/* TestPanic */}} + {{- else if eq .ExpectedOutcome 3 }}{{/* TestPanic */}} {{- if (not $first) }}{{ println "" }}{{- end }}{{ $first = false }} t.Run("panic", func(t *testing.T) { t.Parallel() diff --git a/codegen/internal/generator/templates/assertion_helpers.gotmpl b/codegen/internal/generator/templates/assertion_helpers.gotmpl index bbea35ef8..cc5737835 100644 --- a/codegen/internal/generator/templates/assertion_helpers.gotmpl +++ b/codegen/internal/generator/templates/assertion_helpers.gotmpl @@ -1,6 +1,7 @@ {{ comment .Copyright }} // Code generated with {{ .Tool }}; DO NOT EDIT. +// {{ .Header }} package {{ .Package }} diff --git a/codegen/internal/generator/templates/assertion_helpers_test.gotmpl b/codegen/internal/generator/templates/assertion_helpers_test.gotmpl index bd7ca5a2c..a48d797a3 100644 --- a/codegen/internal/generator/templates/assertion_helpers_test.gotmpl +++ b/codegen/internal/generator/templates/assertion_helpers_test.gotmpl @@ -1,6 +1,7 @@ {{ comment .Copyright }} // Code generated with {{ .Tool }}; DO NOT EDIT. +// {{ .Header }} package {{ .Package }} diff --git a/codegen/internal/generator/templates/assertion_types.gotmpl b/codegen/internal/generator/templates/assertion_types.gotmpl index d298221a5..35f3dc5f9 100644 --- a/codegen/internal/generator/templates/assertion_types.gotmpl +++ b/codegen/internal/generator/templates/assertion_types.gotmpl @@ -1,6 +1,7 @@ {{ comment .Copyright }} // Code generated with {{ .Tool }}; DO NOT EDIT. +// {{ .Header }} package {{ .Package }} diff --git a/codegen/internal/generator/templates/doc_index.md.gotmpl b/codegen/internal/generator/templates/doc_index.md.gotmpl new file mode 100644 index 000000000..d9accb1b2 --- /dev/null +++ b/codegen/internal/generator/templates/doc_index.md.gotmpl @@ -0,0 +1,52 @@ +--- +title: {{ .Title | quote }} +description: | + {{ titleize .Description }}. + + + Find the assertion function you need for your data. +weight: 1 +modified: {{ date }} +--- + +**Go testing assertions for the rest of us** + +The [`testify/v2`][testifyv2] package has a fairly large API surface. + +This is in part inherited from the original [`testify`][testifyv1] and also +largely due to the many available variants of the same assertion function. + +The following sections organize the assertion API into usage domains, +with all documented exported variants documented in a more concise form than the +[reference godoc documentation]({{ .PkgGoDevURL }}). + +[![GoDoc][godoc-badge]][godoc-url] +{class="inline-badge"} + +## Domains + +The `testify` API is organized in {{ .RefCount }} domains shown below. + +{{ print "{{" }}< children type="card" description="true" >{{ print "}}" }} + +--- + {{ range .Index }} +- [{{ .Title }}]({{ .Link }}) - {{ titleize .Description }} ({{ .RefCount }}) + {{- end }} + +--- + +Generated with {{ .Package.Tool }} + +[godoc-badge]: {{ godocbadge .PkgGoDevURL }} +[godoc-url]: {{ .PkgGoDevURL }} +[testifyv2]: {{ .GitHubURL }} +[testifyv1]: https://github.com/stretchr/testify + + diff --git a/codegen/internal/generator/templates/doc_page.md.gotmpl b/codegen/internal/generator/templates/doc_page.md.gotmpl new file mode 100644 index 000000000..b6bfe30fc --- /dev/null +++ b/codegen/internal/generator/templates/doc_page.md.gotmpl @@ -0,0 +1,175 @@ +--- +title: {{ .Title | quote }} +description: {{ titleize .Description | quote }} +modified: {{ date }} +weight: {{ .Weight }} +domains: + - {{ .Domain | quote }} +keywords: +{{- with .Package }} + {{- $enableFormat := .EnableFormat }} + {{- range .Functions }} + - {{ .Name | quote }} + {{- if $enableFormat }} + - {{ printf "%sf" .Name | quote }} + {{- end }} + {{- end }} +{{- end }} +--- + +{{ titleize .Description }}{{/* description of the domain, as annotated in internal/assertions/doc.go */}} + +## Assertions + +[![GoDoc][godoc-badge]][godoc-url] +{class="inline-badge"} + +{{- $extraPackages := .ExtraPackages }} +{{- $github := .GitHubURL }} +{{- $godoc := .PkgGoDevURL }} + +_All links point to <{{ $godoc }}>_ + +This domain exposes {{ .RefCount }} functionalities. + +{{- with .Package }} + {{- range .Functions }}{{/* functions in internal/assertions annotated with "domain:" */}} + {{- if or (not .IsGeneric) ($.EnableGenerics) }} + {{- if and (not .IsHelper) (not .IsConstructor) }} + +### {{ .Name }} + {{- if .IsDeprecated }} + +{{ print "{{" }}% expand title="{{ .Name }} [DEPRECATED]" %{{ print "}}" }} +
{{/* like godoc TODO: use css */}} + {{- end }} + +{{ mdformat .DocString }}{{/* reformat inner markdown and reformat */}} + + {{- $funcName := .Name }} + {{- with $extraPackages }} + +{{ print "{{" }}< tabs >{{ print "}}" }} + {{- range .LookupFunction $funcName }} +{{ print "{{" }}% tab title="{{ .Package }}" style="secondary" %{{ print "}}" }} +| Signature | Usage | +|--|--| +| [`{{.Package }}.{{ .Name }}({{ params .AllParams }}) {{ returns .Returns }}`]({{ $godoc }}/{{ .Package }}#{{ .Name }}) | package-level function | + {{- if .EnableFormat }} +| [`{{ .Package }}.{{ .Name }}f(t T, {{ params .Params }}, msg string, args ...any) {{ returns .Returns }}`]({{ $godoc }}/{{ .Package }}#{{ .Name }}f) | formatted variant | + {{- end }} + {{- if .EnableForward }} +| [`{{ .Package}}.(*{{ .Receiver }}).{{ .Name }}({{ params .Params }}) {{ returns .Returns }}`]({{ $godoc }}/{{ .Package }}#{{ .Receiver }}.{{ .Name }}) | method variant | + {{- end }} + {{- if and .EnableForward .EnableFormat }} +| [`{{ .Package }}.(*{{ .Receiver }}).{{ .Name }}f({{ params .Params }}, msg string, args ..any)`]({{ $godoc }}/{{ .Package }}#{{ .Receiver }}.{{ .Name }}f) | method formatted variant | + {{- end }} +{{ print "{{" }}% /tab %{{ print "}}" }} + {{- end }} + {{- end }} + +{{ print "{{" }}% tab title="internal" style="accent" icon="wrench" %{{ print "}}" }} +| Signature | Usage | +|--|--| + {{- $pkg := $.Package.Package }} +| [`{{ $pkg }}.{{ .Name }}({{ params .AllParams }}) {{ returns .Returns }}`]({{ $godoc }}/internal/{{ $pkg }}#{{ .Name }}) | internal implementation | + +**Source:** [{{ .SourcePackage }}#{{ .Name }}]({{ sourceLink $github .SourceLink }}) + + {{- range .ExtraComments }} + {{- if eq .Tag 2 }}{{/* maintainers section */}} + +> **Maintainer Note** +> {{ .Text }}{{/* Proposal for enhancement: clean rendered markdown for multi-line comments */}} + {{- else if eq .Tag 4 }}{{/* notes section */}} + +> **Note** +> {{ .Text }} + {{- end }} + {{- end }} +{{ print "{{" }}% /tab %{{ print "}}" }} +{{ print "{{" }}< /tabs >{{ print "}}" }} + {{- if .IsDeprecated }} +
+{{ print "{{" }}% /expand %{{ print "}}" }} + {{- end }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} + +--- + +{{- with .Package }}{{ if .HasHelpers }} + +## Other helpers + {{- range .Functions }}{{/* functions in internal/assertions annotated with "domain:" */}} + {{- if or (not .IsGeneric) ($.EnableGenerics) }} + {{- if or .IsHelper .IsConstructor }} + +### {{ .Name }} + {{- if .IsDeprecated }} + +{{ print "{{" }}% expand title="{{ .Name }} [DEPRECATED]" %{{ print "}}" }} +
{{/* like godoc */}} + {{- end }} + +{{ mdformat .DocString }} + + {{- $funcName := .Name }} + {{- with $extraPackages }} + +{{ print "{{" }}< tabs >{{ print "}}" }} + {{- range .LookupFunction $funcName }} +{{ print "{{" }}% tab title="{{ .Package }}" style="secondary" %{{ print "}}" }} +| Signature | Usage | +|--|--| +| [`{{.Package }}.{{ .Name }}({{ params .AllParams }}) {{ returns .Returns }}`]({{ $godoc }}/{{ .Package }}#{{ .Name }}) | package-level function | +{{ print "{{" }}% /tab %{{ print "}}" }} + {{- end }} + {{- end }} + +{{ print "{{" }}% tab title="internal" style="accent" icon="wrench" %{{ print "}}" }} +| Signature | Usage | +|--|--| + {{- $pkg := $.Package.Package }} +| [`{{ $pkg }}.{{ .Name }}({{ params .AllParams }}) {{ returns .Returns }}`]({{ $godoc }}/internal/{{ $pkg }}#{{ .Name }}) | internal implementation | + +**Source:** [{{ .SourcePackage }}#{{ .Name }}]({{ sourceLink $github .SourceLink }}) + + {{- range .ExtraComments }} + {{- if eq .Tag 2 }}{{/* maintainers section */}} + +> **Maintainer Note** +> {{ .Text }}{{/* Proposal for enhancement: clean rendered markdown for multi-line comments */}} + {{- else if eq .Tag 4 }}{{/* notes section */}} + +> **Note** +> {{ .Text }} + {{- end }} + {{- end }} +{{ print "{{" }}% /tab %{{ print "}}" }} +{{ print "{{" }}< /tabs >{{ print "}}" }} + {{- if .IsDeprecated }} +
+{{ print "{{" }}% /expand %{{ print "}}" }} + {{- end }} + {{- end }} + {{- end }} + {{- end }} +{{- end }}{{- end }} + +--- + +Generated with {{ .Package.Tool }} + +[godoc-badge]: {{ godocbadge .PkgGoDevURL }} +[godoc-url]: {{ .PkgGoDevURL }} + + diff --git a/codegen/internal/generator/templates/requirement_assertions.gotmpl b/codegen/internal/generator/templates/requirement_assertions.gotmpl index f97d57e11..7322b9f1e 100644 --- a/codegen/internal/generator/templates/requirement_assertions.gotmpl +++ b/codegen/internal/generator/templates/requirement_assertions.gotmpl @@ -1,6 +1,7 @@ {{ comment .Copyright }} // Code generated with {{ .Tool }}; DO NOT EDIT. +// {{ .Header }} package {{ .Package }} diff --git a/codegen/internal/generator/templates/requirement_format.gotmpl b/codegen/internal/generator/templates/requirement_format.gotmpl index 47ef5f37f..7b9c70db7 100644 --- a/codegen/internal/generator/templates/requirement_format.gotmpl +++ b/codegen/internal/generator/templates/requirement_format.gotmpl @@ -1,6 +1,7 @@ {{ comment .Copyright }} // Code generated with {{ .Tool }}; DO NOT EDIT. +// {{ .Header }} package {{ .Package }} diff --git a/codegen/internal/generator/templates/requirement_forward.gotmpl b/codegen/internal/generator/templates/requirement_forward.gotmpl index cd5614dcf..0b994d540 100644 --- a/codegen/internal/generator/templates/requirement_forward.gotmpl +++ b/codegen/internal/generator/templates/requirement_forward.gotmpl @@ -1,6 +1,7 @@ {{ comment .Copyright }} // Code generated with {{ .Tool }}; DO NOT EDIT. +// {{ .Header }} package {{ .Package }} diff --git a/codegen/internal/model/documentation.go b/codegen/internal/model/documentation.go new file mode 100644 index 000000000..d67c91aab --- /dev/null +++ b/codegen/internal/model/documentation.go @@ -0,0 +1,131 @@ +package model + +import ( + "slices" +) + +type Documentation struct { + Package *AssertionPackage // the complete source package being documented + Documents []Document // Documents is a collection of markdown pages or folders +} + +// NewDocumentation builds an empty [Documentation]. +func NewDocumentation() *Documentation { + return &Documentation{} +} + +func (d *Documentation) Merge(doc Documentation) { + if len(d.Documents) == 0 { + // just replace + *d = doc + + return + } + + if len(d.Documents) == 1 { + // creates a hierarchy + current := Document{ + Title: d.Package.Package, + Path: d.Package.Package, + Kind: KindFolder, + Package: d.Package, + Documents: d.Documents, + } + + next := Document{ + Title: doc.Package.Package, + Path: doc.Package.Package, + Kind: KindFolder, + Package: doc.Package, + Documents: doc.Documents, + } + + d.Package = nil + d.Documents = []Document{ + current, + next, + } + + return + } + + // simply append a new folder + d.Documents = append(d.Documents, Document{ + Title: doc.Package.Package, + Path: doc.Package.Package, + Kind: KindFolder, + Package: doc.Package, + Documents: doc.Documents, + }) +} + +type KindDoc uint8 + +const ( + KindPage KindDoc = iota + KindIndex + KindFolder +) + +type Document struct { + Title string + Domain string + Description string + Path string // document folder, relative to its parent if any + File string // document name, e.g. _index.md, string.md + GitHubURL string + PkgGoDevURL string + Kind KindDoc // page or index or folder + Documents []Document // for folders, their content [Document] + Index []IndexEntry // for indexes, their entry list + Package *AssertionPackage // subset of the package that pertains to this document + ExtraPackages ExtraPackages + RefCount int + Weight int +} + +type ExtraPackages []*AssertionPackage + +func (pkgs ExtraPackages) LookupFunction(name string) []FunctionWithContext { + const defaultVariants = 8 + result := make([]FunctionWithContext, 0, defaultVariants) + + for _, pkg := range pkgs { + for _, fn := range pkg.Functions { + if fn.Name == name { + result = append(result, FunctionWithContext{ + Function: fn, + Package: pkg.Package, + Receiver: pkg.Receiver, + EnableFormat: pkg.EnableFormat, + EnableForward: pkg.EnableForward, + EnableGenerics: pkg.EnableGenerics, + EnableExamples: pkg.EnableExamples, + }) + break + } + } + } + + return slices.Clip(result) +} + +type FunctionWithContext struct { + Function + + Package string + Receiver string + EnableFormat bool + EnableForward bool + EnableGenerics bool + EnableExamples bool +} + +type IndexEntry struct { + Name string + Title string + Description string + Link string + RefCount int + Weight int +} diff --git a/codegen/internal/model/model.go b/codegen/internal/model/model.go index 856baa2c9..0380e5fc6 100644 --- a/codegen/internal/model/model.go +++ b/codegen/internal/model/model.go @@ -4,14 +4,16 @@ package model import ( + "go/ast" + "go/token" "maps" "slices" - "strings" ) // AssertionPackage describes the internal/assertions package. type AssertionPackage struct { Tool string + Header string Package string DocString string Copyright string @@ -28,6 +30,19 @@ type AssertionPackage struct { Types []Ident Consts []Ident Vars []Ident + + // extraneous information when scanning in collectDoc mode + ExtraComments []ExtraComment +} + +func (a *AssertionPackage) HasHelpers() (ok bool) { + for _, fn := range a.Functions { + if fn.IsHelper || fn.IsConstructor { + return true + } + } + + return false } // New empty [AssertionPackage]. @@ -73,8 +88,14 @@ type Function struct { Returns Parameters IsGeneric bool IsHelper bool + IsDeprecated bool IsConstructor bool Tests []Test + + // extraneous information when scanning in collectDoc mode + Domain string + SourceLink *token.Position + ExtraComments []ExtraComment } func (f Function) HasTest() bool { @@ -89,42 +110,6 @@ func (f Function) HasSuccessTest() bool { type Parameters []Parameter -func (vars Parameters) String() string { - var b strings.Builder - l := len(vars) - - if l == 0 { - return "" - } - - if l > 1 || vars[0].Name != "" { - b.WriteByte('(') - } - - if vars[0].Name != "" { - b.WriteString(vars[0].Name) - b.WriteByte(' ') - } - b.WriteString(vars[0].GoType) - - for _, v := range vars[1:] { - b.WriteByte(',') - b.WriteByte(' ') - if v.Name != "" { - b.WriteString(v.Name) - b.WriteByte(' ') - } - b.WriteString(v.GoType) - - } - - if l > 1 || vars[0].Name != "" { - b.WriteByte(')') - } - - return b.String() -} - // Parameter represents a function parameter or return value. type Parameter struct { Name string @@ -141,20 +126,56 @@ type Ident struct { TargetPackage string DocString string IsAlias bool - Function *Function // for function types + IsDeprecated bool + Function *Function // for function types (or vars) + + // extraneous information when scanning in collectDoc mode + Domain string + SourceLink *token.Position + ExtraComments []ExtraComment } -// Test captures simple test values to use with generated tests. +// TestValue represents a single parsed test value expression. +// +// It stores both the original string (for debugging/audit) and the parsed AST. +type TestValue struct { + Raw string // Original string from doc comment + Expr ast.Expr // Parsed Go expression (nil if parse failed) + Error error // Parse error if any +} + +// Test captures test values to use with generated tests. +// +// Test values are parsed as Go expressions and stored with their AST representation. type Test struct { - TestedValue string - ExpectedOutcome TestExpectedOutcome - AssertionMessage string + TestedValue string // DEPRECATED: Original raw string, kept for backward compatibility + TestedValues []TestValue // Parsed test value expressions + ExpectedOutcome TestExpectedOutcome // Expected test outcome (success/failure/panic) + AssertionMessage string // Optional assertion message for panic tests } type TestExpectedOutcome uint8 const ( - TestSuccess TestExpectedOutcome = iota + TestNone TestExpectedOutcome = iota + TestSuccess TestFailure TestPanic ) + +type CommentTag uint8 + +const ( + CommentTagNone CommentTag = iota + CommentTagDomain + CommentTagMaintainer + CommentTagMention + CommentTagNote + CommentTagDomainDescription +) + +type ExtraComment struct { + Tag CommentTag + Key string + Text string +} diff --git a/codegen/internal/scanner/comments-parser/doc.go b/codegen/internal/scanner/comments-parser/doc.go new file mode 100644 index 000000000..bbf1ce635 --- /dev/null +++ b/codegen/internal/scanner/comments-parser/doc.go @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + +// Package parser provides text parsing utilities for extracting structured information from comments. +// +// This package contains pure text parsing logic with no AST dependencies. It parses: +// +// - Test values from "Examples:" sections in doc comments +// - Tagged comments (domain, maintainer, note, mention) +// - Domain descriptions from package-level comments +// +// The parsers use regular expressions to identify sections and extract values. +// All parsing is case-insensitive and supports multiple formatting styles. +package parser diff --git a/codegen/internal/scanner/comments-parser/examples.go b/codegen/internal/scanner/comments-parser/examples.go new file mode 100644 index 000000000..b8ae69853 --- /dev/null +++ b/codegen/internal/scanner/comments-parser/examples.go @@ -0,0 +1,118 @@ +// SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + +package parser + +import ( + "slices" + "strings" + + "github.com/go-openapi/testify/v2/codegen/internal/model" +) + +// ParseTestExamples extracts test examples from doc comment text. +// +// It looks for an "Examples:" or "Example:" section and parses lines like: +// - success: +// - failure: +// - panic: +// +// After each value line, an optional comment line can provide an assertion message. +// Empty values or values marked "// NOT IMPLEMENTED" are skipped. +func ParseTestExamples(text string) []model.Test { + const usualNumberOfExamples = 2 + tests := make([]model.Test, 0, usualNumberOfExamples) + + inExamplesSection := false // we expect an Example[s] or # Example[s] section + inValueSection := false + + const ( + successPrefix = "success" + failurePrefix = "failure" + panicPrefix = "panic" + examplePrefix = "example" + ) + + startExampleSection := StartSectionFunc(examplePrefix) // e.g. Example:, examples:, # Example, ... + startValueSuccess := StartValueFunc(successPrefix) // e.g. success: , Success:, "SuCCess :" + startValueFailure := StartValueFunc(failurePrefix) + startValuePanic := StartValueFunc(panicPrefix) + startExampleValue := func(line string) (string, model.TestExpectedOutcome, bool) { + val, ok := startValueSuccess(line) + if ok { + return val, model.TestSuccess, true + } + val, ok = startValueFailure(line) + if ok { + return val, model.TestFailure, true + } + val, ok = startValuePanic(line) + if ok { + return val, model.TestPanic, true + } + return "", model.TestNone, false + } + + for line := range strings.SplitSeq(text, "\n") { + line = strings.TrimSpace(line) + value, outcome, isExampleValue := startExampleValue(line) + if inValueSection && len(tests) > 0 && line != "" && !StartAnotherSection(line) && !isExampleValue { + // check if a comment line follows an example value: this would be the assertion message + tests[len(tests)-1].AssertionMessage = line + inValueSection = false + + continue + } + inValueSection = false + + // check if we're entering the Examples section + if startExampleSection(line) { + inExamplesSection = true + continue + } + + // skip until we find the Examples section + if !inExamplesSection { + continue + } + + // stop if we hit another section (starts with capital letter and ends with colon) + if StartAnotherSection(line) { + break + } + + // parse test lines: "success: { values to put in the test }", "failure: ..." or "panic: ..." + // after each value line, we may found an additional text to be used as an assertion message. + if !isExampleValue { + continue + } + + inValueSection = true // an extra comment may appear right after the value line + testcase, ok := parseTestValue(outcome, value) + if !ok { + continue // there is a value, but it's empty or marked "// NOT IMPLEMENTED" + } + + tests = append(tests, testcase) + } + + return slices.Clip(tests) +} + +func parseTestValue(outcome model.TestExpectedOutcome, value string) (model.Test, bool) { + value = strings.TrimSpace(value) + _, isTodo := strings.CutPrefix(value, "// NOT IMPLEMENTED") + + if value == "" || isTodo { + return model.Test{}, false + } + + // Parse test values as Go expressions + parsedValues := ParseTestValues(value) + + return model.Test{ + TestedValue: value, // Keep original for backward compatibility + TestedValues: parsedValues, // New parsed representation + ExpectedOutcome: outcome, + }, true +} diff --git a/codegen/internal/scanner/comments-parser/examples_test.go b/codegen/internal/scanner/comments-parser/examples_test.go new file mode 100644 index 000000000..4c6a7bc01 --- /dev/null +++ b/codegen/internal/scanner/comments-parser/examples_test.go @@ -0,0 +1,350 @@ +// SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + +package parser + +import ( + "iter" + "slices" + "testing" + + "github.com/go-openapi/testify/v2/codegen/internal/model" +) + +func TestParseTestExamples(t *testing.T) { + t.Parallel() + + for c := range parseTestExamplesCases() { + t.Run(c.name, func(t *testing.T) { + t.Parallel() + + result := ParseTestExamples(c.input) + + if len(result) != len(c.expected) { + t.Fatalf("ParseTestExamples() returned %d tests, expected %d\nGot: %+v\nExpected: %+v", + len(result), len(c.expected), result, c.expected) + } + + for i, expected := range c.expected { + got := result[i] + if got.TestedValue != expected.TestedValue { + t.Errorf("Test[%d].TestedValue = %q, expected %q", i, got.TestedValue, expected.TestedValue) + } + if got.ExpectedOutcome != expected.ExpectedOutcome { + t.Errorf("Test[%d].ExpectedOutcome = %v, expected %v", i, got.ExpectedOutcome, expected.ExpectedOutcome) + } + if got.AssertionMessage != expected.AssertionMessage { + t.Errorf("Test[%d].AssertionMessage = %q, expected %q", i, got.AssertionMessage, expected.AssertionMessage) + } + } + }) + } +} + +/* Test case iterators */ + +type parseTestExamplesCase struct { + name string + input string + expected []model.Test +} + +func parseTestExamplesCases() iter.Seq[parseTestExamplesCase] { + return slices.Values([]parseTestExamplesCase{ + { + name: "success and failure examples", + input: `Some function description. + +Examples: + success: 123, 456 + failure: 789, 012`, + expected: []model.Test{ + {TestedValue: "123, 456", ExpectedOutcome: model.TestSuccess}, + {TestedValue: "789, 012", ExpectedOutcome: model.TestFailure}, + }, + }, + { + name: "panic example", + input: `Examples: + panic: nil, "something" + should panic with nil pointer`, + expected: []model.Test{ + { + TestedValue: "nil, \"something\"", + ExpectedOutcome: model.TestPanic, + AssertionMessage: "should panic with nil pointer", + }, + }, + }, + { + name: "mixed examples with assertion messages", + input: `Examples: + success: "hello", "hello" + failure: "hello", "world" + strings don't match + panic: invalid, args + Expected to panic`, + expected: []model.Test{ + {TestedValue: "\"hello\", \"hello\"", ExpectedOutcome: model.TestSuccess}, + { + TestedValue: "\"hello\", \"world\"", + ExpectedOutcome: model.TestFailure, + AssertionMessage: "strings don't match", + }, + { + TestedValue: "invalid, args", + ExpectedOutcome: model.TestPanic, + AssertionMessage: "Expected to panic", + }, + }, + }, + { + name: "case insensitive success/failure/panic", + input: `Examples: + SUCCESS: 1, 1 + FAILURE: 1, 2 + PANIC: bad`, + expected: []model.Test{ + {TestedValue: "1, 1", ExpectedOutcome: model.TestSuccess}, + {TestedValue: "1, 2", ExpectedOutcome: model.TestFailure}, + {TestedValue: "bad", ExpectedOutcome: model.TestPanic}, + }, + }, + { + name: "markdown header style", + input: `# Examples + + success: true, true + failure: true, false`, + expected: []model.Test{ + {TestedValue: "true, true", ExpectedOutcome: model.TestSuccess}, + {TestedValue: "true, false", ExpectedOutcome: model.TestFailure}, + }, + }, + { + name: "plural form", + input: `Examples: + success: 42`, + expected: []model.Test{ + {TestedValue: "42", ExpectedOutcome: model.TestSuccess}, + }, + }, + { + name: "singular form", + input: `Example: + success: 100`, + expected: []model.Test{ + {TestedValue: "100", ExpectedOutcome: model.TestSuccess}, + }, + }, + { + name: "skip NOT IMPLEMENTED values", + input: `Examples: + success: 123 + failure: // NOT IMPLEMENTED + success: 456`, + expected: []model.Test{ + {TestedValue: "123", ExpectedOutcome: model.TestSuccess}, + {TestedValue: "456", ExpectedOutcome: model.TestSuccess}, + }, + }, + { + name: "skip empty values", + input: `Examples: + success: 123 + failure: + success: 456`, + expected: []model.Test{ + {TestedValue: "123", ExpectedOutcome: model.TestSuccess}, + {TestedValue: "456", ExpectedOutcome: model.TestSuccess}, + }, + }, + { + name: "skip whitespace-only values", + input: `Examples: + success: 123 + failure: + success: 456`, + expected: []model.Test{ + {TestedValue: "123", ExpectedOutcome: model.TestSuccess}, + {TestedValue: "456", ExpectedOutcome: model.TestSuccess}, + }, + }, + { + name: "stop at next section", + input: `Examples: + success: 123 + failure: 456 + +Usage: + success: this should not be parsed`, + expected: []model.Test{ + {TestedValue: "123", ExpectedOutcome: model.TestSuccess}, + {TestedValue: "456", ExpectedOutcome: model.TestFailure}, + }, + }, + { + name: "complex values with quotes and commas", + input: `Examples: + success: "hello, world", "hello, world" + failure: map[string]int{"a": 1}, map[string]int{"b": 2}`, + expected: []model.Test{ + {TestedValue: `"hello, world", "hello, world"`, ExpectedOutcome: model.TestSuccess}, + {TestedValue: `map[string]int{"a": 1}, map[string]int{"b": 2}`, ExpectedOutcome: model.TestFailure}, + }, + }, + { + name: "assertion message on line after value", + input: `Examples: + success: 1, 1 + Both values are equal + failure: 1, 2 + Values differ`, + expected: []model.Test{ + { + TestedValue: "1, 1", + ExpectedOutcome: model.TestSuccess, + AssertionMessage: "Both values are equal", + }, + { + TestedValue: "1, 2", + ExpectedOutcome: model.TestFailure, + AssertionMessage: "Values differ", + }, + }, + }, + { + name: "no assertion message when next line is value", + input: `Examples: + success: 1, 1 + failure: 1, 2`, + expected: []model.Test{ + {TestedValue: "1, 1", ExpectedOutcome: model.TestSuccess}, + {TestedValue: "1, 2", ExpectedOutcome: model.TestFailure}, + }, + }, + { + name: "assertion message not consumed by next value", + input: `Examples: + success: 1, 1 + Equal values + success: 2, 2`, + expected: []model.Test{ + { + TestedValue: "1, 1", + ExpectedOutcome: model.TestSuccess, + AssertionMessage: "Equal values", + }, + {TestedValue: "2, 2", ExpectedOutcome: model.TestSuccess}, + }, + }, + { + name: "no examples section", + input: `This is just a description without examples.`, + expected: []model.Test{}, + }, + { + name: "empty examples section", + input: `Examples: + +Next section.`, + expected: []model.Test{}, + }, + { + name: "examples section with no valid values", + input: `Examples: + Just some text here. + No actual test values.`, + expected: []model.Test{}, + }, + { + name: "leading and trailing whitespace in values", + input: `Examples: + success: 123, 456 + failure: "hello" `, + expected: []model.Test{ + {TestedValue: "123, 456", ExpectedOutcome: model.TestSuccess}, + {TestedValue: `"hello"`, ExpectedOutcome: model.TestFailure}, + }, + }, + { + name: "multiple assertion messages only first consumed", + input: `Examples: + success: 1 + First message + Second message should not be consumed + failure: 2`, + expected: []model.Test{ + { + TestedValue: "1", + ExpectedOutcome: model.TestSuccess, + AssertionMessage: "First message", + }, + {TestedValue: "2", ExpectedOutcome: model.TestFailure}, + }, + }, + { + name: "assertion message with special characters", + input: `Examples: + failure: nil, "test" + Expected: non-nil value, got: `, + expected: []model.Test{ + { + TestedValue: `nil, "test"`, + ExpectedOutcome: model.TestFailure, + AssertionMessage: "Expected: non-nil value, got: ", + }, + }, + }, + { + name: "case insensitive example header", + input: `EXAMPLES: + success: 1`, + expected: []model.Test{ + {TestedValue: "1", ExpectedOutcome: model.TestSuccess}, + }, + }, + { + name: "values with leading whitespace", + input: `Examples: + success: 123 + failure: 456`, + expected: []model.Test{ + {TestedValue: "123", ExpectedOutcome: model.TestSuccess}, + {TestedValue: "456", ExpectedOutcome: model.TestFailure}, + }, + }, + { + name: "TODO with description", + input: `Examples: + success: // NOT IMPLEMENTED: implement this test + failure: 456`, + expected: []model.Test{ + {TestedValue: "456", ExpectedOutcome: model.TestFailure}, + }, + }, + { + name: "empty line between examples", + input: `Examples: + success: 1 + + failure: 2`, + expected: []model.Test{ + {TestedValue: "1", ExpectedOutcome: model.TestSuccess}, + {TestedValue: "2", ExpectedOutcome: model.TestFailure}, + }, + }, + { + name: "assertion message separated by empty line", + input: `Examples: + success: 1 + + Some text here should not be an assertion message + failure: 2`, + expected: []model.Test{ + {TestedValue: "1", ExpectedOutcome: model.TestSuccess}, + {TestedValue: "2", ExpectedOutcome: model.TestFailure}, + }, + }, + }) +} diff --git a/codegen/internal/scanner/comments-parser/expressions.go b/codegen/internal/scanner/comments-parser/expressions.go new file mode 100644 index 000000000..3411509d4 --- /dev/null +++ b/codegen/internal/scanner/comments-parser/expressions.go @@ -0,0 +1,144 @@ +// SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + +package parser + +import ( + "fmt" + "go/ast" + "go/format" + "go/parser" + "go/token" + "strings" + + "github.com/go-openapi/testify/v2/codegen/internal/model" +) + +// ParseTestValues parses a comma-separated list of Go expressions. +// +// Examples: +// - "123, 456" → two integer literals +// - "assertions.ErrTest, nil" → selector expression and nil +// - "[]int{1,2,3}, []int{4,5,6}" → two composite literals +// - `"a,b", "c,d"` → two string literals with commas inside +// +// Implementation: Wraps input in []any{...} and parses as composite literal, +// then extracts the elements. This leverages Go's parser to handle all edge cases +// (strings, runes, comments, nesting, etc.) correctly. +func ParseTestValues(input string) []model.TestValue { + input = strings.TrimSpace(input) + if input == "" { + return nil + } + + // Wrap in composite literal to let Go's parser handle comma splitting + wrapped := "[]any{" + input + "}" + + // Parse the wrapped expression + expr, err := parser.ParseExpr(wrapped) + if err != nil { + // If parsing fails, return a single TestValue with the error + return []model.TestValue{{ + Raw: input, + Expr: nil, + Error: fmt.Errorf("invalid Go expression list %q: %w", input, err), + }} + } + + // Extract elements from the composite literal + compositeLit, ok := expr.(*ast.CompositeLit) + if !ok { + // Should never happen if parser succeeded + return []model.TestValue{{ + Raw: input, + Expr: nil, + Error: fmt.Errorf("internal error: expected composite literal, got %T", expr), + }} + } + + // Convert each element to TestValue + result := make([]model.TestValue, 0, len(compositeLit.Elts)) + + // We need to extract the original source text for each element + // Since we don't have position info for the original input, we'll format the AST + fset := token.NewFileSet() + for _, elt := range compositeLit.Elts { + // Format the expression back to source code + var buf strings.Builder + if err := format.Node(&buf, fset, elt); err != nil { + result = append(result, model.TestValue{ + Raw: "", + Expr: elt, + Error: fmt.Errorf("failed to format expression: %w", err), + }) + continue + } + + result = append(result, model.TestValue{ + Raw: buf.String(), + Expr: elt, + Error: nil, + }) + } + + return result +} + +// ParseExprWithFileSet is like ParseTestValues but uses a provided FileSet +// for better error reporting with file/line/column information. +// +// The filename parameter is used for error messages (e.g., "assertion.go:42"). +func ParseExprWithFileSet(fset *token.FileSet, filename string, input string) ([]model.TestValue, error) { + input = strings.TrimSpace(input) + if input == "" { + return nil, nil + } + + // Wrap in composite literal to let Go's parser handle comma splitting + wrapped := "[]any{" + input + "}" + + // Parse with FileSet for better error reporting + expr, err := parser.ParseExprFrom(fset, filename, wrapped, 0) + if err != nil { + return []model.TestValue{{ + Raw: input, + Expr: nil, + Error: fmt.Errorf("invalid Go expression list %q: %w", input, err), + }}, err + } + + // Extract elements from the composite literal + compositeLit, ok := expr.(*ast.CompositeLit) + if !ok { + err := fmt.Errorf("internal error: expected composite literal, got %T", expr) + return []model.TestValue{{ + Raw: input, + Expr: nil, + Error: err, + }}, err + } + + // Convert each element to TestValue + result := make([]model.TestValue, 0, len(compositeLit.Elts)) + + for _, elt := range compositeLit.Elts { + // Format the expression back to source code + var buf strings.Builder + if err := format.Node(&buf, fset, elt); err != nil { + result = append(result, model.TestValue{ + Raw: "", + Expr: elt, + Error: fmt.Errorf("failed to format expression: %w", err), + }) + continue + } + + result = append(result, model.TestValue{ + Raw: buf.String(), + Expr: elt, + Error: nil, + }) + } + + return result, nil +} diff --git a/codegen/internal/scanner/comments-parser/expressions_test.go b/codegen/internal/scanner/comments-parser/expressions_test.go new file mode 100644 index 000000000..896de818c --- /dev/null +++ b/codegen/internal/scanner/comments-parser/expressions_test.go @@ -0,0 +1,254 @@ +// SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + +package parser + +import ( + "go/token" + "iter" + "slices" + "testing" + + "github.com/go-openapi/testify/v2/codegen/internal/model" +) + +// TestParseTestValues tests parsing of test value expressions. +func TestParseTestValues(t *testing.T) { + t.Parallel() + + for c := range parseTestValuesTestCases() { + t.Run(c.name, func(t *testing.T) { + t.Parallel() + + result := ParseTestValues(c.input) + + if len(result) != c.expectedCount { + t.Errorf("Expected %d values, got %d", c.expectedCount, len(result)) + return + } + + for i, val := range result { + t.Run("check single parsed test value", testSingleTestValue(c, val, i)) + } + }) + } +} + +func testSingleTestValue(c parseTestCase, val model.TestValue, i int) func(*testing.T) { + return func(t *testing.T) { + // Check raw string is preserved + expectedRaw := c.expectedRaw[i] + if val.Raw != expectedRaw { + t.Errorf("Value %d: expected raw %q, got %q", i, expectedRaw, val.Raw) + } + + // Check parse success/failure + shouldParse := true // Default: expect successful parse + if c.shouldParsePerVal != nil && i < len(c.shouldParsePerVal) { + shouldParse = c.shouldParsePerVal[i] + } + + if shouldParse { + if val.Error != nil { + t.Errorf("Value %d: unexpected parse error: %v", i, val.Error) + } + if val.Expr == nil { + t.Errorf("Value %d: expected parsed expression, got nil", i) + } + } else if val.Error == nil { + t.Errorf("Value %d: expected parse error, got none", i) + } + } +} + +// TestParseExprWithFileSet tests parsing with FileSet for error reporting. +func TestParseExprWithFileSet(t *testing.T) { + t.Parallel() + + fset := token.NewFileSet() + + for c := range parseExprWithFileSetTestCases() { + t.Run(c.name, func(t *testing.T) { + t.Parallel() + + result, err := ParseExprWithFileSet(fset, "test.go", c.input) + + if c.shouldError { + if err == nil { + t.Error("Expected error, got none") + } + } else { + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + } + + if len(result) != c.expectedCount { + t.Errorf("Expected %d values, got %d", c.expectedCount, len(result)) + } + }) + } +} + +/* Test case iterators */ + +type parseTestCase struct { + name string + input string + expectedCount int + expectedRaw []string + shouldParsePerVal []bool // per-value parse expectations (nil means all should parse) +} + +func parseTestValuesTestCases() iter.Seq[parseTestCase] { + return slices.Values([]parseTestCase{ + { + name: "two integer literals", + input: "123, 456", + expectedCount: 2, + expectedRaw: []string{"123", "456"}, + }, + { + name: "string literals", + input: `"hello", "world"`, + expectedCount: 2, + expectedRaw: []string{`"hello"`, `"world"`}, + }, + { + name: "string literals with commas inside", + input: `"a,b", "c,d"`, + expectedCount: 2, + expectedRaw: []string{`"a,b"`, `"c,d"`}, + }, + { + name: "mixed strings and values with commas", + input: `"hello, world", 123, "foo,bar,baz"`, + expectedCount: 3, + expectedRaw: []string{`"hello, world"`, `123`, `"foo,bar,baz"`}, + }, + { + name: "nil and boolean", + input: "nil, true, false", + expectedCount: 3, + expectedRaw: []string{"nil", "true", "false"}, + }, + { + name: "selector expressions", + input: "assertions.ErrTest, pkg.Type", + expectedCount: 2, + expectedRaw: []string{"assertions.ErrTest", "pkg.Type"}, + }, + { + name: "composite literals", + input: "[]int{1,2,3}, []int{4,5,6}", + expectedCount: 2, + expectedRaw: []string{"[]int{1, 2, 3}", "[]int{4, 5, 6}"}, // go/format adds spaces + }, + { + name: "struct literals", + input: "pkg.Type{Field: value}, pkg.Type{}", + expectedCount: 2, + expectedRaw: []string{"pkg.Type{Field: value}", "pkg.Type{}"}, + }, + { + name: "function calls", + input: "fn(a, b), fn(c)", + expectedCount: 2, + expectedRaw: []string{"fn(a, b)", "fn(c)"}, + }, + { + name: "unary expressions", + input: "&x, *ptr, -123", + expectedCount: 3, + expectedRaw: []string{"&x", "*ptr", "-123"}, + }, + { + name: "type assertions", + input: "x.(int), y.(string)", + expectedCount: 2, + expectedRaw: []string{"x.(int)", "y.(string)"}, + }, + { + name: "empty string", + input: "", + expectedCount: 0, + expectedRaw: []string{}, + }, + { + name: "whitespace only", + input: " ", + expectedCount: 0, + expectedRaw: []string{}, + }, + { + name: "single value", + input: "123", + expectedCount: 1, + expectedRaw: []string{"123"}, + }, + { + name: "invalid expression", + input: "123 +", + expectedCount: 1, + expectedRaw: []string{"123 +"}, + shouldParsePerVal: []bool{false}, + }, + { + name: "unclosed paren", + input: "(123", + expectedCount: 1, + expectedRaw: []string{"(123"}, + shouldParsePerVal: []bool{false}, + }, + { + name: "invalid characters", + input: "@#$%", + expectedCount: 1, + expectedRaw: []string{"@#$%"}, + shouldParsePerVal: []bool{false}, + }, + { + name: "mixed valid and invalid", + input: "123, @invalid", + expectedCount: 1, // Parse fails for whole input, returns single error + expectedRaw: []string{"123, @invalid"}, + shouldParsePerVal: []bool{false}, // Entire input fails to parse + }, + }) +} + +type parseWithFileSetTestCase struct { + name string + input string + expectedCount int + shouldError bool // true if ParseExprWithFileSet should return an error +} + +func parseExprWithFileSetTestCases() iter.Seq[parseWithFileSetTestCase] { + return slices.Values([]parseWithFileSetTestCase{ + { + name: "valid expressions", + input: "123, 456", + expectedCount: 2, + shouldError: false, + }, + { + name: "invalid expression returns error", + input: "123 +", + expectedCount: 1, + shouldError: true, + }, + { + name: "first error is returned", + input: "123, @invalid, 789", + expectedCount: 1, // Parse fails for whole input + shouldError: true, + }, + { + name: "empty input", + input: "", + expectedCount: 0, + shouldError: false, + }, + }) +} diff --git a/codegen/internal/scanner/comments-parser/matchers.go b/codegen/internal/scanner/comments-parser/matchers.go new file mode 100644 index 000000000..c70b1dd59 --- /dev/null +++ b/codegen/internal/scanner/comments-parser/matchers.go @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + +package parser + +import ( + "fmt" + "regexp" + "strings" +) + +// StartSectionFunc creates a matcher function for section headers. +// +// A section header can be: +// - "# Placeholder" or "# Placeholders" (markdown-style) +// - "Placeholder:" or "Placeholders:" (simple colon-style) +// +// Matching is case-insensitive. +func StartSectionFunc(placeholder string) func(string) bool { + sectionRex := regexp.MustCompile( + fmt.Sprintf(`(?i)^\s*(#\s+%[1]ss?\s*$|%[1]ss?\s*:)`, regexp.QuoteMeta(placeholder)), + ) + + return sectionRex.MatchString +} + +// StartValueFunc creates a matcher function for key-value lines. +// +// A value line has the format: "placeholder: value" +// Returns the value part (everything after the colon) and true if matched. +// Matching is case-insensitive. +func StartValueFunc(placeholder string) func(string) (string, bool) { + valueRex := regexp.MustCompile( + fmt.Sprintf(`(?i)^\s*%[1]s\s*:\s*`, regexp.QuoteMeta(placeholder)), + ) + + return func(text string) (string, bool) { + if valueRex.MatchString(text) { + return valueRex.ReplaceAllLiteralString(text, ""), true + } + + return "", false + } +} + +// StartAnotherSection checks if a line starts a new section. +// +// A line starts a new section if it either: +// - Starts with a capital letter and ends with a colon (e.g., "Usage:") +// - Starts with "# " (markdown header) +func StartAnotherSection(text string) bool { + return len(text) > 0 && (text[0] >= 'A' && text[0] <= 'Z' && strings.HasSuffix(text, ":")) || strings.HasPrefix(text, "# ") +} diff --git a/codegen/internal/scanner/comments-parser/matchers_test.go b/codegen/internal/scanner/comments-parser/matchers_test.go new file mode 100644 index 000000000..663b8095b --- /dev/null +++ b/codegen/internal/scanner/comments-parser/matchers_test.go @@ -0,0 +1,171 @@ +// SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + +package parser + +import ( + "fmt" + "iter" + "slices" + "testing" +) + +func TestStartSectionFunc(t *testing.T) { + t.Parallel() + + for c := range startSectionCases() { + t.Run(fmt.Sprintf("%q matches %q", c.placeholder, c.input), func(t *testing.T) { + t.Parallel() + + matcher := StartSectionFunc(c.placeholder) + result := matcher(c.input) + + if result != c.expected { + t.Errorf("StartSectionFunc(%q)(%q) = %v, expected %v", + c.placeholder, c.input, result, c.expected) + } + }) + } +} + +func TestStartValueFunc(t *testing.T) { + t.Parallel() + + for c := range startValueCases() { + t.Run(fmt.Sprintf("%q extracts from %q", c.placeholder, c.input), func(t *testing.T) { + t.Parallel() + + matcher := StartValueFunc(c.placeholder) + value, ok := matcher(c.input) + + if ok != c.expectedOk { + t.Errorf("StartValueFunc(%q)(%q) ok = %v, expected %v", + c.placeholder, c.input, ok, c.expectedOk) + } + + if value != c.expectedValue { + t.Errorf("StartValueFunc(%q)(%q) value = %q, expected %q", + c.placeholder, c.input, value, c.expectedValue) + } + }) + } +} + +func TestStartAnotherSection(t *testing.T) { + t.Parallel() + + for c := range startAnotherSectionCases() { + t.Run(c.input, func(t *testing.T) { + t.Parallel() + + result := StartAnotherSection(c.input) + + if result != c.expected { + t.Errorf("StartAnotherSection(%q) = %v, expected %v", + c.input, result, c.expected) + } + }) + } +} + +/* Test case iterators */ + +type startSectionCase struct { + placeholder string + input string + expected bool +} + +func startSectionCases() iter.Seq[startSectionCase] { + return slices.Values([]startSectionCase{ + // Markdown-style headers + {"Example", "# Example", true}, + {"Example", "# Examples", true}, + {"Example", "# EXAMPLES", true}, + {"Example", " # Examples", true}, + {"Example", "# Examples", true}, + + // Colon-style headers + {"Example", "Example:", true}, + {"Example", "Examples:", true}, + {"Example", "EXAMPLES:", true}, + {"Example", " Examples:", true}, + {"Example", "Examples :", true}, + + // Domain examples + {"Domain", "# Domains", true}, + {"Domain", "Domains:", true}, + + // Negative cases + {"Example", "# Usage", false}, + {"Example", "Examples", false}, + {"Example", "# Examples are here", false}, + {"Example", "", false}, + {"Example", "Some Examples:", false}, + + // Special characters in placeholder + {"Pre-commit", "# Pre-commits", true}, + {"Test(s)", "# Test(s)", true}, + }) +} + +type startValueCase struct { + placeholder string + input string + expectedValue string + expectedOk bool +} + +func startValueCases() iter.Seq[startValueCase] { + return slices.Values([]startValueCase{ + // Basic matching + {"success", "success: 123, 456", "123, 456", true}, + {"success", "SUCCESS: value", "value", true}, + {"failure", " failure: value", "value", true}, + {"panic", "panic : value", "value", true}, + {"domain", "domain:", "", true}, + {"note", "note: Error: something failed", "Error: something failed", true}, + + // Domain tag examples + {"domain", "domain: string", "string", true}, + {"maintainer", "maintainer: @username", "@username", true}, + + // Negative cases + {"success", "failure: value", "", false}, + {"success", "success value", "", false}, + {"success", "", "", false}, + {"success", "not success: value", "", false}, + + // Special characters + {"pre-commit", "pre-commit: hook", "hook", true}, + }) +} + +type startAnotherSectionCase struct { + input string + expected bool +} + +func startAnotherSectionCases() iter.Seq[startAnotherSectionCase] { + return slices.Values([]startAnotherSectionCase{ + // Capital letter + colon (section headers) + {"Examples:", true}, + {"Usage:", true}, + {"Notes:", true}, + {"Parameters:", true}, + + // Markdown headers + {"# Section", true}, + {"# Header", true}, + {"# examples", true}, // markdown header still matches + + // Negative cases + {"examples:", false}, + {"Examples", false}, + {"Examples are here", false}, + {"", false}, + {"This is a description line.", false}, + {"success: 123", false}, + {" Examples:", false}, // indented section header + }) +} diff --git a/codegen/internal/scanner/comments-parser/relocation.go b/codegen/internal/scanner/comments-parser/relocation.go new file mode 100644 index 000000000..2911b4f1f --- /dev/null +++ b/codegen/internal/scanner/comments-parser/relocation.go @@ -0,0 +1,235 @@ +// SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + +package parser + +import ( + "go/ast" + "go/format" + "go/token" + "strings" + "unicode" + + "github.com/go-openapi/testify/v2/codegen/internal/model" +) + +const ( + // Exception: PanicTestFunc should always use "assertions" package. + panicTestFunc = "PanicTestFunc" + assertions = "assertions" +) + +// RelocateTestValue relocates a test value expression from one package to another. +// +// It walks the AST and: +// 1. Changes qualified identifiers: fromPkg.X → toPkg.X (except exceptions) +// 2. Qualifies unqualified exported identifiers: X → toPkg.X +// +// Exception: PanicTestFunc always stays with assertions package. +func RelocateTestValue(tv model.TestValue, fromPkg, toPkg string) model.TestValue { + if tv.Expr == nil || fromPkg == "" || toPkg == "" { + return tv + } + + // Handle root-level unqualified identifier specially + expr := tv.Expr + if ident, ok := expr.(*ast.Ident); ok && shouldQualify(ident) { + // Special case for PanicTestFunc: always use assertions + pkg := toPkg + if ident.Name == panicTestFunc { + pkg = assertions + } + expr = &ast.SelectorExpr{ + X: &ast.Ident{Name: pkg}, + Sel: ident, + } + } + + // Walk the AST and modify selectors in place + ast.Inspect(expr, func(n ast.Node) bool { + if node, ok := n.(*ast.SelectorExpr); ok { + // Handle qualified identifiers: pkg.Identifier + relocateSelectorExpr(node, fromPkg, toPkg) + } + + return true + }) + + // Second pass: qualify unqualified exported identifiers in sub-expressions + qualifyUnqualifiedIdents(expr, toPkg) + + // Re-render the modified AST to string + fset := token.NewFileSet() + var buf strings.Builder + if err := format.Node(&buf, fset, expr); err != nil { + return model.TestValue{ + Raw: tv.Raw, + Expr: tv.Expr, + Error: err, + } + } + + return model.TestValue{ + Raw: buf.String(), + Expr: expr, + Error: nil, + } +} + +// relocateSelectorExpr handles selector expressions: +// 1. Package selectors: assertions.CollectT → assert.CollectT +// 2. Method calls on unqualified types: ErrTest.Error() → assert.ErrTest.Error() +func relocateSelectorExpr(sel *ast.SelectorExpr, fromPkg, toPkg string) { + ident, ok := sel.X.(*ast.Ident) + if !ok { + return + } + + // Exception: PanicTestFunc always uses assertions package + if sel.Sel.Name == panicTestFunc { + ident.Name = assertions + return + } + + // Case 1: Package selector (assertions.CollectT → assert.CollectT) + if ident.Name == fromPkg { + ident.Name = toPkg + return + } + + // Case 2: Unqualified identifier in selector (ErrTest.Error() → assert.ErrTest.Error()) + // Need to wrap the identifier in a SelectorExpr + if shouldQualify(ident) { + sel.X = qualifyIdent(ident, toPkg) + } +} + +// qualifyUnqualifiedIdents adds package qualifiers to unqualified exported identifiers. +// +// This recursively processes all type expressions in the AST, replacing unqualified +// identifiers with qualified SelectorExpr nodes. +func qualifyUnqualifiedIdents(expr ast.Expr, pkg string) { + ast.Inspect(expr, func(n ast.Node) bool { + switch parent := n.(type) { + case *ast.CallExpr: + // Qualify function name + if ident, ok := parent.Fun.(*ast.Ident); ok && shouldQualify(ident) { + parent.Fun = qualifyIdent(ident, pkg) + } + // Qualify arguments + for i, arg := range parent.Args { + if ident, ok := arg.(*ast.Ident); ok && shouldQualify(ident) { + parent.Args[i] = qualifyIdent(ident, pkg) + } + } + + case *ast.CompositeLit: + // Qualify type in composite literal + parent.Type = qualifyTypeExpr(parent.Type, pkg) + + case *ast.TypeAssertExpr: + // Qualify type in type assertion + parent.Type = qualifyTypeExpr(parent.Type, pkg) + + case *ast.StarExpr: + // Qualify type inside pointer: *T → *pkg.T + parent.X = qualifyTypeExpr(parent.X, pkg) + + case *ast.ArrayType: + // Qualify element type in array/slice: []T → []pkg.T + parent.Elt = qualifyTypeExpr(parent.Elt, pkg) + + case *ast.MapType: + // Qualify key and value types in map: map[K]V → map[pkg.K]pkg.V + parent.Key = qualifyTypeExpr(parent.Key, pkg) + parent.Value = qualifyTypeExpr(parent.Value, pkg) + + case *ast.ChanType: + // Qualify type in channel: chan T → chan pkg.T + parent.Value = qualifyTypeExpr(parent.Value, pkg) + + case *ast.Field: + // Qualify type in function parameters, struct fields, etc. + parent.Type = qualifyTypeExpr(parent.Type, pkg) + } + + return true + }) +} + +// qualifyTypeExpr recursively qualifies unqualified identifiers in a type expression. +func qualifyTypeExpr(typ ast.Expr, pkg string) ast.Expr { + if typ == nil { + return nil + } + + switch t := typ.(type) { + case *ast.Ident: + if shouldQualify(t) { + return qualifyIdent(t, pkg) + } + return t + + case *ast.StarExpr: + t.X = qualifyTypeExpr(t.X, pkg) + return t + + case *ast.ArrayType: + t.Elt = qualifyTypeExpr(t.Elt, pkg) + return t + + case *ast.MapType: + t.Key = qualifyTypeExpr(t.Key, pkg) + t.Value = qualifyTypeExpr(t.Value, pkg) + return t + + case *ast.ChanType: + t.Value = qualifyTypeExpr(t.Value, pkg) + return t + + default: + return typ + } +} + +// qualifyIdent wraps an identifier in a SelectorExpr with the appropriate package. +func qualifyIdent(ident *ast.Ident, pkg string) *ast.SelectorExpr { + // Special case for PanicTestFunc: always use assertions + targetPkg := pkg + if ident.Name == panicTestFunc { + targetPkg = assertions + } + + return &ast.SelectorExpr{ + X: &ast.Ident{Name: targetPkg}, + Sel: ident, + } +} + +// shouldQualify returns true if an identifier should be qualified with a package name. +// +// Qualifies exported identifiers (uppercase start) except: +// - Language keywords and built-ins +func shouldQualify(ident *ast.Ident) bool { + name := ident.Name + if name == "" { + return false + } + + // Don't qualify if not exported (lowercase start) + if !unicode.IsUpper(rune(name[0])) { + return false + } + + // Don't qualify language built-ins + switch name { + case "bool", "byte", "complex64", "complex128", + "error", "float32", "float64", + "int", "int8", "int16", "int32", "int64", + "rune", "string", + "uint", "uint8", "uint16", "uint32", "uint64", "uintptr": + return false + } + + return true +} diff --git a/codegen/internal/scanner/comments-parser/relocation_test.go b/codegen/internal/scanner/comments-parser/relocation_test.go new file mode 100644 index 000000000..6570eaea9 --- /dev/null +++ b/codegen/internal/scanner/comments-parser/relocation_test.go @@ -0,0 +1,219 @@ +// SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + +package parser + +import ( + "iter" + "slices" + "testing" +) + +// TestRelocateTestValue tests AST-based relocation of test values. +func TestRelocateTestValue(t *testing.T) { + t.Parallel() + + for c := range relocateTestCases() { + t.Run(c.name, func(t *testing.T) { + t.Parallel() + + // Parse the input + values := ParseTestValues(c.input) + if len(values) != 1 { + t.Fatalf("Expected 1 value, got %d", len(values)) + } + + original := values[0] + if original.Error != nil { + t.Fatalf("Parse error: %v", original.Error) + } + + // Relocate + relocated := RelocateTestValue(original, c.fromPkg, c.toPkg) + + if relocated.Error != nil { + t.Fatalf("Relocation error: %v", relocated.Error) + } + + if relocated.Raw != c.expected { + t.Errorf("Expected %q, got %q", c.expected, relocated.Raw) + } + }) + } +} + +/* Test case iterators */ + +type relocateTestCase struct { + name string + input string + fromPkg string + toPkg string + expected string +} + +func relocateTestCases() iter.Seq[relocateTestCase] { + return slices.Values([]relocateTestCase{ + { + name: "qualified selector: assertions.ErrTest → assert.ErrTest", + input: "assertions.ErrTest", + fromPkg: "assertions", + toPkg: "assert", + expected: "assert.ErrTest", + }, + { + name: "qualified selector: assertions.CollectT → assert.CollectT", + input: "assertions.CollectT", + fromPkg: "assertions", + toPkg: "assert", + expected: "assert.CollectT", + }, + { + name: "unqualified type: ErrTest → assert.ErrTest", + input: "ErrTest", + fromPkg: "assertions", + toPkg: "assert", + expected: "assert.ErrTest", // Should qualify unqualified exported ident + }, + { + name: "unqualified type: CollectT → assert.CollectT", + input: "CollectT", + fromPkg: "assertions", + toPkg: "assert", + expected: "assert.CollectT", + }, + { + name: "exception: assertions.PanicTestFunc stays assertions.PanicTestFunc", + input: "assertions.PanicTestFunc", + fromPkg: "assertions", + toPkg: "assert", + expected: "assertions.PanicTestFunc", // Exception: keep assertions + }, + { + name: "exception: PanicTestFunc → assertions.PanicTestFunc", + input: "PanicTestFunc", + fromPkg: "assertions", + toPkg: "assert", + expected: "assertions.PanicTestFunc", // Exception even when unqualified + }, + { + name: "composite literal: assertions.ErrTest{} → assert.ErrTest{}", + input: "assertions.ErrTest{}", + fromPkg: "assertions", + toPkg: "assert", + expected: "assert.ErrTest{}", + }, + { + name: "composite literal unqualified: CollectT{} → assert.CollectT{}", + input: "CollectT{}", + fromPkg: "assertions", + toPkg: "assert", + expected: "assert.CollectT{}", + }, + { + name: "function call: assertions.NewT() → assert.NewT()", + input: "assertions.NewT()", + fromPkg: "assertions", + toPkg: "assert", + expected: "assert.NewT()", + }, + { + name: "function call unqualified: NewT() → assert.NewT()", + input: "NewT()", + fromPkg: "assertions", + toPkg: "assert", + expected: "assert.NewT()", + }, + { + name: "lowercase identifier unchanged: true", + input: "true", + fromPkg: "assertions", + toPkg: "assert", + expected: "true", // Don't qualify lowercase/built-ins + }, + { + name: "nil unchanged", + input: "nil", + fromPkg: "assertions", + toPkg: "assert", + expected: "nil", + }, + { + name: "string literal unchanged", + input: `"hello"`, + fromPkg: "assertions", + toPkg: "assert", + expected: `"hello"`, + }, + { + name: "integer literal unchanged", + input: "123", + fromPkg: "assertions", + toPkg: "assert", + expected: "123", + }, + { + name: "complex expression with multiple relocations", + input: "func(t assertions.TestingT, fn assertions.PanicTestFunc) bool { return true }", + fromPkg: "assertions", + toPkg: "assert", + expected: "func(t assert.TestingT, fn assertions.PanicTestFunc) bool {\n\treturn true\n}", // go/format adds newlines + }, + { + name: "pointer type: *CollectT → *assert.CollectT", + input: "*CollectT", + fromPkg: "assertions", + toPkg: "assert", + expected: "*assert.CollectT", + }, + { + name: "function with pointer parameter: func(c *CollectT) → func(c *assert.CollectT)", + input: "func(c *CollectT) { }", + fromPkg: "assertions", + toPkg: "assert", + expected: "func(c *assert.CollectT) {\n}", // go/format adds newline + }, + { + name: "slice type: []TestingT → []assert.TestingT", + input: "[]TestingT", + fromPkg: "assertions", + toPkg: "assert", + expected: "[]assert.TestingT", + }, + { + name: "map type: map[string]TestingT → map[string]assert.TestingT", + input: "map[string]TestingT", + fromPkg: "assertions", + toPkg: "assert", + expected: "map[string]assert.TestingT", + }, + { + name: "channel type: chan CollectT → chan assert.CollectT", + input: "chan CollectT", + fromPkg: "assertions", + toPkg: "assert", + expected: "chan assert.CollectT", + }, + { + name: "method call on unqualified variable: ErrTest.Error() → assert.ErrTest.Error()", + input: "ErrTest.Error()", + fromPkg: "assertions", + toPkg: "assert", + expected: "assert.ErrTest.Error()", + }, + { + name: "field access on unqualified variable: Config.Value → assert.Config.Value", + input: "Config.Value", + fromPkg: "assertions", + toPkg: "assert", + expected: "assert.Config.Value", + }, + { + name: "function call with unqualified argument: panic(ErrTest) → panic(assert.ErrTest)", + input: "panic(ErrTest)", + fromPkg: "assertions", + toPkg: "assert", + expected: "panic(assert.ErrTest)", + }, + }) +} diff --git a/codegen/internal/scanner/comments-parser/tags.go b/codegen/internal/scanner/comments-parser/tags.go new file mode 100644 index 000000000..51dd8aa13 --- /dev/null +++ b/codegen/internal/scanner/comments-parser/tags.go @@ -0,0 +1,147 @@ +// SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + +package parser + +import ( + "regexp" + "strings" + + "github.com/go-openapi/testify/v2/codegen/internal/model" +) + +// ParseTaggedComments extracts tagged comments from text. +// +// Recognized tags: +// - domain: - single-line domain tag +// - maintainer: - multi-line maintainer note +// - note: - multi-line note +// - mention: - single-line mention +// +// Multi-line tags continue until the next tagged line or end of text. +func ParseTaggedComments(text string) []model.ExtraComment { + const ( + domainPrefix = "domain" + maintainerPrefix = "maintainer" + notePrefix = "note" + mentionPrefix = "mention" + ) + + inValue := false + startValueDomain := StartValueFunc(domainPrefix) + startValueMaintainer := StartValueFunc(maintainerPrefix) + startValueNote := StartValueFunc(notePrefix) + startValueMention := StartValueFunc(mentionPrefix) + startTaggedValue := func(line string) (key string, val string, tag model.CommentTag, multiline bool, ok bool) { + val, ok = startValueDomain(line) + if ok { + return val, "", model.CommentTagDomain, false, true + } + val, ok = startValueMaintainer(line) + if ok { + return "", val, model.CommentTagMaintainer, true, true + } + val, ok = startValueNote(line) + if ok { + return "", val, model.CommentTagNote, true, true + } + val, ok = startValueMention(line) + if ok { + return "", val, model.CommentTagMention, false, true + } + + return "", "", model.CommentTagNone, false, false + } + + result := make([]model.ExtraComment, 0) + + for line := range strings.SplitSeq(text, "\n") { + key, val, tag, multiline, ok := startTaggedValue(line) + if inValue && len(result) > 0 && len(line) > 0 && !ok { + result[len(result)-1].Text += "\n" + line + + continue + } + + if ok { + if multiline { + inValue = true + } + + result = append(result, model.ExtraComment{ + Tag: tag, + Key: key, + Text: val, + }) + + continue + } + + inValue = false + } + + return result +} + +// ParseDomainDescriptions extracts domain descriptions from package-level comment text. +// +// It looks for a "Domains:" or "Domain:" section with entries like: +// - string: assertions on strings +// - json: assertions on JSON documents +// +// The domain name is normalized to lowercase. +func ParseDomainDescriptions(text string) []model.ExtraComment { + const allocatedDomains = 20 + + result := make([]model.ExtraComment, 0, allocatedDomains) + + const ( + domainPrefix = "domain" // e.g. domain:, domains:, # domain, # Domains + rexCapturedGroups = 2 + ) + + domainRex := regexp.MustCompile(`^\s*(?:(?:[\-\*])|(?:\d+\.))\s+(\w+)\s*:\s+(.+)`) + startDomainSection := StartSectionFunc(domainPrefix) + inDomainSection := false + + for line := range strings.SplitSeq(text, "\n") { + if startDomainSection(line) { + inDomainSection = true + continue + } + if !inDomainSection { + continue + } + + if StartAnotherSection(line) { + break + } + + // parse domain descriptions + matches := domainRex.FindStringSubmatch(line) + if len(matches) < rexCapturedGroups+1 { + continue + } + domain := strings.ToLower(matches[1]) + description := strings.TrimSpace(matches[2]) + + result = append(result, model.ExtraComment{ + Tag: model.CommentTagDomainDescription, + Key: domain, + Text: description, + }) + } + + return result +} + +// DomainFromExtraComments retrieves the domain from preparsed extra (tagged) comment. +func DomainFromExtraComments(taggedComments []model.ExtraComment) string { + for _, taggedComment := range taggedComments { + if taggedComment.Tag == model.CommentTagDomain { + return taggedComment.Key + } + } + + return "" +} diff --git a/codegen/internal/scanner/comments-parser/tags_test.go b/codegen/internal/scanner/comments-parser/tags_test.go new file mode 100644 index 000000000..776e36d99 --- /dev/null +++ b/codegen/internal/scanner/comments-parser/tags_test.go @@ -0,0 +1,407 @@ +// SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + +package parser + +import ( + "iter" + "slices" + "testing" + + "github.com/go-openapi/testify/v2/codegen/internal/model" +) + +func TestParseTaggedComments(t *testing.T) { + t.Parallel() + + for c := range parseTaggedCommentsCases() { + t.Run(c.name, func(t *testing.T) { + t.Parallel() + + result := ParseTaggedComments(c.input) + + if len(result) != len(c.expected) { + t.Fatalf("ParseTaggedComments() returned %d comments, expected %d\nGot: %+v\nExpected: %+v", + len(result), len(c.expected), result, c.expected) + } + + for i, expected := range c.expected { + got := result[i] + if got.Tag != expected.Tag { + t.Errorf("Comment[%d].Tag = %v, expected %v", i, got.Tag, expected.Tag) + } + if got.Key != expected.Key { + t.Errorf("Comment[%d].Key = %q, expected %q", i, got.Key, expected.Key) + } + if got.Text != expected.Text { + t.Errorf("Comment[%d].Text = %q, expected %q", i, got.Text, expected.Text) + } + } + }) + } +} + +func TestParseDomainDescriptions(t *testing.T) { + t.Parallel() + + for c := range parseDomainDescriptionsCases() { + t.Run(c.name, func(t *testing.T) { + t.Parallel() + + result := ParseDomainDescriptions(c.input) + + if len(result) != len(c.expected) { + t.Fatalf("ParseDomainDescriptions() returned %d domains, expected %d\nGot: %+v\nExpected: %+v", + len(result), len(c.expected), result, c.expected) + } + + for i, expected := range c.expected { + got := result[i] + if got.Tag != expected.Tag { + t.Errorf("Domain[%d].Tag = %v, expected %v", i, got.Tag, expected.Tag) + } + if got.Key != expected.Key { + t.Errorf("Domain[%d].Key = %q, expected %q", i, got.Key, expected.Key) + } + if got.Text != expected.Text { + t.Errorf("Domain[%d].Text = %q, expected %q", i, got.Text, expected.Text) + } + } + }) + } +} + +func TestDomainFromExtraComments(t *testing.T) { + t.Parallel() + + for c := range domainFromExtraCommentsCases() { + t.Run(c.name, func(t *testing.T) { + t.Parallel() + + result := DomainFromExtraComments(c.comments) + + if result != c.expected { + t.Errorf("DomainFromExtraComments() = %q, expected %q", result, c.expected) + } + }) + } +} + +/* Test case iterators */ + +type parseTaggedCommentsCase struct { + name string + input string + expected []model.ExtraComment +} + +func parseTaggedCommentsCases() iter.Seq[parseTaggedCommentsCase] { + return slices.Values([]parseTaggedCommentsCase{ + { + name: "single domain tag", + input: `Some description. +domain: string +More text.`, + expected: []model.ExtraComment{ + {Tag: model.CommentTagDomain, Key: "string", Text: ""}, + }, + }, + { + name: "multiple domain tags", + input: `domain: string +domain: boolean`, + expected: []model.ExtraComment{ + {Tag: model.CommentTagDomain, Key: "string", Text: ""}, + {Tag: model.CommentTagDomain, Key: "boolean", Text: ""}, + }, + }, + { + name: "maintainer tag single line", + input: `maintainer: @username`, + expected: []model.ExtraComment{ + {Tag: model.CommentTagMaintainer, Key: "", Text: "@username"}, + }, + }, + { + name: "maintainer tag multiline", + input: `maintainer: This function needs review. +It has some edge cases that should be documented. +The implementation could be improved.`, + expected: []model.ExtraComment{ + {Tag: model.CommentTagMaintainer, Key: "", Text: "This function needs review.\nIt has some edge cases that should be documented.\nThe implementation could be improved."}, + }, + }, + { + name: "note tag multiline", + input: `note: This is important. +This continues the note. +And this too.`, + expected: []model.ExtraComment{ + {Tag: model.CommentTagNote, Key: "", Text: "This is important.\nThis continues the note.\nAnd this too."}, + }, + }, + { + name: "mention tag", + input: `mention: Related to issue #123`, + expected: []model.ExtraComment{ + {Tag: model.CommentTagMention, Key: "", Text: "Related to issue #123"}, + }, + }, + { + name: "mixed tags", + input: `domain: error +maintainer: @alice +This needs attention. +note: Check performance impact. +mention: See also HTTPError`, + expected: []model.ExtraComment{ + {Tag: model.CommentTagDomain, Key: "error", Text: ""}, + {Tag: model.CommentTagMaintainer, Key: "", Text: "@alice\nThis needs attention."}, + {Tag: model.CommentTagNote, Key: "", Text: "Check performance impact."}, + {Tag: model.CommentTagMention, Key: "", Text: "See also HTTPError"}, + }, + }, + { + name: "multiline interrupted by new tag", + input: `maintainer: First maintainer note. +This continues. +note: New section starts here. +And continues.`, + expected: []model.ExtraComment{ + {Tag: model.CommentTagMaintainer, Key: "", Text: "First maintainer note.\nThis continues."}, + {Tag: model.CommentTagNote, Key: "", Text: "New section starts here.\nAnd continues."}, + }, + }, + { + name: "case insensitive tags", + input: `DOMAIN: string +Maintainer: @bob +NOTE: Important info`, + expected: []model.ExtraComment{ + {Tag: model.CommentTagDomain, Key: "string", Text: ""}, + {Tag: model.CommentTagMaintainer, Key: "", Text: "@bob"}, + {Tag: model.CommentTagNote, Key: "", Text: "Important info"}, + }, + }, + { + name: "no tags", + input: `Just regular text without any tags.`, + expected: []model.ExtraComment{}, + }, + { + name: "empty input", + input: "", + expected: []model.ExtraComment{}, + }, + { + name: "tags with extra whitespace", + input: ` domain : string + maintainer : @user `, + expected: []model.ExtraComment{ + {Tag: model.CommentTagDomain, Key: "string", Text: ""}, + {Tag: model.CommentTagMaintainer, Key: "", Text: "@user "}, + }, + }, + { + name: "multiline ends with empty line", + input: `note: First line. +Second line. + +domain: string`, + expected: []model.ExtraComment{ + {Tag: model.CommentTagNote, Key: "", Text: "First line.\nSecond line."}, + {Tag: model.CommentTagDomain, Key: "string", Text: ""}, + }, + }, + }) +} + +type parseDomainDescriptionsCase struct { + name string + input string + expected []model.ExtraComment +} + +func parseDomainDescriptionsCases() iter.Seq[parseDomainDescriptionsCase] { + return slices.Values([]parseDomainDescriptionsCase{ + { + name: "domains section with bullet points", + input: `Package assertions provides testing utilities. + +Domains: + - string: assertions on strings + - json: assertions on JSON documents + - yaml: assertions on YAML documents`, + expected: []model.ExtraComment{ + {Tag: model.CommentTagDomainDescription, Key: "string", Text: "assertions on strings"}, + {Tag: model.CommentTagDomainDescription, Key: "json", Text: "assertions on JSON documents"}, + {Tag: model.CommentTagDomainDescription, Key: "yaml", Text: "assertions on YAML documents"}, + }, + }, + { + name: "domains section with asterisks", + input: `Domains: + * boolean: true/false assertions + * number: numeric comparisons`, + expected: []model.ExtraComment{ + {Tag: model.CommentTagDomainDescription, Key: "boolean", Text: "true/false assertions"}, + {Tag: model.CommentTagDomainDescription, Key: "number", Text: "numeric comparisons"}, + }, + }, + { + name: "domains section with numbered list", + input: `Domains: + 1. error: error handling assertions + 2. http: HTTP response assertions`, + expected: []model.ExtraComment{ + {Tag: model.CommentTagDomainDescription, Key: "error", Text: "error handling assertions"}, + {Tag: model.CommentTagDomainDescription, Key: "http", Text: "HTTP response assertions"}, + }, + }, + { + name: "markdown header style", + input: `# Domains + + - file: file system assertions + - time: time-based assertions`, + expected: []model.ExtraComment{ + {Tag: model.CommentTagDomainDescription, Key: "file", Text: "file system assertions"}, + {Tag: model.CommentTagDomainDescription, Key: "time", Text: "time-based assertions"}, + }, + }, + { + name: "case insensitive domain header", + input: `DOMAINS: + - panic: panic assertions`, + expected: []model.ExtraComment{ + {Tag: model.CommentTagDomainDescription, Key: "panic", Text: "panic assertions"}, + }, + }, + { + name: "singular form", + input: `Domain: + - collection: collection assertions`, + expected: []model.ExtraComment{ + {Tag: model.CommentTagDomainDescription, Key: "collection", Text: "collection assertions"}, + }, + }, + { + name: "stops at next section", + input: `Domains: + - string: string assertions + - json: JSON assertions + +Usage: + - boolean: this should not be parsed`, + expected: []model.ExtraComment{ + {Tag: model.CommentTagDomainDescription, Key: "string", Text: "string assertions"}, + {Tag: model.CommentTagDomainDescription, Key: "json", Text: "JSON assertions"}, + }, + }, + { + name: "normalizes domain names to lowercase", + input: `Domains: + - String: string assertions + - JSON: JSON assertions`, + expected: []model.ExtraComment{ + {Tag: model.CommentTagDomainDescription, Key: "string", Text: "string assertions"}, + {Tag: model.CommentTagDomainDescription, Key: "json", Text: "JSON assertions"}, + }, + }, + { + name: "handles extra whitespace", + input: `Domains: + - string : assertions on strings `, + expected: []model.ExtraComment{ + {Tag: model.CommentTagDomainDescription, Key: "string", Text: "assertions on strings"}, + }, + }, + { + name: "no domains section", + input: `This package has no domains section.`, + expected: []model.ExtraComment{}, + }, + { + name: "domains section with no entries", + input: `Domains: + +Usage follows.`, + expected: []model.ExtraComment{}, + }, + { + name: "skip non-matching lines", + input: `Domains: + Some intro text. + - string: string assertions + Not a domain line. + - json: JSON assertions`, + expected: []model.ExtraComment{ + {Tag: model.CommentTagDomainDescription, Key: "string", Text: "string assertions"}, + {Tag: model.CommentTagDomainDescription, Key: "json", Text: "JSON assertions"}, + }, + }, + { + name: "description with colons", + input: `Domains: + - http: HTTP assertions: status codes, headers, bodies`, + expected: []model.ExtraComment{ + {Tag: model.CommentTagDomainDescription, Key: "http", Text: "HTTP assertions: status codes, headers, bodies"}, + }, + }, + }) +} + +type domainFromExtraCommentsCase struct { + name string + comments []model.ExtraComment + expected string +} + +func domainFromExtraCommentsCases() iter.Seq[domainFromExtraCommentsCase] { + return slices.Values([]domainFromExtraCommentsCase{ + { + name: "find domain tag", + comments: []model.ExtraComment{ + {Tag: model.CommentTagNote, Key: "", Text: "Some note"}, + {Tag: model.CommentTagDomain, Key: "string", Text: ""}, + {Tag: model.CommentTagMaintainer, Key: "", Text: "@user"}, + }, + expected: "string", + }, + { + name: "first domain wins", + comments: []model.ExtraComment{ + {Tag: model.CommentTagDomain, Key: "boolean", Text: ""}, + {Tag: model.CommentTagDomain, Key: "string", Text: ""}, + }, + expected: "boolean", + }, + { + name: "no domain tag", + comments: []model.ExtraComment{ + {Tag: model.CommentTagNote, Key: "", Text: "Note"}, + {Tag: model.CommentTagMaintainer, Key: "", Text: "@user"}, + }, + expected: "", + }, + { + name: "empty comments", + comments: []model.ExtraComment{}, + expected: "", + }, + { + name: "nil comments", + comments: nil, + expected: "", + }, + { + name: "domain at end", + comments: []model.ExtraComment{ + {Tag: model.CommentTagMaintainer, Key: "", Text: "@user"}, + {Tag: model.CommentTagNote, Key: "", Text: "Note"}, + {Tag: model.CommentTagDomain, Key: "error", Text: ""}, + }, + expected: "error", + }, + }) +} diff --git a/codegen/internal/scanner/comments/bridge.go b/codegen/internal/scanner/comments/bridge.go new file mode 100644 index 000000000..51b0c6cd6 --- /dev/null +++ b/codegen/internal/scanner/comments/bridge.go @@ -0,0 +1,72 @@ +// SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + +package comments + +import ( + "go/ast" + "go/token" + "go/types" + + "golang.org/x/tools/go/ast/astutil" + "golang.org/x/tools/go/packages" +) + +// BuildFilesMap constructs a lookup index to help bridge token position vs ast.File. +// +// This creates a map from token.File (file metadata) to ast.File (syntax tree), +// enabling O(1) lookup after O(n) construction. +func BuildFilesMap(pkg *packages.Package) map[*token.File]*ast.File { + filesMap := make(map[*token.File]*ast.File, len(pkg.Syntax)) + + for _, astFile := range pkg.Syntax { + tokenFile := pkg.Fset.File(astFile.Pos()) // o(log n) lookup + filesMap[tokenFile] = astFile + } + + return filesMap +} + +// resolveDeclFromObject finds the AST declaration for a types.Object using position-based lookup. +// +// Returns both the declaration and the file containing it, or (nil, nil) if not found. +func resolveDeclFromObject(object types.Object, fileSet *token.FileSet, filesMap map[*token.File]*ast.File) (ast.Decl, *ast.File) { + pos := object.Pos() + if !pos.IsValid() { + return nil, nil + } + + tokenFile := fileSet.File(pos) // o(log n) lookup + if tokenFile == nil { + return nil, nil + } + + astFile := filesMap[tokenFile] // o(1) lookup + if astFile == nil { + return nil, nil + } + + path, _ := astutil.PathEnclosingInterval(astFile, pos, pos) + for _, node := range path { + declaration, ok := node.(ast.Decl) + if !ok { + continue + } + + return declaration, astFile + } + + return nil, astFile +} + +// findCommentInFile finds a comment group within a position range. +func findCommentInFile(astFile *ast.File, minPos, maxPos token.Pos) *ast.CommentGroup { + for _, commentGroup := range astFile.Comments { + pos := commentGroup.Pos() + if pos > minPos && pos < maxPos { + return commentGroup + } + } + + return nil +} diff --git a/codegen/internal/scanner/comments/doc.go b/codegen/internal/scanner/comments/doc.go new file mode 100644 index 000000000..938414655 --- /dev/null +++ b/codegen/internal/scanner/comments/doc.go @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + +// Package comments provides utilities for extracting comments from Go source code. +// +// This package handles the bridge between go/types (semantic view) and go/ast (syntactic view) +// using position-based lookup. It extracts: +// +// - Package-level comments and copyright headers +// - Doc comments for functions, types, constants, and variables +// - Body comments from within functions and type declarations +// - Domain descriptions from package comments +// +// The Extractor type maintains the context needed for position-based lookup, +// including the file set, files map, and declaration cache. +package comments diff --git a/codegen/internal/scanner/comments/extractor.go b/codegen/internal/scanner/comments/extractor.go new file mode 100644 index 000000000..7393ca184 --- /dev/null +++ b/codegen/internal/scanner/comments/extractor.go @@ -0,0 +1,299 @@ +// SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + +package comments + +import ( + "go/ast" + "go/token" + "go/types" + "regexp" + "strings" + + "github.com/go-openapi/testify/v2/codegen/internal/model" + parser "github.com/go-openapi/testify/v2/codegen/internal/scanner/comments-parser" +) + +var copyrightRex = regexp.MustCompile(`(?i)copyright`) + +type cacheEntry struct { + Decl ast.Decl + File *ast.File +} + +// Extractor extracts comments from Go source code using position-based AST/types bridging. +type Extractor struct { + syntaxPackage []*ast.File + fileSet *token.FileSet + filesMap map[*token.File]*ast.File + declCache map[types.Object]cacheEntry +} + +// New creates a comment extractor. +// +// The syntaxPackage provides access to all AST files in the package. +// The fileSet and filesMap enable position-based lookup to bridge types.Object to ast.Decl. +func New(syntaxPackage []*ast.File, fileSet *token.FileSet, filesMap map[*token.File]*ast.File) *Extractor { + return &Extractor{ + syntaxPackage: syntaxPackage, + fileSet: fileSet, + filesMap: filesMap, + declCache: make(map[types.Object]cacheEntry), + } +} + +// ExtractPackageComments finds the package-level comment that describes the source package. +// +// This is a single comment group from the first file that has package documentation. +// Never returns nil. +func (e *Extractor) ExtractPackageComments() *ast.CommentGroup { + for _, file := range e.syntaxPackage { + if file.Doc == nil { + continue + } + + return file.Doc + } + + // safeguard: never returns nil + return &ast.CommentGroup{} +} + +// ExtractCopyright scans source files and expects that at least one contains +// a leading comment line with the string "copyright". +// +// Never returns nil. +func (e *Extractor) ExtractCopyright() *ast.CommentGroup { +FILE: + for _, file := range e.syntaxPackage { + if len(file.Comments) == 0 { + continue + } + + // Check all comment groups before the package declaration + // (needed for files with build tags, where copyright is not the first group) + for _, group := range file.Comments { + // Skip comment groups that come after the package declaration + if group.Pos() >= file.Package { + continue FILE + } + + // Check if this group contains a copyright notice + for _, line := range group.List { + if copyrightRex.MatchString(line.Text) { + return group + } + } + } + } + + // safeguard: never returns nil + return &ast.CommentGroup{} +} + +// ExtractComments retrieves the docstring for an object. +// +// Never returns nil: if no comment is present an empty but valid [ast.CommentGroup] is returned. +// +// NOTE: comment prioritization rule is: +// 1. GenDecl.Doc = leading comment for the entire declaration block +// 2. Spec.Doc = individual doc for that specific spec +func (e *Extractor) ExtractComments(object types.Object) *ast.CommentGroup { + entry, ok := e.declCache[object] + if ok { + // short-circuit already resolved declarations + if entry.Decl == nil { + return &ast.CommentGroup{} + } + + return extractCommentFromDecl(entry.Decl, object) + } + + declaration, astFile := resolveDeclFromObject(object, e.fileSet, e.filesMap) + if declaration == nil { + e.declCache[object] = cacheEntry{} + + return &ast.CommentGroup{} + } + + e.declCache[object] = cacheEntry{ + Decl: declaration, + File: astFile, + } + + return extractCommentFromDecl(declaration, object) +} + +// ExtractExtraComments retrieves body comments (inside functions or type declarations). +// +// Returns nil if no extra comments are found. +func (e *Extractor) ExtractExtraComments(object types.Object) []model.ExtraComment { + entry, ok := e.declCache[object] + if ok { + // short-circuit already resolved declarations + if entry.Decl == nil { + return nil + } + + return extractExtraCommentsFromDecl(entry.Decl, entry.File, object) + } + + declaration, astFile := resolveDeclFromObject(object, e.fileSet, e.filesMap) + if declaration == nil || astFile == nil { + e.declCache[object] = cacheEntry{} + + return nil + } + + e.declCache[object] = cacheEntry{ + Decl: declaration, + File: astFile, + } + + return extractExtraCommentsFromDecl(declaration, astFile, object) +} + +// ExtractDomainDescriptions extracts domain descriptions from package-level comments. +// +// Returns nil if no domain descriptions are found. +func (e *Extractor) ExtractDomainDescriptions() []model.ExtraComment { + // find the documented file (e.g. doc.go) that contains package-level comments + var documentedFile *ast.File + + for _, file := range e.syntaxPackage { + if file.Doc == nil { + continue + } + + documentedFile = file + break + } + + if documentedFile == nil { + return nil + } + + // collect all comment text from the documented file + var allComments strings.Builder + for _, group := range documentedFile.Comments { + if group != nil { + allComments.WriteString(group.Text()) + allComments.WriteString("\n") + } + } + + return parser.ParseDomainDescriptions(allComments.String()) +} + +func extractCommentFromDecl(declaration ast.Decl, object types.Object) *ast.CommentGroup { + switch typedDeclaration := declaration.(type) { + case *ast.GenDecl: // const, var, type declaration + for _, spec := range typedDeclaration.Specs { + comment := extractCommentFromTypeSpec(typedDeclaration, spec, object) + if comment != nil { + return comment + } + } + case *ast.FuncDecl: // a function + if typedDeclaration.Doc != nil { + return typedDeclaration.Doc + } + } + + return &ast.CommentGroup{} +} + +func extractExtraCommentsFromDecl(declaration ast.Decl, astFile *ast.File, object types.Object) []model.ExtraComment { + var comments *ast.CommentGroup + + switch typedDeclaration := declaration.(type) { + case *ast.GenDecl: // const, var, type declaration + for _, spec := range typedDeclaration.Specs { + comments = extractExtraCommentFromTypeSpec(astFile, spec, object) + if comments != nil { + break + } + } + case *ast.FuncDecl: // a function + bodyStart := typedDeclaration.Body.Lbrace + bodyEnd := typedDeclaration.Body.Rbrace + comments = findCommentInFile(astFile, bodyStart, bodyEnd) + } + + if comments == nil { + return nil + } + + return parser.ParseTaggedComments(comments.Text()) +} + +func extractCommentFromTypeSpec(typedDeclaration *ast.GenDecl, spec ast.Spec, object types.Object) *ast.CommentGroup { + switch typedSpec := spec.(type) { + case *ast.TypeSpec: + // for TypeSpec, check if it matches (though usually only 1 spec per GenDecl for types) + if typedSpec.Name.Name != object.Name() { + return nil + } + + // return Doc, checking both GenDecl.Doc and TypeSpec.Doc + if typedDeclaration.Doc != nil { + return typedDeclaration.Doc + } + + if typedSpec.Doc != nil { + return typedSpec.Doc + } + case *ast.ValueSpec: + for _, ident := range typedSpec.Names { + if ident.Name != object.Name() { + return nil + } + + // return Doc, checking both GenDecl.Doc and ValueSpec.Doc + if typedDeclaration.Doc != nil { + return typedDeclaration.Doc + } + + if typedSpec.Doc != nil { + return typedSpec.Doc + } + } + } + + return nil +} + +func extractExtraCommentFromTypeSpec(astFile *ast.File, spec ast.Spec, object types.Object) *ast.CommentGroup { + typedSpec, isTypeSpec := spec.(*ast.TypeSpec) + if !isTypeSpec { + // case *ast.ValueSpec: // not supported for now: only types + return nil + } + + // for TypeSpec, check if it matches (though usually only 1 spec per GenDecl for types) + if typedSpec.Name.Name != object.Name() { + return nil + } + + var bodyStart, bodyEnd token.Pos + + switch kind := typedSpec.Type.(type) { + case *ast.StructType: + bodyStart = kind.Fields.Opening // position of '{' + bodyEnd = kind.Fields.Closing // position of '}' + + case *ast.InterfaceType: + bodyStart = kind.Methods.Opening // position of '{' + bodyEnd = kind.Methods.Closing // position of '}' + // other type declarations (maps, functions, ...): not supported for now + } + + if bodyEnd > bodyStart { + comments := findCommentInFile(astFile, bodyStart, bodyEnd) + if comments != nil { + return comments + } + } + + return nil +} diff --git a/codegen/internal/scanner/comments/extractor_test.go b/codegen/internal/scanner/comments/extractor_test.go new file mode 100644 index 000000000..4353c9416 --- /dev/null +++ b/codegen/internal/scanner/comments/extractor_test.go @@ -0,0 +1,519 @@ +// SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + +package comments + +import ( + "strings" + "sync" + "testing" + + "golang.org/x/tools/go/packages" + + "github.com/go-openapi/testify/v2/codegen/internal/model" +) + +//nolint:gochecknoglobals // we use [sync.Once] to cache the parsed test source and run the tests faster +var ( + loadTestPackageOnce sync.Once + cachedTestPackage *packages.Package +) + +// loadTestPackage loads the testdata/assertions package for testing. +func loadTestPackage(t *testing.T) (*packages.Package, *Extractor) { + t.Helper() + + loadTestPackageOnce.Do(func() { + cfg := &packages.Config{ + Mode: packages.NeedName | + packages.NeedFiles | + packages.NeedCompiledGoFiles | + packages.NeedImports | + packages.NeedDeps | + packages.NeedTypes | + packages.NeedSyntax | + packages.NeedTypesInfo, + BuildFlags: []string{"-tags", "integrationtest"}, + } + + pkgs, err := packages.Load(cfg, "./testdata/assertions") + if err != nil { + t.Fatalf("Failed to load test package: %v", err) + } + + if len(pkgs) != 1 { + t.Fatalf("Expected 1 package, got %d", len(pkgs)) + } + + pkg := pkgs[0] + if len(pkg.Errors) > 0 { + t.Fatalf("Package has errors: %v", pkg.Errors) + } + + cachedTestPackage = pkg + }) + + pkg := cachedTestPackage + filesMap := BuildFilesMap(pkg) + extractor := New(pkg.Syntax, pkg.Fset, filesMap) + + return pkg, extractor +} + +func TestNew(t *testing.T) { + _, extractor := loadTestPackage(t) + + if extractor == nil { + t.Fatal("New() returned nil") + } + if extractor.syntaxPackage == nil { + t.Error("syntaxPackage is nil") + } + if extractor.fileSet == nil { + t.Error("fileSet is nil") + } + if extractor.filesMap == nil { + t.Error("filesMap is nil") + } + if extractor.declCache == nil { + t.Error("declCache is nil") + } +} + +func TestExtractPackageComments(t *testing.T) { + _, extractor := loadTestPackage(t) + + comments := extractor.ExtractPackageComments() + + if comments == nil { + t.Fatal("ExtractPackageComments() returned nil") + } + + text := comments.Text() + if text == "" { + t.Error("Package comments are empty") + } + + // Should contain package description from doc.go + if !strings.Contains(text, "Package assertions") { + t.Errorf("Package comments should mention 'Package assertions', got: %s", text) + } +} + +func TestExtractCopyright(t *testing.T) { + _, extractor := loadTestPackage(t) + + copyright := extractor.ExtractCopyright() + + if copyright == nil { + t.Fatal("ExtractCopyright() returned nil") + } + + text := copyright.Text() + if text == "" { + t.Error("Copyright is empty") + } + + // Should contain copyright notice + if !strings.Contains(strings.ToLower(text), "copyright") { + t.Errorf("Copyright should contain 'copyright', got: %s", text) + } + + // Should contain SPDX header + if !strings.Contains(text, "SPDX-FileCopyrightText") { + t.Errorf("Copyright should contain SPDX header, got: %s", text) + } +} + +func TestExtractDomainDescriptions(t *testing.T) { + _, extractor := loadTestPackage(t) + + domains := extractor.ExtractDomainDescriptions() + + if domains == nil { + t.Fatal("ExtractDomainDescriptions() returned nil") + } + + if len(domains) == 0 { + t.Error("Expected domain descriptions, got none") + } + + // Verify we found expected domains + expectedDomains := map[string]bool{ + "boolean": false, + "collection": false, + "compare": false, + "equal": false, + "error": false, + } + + for _, domain := range domains { + if domain.Tag != model.CommentTagDomainDescription { + t.Errorf("Expected tag %v, got %v", model.CommentTagDomainDescription, domain.Tag) + } + + if _, exists := expectedDomains[domain.Key]; exists { + expectedDomains[domain.Key] = true + } + + // Verify description is not empty + if domain.Text == "" { + t.Errorf("Domain %q has empty description", domain.Key) + } + } + + // Check that we found at least some expected domains + foundCount := 0 + for domain, found := range expectedDomains { + if found { + foundCount++ + } else { + t.Logf("Domain %q not found (may be OK if doc.go doesn't list it)", domain) + } + } + + if foundCount == 0 { + t.Error("Expected to find at least one domain from the expected list") + } +} + +func TestExtractComments(t *testing.T) { + pkg, extractor := loadTestPackage(t) + + // Find the "True" function + scope := pkg.Types.Scope() + trueObj := scope.Lookup("True") + if trueObj == nil { + t.Fatal("Could not find 'True' function in package scope") + } + + comments := extractor.ExtractComments(trueObj) + + if comments == nil { + t.Fatal("ExtractComments() returned nil") + } + + text := comments.Text() + if text == "" { + t.Error("Function comments are empty") + } + + // Should contain function description + if !strings.Contains(text, "True asserts") { + t.Errorf("Expected function description, got: %s", text) + } + + // Should contain Examples section + if !strings.Contains(text, "Examples:") { + t.Errorf("Expected Examples section, got: %s", text) + } +} + +func TestExtractComments_Caching(t *testing.T) { + pkg, extractor := loadTestPackage(t) + + scope := pkg.Types.Scope() + trueObj := scope.Lookup("True") + if trueObj == nil { + t.Fatal("Could not find 'True' function") + } + + // First call - should populate cache + comments1 := extractor.ExtractComments(trueObj) + + // Second call - should use cache + comments2 := extractor.ExtractComments(trueObj) + + // Should return same content + if comments1.Text() != comments2.Text() { + t.Error("Cached comments differ from original") + } + + // Verify cache was populated + if _, cached := extractor.declCache[trueObj]; !cached { + t.Error("Object not found in cache after ExtractComments()") + } +} + +func TestExtractExtraComments(t *testing.T) { + pkg, extractor := loadTestPackage(t) + + // Find the "True" function + scope := pkg.Types.Scope() + trueObj := scope.Lookup("True") + if trueObj == nil { + t.Fatal("Could not find 'True' function in package scope") + } + + extraComments := extractor.ExtractExtraComments(trueObj) + + // The True function has "// Domain: boolean" inside it + if len(extraComments) == 0 { + t.Fatal("Expected extra comments (domain tag), got none") + } + + // Should have parsed the domain tag + foundDomain := false + for _, comment := range extraComments { + if comment.Tag == model.CommentTagDomain && comment.Key == "boolean" { + foundDomain = true + break + } + } + + if !foundDomain { + t.Errorf("Expected to find domain tag with key 'boolean', got: %+v", extraComments) + } +} + +func TestExtractComments_NoComment(t *testing.T) { + pkg, extractor := loadTestPackage(t) + + // Try to get comments for a type that exists but might not have comments + scope := pkg.Types.Scope() + // Look for the "T" type (testing interface) + tObj := scope.Lookup("T") + + if tObj == nil { + // If T doesn't exist in testdata, that's fine - skip this test + t.Skip("Type 'T' not found in test package") + } + + comments := extractor.ExtractComments(tObj) + + // Should never return nil, even if no comments + if comments == nil { + t.Error("ExtractComments() returned nil for object without comments") + } +} + +func TestExtractComments_MultipleFunctions(t *testing.T) { + pkg, extractor := loadTestPackage(t) + + scope := pkg.Types.Scope() + + testCases := []struct { + funcName string + shouldContain string + }{ + { + funcName: "True", + shouldContain: "True asserts", + }, + { + funcName: "False", + shouldContain: "False asserts", + }, + } + + for _, tc := range testCases { + t.Run(tc.funcName, func(t *testing.T) { + obj := scope.Lookup(tc.funcName) + if obj == nil { + t.Fatalf("Could not find function %q", tc.funcName) + } + + comments := extractor.ExtractComments(obj) + text := comments.Text() + + if !strings.Contains(text, tc.shouldContain) { + t.Errorf("Expected comment to contain %q, got: %s", tc.shouldContain, text) + } + }) + } +} + +func TestExtractComments_TypeDeclaration(t *testing.T) { + pkg, extractor := loadTestPackage(t) + + // Find the "CollectT" type + scope := pkg.Types.Scope() + collectTObj := scope.Lookup("CollectT") + if collectTObj == nil { + t.Fatal("Could not find 'CollectT' type in package scope") + } + + comments := extractor.ExtractComments(collectTObj) + + if comments == nil { + t.Fatal("ExtractComments() returned nil for type") + } + + text := comments.Text() + if text == "" { + t.Error("Type comments are empty") + } + + // Should contain type description + if !strings.Contains(text, "CollectT") { + t.Errorf("Expected type description, got: %s", text) + } +} + +func TestExtractExtraComments_TypeWithBodyComments(t *testing.T) { + pkg, extractor := loadTestPackage(t) + + // Find the "CollectT" type which has maintainer comments inside + scope := pkg.Types.Scope() + collectTObj := scope.Lookup("CollectT") + if collectTObj == nil { + t.Fatal("Could not find 'CollectT' type") + } + + extraComments := extractor.ExtractExtraComments(collectTObj) + + // The CollectT struct has "// Maintainer:" comments inside it + if len(extraComments) == 0 { + t.Fatal("Expected extra comments (maintainer tag), got none") + } + + // Should have parsed the maintainer tag + foundMaintainer := false + for _, comment := range extraComments { + if comment.Tag == model.CommentTagMaintainer { + foundMaintainer = true + // Verify maintainer text is not empty + if comment.Text == "" { + t.Error("Maintainer comment has empty text") + } + // Should mention the maintainer notes + if !strings.Contains(comment.Text, "runtime.GoExit") && !strings.Contains(comment.Text, "deprecated") { + t.Logf("Maintainer comment: %q", comment.Text) + } + break + } + } + + if !foundMaintainer { + t.Errorf("Expected to find maintainer tag, got: %+v", extraComments) + } +} + +func TestExtractExtraComments_Caching(t *testing.T) { + pkg, extractor := loadTestPackage(t) + + scope := pkg.Types.Scope() + collectTObj := scope.Lookup("CollectT") + if collectTObj == nil { + t.Fatal("Could not find 'CollectT' type") + } + + // First call - should populate cache + extraComments1 := extractor.ExtractExtraComments(collectTObj) + + // Second call - should use cache + extraComments2 := extractor.ExtractExtraComments(collectTObj) + + // Should return same number of comments + if len(extraComments1) != len(extraComments2) { + t.Error("Cached extra comments differ from original") + } + + // Verify cache was populated + if _, cached := extractor.declCache[collectTObj]; !cached { + t.Error("Object not found in cache after ExtractExtraComments()") + } +} + +func TestExtractComments_VariableDeclaration(t *testing.T) { + pkg, extractor := loadTestPackage(t) + + // Find the "ErrTest" variable + scope := pkg.Types.Scope() + errTestObj := scope.Lookup("ErrTest") + if errTestObj == nil { + t.Fatal("Could not find 'ErrTest' variable in package scope") + } + + comments := extractor.ExtractComments(errTestObj) + + if comments == nil { + t.Fatal("ExtractComments() returned nil for variable") + } + + text := comments.Text() + if text == "" { + t.Error("Variable comments are empty") + } + + // Should contain variable description + if !strings.Contains(text, "ErrTest") { + t.Errorf("Expected variable description, got: %s", text) + } + + if !strings.Contains(text, "error instance") { + t.Errorf("Expected description 'error instance', got: %s", text) + } +} + +func TestExtractExtraComments_InterfaceWithBodyComments(t *testing.T) { + pkg, extractor := loadTestPackage(t) + + // Find the "T" interface which has maintainer comments inside + scope := pkg.Types.Scope() + tObj := scope.Lookup("T") + if tObj == nil { + t.Fatal("Could not find 'T' interface") + } + + extraComments := extractor.ExtractExtraComments(tObj) + + // Note: Comments attached to interface methods (not floating) may not be + // extracted as "extra comments" - they're associated with the method. + // This test verifies the ExtractExtraComments path for interfaces, + // even if the result is empty for interfaces with only method-attached comments. + if extraComments == nil { + // nil is acceptable for interfaces without floating comments + t.Log("No extra comments found for interface (expected if comments are method-attached)") + return + } + + // If we do have comments, verify they're parsed correctly + if len(extraComments) > 0 { + foundMaintainer := false + for _, comment := range extraComments { + if comment.Tag == model.CommentTagMaintainer { + foundMaintainer = true + // Verify maintainer text is not empty + if comment.Text == "" { + t.Error("Maintainer comment has empty text") + } + t.Logf("Found maintainer comment: %q", comment.Text) + break + } + } + + if !foundMaintainer { + t.Logf("Extra comments found but no maintainer tag: %+v", extraComments) + } + } +} + +func TestBuildFilesMap(t *testing.T) { + pkg, _ := loadTestPackage(t) + + filesMap := BuildFilesMap(pkg) + + if filesMap == nil { + t.Fatal("BuildFilesMap() returned nil") + } + + if len(filesMap) == 0 { + t.Error("BuildFilesMap() returned empty map") + } + + // Should have entries for each syntax file + if len(filesMap) != len(pkg.Syntax) { + t.Errorf("Expected %d entries in filesMap, got %d", len(pkg.Syntax), len(filesMap)) + } + + // Verify map structure + for tokenFile, astFile := range filesMap { + if tokenFile == nil { + t.Error("Found nil token.File key in filesMap") + } + if astFile == nil { + t.Error("Found nil ast.File value in filesMap") + } + } +} diff --git a/codegen/internal/scanner/comments/testdata/assertions/boolean.go b/codegen/internal/scanner/comments/testdata/assertions/boolean.go new file mode 100644 index 000000000..f6035b17c --- /dev/null +++ b/codegen/internal/scanner/comments/testdata/assertions/boolean.go @@ -0,0 +1,50 @@ +//go:build integrationtest + +// SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + +package assertions + +// True asserts that the specified value is true. +// +// Usage: +// +// assertions.True(t, myBool) +// +// Examples: +// +// success: 1 == 1 +// failure: 1 == 0 +func True(t T, value bool, msgAndArgs ...any) bool { + // Domain: boolean + if !value { + if h, ok := t.(H); ok { + h.Helper() + } + return Fail(t, "Should be true", msgAndArgs...) + } + + return true +} + +// False asserts that the specified value is false. +// +// Usage: +// +// assertions.False(t, myBool) +// +// Examples: +// +// success: 1 == 0 +// failure: 1 == 1 +func False(t T, value bool, msgAndArgs ...any) bool { + // Domain: boolean + if value { + if h, ok := t.(H); ok { + h.Helper() + } + return Fail(t, "Should be false", msgAndArgs...) + } + + return true +} diff --git a/codegen/internal/scanner/comments/testdata/assertions/condition.go b/codegen/internal/scanner/comments/testdata/assertions/condition.go new file mode 100644 index 000000000..8667d4d08 --- /dev/null +++ b/codegen/internal/scanner/comments/testdata/assertions/condition.go @@ -0,0 +1,234 @@ +//go:build integrationtest + +// SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + +package assertions + +import ( + "fmt" + "runtime" + "time" +) + +// Condition uses a Comparison to assert a complex condition. +// +// Examples: +// +// success: func() bool { return true } +// failure: func() bool { return false } +func Condition(t T, comp Comparison, msgAndArgs ...any) bool { + if h, ok := t.(H); ok { + h.Helper() + } + result := comp() + if !result { + Fail(t, "Condition failed!", msgAndArgs...) + } + return result +} + +// 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) +// +// Examples: +// +// success: func() bool { return true }, 100*time.Millisecond, 20*time.Millisecond +// failure: func() bool { return false }, 100*time.Millisecond, 20*time.Millisecond +func Eventually(t T, condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...any) bool { + if h, ok := t.(H); 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 [T] interface and collects all errors. +type CollectT struct { + // Maintainer: + // 1. we should verify the the use of runtime.GoExit is correct in this context. + // 2. we should remove the deprecated methods. + // + // 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 ...any) { + 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(T) { + 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") +// +// Examples: +// +// success: func(c *CollectT) { True(c,true) }, 100*time.Millisecond, 20*time.Millisecond +// failure: func(c *CollectT) { False(c,true) }, 100*time.Millisecond, 20*time.Millisecond +func EventuallyWithT(t T, condition func(collect *CollectT), waitFor time.Duration, tick time.Duration, msgAndArgs ...any) bool { + if h, ok := t.(H); 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) +// +// Examples: +// +// success: func() bool { return false }, 100*time.Millisecond, 20*time.Millisecond +// failure: func() bool { return true }, 100*time.Millisecond, 20*time.Millisecond +func Never(t T, condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...any) bool { + if h, ok := t.(H); 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 + } + } +} diff --git a/codegen/internal/scanner/comments/testdata/assertions/doc.go b/codegen/internal/scanner/comments/testdata/assertions/doc.go new file mode 100644 index 000000000..0b99b098e --- /dev/null +++ b/codegen/internal/scanner/comments/testdata/assertions/doc.go @@ -0,0 +1,32 @@ +//go:build integrationtest + +// SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + +// Package assertions holds the internal implementation +// of all the helper functions exposed by testify. +// +// It serves as a base to develop and test testify, +// whereas the publicly exposed API (in packages assert and require) +// is generated. +// +// # Domains +// +// - boolean: asserting boolean values +// - collection: asserting slices and maps +// - compare: comparing ordered values +// - condition: expressing assertions using conditions +// - equal: asserting two things are equal +// - error: asserting errors +// - file: asserting os files +// - helpers: extra helpers that are not assertions +// - http: asserting http response and body +// - json: asserting JSON documents +// - number: asserting numbers +// - order: asserting how collection are ordered +// - panic: asserting a panic behavior +// - string: asserting strings +// - time: asserting times and durations +// - type: asserting types rather than values +// - yaml: asserting yaml documents +package assertions diff --git a/codegen/internal/scanner/comments/testdata/assertions/helpers.go b/codegen/internal/scanner/comments/testdata/assertions/helpers.go new file mode 100644 index 000000000..1ecda67cd --- /dev/null +++ b/codegen/internal/scanner/comments/testdata/assertions/helpers.go @@ -0,0 +1,27 @@ +//go:build integrationtest + +// SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + +package assertions + +// T is the minimal interface for test assertions (stub for testdata). +type T interface { + // Maintainers: we should clarify when we need to impose FailNow on the interface and when we don't. + Errorf(format string, args ...any) + FailNow() +} + +// H is the interface for test helpers (stub for testdata). +type H interface { + Helper() +} + +// Comparison is a function type for custom assertions (stub for testdata). +type Comparison func() bool + +// Fail reports a failure (stub for testdata). +func Fail(t T, failureMessage string, msgAndArgs ...any) bool { + t.Errorf(failureMessage) + return false +} diff --git a/codegen/internal/scanner/comments/testdata/assertions/vars.go b/codegen/internal/scanner/comments/testdata/assertions/vars.go new file mode 100644 index 000000000..70bc6208a --- /dev/null +++ b/codegen/internal/scanner/comments/testdata/assertions/vars.go @@ -0,0 +1,15 @@ +//go:build integrationtest + +// SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + +package assertions + +import "errors" + +// ErrTest 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 ErrTest = errors.New("assert.ErrTest general error for testing") diff --git a/codegen/internal/scanner/imports.go b/codegen/internal/scanner/imports.go new file mode 100644 index 000000000..08187321d --- /dev/null +++ b/codegen/internal/scanner/imports.go @@ -0,0 +1,55 @@ +// SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + +package scanner + +import ( + "strings" + + "golang.org/x/tools/go/packages" +) + +// buildImportAliases scans import declarations to find aliases used in the source. +// +// This bridges the AST view (import aliases) with the types view (package paths). +func buildImportAliases(pkg *packages.Package) map[string]string { + aliases := make(map[string]string) + + for _, astFile := range pkg.Syntax { + for _, importSpec := range astFile.Imports { + // get the import path (remove quotes) + importPath := strings.Trim(importSpec.Path.Value, `"`) + + var alias string + if importSpec.Name != nil { + // explicit alias: import foo "bar/baz" + alias = importSpec.Name.Name + + // skip blank imports and dot imports for type qualification + if alias == "_" || alias == "." { + continue + } + } else { + // no explicit alias - need to determine the package name + // try to find it in the loaded imports + for _, imported := range pkg.Imports { + if imported.PkgPath == importPath { + alias = imported.Name + break + } + } + + // fallback: use last segment of import path + if alias == "" { + parts := strings.Split(importPath, "/") + alias = parts[len(parts)-1] + } + } + + // store the mapping (later imports override earlier ones if there are conflicts) + aliases[importPath] = alias + } + } + + return aliases +} diff --git a/codegen/internal/scanner/module.go b/codegen/internal/scanner/module.go new file mode 100644 index 000000000..2d89dfea8 --- /dev/null +++ b/codegen/internal/scanner/module.go @@ -0,0 +1,88 @@ +// SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + +package scanner + +import ( + "bytes" + "context" + "fmt" + "log" + "os/exec" + "runtime/debug" + "time" + "unicode" +) + +// header returns a codegen header, more detailed than the Tool information. +func header() string { + moduleVersion := moduleVersion() + repoVersion := gitDescribe() + ts := time.Now().Format(time.DateOnly) + + return fmt.Sprintf( + "Generated on %s (version %s) using codegen version %s", + ts, repoVersion, moduleVersion, + ) +} + +func gitDescribe() string { + const cmdTimeout = 10 * time.Second + ctx, cancel := context.WithTimeout(context.Background(), cmdTimeout) + defer cancel() + + gitDesc, err := exec.CommandContext(ctx, "git", "rev-parse", "--short", "HEAD").CombinedOutput() + if err != nil { + log.Printf("warning: attempted to run git describe, but got: %v. Skipped", err) + return "" + } + + return string(bytes.TrimRightFunc(gitDesc, func(r rune) bool { + return r == '\n' || r == '\r' || unicode.IsSpace(r) + })) +} + +// moduleName returns the main module name of the caller. +// +// This identifies the tool currently running the analysis. +// +// NOTE: no versioning here - the header provides more information. +func moduleName() string { + info, ok := debug.ReadBuildInfo() + if !ok { + return "" + } + + return info.Main.Path +} + +func moduleVersion() string { + info, ok := debug.ReadBuildInfo() + if !ok { + return "" + } + + var ( + modVersion string + gitVersion string + ) + + for _, setting := range info.Settings { + if setting.Key == "vcs.revision" { + gitVersion = setting.Value + } + } + + if info.Main.Version == "(devel)" { + modVersion = "master" + } else { + modVersion = info.Main.Version + } + + final := modVersion + if gitVersion != "" { + final += " [sha: " + gitVersion + "]" + } + + return final +} diff --git a/codegen/internal/scanner/module_test.go b/codegen/internal/scanner/module_test.go new file mode 100644 index 000000000..4f3d3e818 --- /dev/null +++ b/codegen/internal/scanner/module_test.go @@ -0,0 +1,25 @@ +package scanner + +import "testing" + +func TestModuleName(t *testing.T) { + const expected = "github.com/go-openapi/testify/v2/codegen" + result := moduleName() + if result != expected { + t.Errorf("Expected %q but got %q", expected, result) + } +} + +func TestGitDescribe(t *testing.T) { + result := gitDescribe() + if result == "" { + t.Errorf("Expected a non-empty string but got %q", result) + } +} + +func TestModuleVersion(t *testing.T) { + result := moduleVersion() + if result == "" { + t.Errorf("Expected a non-empty string but got %q", result) + } +} diff --git a/codegen/internal/scanner/options.go b/codegen/internal/scanner/options.go index 094ef92ce..6ad70236f 100644 --- a/codegen/internal/scanner/options.go +++ b/codegen/internal/scanner/options.go @@ -3,11 +3,14 @@ package scanner +const stdTarget = "github.com/go-openapi/testify/v2/internal/assertions" + type Option func(*options) type options struct { - dir string - pkg string + dir string + pkg string + collectDoc bool } // WithWorkDir gives a workdir to the code scanner. @@ -24,10 +27,17 @@ func WithPackage(pkg string) Option { } } +// WithCollectDoc indicates that the scanner should extract more information from comments. +func WithCollectDoc(enabled bool) Option { + return func(o *options) { + o.collectDoc = enabled + } +} + func optionsWithDefaults(opts []Option) options { o := options{ dir: "..", - pkg: "github.com/go-openapi/testify/v2/internal/assertions", + pkg: stdTarget, } for _, apply := range opts { diff --git a/codegen/internal/scanner/scanner.go b/codegen/internal/scanner/scanner.go index 4f2ff2bb6..01163f56f 100644 --- a/codegen/internal/scanner/scanner.go +++ b/codegen/internal/scanner/scanner.go @@ -4,40 +4,38 @@ package scanner import ( + "errors" "fmt" "go/ast" "go/token" "go/types" "path/filepath" "regexp" - "runtime/debug" - "strings" - "github.com/go-openapi/testify/v2/codegen/internal/model" - "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/go/packages" + + "github.com/go-openapi/testify/v2/codegen/internal/model" + "github.com/go-openapi/testify/v2/codegen/internal/scanner/comments" + parser "github.com/go-openapi/testify/v2/codegen/internal/scanner/comments-parser" + "github.com/go-openapi/testify/v2/codegen/internal/scanner/signature" ) const pkgLoadMode = packages.NeedName | packages.NeedFiles | packages.NeedImports | packages.NeedDeps | packages.NeedTypes | packages.NeedSyntax | packages.NeedTypesInfo -const ( - successPrefix = "success:" - failurePrefix = "failure:" - panicPrefix = "panic:" -) - // Scanner scans the internal/assertions package and extracts all functions, types, and metadata. type Scanner struct { options - scanConfig *packages.Config - syntaxPackage []*ast.File - typedPackage *types.Package - typesInfo *types.Info - filesMap map[*token.File]*ast.File - fileSet *token.FileSet - importAliases map[string]string // import path -> alias name used in source - result *model.AssertionPackage + scanConfig *packages.Config + syntaxPackage []*ast.File + typedPackage *types.Package + typesInfo *types.Info + filesMap map[*token.File]*ast.File + fileSet *token.FileSet + importAliases map[string]string // import path -> alias name used in source + sigExtractor *signature.Extractor + commentExtractor *comments.Extractor + result *model.AssertionPackage } // New [Scanner] ready to [Scanner.Scan] code. @@ -75,21 +73,35 @@ func (s *Scanner) Scan() (*model.AssertionPackage, error) { if s.typedPackage == nil { return nil, fmt.Errorf("something is wrong. Could not parse packages: %v", s.pkg) } + + // stash everything we need from [packages.Package] s.typesInfo = pkg.TypesInfo - s.filesMap = buildFilesMap(pkg) + s.filesMap = comments.BuildFilesMap(pkg) s.fileSet = pkg.Fset s.importAliases = buildImportAliases(pkg) + s.sigExtractor = signature.New(s.typedPackage, s.importAliases) + s.commentExtractor = comments.New(s.syntaxPackage, s.fileSet, s.filesMap) - s.resolveScope() + if err := s.resolveScope(); err != nil { + return nil, err + } + + // post-collection enrichments s.result.Tool = moduleName() - s.result.Copyright = s.extractCopyright().Text() - s.result.DocString = s.extractPackageComments().Text() + s.result.Header = header() + s.result.Copyright = s.commentExtractor.ExtractCopyright().Text() + s.result.DocString = s.commentExtractor.ExtractPackageComments().Text() s.result.TestDataPath = filepath.Join(pkg.Dir, "testdata") + if s.collectDoc { + // extract domain descriptions from package comments + s.result.ExtraComments = s.commentExtractor.ExtractDomainDescriptions() + } + return s.result, nil } -func (s *Scanner) resolveScope() { +func (s *Scanner) resolveScope() error { s.result.Package = s.typedPackage.Path() for _, pkg := range s.typedPackage.Imports() { @@ -120,63 +132,24 @@ func (s *Scanner) resolveScope() { continue } } -} - -// buildFilesMap constructs a lookup index to help bridge token position vs ast.File. -func buildFilesMap(pkg *packages.Package) map[*token.File]*ast.File { - filesMap := make(map[*token.File]*ast.File, len(pkg.Syntax)) - - for _, astFile := range pkg.Syntax { - tokenFile := pkg.Fset.File(astFile.Pos()) // o(log n) lookup - filesMap[tokenFile] = astFile - } - - return filesMap -} -// buildImportAliases scans import declarations to find aliases used in the source. -// -// This bridges the AST view (import aliases) with the types view (package paths). -func buildImportAliases(pkg *packages.Package) map[string]string { - aliases := make(map[string]string) - - for _, astFile := range pkg.Syntax { - for _, importSpec := range astFile.Imports { - // Get the import path (remove quotes) - importPath := strings.Trim(importSpec.Path.Value, `"`) - - var alias string - if importSpec.Name != nil { - // Explicit alias: import foo "bar/baz" - alias = importSpec.Name.Name - - // Skip blank imports and dot imports for type qualification - if alias == "_" || alias == "." { - continue - } - } else { - // No explicit alias - need to determine the package name - // Try to find it in the loaded imports - for _, imported := range pkg.Imports { - if imported.PkgPath == importPath { - alias = imported.Name - break - } - } - - // Fallback: use last segment of import path - if alias == "" { - parts := strings.Split(importPath, "/") - alias = parts[len(parts)-1] + // Accumulate parse errors from test values + var parseErrors []error + for _, fn := range s.result.Functions { + for _, test := range fn.Tests { + for _, val := range test.TestedValues { + if val.Error != nil { + parseErrors = append(parseErrors, fmt.Errorf("function %s: invalid test value %q: %w", fn.Name, val.Raw, val.Error)) } } - - // Store the mapping (later imports override earlier ones if there are conflicts) - aliases[importPath] = alias } } - return aliases + if len(parseErrors) > 0 { + return errors.Join(parseErrors...) + } + + return nil } func (s *Scanner) addImport(pkg *types.Package) { @@ -195,17 +168,24 @@ func (s *Scanner) addImport(pkg *types.Package) { } func (s *Scanner) addFunction(object *types.Func) { - docComment := s.extractComments(object) + docComment := s.commentExtractor.ExtractComments(object) - // Extract the base function signature - function := s.extractFunctionSignature(object.Signature(), object.Name()) + // extract the base function signature + function := s.sigExtractor.ExtractFunctionSignature(object.Signature(), object.Name()) - // Add function-specific metadata + // add function-specific metadata function.ID = object.Id() function.SourcePackage = object.Pkg().Path() // full package name function.TargetPackage = object.Pkg().Name() // short package name function.DocString = docComment.Text() - function.Tests = parseTestExamples(docComment) // extract test values from Example section + function.Tests = parser.ParseTestExamples(docComment.Text()) + if s.collectDoc { + function.ExtraComments = s.commentExtractor.ExtractExtraComments(object) + pos := s.fileSet.Position(object.Pos()) + function.SourceLink = &pos + function.Domain = parser.DomainFromExtraComments(function.ExtraComments) + } + function.IsDeprecated = isDeprecated(function.DocString) s.result.Functions = append(s.result.Functions, function) } @@ -216,7 +196,13 @@ func (s *Scanner) addConst(object *types.Const) { Name: object.Name(), SourcePackage: object.Pkg().Path(), TargetPackage: object.Pkg().Name(), // short package name - DocString: s.extractComments(object).Text(), + DocString: s.commentExtractor.ExtractComments(object).Text(), + } + constant.IsDeprecated = isDeprecated(constant.DocString) + + if s.collectDoc { + pos := s.fileSet.Position(object.Pos()) + constant.SourceLink = &pos } s.result.Consts = append(s.result.Consts, constant) } @@ -227,12 +213,18 @@ func (s *Scanner) addVar(object *types.Var) { Name: object.Name(), SourcePackage: object.Pkg().Path(), TargetPackage: object.Pkg().Name(), // short package name - DocString: s.extractComments(object).Text(), + DocString: s.commentExtractor.ExtractComments(object).Text(), + } + variable.IsDeprecated = isDeprecated(variable.DocString) + + if s.collectDoc { + pos := s.fileSet.Position(object.Pos()) + variable.SourceLink = &pos } // Check if this variable is a function type if sig, ok := object.Type().(*types.Signature); ok { - fn := s.extractFunctionSignature(sig, object.Name()) + fn := s.sigExtractor.ExtractFunctionSignature(sig, object.Name()) variable.Function = &fn } @@ -246,401 +238,39 @@ func (s *Scanner) addNamedType(object *types.TypeName) { SourcePackage: object.Pkg().Path(), TargetPackage: object.Pkg().Name(), // short package name IsAlias: object.IsAlias(), - DocString: s.extractComments(object).Text(), + DocString: s.commentExtractor.ExtractComments(object).Text(), } + namedType.IsDeprecated = isDeprecated(namedType.DocString) - // Check if this named type is a function type + // check if this named type is a function type. // For named types, we need to check the underlying type if named, ok := object.Type().(*types.Named); ok { if sig, ok := named.Underlying().(*types.Signature); ok { - fn := s.extractFunctionSignature(sig, object.Name()) + fn := s.sigExtractor.ExtractFunctionSignature(sig, object.Name()) namedType.Function = &fn } } else if sig, ok := object.Type().(*types.Signature); ok { - // Type alias to a function type - fn := s.extractFunctionSignature(sig, object.Name()) + // type alias to a function type + fn := s.sigExtractor.ExtractFunctionSignature(sig, object.Name()) namedType.Function = &fn } - s.result.Types = append(s.result.Types, namedType) -} - -// extractFunctionSignature extracts function signature details from a types.Signature. -// -// This is used for both regular functions and function types (var/type declarations). -func (s *Scanner) extractFunctionSignature(signature *types.Signature, name string) model.Function { - function := model.Function{ - Name: name, - } - - // Check if generic - isGeneric := signature.TypeParams() != nil - function.IsGeneric = isGeneric - - // Extract parameters - params := signature.Params() - function.AllParams = make(model.Parameters, 0, params.Len()) - function.Params = make(model.Parameters, 0, params.Len()) - - for param := range params.Variables() { - p := model.Parameter{ - Name: param.Name(), - GoType: s.elidedType(param.Type()), - Selector: s.elidedQualifier(param.Type()), - } - function.AllParams = append(function.AllParams, p) - - if p.Name != "t" && p.Name != "msgAndArgs" { - // filtered params - function.Params = append(function.Params, p) - } - } - - // Handle variadic parameters - l := params.Len() - if signature.Variadic() && l > 0 { - // introduce ellipsis notation - lastParam := params.At(l - 1) - lastType := lastParam.Type() - if asSlice, ok := lastType.(*types.Slice); ok { // should be always ok - elemGoType := s.elidedType(asSlice.Elem()) - function.AllParams[len(function.AllParams)-1].GoType = "..." + elemGoType - function.AllParams[len(function.AllParams)-1].IsVariadic = true - - // check filtered params - if len(function.Params) > 0 && function.Params[len(function.Params)-1].Name == function.AllParams[len(function.AllParams)-1].Name { - function.Params[len(function.Params)-1].GoType = "..." + elemGoType - function.Params[len(function.Params)-1].IsVariadic = true - } - } - } - - // Extract return values - results := signature.Results() - function.Returns = make(model.Parameters, 0, results.Len()) - for result := range results.Variables() { - function.Returns = append(function.Returns, model.Parameter{ - Name: result.Name(), - GoType: s.elidedType(result.Type()), - Selector: s.elidedQualifier(result.Type()), - }) - } - - // Detect helpers (not assertions) - function.IsHelper = l == 0 || (len(function.AllParams) > 0 && function.AllParams[0].Name != "t") - function.IsConstructor = name == "New" - - return function -} - -// qualifier returns the appropriate package name for type qualification. -// -// It uses import aliases from the source (AST) rather than the package's actual name. -func (s *Scanner) qualifier(pkg *types.Package) string { - if pkg == nil { - return "" - } - - // If it's the current package being scanned, no qualification needed - if pkg == s.typedPackage { - return "" - } - - // Look up the alias used in source imports - if alias, ok := s.importAliases[pkg.Path()]; ok { - return alias - } - - // Fallback to the package's actual name - return pkg.Name() -} - -// elidedType returns a string representation of the type with package names as they appear in source. -// -// Uses import aliases when available (e.g., "httputil.Handler" if imported as "httputil"). -func (s *Scanner) elidedType(t types.Type) string { - return types.TypeString(t, s.qualifier) -} - -// elidedQualifier returns the selector used for a type, as its import package alias used in source, -// or the empty string if this is a local declaration. -func (s *Scanner) elidedQualifier(t types.Type) string { - const maxInterestingParts = 2 - parts := strings.SplitN(types.TypeString(t, s.qualifier), ".", maxInterestingParts) - if len(parts) > 1 { - return parts[0] - } - - return "" -} - -// extractPackageComments finds the package-level comment that -// describes the source package. -func (s *Scanner) extractPackageComments() *ast.CommentGroup { - for _, file := range s.syntaxPackage { - if file.Doc == nil { - continue - } - - return file.Doc - } - - // safeguard: never returns nil - return &ast.CommentGroup{} -} - -var copyrightRex = regexp.MustCompile(`(?i)copyright`) - -// extractCopyright scans source files and expects that at least one of those contains -// a leading comment line with the string "copyright". This comment group is assigned -// to the copyright heading that will be retained. -func (s *Scanner) extractCopyright() *ast.CommentGroup { -FILE: - for _, file := range s.syntaxPackage { - if len(file.Comments) == 0 { - continue - } - - firstGroup := file.Comments[0] - - for _, line := range firstGroup.List { - if line.Slash >= file.Package { - // copyright comment must be before the "package" stanza - continue FILE - } - - if copyrightRex.MatchString(line.Text) { - // first file found with leading comments - return firstGroup - } - } - } - - // safeguard: never returns nil - return &ast.CommentGroup{} -} - -// extractComments retrieves the docstring for an object. -// -// It never returns nil: if no comment is present an empty but valid [ast.CommentGroup] is returned. -// -// NOTE: comment prioritization rule is: -// 1. GenDecl.Doc = leading comment for the entire declaration block -// 2. Spec.Doc = individual doc for that specific spec -func (s *Scanner) extractComments(object types.Object) *ast.CommentGroup { - pos := object.Pos() - if !pos.IsValid() { - return &ast.CommentGroup{} - } - - tokenFile := s.fileSet.File(pos) // o(log n) lookup - if tokenFile == nil { - return &ast.CommentGroup{} - } - - astFile := s.filesMap[tokenFile] // o(1) lookup - if astFile == nil { - return &ast.CommentGroup{} - } - - path, _ := astutil.PathEnclosingInterval(astFile, pos, pos) - for _, node := range path { - declaration, ok := node.(ast.Decl) - if !ok { - continue - } - - return extractCommentFromDecl(declaration, object) - } - - return &ast.CommentGroup{} -} - -func extractCommentFromDecl(declaration ast.Decl, object types.Object) *ast.CommentGroup { - switch typedDeclaration := declaration.(type) { - case *ast.GenDecl: // const, var, type declaration - for _, spec := range typedDeclaration.Specs { - comment := extractCommentFromTypeSpec(typedDeclaration, spec, object) - if comment != nil { - return comment - } - } - case *ast.FuncDecl: // a function - if typedDeclaration.Doc != nil { - return typedDeclaration.Doc - } + if s.collectDoc { + pos := s.fileSet.Position(object.Pos()) + namedType.SourceLink = &pos + namedType.ExtraComments = s.commentExtractor.ExtractExtraComments(object) + namedType.Domain = parser.DomainFromExtraComments(namedType.ExtraComments) } - return &ast.CommentGroup{} -} - -func extractCommentFromTypeSpec(typedDeclaration *ast.GenDecl, spec ast.Spec, object types.Object) *ast.CommentGroup { - switch typedSpec := spec.(type) { - case *ast.TypeSpec: - // for TypeSpec, check if it matches (though usually only 1 spec per GenDecl for types) - if typedSpec.Name.Name != object.Name() { - return nil - } - - // return Doc, checking both GenDecl.Doc and TypeSpec.Doc - if typedDeclaration.Doc != nil { - return typedDeclaration.Doc - } - - if typedSpec.Doc != nil { - return typedSpec.Doc - } - case *ast.ValueSpec: - for _, ident := range typedSpec.Names { - if ident.Name != object.Name() { - return nil - } - - // return Doc, checking both GenDecl.Doc and ValueSpec.Doc - if typedDeclaration.Doc != nil { - return typedDeclaration.Doc - } - - if typedSpec.Doc != nil { - return typedSpec.Doc - } - } - } - - return nil -} - -// parseTestExamples extracts test examples from doc comments. -// -// It looks for an "Examples:" or "Example:" section and parses lines like: -// - success: -// - failure: -// - panic: -// -// Assumption: we use regular "//" comments, not "/* ... */". -func parseTestExamples(docComment *ast.CommentGroup) []model.Test { - if docComment == nil { - return nil - } - - const usualNumberOfExamples = 2 - tests := make([]model.Test, 0, usualNumberOfExamples) - - inExamplesSection := false // comments are expected to contain an Example[s] or # Example[s] section - inValueSection := false - - for _, comment := range docComment.List { - text := strings.TrimPrefix(comment.Text, "//") - text = strings.TrimSpace(text) - - if inValueSection && len(tests) > 0 && text != "" && !startAnotherSection(text) && !startExampleValue(text) { - // check if a comment line follows an example value: this would the assertion message - tests[len(tests)-1].AssertionMessage = text - inValueSection = false - - continue - } - - inValueSection = false - - // Check if we're entering the Examples section - if startExampleSection(text) { - inExamplesSection = true - continue - } - - // Skip until we find the Examples section - if !inExamplesSection { - continue - } - - // Stop if we hit another section (starts with capital letter and ends with colon) - if startAnotherSection(text) { - break - } - - // parse test lines: "success: { values to put in the test }", "failure: ..." or "panic: ..." - // After each value line, we may found an additional text to be used as an assertion message. - switch { - case strings.HasPrefix(text, successPrefix): - inValueSection = true - if testcase, ok := parseTestValue(successPrefix, model.TestSuccess, text); ok { - tests = append(tests, testcase) - } - case strings.HasPrefix(text, failurePrefix): - inValueSection = true - if testcase, ok := parseTestValue(failurePrefix, model.TestFailure, text); ok { - tests = append(tests, testcase) - } - case strings.HasPrefix(text, panicPrefix): - inValueSection = true - if testcase, ok := parseTestValue(panicPrefix, model.TestPanic, text); ok { - tests = append(tests, testcase) - } - } - } - - return tests + s.result.Types = append(s.result.Types, namedType) } -// moduleName returns the main module name of the caller. +// This is the regexp used by pkgsite. +// See: https://cs.opensource.google/go/x/pkgsite/+/master:internal/godoc/dochtml/deprecated.go // -// This identifies the tool currently running the analysis. -func moduleName() string { - info, ok := debug.ReadBuildInfo() - if !ok { - return "" - } - var ( - modVersion string - gitVersion string - ) - - for _, setting := range info.Settings { - if setting.Key == "vcs.revision" { - gitVersion = setting.Value - } - } - - if info.Main.Version == "(devel)" { - modVersion = "master" - } else { - modVersion = info.Main.Version - } - - final := info.Main.Path + "@" + modVersion - if gitVersion != "" { - final += " [sha: " + gitVersion + "]" - } - - return final -} - -func startExampleSection(text string) bool { - return strings.HasPrefix(text, "Examples:") || - strings.HasPrefix(text, "Example:") || - strings.HasPrefix(text, "# Example") || - strings.HasPrefix(text, "# Examples") -} - -func startAnotherSection(text string) bool { - return len(text) > 0 && (text[0] >= 'A' && text[0] <= 'Z' && strings.HasSuffix(text, ":")) || strings.HasPrefix(text, "# ") -} - -func startExampleValue(text string) bool { - return strings.HasPrefix(text, successPrefix) || strings.HasPrefix(text, failurePrefix) || strings.HasPrefix(text, panicPrefix) -} - -func parseTestValue(placeHolder string, outcome model.TestExpectedOutcome, text string) (model.Test, bool) { - value := strings.TrimSpace(strings.TrimPrefix(text, placeHolder)) - _, isTodo := strings.CutPrefix(value, "// TODO") - - if value != "" && !isTodo { - return model.Test{ - TestedValue: value, - ExpectedOutcome: outcome, - }, true - } +// "Deprecated:" at the start of a paragraph. +var deprecatedRx = regexp.MustCompile(`(^|\n\s*\n)\s*Deprecated:`) - return model.Test{}, false +func isDeprecated(comment string) bool { + return deprecatedRx.MatchString(comment) } diff --git a/codegen/internal/scanner/scanner_test.go b/codegen/internal/scanner/scanner_test.go index 14c3e5c6f..69378eff5 100644 --- a/codegen/internal/scanner/scanner_test.go +++ b/codegen/internal/scanner/scanner_test.go @@ -14,7 +14,7 @@ import ( // // You may use this smoke test to verify the content of the constructed model. func TestScanner(t *testing.T) { - s := New() + s := New(WithCollectDoc(true)) r, err := s.Scan() if err != nil { diff --git a/codegen/internal/scanner/signature/doc.go b/codegen/internal/scanner/signature/doc.go new file mode 100644 index 000000000..a30af5050 --- /dev/null +++ b/codegen/internal/scanner/signature/doc.go @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + +// Package signature provides utilities for extracting and formatting Go function signatures and types. +// +// This package handles: +// +// - Extracting function signatures from types.Signature +// - Type qualification using import aliases +// - Converting types to strings with proper package selectors +// +// The Extractor type maintains the context needed for proper type qualification, +// including the current package and import aliases from the source code. +package signature diff --git a/codegen/internal/scanner/signature/extractor.go b/codegen/internal/scanner/signature/extractor.go new file mode 100644 index 000000000..f69985086 --- /dev/null +++ b/codegen/internal/scanner/signature/extractor.go @@ -0,0 +1,138 @@ +// SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + +package signature + +import ( + "go/types" + "strings" + + "github.com/go-openapi/testify/v2/codegen/internal/model" +) + +// Extractor extracts function signatures and formats types with proper package qualification. +type Extractor struct { + currentPackage *types.Package // the package being scanned + importAliases map[string]string // import path -> alias name used in source +} + +// New creates a signature extractor. +// +// The currentPackage is used to determine when type qualification is needed. +// The importAliases map provides the mapping from import paths to the aliases +// used in the source code (e.g., "net/http/httputil" -> "httputil"). +func New(currentPackage *types.Package, importAliases map[string]string) *Extractor { + return &Extractor{ + currentPackage: currentPackage, + importAliases: importAliases, + } +} + +// ExtractFunctionSignature extracts function signature details from a types.Signature. +// +// This is used for both regular functions and function types (var/type declarations). +func (e *Extractor) ExtractFunctionSignature(signature *types.Signature, name string) model.Function { + function := model.Function{ + Name: name, + } + + // check if generic + isGeneric := signature.TypeParams() != nil + function.IsGeneric = isGeneric + + // extract parameters + params := signature.Params() + function.AllParams = make(model.Parameters, 0, params.Len()) + function.Params = make(model.Parameters, 0, params.Len()) + + for param := range params.Variables() { + p := model.Parameter{ + Name: param.Name(), + GoType: e.ElidedType(param.Type()), + Selector: e.ElidedQualifier(param.Type()), + } + function.AllParams = append(function.AllParams, p) + + if p.Name != "t" && p.Name != "msgAndArgs" { + // filtered params + function.Params = append(function.Params, p) + } + } + + // handle variadic parameters + l := params.Len() + if signature.Variadic() && l > 0 { + // introduce ellipsis notation + lastParam := params.At(l - 1) + lastType := lastParam.Type() + if asSlice, ok := lastType.(*types.Slice); ok { // should be always ok + elemGoType := e.ElidedType(asSlice.Elem()) + function.AllParams[len(function.AllParams)-1].GoType = "..." + elemGoType + function.AllParams[len(function.AllParams)-1].IsVariadic = true + + // check filtered params + if len(function.Params) > 0 && function.Params[len(function.Params)-1].Name == function.AllParams[len(function.AllParams)-1].Name { + function.Params[len(function.Params)-1].GoType = "..." + elemGoType + function.Params[len(function.Params)-1].IsVariadic = true + } + } + } + + // extract return values + results := signature.Results() + function.Returns = make(model.Parameters, 0, results.Len()) + for result := range results.Variables() { + function.Returns = append(function.Returns, model.Parameter{ + Name: result.Name(), + GoType: e.ElidedType(result.Type()), + Selector: e.ElidedQualifier(result.Type()), + }) + } + + // detect helpers (not assertions) + function.IsHelper = l == 0 || (len(function.AllParams) > 0 && function.AllParams[0].Name != "t") + function.IsConstructor = name == "New" + + return function +} + +// Qualifier returns the appropriate package name for type qualification. +// +// It uses import aliases from the source (AST) rather than the package's actual name. +func (e *Extractor) Qualifier(pkg *types.Package) string { + if pkg == nil { + return "" + } + + // if it's the current package being scanned, no qualification needed + if pkg == e.currentPackage { + return "" + } + + // look up the alias used in source imports + if alias, ok := e.importAliases[pkg.Path()]; ok { + return alias + } + + // fallback to the package's actual name + return pkg.Name() +} + +// ElidedType returns a string representation of the type with package names as they appear in source. +// +// Uses import aliases when available (e.g., "httputil.Handler" if imported as "httputil"). +func (e *Extractor) ElidedType(t types.Type) string { + return types.TypeString(t, e.Qualifier) +} + +// ElidedQualifier returns the selector used for a type, as its import package alias used in source, +// or the empty string if this is a local declaration. +func (e *Extractor) ElidedQualifier(t types.Type) string { + const maxInterestingParts = 2 + parts := strings.SplitN(types.TypeString(t, e.Qualifier), ".", maxInterestingParts) + if len(parts) > 1 { + return parts[0] + } + + return "" +} diff --git a/codegen/internal/scanner/signature/extractor_test.go b/codegen/internal/scanner/signature/extractor_test.go new file mode 100644 index 000000000..f0f08335a --- /dev/null +++ b/codegen/internal/scanner/signature/extractor_test.go @@ -0,0 +1,512 @@ +// SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + +package signature + +import ( + "go/types" + "iter" + "slices" + "testing" +) + +func TestQualifier(t *testing.T) { + t.Parallel() + + // Create test packages + currentPkg := types.NewPackage("github.com/example/test", "test") + otherPkg := types.NewPackage("net/http", "http") + aliasedPkg := types.NewPackage("net/http/httputil", "httputil") + + for c := range qualifierCases(currentPkg, otherPkg, aliasedPkg) { + t.Run(c.name, func(t *testing.T) { + t.Parallel() + + e := New(c.currentPkg, c.importAliases) + result := e.Qualifier(c.pkg) + + if result != c.expected { + t.Errorf("Qualifier() = %q, expected %q", result, c.expected) + } + }) + } +} + +func TestElidedQualifier(t *testing.T) { + t.Parallel() + + currentPkg := types.NewPackage("github.com/example/test", "test") + otherPkg := types.NewPackage("net/http", "http") + + for c := range elidedQualifierCases(currentPkg, otherPkg) { + t.Run(c.name, func(t *testing.T) { + t.Parallel() + + e := New(c.currentPkg, c.importAliases) + result := e.ElidedQualifier(c.typ) + + if result != c.expected { + t.Errorf("ElidedQualifier() = %q, expected %q", result, c.expected) + } + }) + } +} + +func TestElidedType(t *testing.T) { + t.Parallel() + + currentPkg := types.NewPackage("github.com/example/test", "test") + otherPkg := types.NewPackage("net/http", "http") + + for c := range elidedTypeCases(currentPkg, otherPkg) { + t.Run(c.name, func(t *testing.T) { + t.Parallel() + + e := New(c.currentPkg, c.importAliases) + result := e.ElidedType(c.typ) + + if result != c.expected { + t.Errorf("ElidedType() = %q, expected %q", result, c.expected) + } + }) + } +} + +func TestExtractFunctionSignature(t *testing.T) { + t.Parallel() + + currentPkg := types.NewPackage("github.com/example/test", "test") + + // Helper to create a signature + makeSig := func(params, results []*types.Var, variadic bool) *types.Signature { + return types.NewSignatureType(nil, nil, nil, + types.NewTuple(params...), + types.NewTuple(results...), + variadic) + } + + for c := range extractFunctionSignatureCases(currentPkg, makeSig) { + t.Run(c.name, func(t *testing.T) { + t.Parallel() + + e := New(currentPkg, map[string]string{}) + result := e.ExtractFunctionSignature(c.signature, c.funcName) + + if result.Name != c.expectedName { + t.Errorf("Name = %q, expected %q", result.Name, c.expectedName) + } + if result.IsHelper != c.expectedHelper { + t.Errorf("IsHelper = %v, expected %v", result.IsHelper, c.expectedHelper) + } + if result.IsConstructor != c.expectedCtor { + t.Errorf("IsConstructor = %v, expected %v", result.IsConstructor, c.expectedCtor) + } + if len(result.Params) != c.paramsLen { + t.Errorf("len(Params) = %d, expected %d", len(result.Params), c.paramsLen) + } + if len(result.Returns) != c.returnsLen { + t.Errorf("len(Returns) = %d, expected %d", len(result.Returns), c.returnsLen) + } + if len(result.AllParams) != c.allParamsLen { + t.Errorf("len(AllParams) = %d, expected %d", len(result.AllParams), c.allParamsLen) + } + }) + } +} + +func TestExtractFunctionSignature_Variadic(t *testing.T) { + currentPkg := types.NewPackage("github.com/example/test", "test") + + // Create variadic signature + params := []*types.Var{ + types.NewVar(0, currentPkg, "t", types.NewInterfaceType(nil, nil)), + types.NewVar(0, currentPkg, "values", types.NewSlice(types.Typ[types.String])), + } + results := []*types.Var{ + types.NewVar(0, currentPkg, "", types.Typ[types.Bool]), + } + sig := types.NewSignatureType(nil, nil, nil, + types.NewTuple(params...), + types.NewTuple(results...), + true) // variadic + + e := New(currentPkg, map[string]string{}) + result := e.ExtractFunctionSignature(sig, "TestFunc") + + // Check that last param has ellipsis notation + if len(result.AllParams) != 2 { + t.Fatalf("Expected 2 AllParams, got %d", len(result.AllParams)) + } + + lastParam := result.AllParams[len(result.AllParams)-1] + if lastParam.GoType != "...string" { + t.Errorf("Last param GoType = %q, expected %q", lastParam.GoType, "...string") + } + if !lastParam.IsVariadic { + t.Errorf("Last param IsVariadic = false, expected true") + } + + // Check filtered params also have ellipsis + if len(result.Params) != 1 { + t.Fatalf("Expected 1 filtered Param, got %d", len(result.Params)) + } + if result.Params[0].GoType != "...string" { + t.Errorf("Filtered param GoType = %q, expected %q", result.Params[0].GoType, "...string") + } +} + +func TestExtractFunctionSignature_GenericFunction(t *testing.T) { + currentPkg := types.NewPackage("github.com/example/test", "test") + + // Create a generic signature with type parameters + typeParam := types.NewTypeParam(types.NewTypeName(0, currentPkg, "T", nil), types.NewInterfaceType(nil, nil).Complete()) + + params := []*types.Var{ + types.NewVar(0, currentPkg, "t", types.NewInterfaceType(nil, nil)), + types.NewVar(0, currentPkg, "value", typeParam), + } + results := []*types.Var{ + types.NewVar(0, currentPkg, "", types.Typ[types.Bool]), + } + + sig := types.NewSignatureType(nil, nil, nil, + types.NewTuple(params...), + types.NewTuple(results...), + false) + + // Manually set type params on signature using reflection or accept that + // we can't easily test generics without more complex setup. + // For now, test non-generic case and verify IsGeneric is false. + e := New(currentPkg, map[string]string{}) + result := e.ExtractFunctionSignature(sig, "GenericFunc") + + // Without type params attached, this will be false + if result.IsGeneric { + t.Errorf("IsGeneric = true, expected false (cannot easily construct generic sig in test)") + } +} + +/* Test case iterators */ + +type qualifierCase struct { + name string + currentPkg *types.Package + importAliases map[string]string + pkg *types.Package + expected string +} + +func qualifierCases(currentPkg, otherPkg, aliasedPkg *types.Package) iter.Seq[qualifierCase] { + return slices.Values([]qualifierCase{ + { + name: "nil package", + currentPkg: currentPkg, + importAliases: map[string]string{}, + pkg: nil, + expected: "", + }, + { + name: "current package - no qualification", + currentPkg: currentPkg, + importAliases: map[string]string{}, + pkg: currentPkg, + expected: "", + }, + { + name: "other package - use package name", + currentPkg: currentPkg, + importAliases: map[string]string{}, + pkg: otherPkg, + expected: "http", + }, + { + name: "aliased package - use alias", + currentPkg: currentPkg, + importAliases: map[string]string{ + "net/http/httputil": "httputil", + }, + pkg: aliasedPkg, + expected: "httputil", + }, + { + name: "aliased package with different alias", + currentPkg: currentPkg, + importAliases: map[string]string{ + "net/http/httputil": "util", + }, + pkg: aliasedPkg, + expected: "util", + }, + { + name: "package with alias but using actual name as fallback", + currentPkg: currentPkg, + importAliases: map[string]string{ + "some/other/path": "other", + }, + pkg: otherPkg, + expected: "http", // falls back to package name + }, + }) +} + +type elidedQualifierCase struct { + name string + currentPkg *types.Package + importAliases map[string]string + typ types.Type + expected string +} + +func elidedQualifierCases(currentPkg, otherPkg *types.Package) iter.Seq[elidedQualifierCase] { + return slices.Values([]elidedQualifierCase{ + { + name: "basic type - no qualifier", + currentPkg: currentPkg, + importAliases: map[string]string{}, + typ: types.Typ[types.String], + expected: "", + }, + { + name: "named type from other package", + currentPkg: currentPkg, + importAliases: map[string]string{}, + typ: types.NewNamed(types.NewTypeName(0, otherPkg, "Request", nil), types.NewStruct(nil, nil), nil), + expected: "http", + }, + { + name: "pointer to basic type", + currentPkg: currentPkg, + importAliases: map[string]string{}, + typ: types.NewPointer(types.Typ[types.Int]), + expected: "", + }, + { + name: "pointer to named type", + currentPkg: currentPkg, + importAliases: map[string]string{}, + typ: types.NewPointer(types.NewNamed(types.NewTypeName(0, otherPkg, "Request", nil), types.NewStruct(nil, nil), nil)), + expected: "*http", // includes pointer in qualifier + }, + { + name: "aliased package qualifier", + currentPkg: currentPkg, + importAliases: map[string]string{ + "net/http": "h", + }, + typ: types.NewNamed(types.NewTypeName(0, otherPkg, "Request", nil), types.NewStruct(nil, nil), nil), + expected: "h", + }, + }) +} + +type elidedTypeCase struct { + name string + currentPkg *types.Package + importAliases map[string]string + typ types.Type + expected string +} + +func elidedTypeCases(currentPkg, otherPkg *types.Package) iter.Seq[elidedTypeCase] { + return slices.Values([]elidedTypeCase{ + { + name: "string type", + currentPkg: currentPkg, + importAliases: map[string]string{}, + typ: types.Typ[types.String], + expected: "string", + }, + { + name: "int type", + currentPkg: currentPkg, + importAliases: map[string]string{}, + typ: types.Typ[types.Int], + expected: "int", + }, + { + name: "bool type", + currentPkg: currentPkg, + importAliases: map[string]string{}, + typ: types.Typ[types.Bool], + expected: "bool", + }, + { + name: "pointer to string", + currentPkg: currentPkg, + importAliases: map[string]string{}, + typ: types.NewPointer(types.Typ[types.String]), + expected: "*string", + }, + { + name: "slice of int", + currentPkg: currentPkg, + importAliases: map[string]string{}, + typ: types.NewSlice(types.Typ[types.Int]), + expected: "[]int", + }, + { + name: "named type from other package", + currentPkg: currentPkg, + importAliases: map[string]string{}, + typ: types.NewNamed(types.NewTypeName(0, otherPkg, "Request", nil), types.NewStruct(nil, nil), nil), + expected: "http.Request", + }, + { + name: "named type with alias", + currentPkg: currentPkg, + importAliases: map[string]string{ + "net/http": "h", + }, + typ: types.NewNamed(types.NewTypeName(0, otherPkg, "Request", nil), types.NewStruct(nil, nil), nil), + expected: "h.Request", + }, + { + name: "interface type", + currentPkg: currentPkg, + importAliases: map[string]string{}, + typ: types.NewInterfaceType(nil, nil), + expected: "interface{}", + }, + { + name: "empty interface (any)", + currentPkg: currentPkg, + importAliases: map[string]string{}, + typ: types.NewInterfaceType(nil, nil).Complete(), + expected: "interface{}", + }, + }) +} + +type extractFunctionSignatureCase struct { + name string + funcName string + signature *types.Signature + expectedName string + expectedHelper bool + expectedCtor bool + paramsLen int + returnsLen int + allParamsLen int +} + +func extractFunctionSignatureCases(currentPkg *types.Package, makeSig func([]*types.Var, []*types.Var, bool) *types.Signature) iter.Seq[extractFunctionSignatureCase] { + return slices.Values([]extractFunctionSignatureCase{ + { + name: "simple assertion function", + funcName: "Equal", + signature: makeSig( + []*types.Var{ + types.NewVar(0, currentPkg, "t", types.NewInterfaceType(nil, nil)), + types.NewVar(0, currentPkg, "expected", types.NewInterfaceType(nil, nil)), + types.NewVar(0, currentPkg, "actual", types.NewInterfaceType(nil, nil)), + }, + []*types.Var{ + types.NewVar(0, currentPkg, "", types.Typ[types.Bool]), + }, + false, + ), + expectedName: "Equal", + expectedHelper: false, + expectedCtor: false, + paramsLen: 2, // filtered (excludes 't') + returnsLen: 1, + allParamsLen: 3, + }, + { + name: "variadic assertion function", + funcName: "True", + signature: makeSig( + []*types.Var{ + types.NewVar(0, currentPkg, "t", types.NewInterfaceType(nil, nil)), + types.NewVar(0, currentPkg, "value", types.Typ[types.Bool]), + types.NewVar(0, currentPkg, "msgAndArgs", types.NewSlice(types.NewInterfaceType(nil, nil))), + }, + []*types.Var{ + types.NewVar(0, currentPkg, "", types.Typ[types.Bool]), + }, + true, + ), + expectedName: "True", + expectedHelper: false, + expectedCtor: false, + paramsLen: 1, // filtered (excludes 't' and 'msgAndArgs') + returnsLen: 1, + allParamsLen: 3, + }, + { + name: "helper function - no params", + funcName: "Helper", + signature: makeSig( + []*types.Var{}, + []*types.Var{ + types.NewVar(0, currentPkg, "", types.Typ[types.String]), + }, + false, + ), + expectedName: "Helper", + expectedHelper: true, + expectedCtor: false, + paramsLen: 0, + returnsLen: 1, + allParamsLen: 0, + }, + { + name: "constructor function", + funcName: "New", + signature: makeSig( + []*types.Var{ + types.NewVar(0, currentPkg, "t", types.NewInterfaceType(nil, nil)), + }, + []*types.Var{ + types.NewVar(0, currentPkg, "", types.NewPointer(types.NewNamed( + types.NewTypeName(0, currentPkg, "Assertions", nil), + types.NewStruct(nil, nil), nil))), + }, + false, + ), + expectedName: "New", + expectedHelper: false, + expectedCtor: true, + paramsLen: 0, // filtered (excludes 't') + returnsLen: 1, + allParamsLen: 1, + }, + { + name: "helper - first param not 't'", + funcName: "CalcValue", + signature: makeSig( + []*types.Var{ + types.NewVar(0, currentPkg, "x", types.Typ[types.Int]), + types.NewVar(0, currentPkg, "y", types.Typ[types.Int]), + }, + []*types.Var{ + types.NewVar(0, currentPkg, "", types.Typ[types.Int]), + }, + false, + ), + expectedName: "CalcValue", + expectedHelper: true, // first param is not 't' + expectedCtor: false, + paramsLen: 2, // not filtered + returnsLen: 1, + allParamsLen: 2, + }, + { + name: "no return values", + funcName: "FailNow", + signature: makeSig( + []*types.Var{ + types.NewVar(0, currentPkg, "t", types.NewInterfaceType(nil, nil)), + }, + []*types.Var{}, + false, + ), + expectedName: "FailNow", + expectedHelper: false, + expectedCtor: false, + paramsLen: 0, // filtered (excludes 't') + returnsLen: 0, + allParamsLen: 1, + }, + }) +} diff --git a/codegen/main.go b/codegen/main.go index 8561508d1..36f096103 100644 --- a/codegen/main.go +++ b/codegen/main.go @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + // Package main provides the testify code generator. // // This program reads all assertion functions from the internal/assertions package and @@ -10,12 +13,15 @@ import ( "strings" "github.com/go-openapi/testify/v2/codegen/internal/generator" + "github.com/go-openapi/testify/v2/codegen/internal/model" "github.com/go-openapi/testify/v2/codegen/internal/scanner" ) type config struct { dir string targetRoot string + targetDoc string + inputPkg string outputPkgs string @@ -25,6 +31,7 @@ type config struct { includeGen bool includeHlp bool includeExa bool + includeDoc bool runExa bool } @@ -43,6 +50,7 @@ func registerFlags(cfg *config) { flag.StringVar(&cfg.inputPkg, "input-package", "github.com/go-openapi/testify/v2/internal/assertions", "source package to scan to produce generated output") flag.StringVar(&cfg.outputPkgs, "output-packages", "assert,require", "package(s) to generate") flag.StringVar(&cfg.targetRoot, "target-root", "..", "where to place the generated packages (default is in the parent folder)") + flag.StringVar(&cfg.targetDoc, "target-doc", "docs/doc-site/api", "where to place the generated documentation, relative to the target root") flag.BoolVar(&cfg.includeFmt, "include-format-funcs", true, "include format functions such as Errorf and Equalf") flag.BoolVar(&cfg.includeFwd, "include-forward-funcs", true, "include forward assertions functions as methods of the Assertions object") @@ -51,12 +59,14 @@ func registerFlags(cfg *config) { flag.BoolVar(&cfg.includeHlp, "include-helpers", true, "include helper functions that are not assertions") flag.BoolVar(&cfg.includeExa, "include-examples", true, "include generated testable examples") flag.BoolVar(&cfg.runExa, "runnable-examples", true, "include Output to generated testable examples, so they are run as tests") + flag.BoolVar(&cfg.includeDoc, "include-doc", true, "include generated markdown documentation") } func execute(cfg *config) error { scanner := scanner.New( scanner.WithWorkDir(cfg.dir), scanner.WithPackage(cfg.inputPkg), + scanner.WithCollectDoc(cfg.includeDoc), // collects extra documentation from comments ) scanned, err := scanner.Scan() if err != nil { @@ -64,6 +74,7 @@ func execute(cfg *config) error { } builder := generator.New(scanned) + var doc model.Documentation for targetPkg := range strings.SplitSeq(cfg.outputPkgs, ",") { err = builder.Generate( @@ -78,10 +89,36 @@ func execute(cfg *config) error { generator.WithIncludeHelpers(cfg.includeHlp), generator.WithIncludeExamples(cfg.includeExa), generator.WithRunnableExamples(cfg.runExa), + generator.WithIncludeDoc(cfg.includeDoc), ) if err != nil { return err } + + if cfg.includeDoc { + // stash the transformed doc + doc.Merge(builder.Documentation()) + } + } + if !cfg.includeDoc { + // we're done with codegen + return nil + } + + // and now for something completely different: generating the documentation + documentalist := generator.NewDocGenerator(doc) + err = documentalist.Generate( + // where to generate + generator.WithTargetRoot(cfg.targetRoot), + generator.WithTargetDoc(cfg.targetDoc), + // what to generate + generator.WithIncludeFormatFuncs(cfg.includeFmt), + generator.WithIncludeForwardFuncs(cfg.includeFwd), + generator.WithIncludeGenerics(cfg.includeGen), + generator.WithIncludeHelpers(cfg.includeHlp), + ) + if err != nil { + return err } return nil diff --git a/docs/EXAMPLES_STATUS.md b/docs/EXAMPLES_STATUS.md deleted file mode 100644 index dc2f6f659..000000000 --- a/docs/EXAMPLES_STATUS.md +++ /dev/null @@ -1,148 +0,0 @@ -# Test Examples Status - -This document tracks which assertion functions have test examples added. - -## ✅ Completed (with values) - -### boolean.go -- [x] True - `success: 1 == 1`, `failure: 1 == 0` -- [x] False - `success: 1 == 0`, `failure: 1 == 1` - -### collection.go -- [x] Len - `success: []string{"A","B"}, 2`, `failure: []string{"A","B"}, 1` -- [x] Contains - `success: []string{"A","B"}, "A"`, `failure: []string{"A","B"}, "C"` -- [x] NotContains - `success: []string{"A","B"}, "C"`, `failure: []string{"A","B"}, "B"` - -### equal.go -- [x] Equal - `success: 123, 123`, `failure: 123, 456` -- [x] EqualValues - `success: uint32(123), int32(123)`, `failure: uint32(123), int32(456)` - -### error.go -- [x] NoError - `success: nil`, `failure: ErrTest` -- [x] Error - `success: ErrTest`, `failure: nil` -- [x] EqualError - `success: ErrTest, "assert.ErrTest general error for testing"`, `failure: ErrTest, "wrong error message"` -- [x] ErrorContains - `success: ErrTest, "general error"`, `failure: ErrTest, "not in message"` - -### yaml.go -- [x] YAMLEq - `panic:` (with message about yaml feature) - -## 📝 Completed (with TODO placeholders) - -### equal.go -- [x] Same - TODO: add pointer test values -- [x] NotSame - TODO: add different pointer test values -- [x] EqualExportedValues - TODO: add struct test values - -## ⏳ Remaining Functions (need examples) - -### equal.go -- [ ] NotEqual -- [ ] NotEqualValues -- [ ] Exactly - -### collection.go -- [ ] Empty -- [ ] NotEmpty -- [ ] Subset -- [ ] NotSubset -- [ ] ElementsMatch -- [ ] NotElementsMatch - -### compare.go -- [ ] Greater -- [ ] GreaterOrEqual -- [ ] Less -- [ ] LessOrEqual -- [ ] Positive -- [ ] Negative - -### error.go -- [ ] ErrorIs -- [ ] ErrorAs -- [ ] NotErrorIs - -### object.go -- [ ] Nil -- [ ] NotNil -- [ ] IsType -- [ ] NotNil -- [ ] Zero -- [ ] NotZero - -### panic.go -- [ ] Panics -- [ ] PanicsWithValue -- [ ] PanicsWithError -- [ ] NotPanics - -### string.go -- [ ] Regexp -- [ ] NotRegexp - -### number.go -- [ ] InDelta -- [ ] InDeltaSlice -- [ ] InDeltaMapValues -- [ ] InEpsilon -- [ ] InEpsilonSlice - -### condition.go -- [ ] Condition - -### http.go -- [ ] HTTPSuccess -- [ ] HTTPRedirect -- [ ] HTTPError -- [ ] HTTPStatusCode -- [ ] HTTPBodyContains -- [ ] HTTPBodyNotContains - -### json.go -- [ ] JSONEq - -### file.go -- [ ] FileExists -- [ ] NoFileExists -- [ ] DirExists -- [ ] NoDirExists - -### time.go -- [ ] WithinDuration -- [ ] WithinRange - -### type.go -- [ ] Implements - -### testing.go -- [ ] Never -- [ ] Eventually -- [ ] EventuallyWithT - -### assertion.go -- [ ] Fail -- [ ] FailNow - -## Template for Adding Examples - -```go -// FunctionName does something. -// -// Usage: -// -// assert.FunctionName(t, args...) -// -// Examples: -// -// success: -// failure: -// panic: -// -func FunctionName(...) bool { -``` - -## Notes - -- Use actual Go values that can be directly inserted into test code -- For complex types, use TODO comments -- success/failure are required, panic is optional -- All values will be injected directly into generated test code diff --git a/docs/MAINTAINERS.md b/docs/MAINTAINERS.md index e17b822f1..f211c183a 100644 --- a/docs/MAINTAINERS.md +++ b/docs/MAINTAINERS.md @@ -246,6 +246,24 @@ The "Examples:" section in doc comments drives test and example generation: // failure: []string{"a"}, 5 ``` +**Important: Placeholder marker for complex values** + +When example values are too complex to represent inline (pointers, large structs, etc.), use `// NOT IMPLEMENTED` as the placeholder marker: + +```go +// Examples: +// +// success: &customStruct{Field: "value"}, // NOT IMPLEMENTED +// failure: complexType{}, // NOT IMPLEMENTED +``` + +**Never use `// TODO`** - it triggers false positives in: +- Code quality analyzers (linters scanning for TODO items) +- Project management tools (TODO trackers) +- IDE warnings about unfinished work + +The `// NOT IMPLEMENTED` marker clearly indicates a placeholder without triggering these tools. + ### Special Cases in Generated Tests For complex assertions requiring special setup, the test templates support conditional logic. See `codegen/internal/generator/templates/assertion_assertions_test.gotmpl` for examples of: diff --git a/docs/doc-site/_index.md b/docs/doc-site/_index.md new file mode 100644 index 000000000..172401bdc --- /dev/null +++ b/docs/doc-site/_index.md @@ -0,0 +1,128 @@ +--- +title: "Testify v2" +type: home +description: 'Go testing assertions for the rest of us' +weight: 1 +--- + +**Go testing assertions for the rest of us** + +{{% notice info %}} +This is the home of `github.com/go-openapi/testify/v2`, an active, opinionated fork of `github.com/stretchr/testify`. +{{% /notice %}} + +## Testify v2 - The v2 our tests wanted + +A set of `go` packages that provide tools for testifying that your code behaves as you intended. + +This is the go-openapi fork of the great [testify](https://github.com/stretchr/testify) package. + +### Status + +{{% button href="https://github.com/go-openapi/testify/fork" hint="fork me on github" style=primary icon=code-fork %}}Fork me{{% /button %}} +Design and exploration phase. Contributions and proposals are welcome. + +### Motivation + +From the maintainers of `testify`, it looks like a v2 will eventually be released, but they'll do it at their own pace. + +We like all the principles they put forward to build this v2. [See discussion about v2](https://github.com/stretchr/testify/discussions/1560) + +However, at `go-openapi` we would like to address the well-known issues in `testify` with different priorities. + +With this fork, we want to: +1. remove all external dependencies. +2. make it easy to maintain and extend. +3. pare down some of the chrome that has been added over the years. + +{{% notice style="primary" title="Extended hand" icon="hand" %}} +We hope that this endeavor will help the original project with a live-drill of what a v2 could look like. +Hopefully, some of our ideas here will eventually percolate back into the original project. + +We are always happy to discuss with people who face the same problems as we do: avoid breaking changes, +APIs that became bloated over a decade or so, uncontrolled dependencies, difficult choices when it comes to introduce +breaking changes, conflicting demands from users etc. +{{% /notice %}} + +More about our motivations in the project's [README](README.md). + +### Getting started + +To use this package in your projects: + +```cmd + go get github.com/go-openapi/testify/v2 +``` + +... and start writing tests. Look at our [examples](./examples/). + +### Basic usage + +`testify` simplifies your test assertions like so. + +{{< cards >}} +{{% card title="Standard library" %}} +```go + import ( + "testing" + ) + ... + + const expected = "expected result" + + result := printImports(input) + if result != expected { + t.Errorf( + "Expected: %s. Got: %s", + expected, result, + ) + + return + } +``` +{{% /card %}} + +{{% card title="testify" %}} +```go + import ( + "testing" + "github.com/go-openapi/testify/v2" + ) + ... + + const expected = "expected result" + + require.Equalf(t, + expected, printImports(input), + "Expected: %s. Got: %s", + expected, result, + ) +``` +{{% /card %}} +{{< /cards >}} + +## Licensing + +`SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers` + +This library ships under the [SPDX-License-Identifier: Apache-2.0](./project/LICENSE.md). + +See the license [NOTICE](./project/NOTICE.md), which recalls the licensing terms of all the pieces of software +distributed with this fork, including internalized libraries. + +## 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. + +{{% notice style="primary" title="Info" icon="info" %}} +Code generation is used. Run `go generate ./...` to update generated files. +{{% /notice %}} + +See also the [CONTRIBUTING guidelines](./project/contributing/CONTRIBUTING.md). + +--- + +{{< children type="card" description="true" >}} diff --git a/docs/doc-site/api/_index.md b/docs/doc-site/api/_index.md new file mode 100644 index 000000000..367e8cd2b --- /dev/null +++ b/docs/doc-site/api/_index.md @@ -0,0 +1,70 @@ +--- +title: "Assertions index" +description: | + Index Of Assertion Domains. + + + Find the assertion function you need for your data. +weight: 1 +modified: 2026-01-02 +--- + +**Go testing assertions for the rest of us** + +The [`testify/v2`][testifyv2] package has a fairly large API surface. + +This is in part inherited from the original [`testify`][testifyv1] and also +largely due to the many available variants of the same assertion function. + +The following sections organize the assertion API into usage domains, +with all documented exported variants documented in a more concise form than the +[reference godoc documentation](https://pkg.go.dev/github.com/go-openapi/testify/v2). + +[![GoDoc][godoc-badge]][godoc-url] +{class="inline-badge"} + +## Domains + +The `testify` API is organized in 18 domains shown below. + +{{< children type="card" description="true" >}} + +--- + +- [Boolean](./boolean.md) - Asserting Boolean Values (2) +- [Collection](./collection.md) - Asserting Slices And Maps (7) +- [Comparison](./comparison.md) - Comparing Ordered Values (6) +- [Condition](./condition.md) - Expressing Assertions Using Conditions (4) +- [Equality](./equality.md) - Asserting Two Things Are Equal (12) +- [Error](./error.md) - Asserting Errors (8) +- [File](./file.md) - Asserting OS Files (6) +- [Http](./http.md) - Asserting HTTP Response And Body (7) +- [Json](./json.md) - Asserting JSON Documents (2) +- [Number](./number.md) - Asserting Numbers (5) +- [Ordering](./ordering.md) - Asserting How Collections Are Ordered (4) +- [Panic](./panic.md) - Asserting A Panic Behavior (4) +- [String](./string.md) - Asserting Strings (2) +- [Testing](./testing.md) - Mimicks Methods From The Testing Standard Library (2) +- [Time](./time.md) - Asserting Times And Durations (2) +- [Type](./type.md) - Asserting Types Rather Than Values (6) +- [Yaml](./yaml.md) - Asserting Yaml Documents (1) +- [Common](./common.md) - Other Uncategorized Helpers (4) + +--- + +Generated with github.com/go-openapi/testify/v2/codegen + +[godoc-badge]: https://pkg.go.dev/badge/github.com/go-openapi/testify/v2 +[godoc-url]: https://pkg.go.dev/github.com/go-openapi/testify/v2 +[testifyv2]: https://github.com/go-openapi/testify +[testifyv1]: https://github.com/stretchr/testify + + diff --git a/docs/doc-site/api/boolean.md b/docs/doc-site/api/boolean.md new file mode 100644 index 000000000..2d0abeec5 --- /dev/null +++ b/docs/doc-site/api/boolean.md @@ -0,0 +1,137 @@ +--- +title: "Boolean" +description: "Asserting Boolean Values" +modified: 2026-01-02 +weight: 1 +domains: + - "boolean" +keywords: + - "False" + - "Falsef" + - "True" + - "Truef" +--- + +Asserting Boolean Values + +## Assertions + +[![GoDoc][godoc-badge]][godoc-url] +{class="inline-badge"} + +_All links point to _ + +This domain exposes 2 functionalities. + +### False + +False asserts that the specified value is false. + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go + assertions.False(t, myBool) +``` +{{< /tab >}} +{{% tab title="Examples" %}} +```go + success: 1 == 0 + failure: 1 == 1 +``` +{{< /tab >}} +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.False(t T, value bool, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#False) | package-level function | +| [`assert.Falsef(t T, value bool, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Falsef) | formatted variant | +| [`assert.(*Assertions).False(value bool) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.False) | method variant | +| [`assert.(*Assertions).Falsef(value bool, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.Falsef) | method formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.False(t T, value bool, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#False) | package-level function | +| [`require.Falsef(t T, value bool, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Falsef) | formatted variant | +| [`require.(*Assertions).False(value bool) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.False) | method variant | +| [`require.(*Assertions).Falsef(value bool, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.Falsef) | method formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.False(t T, value bool, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#False) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#False](https://github.com/go-openapi/testify/blob/master/internal/assertions/boolean.go#L38) +{{% /tab %}} +{{< /tabs >}} + +### True + +True asserts that the specified value is true. + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go + assertions.True(t, myBool) +``` +{{< /tab >}} +{{% tab title="Examples" %}} +```go + success: 1 == 1 + failure: 1 == 0 +``` +{{< /tab >}} +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.True(t T, value bool, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#True) | package-level function | +| [`assert.Truef(t T, value bool, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Truef) | formatted variant | +| [`assert.(*Assertions).True(value bool) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.True) | method variant | +| [`assert.(*Assertions).Truef(value bool, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.Truef) | method formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.True(t T, value bool, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#True) | package-level function | +| [`require.Truef(t T, value bool, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Truef) | formatted variant | +| [`require.(*Assertions).True(value bool) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.True) | method variant | +| [`require.(*Assertions).Truef(value bool, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.Truef) | method formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.True(t T, value bool, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#True) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#True](https://github.com/go-openapi/testify/blob/master/internal/assertions/boolean.go#L16) +{{% /tab %}} +{{< /tabs >}} + +--- + +--- + +Generated with github.com/go-openapi/testify/v2/codegen + +[godoc-badge]: https://pkg.go.dev/badge/github.com/go-openapi/testify/v2 +[godoc-url]: https://pkg.go.dev/github.com/go-openapi/testify/v2 + + diff --git a/docs/doc-site/api/collection.md b/docs/doc-site/api/collection.md new file mode 100644 index 000000000..356c2b7ef --- /dev/null +++ b/docs/doc-site/api/collection.md @@ -0,0 +1,423 @@ +--- +title: "Collection" +description: "Asserting Slices And Maps" +modified: 2026-01-02 +weight: 2 +domains: + - "collection" +keywords: + - "Contains" + - "Containsf" + - "ElementsMatch" + - "ElementsMatchf" + - "Len" + - "Lenf" + - "NotContains" + - "NotContainsf" + - "NotElementsMatch" + - "NotElementsMatchf" + - "NotSubset" + - "NotSubsetf" + - "Subset" + - "Subsetf" +--- + +Asserting Slices And Maps + +## Assertions + +[![GoDoc][godoc-badge]][godoc-url] +{class="inline-badge"} + +_All links point to _ + +This domain exposes 7 functionalities. + +### Contains + +Contains asserts that the specified string, list(array, slice...) or map contains the +specified substring or element. + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go + assertions.Contains(t, "Hello World", "World") + assertions.Contains(t, []string{"Hello", "World"}, "World") + assertions.Contains(t, map[string]string{"Hello": "World"}, "Hello") +``` +{{< /tab >}} +{{% tab title="Examples" %}} +```go + success: []string{"A","B"}, "A" + failure: []string{"A","B"}, "C" +``` +{{< /tab >}} +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.Contains(t T, s any, contains any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Contains) | package-level function | +| [`assert.Containsf(t T, s any, contains any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Containsf) | formatted variant | +| [`assert.(*Assertions).Contains(s any, contains any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.Contains) | method variant | +| [`assert.(*Assertions).Containsf(s any, contains any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.Containsf) | method formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.Contains(t T, s any, contains any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Contains) | package-level function | +| [`require.Containsf(t T, s any, contains any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Containsf) | formatted variant | +| [`require.(*Assertions).Contains(s any, contains any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.Contains) | method variant | +| [`require.(*Assertions).Containsf(s any, contains any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.Containsf) | method formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.Contains(t T, s any, contains any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#Contains) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#Contains](https://github.com/go-openapi/testify/blob/master/internal/assertions/collection.go#L64) +{{% /tab %}} +{{< /tabs >}} + +### ElementsMatch + +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. + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go + assertions.ElementsMatch(t, [1, 3, 2, 3], [1, 3, 3, 2]) +``` +{{< /tab >}} +{{% tab title="Examples" %}} +```go + success: []int{1, 3, 2, 3}, []int{1, 3, 3, 2} + failure: []int{1, 2, 3}, []int{1, 2, 4} +``` +{{< /tab >}} +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.ElementsMatch(t T, listA any, listB any, msgAndArgs ...any) (ok bool)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#ElementsMatch) | package-level function | +| [`assert.ElementsMatchf(t T, listA any, listB any, msg string, args ...any) (ok bool)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#ElementsMatchf) | formatted variant | +| [`assert.(*Assertions).ElementsMatch(listA any, listB any) (ok bool)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.ElementsMatch) | method variant | +| [`assert.(*Assertions).ElementsMatchf(listA any, listB any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.ElementsMatchf) | method formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.ElementsMatch(t T, listA any, listB any, msgAndArgs ...any) (ok bool)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#ElementsMatch) | package-level function | +| [`require.ElementsMatchf(t T, listA any, listB any, msg string, args ...any) (ok bool)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#ElementsMatchf) | formatted variant | +| [`require.(*Assertions).ElementsMatch(listA any, listB any) (ok bool)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.ElementsMatch) | method variant | +| [`require.(*Assertions).ElementsMatchf(listA any, listB any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.ElementsMatchf) | method formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.ElementsMatch(t T, listA any, listB any, msgAndArgs ...any) (ok bool)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#ElementsMatch) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#ElementsMatch](https://github.com/go-openapi/testify/blob/master/internal/assertions/collection.go#L277) +{{% /tab %}} +{{< /tabs >}} + +### Len + +Len asserts that the specified object has specific length. + +Len also fails if the object has a type that len() does not accept. + +The asserted object can be a string, a slice, a map, an array or a channel. + +See also [reflect.Len](https://pkg.go.dev/reflect#Len). + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go + assertions.Len(t, mySlice, 3) + assertions.Len(t, myString, 4) + assertions.Len(t, myMap, 5) +``` +{{< /tab >}} +{{% tab title="Examples" %}} +```go + success: []string{"A","B"}, 2 + failure: []string{"A","B"}, 1 +``` +{{< /tab >}} +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.Len(t T, object any, length int, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Len) | package-level function | +| [`assert.Lenf(t T, object any, length int, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Lenf) | formatted variant | +| [`assert.(*Assertions).Len(object any, length int) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.Len) | method variant | +| [`assert.(*Assertions).Lenf(object any, length int, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.Lenf) | method formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.Len(t T, object any, length int, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Len) | package-level function | +| [`require.Lenf(t T, object any, length int, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Lenf) | formatted variant | +| [`require.(*Assertions).Len(object any, length int) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.Len) | method variant | +| [`require.(*Assertions).Lenf(object any, length int, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.Lenf) | method formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.Len(t T, object any, length int, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#Len) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#Len](https://github.com/go-openapi/testify/blob/master/internal/assertions/collection.go#L31) + +> **Maintainer Note** +> The implementation is based on [reflect.Len]. The potential panic is handled with recover. +A better approach could be to check for the [reflect.Type] before calling [reflect.Len]. + +> **Note** +> (proposals) this does not currently support iterators, or collection objects that have a Len() method. +{{% /tab %}} +{{< /tabs >}} + +### NotContains + +NotContains asserts that the specified string, list(array, slice...) or map does NOT contain the +specified substring or element. + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go + assertions.NotContains(t, "Hello World", "Earth") + assertions.NotContains(t, ["Hello", "World"], "Earth") + assertions.NotContains(t, {"Hello": "World"}, "Earth") +``` +{{< /tab >}} +{{% tab title="Examples" %}} +```go + success: []string{"A","B"}, "C" + failure: []string{"A","B"}, "B" +``` +{{< /tab >}} +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.NotContains(t T, s any, contains any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#NotContains) | package-level function | +| [`assert.NotContainsf(t T, s any, contains any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#NotContainsf) | formatted variant | +| [`assert.(*Assertions).NotContains(s any, contains any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.NotContains) | method variant | +| [`assert.(*Assertions).NotContainsf(s any, contains any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.NotContainsf) | method formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.NotContains(t T, s any, contains any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#NotContains) | package-level function | +| [`require.NotContainsf(t T, s any, contains any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#NotContainsf) | formatted variant | +| [`require.(*Assertions).NotContains(s any, contains any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.NotContains) | method variant | +| [`require.(*Assertions).NotContainsf(s any, contains any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.NotContainsf) | method formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.NotContains(t T, s any, contains any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#NotContains) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#NotContains](https://github.com/go-openapi/testify/blob/master/internal/assertions/collection.go#L94) +{{% /tab %}} +{{< /tabs >}} + +### NotElementsMatch + +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. + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go + assertions.NotElementsMatch(t, [1, 1, 2, 3], [1, 1, 2, 3]) -> false + assertions.NotElementsMatch(t, [1, 1, 2, 3], [1, 2, 3]) -> true + assertions.NotElementsMatch(t, [1, 2, 3], [1, 2, 4]) -> true +``` +{{< /tab >}} +{{% tab title="Examples" %}} +```go + success: []int{1, 2, 3}, []int{1, 2, 4} + failure: []int{1, 3, 2, 3}, []int{1, 3, 3, 2} +``` +{{< /tab >}} +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.NotElementsMatch(t T, listA any, listB any, msgAndArgs ...any) (ok bool)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#NotElementsMatch) | package-level function | +| [`assert.NotElementsMatchf(t T, listA any, listB any, msg string, args ...any) (ok bool)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#NotElementsMatchf) | formatted variant | +| [`assert.(*Assertions).NotElementsMatch(listA any, listB any) (ok bool)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.NotElementsMatch) | method variant | +| [`assert.(*Assertions).NotElementsMatchf(listA any, listB any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.NotElementsMatchf) | method formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.NotElementsMatch(t T, listA any, listB any, msgAndArgs ...any) (ok bool)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#NotElementsMatch) | package-level function | +| [`require.NotElementsMatchf(t T, listA any, listB any, msg string, args ...any) (ok bool)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#NotElementsMatchf) | formatted variant | +| [`require.(*Assertions).NotElementsMatch(listA any, listB any) (ok bool)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.NotElementsMatch) | method variant | +| [`require.(*Assertions).NotElementsMatchf(listA any, listB any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.NotElementsMatchf) | method formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.NotElementsMatch(t T, listA any, listB any, msgAndArgs ...any) (ok bool)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#NotElementsMatch) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#NotElementsMatch](https://github.com/go-openapi/testify/blob/master/internal/assertions/collection.go#L314) +{{% /tab %}} +{{< /tabs >}} + +### NotSubset + +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. + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go + assertions.NotSubset(t, [1, 3, 4], [1, 2]) + assertions.NotSubset(t, {"x": 1, "y": 2}, {"z": 3}) + assertions.NotSubset(t, [1, 3, 4], {1: "one", 2: "two"}) + assertions.NotSubset(t, {"x": 1, "y": 2}, ["z"]) +``` +{{< /tab >}} +{{% tab title="Examples" %}} +```go + success: []int{1, 2, 3}, []int{4, 5} + failure: []int{1, 2, 3}, []int{1, 2} +``` +{{< /tab >}} +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.NotSubset(t T, list any, subset any, msgAndArgs ...any) (ok bool)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#NotSubset) | package-level function | +| [`assert.NotSubsetf(t T, list any, subset any, msg string, args ...any) (ok bool)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#NotSubsetf) | formatted variant | +| [`assert.(*Assertions).NotSubset(list any, subset any) (ok bool)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.NotSubset) | method variant | +| [`assert.(*Assertions).NotSubsetf(list any, subset any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.NotSubsetf) | method formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.NotSubset(t T, list any, subset any, msgAndArgs ...any) (ok bool)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#NotSubset) | package-level function | +| [`require.NotSubsetf(t T, list any, subset any, msg string, args ...any) (ok bool)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#NotSubsetf) | formatted variant | +| [`require.(*Assertions).NotSubset(list any, subset any) (ok bool)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.NotSubset) | method variant | +| [`require.(*Assertions).NotSubsetf(list any, subset any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.NotSubsetf) | method formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.NotSubset(t T, list any, subset any, msgAndArgs ...any) (ok bool)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#NotSubset) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#NotSubset](https://github.com/go-openapi/testify/blob/master/internal/assertions/collection.go#L205) +{{% /tab %}} +{{< /tabs >}} + +### Subset + +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. + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go + assertions.Subset(t, [1, 2, 3], [1, 2]) + assertions.Subset(t, {"x": 1, "y": 2}, {"x": 1}) + assertions.Subset(t, [1, 2, 3], {1: "one", 2: "two"}) + assertions.Subset(t, {"x": 1, "y": 2}, ["x"]) +``` +{{< /tab >}} +{{% tab title="Examples" %}} +```go + success: []int{1, 2, 3}, []int{1, 2} + failure: []int{1, 2, 3}, []int{4, 5} +``` +{{< /tab >}} +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.Subset(t T, list any, subset any, msgAndArgs ...any) (ok bool)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Subset) | package-level function | +| [`assert.Subsetf(t T, list any, subset any, msg string, args ...any) (ok bool)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Subsetf) | formatted variant | +| [`assert.(*Assertions).Subset(list any, subset any) (ok bool)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.Subset) | method variant | +| [`assert.(*Assertions).Subsetf(list any, subset any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.Subsetf) | method formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.Subset(t T, list any, subset any, msgAndArgs ...any) (ok bool)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Subset) | package-level function | +| [`require.Subsetf(t T, list any, subset any, msg string, args ...any) (ok bool)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Subsetf) | formatted variant | +| [`require.(*Assertions).Subset(list any, subset any) (ok bool)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.Subset) | method variant | +| [`require.(*Assertions).Subsetf(list any, subset any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.Subsetf) | method formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.Subset(t T, list any, subset any, msgAndArgs ...any) (ok bool)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#Subset) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#Subset](https://github.com/go-openapi/testify/blob/master/internal/assertions/collection.go#L128) +{{% /tab %}} +{{< /tabs >}} + +--- + +--- + +Generated with github.com/go-openapi/testify/v2/codegen + +[godoc-badge]: https://pkg.go.dev/badge/github.com/go-openapi/testify/v2 +[godoc-url]: https://pkg.go.dev/github.com/go-openapi/testify/v2 + + diff --git a/docs/doc-site/api/common.md b/docs/doc-site/api/common.md new file mode 100644 index 000000000..8156537da --- /dev/null +++ b/docs/doc-site/api/common.md @@ -0,0 +1,161 @@ +--- +title: "Common" +description: "Other Uncategorized Helpers" +modified: 2026-01-02 +weight: 18 +domains: + - "common" +keywords: + - "CallerInfo" + - "CallerInfof" + - "New" + - "Newf" + - "ObjectsAreEqual" + - "ObjectsAreEqualf" + - "ObjectsAreEqualValues" + - "ObjectsAreEqualValuesf" +--- + +Other Uncategorized Helpers + +## Assertions + +[![GoDoc][godoc-badge]][godoc-url] +{class="inline-badge"} + +_All links point to _ + +This domain exposes 4 functionalities. + +--- + +## Other helpers + +### CallerInfo + +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. + + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.CallerInfo() []string`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#CallerInfo) | package-level function | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.CallerInfo() []string`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#CallerInfo) | package-level function | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.CallerInfo() []string`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#CallerInfo) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#CallerInfo](https://github.com/go-openapi/testify/blob/master/internal/assertions/testing.go#L19) + +> **Maintainer Note** +> it is not necessary to export CallerInfo. This should remain an internal implementation detail. +{{% /tab %}} +{{< /tabs >}} + +### New + +New makes a new [Assertions] object for the specified [T]. + + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.New(t T) *Assertions`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#New) | package-level function | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.New(t T) *Assertions`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#New) | package-level function | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.New(t T) *Assertions`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#New) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#New](https://github.com/go-openapi/testify/blob/master/internal/assertions/assertion.go#L12) +{{% /tab %}} +{{< /tabs >}} + +### ObjectsAreEqual + +ObjectsAreEqual determines if two objects are considered equal. + +This function does no assertion of any kind. + + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.ObjectsAreEqual(expected any, actual any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#ObjectsAreEqual) | package-level function | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.ObjectsAreEqual(expected any, actual any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#ObjectsAreEqual) | package-level function | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.ObjectsAreEqual(expected any, actual any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#ObjectsAreEqual) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#ObjectsAreEqual](https://github.com/go-openapi/testify/blob/master/internal/assertions/object.go#L14) +{{% /tab %}} +{{< /tabs >}} + +### ObjectsAreEqualValues + +ObjectsAreEqualValues gets whether two objects are equal, or if their +values are equal. + + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.ObjectsAreEqualValues(expected any, actual any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#ObjectsAreEqualValues) | package-level function | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.ObjectsAreEqualValues(expected any, actual any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#ObjectsAreEqualValues) | package-level function | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.ObjectsAreEqualValues(expected any, actual any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#ObjectsAreEqualValues) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#ObjectsAreEqualValues](https://github.com/go-openapi/testify/blob/master/internal/assertions/object.go#L38) +{{% /tab %}} +{{< /tabs >}} + +--- + +Generated with github.com/go-openapi/testify/v2/codegen + +[godoc-badge]: https://pkg.go.dev/badge/github.com/go-openapi/testify/v2 +[godoc-url]: https://pkg.go.dev/github.com/go-openapi/testify/v2 + + diff --git a/docs/doc-site/api/comparison.md b/docs/doc-site/api/comparison.md new file mode 100644 index 000000000..2217f9646 --- /dev/null +++ b/docs/doc-site/api/comparison.md @@ -0,0 +1,345 @@ +--- +title: "Comparison" +description: "Comparing Ordered Values" +modified: 2026-01-02 +weight: 3 +domains: + - "comparison" +keywords: + - "Greater" + - "Greaterf" + - "GreaterOrEqual" + - "GreaterOrEqualf" + - "Less" + - "Lessf" + - "LessOrEqual" + - "LessOrEqualf" + - "Negative" + - "Negativef" + - "Positive" + - "Positivef" +--- + +Comparing Ordered Values + +## Assertions + +[![GoDoc][godoc-badge]][godoc-url] +{class="inline-badge"} + +_All links point to _ + +This domain exposes 6 functionalities. + +### Greater + +Greater asserts that the first element is strictly greater than the second. + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go + assertions.Greater(t, 2, 1) + assertions.Greater(t, float64(2), float64(1)) + assertions.Greater(t, "b", "a") +``` +{{< /tab >}} +{{% tab title="Examples" %}} +```go + success: 2, 1 + failure: 1, 2 +``` +{{< /tab >}} +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.Greater(t T, e1 any, e2 any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Greater) | package-level function | +| [`assert.Greaterf(t T, e1 any, e2 any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Greaterf) | formatted variant | +| [`assert.(*Assertions).Greater(e1 any, e2 any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.Greater) | method variant | +| [`assert.(*Assertions).Greaterf(e1 any, e2 any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.Greaterf) | method formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.Greater(t T, e1 any, e2 any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Greater) | package-level function | +| [`require.Greaterf(t T, e1 any, e2 any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Greaterf) | formatted variant | +| [`require.(*Assertions).Greater(e1 any, e2 any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.Greater) | method variant | +| [`require.(*Assertions).Greaterf(e1 any, e2 any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.Greaterf) | method formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.Greater(t T, e1 any, e2 any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#Greater) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#Greater](https://github.com/go-openapi/testify/blob/master/internal/assertions/compare.go#L56) +{{% /tab %}} +{{< /tabs >}} + +### GreaterOrEqual + +GreaterOrEqual asserts that the first element is greater than or equal to the second. + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go + assertions.GreaterOrEqual(t, 2, 1) + assertions.GreaterOrEqual(t, 2, 2) + assertions.GreaterOrEqual(t, "b", "a") + assertions.GreaterOrEqual(t, "b", "b") +``` +{{< /tab >}} +{{% tab title="Examples" %}} +```go + success: 2, 1 + failure: 1, 2 +``` +{{< /tab >}} +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.GreaterOrEqual(t T, e1 any, e2 any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#GreaterOrEqual) | package-level function | +| [`assert.GreaterOrEqualf(t T, e1 any, e2 any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#GreaterOrEqualf) | formatted variant | +| [`assert.(*Assertions).GreaterOrEqual(e1 any, e2 any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.GreaterOrEqual) | method variant | +| [`assert.(*Assertions).GreaterOrEqualf(e1 any, e2 any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.GreaterOrEqualf) | method formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.GreaterOrEqual(t T, e1 any, e2 any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#GreaterOrEqual) | package-level function | +| [`require.GreaterOrEqualf(t T, e1 any, e2 any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#GreaterOrEqualf) | formatted variant | +| [`require.(*Assertions).GreaterOrEqual(e1 any, e2 any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.GreaterOrEqual) | method variant | +| [`require.(*Assertions).GreaterOrEqualf(e1 any, e2 any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.GreaterOrEqualf) | method formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.GreaterOrEqual(t T, e1 any, e2 any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#GreaterOrEqual) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#GreaterOrEqual](https://github.com/go-openapi/testify/blob/master/internal/assertions/compare.go#L78) +{{% /tab %}} +{{< /tabs >}} + +### Less + +Less asserts that the first element is strictly less than the second. + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go + assertions.Less(t, 1, 2) + assertions.Less(t, float64(1), float64(2)) + assertions.Less(t, "a", "b") +``` +{{< /tab >}} +{{% tab title="Examples" %}} +```go + success: 1, 2 + failure: 2, 1 +``` +{{< /tab >}} +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.Less(t T, e1 any, e2 any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Less) | package-level function | +| [`assert.Lessf(t T, e1 any, e2 any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Lessf) | formatted variant | +| [`assert.(*Assertions).Less(e1 any, e2 any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.Less) | method variant | +| [`assert.(*Assertions).Lessf(e1 any, e2 any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.Lessf) | method formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.Less(t T, e1 any, e2 any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Less) | package-level function | +| [`require.Lessf(t T, e1 any, e2 any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Lessf) | formatted variant | +| [`require.(*Assertions).Less(e1 any, e2 any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.Less) | method variant | +| [`require.(*Assertions).Lessf(e1 any, e2 any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.Lessf) | method formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.Less(t T, e1 any, e2 any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#Less) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#Less](https://github.com/go-openapi/testify/blob/master/internal/assertions/compare.go#L99) +{{% /tab %}} +{{< /tabs >}} + +### LessOrEqual + +LessOrEqual asserts that the first element is less than or equal to the second. + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go + assertions.LessOrEqual(t, 1, 2) + assertions.LessOrEqual(t, 2, 2) + assertions.LessOrEqual(t, "a", "b") + assertions.LessOrEqual(t, "b", "b") +``` +{{< /tab >}} +{{% tab title="Examples" %}} +```go + success: 1, 2 + failure: 2, 1 +``` +{{< /tab >}} +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.LessOrEqual(t T, e1 any, e2 any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#LessOrEqual) | package-level function | +| [`assert.LessOrEqualf(t T, e1 any, e2 any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#LessOrEqualf) | formatted variant | +| [`assert.(*Assertions).LessOrEqual(e1 any, e2 any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.LessOrEqual) | method variant | +| [`assert.(*Assertions).LessOrEqualf(e1 any, e2 any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.LessOrEqualf) | method formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.LessOrEqual(t T, e1 any, e2 any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#LessOrEqual) | package-level function | +| [`require.LessOrEqualf(t T, e1 any, e2 any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#LessOrEqualf) | formatted variant | +| [`require.(*Assertions).LessOrEqual(e1 any, e2 any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.LessOrEqual) | method variant | +| [`require.(*Assertions).LessOrEqualf(e1 any, e2 any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.LessOrEqualf) | method formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.LessOrEqual(t T, e1 any, e2 any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#LessOrEqual) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#LessOrEqual](https://github.com/go-openapi/testify/blob/master/internal/assertions/compare.go#L121) +{{% /tab %}} +{{< /tabs >}} + +### Negative + +Negative asserts that the specified element is strictly negative. + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go + assertions.Negative(t, -1) + assertions.Negative(t, -1.23) +``` +{{< /tab >}} +{{% tab title="Examples" %}} +```go + success: -1 + failure: 1 +``` +{{< /tab >}} +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.Negative(t T, e any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Negative) | package-level function | +| [`assert.Negativef(t T, e any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Negativef) | formatted variant | +| [`assert.(*Assertions).Negative(e any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.Negative) | method variant | +| [`assert.(*Assertions).Negativef(e any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.Negativef) | method formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.Negative(t T, e any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Negative) | package-level function | +| [`require.Negativef(t T, e any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Negativef) | formatted variant | +| [`require.(*Assertions).Negative(e any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.Negative) | method variant | +| [`require.(*Assertions).Negativef(e any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.Negativef) | method formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.Negative(t T, e any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#Negative) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#Negative](https://github.com/go-openapi/testify/blob/master/internal/assertions/compare.go#L162) +{{% /tab %}} +{{< /tabs >}} + +### Positive + +Positive asserts that the specified element is strictly positive. + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go + assertions.Positive(t, 1) + assertions.Positive(t, 1.23) +``` +{{< /tab >}} +{{% tab title="Examples" %}} +```go + success: 1 + failure: -1 +``` +{{< /tab >}} +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.Positive(t T, e any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Positive) | package-level function | +| [`assert.Positivef(t T, e any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Positivef) | formatted variant | +| [`assert.(*Assertions).Positive(e any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.Positive) | method variant | +| [`assert.(*Assertions).Positivef(e any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.Positivef) | method formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.Positive(t T, e any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Positive) | package-level function | +| [`require.Positivef(t T, e any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Positivef) | formatted variant | +| [`require.(*Assertions).Positive(e any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.Positive) | method variant | +| [`require.(*Assertions).Positivef(e any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.Positivef) | method formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.Positive(t T, e any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#Positive) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#Positive](https://github.com/go-openapi/testify/blob/master/internal/assertions/compare.go#L141) +{{% /tab %}} +{{< /tabs >}} + +--- + +--- + +Generated with github.com/go-openapi/testify/v2/codegen + +[godoc-badge]: https://pkg.go.dev/badge/github.com/go-openapi/testify/v2 +[godoc-url]: https://pkg.go.dev/github.com/go-openapi/testify/v2 + + diff --git a/docs/doc-site/api/condition.md b/docs/doc-site/api/condition.md new file mode 100644 index 000000000..7c8ef82b4 --- /dev/null +++ b/docs/doc-site/api/condition.md @@ -0,0 +1,252 @@ +--- +title: "Condition" +description: "Expressing Assertions Using Conditions" +modified: 2026-01-02 +weight: 4 +domains: + - "condition" +keywords: + - "Condition" + - "Conditionf" + - "Eventually" + - "Eventuallyf" + - "EventuallyWithT" + - "EventuallyWithTf" + - "Never" + - "Neverf" +--- + +Expressing Assertions Using Conditions + +## Assertions + +[![GoDoc][godoc-badge]][godoc-url] +{class="inline-badge"} + +_All links point to _ + +This domain exposes 4 functionalities. + +### Condition + +Condition uses a Comparison to assert a complex condition. + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go + assertions.Condition(t, func() bool { return myCondition }) +``` +{{< /tab >}} +{{% tab title="Examples" %}} +```go + success: func() bool { return true } + failure: func() bool { return false } +``` +{{< /tab >}} +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.Condition(t T, comp Comparison, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Condition) | package-level function | +| [`assert.Conditionf(t T, comp Comparison, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Conditionf) | formatted variant | +| [`assert.(*Assertions).Condition(comp Comparison) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.Condition) | method variant | +| [`assert.(*Assertions).Conditionf(comp Comparison, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.Conditionf) | method formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.Condition(t T, comp Comparison, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Condition) | package-level function | +| [`require.Conditionf(t T, comp Comparison, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Conditionf) | formatted variant | +| [`require.(*Assertions).Condition(comp Comparison) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.Condition) | method variant | +| [`require.(*Assertions).Conditionf(comp Comparison, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.Conditionf) | method formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.Condition(t T, comp Comparison, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#Condition) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#Condition](https://github.com/go-openapi/testify/blob/master/internal/assertions/condition.go#L22) +{{% /tab %}} +{{< /tabs >}} + +### Eventually + +Eventually asserts that given condition will be met in waitFor time, +periodically checking target function each tick. + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go + assertions.Eventually(t, func() bool { return true; }, time.Second, 10*time.Millisecond) +``` +{{< /tab >}} +{{% tab title="Examples" %}} +```go + success: func() bool { return true }, 100*time.Millisecond, 20*time.Millisecond + failure: func() bool { return false }, 100*time.Millisecond, 20*time.Millisecond +``` +{{< /tab >}} +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.Eventually(t T, condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Eventually) | package-level function | +| [`assert.Eventuallyf(t T, condition func() bool, waitFor time.Duration, tick time.Duration, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Eventuallyf) | formatted variant | +| [`assert.(*Assertions).Eventually(condition func() bool, waitFor time.Duration, tick time.Duration) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.Eventually) | method variant | +| [`assert.(*Assertions).Eventuallyf(condition func() bool, waitFor time.Duration, tick time.Duration, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.Eventuallyf) | method formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.Eventually(t T, condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Eventually) | package-level function | +| [`require.Eventuallyf(t T, condition func() bool, waitFor time.Duration, tick time.Duration, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Eventuallyf) | formatted variant | +| [`require.(*Assertions).Eventually(condition func() bool, waitFor time.Duration, tick time.Duration) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.Eventually) | method variant | +| [`require.(*Assertions).Eventuallyf(condition func() bool, waitFor time.Duration, tick time.Duration, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.Eventuallyf) | method formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.Eventually(t T, condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#Eventually) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#Eventually](https://github.com/go-openapi/testify/blob/master/internal/assertions/condition.go#L45) +{{% /tab %}} +{{< /tabs >}} + +### EventuallyWithT + +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. + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go + externalValue := false + go func() { + time.Sleep(8*time.Second) + externalValue = true + }() + assertions.EventuallyWithT(t, func(c *assertions.CollectT) { + // add assertions as needed; any assertion failure will fail the current tick + assertions.True(c, externalValue, "expected 'externalValue' to be true") + }, 10*time.Second, 1*time.Second, "external state has not changed to 'true'; still false") +``` +{{< /tab >}} +{{% tab title="Examples" %}} +```go + success: func(c *CollectT) { True(c,true) }, 100*time.Millisecond, 20*time.Millisecond + failure: func(c *CollectT) { False(c,true) }, 100*time.Millisecond, 20*time.Millisecond +``` +{{< /tab >}} +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.EventuallyWithT(t T, condition func(collect *CollectT), waitFor time.Duration, tick time.Duration, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#EventuallyWithT) | package-level function | +| [`assert.EventuallyWithTf(t T, condition func(collect *CollectT), waitFor time.Duration, tick time.Duration, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#EventuallyWithTf) | formatted variant | +| [`assert.(*Assertions).EventuallyWithT(condition func(collect *CollectT), waitFor time.Duration, tick time.Duration) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.EventuallyWithT) | method variant | +| [`assert.(*Assertions).EventuallyWithTf(condition func(collect *CollectT), waitFor time.Duration, tick time.Duration, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.EventuallyWithTf) | method formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.EventuallyWithT(t T, condition func(collect *CollectT), waitFor time.Duration, tick time.Duration, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#EventuallyWithT) | package-level function | +| [`require.EventuallyWithTf(t T, condition func(collect *CollectT), waitFor time.Duration, tick time.Duration, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#EventuallyWithTf) | formatted variant | +| [`require.(*Assertions).EventuallyWithT(condition func(collect *CollectT), waitFor time.Duration, tick time.Duration) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.EventuallyWithT) | method variant | +| [`require.(*Assertions).EventuallyWithTf(condition func(collect *CollectT), waitFor time.Duration, tick time.Duration, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.EventuallyWithTf) | method formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.EventuallyWithT(t T, condition func(collect *CollectT), waitFor time.Duration, tick time.Duration, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#EventuallyWithT) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#EventuallyWithT](https://github.com/go-openapi/testify/blob/master/internal/assertions/condition.go#L144) +{{% /tab %}} +{{< /tabs >}} + +### Never + +Never asserts that the given condition doesn't satisfy in waitFor time, +periodically checking the target function each tick. + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go + assertions.Never(t, func() bool { return false; }, time.Second, 10*time.Millisecond) +``` +{{< /tab >}} +{{% tab title="Examples" %}} +```go + success: func() bool { return false }, 100*time.Millisecond, 20*time.Millisecond + failure: func() bool { return true }, 100*time.Millisecond, 20*time.Millisecond +``` +{{< /tab >}} +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.Never(t T, condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Never) | package-level function | +| [`assert.Neverf(t T, condition func() bool, waitFor time.Duration, tick time.Duration, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Neverf) | formatted variant | +| [`assert.(*Assertions).Never(condition func() bool, waitFor time.Duration, tick time.Duration) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.Never) | method variant | +| [`assert.(*Assertions).Neverf(condition func() bool, waitFor time.Duration, tick time.Duration, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.Neverf) | method formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.Never(t T, condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Never) | package-level function | +| [`require.Neverf(t T, condition func() bool, waitFor time.Duration, tick time.Duration, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Neverf) | formatted variant | +| [`require.(*Assertions).Never(condition func() bool, waitFor time.Duration, tick time.Duration) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.Never) | method variant | +| [`require.(*Assertions).Neverf(condition func() bool, waitFor time.Duration, tick time.Duration, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.Neverf) | method formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.Never(t T, condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#Never) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#Never](https://github.com/go-openapi/testify/blob/master/internal/assertions/condition.go#L204) +{{% /tab %}} +{{< /tabs >}} + +--- + +--- + +Generated with github.com/go-openapi/testify/v2/codegen + +[godoc-badge]: https://pkg.go.dev/badge/github.com/go-openapi/testify/v2 +[godoc-url]: https://pkg.go.dev/github.com/go-openapi/testify/v2 + + diff --git a/docs/doc-site/api/equality.md b/docs/doc-site/api/equality.md new file mode 100644 index 000000000..0f76c0fea --- /dev/null +++ b/docs/doc-site/api/equality.md @@ -0,0 +1,660 @@ +--- +title: "Equality" +description: "Asserting Two Things Are Equal" +modified: 2026-01-02 +weight: 5 +domains: + - "equality" +keywords: + - "Empty" + - "Emptyf" + - "Equal" + - "Equalf" + - "EqualExportedValues" + - "EqualExportedValuesf" + - "EqualValues" + - "EqualValuesf" + - "Exactly" + - "Exactlyf" + - "Nil" + - "Nilf" + - "NotEmpty" + - "NotEmptyf" + - "NotEqual" + - "NotEqualf" + - "NotEqualValues" + - "NotEqualValuesf" + - "NotNil" + - "NotNilf" + - "NotSame" + - "NotSamef" + - "Same" + - "Samef" +--- + +Asserting Two Things Are Equal + +## Assertions + +[![GoDoc][godoc-badge]][godoc-url] +{class="inline-badge"} + +_All links point to _ + +This domain exposes 12 functionalities. + +### Empty + +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". + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go + assertions.Empty(t, obj) +``` +{{< /tab >}} +{{% tab title="Examples" %}} +```go + success: "" + failure: "not empty" +``` +{{< /tab >}} +{{< /tabs >}} +{{% /expand %}} + +[Zero values]: https://go.dev/ref/spec#The_zero_value + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.Empty(t T, object any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Empty) | package-level function | +| [`assert.Emptyf(t T, object any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Emptyf) | formatted variant | +| [`assert.(*Assertions).Empty(object any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.Empty) | method variant | +| [`assert.(*Assertions).Emptyf(object any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.Emptyf) | method formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.Empty(t T, object any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Empty) | package-level function | +| [`require.Emptyf(t T, object any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Emptyf) | formatted variant | +| [`require.(*Assertions).Empty(object any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.Empty) | method variant | +| [`require.(*Assertions).Emptyf(object any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.Emptyf) | method formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.Empty(t T, object any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#Empty) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#Empty](https://github.com/go-openapi/testify/blob/master/internal/assertions/equal.go#L276) +{{% /tab %}} +{{< /tabs >}} + +### Equal + +Equal asserts that two objects are equal. + +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. + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go + assertions.Equal(t, 123, 123) +``` +{{< /tab >}} +{{% tab title="Examples" %}} +```go + success: 123, 123 + failure: 123, 456 +``` +{{< /tab >}} +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.Equal(t T, expected any, actual any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Equal) | package-level function | +| [`assert.Equalf(t T, expected any, actual any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Equalf) | formatted variant | +| [`assert.(*Assertions).Equal(expected any, actual any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.Equal) | method variant | +| [`assert.(*Assertions).Equalf(expected any, actual any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.Equalf) | method formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.Equal(t T, expected any, actual any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Equal) | package-level function | +| [`require.Equalf(t T, expected any, actual any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Equalf) | formatted variant | +| [`require.(*Assertions).Equal(expected any, actual any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.Equal) | method variant | +| [`require.(*Assertions).Equalf(expected any, actual any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.Equalf) | method formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.Equal(t T, expected any, actual any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#Equal) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#Equal](https://github.com/go-openapi/testify/blob/master/internal/assertions/equal.go#L28) +{{% /tab %}} +{{< /tabs >}} + +### EqualExportedValues + +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. + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go + type S struct { + Exported int + notExported int + } + assertions.EqualExportedValues(t, S{1, 2}, S{1, 3}) => true + assertions.EqualExportedValues(t, S{1, 2}, S{2, 3}) => false +``` +{{< /tab >}} +{{% tab title="Examples" %}} +```go + success: &dummyStruct{A: "a", b: 1}, &dummyStruct{A: "a", b: 2} + failure: &dummyStruct{A: "a", b: 1}, &dummyStruct{A: "b", b: 1} +``` +{{< /tab >}} +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.EqualExportedValues(t T, expected any, actual any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#EqualExportedValues) | package-level function | +| [`assert.EqualExportedValuesf(t T, expected any, actual any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#EqualExportedValuesf) | formatted variant | +| [`assert.(*Assertions).EqualExportedValues(expected any, actual any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.EqualExportedValues) | method variant | +| [`assert.(*Assertions).EqualExportedValuesf(expected any, actual any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.EqualExportedValuesf) | method formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.EqualExportedValues(t T, expected any, actual any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#EqualExportedValues) | package-level function | +| [`require.EqualExportedValuesf(t T, expected any, actual any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#EqualExportedValuesf) | formatted variant | +| [`require.(*Assertions).EqualExportedValues(expected any, actual any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.EqualExportedValues) | method variant | +| [`require.(*Assertions).EqualExportedValuesf(expected any, actual any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.EqualExportedValuesf) | method formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.EqualExportedValues(t T, expected any, actual any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#EqualExportedValues) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#EqualExportedValues](https://github.com/go-openapi/testify/blob/master/internal/assertions/equal.go#L161) +{{% /tab %}} +{{< /tabs >}} + +### EqualValues + +EqualValues asserts that two objects are equal or convertible to the larger +type and equal. + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go + assertions.EqualValues(t, uint32(123), int32(123)) +``` +{{< /tab >}} +{{% tab title="Examples" %}} +```go + success: uint32(123), int32(123) + failure: uint32(123), int32(456) +``` +{{< /tab >}} +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.EqualValues(t T, expected any, actual any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#EqualValues) | package-level function | +| [`assert.EqualValuesf(t T, expected any, actual any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#EqualValuesf) | formatted variant | +| [`assert.(*Assertions).EqualValues(expected any, actual any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.EqualValues) | method variant | +| [`assert.(*Assertions).EqualValuesf(expected any, actual any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.EqualValuesf) | method formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.EqualValues(t T, expected any, actual any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#EqualValues) | package-level function | +| [`require.EqualValuesf(t T, expected any, actual any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#EqualValuesf) | formatted variant | +| [`require.(*Assertions).EqualValues(expected any, actual any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.EqualValues) | method variant | +| [`require.(*Assertions).EqualValuesf(expected any, actual any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.EqualValuesf) | method formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.EqualValues(t T, expected any, actual any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#EqualValues) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#EqualValues](https://github.com/go-openapi/testify/blob/master/internal/assertions/equal.go#L127) +{{% /tab %}} +{{< /tabs >}} + +### Exactly + +Exactly asserts that two objects are equal in value and type. + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go + assertions.Exactly(t, int32(123), int64(123)) +``` +{{< /tab >}} +{{% tab title="Examples" %}} +```go + success: int32(123), int32(123) + failure: int32(123), int64(123) +``` +{{< /tab >}} +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.Exactly(t T, expected any, actual any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Exactly) | package-level function | +| [`assert.Exactlyf(t T, expected any, actual any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Exactlyf) | formatted variant | +| [`assert.(*Assertions).Exactly(expected any, actual any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.Exactly) | method variant | +| [`assert.(*Assertions).Exactlyf(expected any, actual any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.Exactlyf) | method formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.Exactly(t T, expected any, actual any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Exactly) | package-level function | +| [`require.Exactlyf(t T, expected any, actual any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Exactlyf) | formatted variant | +| [`require.(*Assertions).Exactly(expected any, actual any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.Exactly) | method variant | +| [`require.(*Assertions).Exactlyf(expected any, actual any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.Exactlyf) | method formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.Exactly(t T, expected any, actual any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#Exactly) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#Exactly](https://github.com/go-openapi/testify/blob/master/internal/assertions/equal.go#L198) +{{% /tab %}} +{{< /tabs >}} + +### Nil + +Nil asserts that the specified object is nil. + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go + assertions.Nil(t, err) +``` +{{< /tab >}} +{{% tab title="Examples" %}} +```go + success: nil + failure: "not nil" +``` +{{< /tab >}} +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.Nil(t T, object any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Nil) | package-level function | +| [`assert.Nilf(t T, object any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Nilf) | formatted variant | +| [`assert.(*Assertions).Nil(object any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.Nil) | method variant | +| [`assert.(*Assertions).Nilf(object any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.Nilf) | method formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.Nil(t T, object any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Nil) | package-level function | +| [`require.Nilf(t T, object any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Nilf) | formatted variant | +| [`require.(*Assertions).Nil(object any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.Nil) | method variant | +| [`require.(*Assertions).Nilf(object any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.Nilf) | method formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.Nil(t T, object any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#Nil) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#Nil](https://github.com/go-openapi/testify/blob/master/internal/assertions/equal.go#L245) +{{% /tab %}} +{{< /tabs >}} + +### NotEmpty + +NotEmpty asserts that the specified object is NOT [Empty]. + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go + if assert.NotEmpty(t, obj) { + assertions.Equal(t, "two", obj[1]) + } +``` +{{< /tab >}} +{{% tab title="Examples" %}} +```go + success: "not empty" + failure: "" +``` +{{< /tab >}} +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.NotEmpty(t T, object any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#NotEmpty) | package-level function | +| [`assert.NotEmptyf(t T, object any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#NotEmptyf) | formatted variant | +| [`assert.(*Assertions).NotEmpty(object any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.NotEmpty) | method variant | +| [`assert.(*Assertions).NotEmptyf(object any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.NotEmptyf) | method formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.NotEmpty(t T, object any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#NotEmpty) | package-level function | +| [`require.NotEmptyf(t T, object any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#NotEmptyf) | formatted variant | +| [`require.(*Assertions).NotEmpty(object any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.NotEmpty) | method variant | +| [`require.(*Assertions).NotEmptyf(object any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.NotEmptyf) | method formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.NotEmpty(t T, object any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#NotEmpty) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#NotEmpty](https://github.com/go-openapi/testify/blob/master/internal/assertions/equal.go#L301) +{{% /tab %}} +{{< /tabs >}} + +### NotEqual + +NotEqual asserts that the specified values are NOT equal. + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go + assertions.NotEqual(t, obj1, obj2) +Pointer variable equality is determined based on the equality of the +referenced values (as opposed to the memory addresses). +``` +{{< /tab >}} +{{% tab title="Examples" %}} +```go + success: 123, 456 + failure: 123, 123 +``` +{{< /tab >}} +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.NotEqual(t T, expected any, actual any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#NotEqual) | package-level function | +| [`assert.NotEqualf(t T, expected any, actual any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#NotEqualf) | formatted variant | +| [`assert.(*Assertions).NotEqual(expected any, actual any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.NotEqual) | method variant | +| [`assert.(*Assertions).NotEqualf(expected any, actual any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.NotEqualf) | method formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.NotEqual(t T, expected any, actual any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#NotEqual) | package-level function | +| [`require.NotEqualf(t T, expected any, actual any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#NotEqualf) | formatted variant | +| [`require.(*Assertions).NotEqual(expected any, actual any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.NotEqual) | method variant | +| [`require.(*Assertions).NotEqualf(expected any, actual any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.NotEqualf) | method formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.NotEqual(t T, expected any, actual any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#NotEqual) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#NotEqual](https://github.com/go-openapi/testify/blob/master/internal/assertions/equal.go#L327) +{{% /tab %}} +{{< /tabs >}} + +### NotEqualValues + +NotEqualValues asserts that two objects are not equal even when converted to the same type. + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go + assertions.NotEqualValues(t, obj1, obj2) +``` +{{< /tab >}} +{{% tab title="Examples" %}} +```go + success: uint32(123), int32(456) + failure: uint32(123), int32(123) +``` +{{< /tab >}} +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.NotEqualValues(t T, expected any, actual any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#NotEqualValues) | package-level function | +| [`assert.NotEqualValuesf(t T, expected any, actual any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#NotEqualValuesf) | formatted variant | +| [`assert.(*Assertions).NotEqualValues(expected any, actual any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.NotEqualValues) | method variant | +| [`assert.(*Assertions).NotEqualValuesf(expected any, actual any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.NotEqualValuesf) | method formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.NotEqualValues(t T, expected any, actual any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#NotEqualValues) | package-level function | +| [`require.NotEqualValuesf(t T, expected any, actual any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#NotEqualValuesf) | formatted variant | +| [`require.(*Assertions).NotEqualValues(expected any, actual any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.NotEqualValues) | method variant | +| [`require.(*Assertions).NotEqualValuesf(expected any, actual any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.NotEqualValuesf) | method formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.NotEqualValues(t T, expected any, actual any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#NotEqualValues) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#NotEqualValues](https://github.com/go-openapi/testify/blob/master/internal/assertions/equal.go#L354) +{{% /tab %}} +{{< /tabs >}} + +### NotNil + +NotNil asserts that the specified object is not nil. + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go +assertions.NotNil(t, err) +``` +{{< /tab >}} +{{% tab title="Examples" %}} +```go + success: "not nil" + failure: nil +``` +{{< /tab >}} +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.NotNil(t T, object any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#NotNil) | package-level function | +| [`assert.NotNilf(t T, object any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#NotNilf) | formatted variant | +| [`assert.(*Assertions).NotNil(object any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.NotNil) | method variant | +| [`assert.(*Assertions).NotNilf(object any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.NotNilf) | method formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.NotNil(t T, object any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#NotNil) | package-level function | +| [`require.NotNilf(t T, object any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#NotNilf) | formatted variant | +| [`require.(*Assertions).NotNil(object any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.NotNil) | method variant | +| [`require.(*Assertions).NotNilf(object any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.NotNilf) | method formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.NotNil(t T, object any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#NotNil) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#NotNil](https://github.com/go-openapi/testify/blob/master/internal/assertions/equal.go#L224) +{{% /tab %}} +{{< /tabs >}} + +### NotSame + +NotSame asserts that two pointers do not reference the same object. + +Both arguments must be pointer variables. Pointer variable sameness is +determined based on the equality of both type and value. + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go + assertions.NotSame(t, ptr1, ptr2) +``` +{{< /tab >}} +{{% tab title="Examples" %}} +```go + success: &staticVar, ptr("static string") + failure: &staticVar, staticVarPtr +``` +{{< /tab >}} +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.NotSame(t T, expected any, actual any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#NotSame) | package-level function | +| [`assert.NotSamef(t T, expected any, actual any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#NotSamef) | formatted variant | +| [`assert.(*Assertions).NotSame(expected any, actual any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.NotSame) | method variant | +| [`assert.(*Assertions).NotSamef(expected any, actual any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.NotSamef) | method formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.NotSame(t T, expected any, actual any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#NotSame) | package-level function | +| [`require.NotSamef(t T, expected any, actual any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#NotSamef) | formatted variant | +| [`require.(*Assertions).NotSame(expected any, actual any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.NotSame) | method variant | +| [`require.(*Assertions).NotSamef(expected any, actual any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.NotSamef) | method formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.NotSame(t T, expected any, actual any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#NotSame) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#NotSame](https://github.com/go-openapi/testify/blob/master/internal/assertions/equal.go#L96) +{{% /tab %}} +{{< /tabs >}} + +### Same + +Same asserts that two pointers reference the same object. + +Both arguments must be pointer variables. Pointer variable sameness is +determined based on the equality of both type and value. + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go + assertions.Same(t, ptr1, ptr2) +``` +{{< /tab >}} +{{% tab title="Examples" %}} +```go + success: &staticVar, staticVarPtr + failure: &staticVar, ptr("static string") +``` +{{< /tab >}} +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.Same(t T, expected any, actual any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Same) | package-level function | +| [`assert.Samef(t T, expected any, actual any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Samef) | formatted variant | +| [`assert.(*Assertions).Same(expected any, actual any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.Same) | method variant | +| [`assert.(*Assertions).Samef(expected any, actual any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.Samef) | method formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.Same(t T, expected any, actual any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Same) | package-level function | +| [`require.Samef(t T, expected any, actual any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Samef) | formatted variant | +| [`require.(*Assertions).Same(expected any, actual any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.Same) | method variant | +| [`require.(*Assertions).Samef(expected any, actual any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.Samef) | method formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.Same(t T, expected any, actual any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#Same) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#Same](https://github.com/go-openapi/testify/blob/master/internal/assertions/equal.go#L62) +{{% /tab %}} +{{< /tabs >}} + +--- + +--- + +Generated with github.com/go-openapi/testify/v2/codegen + +[godoc-badge]: https://pkg.go.dev/badge/github.com/go-openapi/testify/v2 +[godoc-url]: https://pkg.go.dev/github.com/go-openapi/testify/v2 + + diff --git a/docs/doc-site/api/error.md b/docs/doc-site/api/error.md new file mode 100644 index 000000000..a9b8f8342 --- /dev/null +++ b/docs/doc-site/api/error.md @@ -0,0 +1,446 @@ +--- +title: "Error" +description: "Asserting Errors" +modified: 2026-01-02 +weight: 6 +domains: + - "error" +keywords: + - "EqualError" + - "EqualErrorf" + - "Error" + - "Errorf" + - "ErrorAs" + - "ErrorAsf" + - "ErrorContains" + - "ErrorContainsf" + - "ErrorIs" + - "ErrorIsf" + - "NoError" + - "NoErrorf" + - "NotErrorAs" + - "NotErrorAsf" + - "NotErrorIs" + - "NotErrorIsf" +--- + +Asserting Errors + +## Assertions + +[![GoDoc][godoc-badge]][godoc-url] +{class="inline-badge"} + +_All links point to _ + +This domain exposes 8 functionalities. + +### EqualError + +EqualError asserts that a function returned a non-nil error (i.e. an error) +and that it is equal to the provided error. + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go + actualObj, err := SomeFunction() + assertions.EqualError(t, err, expectedErrorString) +``` +{{< /tab >}} +{{% tab title="Examples" %}} +```go + success: ErrTest, "assert.ErrTest general error for testing" + failure: ErrTest, "wrong error message" +``` +{{< /tab >}} +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.EqualError(t T, theError error, errString string, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#EqualError) | package-level function | +| [`assert.EqualErrorf(t T, theError error, errString string, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#EqualErrorf) | formatted variant | +| [`assert.(*Assertions).EqualError(theError error, errString string) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.EqualError) | method variant | +| [`assert.(*Assertions).EqualErrorf(theError error, errString string, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.EqualErrorf) | method formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.EqualError(t T, theError error, errString string, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#EqualError) | package-level function | +| [`require.EqualErrorf(t T, theError error, errString string, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#EqualErrorf) | formatted variant | +| [`require.(*Assertions).EqualError(theError error, errString string) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.EqualError) | method variant | +| [`require.(*Assertions).EqualErrorf(theError error, errString string, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.EqualErrorf) | method formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.EqualError(t T, theError error, errString string, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#EqualError) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#EqualError](https://github.com/go-openapi/testify/blob/master/internal/assertions/error.go#L80) +{{% /tab %}} +{{< /tabs >}} + +### Error + +Error asserts that a function returned a non-nil error (ie. an error). + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go + actualObj, err := SomeFunction() + assertions.Error(t, err) +``` +{{< /tab >}} +{{% tab title="Examples" %}} +```go + success: ErrTest + failure: nil +``` +{{< /tab >}} +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.Error(t T, err error, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Error) | package-level function | +| [`assert.Errorf(t T, err error, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Errorf) | formatted variant | +| [`assert.(*Assertions).Error(err error) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.Error) | method variant | +| [`assert.(*Assertions).Errorf(err error, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.Errorf) | method formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.Error(t T, err error, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Error) | package-level function | +| [`require.Errorf(t T, err error, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Errorf) | formatted variant | +| [`require.(*Assertions).Error(err error) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.Error) | method variant | +| [`require.(*Assertions).Errorf(err error, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.Errorf) | method formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.Error(t T, err error, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#Error) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#Error](https://github.com/go-openapi/testify/blob/master/internal/assertions/error.go#L56) +{{% /tab %}} +{{< /tabs >}} + +### ErrorAs + +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](https://pkg.go.dev/errors#As). + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go + assertions.ErrorAs(t, err, &target) +``` +{{< /tab >}} +{{% tab title="Examples" %}} +```go + success: fmt.Errorf("wrap: %w", &dummyError{}), new(*dummyError) + failure: ErrTest, new(*dummyError) +``` +{{< /tab >}} +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.ErrorAs(t T, err error, target any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#ErrorAs) | package-level function | +| [`assert.ErrorAsf(t T, err error, target any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#ErrorAsf) | formatted variant | +| [`assert.(*Assertions).ErrorAs(err error, target any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.ErrorAs) | method variant | +| [`assert.(*Assertions).ErrorAsf(err error, target any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.ErrorAsf) | method formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.ErrorAs(t T, err error, target any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#ErrorAs) | package-level function | +| [`require.ErrorAsf(t T, err error, target any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#ErrorAsf) | formatted variant | +| [`require.(*Assertions).ErrorAs(err error, target any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.ErrorAs) | method variant | +| [`require.(*Assertions).ErrorAsf(err error, target any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.ErrorAsf) | method formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.ErrorAs(t T, err error, target any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#ErrorAs) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#ErrorAs](https://github.com/go-openapi/testify/blob/master/internal/assertions/error.go#L211) +{{% /tab %}} +{{< /tabs >}} + +### ErrorContains + +ErrorContains asserts that a function returned a non-nil error (i.e. an +error) and that the error contains the specified substring. + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go + actualObj, err := SomeFunction() + assertions.ErrorContains(t, err, expectedErrorSubString) +``` +{{< /tab >}} +{{% tab title="Examples" %}} +```go + success: ErrTest, "general error" + failure: ErrTest, "not in message" +``` +{{< /tab >}} +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.ErrorContains(t T, theError error, contains string, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#ErrorContains) | package-level function | +| [`assert.ErrorContainsf(t T, theError error, contains string, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#ErrorContainsf) | formatted variant | +| [`assert.(*Assertions).ErrorContains(theError error, contains string) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.ErrorContains) | method variant | +| [`assert.(*Assertions).ErrorContainsf(theError error, contains string, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.ErrorContainsf) | method formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.ErrorContains(t T, theError error, contains string, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#ErrorContains) | package-level function | +| [`require.ErrorContainsf(t T, theError error, contains string, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#ErrorContainsf) | formatted variant | +| [`require.(*Assertions).ErrorContains(theError error, contains string) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.ErrorContains) | method variant | +| [`require.(*Assertions).ErrorContainsf(theError error, contains string, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.ErrorContainsf) | method formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.ErrorContains(t T, theError error, contains string, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#ErrorContains) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#ErrorContains](https://github.com/go-openapi/testify/blob/master/internal/assertions/error.go#L111) +{{% /tab %}} +{{< /tabs >}} + +### ErrorIs + +ErrorIs asserts that at least one of the errors in err's chain matches target. + +This is a wrapper for [errors.Is](https://pkg.go.dev/errors#Is). + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go + assertions.ErrorIs(t, err, io.EOF) +``` +{{< /tab >}} +{{% tab title="Examples" %}} +```go + success: fmt.Errorf("wrap: %w", io.EOF), io.EOF + failure: ErrTest, io.EOF +``` +{{< /tab >}} +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.ErrorIs(t T, err error, target error, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#ErrorIs) | package-level function | +| [`assert.ErrorIsf(t T, err error, target error, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#ErrorIsf) | formatted variant | +| [`assert.(*Assertions).ErrorIs(err error, target error) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.ErrorIs) | method variant | +| [`assert.(*Assertions).ErrorIsf(err error, target error, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.ErrorIsf) | method formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.ErrorIs(t T, err error, target error, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#ErrorIs) | package-level function | +| [`require.ErrorIsf(t T, err error, target error, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#ErrorIsf) | formatted variant | +| [`require.(*Assertions).ErrorIs(err error, target error) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.ErrorIs) | method variant | +| [`require.(*Assertions).ErrorIsf(err error, target error, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.ErrorIsf) | method formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.ErrorIs(t T, err error, target error, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#ErrorIs) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#ErrorIs](https://github.com/go-openapi/testify/blob/master/internal/assertions/error.go#L140) +{{% /tab %}} +{{< /tabs >}} + +### NoError + +NoError asserts that a function returned a nil error (ie. no error). + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go + actualObj, err := SomeFunction() + if assert.NoError(t, err) { + assertions.Equal(t, expectedObj, actualObj) + } +``` +{{< /tab >}} +{{% tab title="Examples" %}} +```go + success: nil + failure: ErrTest +``` +{{< /tab >}} +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.NoError(t T, err error, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#NoError) | package-level function | +| [`assert.NoErrorf(t T, err error, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#NoErrorf) | formatted variant | +| [`assert.(*Assertions).NoError(err error) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.NoError) | method variant | +| [`assert.(*Assertions).NoErrorf(err error, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.NoErrorf) | method formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.NoError(t T, err error, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#NoError) | package-level function | +| [`require.NoErrorf(t T, err error, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#NoErrorf) | formatted variant | +| [`require.(*Assertions).NoError(err error) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.NoError) | method variant | +| [`require.(*Assertions).NoErrorf(err error, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.NoErrorf) | method formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.NoError(t T, err error, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#NoError) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#NoError](https://github.com/go-openapi/testify/blob/master/internal/assertions/error.go#L33) +{{% /tab %}} +{{< /tabs >}} + +### NotErrorAs + +NotErrorAs asserts that none of the errors in err's chain matches target, +but if so, sets target to that error value. + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go + assertions.NotErrorAs(t, err, &target) +``` +{{< /tab >}} +{{% tab title="Examples" %}} +```go + success: ErrTest, new(*dummyError) + failure: fmt.Errorf("wrap: %w", &dummyError{}), new(*dummyError) +``` +{{< /tab >}} +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.NotErrorAs(t T, err error, target any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#NotErrorAs) | package-level function | +| [`assert.NotErrorAsf(t T, err error, target any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#NotErrorAsf) | formatted variant | +| [`assert.(*Assertions).NotErrorAs(err error, target any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.NotErrorAs) | method variant | +| [`assert.(*Assertions).NotErrorAsf(err error, target any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.NotErrorAsf) | method formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.NotErrorAs(t T, err error, target any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#NotErrorAs) | package-level function | +| [`require.NotErrorAsf(t T, err error, target any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#NotErrorAsf) | formatted variant | +| [`require.(*Assertions).NotErrorAs(err error, target any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.NotErrorAs) | method variant | +| [`require.(*Assertions).NotErrorAsf(err error, target any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.NotErrorAsf) | method formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.NotErrorAs(t T, err error, target any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#NotErrorAs) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#NotErrorAs](https://github.com/go-openapi/testify/blob/master/internal/assertions/error.go#L245) +{{% /tab %}} +{{< /tabs >}} + +### NotErrorIs + +NotErrorIs asserts that none of the errors in err's chain matches target. + +This is a wrapper for [errors.Is](https://pkg.go.dev/errors#Is). + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go + assertions.NotErrorIs(t, err, io.EOF) +``` +{{< /tab >}} +{{% tab title="Examples" %}} +```go + success: ErrTest, io.EOF + failure: fmt.Errorf("wrap: %w", io.EOF), io.EOF +``` +{{< /tab >}} +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.NotErrorIs(t T, err error, target error, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#NotErrorIs) | package-level function | +| [`assert.NotErrorIsf(t T, err error, target error, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#NotErrorIsf) | formatted variant | +| [`assert.(*Assertions).NotErrorIs(err error, target error) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.NotErrorIs) | method variant | +| [`assert.(*Assertions).NotErrorIsf(err error, target error, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.NotErrorIsf) | method formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.NotErrorIs(t T, err error, target error, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#NotErrorIs) | package-level function | +| [`require.NotErrorIsf(t T, err error, target error, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#NotErrorIsf) | formatted variant | +| [`require.(*Assertions).NotErrorIs(err error, target error) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.NotErrorIs) | method variant | +| [`require.(*Assertions).NotErrorIsf(err error, target error, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.NotErrorIsf) | method formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.NotErrorIs(t T, err error, target error, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#NotErrorIs) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#NotErrorIs](https://github.com/go-openapi/testify/blob/master/internal/assertions/error.go#L177) +{{% /tab %}} +{{< /tabs >}} + +--- + +--- + +Generated with github.com/go-openapi/testify/v2/codegen + +[godoc-badge]: https://pkg.go.dev/badge/github.com/go-openapi/testify/v2 +[godoc-url]: https://pkg.go.dev/github.com/go-openapi/testify/v2 + + diff --git a/docs/doc-site/api/file.md b/docs/doc-site/api/file.md new file mode 100644 index 000000000..4e311c9e9 --- /dev/null +++ b/docs/doc-site/api/file.md @@ -0,0 +1,339 @@ +--- +title: "File" +description: "Asserting OS Files" +modified: 2026-01-02 +weight: 7 +domains: + - "file" +keywords: + - "DirExists" + - "DirExistsf" + - "FileEmpty" + - "FileEmptyf" + - "FileExists" + - "FileExistsf" + - "FileNotEmpty" + - "FileNotEmptyf" + - "NoDirExists" + - "NoDirExistsf" + - "NoFileExists" + - "NoFileExistsf" +--- + +Asserting OS Files + +## Assertions + +[![GoDoc][godoc-badge]][godoc-url] +{class="inline-badge"} + +_All links point to _ + +This domain exposes 6 functionalities. + +### DirExists + +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. + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go + assertions.DirExists(t, "path/to/directory") +``` +{{< /tab >}} +{{% tab title="Examples" %}} +```go + success: filepath.Join(testDataPath(),"existing_dir") + failure: filepath.Join(testDataPath(),"non_existing_dir") +``` +{{< /tab >}} +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.DirExists(t T, path string, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#DirExists) | package-level function | +| [`assert.DirExistsf(t T, path string, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#DirExistsf) | formatted variant | +| [`assert.(*Assertions).DirExists(path string) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.DirExists) | method variant | +| [`assert.(*Assertions).DirExistsf(path string, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.DirExistsf) | method formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.DirExists(t T, path string, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#DirExists) | package-level function | +| [`require.DirExistsf(t T, path string, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#DirExistsf) | formatted variant | +| [`require.(*Assertions).DirExists(path string) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.DirExists) | method variant | +| [`require.(*Assertions).DirExistsf(path string, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.DirExistsf) | method formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.DirExists(t T, path string, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#DirExists) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#DirExists](https://github.com/go-openapi/testify/blob/master/internal/assertions/file.go#L78) +{{% /tab %}} +{{< /tabs >}} + +### FileEmpty + +FileEmpty checks whether a file exists in the given path and is empty. +It fails if the file is not empty, if the path points to a directory or there is an error when trying to check the file. + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go + assertions.FileEmpty(t, "path/to/file") +``` +{{< /tab >}} +{{% tab title="Examples" %}} +```go + success: filepath.Join(testDataPath(),"empty_file") + failure: filepath.Join(testDataPath(),"existing_file") +``` +{{< /tab >}} +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.FileEmpty(t T, path string, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#FileEmpty) | package-level function | +| [`assert.FileEmptyf(t T, path string, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#FileEmptyf) | formatted variant | +| [`assert.(*Assertions).FileEmpty(path string) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.FileEmpty) | method variant | +| [`assert.(*Assertions).FileEmptyf(path string, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.FileEmptyf) | method formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.FileEmpty(t T, path string, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#FileEmpty) | package-level function | +| [`require.FileEmptyf(t T, path string, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#FileEmptyf) | formatted variant | +| [`require.(*Assertions).FileEmpty(path string) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.FileEmpty) | method variant | +| [`require.(*Assertions).FileEmptyf(path string, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.FileEmptyf) | method formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.FileEmpty(t T, path string, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#FileEmpty) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#FileEmpty](https://github.com/go-openapi/testify/blob/master/internal/assertions/file.go#L136) +{{% /tab %}} +{{< /tabs >}} + +### FileExists + +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. + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go + assertions.FileExists(t, "path/to/file") +``` +{{< /tab >}} +{{% tab title="Examples" %}} +```go + success: filepath.Join(testDataPath(),"existing_file") + failure: filepath.Join(testDataPath(),"non_existing_file") +``` +{{< /tab >}} +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.FileExists(t T, path string, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#FileExists) | package-level function | +| [`assert.FileExistsf(t T, path string, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#FileExistsf) | formatted variant | +| [`assert.(*Assertions).FileExists(path string) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.FileExists) | method variant | +| [`assert.(*Assertions).FileExistsf(path string, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.FileExistsf) | method formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.FileExists(t T, path string, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#FileExists) | package-level function | +| [`require.FileExistsf(t T, path string, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#FileExistsf) | formatted variant | +| [`require.(*Assertions).FileExists(path string) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.FileExists) | method variant | +| [`require.(*Assertions).FileExistsf(path string, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.FileExistsf) | method formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.FileExists(t T, path string, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#FileExists) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#FileExists](https://github.com/go-openapi/testify/blob/master/internal/assertions/file.go#L23) +{{% /tab %}} +{{< /tabs >}} + +### FileNotEmpty + +FileNotEmpty checks whether a file exists in the given path and is not empty. +It fails if the file is empty, if the path points to a directory or there is an error when trying to check the file. + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go + assertions.FileNotEmpty(t, "path/to/file") +``` +{{< /tab >}} +{{% tab title="Examples" %}} +```go + success: filepath.Join(testDataPath(),"existing_file") + failure: filepath.Join(testDataPath(),"empty_file") +``` +{{< /tab >}} +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.FileNotEmpty(t T, path string, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#FileNotEmpty) | package-level function | +| [`assert.FileNotEmptyf(t T, path string, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#FileNotEmptyf) | formatted variant | +| [`assert.(*Assertions).FileNotEmpty(path string) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.FileNotEmpty) | method variant | +| [`assert.(*Assertions).FileNotEmptyf(path string, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.FileNotEmptyf) | method formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.FileNotEmpty(t T, path string, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#FileNotEmpty) | package-level function | +| [`require.FileNotEmptyf(t T, path string, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#FileNotEmptyf) | formatted variant | +| [`require.(*Assertions).FileNotEmpty(path string) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.FileNotEmpty) | method variant | +| [`require.(*Assertions).FileNotEmptyf(path string, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.FileNotEmptyf) | method formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.FileNotEmpty(t T, path string, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#FileNotEmpty) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#FileNotEmpty](https://github.com/go-openapi/testify/blob/master/internal/assertions/file.go#L177) +{{% /tab %}} +{{< /tabs >}} + +### NoDirExists + +NoDirExists checks whether a directory does not exist in the given path. +It fails if the path points to an existing _directory_ only. + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go + assertions.NoDirExists(t, "path/to/directory") +``` +{{< /tab >}} +{{% tab title="Examples" %}} +```go + success: filepath.Join(testDataPath(),"non_existing_dir") + failure: filepath.Join(testDataPath(),"existing_dir") +``` +{{< /tab >}} +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.NoDirExists(t T, path string, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#NoDirExists) | package-level function | +| [`assert.NoDirExistsf(t T, path string, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#NoDirExistsf) | formatted variant | +| [`assert.(*Assertions).NoDirExists(path string) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.NoDirExists) | method variant | +| [`assert.(*Assertions).NoDirExistsf(path string, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.NoDirExistsf) | method formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.NoDirExists(t T, path string, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#NoDirExists) | package-level function | +| [`require.NoDirExistsf(t T, path string, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#NoDirExistsf) | formatted variant | +| [`require.(*Assertions).NoDirExists(path string) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.NoDirExists) | method variant | +| [`require.(*Assertions).NoDirExistsf(path string, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.NoDirExistsf) | method formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.NoDirExists(t T, path string, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#NoDirExists) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#NoDirExists](https://github.com/go-openapi/testify/blob/master/internal/assertions/file.go#L107) +{{% /tab %}} +{{< /tabs >}} + +### NoFileExists + +NoFileExists checks whether a file does not exist in a given path. It fails +if the path points to an existing _file_ only. + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go + assertions.NoFileExists(t, "path/to/file") +``` +{{< /tab >}} +{{% tab title="Examples" %}} +```go + success: filepath.Join(testDataPath(),"non_existing_file") + failure: filepath.Join(testDataPath(),"existing_file") +``` +{{< /tab >}} +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.NoFileExists(t T, path string, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#NoFileExists) | package-level function | +| [`assert.NoFileExistsf(t T, path string, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#NoFileExistsf) | formatted variant | +| [`assert.(*Assertions).NoFileExists(path string) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.NoFileExists) | method variant | +| [`assert.(*Assertions).NoFileExistsf(path string, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.NoFileExistsf) | method formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.NoFileExists(t T, path string, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#NoFileExists) | package-level function | +| [`require.NoFileExistsf(t T, path string, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#NoFileExistsf) | formatted variant | +| [`require.(*Assertions).NoFileExists(path string) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.NoFileExists) | method variant | +| [`require.(*Assertions).NoFileExistsf(path string, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.NoFileExistsf) | method formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.NoFileExists(t T, path string, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#NoFileExists) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#NoFileExists](https://github.com/go-openapi/testify/blob/master/internal/assertions/file.go#L52) +{{% /tab %}} +{{< /tabs >}} + +--- + +--- + +Generated with github.com/go-openapi/testify/v2/codegen + +[godoc-badge]: https://pkg.go.dev/badge/github.com/go-openapi/testify/v2 +[godoc-url]: https://pkg.go.dev/github.com/go-openapi/testify/v2 + + diff --git a/docs/doc-site/api/http.md b/docs/doc-site/api/http.md new file mode 100644 index 000000000..2a7605cbe --- /dev/null +++ b/docs/doc-site/api/http.md @@ -0,0 +1,377 @@ +--- +title: "Http" +description: "Asserting HTTP Response And Body" +modified: 2026-01-02 +weight: 8 +domains: + - "http" +keywords: + - "HTTPBody" + - "HTTPBodyf" + - "HTTPBodyContains" + - "HTTPBodyContainsf" + - "HTTPBodyNotContains" + - "HTTPBodyNotContainsf" + - "HTTPError" + - "HTTPErrorf" + - "HTTPRedirect" + - "HTTPRedirectf" + - "HTTPStatusCode" + - "HTTPStatusCodef" + - "HTTPSuccess" + - "HTTPSuccessf" +--- + +Asserting HTTP Response And Body + +## Assertions + +[![GoDoc][godoc-badge]][godoc-url] +{class="inline-badge"} + +_All links point to _ + +This domain exposes 7 functionalities. + +### HTTPBodyContains + +HTTPBodyContains asserts that a specified handler returns a body that contains a string. + +Returns whether the assertion was successful (true) or not (false). + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go + assertions.HTTPBodyContains(t, myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky") +``` +{{< /tab >}} +{{% tab title="Examples" %}} +```go + success: httpBody, "GET", "/", url.Values{"name": []string{"World"}}, "Hello, World!" + failure: httpBody, "GET", "/", url.Values{"name": []string{"Bob"}}, "Hello, World!" +``` +{{< /tab >}} +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.HTTPBodyContains(t T, handler http.HandlerFunc, method string, url string, values url.Values, str any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#HTTPBodyContains) | package-level function | +| [`assert.HTTPBodyContainsf(t T, handler http.HandlerFunc, method string, url string, values url.Values, str any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#HTTPBodyContainsf) | formatted variant | +| [`assert.(*Assertions).HTTPBodyContains(handler http.HandlerFunc, method string, url string, values url.Values, str any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.HTTPBodyContains) | method variant | +| [`assert.(*Assertions).HTTPBodyContainsf(handler http.HandlerFunc, method string, url string, values url.Values, str any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.HTTPBodyContainsf) | method formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.HTTPBodyContains(t T, handler http.HandlerFunc, method string, url string, values url.Values, str any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#HTTPBodyContains) | package-level function | +| [`require.HTTPBodyContainsf(t T, handler http.HandlerFunc, method string, url string, values url.Values, str any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#HTTPBodyContainsf) | formatted variant | +| [`require.(*Assertions).HTTPBodyContains(handler http.HandlerFunc, method string, url string, values url.Values, str any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.HTTPBodyContains) | method variant | +| [`require.(*Assertions).HTTPBodyContainsf(handler http.HandlerFunc, method string, url string, values url.Values, str any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.HTTPBodyContainsf) | method formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.HTTPBodyContains(t T, handler http.HandlerFunc, method string, url string, values url.Values, str any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#HTTPBodyContains) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#HTTPBodyContains](https://github.com/go-openapi/testify/blob/master/internal/assertions/http.go#L163) +{{% /tab %}} +{{< /tabs >}} + +### HTTPBodyNotContains + +HTTPBodyNotContains asserts that a specified handler returns a +body that does not contain a string. + +Returns whether the assertion was successful (true) or not (false). + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go + assertions.HTTPBodyNotContains(t, myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky") +``` +{{< /tab >}} +{{% tab title="Examples" %}} +```go + success: httpBody, "GET", "/", url.Values{"name": []string{"World"}}, "Hello, Bob!" + failure: httpBody, "GET", "/", url.Values{"name": []string{"Bob"}}, "Hello, Bob!" +``` +{{< /tab >}} +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.HTTPBodyNotContains(t T, handler http.HandlerFunc, method string, url string, values url.Values, str any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#HTTPBodyNotContains) | package-level function | +| [`assert.HTTPBodyNotContainsf(t T, handler http.HandlerFunc, method string, url string, values url.Values, str any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#HTTPBodyNotContainsf) | formatted variant | +| [`assert.(*Assertions).HTTPBodyNotContains(handler http.HandlerFunc, method string, url string, values url.Values, str any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.HTTPBodyNotContains) | method variant | +| [`assert.(*Assertions).HTTPBodyNotContainsf(handler http.HandlerFunc, method string, url string, values url.Values, str any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.HTTPBodyNotContainsf) | method formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.HTTPBodyNotContains(t T, handler http.HandlerFunc, method string, url string, values url.Values, str any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#HTTPBodyNotContains) | package-level function | +| [`require.HTTPBodyNotContainsf(t T, handler http.HandlerFunc, method string, url string, values url.Values, str any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#HTTPBodyNotContainsf) | formatted variant | +| [`require.(*Assertions).HTTPBodyNotContains(handler http.HandlerFunc, method string, url string, values url.Values, str any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.HTTPBodyNotContains) | method variant | +| [`require.(*Assertions).HTTPBodyNotContainsf(handler http.HandlerFunc, method string, url string, values url.Values, str any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.HTTPBodyNotContainsf) | method formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.HTTPBodyNotContains(t T, handler http.HandlerFunc, method string, url string, values url.Values, str any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#HTTPBodyNotContains) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#HTTPBodyNotContains](https://github.com/go-openapi/testify/blob/master/internal/assertions/http.go#L191) +{{% /tab %}} +{{< /tabs >}} + +### HTTPError + +HTTPError asserts that a specified handler returns an error status code. + +Returns whether the assertion was successful (true) or not (false). + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go + assertions.HTTPError(t, myHandler, "POST", "/a/b/c", url.Values{"a": []string{"b", "c"}} +``` +{{< /tab >}} +{{% tab title="Examples" %}} +```go + success: httpError, "GET", "/", nil + failure: httpOK, "GET", "/", nil +``` +{{< /tab >}} +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.HTTPError(t T, handler http.HandlerFunc, method string, url string, values url.Values, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#HTTPError) | package-level function | +| [`assert.HTTPErrorf(t T, handler http.HandlerFunc, method string, url string, values url.Values, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#HTTPErrorf) | formatted variant | +| [`assert.(*Assertions).HTTPError(handler http.HandlerFunc, method string, url string, values url.Values) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.HTTPError) | method variant | +| [`assert.(*Assertions).HTTPErrorf(handler http.HandlerFunc, method string, url string, values url.Values, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.HTTPErrorf) | method formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.HTTPError(t T, handler http.HandlerFunc, method string, url string, values url.Values, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#HTTPError) | package-level function | +| [`require.HTTPErrorf(t T, handler http.HandlerFunc, method string, url string, values url.Values, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#HTTPErrorf) | formatted variant | +| [`require.(*Assertions).HTTPError(handler http.HandlerFunc, method string, url string, values url.Values) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.HTTPError) | method variant | +| [`require.(*Assertions).HTTPErrorf(handler http.HandlerFunc, method string, url string, values url.Values, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.HTTPErrorf) | method formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.HTTPError(t T, handler http.HandlerFunc, method string, url string, values url.Values, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#HTTPError) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#HTTPError](https://github.com/go-openapi/testify/blob/master/internal/assertions/http.go#L87) +{{% /tab %}} +{{< /tabs >}} + +### HTTPRedirect + +HTTPRedirect asserts that a specified handler returns a redirect status code. + +Returns whether the assertion was successful (true) or not (false). + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go + assertions.HTTPRedirect(t, myHandler, "GET", "/a/b/c", url.Values{"a": []string{"b", "c"}} +``` +{{< /tab >}} +{{% tab title="Examples" %}} +```go + success: httpRedirect, "GET", "/", nil + failure: httpError, "GET", "/", nil +``` +{{< /tab >}} +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.HTTPRedirect(t T, handler http.HandlerFunc, method string, url string, values url.Values, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#HTTPRedirect) | package-level function | +| [`assert.HTTPRedirectf(t T, handler http.HandlerFunc, method string, url string, values url.Values, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#HTTPRedirectf) | formatted variant | +| [`assert.(*Assertions).HTTPRedirect(handler http.HandlerFunc, method string, url string, values url.Values) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.HTTPRedirect) | method variant | +| [`assert.(*Assertions).HTTPRedirectf(handler http.HandlerFunc, method string, url string, values url.Values, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.HTTPRedirectf) | method formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.HTTPRedirect(t T, handler http.HandlerFunc, method string, url string, values url.Values, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#HTTPRedirect) | package-level function | +| [`require.HTTPRedirectf(t T, handler http.HandlerFunc, method string, url string, values url.Values, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#HTTPRedirectf) | formatted variant | +| [`require.(*Assertions).HTTPRedirect(handler http.HandlerFunc, method string, url string, values url.Values) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.HTTPRedirect) | method variant | +| [`require.(*Assertions).HTTPRedirectf(handler http.HandlerFunc, method string, url string, values url.Values, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.HTTPRedirectf) | method formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.HTTPRedirect(t T, handler http.HandlerFunc, method string, url string, values url.Values, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#HTTPRedirect) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#HTTPRedirect](https://github.com/go-openapi/testify/blob/master/internal/assertions/http.go#L57) +{{% /tab %}} +{{< /tabs >}} + +### HTTPStatusCode + +HTTPStatusCode asserts that a specified handler returns a specified status code. + +Returns whether the assertion was successful (true) or not (false). + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go + assertions.HTTPStatusCode(t, myHandler, "GET", "/notImplemented", nil, 501) +``` +{{< /tab >}} +{{% tab title="Examples" %}} +```go + success: httpOK, "GET", "/", nil, http.StatusOK + failure: httpError, "GET", "/", nil, http.StatusOK +``` +{{< /tab >}} +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.HTTPStatusCode(t T, handler http.HandlerFunc, method string, url string, values url.Values, statuscode int, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#HTTPStatusCode) | package-level function | +| [`assert.HTTPStatusCodef(t T, handler http.HandlerFunc, method string, url string, values url.Values, statuscode int, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#HTTPStatusCodef) | formatted variant | +| [`assert.(*Assertions).HTTPStatusCode(handler http.HandlerFunc, method string, url string, values url.Values, statuscode int) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.HTTPStatusCode) | method variant | +| [`assert.(*Assertions).HTTPStatusCodef(handler http.HandlerFunc, method string, url string, values url.Values, statuscode int, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.HTTPStatusCodef) | method formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.HTTPStatusCode(t T, handler http.HandlerFunc, method string, url string, values url.Values, statuscode int, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#HTTPStatusCode) | package-level function | +| [`require.HTTPStatusCodef(t T, handler http.HandlerFunc, method string, url string, values url.Values, statuscode int, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#HTTPStatusCodef) | formatted variant | +| [`require.(*Assertions).HTTPStatusCode(handler http.HandlerFunc, method string, url string, values url.Values, statuscode int) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.HTTPStatusCode) | method variant | +| [`require.(*Assertions).HTTPStatusCodef(handler http.HandlerFunc, method string, url string, values url.Values, statuscode int, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.HTTPStatusCodef) | method formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.HTTPStatusCode(t T, handler http.HandlerFunc, method string, url string, values url.Values, statuscode int, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#HTTPStatusCode) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#HTTPStatusCode](https://github.com/go-openapi/testify/blob/master/internal/assertions/http.go#L117) +{{% /tab %}} +{{< /tabs >}} + +### HTTPSuccess + +HTTPSuccess asserts that a specified handler returns a success status code. + +Returns whether the assertion was successful (true) or not (false). + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go + assertions.HTTPSuccess(t, myHandler, "POST", "http://www.google.com", nil) +``` +{{< /tab >}} +{{% tab title="Examples" %}} +```go + success: httpOK, "GET", "/", nil + failure: httpError, "GET", "/", nil +``` +{{< /tab >}} +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.HTTPSuccess(t T, handler http.HandlerFunc, method string, url string, values url.Values, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#HTTPSuccess) | package-level function | +| [`assert.HTTPSuccessf(t T, handler http.HandlerFunc, method string, url string, values url.Values, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#HTTPSuccessf) | formatted variant | +| [`assert.(*Assertions).HTTPSuccess(handler http.HandlerFunc, method string, url string, values url.Values) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.HTTPSuccess) | method variant | +| [`assert.(*Assertions).HTTPSuccessf(handler http.HandlerFunc, method string, url string, values url.Values, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.HTTPSuccessf) | method formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.HTTPSuccess(t T, handler http.HandlerFunc, method string, url string, values url.Values, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#HTTPSuccess) | package-level function | +| [`require.HTTPSuccessf(t T, handler http.HandlerFunc, method string, url string, values url.Values, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#HTTPSuccessf) | formatted variant | +| [`require.(*Assertions).HTTPSuccess(handler http.HandlerFunc, method string, url string, values url.Values) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.HTTPSuccess) | method variant | +| [`require.(*Assertions).HTTPSuccessf(handler http.HandlerFunc, method string, url string, values url.Values, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.HTTPSuccessf) | method formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.HTTPSuccess(t T, handler http.HandlerFunc, method string, url string, values url.Values, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#HTTPSuccess) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#HTTPSuccess](https://github.com/go-openapi/testify/blob/master/internal/assertions/http.go#L27) +{{% /tab %}} +{{< /tabs >}} + +--- + +## Other helpers + +### HTTPBody + +HTTPBody is a helper that returns the HTTP body of the response. +It returns the empty string if building a new request fails. + + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.HTTPBody(handler http.HandlerFunc, method string, url string, values url.Values) string`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#HTTPBody) | package-level function | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.HTTPBody(handler http.HandlerFunc, method string, url string, values url.Values) string`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#HTTPBody) | package-level function | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.HTTPBody(handler http.HandlerFunc, method string, url string, values url.Values) string`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#HTTPBody) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#HTTPBody](https://github.com/go-openapi/testify/blob/master/internal/assertions/http.go#L137) +{{% /tab %}} +{{< /tabs >}} + +--- + +Generated with github.com/go-openapi/testify/v2/codegen + +[godoc-badge]: https://pkg.go.dev/badge/github.com/go-openapi/testify/v2 +[godoc-url]: https://pkg.go.dev/github.com/go-openapi/testify/v2 + + diff --git a/docs/doc-site/api/json.md b/docs/doc-site/api/json.md new file mode 100644 index 000000000..b1d4ba3ee --- /dev/null +++ b/docs/doc-site/api/json.md @@ -0,0 +1,137 @@ +--- +title: "Json" +description: "Asserting JSON Documents" +modified: 2026-01-02 +weight: 9 +domains: + - "json" +keywords: + - "JSONEq" + - "JSONEqf" + - "JSONEqBytes" + - "JSONEqBytesf" +--- + +Asserting JSON Documents + +## Assertions + +[![GoDoc][godoc-badge]][godoc-url] +{class="inline-badge"} + +_All links point to _ + +This domain exposes 2 functionalities. + +### JSONEq + +JSONEq asserts that two JSON strings are equivalent. + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go + assertions.JSONEq(t, `{"hello": "world", "foo": "bar"}`, `{"foo": "bar", "hello": "world"}`) +``` +{{< /tab >}} +{{% tab title="Examples" %}} +```go + success: `{"hello": "world", "foo": "bar"}`, `{"foo": "bar", "hello": "world"}` + failure: `{"hello": "world", "foo": "bar"}`, `[{"foo": "bar"}, {"hello": "world"}]` +``` +{{< /tab >}} +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.JSONEq(t T, expected string, actual string, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#JSONEq) | package-level function | +| [`assert.JSONEqf(t T, expected string, actual string, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#JSONEqf) | formatted variant | +| [`assert.(*Assertions).JSONEq(expected string, actual string) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.JSONEq) | method variant | +| [`assert.(*Assertions).JSONEqf(expected string, actual string, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.JSONEqf) | method formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.JSONEq(t T, expected string, actual string, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#JSONEq) | package-level function | +| [`require.JSONEqf(t T, expected string, actual string, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#JSONEqf) | formatted variant | +| [`require.(*Assertions).JSONEq(expected string, actual string) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.JSONEq) | method variant | +| [`require.(*Assertions).JSONEqf(expected string, actual string, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.JSONEqf) | method formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.JSONEq(t T, expected string, actual string, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#JSONEq) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#JSONEq](https://github.com/go-openapi/testify/blob/master/internal/assertions/json.go#L55) +{{% /tab %}} +{{< /tabs >}} + +### JSONEqBytes + +JSONEqBytes asserts that two JSON byte slices are equivalent. + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go + assertions.JSONEqBytes(t, []byte(`{"hello": "world", "foo": "bar"}`), []byte(`{"foo": "bar", "hello": "world"}`)) +``` +{{< /tab >}} +{{% tab title="Examples" %}} +```go + success: []byte(`{"hello": "world", "foo": "bar"}`), []byte(`{"foo": "bar", "hello": "world"}`) + failure: []byte(`{"hello": "world", "foo": "bar"}`), []byte(`[{"foo": "bar"}, {"hello": "world"}]`) +``` +{{< /tab >}} +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.JSONEqBytes(t T, expected []byte, actual []byte, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#JSONEqBytes) | package-level function | +| [`assert.JSONEqBytesf(t T, expected []byte, actual []byte, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#JSONEqBytesf) | formatted variant | +| [`assert.(*Assertions).JSONEqBytes(expected []byte, actual []byte) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.JSONEqBytes) | method variant | +| [`assert.(*Assertions).JSONEqBytesf(expected []byte, actual []byte, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.JSONEqBytesf) | method formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.JSONEqBytes(t T, expected []byte, actual []byte, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#JSONEqBytes) | package-level function | +| [`require.JSONEqBytesf(t T, expected []byte, actual []byte, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#JSONEqBytesf) | formatted variant | +| [`require.(*Assertions).JSONEqBytes(expected []byte, actual []byte) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.JSONEqBytes) | method variant | +| [`require.(*Assertions).JSONEqBytesf(expected []byte, actual []byte, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.JSONEqBytesf) | method formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.JSONEqBytes(t T, expected []byte, actual []byte, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#JSONEqBytes) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#JSONEqBytes](https://github.com/go-openapi/testify/blob/master/internal/assertions/json.go#L22) +{{% /tab %}} +{{< /tabs >}} + +--- + +--- + +Generated with github.com/go-openapi/testify/v2/codegen + +[godoc-badge]: https://pkg.go.dev/badge/github.com/go-openapi/testify/v2 +[godoc-url]: https://pkg.go.dev/github.com/go-openapi/testify/v2 + + diff --git a/docs/doc-site/api/number.md b/docs/doc-site/api/number.md new file mode 100644 index 000000000..b0b4f71be --- /dev/null +++ b/docs/doc-site/api/number.md @@ -0,0 +1,284 @@ +--- +title: "Number" +description: "Asserting Numbers" +modified: 2026-01-02 +weight: 10 +domains: + - "number" +keywords: + - "InDelta" + - "InDeltaf" + - "InDeltaMapValues" + - "InDeltaMapValuesf" + - "InDeltaSlice" + - "InDeltaSlicef" + - "InEpsilon" + - "InEpsilonf" + - "InEpsilonSlice" + - "InEpsilonSlicef" +--- + +Asserting Numbers + +## Assertions + +[![GoDoc][godoc-badge]][godoc-url] +{class="inline-badge"} + +_All links point to _ + +This domain exposes 5 functionalities. + +### InDelta + +InDelta asserts that the two numerals are within delta of each other. + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go +assertions.InDelta(t, math.Pi, 22/7.0, 0.01) +``` +{{< /tab >}} +{{% tab title="Examples" %}} +```go + success: 1.0, 1.01, 0.02 + failure: 1.0, 1.1, 0.05 +``` +{{< /tab >}} +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.InDelta(t T, expected any, actual any, delta float64, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#InDelta) | package-level function | +| [`assert.InDeltaf(t T, expected any, actual any, delta float64, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#InDeltaf) | formatted variant | +| [`assert.(*Assertions).InDelta(expected any, actual any, delta float64) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.InDelta) | method variant | +| [`assert.(*Assertions).InDeltaf(expected any, actual any, delta float64, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.InDeltaf) | method formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.InDelta(t T, expected any, actual any, delta float64, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#InDelta) | package-level function | +| [`require.InDeltaf(t T, expected any, actual any, delta float64, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#InDeltaf) | formatted variant | +| [`require.(*Assertions).InDelta(expected any, actual any, delta float64) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.InDelta) | method variant | +| [`require.(*Assertions).InDeltaf(expected any, actual any, delta float64, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.InDeltaf) | method formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.InDelta(t T, expected any, actual any, delta float64, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#InDelta) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#InDelta](https://github.com/go-openapi/testify/blob/master/internal/assertions/number.go#L24) +{{% /tab %}} +{{< /tabs >}} + +### InDeltaMapValues + +InDeltaMapValues is the same as InDelta, but it compares all values between two maps. Both maps must have exactly the same keys. + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go + assertions.InDeltaMapValues(t, map[string]float64{"a": 1.0}, map[string]float64{"a": 1.01}, 0.02) +``` +{{< /tab >}} +{{% tab title="Examples" %}} +```go + success: map[string]float64{"a": 1.0}, map[string]float64{"a": 1.01}, 0.02 + failure: map[string]float64{"a": 1.0}, map[string]float64{"a": 1.1}, 0.05 +``` +{{< /tab >}} +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.InDeltaMapValues(t T, expected any, actual any, delta float64, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#InDeltaMapValues) | package-level function | +| [`assert.InDeltaMapValuesf(t T, expected any, actual any, delta float64, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#InDeltaMapValuesf) | formatted variant | +| [`assert.(*Assertions).InDeltaMapValues(expected any, actual any, delta float64) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.InDeltaMapValues) | method variant | +| [`assert.(*Assertions).InDeltaMapValuesf(expected any, actual any, delta float64, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.InDeltaMapValuesf) | method formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.InDeltaMapValues(t T, expected any, actual any, delta float64, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#InDeltaMapValues) | package-level function | +| [`require.InDeltaMapValuesf(t T, expected any, actual any, delta float64, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#InDeltaMapValuesf) | formatted variant | +| [`require.(*Assertions).InDeltaMapValues(expected any, actual any, delta float64) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.InDeltaMapValues) | method variant | +| [`require.(*Assertions).InDeltaMapValuesf(expected any, actual any, delta float64, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.InDeltaMapValuesf) | method formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.InDeltaMapValues(t T, expected any, actual any, delta float64, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#InDeltaMapValues) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#InDeltaMapValues](https://github.com/go-openapi/testify/blob/master/internal/assertions/number.go#L101) +{{% /tab %}} +{{< /tabs >}} + +### InDeltaSlice + +InDeltaSlice is the same as InDelta, except it compares two slices. + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go + assertions.InDeltaSlice(t, []float64{1.0, 2.0}, []float64{1.01, 2.01}, 0.02) +``` +{{< /tab >}} +{{% tab title="Examples" %}} +```go + success: []float64{1.0, 2.0}, []float64{1.01, 2.01}, 0.02 + failure: []float64{1.0, 2.0}, []float64{1.1, 2.1}, 0.05 +``` +{{< /tab >}} +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.InDeltaSlice(t T, expected any, actual any, delta float64, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#InDeltaSlice) | package-level function | +| [`assert.InDeltaSlicef(t T, expected any, actual any, delta float64, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#InDeltaSlicef) | formatted variant | +| [`assert.(*Assertions).InDeltaSlice(expected any, actual any, delta float64) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.InDeltaSlice) | method variant | +| [`assert.(*Assertions).InDeltaSlicef(expected any, actual any, delta float64, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.InDeltaSlicef) | method formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.InDeltaSlice(t T, expected any, actual any, delta float64, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#InDeltaSlice) | package-level function | +| [`require.InDeltaSlicef(t T, expected any, actual any, delta float64, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#InDeltaSlicef) | formatted variant | +| [`require.(*Assertions).InDeltaSlice(expected any, actual any, delta float64) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.InDeltaSlice) | method variant | +| [`require.(*Assertions).InDeltaSlicef(expected any, actual any, delta float64, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.InDeltaSlicef) | method formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.InDeltaSlice(t T, expected any, actual any, delta float64, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#InDeltaSlice) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#InDeltaSlice](https://github.com/go-openapi/testify/blob/master/internal/assertions/number.go#L67) +{{% /tab %}} +{{< /tabs >}} + +### InEpsilon + +InEpsilon asserts that expected and actual have a relative error less than epsilon. + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go + assertions.InEpsilon(t, 100.0, 101.0, 0.02) +``` +{{< /tab >}} +{{% tab title="Examples" %}} +```go + success: 100.0, 101.0, 0.02 + failure: 100.0, 110.0, 0.05 +``` +{{< /tab >}} +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.InEpsilon(t T, expected any, actual any, epsilon float64, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#InEpsilon) | package-level function | +| [`assert.InEpsilonf(t T, expected any, actual any, epsilon float64, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#InEpsilonf) | formatted variant | +| [`assert.(*Assertions).InEpsilon(expected any, actual any, epsilon float64) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.InEpsilon) | method variant | +| [`assert.(*Assertions).InEpsilonf(expected any, actual any, epsilon float64, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.InEpsilonf) | method formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.InEpsilon(t T, expected any, actual any, epsilon float64, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#InEpsilon) | package-level function | +| [`require.InEpsilonf(t T, expected any, actual any, epsilon float64, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#InEpsilonf) | formatted variant | +| [`require.(*Assertions).InEpsilon(expected any, actual any, epsilon float64) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.InEpsilon) | method variant | +| [`require.(*Assertions).InEpsilonf(expected any, actual any, epsilon float64, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.InEpsilonf) | method formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.InEpsilon(t T, expected any, actual any, epsilon float64, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#InEpsilon) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#InEpsilon](https://github.com/go-openapi/testify/blob/master/internal/assertions/number.go#L155) +{{% /tab %}} +{{< /tabs >}} + +### InEpsilonSlice + +InEpsilonSlice is the same as InEpsilon, except it compares each value from two slices. + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go + assertions.InEpsilonSlice(t, []float64{100.0, 200.0}, []float64{101.0, 202.0}, 0.02) +``` +{{< /tab >}} +{{% tab title="Examples" %}} +```go + success: []float64{100.0, 200.0}, []float64{101.0, 202.0}, 0.02 + failure: []float64{100.0, 200.0}, []float64{110.0, 220.0}, 0.05 +``` +{{< /tab >}} +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.InEpsilonSlice(t T, expected any, actual any, epsilon float64, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#InEpsilonSlice) | package-level function | +| [`assert.InEpsilonSlicef(t T, expected any, actual any, epsilon float64, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#InEpsilonSlicef) | formatted variant | +| [`assert.(*Assertions).InEpsilonSlice(expected any, actual any, epsilon float64) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.InEpsilonSlice) | method variant | +| [`assert.(*Assertions).InEpsilonSlicef(expected any, actual any, epsilon float64, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.InEpsilonSlicef) | method formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.InEpsilonSlice(t T, expected any, actual any, epsilon float64, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#InEpsilonSlice) | package-level function | +| [`require.InEpsilonSlicef(t T, expected any, actual any, epsilon float64, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#InEpsilonSlicef) | formatted variant | +| [`require.(*Assertions).InEpsilonSlice(expected any, actual any, epsilon float64) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.InEpsilonSlice) | method variant | +| [`require.(*Assertions).InEpsilonSlicef(expected any, actual any, epsilon float64, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.InEpsilonSlicef) | method formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.InEpsilonSlice(t T, expected any, actual any, epsilon float64, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#InEpsilonSlice) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#InEpsilonSlice](https://github.com/go-openapi/testify/blob/master/internal/assertions/number.go#L188) +{{% /tab %}} +{{< /tabs >}} + +--- + +--- + +Generated with github.com/go-openapi/testify/v2/codegen + +[godoc-badge]: https://pkg.go.dev/badge/github.com/go-openapi/testify/v2 +[godoc-url]: https://pkg.go.dev/github.com/go-openapi/testify/v2 + + diff --git a/docs/doc-site/api/ordering.md b/docs/doc-site/api/ordering.md new file mode 100644 index 000000000..b33fba1b9 --- /dev/null +++ b/docs/doc-site/api/ordering.md @@ -0,0 +1,243 @@ +--- +title: "Ordering" +description: "Asserting How Collections Are Ordered" +modified: 2026-01-02 +weight: 11 +domains: + - "ordering" +keywords: + - "IsDecreasing" + - "IsDecreasingf" + - "IsIncreasing" + - "IsIncreasingf" + - "IsNonDecreasing" + - "IsNonDecreasingf" + - "IsNonIncreasing" + - "IsNonIncreasingf" +--- + +Asserting How Collections Are Ordered + +## Assertions + +[![GoDoc][godoc-badge]][godoc-url] +{class="inline-badge"} + +_All links point to _ + +This domain exposes 4 functionalities. + +### IsDecreasing + +IsDecreasing asserts that the collection is decreasing. + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go + assertions.IsDecreasing(t, []int{2, 1, 0}) + assertions.IsDecreasing(t, []float{2, 1}) + assertions.IsDecreasing(t, []string{"b", "a"}) +``` +{{< /tab >}} +{{% tab title="Examples" %}} +```go + success: []int{3, 2, 1} + failure: []int{1, 2, 3} +``` +{{< /tab >}} +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.IsDecreasing(t T, object any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#IsDecreasing) | package-level function | +| [`assert.IsDecreasingf(t T, object any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#IsDecreasingf) | formatted variant | +| [`assert.(*Assertions).IsDecreasing(object any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.IsDecreasing) | method variant | +| [`assert.(*Assertions).IsDecreasingf(object any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.IsDecreasingf) | method formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.IsDecreasing(t T, object any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#IsDecreasing) | package-level function | +| [`require.IsDecreasingf(t T, object any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#IsDecreasingf) | formatted variant | +| [`require.(*Assertions).IsDecreasing(object any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.IsDecreasing) | method variant | +| [`require.(*Assertions).IsDecreasingf(object any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.IsDecreasingf) | method formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.IsDecreasing(t T, object any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#IsDecreasing) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#IsDecreasing](https://github.com/go-openapi/testify/blob/master/internal/assertions/order.go#L63) +{{% /tab %}} +{{< /tabs >}} + +### IsIncreasing + +IsIncreasing asserts that the collection is increasing. + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go + assertions.IsIncreasing(t, []int{1, 2, 3}) + assertions.IsIncreasing(t, []float{1, 2}) + assertions.IsIncreasing(t, []string{"a", "b"}) +``` +{{< /tab >}} +{{% tab title="Examples" %}} +```go + success: []int{1, 2, 3} + failure: []int{1, 1, 2} +``` +{{< /tab >}} +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.IsIncreasing(t T, object any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#IsIncreasing) | package-level function | +| [`assert.IsIncreasingf(t T, object any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#IsIncreasingf) | formatted variant | +| [`assert.(*Assertions).IsIncreasing(object any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.IsIncreasing) | method variant | +| [`assert.(*Assertions).IsIncreasingf(object any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.IsIncreasingf) | method formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.IsIncreasing(t T, object any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#IsIncreasing) | package-level function | +| [`require.IsIncreasingf(t T, object any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#IsIncreasingf) | formatted variant | +| [`require.(*Assertions).IsIncreasing(object any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.IsIncreasing) | method variant | +| [`require.(*Assertions).IsIncreasingf(object any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.IsIncreasingf) | method formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.IsIncreasing(t T, object any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#IsIncreasing) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#IsIncreasing](https://github.com/go-openapi/testify/blob/master/internal/assertions/order.go#L23) +{{% /tab %}} +{{< /tabs >}} + +### IsNonDecreasing + +IsNonDecreasing asserts that the collection is not decreasing. + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go + assertions.IsNonDecreasing(t, []int{1, 1, 2}) + assertions.IsNonDecreasing(t, []float{1, 2}) + assertions.IsNonDecreasing(t, []string{"a", "b"}) +``` +{{< /tab >}} +{{% tab title="Examples" %}} +```go + success: []int{1, 1, 2} + failure: []int{2, 1, 1} +``` +{{< /tab >}} +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.IsNonDecreasing(t T, object any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#IsNonDecreasing) | package-level function | +| [`assert.IsNonDecreasingf(t T, object any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#IsNonDecreasingf) | formatted variant | +| [`assert.(*Assertions).IsNonDecreasing(object any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.IsNonDecreasing) | method variant | +| [`assert.(*Assertions).IsNonDecreasingf(object any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.IsNonDecreasingf) | method formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.IsNonDecreasing(t T, object any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#IsNonDecreasing) | package-level function | +| [`require.IsNonDecreasingf(t T, object any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#IsNonDecreasingf) | formatted variant | +| [`require.(*Assertions).IsNonDecreasing(object any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.IsNonDecreasing) | method variant | +| [`require.(*Assertions).IsNonDecreasingf(object any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.IsNonDecreasingf) | method formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.IsNonDecreasing(t T, object any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#IsNonDecreasing) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#IsNonDecreasing](https://github.com/go-openapi/testify/blob/master/internal/assertions/order.go#L83) +{{% /tab %}} +{{< /tabs >}} + +### IsNonIncreasing + +IsNonIncreasing asserts that the collection is not increasing. + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go + assertions.IsNonIncreasing(t, []int{2, 1, 1}) + assertions.IsNonIncreasing(t, []float{2, 1}) + assertions.IsNonIncreasing(t, []string{"b", "a"}) +``` +{{< /tab >}} +{{% tab title="Examples" %}} +```go + success: []int{2, 1, 1} + failure: []int{1, 2, 3} +``` +{{< /tab >}} +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.IsNonIncreasing(t T, object any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#IsNonIncreasing) | package-level function | +| [`assert.IsNonIncreasingf(t T, object any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#IsNonIncreasingf) | formatted variant | +| [`assert.(*Assertions).IsNonIncreasing(object any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.IsNonIncreasing) | method variant | +| [`assert.(*Assertions).IsNonIncreasingf(object any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.IsNonIncreasingf) | method formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.IsNonIncreasing(t T, object any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#IsNonIncreasing) | package-level function | +| [`require.IsNonIncreasingf(t T, object any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#IsNonIncreasingf) | formatted variant | +| [`require.(*Assertions).IsNonIncreasing(object any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.IsNonIncreasing) | method variant | +| [`require.(*Assertions).IsNonIncreasingf(object any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.IsNonIncreasingf) | method formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.IsNonIncreasing(t T, object any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#IsNonIncreasing) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#IsNonIncreasing](https://github.com/go-openapi/testify/blob/master/internal/assertions/order.go#L43) +{{% /tab %}} +{{< /tabs >}} + +--- + +--- + +Generated with github.com/go-openapi/testify/v2/codegen + +[godoc-badge]: https://pkg.go.dev/badge/github.com/go-openapi/testify/v2 +[godoc-url]: https://pkg.go.dev/github.com/go-openapi/testify/v2 + + diff --git a/docs/doc-site/api/panic.md b/docs/doc-site/api/panic.md new file mode 100644 index 000000000..ab51a4752 --- /dev/null +++ b/docs/doc-site/api/panic.md @@ -0,0 +1,238 @@ +--- +title: "Panic" +description: "Asserting A Panic Behavior" +modified: 2026-01-02 +weight: 12 +domains: + - "panic" +keywords: + - "NotPanics" + - "NotPanicsf" + - "Panics" + - "Panicsf" + - "PanicsWithError" + - "PanicsWithErrorf" + - "PanicsWithValue" + - "PanicsWithValuef" +--- + +Asserting A Panic Behavior + +## Assertions + +[![GoDoc][godoc-badge]][godoc-url] +{class="inline-badge"} + +_All links point to _ + +This domain exposes 4 functionalities. + +### NotPanics + +NotPanics asserts that the code inside the specified PanicTestFunc does NOT panic. + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go + assertions.NotPanics(t, func(){ RemainCalm() }) +``` +{{< /tab >}} +{{% tab title="Examples" %}} +```go + success: func() { } + failure: func() { panic("panicking") } +``` +{{< /tab >}} +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.NotPanics(t T, f assertions.PanicTestFunc, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#NotPanics) | package-level function | +| [`assert.NotPanicsf(t T, f assertions.PanicTestFunc, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#NotPanicsf) | formatted variant | +| [`assert.(*Assertions).NotPanics(f assertions.PanicTestFunc) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.NotPanics) | method variant | +| [`assert.(*Assertions).NotPanicsf(f assertions.PanicTestFunc, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.NotPanicsf) | method formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.NotPanics(t T, f assertions.PanicTestFunc, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#NotPanics) | package-level function | +| [`require.NotPanicsf(t T, f assertions.PanicTestFunc, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#NotPanicsf) | formatted variant | +| [`require.(*Assertions).NotPanics(f assertions.PanicTestFunc) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.NotPanics) | method variant | +| [`require.(*Assertions).NotPanicsf(f assertions.PanicTestFunc, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.NotPanicsf) | method formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.NotPanics(t T, f assertions.PanicTestFunc, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#NotPanics) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#NotPanics](https://github.com/go-openapi/testify/blob/master/internal/assertions/panic.go#L116) +{{% /tab %}} +{{< /tabs >}} + +### Panics + +Panics asserts that the code inside the specified PanicTestFunc panics. + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go + assertions.Panics(t, func(){ GoCrazy() }) +``` +{{< /tab >}} +{{% tab title="Examples" %}} +```go + success: func() { panic("panicking") } + failure: func() { } +``` +{{< /tab >}} +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.Panics(t T, f assertions.PanicTestFunc, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Panics) | package-level function | +| [`assert.Panicsf(t T, f assertions.PanicTestFunc, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Panicsf) | formatted variant | +| [`assert.(*Assertions).Panics(f assertions.PanicTestFunc) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.Panics) | method variant | +| [`assert.(*Assertions).Panicsf(f assertions.PanicTestFunc, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.Panicsf) | method formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.Panics(t T, f assertions.PanicTestFunc, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Panics) | package-level function | +| [`require.Panicsf(t T, f assertions.PanicTestFunc, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Panicsf) | formatted variant | +| [`require.(*Assertions).Panics(f assertions.PanicTestFunc) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.Panics) | method variant | +| [`require.(*Assertions).Panicsf(f assertions.PanicTestFunc, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.Panicsf) | method formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.Panics(t T, f assertions.PanicTestFunc, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#Panics) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#Panics](https://github.com/go-openapi/testify/blob/master/internal/assertions/panic.go#L29) +{{% /tab %}} +{{< /tabs >}} + +### PanicsWithError + +PanicsWithError asserts that the code inside the specified PanicTestFunc +panics, and that the recovered panic value is an error that satisfies the +EqualError comparison. + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go + assertions.PanicsWithError(t, "crazy error", func(){ GoCrazy() }) +``` +{{< /tab >}} +{{% tab title="Examples" %}} +```go + success: ErrTest.Error(), func() { panic(ErrTest) } + failure: ErrTest.Error(), func() { } +``` +{{< /tab >}} +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.PanicsWithError(t T, errString string, f assertions.PanicTestFunc, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#PanicsWithError) | package-level function | +| [`assert.PanicsWithErrorf(t T, errString string, f assertions.PanicTestFunc, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#PanicsWithErrorf) | formatted variant | +| [`assert.(*Assertions).PanicsWithError(errString string, f assertions.PanicTestFunc) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.PanicsWithError) | method variant | +| [`assert.(*Assertions).PanicsWithErrorf(errString string, f assertions.PanicTestFunc, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.PanicsWithErrorf) | method formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.PanicsWithError(t T, errString string, f assertions.PanicTestFunc, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#PanicsWithError) | package-level function | +| [`require.PanicsWithErrorf(t T, errString string, f assertions.PanicTestFunc, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#PanicsWithErrorf) | formatted variant | +| [`require.(*Assertions).PanicsWithError(errString string, f assertions.PanicTestFunc) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.PanicsWithError) | method variant | +| [`require.(*Assertions).PanicsWithErrorf(errString string, f assertions.PanicTestFunc, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.PanicsWithErrorf) | method formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.PanicsWithError(t T, errString string, f assertions.PanicTestFunc, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#PanicsWithError) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#PanicsWithError](https://github.com/go-openapi/testify/blob/master/internal/assertions/panic.go#L82) +{{% /tab %}} +{{< /tabs >}} + +### PanicsWithValue + +PanicsWithValue asserts that the code inside the specified PanicTestFunc panics, and that +the recovered panic value equals the expected panic value. + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go + assertions.PanicsWithValue(t, "crazy error", func(){ GoCrazy() }) +``` +{{< /tab >}} +{{% tab title="Examples" %}} +```go + success: "panicking", func() { panic("panicking") } + failure: "panicking", func() { } +``` +{{< /tab >}} +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.PanicsWithValue(t T, expected any, f assertions.PanicTestFunc, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#PanicsWithValue) | package-level function | +| [`assert.PanicsWithValuef(t T, expected any, f assertions.PanicTestFunc, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#PanicsWithValuef) | formatted variant | +| [`assert.(*Assertions).PanicsWithValue(expected any, f assertions.PanicTestFunc) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.PanicsWithValue) | method variant | +| [`assert.(*Assertions).PanicsWithValuef(expected any, f assertions.PanicTestFunc, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.PanicsWithValuef) | method formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.PanicsWithValue(t T, expected any, f assertions.PanicTestFunc, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#PanicsWithValue) | package-level function | +| [`require.PanicsWithValuef(t T, expected any, f assertions.PanicTestFunc, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#PanicsWithValuef) | formatted variant | +| [`require.(*Assertions).PanicsWithValue(expected any, f assertions.PanicTestFunc) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.PanicsWithValue) | method variant | +| [`require.(*Assertions).PanicsWithValuef(expected any, f assertions.PanicTestFunc, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.PanicsWithValuef) | method formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.PanicsWithValue(t T, expected any, f assertions.PanicTestFunc, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#PanicsWithValue) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#PanicsWithValue](https://github.com/go-openapi/testify/blob/master/internal/assertions/panic.go#L53) +{{% /tab %}} +{{< /tabs >}} + +--- + +--- + +Generated with github.com/go-openapi/testify/v2/codegen + +[godoc-badge]: https://pkg.go.dev/badge/github.com/go-openapi/testify/v2 +[godoc-url]: https://pkg.go.dev/github.com/go-openapi/testify/v2 + + diff --git a/docs/doc-site/api/string.md b/docs/doc-site/api/string.md new file mode 100644 index 000000000..e7c41a4d8 --- /dev/null +++ b/docs/doc-site/api/string.md @@ -0,0 +1,139 @@ +--- +title: "String" +description: "Asserting Strings" +modified: 2026-01-02 +weight: 13 +domains: + - "string" +keywords: + - "NotRegexp" + - "NotRegexpf" + - "Regexp" + - "Regexpf" +--- + +Asserting Strings + +## Assertions + +[![GoDoc][godoc-badge]][godoc-url] +{class="inline-badge"} + +_All links point to _ + +This domain exposes 2 functionalities. + +### NotRegexp + +NotRegexp asserts that a specified regexp does not match a string. + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go + assertions.NotRegexp(t, regexp.MustCompile("starts"), "it's starting") + assertions.NotRegexp(t, "^start", "it's not starting") +``` +{{< /tab >}} +{{% tab title="Examples" %}} +```go + success: "^start", "not starting" + failure: "^start", "starting" +``` +{{< /tab >}} +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.NotRegexp(t T, rx any, str any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#NotRegexp) | package-level function | +| [`assert.NotRegexpf(t T, rx any, str any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#NotRegexpf) | formatted variant | +| [`assert.(*Assertions).NotRegexp(rx any, str any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.NotRegexp) | method variant | +| [`assert.(*Assertions).NotRegexpf(rx any, str any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.NotRegexpf) | method formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.NotRegexp(t T, rx any, str any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#NotRegexp) | package-level function | +| [`require.NotRegexpf(t T, rx any, str any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#NotRegexpf) | formatted variant | +| [`require.(*Assertions).NotRegexp(rx any, str any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.NotRegexp) | method variant | +| [`require.(*Assertions).NotRegexpf(rx any, str any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.NotRegexpf) | method formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.NotRegexp(t T, rx any, str any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#NotRegexp) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#NotRegexp](https://github.com/go-openapi/testify/blob/master/internal/assertions/string.go#L53) +{{% /tab %}} +{{< /tabs >}} + +### Regexp + +Regexp asserts that a specified regexp matches a string. + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go + assertions.Regexp(t, regexp.MustCompile("start"), "it's starting") + assertions.Regexp(t, "start...$", "it's not starting") +``` +{{< /tab >}} +{{% tab title="Examples" %}} +```go + success: "^start", "starting" + failure: "^start", "not starting" +``` +{{< /tab >}} +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.Regexp(t T, rx any, str any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Regexp) | package-level function | +| [`assert.Regexpf(t T, rx any, str any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Regexpf) | formatted variant | +| [`assert.(*Assertions).Regexp(rx any, str any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.Regexp) | method variant | +| [`assert.(*Assertions).Regexpf(rx any, str any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.Regexpf) | method formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.Regexp(t T, rx any, str any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Regexp) | package-level function | +| [`require.Regexpf(t T, rx any, str any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Regexpf) | formatted variant | +| [`require.(*Assertions).Regexp(rx any, str any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.Regexp) | method variant | +| [`require.(*Assertions).Regexpf(rx any, str any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.Regexpf) | method formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.Regexp(t T, rx any, str any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#Regexp) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#Regexp](https://github.com/go-openapi/testify/blob/master/internal/assertions/string.go#L22) +{{% /tab %}} +{{< /tabs >}} + +--- + +--- + +Generated with github.com/go-openapi/testify/v2/codegen + +[godoc-badge]: https://pkg.go.dev/badge/github.com/go-openapi/testify/v2 +[godoc-url]: https://pkg.go.dev/github.com/go-openapi/testify/v2 + + diff --git a/docs/doc-site/api/testing.md b/docs/doc-site/api/testing.md new file mode 100644 index 000000000..6e2b491de --- /dev/null +++ b/docs/doc-site/api/testing.md @@ -0,0 +1,135 @@ +--- +title: "Testing" +description: "Mimicks Methods From The Testing Standard Library" +modified: 2026-01-02 +weight: 14 +domains: + - "testing" +keywords: + - "Fail" + - "Failf" + - "FailNow" + - "FailNowf" +--- + +Mimicks Methods From The Testing Standard Library + +## Assertions + +[![GoDoc][godoc-badge]][godoc-url] +{class="inline-badge"} + +_All links point to _ + +This domain exposes 2 functionalities. + +### Fail + +Fail reports a failure through. + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go + assertions.Fail(t, "failed") +``` +{{< /tab >}} +{{% tab title="Examples" %}} +```go + failure: "failed" +``` +{{< /tab >}} +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.Fail(t T, failureMessage string, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Fail) | package-level function | +| [`assert.Failf(t T, failureMessage string, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Failf) | formatted variant | +| [`assert.(*Assertions).Fail(failureMessage string) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.Fail) | method variant | +| [`assert.(*Assertions).Failf(failureMessage string, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.Failf) | method formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.Fail(t T, failureMessage string, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Fail) | package-level function | +| [`require.Failf(t T, failureMessage string, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Failf) | formatted variant | +| [`require.(*Assertions).Fail(failureMessage string) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.Fail) | method variant | +| [`require.(*Assertions).Failf(failureMessage string, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.Failf) | method formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.Fail(t T, failureMessage string, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#Fail) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#Fail](https://github.com/go-openapi/testify/blob/master/internal/assertions/testing.go#L68) +{{% /tab %}} +{{< /tabs >}} + +### FailNow + +FailNow fails test. + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go + assertions.FailNow(t, "failed") +``` +{{< /tab >}} +{{% tab title="Examples" %}} +```go + failure: "failed" +``` +{{< /tab >}} +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.FailNow(t T, failureMessage string, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#FailNow) | package-level function | +| [`assert.FailNowf(t T, failureMessage string, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#FailNowf) | formatted variant | +| [`assert.(*Assertions).FailNow(failureMessage string) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.FailNow) | method variant | +| [`assert.(*Assertions).FailNowf(failureMessage string, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.FailNowf) | method formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.FailNow(t T, failureMessage string, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#FailNow) | package-level function | +| [`require.FailNowf(t T, failureMessage string, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#FailNowf) | formatted variant | +| [`require.(*Assertions).FailNow(failureMessage string) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.FailNow) | method variant | +| [`require.(*Assertions).FailNowf(failureMessage string, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.FailNowf) | method formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.FailNow(t T, failureMessage string, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#FailNow) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#FailNow](https://github.com/go-openapi/testify/blob/master/internal/assertions/testing.go#L37) +{{% /tab %}} +{{< /tabs >}} + +--- + +--- + +Generated with github.com/go-openapi/testify/v2/codegen + +[godoc-badge]: https://pkg.go.dev/badge/github.com/go-openapi/testify/v2 +[godoc-url]: https://pkg.go.dev/github.com/go-openapi/testify/v2 + + diff --git a/docs/doc-site/api/time.md b/docs/doc-site/api/time.md new file mode 100644 index 000000000..7aa6c20c8 --- /dev/null +++ b/docs/doc-site/api/time.md @@ -0,0 +1,137 @@ +--- +title: "Time" +description: "Asserting Times And Durations" +modified: 2026-01-02 +weight: 15 +domains: + - "time" +keywords: + - "WithinDuration" + - "WithinDurationf" + - "WithinRange" + - "WithinRangef" +--- + +Asserting Times And Durations + +## Assertions + +[![GoDoc][godoc-badge]][godoc-url] +{class="inline-badge"} + +_All links point to _ + +This domain exposes 2 functionalities. + +### WithinDuration + +WithinDuration asserts that the two times are within duration delta of each other. + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go + assertions.WithinDuration(t, time.Now(), 10*time.Second) +``` +{{< /tab >}} +{{% tab title="Examples" %}} +```go + success: time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC), time.Date(2024, 1, 1, 12, 0, 1, 0, time.UTC), 2*time.Second + failure: time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC), time.Date(2024, 1, 1, 12, 0, 10, 0, time.UTC), 1*time.Second +``` +{{< /tab >}} +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.WithinDuration(t T, expected time.Time, actual time.Time, delta time.Duration, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#WithinDuration) | package-level function | +| [`assert.WithinDurationf(t T, expected time.Time, actual time.Time, delta time.Duration, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#WithinDurationf) | formatted variant | +| [`assert.(*Assertions).WithinDuration(expected time.Time, actual time.Time, delta time.Duration) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.WithinDuration) | method variant | +| [`assert.(*Assertions).WithinDurationf(expected time.Time, actual time.Time, delta time.Duration, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.WithinDurationf) | method formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.WithinDuration(t T, expected time.Time, actual time.Time, delta time.Duration, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#WithinDuration) | package-level function | +| [`require.WithinDurationf(t T, expected time.Time, actual time.Time, delta time.Duration, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#WithinDurationf) | formatted variant | +| [`require.(*Assertions).WithinDuration(expected time.Time, actual time.Time, delta time.Duration) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.WithinDuration) | method variant | +| [`require.(*Assertions).WithinDurationf(expected time.Time, actual time.Time, delta time.Duration, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.WithinDurationf) | method formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.WithinDuration(t T, expected time.Time, actual time.Time, delta time.Duration, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#WithinDuration) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#WithinDuration](https://github.com/go-openapi/testify/blob/master/internal/assertions/time.go#L21) +{{% /tab %}} +{{< /tabs >}} + +### WithinRange + +WithinRange asserts that a time is within a time range (inclusive). + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go + assertions.WithinRange(t, time.Now(), time.Now().Add(-time.Second), time.Now().Add(time.Second)) +``` +{{< /tab >}} +{{% tab title="Examples" %}} +```go + success: time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC), time.Date(2024, 1, 1, 11, 0, 0, 0, time.UTC), time.Date(2024, 1, 1, 13, 0, 0, 0, time.UTC) + failure: time.Date(2024, 1, 1, 14, 0, 0, 0, time.UTC), time.Date(2024, 1, 1, 11, 0, 0, 0, time.UTC), time.Date(2024, 1, 1, 13, 0, 0, 0, time.UTC) +``` +{{< /tab >}} +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.WithinRange(t T, actual time.Time, start time.Time, end time.Time, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#WithinRange) | package-level function | +| [`assert.WithinRangef(t T, actual time.Time, start time.Time, end time.Time, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#WithinRangef) | formatted variant | +| [`assert.(*Assertions).WithinRange(actual time.Time, start time.Time, end time.Time) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.WithinRange) | method variant | +| [`assert.(*Assertions).WithinRangef(actual time.Time, start time.Time, end time.Time, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.WithinRangef) | method formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.WithinRange(t T, actual time.Time, start time.Time, end time.Time, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#WithinRange) | package-level function | +| [`require.WithinRangef(t T, actual time.Time, start time.Time, end time.Time, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#WithinRangef) | formatted variant | +| [`require.(*Assertions).WithinRange(actual time.Time, start time.Time, end time.Time) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.WithinRange) | method variant | +| [`require.(*Assertions).WithinRangef(actual time.Time, start time.Time, end time.Time, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.WithinRangef) | method formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.WithinRange(t T, actual time.Time, start time.Time, end time.Time, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#WithinRange) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#WithinRange](https://github.com/go-openapi/testify/blob/master/internal/assertions/time.go#L45) +{{% /tab %}} +{{< /tabs >}} + +--- + +--- + +Generated with github.com/go-openapi/testify/v2/codegen + +[godoc-badge]: https://pkg.go.dev/badge/github.com/go-openapi/testify/v2 +[godoc-url]: https://pkg.go.dev/github.com/go-openapi/testify/v2 + + diff --git a/docs/doc-site/api/type.md b/docs/doc-site/api/type.md new file mode 100644 index 000000000..82705771f --- /dev/null +++ b/docs/doc-site/api/type.md @@ -0,0 +1,333 @@ +--- +title: "Type" +description: "Asserting Types Rather Than Values" +modified: 2026-01-02 +weight: 16 +domains: + - "type" +keywords: + - "Implements" + - "Implementsf" + - "IsNotType" + - "IsNotTypef" + - "IsType" + - "IsTypef" + - "NotImplements" + - "NotImplementsf" + - "NotZero" + - "NotZerof" + - "Zero" + - "Zerof" +--- + +Asserting Types Rather Than Values + +## Assertions + +[![GoDoc][godoc-badge]][godoc-url] +{class="inline-badge"} + +_All links point to _ + +This domain exposes 6 functionalities. + +### Implements + +Implements asserts that an object is implemented by the specified interface. + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go + assertions.Implements(t, (*MyInterface)(nil), new(MyObject)) +``` +{{< /tab >}} +{{% tab title="Examples" %}} +```go + success: ptr(dummyInterface), new(testing.T) + failure: (*error)(nil), new(testing.T) +``` +{{< /tab >}} +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.Implements(t T, interfaceObject any, object any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Implements) | package-level function | +| [`assert.Implementsf(t T, interfaceObject any, object any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Implementsf) | formatted variant | +| [`assert.(*Assertions).Implements(interfaceObject any, object any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.Implements) | method variant | +| [`assert.(*Assertions).Implementsf(interfaceObject any, object any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.Implementsf) | method formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.Implements(t T, interfaceObject any, object any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Implements) | package-level function | +| [`require.Implementsf(t T, interfaceObject any, object any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Implementsf) | formatted variant | +| [`require.(*Assertions).Implements(interfaceObject any, object any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.Implements) | method variant | +| [`require.(*Assertions).Implementsf(interfaceObject any, object any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.Implementsf) | method formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.Implements(t T, interfaceObject any, object any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#Implements) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#Implements](https://github.com/go-openapi/testify/blob/master/internal/assertions/type.go#L21) +{{% /tab %}} +{{< /tabs >}} + +### IsNotType + +IsNotType asserts that the specified objects are not of the same type. + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go + assertions.IsNotType(t, &NotMyStruct{}, &MyStruct{}) +``` +{{< /tab >}} +{{% tab title="Examples" %}} +```go + success: int32(123), int64(456) + failure: 123, 456 +``` +{{< /tab >}} +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.IsNotType(t T, theType any, object any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#IsNotType) | package-level function | +| [`assert.IsNotTypef(t T, theType any, object any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#IsNotTypef) | formatted variant | +| [`assert.(*Assertions).IsNotType(theType any, object any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.IsNotType) | method variant | +| [`assert.(*Assertions).IsNotTypef(theType any, object any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.IsNotTypef) | method formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.IsNotType(t T, theType any, object any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#IsNotType) | package-level function | +| [`require.IsNotTypef(t T, theType any, object any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#IsNotTypef) | formatted variant | +| [`require.(*Assertions).IsNotType(theType any, object any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.IsNotType) | method variant | +| [`require.(*Assertions).IsNotTypef(theType any, object any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.IsNotTypef) | method formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.IsNotType(t T, theType any, object any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#IsNotType) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#IsNotType](https://github.com/go-openapi/testify/blob/master/internal/assertions/type.go#L96) +{{% /tab %}} +{{< /tabs >}} + +### IsType + +IsType asserts that the specified objects are of the same type. + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go + assertions.IsType(t, &MyStruct{}, &MyStruct{}) +``` +{{< /tab >}} +{{% tab title="Examples" %}} +```go + success: 123, 456 + failure: int32(123), int64(456) +``` +{{< /tab >}} +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.IsType(t T, expectedType any, object any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#IsType) | package-level function | +| [`assert.IsTypef(t T, expectedType any, object any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#IsTypef) | formatted variant | +| [`assert.(*Assertions).IsType(expectedType any, object any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.IsType) | method variant | +| [`assert.(*Assertions).IsTypef(expectedType any, object any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.IsTypef) | method formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.IsType(t T, expectedType any, object any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#IsType) | package-level function | +| [`require.IsTypef(t T, expectedType any, object any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#IsTypef) | formatted variant | +| [`require.(*Assertions).IsType(expectedType any, object any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.IsType) | method variant | +| [`require.(*Assertions).IsTypef(expectedType any, object any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.IsTypef) | method formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.IsType(t T, expectedType any, object any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#IsType) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#IsType](https://github.com/go-openapi/testify/blob/master/internal/assertions/type.go#L75) +{{% /tab %}} +{{< /tabs >}} + +### NotImplements + +NotImplements asserts that an object does not implement the specified interface. + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go + assertions.NotImplements(t, (*MyInterface)(nil), new(MyObject)) +``` +{{< /tab >}} +{{% tab title="Examples" %}} +```go + success: (*error)(nil), new(testing.T) + failure: ptr(dummyInterface), new(testing.T) +``` +{{< /tab >}} +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.NotImplements(t T, interfaceObject any, object any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#NotImplements) | package-level function | +| [`assert.NotImplementsf(t T, interfaceObject any, object any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#NotImplementsf) | formatted variant | +| [`assert.(*Assertions).NotImplements(interfaceObject any, object any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.NotImplements) | method variant | +| [`assert.(*Assertions).NotImplementsf(interfaceObject any, object any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.NotImplementsf) | method formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.NotImplements(t T, interfaceObject any, object any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#NotImplements) | package-level function | +| [`require.NotImplementsf(t T, interfaceObject any, object any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#NotImplementsf) | formatted variant | +| [`require.(*Assertions).NotImplements(interfaceObject any, object any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.NotImplements) | method variant | +| [`require.(*Assertions).NotImplementsf(interfaceObject any, object any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.NotImplementsf) | method formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.NotImplements(t T, interfaceObject any, object any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#NotImplements) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#NotImplements](https://github.com/go-openapi/testify/blob/master/internal/assertions/type.go#L48) +{{% /tab %}} +{{< /tabs >}} + +### NotZero + +NotZero asserts that i is not the zero value for its type. + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go + assertions.NotZero(t, obj) +``` +{{< /tab >}} +{{% tab title="Examples" %}} +```go + success: 1 + failure: 0 +``` +{{< /tab >}} +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.NotZero(t T, i any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#NotZero) | package-level function | +| [`assert.NotZerof(t T, i any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#NotZerof) | formatted variant | +| [`assert.(*Assertions).NotZero(i any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.NotZero) | method variant | +| [`assert.(*Assertions).NotZerof(i any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.NotZerof) | method formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.NotZero(t T, i any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#NotZero) | package-level function | +| [`require.NotZerof(t T, i any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#NotZerof) | formatted variant | +| [`require.(*Assertions).NotZero(i any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.NotZero) | method variant | +| [`require.(*Assertions).NotZerof(i any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.NotZerof) | method formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.NotZero(t T, i any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#NotZero) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#NotZero](https://github.com/go-openapi/testify/blob/master/internal/assertions/type.go#L138) +{{% /tab %}} +{{< /tabs >}} + +### Zero + +Zero asserts that i is the zero value for its type. + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go + assertions.Zero(t, obj) +``` +{{< /tab >}} +{{% tab title="Examples" %}} +```go + success: 0 + failure: 1 +``` +{{< /tab >}} +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.Zero(t T, i any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Zero) | package-level function | +| [`assert.Zerof(t T, i any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Zerof) | formatted variant | +| [`assert.(*Assertions).Zero(i any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.Zero) | method variant | +| [`assert.(*Assertions).Zerof(i any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.Zerof) | method formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.Zero(t T, i any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Zero) | package-level function | +| [`require.Zerof(t T, i any, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Zerof) | formatted variant | +| [`require.(*Assertions).Zero(i any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.Zero) | method variant | +| [`require.(*Assertions).Zerof(i any, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.Zerof) | method formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.Zero(t T, i any, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#Zero) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#Zero](https://github.com/go-openapi/testify/blob/master/internal/assertions/type.go#L117) +{{% /tab %}} +{{< /tabs >}} + +--- + +--- + +Generated with github.com/go-openapi/testify/v2/codegen + +[godoc-badge]: https://pkg.go.dev/badge/github.com/go-openapi/testify/v2 +[godoc-url]: https://pkg.go.dev/github.com/go-openapi/testify/v2 + + diff --git a/docs/doc-site/api/yaml.md b/docs/doc-site/api/yaml.md new file mode 100644 index 000000000..3003c95cf --- /dev/null +++ b/docs/doc-site/api/yaml.md @@ -0,0 +1,98 @@ +--- +title: "Yaml" +description: "Asserting Yaml Documents" +modified: 2026-01-02 +weight: 17 +domains: + - "yaml" +keywords: + - "YAMLEq" + - "YAMLEqf" +--- + +Asserting Yaml Documents + +## Assertions + +[![GoDoc][godoc-badge]][godoc-url] +{class="inline-badge"} + +_All links point to _ + +This domain exposes 1 functionalities. + +### YAMLEq + +YAMLEq asserts that the first documents in the two YAML strings are equivalent. + +{{% expand title="Examples" %}} +{{< tabs >}} +{{% tab title="Usage" %}} +```go + expected := `--- + key: value + --- + key: this is a second document, it is not evaluated + ` + actual := `--- + key: value + --- + key: this is a subsequent document, it is not evaluated + ` + assertions.YAMLEq(t, expected, actual) +``` +{{< /tab >}} +{{% tab title="Examples" %}} +```go + panic: "key: value", "key: value" + should panic without the yaml feature enabled. +``` +{{< /tab >}} +{{< /tabs >}} +{{% /expand %}} + +{{< tabs >}} +{{% tab title="assert" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`assert.YAMLEq(t T, expected string, actual string, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#YAMLEq) | package-level function | +| [`assert.YAMLEqf(t T, expected string, actual string, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#YAMLEqf) | formatted variant | +| [`assert.(*Assertions).YAMLEq(expected string, actual string) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.YAMLEq) | method variant | +| [`assert.(*Assertions).YAMLEqf(expected string, actual string, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#Assertions.YAMLEqf) | method formatted variant | +{{% /tab %}} +{{% tab title="require" style="secondary" %}} +| Signature | Usage | +|--|--| +| [`require.YAMLEq(t T, expected string, actual string, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#YAMLEq) | package-level function | +| [`require.YAMLEqf(t T, expected string, actual string, msg string, args ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#YAMLEqf) | formatted variant | +| [`require.(*Assertions).YAMLEq(expected string, actual string) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.YAMLEq) | method variant | +| [`require.(*Assertions).YAMLEqf(expected string, actual string, msg string, args ..any)`](https://pkg.go.dev/github.com/go-openapi/testify/v2/require#Assertions.YAMLEqf) | method formatted variant | +{{% /tab %}} + +{{% tab title="internal" style="accent" icon="wrench" %}} +| Signature | Usage | +|--|--| +| [`assertions.YAMLEq(t T, expected string, actual string, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#YAMLEq) | internal implementation | + +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#YAMLEq](https://github.com/go-openapi/testify/blob/master/internal/assertions/yaml.go#L32) +{{% /tab %}} +{{< /tabs >}} + +--- + +--- + +Generated with github.com/go-openapi/testify/v2/codegen + +[godoc-badge]: https://pkg.go.dev/badge/github.com/go-openapi/testify/v2 +[godoc-url]: https://pkg.go.dev/github.com/go-openapi/testify/v2 + + diff --git a/docs/doc-site/examples/EXAMPLES.md b/docs/doc-site/examples/EXAMPLES.md new file mode 100644 index 000000000..5297a4e96 --- /dev/null +++ b/docs/doc-site/examples/EXAMPLES.md @@ -0,0 +1,481 @@ +--- +title: "Examples" +description: "Practical examples for using testify v2" +weight: 1 +--- + +{{% notice primary "TL;DR" "meteor" %}} +> If you've already used `github.com/stretchr/testify`, adopting v2 will be straightforward. +{{% /notice %}} + +## Quick Start + +The simplest way to get started with testify is using the `assert` package: + +```go +import ( + "testing" + "github.com/go-openapi/testify/v2/assert" +) + +func TestCalculator(t *testing.T) { + result := Add(2, 3) + assert.Equal(t, 5, result) +} +``` + +## assert vs require + +**Use `assert`** when you want tests to continue after a failure: + +```go +func TestUser(t *testing.T) { + user := GetUser(123) + + // All three checks run, even if the first fails + assert.NotNil(t, user) + assert.Equal(t, "Alice", user.Name) + assert.Equal(t, 25, user.Age) +} +``` + +**Use `require`** when subsequent checks depend on earlier ones: + +```go +import "github.com/go-openapi/testify/v2/require" + +func TestUser(t *testing.T) { + user := GetUser(123) + + // Stop immediately if user is nil (prevents panic on next line) + require.NotNil(t, user) + + // Only runs if user is not nil + assert.Equal(t, "Alice", user.Name) +} +``` + +**Rule of thumb:** Use `require` for preconditions, `assert` for actual checks. + +--- + +## Common Assertions + +### Equality + +```go +func TestEquality(t *testing.T) { + // Basic equality + assert.Equal(t, 42, actualValue) + + // Deep equality for slices, maps, structs + assert.Equal(t, []int{1, 2, 3}, result) + + // Check inequality + assert.NotEqual(t, 0, result) + + // Type-converting equality (123 == int64(123)) + assert.EqualValues(t, 123, int64(123)) +} +``` + +### Collections + +```go +func TestCollections(t *testing.T) { + list := []string{"apple", "banana", "cherry"} + + // Check if collection contains an element + assert.Contains(t, list, "banana") + assert.NotContains(t, list, "orange") + + // Check length + assert.Len(t, list, 3) + + // Check if empty + assert.NotEmpty(t, list) + assert.Empty(t, []string{}) + + // Check if all elements match (order doesn't matter) + assert.ElementsMatch(t, []int{3, 1, 2}, []int{1, 2, 3}) +} +``` + +### Errors + +```go +func TestErrors(t *testing.T) { + // Check if function returns an error + err := DoSomething() + assert.Error(t, err) + + // Check if function succeeds + err = DoSomethingElse() + assert.NoError(t, err) + + // Check specific error message + err = Divide(10, 0) + assert.EqualError(t, err, "division by zero") + + // Check if error contains text + assert.ErrorContains(t, err, "division") + + // Check error type with errors.Is + assert.ErrorIs(t, err, ErrDivisionByZero) +} +``` + +### Nil Checks + +```go +func TestNil(t *testing.T) { + var ptr *User + + assert.Nil(t, ptr) + + user := &User{Name: "Alice"} + assert.NotNil(t, user) +} +``` + +### Boolean and Comparisons + +```go +func TestBooleans(t *testing.T) { + assert.True(t, isValid) + assert.False(t, hasErrors) + + // Numeric comparisons + assert.Greater(t, 10, 5) + assert.GreaterOrEqual(t, 10, 10) + assert.Less(t, 5, 10) + assert.LessOrEqual(t, 5, 5) +} +``` + +--- + +## Assertion Variants + +Testify provides multiple ways to call assertions: + +### 1. Package-Level Functions + +```go +func TestPackageLevel(t *testing.T) { + assert.Equal(t, 42, result) + require.NotNil(t, user) +} +``` + +### 2. Formatted Variants (Custom Messages) + +```go +func TestFormatted(t *testing.T) { + // Add custom failure message with formatting + assert.Equalf(t, 42, result, "expected answer to be %d", 42) + require.NotNilf(t, user, "user %d should exist", userID) +} +``` + +### 3. Forward Methods (Cleaner Syntax) + +```go +func TestForward(t *testing.T) { + a := assert.New(t) + r := require.New(t) + + // No need to pass 't' each time + a.Equal(42, result) + a.NotEmpty(list) + + r.NotNil(user) + r.NoError(err) +} +``` + +### 4. Forward Methods with Formatting + +```go +func TestForwardFormatted(t *testing.T) { + a := assert.New(t) + + a.Equalf(42, result, "expected answer to be %d", 42) + a.Lenf(list, 3, "expected %d items", 3) +} +``` + +**Recommendation:** Use package-level functions for simple tests, forward methods for tests with many assertions. + +--- + +## Table-Driven Tests + +The idiomatic Go pattern for testing multiple cases should be: + +```go +func TestAdd(t *testing.T) { + tests := []struct { + name string + a, b int + expected int + }{ + {"positive numbers", 2, 3, 5}, + {"negative numbers", -2, -3, -5}, + {"mixed signs", -2, 3, 1}, + {"with zero", 0, 5, 5}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := Add(tt.a, tt.b) + assert.Equal(t, tt.expected, result) + }) + } +} +``` + +With forward methods for cleaner syntax: + +```go +func TestAdd(t *testing.T) { + tests := []struct { + name string + a, b int + expected int + }{ + {"positive numbers", 2, 3, 5}, + {"negative numbers", -2, -3, -5}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := assert.New(t) + + result := Add(tt.a, tt.b) + a.Equal(tt.expected, result) + a.Greater(result, tt.a) + }) + } +} +``` + +--- + +## Real-World Examples + +### Testing HTTP Handlers + +```go +import ( + "net/http" + "net/http/httptest" + "github.com/go-openapi/testify/v2/assert" + "github.com/go-openapi/testify/v2/require" +) + +func TestUserHandler(t *testing.T) { + req := httptest.NewRequest("GET", "/users/123", nil) + w := httptest.NewRecorder() + + handler := NewUserHandler() + handler.ServeHTTP(w, req) + + require.Equal(t, http.StatusOK, w.Code) + + // Check response body contains expected data + body := w.Body.String() + assert.Contains(t, body, `"name":"Alice"`) + assert.Contains(t, body, `"id":123`) +} +``` + +### Testing JSON + +```go +func TestJSONResponse(t *testing.T) { + expected := `{"name":"Alice","age":25}` + actual := `{"age":25,"name":"Alice"}` + + // JSONEq compares JSON semantically (ignores key order, whitespace) + assert.JSONEq(t, expected, actual) +} +``` + +### Testing with Subtests + +```go +func TestUserOperations(t *testing.T) { + user := &User{ID: 123, Name: "Alice"} + + t.Run("creation", func(t *testing.T) { + assert.NotNil(t, user) + assert.Equal(t, 123, user.ID) + }) + + t.Run("update", func(t *testing.T) { + user.Name = "Bob" + assert.Equal(t, "Bob", user.Name) + }) + + t.Run("deletion", func(t *testing.T) { + err := DeleteUser(user.ID) + assert.NoError(t, err) + }) +} +``` + +### Testing Panics + +```go +func TestPanics(t *testing.T) { + // Function should panic + assert.Panics(t, func() { + Divide(10, 0) + }) + + // Function should NOT panic + assert.NotPanics(t, func() { + Divide(10, 2) + }) + + // Function should panic with specific value + assert.PanicsWithValue(t, "division by zero", func() { + Divide(10, 0) + }) +} +``` + +--- + +## Advanced Patterns + +### Setup and Teardown + +```go +func TestWithSetup(t *testing.T) { + // Setup + db := setupTestDatabase(t) + defer db.Close() // Teardown + + // Test + user := &User{Name: "Alice"} + err := db.Save(user) + require.NoError(t, err) + + // Verify + loaded, err := db.Find(user.ID) + require.NoError(t, err) + assert.Equal(t, "Alice", loaded.Name) +} +``` + +### Helper Functions + +```go +func assertUserValid(t *testing.T, user *User) { + t.Helper() // Mark as helper for better error messages + + assert.NotNil(t, user) + assert.NotEmpty(t, user.Name) + assert.Greater(t, user.Age, 0) +} + +func TestUsers(t *testing.T) { + user := GetUser(123) + assertUserValid(t, user) // Failures point to this line, not inside helper +} +``` + +### Combining Multiple Assertions + +```go +func TestUserCompleteness(t *testing.T) { + a := assert.New(t) + user := GetUser(123) + + // Chain multiple checks cleanly + a.NotNil(user) + a.NotEmpty(user.Name) + a.NotEmpty(user.Email) + a.Greater(user.Age, 0) + a.True(user.Active) +} +``` + +--- + +## YAML Support (Optional) + +YAML assertions require explicit opt-in: + +```go +import ( + "testing" + "github.com/go-openapi/testify/v2/assert" + _ "github.com/go-openapi/testify/v2/enable/yaml" // Enable YAML support +) + +func TestYAML(t *testing.T) { + expected := ` +name: Alice +age: 25 +` + actual := ` +age: 25 +name: Alice +` + + // YAMLEq compares YAML semantically + assert.YAMLEq(t, expected, actual) +} +``` + +**Note:** Without the `enable/yaml` import, YAML assertions will panic with a helpful message. + +--- + +## Best Practices + +1. **Use `require` for preconditions** - Stop test immediately if setup fails +2. **Use `assert` for actual checks** - See all failures in one test run +3. **Add custom messages for complex checks** - Use formatted variants when assertion failure needs context +4. **Prefer table-driven tests** - Test multiple cases systematically +5. **Use forward methods for many assertions** - Reduces repetition in long tests +6. **Keep tests focused** - One logical concept per test function +7. **Use subtests for related scenarios** - Group related checks with `t.Run()` +8. **Mark helpers with `t.Helper()`** - Get better error locations + +--- + +## Migration from stdlib testing + +**Before (stdlib):** +```go +func TestOld(t *testing.T) { + result := Calculate(5) + if result != 10 { + t.Errorf("Expected 10, got %d", result) + } + if len(items) == 0 { + t.Error("Expected non-empty items") + } +} +``` + +**After (testify):** +```go +func TestNew(t *testing.T) { + a := assert.New(t) + + result := Calculate(5) + a.Equal(10, result) + a.NotEmpty(items) +} +``` + +**Benefits:** +- More readable - assertions read like English +- Better error messages - shows expected vs actual automatically +- Less boilerplate - no manual formatting +- More assertions - Contains, ElementsMatch, JSONEq, etc. diff --git a/docs/doc-site/examples/TUTORIAL.md b/docs/doc-site/examples/TUTORIAL.md new file mode 100644 index 000000000..ace8f93f4 --- /dev/null +++ b/docs/doc-site/examples/TUTORIAL.md @@ -0,0 +1,645 @@ +--- +title: "Tutorial" +description: "How to write great tests with go" +weight: 10 +--- + +## What makes a good test? + +A good test is: + +1. **Focused** - Tests one logical concept +2. **Independent** - Can run in any order, in parallel +3. **Repeatable** - Same input always produces same result +4. **Fast** - Runs quickly to encourage frequent execution +5. **Have clear expectations** - Failure messages immediately show what broke + +**With testify, you write tests that read like documentation:** + +```go +func TestUserCreation(t *testing.T) { + user := CreateUser("alice@example.com") + + require.NotNil(t, user) + assert.Equal(t, "alice@example.com", user.Email) // if user is nil, will fail and stop before + assert.True(t, user.Active) +} +``` +{{% notice style="tip" title="tip" icon="meteor" %}} +Adopt a test layout similar to your functionality. + +```sh +# ❌ Don't do this - confusing +boolean.go +file.go +all_test.go +``` + +```go +// ✅ Better - clear mapping between features and tests +boolean.go +boolean_test.go +file.go +file_test.go +``` +{{% /notice %}} + +The assertions are self-documenting - you can read the test and immediately understand what behavior is being verified. + +--- + +## Patterns + +### Simple test logic + +Oftentimes, much of the test logic can be replaced by a proper use of `require`. + +```go +// ❌ Don't do this - repetitive and hard to maintain +func TestUserCreation(t *testing.T) { + user := CreateUser("alice@example.com") + + if assert.NotNil(t, user) { + assert.Equal(t, "alice@example.com", user.Email) // if user is nil, will skip this test + assert.True(t, user.Active) + } +} +``` + +```go +// ✅ Better - linear flow, no indented subcases +func TestUserCreation(t *testing.T) { + user := CreateUser("alice@example.com") + + require.NotNil(t, user) + assert.Equal(t, "alice@example.com", user.Email) // if user is nil, will fail and stop before + assert.True(t, user.Active) +} +``` + +### Table-Driven Tests with Iterator Pattern + +The **iterator pattern** is the idiomatic way to write table-driven tests in Go 1.23+. This repository uses it extensively, and you should too. + +#### Why Table-Driven Tests? + +Instead of writing separate test functions for each case: + +```go +// ❌ Don't do this - repetitive and hard to maintain +func TestAdd_PositiveNumbers(t *testing.T) { + result := Add(2, 3) + assert.Equal(t, 5, result) +} + +func TestAdd_NegativeNumbers(t *testing.T) { + result := Add(-2, -3) + assert.Equal(t, -5, result) +} + +func TestAdd_MixedSigns(t *testing.T) { + result := Add(-2, 3) + assert.Equal(t, 1, result) +} +``` + +Write one test function with multiple cases: + +```go +// ✅ Better - all cases in one place +func TestAdd(t *testing.T) { + // All test cases defined once + // Test logic written once + // Easy to add new cases + for c := range addTestCases() { + t.Run(c.name, func(t *testing.T) { + t.Parallel() + + result := Add(c.a, c.b) + assert.Equal(t, c.expected, result) + }) + } +} + +func addTestCases() iter.Seq[addTestCase] { + ... +} +``` + +#### The Iterator Pattern + +**Structure:** + +```go +import ( + "iter" + "slices" + "testing" + "github.com/go-openapi/testify/v2/assert" +) + +// 1. Define a test case struct +type addTestCase struct { + name string + a, b int + expected int +} + +// 2. Create an iterator function returning iter.Seq[T] +func addTestCases() iter.Seq[addTestCase] { + return slices.Values([]addTestCase{ + { + name: "positive numbers", + a: 2, + b: 3, + expected: 5, + }, + { + name: "negative numbers", + a: -2, + b: -3, + expected: -5, + }, + { + name: "mixed signs", + a: -2, + b: 3, + expected: 1, + }, + { + name: "with zero", + a: 0, + b: 5, + expected: 5, + }, + }) +} + +// 3. Test function iterates over cases using range +func TestAdd(t *testing.T) { + t.Parallel() + + for c := range addTestCases() { + t.Run(c.name, func(t *testing.T) { + t.Parallel() + + result := Add(c.a, c.b) + assert.Equal(t, c.expected, result) + }) + } +} +``` + +#### Why This Pattern Is Better + +**Clean separation of concerns:** +- Test data (in iterator function) separate from test logic (in test function) +- Easy to see all test cases at a glance +- Easy to add new cases without touching test logic + +**Type safety:** +- Compiler enforces struct fields +- No risk of wrong number of arguments +- IDE autocomplete works perfectly + +**Excellent for parallel execution:** +- Both the outer test and subtests can run in parallel +- `t.Parallel()` catches race conditions early + +**Reusable:** +- Iterator functions can be reused across multiple test functions +- Share test cases between related tests + +**Maintainable:** +- Adding a case: just append to the slice +- Changing test logic: edit one place +- Renaming fields: IDE refactoring works + +#### Comparison with Traditional Pattern + +**Traditional inline pattern:** + +```go +func TestAdd(t *testing.T) { + tests := []struct { + name string + a, b int + expected int + }{ + {"positive", 2, 3, 5}, + {"negative", -2, -3, -5}, + // Test data mixed with test function + // Hard to reuse + // No named fields - order matters + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := Add(tt.a, tt.b) + assert.Equal(t, tt.expected, result) + }) + } +} +``` + +**Iterator pattern:** + +```go +// Test logic separate and clean +func TestAdd(t *testing.T) { + t.Parallel() + for c := range addTestCases() { // Clean iteration + // ... + } +} + +type addTestCase struct { + name string + a, b int + expected int +} + +// Test data in separate function - clean, reusable +func addTestCases() iter.Seq[addTestCase] { + return slices.Values([]addTestCase{ + { + name: "positive numbers", // Named fields + a: 2, // Self-documenting + b: 3, + expected: 5, + }, + // More cases... + }) +} +``` + +#### When to Use Iterator Pattern + +**Always use it for:** +- Any test with 2+ test cases +- Tests requiring complex setup per case +- Tests that benefit from parallel execution +- Any table-driven test scenario + +**Example - complex setup:** + +```go +func TestUserValidation(t *testing.T) { + t.Parallel() + + for c := range userValidationCases() { + t.Run(c.name, func(t *testing.T) { + t.Parallel() + + err := ValidateUser(c.user) + + if c.shouldErr { + assert.Error(t, err) + assert.ErrorContains(t, err, c.errMsg) + } else { + assert.NoError(t, err) + } + }) + } +} + +type userValidationCase struct { + name string + user User + shouldErr bool + errMsg string +} + +func userValidationCases() iter.Seq[userValidationCase] { + return slices.Values([]userValidationCase{ + { + name: "valid user", + user: User{ + Name: "Alice", + Email: "alice@example.com", + Age: 25, + }, + shouldErr: false, + }, + { + name: "missing email", + user: User{ + Name: "Bob", + Age: 30, + }, + shouldErr: true, + errMsg: "email is required", + }, + { + name: "invalid age", + user: User{ + Name: "Charlie", + Email: "charlie@example.com", + Age: -5, + }, + shouldErr: true, + errMsg: "age must be positive", + }, + }) +} +``` + +--- + +### Using testify with Iterator Pattern + +The iterator pattern works beautifully with testify's forward methods: + +```go +func TestUserOperations(t *testing.T) { + t.Parallel() + + for c := range userOperationCases() { + t.Run(c.name, func(t *testing.T) { + t.Parallel() + a := assert.New(t) // Forward assertion object + + user := PerformOperation(c.input) + + // Clean assertions without repeating 't' + a.NotNil(user) + a.Equal(c.expectedName, user.Name) + a.Greater(user.ID, 0) + }) + } +} +``` + +--- + +### Helper Functions with t.Helper() + +When extracting common assertions into helper functions, use `t.Helper()` to get better error messages: + +```go +func assertUserValid(t *testing.T, user *User) { + t.Helper() // Makes test failures point to the caller + + assert.NotNil(t, user) + assert.NotEmpty(t, user.Name) + assert.NotEmpty(t, user.Email) + assert.Greater(t, user.Age, 0) +} + +func TestUserCreation(t *testing.T) { + user := CreateUser("alice@example.com") + + // If this fails, error points HERE, not inside assertUserValid + assertUserValid(t, user) +} +``` + +Without `t.Helper()`, failures would show the line number inside `assertUserValid`, making it harder to find the actual failing test. + +--- + +### Parallel Test Execution + +Always use `t.Parallel()` unless you have a specific reason not to: + +```go +func TestAdd(t *testing.T) { + t.Parallel() // Outer test runs in parallel + + for c := range addTestCases() { + t.Run(c.name, func(t *testing.T) { + t.Parallel() // Each subtest runs in parallel + + result := Add(c.a, c.b) + assert.Equal(t, c.expected, result) + }) + } +} +``` + +**Benefits:** +- Tests run faster +- Catches race conditions and shared state bugs +- Encourages writing independent tests + +**When NOT to use parallel:** +- Tests that modify global state +- Tests that use the same external resource (file, database, etc.) +- Integration tests with shared setup + +--- + +### Setup and Teardown + +Use `defer` for cleanup: + +```go +func TestDatabaseOperations(t *testing.T) { + db := setupTestDatabase(t) + t.Cleanup(func() { + _ = db.Close() // Always runs, even if test fails + } + + user := &User{Name: "Alice"} + err := db.Save(user) + require.NoError(t, err) // Stop if save fails + + loaded, err := db.Find(user.ID) + require.NoError(t, err) + assert.Equal(t, "Alice", loaded.Name) +} +``` + +**Pattern for resources:** +1. Create resource +2. Immediately defer cleanup +3. Use the resource +4. Cleanup happens automatically + +--- + +### Edge Cases to Test + +Always include these test categories: + +#### 1. Empty/Zero Values + +```go +{ + name: "empty string", + input: "", + expected: defaultValue, +}, +{ + name: "nil slice", + input: nil, + expected: emptyResult, +}, +``` + +#### 2. Single Element + +```go +{ + name: "single item", + input: []string{"only"}, + expected: "only", +}, +``` + +#### 3. Multiple Elements + +```go +{ + name: "multiple items", + input: []string{"first", "second", "third"}, + expected: "first,second,third", +}, +``` + +#### 4. Boundary Conditions + +```go +{ + name: "maximum value", + input: math.MaxInt64, + expected: overflow, +}, +{ + name: "special characters", + input: "hello@#$%world", + expected: sanitized, +}, +``` + +--- + +### Testing Errors + +**Bad practice - checking error string:** + +```go +// ❌ Fragile - breaks if error message changes +if err == nil || err.Error() != "division by zero" { + t.Error("wrong error") +} +``` + +**Good practice - checking error chain:** + +```go +// ✅ Semantic error checking +assert.Error(t, err) +assert.ErrorContains(t, err, "division") + +// ✅ Check error type (possibly wrapped) +assert.ErrorIs(t, err, ErrDivisionByZero) + +// ✅ Check for specific error message +assert.EqualError(t, err, "division by zero") +``` + +--- + +### Complete Example + +Here's a complete example showing all patterns together: + +```go +package calculator_test + +import ( + "iter" + "slices" + "testing" + "github.com/go-openapi/testify/v2/assert" + "github.com/go-openapi/testify/v2/require" +) + +type divideTestCase struct { + name string + a, b float64 + expected float64 + shouldErr bool +} + +func divideTestCases() iter.Seq[divideTestCase] { + return slices.Values([]divideTestCase{ + { + name: "positive numbers", + a: 10, + b: 2, + expected: 5, + shouldErr: false, + }, + { + name: "negative dividend", + a: -10, + b: 2, + expected: -5, + shouldErr: false, + }, + { + name: "division by zero", + a: 10, + b: 0, + shouldErr: true, + }, + { + name: "zero dividend", + a: 0, + b: 5, + expected: 0, + shouldErr: false, + }, + }) +} + +func TestDivide(t *testing.T) { + t.Parallel() + + for c := range divideTestCases() { + t.Run(c.name, func(t *testing.T) { + t.Parallel() + + result, err := Divide(c.a, c.b) + + if c.shouldErr { + assert.Error(t, err) + assert.ErrorIs(t, err, ErrDivisionByZero) + } else { + require.NoError(t, err) + assert.Equal(t, c.expected, result) + } + }) + } +} +``` + +--- + +## Best Practices Summary + +1. **Use the iterator pattern** - `iter.Seq[T]` for all table-driven tests +2. **Separate test data from test logic** - Iterator functions are your test data +3. **Use `t.Parallel()`** - Both outer tests and subtests +4. **Use `t.Helper()`** - In assertion helper functions +5. **Use `require` for preconditions** - Stop test if setup fails +6. **Use `assert` for checks** - See all failures +7. **Test edge cases** - Empty, single, multiple, boundary conditions +8. **Use forward methods** - `assert.New(t)` for tests with many assertions +9. **Use semantic error checking** - `ErrorIs`, `ErrorContains`, not string comparison +10. **Keep tests focused** - One logical concept per test + +--- + +## Examples in This Repository + +See real-world usage of these patterns: + +- **Iterator Pattern**: `codegen/internal/generator/funcmaps/funcmaps_test.go` +- **Domain Tests**: `codegen/internal/generator/domains/domains_test.go` +- **Assertion Tests**: `internal/assertions/*_test.go` +- **Comprehensive Coverage**: `codegen/internal/scanner/comments-parser/` (all test files) + +**Study these to see the patterns in action!** diff --git a/docs/doc-site/examples/_index.md b/docs/doc-site/examples/_index.md new file mode 100644 index 000000000..6cb17a773 --- /dev/null +++ b/docs/doc-site/examples/_index.md @@ -0,0 +1,7 @@ +--- +title: Examples +description: Examples and tutorials. +weight: 2 +--- + +{{< children type="card" description="true" >}} diff --git a/docs/doc-site/keywords/_index.md b/docs/doc-site/keywords/_index.md new file mode 100644 index 000000000..0e748fefe --- /dev/null +++ b/docs/doc-site/keywords/_index.md @@ -0,0 +1,9 @@ +--- +#type: taxonomy +title: Methods +params: + children: + type: card +--- +Example +{{% children sort="title" %}} diff --git a/docs/doc-site/project/LICENSE.md b/docs/doc-site/project/LICENSE.md new file mode 100644 index 000000000..cfc0d4c38 --- /dev/null +++ b/docs/doc-site/project/LICENSE.md @@ -0,0 +1,208 @@ +--- +title: LICENSE +description: Apache-2.0 License +weight: 10 +--- +``` + 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/docs/doc-site/project/NOTICE.md b/docs/doc-site/project/NOTICE.md new file mode 100644 index 000000000..3edd10e28 --- /dev/null +++ b/docs/doc-site/project/NOTICE.md @@ -0,0 +1,104 @@ +--- +title: NOTICE +description: Code attribution and other LICENSE terms +weight: 11 +--- +``` +// SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + +This software library, github.com/go-openapi/testify, includes software developed +by the go-swagger and go-openapi maintainers ("go-swagger maintainers"). + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this software except in compliance with the License. +You may obtain a copy of the License at + +This software is copied from, derived from, and inspired mainly by github.com/stretchr/testify. +It ships with copies of other software which license terms are recalled below. + + +github.com/stretchr/testify +=========================== + +// SPDX-FileCopyrightText: Copyright (c) 2012-2020 Mat Ryer, Tyler Bunnell and contributors. +// SPDX-License-Identifier: MIT + +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. + +github.com/davecgh/go-spew +=========================== + +// SPDX-FileCopyrightText: Copyright (c) 2012-2016 Dave Collins +// SPDX-License-Identifier: ISC + +ISC License + +Copyright (c) 2012-2016 Dave Collins + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +github.com/pmezard/go-difflib +=========================== + +// SPDX-FileCopyrightText: Copyright (c) 2013, Patrick Mezard +// SPDX-License-Identifier: MIT-like + +Copyright (c) 2013, Patrick Mezard +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + The names of its contributors may not be used to endorse or promote +products derived from this software without specific prior written +permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` diff --git a/docs/doc-site/project/README.md b/docs/doc-site/project/README.md new file mode 100644 index 000000000..f348e5056 --- /dev/null +++ b/docs/doc-site/project/README.md @@ -0,0 +1,161 @@ +--- +title: README +description: | + Introducing `go-openapi/testify/v2`. + + - Approach + - Main features + - Differences with v1 +weight: 1 +--- + +## Motivation + +From the maintainers of `testify`, it looks like a v2 is coming up, but they'll do it at their own pace. + +We like all the principles they put forward to build this v2. [See discussion about v2](https://github.com/stretchr/testify/discussions/1560) + +However, at `go-openapi` we would like to address the well-known issues in `testify` with different priorities. + +--- + +1. We want first to remove all external dependencies. + +> For all our libraries and generated test code we don't want test dependencies +> to drill farther than `import github.com/go-openapi/testify/v2`, but on some specific (and controlled) +> occasions. +> +> In this fork, all external stuff is either internalized (`go-spew`, `difflib`), +> removed (`mocks`, `suite`, `http`) or specifically enabled by importing a specific module +> (`github.com/go-openapi/testify/v2/enable/yaml`). + +2. Make it easy to maintain and extend. + +> For go-openapi, testify should just be yet another part of our toolkit. +> We need it to work, be easily adaptable to our needs and not divert our development effort away from our other repos. +> This big refactor is an investment that has to pay off. + +3. We want to pare down some of the chrome that has been added over the years + +> The `go-openapi` libraries and the `go-swagger` project make a rather limited use of the vast API provided by `testify`. +> +> With this first version of the fork, we have removed `mocks` and `suite`, which we don't use. +> They might be added later on, with better controlled dependencies. +> +> In the forthcoming maintenance of this fork, much of the "chrome" or "ambiguous" API will be pared down. +> There is no commitment yet on the stability of the API. +> +> Chrome would be added later: we have the "enable" packages just for that. + +4. We hope that this endeavor will help the original project with a live-drill of what a v2 could look like. + We are always happy to discuss with people who face the same problems as we do: avoid breaking changes, + APIs that became bloated over a decade or so, uncontrolled dependencies, conflicting demands from users etc. + +### The approach with this fork + +This fork targets **go1.24**. + +* [x] **zero** external dependencies by default +* [x] extra features (and dependencies) are opt-in +* [x] **modernized** code base +* [x] **simplified** maintenance +* [x] can add or remove assertions with ease +* [x] **mitigated API bloat** with comprehensive domain-indexed documentation +* [ ] can leverage generics without backward compatibility concerns + +The approach will be **selective and pragmatic** rather than comprehensive: + +* **Targeted improvements** where generics provide clear value without compromising existing functionality +* **Focus on eliminating anti-patterns** like dummy value instantiation in `IsType` (see #1805) +* **Preserve reflection-based flexibility** for comparing complex types rather than forcing everything through generic constraints +* **Careful constraint design** to ensure type safety without being overly restrictive or permissive + +The goal is to enhance type safety and developer experience where it matters most, +while maintaining the flexibility that makes testify useful for real-world testing scenarios. + +## Breaking changes from v1 + +* `YAMLEq` panics by default: must enable the feature with an additional blank import +* deprecated types and methods have been removed +* removed the `suite` and `http` packages +* replaced internal utility package `_codegen` by `codegen` + +### Other (non-breaking) changes + +* added `JSONEqBytes` +* adapted & merged fixes: see [ROADMAP](./maintainers/ROADMAP.md). + +## Usage at go-openapi + +At this moment, we have identified the following usage in our tools. This API shall remain stable. +Currently, there are no guarantees about the entry points not in this list. + +``` +Condition +Contains,Containsf +Empty,Emptyf +Equal,Equalf +EqualError,EqualErrorf +EqualValues,EqualValuesf +Error,Errorf +ErrorContains +ErrorIs +Fail,Failf +FailNow +False,Falsef +Greater +Implements +InDelta,InDeltaf +IsType,IsTypef +JSONEq,JSONEqf +Len,Lenf +Nil,Nilf +NoError,NoErrorf +NotContains,NotContainsf +NotEmpty,NotEmptyf +NotEqual +NotNil,NotNilf +NotPanics +NotZeroG +Panics,PanicsWithValue +Subset +True,Truef +YAMLEq,YAMLEqf +Zero,Zerof +``` + + +## [The original README](./maintainers/ORIGINAL.md) + + +[test-badge]: https://github.com/go-openapi/testify/actions/workflows/go-test.yml/badge.svg +[test-url]: https://github.com/go-openapi/testify/actions/workflows/go-test.yml +[cov-badge]: https://codecov.io/gh/go-openapi/testify/branch/master/graph/badge.svg +[cov-url]: https://codecov.io/gh/go-openapi/testify +[vuln-scan-badge]: https://github.com/go-openapi/testify/actions/workflows/scanner.yml/badge.svg +[vuln-scan-url]: https://github.com/go-openapi/testify/actions/workflows/scanner.yml +[codeql-badge]: https://github.com/go-openapi/testify/actions/workflows/codeql.yml/badge.svg +[codeql-url]: https://github.com/go-openapi/testify/actions/workflows/codeql.yml + +[release-badge]: https://badge.fury.io/go/github.com%2Fgo-openapi%2Ftestify.svg +[release-url]: https://badge.fury.io/go/github.com%2Fgo-openapi%2Ftestify + +[gocard-badge]: https://goreportcard.com/badge/github.com/go-openapi/testify +[gocard-url]: https://goreportcard.com/report/github.com/go-openapi/testify +[codefactor-badge]: https://img.shields.io/codefactor/grade/github/go-openapi/testify +[codefactor-url]: https://www.codefactor.io/repository/github/go-openapi/testify + +[doc-badge]: https://img.shields.io/badge/doc-site-blue?link=https%3A%2F%2Fgoswagger.io%2Fgo-openapi%2F +[doc-url]: https://goswagger.io/go-openapi +[godoc-badge]: https://pkg.go.dev/badge/github.com/go-openapi/testify +[godoc-url]: http://pkg.go.dev/github.com/go-openapi/testify +[slack-badge]: https://slackin.goswagger.io/badge.svg +[slack-url]: https://slackin.goswagger.io + +[license-badge]: http://img.shields.io/badge/license-Apache%20v2-orange.svg +[license-url]: https://github.com/go-openapi/testify/?tab=Apache-2.0-1-ov-file#readme + +[goversion-badge]: https://img.shields.io/github/go-mod/go-version/go-openapi/testify +[goversion-url]: https://github.com/go-openapi/testify/blob/master/go.mod +[top-badge]: https://img.shields.io/github/languages/top/go-openapi/testify +[commits-badge]: https://img.shields.io/github/commits-since/go-openapi/testify/latest diff --git a/docs/doc-site/project/SECURITY.md b/docs/doc-site/project/SECURITY.md new file mode 100644 index 000000000..d31375250 --- /dev/null +++ b/docs/doc-site/project/SECURITY.md @@ -0,0 +1,43 @@ +--- +title: Security Policy +description: | + This document outlines the commitment and practices of the go-openapi maintainers regarding security +weight: 2 +--- + +This policy outlines the commitment and practices of the go-openapi maintainers regarding security. + +## Supported Versions + +| Version | Supported | +| ------- | ------------------ | +| 2.0.x | :white_check_mark: | + +## Vulnerability checks in place + +This repository uses automated vulnerability scans, at every merged commit and at least once a week. + +We use: + +* [`GitHub CodeQL`][codeql-url] +* [`trivy`][trivy-url] +* [`govulncheck`][govulncheck-url] + +Reports are centralized in github security reports and visible to the maintainers. + +## Reporting a vulnerability + +If you become aware of a security vulnerability that affects the current repository, +**please report it privately to the maintainers** +rather than opening a publicly visible GitHub issue. + +Please follow the instructions provided by github to [Privately report a security vulnerability][github-guidance-url]. + +{{% notice primary "TL;DR" "meteor" %}} +> On Github, navigate to the project's "Security" tab then click on "Report a vulnerability". +{{% /notice %}} + +[codeql-url]: https://github.com/github/codeql +[trivy-url]: https://trivy.dev/docs/latest/getting-started +[govulncheck-url]: https://go.dev/blog/govulncheck +[github-guidance-url]: https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability#privately-reporting-a-security-vulnerability diff --git a/docs/doc-site/project/_index.md b/docs/doc-site/project/_index.md new file mode 100644 index 000000000..e2e58c913 --- /dev/null +++ b/docs/doc-site/project/_index.md @@ -0,0 +1,12 @@ +--- +title: "Project Documentation" +weight: 3 +description: | + Project roadmap, contributing guidelines and maintainer's guide. +--- + +Useful guides for contributors and maintainers. + +--- + +{{< children type="card" description="true" >}} diff --git a/docs/doc-site/project/contributing/CODE_OF_CONDUCT.md b/docs/doc-site/project/contributing/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..bc9e72c7c --- /dev/null +++ b/docs/doc-site/project/contributing/CODE_OF_CONDUCT.md @@ -0,0 +1,78 @@ +--- +title: Code Of Conduct +description: Contributor Covenant Code of Conduct +weight: 11 +--- + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, gender identity and expression, level of experience, +nationality, personal appearance, race, religion, or sexual identity and +orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or +advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at ivan+abuse@flanders.co.nz. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at [https://contributor-covenant.org/version/1/4][version] + +[homepage]: https://contributor-covenant.org +[version]: https://contributor-covenant.org/version/1/4/ diff --git a/docs/doc-site/project/contributing/CONTRIBUTING.md b/docs/doc-site/project/contributing/CONTRIBUTING.md new file mode 100644 index 000000000..6ca50d1a6 --- /dev/null +++ b/docs/doc-site/project/contributing/CONTRIBUTING.md @@ -0,0 +1,287 @@ +--- +title: Contributing Guidelines +description: | + You'll find here general guidelines to contribute to this project. + + There are many ways to help this project. + + Contributing code is only one of those... +weight: 1 +--- +You'll find here general guidelines to contribute to this project. +They mostly correspond to standard practices for open source repositories. + +We have tried to keep things as simple as possible. + +{{% notice primary "TL;DR" "meteor" %}} +> If you're an experienced go developer on github, then you should just feel at home with us +> and you may well skip the rest of this document. +> +> You'll essentially apply the usual guidelines for a go library project on github. +{{% /notice %}} + +These guidelines are common to all libraries published on github by the `go-openapi` organization, +so you'll feel at home with any of our projects. + +You'll find more detailed (or repo-specific) instructions in the [maintainer's docs][maintainers-doc]. + +## How can I contribute? + +{{% tab title="There are many ways to contribute" %}} +There are many ways in which you can contribute, not just code. Here are a few ideas: + + * Reporting Issues / Bugs + * Suggesting Improvements + * Documentation + * Art work that makes the project look great + * Code + * bug fixes and new features that are within the main project scope + * improving test coverage + * addressing code quality issues +{{< /tab >}} + +## Questions & issues + +{{< tabs >}} +{{% tab title="Asking questions" %}} +### Asking a question + +You may inquire anything about this library by reporting a "Question" issue on github. + +You may also join our discord server where you may discuss issues or requests. + +[![Discord Server][discord-badge]][discord-url] + +[discord-badge]: https://img.shields.io/discord/1446918742398341256?logo=discord&label=discord&color=blue +[discord-url]: https://discord.gg/DrafRmZx +{{% /tab %}} + +{{% tab title="Reporting issues" %}} +### Reporting issues + +Reporting a problem with our libraries _is_ a valuable contribution. +You can do this on the github issues page of this repository. + +Please be as specific as possible when describing your issue. + +Whenever relevant, please provide information about your environment (go version, OS). + +Adding a code snippet to reproduce the issue is great, and a big time saver for maintainers. +{{% /tab %}} + +{{% tab title="Triaging issues" %}} +### Triaging issues + +You can help triage issues which may include: + +* reproducing bug reports +* asking for important information, such as version numbers or reproduction instructions +* answering questions and sharing your insight in issue comments +{{% /tab %}} +{{< /tabs >}} + +## Code contributions + +{{< tabs >}} +{{% tab title="All pull requests are welcome" %}} +### Pull requests are always welcome + +We are always thrilled to receive pull requests, and we do our best to +process them as fast as possible. + +Not sure if that typo is worth a pull request? Do it! We will appreciate it. + +If your pull request is not accepted on the first try, don't be discouraged! +If there's a problem with the implementation, hopefully you've received feedback on what to improve. + +If you have a lot of ideas or a lot of issues to solve, try to refrain a bit and post focused +pull requests. +Think that they must be reviewed by a maintainer and it is easy to lost track of things on big PRs. + +We're trying very hard to keep the go-openapi packages lean and focused. +Together, these packages constitute a toolkit: it won't do everything for everybody out of the box, +but everybody can use it to do just about everything related to OpenAPI. + +This means that we might decide against incorporating a new feature. + +However, there might be a way to implement that feature *on top of* our libraries. +{{% /tab %}} + +{{% tab title="Environment" %}} +### Environment + +You just need a `go` compiler to be installed. No special tools are needed to work with our libraries. + +The minimal go compiler version required is always the old stable (latest minor go version - 1). + +Our libraries are designed and tested to work on `Linux`, `MacOS` and `Windows`. + +If you're used to work with `go` you should already have everything in place. + +Although not required, you'll be certainly more productive with a local installation of `golangci-lint`, +the meta-linter our CI uses. + +If you don't have it, you may install it like so: + +```sh +go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest +``` +{{% /tab %}} +{{< /tabs >}} + +### Conventions + +{{< tabs >}} +{{% tab title="Git Flow" %}} +#### Git flow +Fork the repo and make changes to your fork in a feature branch. + +To submit a pull request, push your branch to your fork (e.g. `upstream` remote): +github will propose to open a pull request on the original repository. + +Typically you'd follow some common naming conventions: + +- if it's a bugfix branch, name it `fix/XXX-something`where XXX is the number of the + issue on github +- if it's a feature branch, create an enhancement issue to announce your + intentions, and name it `feature/XXX-something` where XXX is the number of the issue. + +> NOTE: we don't enforce naming conventions on branches: it's your fork after all. +{{% /tab %}} + +{{% tab title="Tests" %}} +#### Tests + +Submit unit tests for your changes. + +Go has a great built-in test framework ; use it! + +Take a look at existing tests for inspiration, and run the full test suite on your branch +before submitting a pull request. + +Our CI measures test coverage and the test coverage of every patch. + +Although not a blocking step - because there are so many special cases - +this is an indicator that maintainers consider when approving a PR. +Please try your best to cover at least 80% of your patch. +{{% /tab %}} + +{{% tab title="Code Style" %}} +#### Code style + +You may read our stance on code style [there](./STYLE.md). +{{% /tab %}} + +{{% tab title="Documentation" %}} +#### Documentation + +Don't forget to update the documentation when creating or modifying a feature. + +Most documentation for this library is directly found in code as comments for godoc. + +The documentation for the go-openapi packages is published on [the public go docs site][go-doc]. + +--- + +Check your documentation changes for clarity, concision, and correctness. + +If you want to assess the rendering of your changes when published to `pkg.go.dev`, you may +want to install the `pkgsite` tool proposed by `golang.org`. + +```sh +go install golang.org/x/pkgsite/cmd/pkgsite@latest +``` + +Then run on the repository folder: +```sh +pkgsite . +``` + +This will run a godoc server locally where you may see the documentation generated from your local repository. + +[go-doc]: https://pkg.go.dev/github.com/go-openapi/testify +{{% /tab %}} + +{{% tab title="Commit Messages" %}} +#### Commit messages + +Pull requests descriptions should be as clear as possible and include a +reference to all the issues that they address. + +Pull requests must not contain commits from other users or branches. + +Commit messages are not required to follow the "conventional commit" rule, but it's certainly a good +thing to follow this guideline (e.g. "fix: blah blah", "ci: did this", "feat: did that" ...). + +The title in your commit message is used directly to produce our release notes: try to keep them neat. + +The commit message body should detail your changes. + +If an issue should be closed by a commit, please add this reference in the commit body: + +``` +* fixes #{issue number} +``` +{{% /tab %}} + +{{% tab title="Code Review" %}} +#### Code review + +Code review comments may be added to your pull request. + +Discuss, then make the suggested modifications and push additional commits to your feature branch. + +Be sure to post a comment after pushing. The new commits will show up in the pull +request automatically, but the reviewers will not be notified unless you comment. + +Before the pull request is merged, +**make sure that you've squasheq your commits into logical units of work** +using `git rebase -i` and `git push -f`. + +After every commit the test suite should be passing. + +Include documentation changes in the same commit so that a revert would remove all traces of the feature or fix. +{{% /tab %}} + +{{% tab title="Sign Your Work" %}} +#### Sign your work + +Software is developped by real people. + +The sign-off is a simple line at the end of your commit message, +which certifies that you wrote it or otherwise have the right to +pass it on as an open-source patch. + +We require the simple DCO below with an email signing your commit. +PGP-signed commit are greatly appreciated but not required. + +The rules are pretty simple: + +* read our [DCO][dco-doc] (from [developercertificate.org][dco-source]) +* if you agree with these terms, then you just add a line to every git commit message + + Signed-off-by: Joe Smith + +using your real name (sorry, no pseudonyms or anonymous contributions.) + +You can add the sign-off when creating the git commit via `git commit -s`. + +[dco-doc]: ./DCO.md +[dco-source]: https://developercertificate.org +{{% /tab %}} +{{< /tabs >}} + +## Code contributions by AI agents + +Our agentic friends are welcome to contribute! + +We only have a few demands to keep-up with human maintainers. + +1. Issues and PRs written or posted by agents should always mention the original (human) poster for reference +2. We don't accept PRs attributed to agents. We don't want commits signed like "author: @claude.code". + Agents or bots may coauthor commits, though. +3. Security vulnerability reports by agents should always be reported privately and mention the original (human) poster + (see also [Security Policy][security-doc]). + +[maintainers-doc]: ../maintainers/MAINTAINERS.md +[security-doc]: ../SECURITY.md diff --git a/docs/doc-site/project/contributing/CONTRIBUTORS.md b/docs/doc-site/project/contributing/CONTRIBUTORS.md new file mode 100644 index 000000000..b20229154 --- /dev/null +++ b/docs/doc-site/project/contributing/CONTRIBUTORS.md @@ -0,0 +1,18 @@ +--- +title: Contributors +description: Acknowledgements to all-time contributors +weight: 10 +--- + +# 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/docs/doc-site/project/contributing/DCO.md b/docs/doc-site/project/contributing/DCO.md new file mode 100644 index 000000000..06b35e8f9 --- /dev/null +++ b/docs/doc-site/project/contributing/DCO.md @@ -0,0 +1,72 @@ +--- +title: Developer's Certificate of Origin +description: Signing your work +weight: 2 +--- +{{% notice style="primary" title="TL;DR" icon="pen-nib" %}} +We welcome this (simple email signature): +``` +Author: Frederic BIDON +Date: Mon Dec 15 11:24:43 2025 +0100 + + feat: brainstrom RAG-equipped linter + + Signed-off-by: Frederic BIDON +``` + +or this (PGP-signed commit): + +``` +gpg: Signature made Tue 30 Dec 2025 09:53:48 PM CET +gpg: using RSA key XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +gpg: Good signature from "Frédéric BIDON " +Author: Frederic BIDON +Date: Tue Dec 30 21:53:48 2025 +0100 + + tentative project doc site + + Signed-off-by: Frederic BIDON +``` + +{{% /notice %}} + +From : + +``` +Developer Certificate of Origin +Version 1.1 + +Copyright (C) 2004, 2006 The Linux Foundation and its contributors. +660 York Street, Suite 102, +San Francisco, CA 94110 USA + +Everyone is permitted to copy and distribute verbatim copies of this +license document, but changing it is not allowed. + + +Developer's Certificate of Origin 1.1 + +By making a contribution to this project, I certify that: + +(a) The contribution was created in whole or in part by me and I + have the right to submit it under the open source license + indicated in the file; or + +(b) The contribution is based upon previous work that, to the best + of my knowledge, is covered under an appropriate open source + license and I have the right under that license to submit that + work with modifications, whether created in whole or in part + by me, under the same open source license (unless I am + permitted to submit under a different license), as indicated + in the file; or + +(c) The contribution was provided directly to me by some other + person who certified (a), (b) or (c) and I have not modified + it. + +(d) I understand and agree that this project and the contribution + are public and that a record of the contribution (including all + personal information I submit with it, including my sign-off) is + maintained indefinitely and may be redistributed consistent with + this project or the open source license(s) involved. +``` diff --git a/docs/doc-site/project/contributing/STYLE.md b/docs/doc-site/project/contributing/STYLE.md new file mode 100644 index 000000000..145f817ca --- /dev/null +++ b/docs/doc-site/project/contributing/STYLE.md @@ -0,0 +1,96 @@ +--- +title: Coding Style +description: Coding style at `go-openapi` +weight: 3 +--- +## Coding style at `go-openapi` + +{{% notice primary "TL;DR" "meteor" %}} +> Let's be honest: at `go-openapi` and `go-swagger` we've never been super-strict on code style and linting. +> +> But perhaps now (2025) is the time to adopt a different stance. +{{% /notice %}} + +Even though our repos have been early adopters of `golangci-lint` years ago +(we used some other metalinter before), our decade-old codebase is only realigned to new rules from time to time. + +Now go-openapi and go-swagger together make up a really large codebase, which is taxing to maintain and keep afloat. + +Code quality and the harmonization of rules have thus become things that we need now. + +## Meta-linter + +Universally formatted go code promotes ease of writing, reading, and maintenance. + +You should run `golangci-lint run` before committing your changes. + +Many editors have plugins that do that automatically. + +> We use the `golangci-lint` meta-linter. The configuration lies in +> [`.golangci.yml`]([golangci.yml]). +> You may read [the linter's configuration reference][golangci-doc] for additional reference. + +## Linting rules posture + +Thanks to go's original design, we developers don't have to waste much time arguing about code figures of style. + +However, the number of available linters has been growing to the point that we need to pick a choice. + +We enable all linters published by `golangci-lint` by default, then disable a few ones. + +{{% tab title="Disabled linters" %}} +Here are the reasons why they are disabled (update: Nov. 2025, `golangci-lint v2.6.1`). + +```yaml + disable: + - depguard # we don't want to configure rules to constrain import. That's the reviewer's job + - exhaustruct # we don't want to configure regexp's to check type name. That's the reviewer's job + - funlen # we accept cognitive complexity as a meaningful metric, but function length is relevant + - godox # we don't see any value in forbidding TODO's etc in code + - nlreturn # we usually apply this "blank line" rule to make code less compact. We just don't want to enforce it + - nonamedreturns # we don't see any valid reason why we couldn't used named returns + - noinlineerr # there is no value added forbidding inlined err + - paralleltest # we like parallel tests. We just don't want them to be enforced everywhere + - recvcheck # we like the idea of having pointer and non-pointer receivers + - testpackage # we like test packages. We just don't want them to be enforced everywhere + - tparallel # see paralleltest + - varnamelen # sometimes, we like short variables. The linter doesn't catch cases when a short name is good + - whitespace # no added value + - wrapcheck # although there is some sense with this linter's general idea, it produces too much noise + - wsl # no added value. Noise + - wsl_v5 # no added value. Noise +``` + +As you may see, we agree with the objective of most linters, at least the principle they are supposed to enforce. +But all linters do not support fine-grained tuning to tolerate some cases and not some others. +{{< /tab >}} + +{{% tab title="Relaxed linter settings" %}} +When this is possible, we enable linters with relaxed constraints. + +```yaml + settings: + dupl: + threshold: 200 # in a older code base such as ours, we have to be tolerant with a little redundancy + # Hopefully, we'll be able to gradually get rid of those. + goconst: + min-len: 2 + min-occurrences: 3 + cyclop: + max-complexity: 20 # the default is too low for most of our functions. 20 is a nicer trade-off + gocyclo: + min-complexity: 20 + exhaustive: # when using default in switch, this should be good enough + default-signifies-exhaustive: true + default-case-required: true + lll: + line-length: 180 # we just want to avoid extremely long lines. + # It is no big deal if a line or two don't fit on your terminal. +``` +{{< /tab >}} + +Final note: since we have switched to a forked version of `stretchr/testify`, +we no longer benefit from the great `testifylint` linter for tests. + +[golangci.yml]: https://github.com/go-openapi/testify/blob/master/.golangci.yml +[golangci-doc]: https://golangci-lint.run/docs/linters/configuration/ diff --git a/docs/doc-site/project/contributing/_index.md b/docs/doc-site/project/contributing/_index.md new file mode 100644 index 000000000..0a53f1776 --- /dev/null +++ b/docs/doc-site/project/contributing/_index.md @@ -0,0 +1,11 @@ +--- +title: "Contributing to Testify v2" +description: "How can I contribute to this project?" +weight: 3 +--- + +How to contribute to [https://github.com/go-openapi/testify][repo-url]? + +{{< children type="card" description="true" >}} + +[repo-url]: https://github.com/go-openapi/testify diff --git a/docs/doc-site/project/maintainers/ARCHITECTURE.md b/docs/doc-site/project/maintainers/ARCHITECTURE.md new file mode 100644 index 000000000..d6ee57dd1 --- /dev/null +++ b/docs/doc-site/project/maintainers/ARCHITECTURE.md @@ -0,0 +1,49 @@ +--- +title: "Architecture" +description: Project structure +weight: 2 +--- + +## Architecture Overview + +{{< mermaid align="center" zoom="true" >}} +graph LR; + classDef event font-size:small,font-family:Monospace; + trigger@{ shape: rounded, label: "go generate" } + codegen[["🛠️ codegen"]] + docs@{ shape: documents, label: "📚 API docs"} + source[["📦 internal/assertions + (source of truth)"]] + + trigger:::event -.-> codegen + source --> codegen --> assert + source --> codegen --> require + codegen --> docs +{{< /mermaid >}} + +**Single Source of Truth: `internal/assertions/`** + +All assertion implementations live in `internal/assertions/`, organized by domain: +- Functions are implemented once with comprehensive tests +- Doc comments include "Examples:" sections that drive test generation (including testable examples) +- Both `assert/` and `require/` packages are 100% generated from this source + +**Code Generator: `codegen/`** + +The generator scans `internal/assertions/` and produces: +- n assertion functions × 8 variants = 608 generated functions (current: n=76) +- Package-level functions (`assert.Equal`, `require.Equal`) +- Format variants (`assert.Equalf`, `require.Equalf`) +- Forward methods (`a.Equal()`, `r.Equal()`) +- Tests for all variants +- Testable examples for godoc +- Documentation for documentation site, organized by domain + +**Generated Packages: `assert/` and `require/`** + +Everything in these packages is generated. Never edit generated files directly. + +Exceptions: +* `doc.go` is not generated +* ad'hoc testable examples are not generated +* the `assert` package contains an `enable` package to enable features. This is not generated. diff --git a/docs/doc-site/project/maintainers/CODEGEN.md b/docs/doc-site/project/maintainers/CODEGEN.md new file mode 100644 index 000000000..001eb1769 --- /dev/null +++ b/docs/doc-site/project/maintainers/CODEGEN.md @@ -0,0 +1,285 @@ +--- +title: Code Generation +description: Code generation workflow and maintenance. +weight: 3 +--- + +## Maintaining Generated Code + +This repository uses code generation extensively to maintain consistency across assertion packages. + +### Code Generation Pipeline + +{{< mermaid align="center" zoom="true" >}} + graph TD + source["📦 internal/assertions/*.go"] + scanner["🔍 Scanner + go/packages + go/types"] + model["fa:fa-database + Model data structures"] + templates["📝 Templates + Go text/template"] + outputs["📤 Generated Code"] + + source --> scanner + scanner --> extract_metadata + extract_metadata --> model + model --> templates + templates --> outputs + + subgraph extract_metadata["Extract Metadata"] + direction BT + extract["Extractor"] + comments["godoc comments"] --o extract + examples["examples: values comments"] --o extract + domains["domain tags"] --o extract + sigs["Function signatures"] --o extract + sigs["Other internal annotations comments"] --o extract + end + + outputs -.-> assert_package + outputs -.-> require_package + outputs -.-> docs@{shape: documents, label: "docs/doc-site/**/*.md"} + + subgraph assert_package + direction BT + assert@{shape: documents, label: "assert/*.go"} + tests_assert["*_test.go files"] --o assert + example_tests_assert["*_examples_test.go files"] --o assert + + subgraph not_generated_assert["*not generated*"] + direction LR + docgo_assert@{ shape: document, label: "doc.go" } + adhoc_assert@{ shape: document, label: "*_adhoc*_test.go" } + enable_assert@{ shape: lin-doc, label: "enable" } + end + end + + subgraph require_package + direction BT + + require@{shape: documents, label: "require/*.go"} + tests_require["*_test.go files"] --o require + example_tests_require["*_examples_test.go files"] --o require + + subgraph not_generated_require["*not generated*"] + direction LR + docgo_require@{ shape: document, label: "doc.go" } + adhoc_require@{ shape: document, label: "*_adhoc*_test.go" } + end + end + + style not_generated_assert fill:#4a9eff,color:#fff + style not_generated_require fill:#4a9eff,color:#fff +{{< /mermaid >}} + +> The generator scans source code, extracts metadata, builds a model, and applies templates to generate ~600+ functions, tests, and documentation from ~70-80+ source functions. + +--- + +### Adding a New Assertion + +**Complete workflow:** + +1. **Add function to `internal/assertions/.go`:** + + The following example would like go to `string.go`, next to the `Regexp` assertion. + + ```go + // StartsWith asserts that the string starts with the given prefix. + // + // Examples: + // + // success: "hello world", "hello" + // failure: "hello world", "bye" + func StartsWith(t T, str, prefix string, msgAndArgs ...any) bool { + if h, ok := t.(H); ok { + h.Helper() + } + if !strings.HasPrefix(str, prefix) { + return Fail(t, fmt.Sprintf("Expected %q to start with %q", str, prefix), msgAndArgs...) + } + return true + } + ``` + +2. **Add tests to `internal/assertions/_test.go`:** + Write comprehensive table-driven tests covering edge cases. + +3. **Run code generation:** + ```bash + go generate ./... + ``` + +4. **Done!** All 8 variants are generated with tests and examples: + - `assert.StartsWith(t, str, prefix)` + - `assert.StartsWithf(t, str, prefix, "msg")` + - `a.StartsWith(str, prefix)` (forward method) + - `a.StartsWithf(str, prefix, "msg")` + - `require.StartsWith(t, str, prefix)` + - `require.StartsWithf(t, str, prefix, "msg")` + - `r.StartsWith(str, prefix)` (forward method) + - `r.StartsWithf(str, prefix, "msg")` + +#### How One Function Becomes Eight + +{{< mermaid align="center" zoom="true" >}} +graph TD + source["1 Source Function + internal/assertions/Equal()"] + + source --> assert_group["assert Package"] + source --> require_group["require Package"] + + assert_group --> assert_pkg["assert.Equal(t, a, b) + package-level"] + assert_group --> assert_fmt["assert.Equalf(t, a, b, msg) + formatted variant"] + assert_group --> assert_fwd["a.Equal(a, b) + forward method"] + assert_group --> assert_fwdfmt["a.Equalf(a, b, msg) + forward + formatted"] + + require_group --> require_pkg["require.Equal(t, a, b) + package-level (fatal)"] + require_group --> require_fmt["require.Equalf(t, a, b, msg) + formatted variant (fatal)"] + require_group --> require_fwd["r.Equal(a, b) + forward method (fatal)"] + require_group --> require_fwdfmt["r.Equalf(a, b, msg) + forward + formatted (fatal)"] + + style source fill:#4a9eff,color:#fff + style assert_group fill:#90ee90,color:#000 + style require_group fill:#ffb6c1,color:#000 +{{< /mermaid >}} + +> **76 functions × 8 variants = 608 generated functions** (plus tests and documentation for each) + +--- + +### Example Annotations Format + +The "Examples:" section in doc comments drives test and example generation: + +```go +// Examples: +// +// success: +// failure: +// panic: +// +``` + +**Rules:** +- Use valid Go expressions that can be directly inserted into test code +- `success:` and `failure:` are required for most assertions +- `panic:` is optional (used for assertions like Panics, YAMLEq) +- Multiple examples of the same type are allowed (e.g., multiple `success:` lines) +- Examples are extracted by the scanner and used to generate: + - Unit tests (success + failure cases) + - Testable examples (success cases only for simplicity) + +**Example with multiple success cases:** +```go +// Examples: +// +// success: []string{"a", "b"}, 2 // slice +// success: map[string]int{"a": 1}, 1 // map +// success: "hello", 5 // string +// failure: []string{"a"}, 5 +``` + +#### Example-Driven Test Generation + +{{< mermaid align="center" zoom="true" >}} + graph LR + doccomment["Doc Comment + with Examples:"] + parser["📖 Example Parser"] + cases["Test Cases + success/failure/panic"] + multiplier["Multiply × 8"] + tests["Generated Tests"] + + doccomment --> parser + parser --> cases + cases --> multiplier + + multiplier --> pkg_assert["assert package test"] + multiplier --> fmt_assert["assert format test"] + multiplier --> fwd_assert["assert forward test"] + multiplier --> fwdfmt_assert["assert fwd+fmt test"] + multiplier --> pkg_require["require package test"] + multiplier --> fmt_require["require format test"] + multiplier --> fwd_require["require forward test"] + multiplier --> fwdfmt_require["require fwd+fmt test"] + + pkg_assert & fmt_assert & fwd_assert & fwdfmt_assert & pkg_require & fmt_require & fwd_require & fwdfmt_require --> tests + + style cases fill:orange,color:black; + style multiplier fill:yellow,color:black; + style tests fill:lightgreen,color:black; +{{< /mermaid >}} + +> Each example in doc comments generates 8 test functions (one per variant), ensuring 100% test coverage of generated code. +> In addition, the generator produces testable examples (somewhat redundant with "passed" tests) so every function gets +> a testable example displayed on pkg.go.dev. + +--- + +### Special Cases in Generated Tests + +For complex assertions requiring special setup, the test templates support conditional logic. See `codegen/internal/generator/templates/assertion_assertions_test.gotmpl` for examples of: +- Custom mock selection based on function behavior (mockT vs mockFailNowT) +- Package-specific test helpers (testDataPath, httpOK, etc.) +- Handling functions without test examples (generates `t.Skip()`) + +Some go expressions won't fit nicely for examples (examples use an external package, e.g. `assert_test`). +To cover these edge cases, a `relocate` function map currently rewrites the example values to be used +from an external package. The relocation uses go parsing capabilities. The only hard-coded exception if for `PanicFunc`. +(see `codegen/internal/generator/funcmap.go`). + +### Generator Flags + +```bash +go run ./codegen/main.go \ + -work-dir=.. \ + -input-package=github.com/go-openapi/testify/v2/internal/assertions \ + -output-packages=assert,require \ + -target-root=.. \ + -include-format-funcs=true \ + -include-forward-funcs=true \ + -include-tests=true \ + -include-examples=true \ + -runnable-examples=true \ + -include-helpers=true \ + -include-generics=false +``` + +Current usage with `go generate` (see `doc.go`): + +```go +//go:generate go run ./codegen/main.go -target-root . -work-dir . +``` + +**Note:** Generic functions are planned but not yet implemented. + +### Verification + +After generation, verify: +```bash +# All tests pass +go test ./... + +# Coverage remains high +go test -cover ./internal/assertions # Should be ~94% +go test -cover ./assert # Should be ~99.5% +go test -cover ./require # Should be ~99.5% + +# Examples are valid +go test -run Example ./assert +go test -run Example ./require +``` + +The 0.5% coverage gap comes from helper functions (non-assertion functions) that don't have "Examples:" annotations. diff --git a/docs/doc-site/project/maintainers/MAINTAINERS.md b/docs/doc-site/project/maintainers/MAINTAINERS.md new file mode 100644 index 000000000..4a2d15a88 --- /dev/null +++ b/docs/doc-site/project/maintainers/MAINTAINERS.md @@ -0,0 +1,185 @@ +--- +title: Maintainer's guide +description: Guidelines for maintainers +weight: 1 +--- + +## Repo structure + +Monorepo with multiple go modules. + +## Repo configuration + +* default branch: master +* protected branches: master +* branch protection rules: + * require pull requests and approval + * required status checks: + - DCO (simple email sign-off) + - Lint + - tests completed +* auto-merge enabled (used for dependabot updates) + +## Continuous Integration + +### Code Quality checks + +* meta-linter: [golangci-lint][golangci-url] +* linter config: [`.golangci.yml`][linter-config] (see our [posture][style-doc] on linters) +* Code quality assessment: [CodeFactor][codefactor-url] +* Code quality badges + * [go report card][gocard-url] + * [CodeFactor][codefactor-url] + +> **NOTES** +> +> codefactor inherits roles from github. There is no need to create a dedicated account. +> +> The codefactor app is installed at the organization level (`github.com/go-openapi`). +> +> There is no special token to setup in github for CI usage. + +### Testing + +* Test reports + * Uploaded to codecov: +* Test coverage reports + * Uploaded to codecov: + +* Fuzz testing + * Fuzz tests are handled separately by CI and may reuse a cached version of the fuzzing corpus. + At this moment, cache may not be shared between feature branches or feature branch and master. + The minimized corpus produced on failure is uploaded as an artifact and should be added manually + to `testdata/fuzz/...`. + +Coverage threshold status is informative and not blocking. +This is because the thresholds are difficult to tune and codecov oftentimes reports false negatives +or may fail to upload coverage. + +All tests across `go-openapi` use our fork of `stretchr/testify` (this repo): `github.com/go-openapi/testify`. +This allows for minimal test dependencies. + +> **NOTES** +> +> codecov inherits roles from github. There is no need to create a dedicated account. +> However, there is only 1 maintainer allowed to be the admin of the organization on codecov +> with their free plan. +> +> The codecov app is installed at the organization level (`github.com/go-openapi`). +> +> There is no special token to setup in github for CI usage. +> A organization-level token used to upload coverage and test reports is managed at codecov: +> no setup is required on github. + +### Automated updates + +* dependabot + * configuration: [`dependabot.yaml`][dependabot-config] + + Principle: + + * codecov applies updates and security patches to the github-actions and golang ecosystems. + * all updates from "trusted" dependencies (github actions, golang.org packages, go-openapi packages + are auto-merged if they successfully pass CI. + +* go version udpates + + Principle: + + * we support the 2 latest minor versions of the go compiler (`stable`, `oldstable`) + * `go.mod` should be updated (manually) whenever there is a new go minor release + (e.g. every 6 months). + +* contributors + * a [`CONTRIBUTORS.md`][contributors-doc] file is updated weekly, with all-time contributors to the repository + * the `github-actions[bot]` posts a pull request to do that automatically + * at this moment, this pull request is not auto-approved/auto-merged (bot cannot approve its own PRs) + +### Vulnerability scanners + +There are 3 complementary scanners - obviously, there is some overlap, but each has a different focus. + +* GitHub `CodeQL` +* `trivy` +* `govulnscan` + +None of these tools require an additional account or token. + +Github CodeQL configuration is set to "Advanced", so we may collect a CI status for this check (e.g. for badges). + +Scanners run on every commit to master and at least once a week. + +Reports are centralized in github security reports for code scanning tools. + +## Releases + +**For single module repos:** + +A bump release workflow can be triggered from the github actions UI to cut a release with a few clicks. + +The release process is minimalist: + +* push a semver tag (i.e v{major}.{minor}.{patch}) to the master branch. +* the CI handles this to generate a github release with release notes + +* release notes generator: git-cliff +* configuration: [`cliff.toml`][cliff-config] + +Tags are preferably PGP-signed. + +The tag message introduces the release notes (e.g. a summary of this release). + +The release notes generator does not assume that commits are necessarily "conventional commits". + +**For mono-repos with multiple modules:** + +The release process is slightly different because we need to update cross-module dependencies +before pushing a tag. + +A bump release workflow (mono-repo) can be triggered from the github actions UI to cut a release with a few clicks. + +It works the same as the one for single module repos, and first creates a PR (auto-merged) +that updates the different go.mod files. + +## Other files + +Standard documentation: + +* [CONTRIBUTING.md][contributing-doc] guidelines +* [DCO.md][dco-doc] terms for first-time contributors to read +* [CODE_OF_CONDUCT.md][coc-doc] +* [SECURITY.md][security-doc] policy: how to report vulnerabilities privately +* [LICENSE][license-doc] terms +* [NOTICE][notice-doc] on supplementary license terms (original authors, copied code etc) + +Reference documentation (released): + +* [pkg.go.dev (fka godoc)][godoc-url] + +## TODOs & other ideas + +A few things remain ahead to ease a bit a maintainer's job: + +* [ ] reuse CI workflows (e.g. in ) +* [ ] reusable actions with custom tools pinned (e.g. in ) +* open-source license checks +* [ ] auto-merge for CONTRIBUTORS.md (requires a github app to produce tokens) +* [x] more automated code renovation / relinting work +* organization-level documentation web site +* ... + +[linter-config]: https://github.com/go-openapi/testify/blob/master/.golangci.yml +[cliff-config]: https://github.com/go-openapi/testify/blob/master/.cliff.toml +[dependabot-config]: https://github.com/go-openapi/testify/blob/master/.github/dependabot.yaml +[gocard-url]: https://goreportcard.com/report/github.com/go-openapi/testify +[codefactor-url]: https://www.codefactor.io/repository/github/go-openapi/testify +[golangci-url]: https://golangci-lint.run/ +[godoc-url]: https://pkg.go.dev/github.com/go-openapi/testify/v2 +[contributors-doc]: ../contributing/CONTRIBUTORS.md +[contributing-doc]: ../contributing/CONTRIBUTING.md +[dco-doc]: ../contributing/DCO.md +[style-doc]: ./STYLE.md +[coc-doc]: ../contributing/CODE_OF_CONDUCT.md +[security-doc]: ../SECURITY.md +[license-doc]: ../LICENSE.md +[notice-doc]: ../NOTICE.md diff --git a/docs/doc-site/project/maintainers/ORIGINAL.md b/docs/doc-site/project/maintainers/ORIGINAL.md new file mode 100644 index 000000000..e85087da0 --- /dev/null +++ b/docs/doc-site/project/maintainers/ORIGINAL.md @@ -0,0 +1,374 @@ +--- +title: Original README +description: | + Original README from github.com/stretchr/testify. + + Always important to remember where we come from. +weight: 10 +--- +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/docs/doc-site/project/maintainers/ROADMAP.md b/docs/doc-site/project/maintainers/ROADMAP.md new file mode 100644 index 000000000..0f8be9eaf --- /dev/null +++ b/docs/doc-site/project/maintainers/ROADMAP.md @@ -0,0 +1,117 @@ +--- +title: "Testify v2 roadmap" +description: "Let's share our plans." +weight: 4 +--- + +## What's next with this project? + +{{< mermaid align="center" zoom="true" >}} +timeline + title Planned releases + section Q4 2025 + v2.0 (Nov 2025) : zero dependencies + : optional dependencies (YAML) + : modernized code (relint) + : JSONEqBytes + section Q1 2026 + v2.1 (Jan 2026) : generated assertions + : complete refactoring + : documentation site + : panic handling fixes + : removed deprecated + v2.2 (Fev 2026) : extension w/ generics + : optional dependencies (colorized) + v2.3 (Mar 2026) : other extensions + section Q2 2026 + 2006 : Twitter +{{< /mermaid >}} + +1. [x] The first release comes with zero dependencies and an unstable API (see below [our use case](#usage-at-go-openapi)) +2. [x] This project is going to be injected as the main and sole test dependency of the `go-openapi` libraries +2. [ ] ... and the `go-swagger` tool +3. [x] Valuable pending pull requests from the original project could be merged (e.g. `JSONEqBytes`) or transformed as "enable" modules (e.g. colorized output) +4. [ ] Unclear assertions may be provided an alternative verb (e.g. `InDelta`) +5. [ ] Since we have leveled the go requirements to the rest of the go-openapi (currently go1.24) there is quite a bit of relinting lying ahead. + +### What won't come anytime soon + +* mocks: we use [mockery](https://github.com/vektra/mockery) and prefer the simpler `matryer` mocking-style. + testify-style mocks are thus not going to be supported anytime soon. +* extra convoluted stuff in the like of `InDeltaSlice` + +## Generics adoption + +### Context from the original repository + +Several attempts have been made to introduce generics in the original stretchr/testify repository: + +* **github.com/stretchr/testify#1308** - Comprehensive refactor replacing `interface{}` with generic type parameters across assertions (Draft, v2.0.0 milestone) +* **github.com/stretchr/testify#1805** - Proposal for generic `IsOfType[T]()` to avoid dummy value instantiation in type checks +* **github.com/stretchr/testify#1685** - Iterator support (`iter.Seq`) for Contains/ElementsMatch assertions (Go 1.23+) +* **github.com/stretchr/testify#1147** - General discussion about generics adoption (marked "Not Planned") + +### Challenges identified + +The original repository's exploration of generics revealed several design challenges: + +1. **Type inference limitations**: Go's type inference struggles with complex generic signatures, often requiring explicit type parameters that burden the API (e.g., `Contains[int, int](arr1, arr2)`) + +2. **Overly broad type constraints**: PR #1308's approach used constraints like `ConvertibleToFloat64` that accepted more types than intended, weakening type safety + +3. **Loss of flexibility**: Testify currently compares non-comparable types (slices, maps) via `reflect.DeepEqual`. Generic constraints would eliminate this capability, as Go generics require comparable or explicitly constrained types + +4. **Breaking changes**: Any comprehensive generics adoption requires a major version bump and Go 1.18+ minimum version + +5. **Inconsistent design patterns**: Different assertions would need different constraint strategies, making a uniform approach difficult + +## PRs from the original repo + +### Already merged or incorporated + +The following proposed contributions to the original repo have been merged or incorporated with +some adaptations into this fork: + +* github.com/stretchr/testify#1513 - JSONEqBytes for byte slice JSON comparison +* github.com/stretchr/testify#1772 - YAML library migration to maintained fork (go.yaml.in/yaml) +* github.com/stretchr/testify#1797 - Codegen package consolidation and licensing +* github.com/stretchr/testify#1356 - panic(nil) handling for Go 1.21+ +* github.com/stretchr/testify#1825 - Fix panic when using EqualValues with uncomparable types [merged] +* github.com/stretchr/testify#1818 - Fix panic on invalid regex in Regexp/NotRegexp assertions [merged] +* github.com/stretchr/testify#1223 - Display uint values in decimal instead of hex in diffs [merged] + +### Planned merges + +#### Critical safety fixes (high priority) + +* Follow / adapt https://github.com/stretchr/testify/pull/1824 + +Not PRs, but reported issues in the original repo (need to investigate): + +* https://github.com/stretchr/testify/issues/1826 +* https://github.com/stretchr/testify/issues/1611 +* https://github.com/stretchr/testify/issues/1813 + +#### Leveraging internalized dependencies (go-spew, difflib) + +These improvements apply to the internalized and modernized copies of dependencies in this fork: + +* github.com/stretchr/testify#1829 - Fix time.Time rendering in diffs (internalized go-spew) +* github.com/stretchr/testify#1822 - Deterministic map ordering in diffs (internalized go-spew) +* github.com/stretchr/testify#1816 - Fix panic on unexported struct key in map (internalized go-spew - may need deeper fix) + +#### UX improvements + +* diff rendering + +### Under consideration + +#### Colorized output + +Several PRs propose colorized terminal output with different approaches and dependencies. +If implemented, this would be provided as an optional `enable/color` module: + +* github.com/stretchr/testify#1467 - Colorized output with terminal detection (most mature implementation) +* github.com/stretchr/testify#1480 - Colorized diffs via TESTIFY_COLORED_DIFF env var +* github.com/stretchr/testify#1232 - Colorized output for expected/actual/errors +* github.com/stretchr/testify#994 - Colorize expected vs actual values diff --git a/docs/doc-site/project/maintainers/_index.md b/docs/doc-site/project/maintainers/_index.md new file mode 100644 index 000000000..4c5de21da --- /dev/null +++ b/docs/doc-site/project/maintainers/_index.md @@ -0,0 +1,7 @@ +--- +title: "Maintainer's Guidelines" +description: "How to maintain this beast?" +weight: 4 +--- + +{{< children type="card" description="true" >}} diff --git a/hack/doc-site/hugo/README.md b/hack/doc-site/hugo/README.md new file mode 100644 index 000000000..f16c1fe36 --- /dev/null +++ b/hack/doc-site/hugo/README.md @@ -0,0 +1,99 @@ +# Hugo Documentation Site + +This directory contains the Hugo configuration for the Testify documentation site. + +## Structure + +``` +hugo/ +├── hugo.yaml # Main Hugo configuration +├── testify.yaml.template # Dynamic config template (version info) +├── gendoc.sh # Local development server script +├── themes/ +│ ├── hugo-relearn/ # Relearn theme (extracted from zip) +│ ├── testify-assets/ # Custom SCSS and assets +│ │ └── _custom.scss # Theme customization +│ └── testify-static/ # Static files (logo, favicon) +└── layouts/ # Custom layouts (override theme) + ├── shortcodes/ # Custom Hugo shortcodes + └── partials/ # Custom partial templates + +``` + +## Content Generation + +Documentation content is generated by the `DocGenerator` to: +``` +../../../docs/doc-site/ +``` + +This directory is mounted as Hugo's content directory via module mounts in `hugo.yaml`. + +## Local Development + +```bash +# Run local Hugo server +./gendoc.sh + +# Site will be available at: +# http://localhost:1313/testify/ +``` + +The `gendoc.sh` script: +1. Extracts version info from git tags and go.mod +2. Generates `testify.yaml` from template +3. Starts Hugo server with both config files +4. Enables live reload and draft content + +## Theme + +Uses **Hugo Relearn** theme (dark variant) following go-swagger patterns. + +Theme customizations: +- Custom SCSS in `themes/testify-assets/_custom.scss` +- Static branding assets in `themes/testify-static/` +- Layout overrides in `layouts/` + +## Configuration + +Two-layer config following go-swagger pattern: + +1. **hugo.yaml** - Static configuration (theme, structure, parameters) +2. **testify.yaml** - Dynamic configuration (version, build time) + +The dynamic config is generated at build time from `testify.yaml.template`. + +## URL Structure + +Following domain-based organization: + +``` +/ # Home page +/boolean/ # Boolean assertions (True, False) +/collection/ # Collection assertions (Contains, Empty, Len) +/compare/ # Comparison assertions (Greater, Less) +/equal/ # Equality assertions (Equal, NotEqual, Same) +/error/ # Error assertions (Error, NoError, ErrorIs) +/file/ # File assertions (FileExists, FileEmpty) +/http/ # HTTP assertions (HTTPSuccess, HTTPError) +/json/ # JSON assertions (JSONEq) +/number/ # Number assertions (InDelta, InEpsilon) +/panic/ # Panic assertions (Panics, NotPanics) +/string/ # String assertions (Regexp, NotRegexp) +/time/ # Time assertions (WithinDuration) +/type/ # Type assertions (IsType, Zero, Implements) +/yaml/ # YAML assertions (YAMLEq) +``` + +## Deployment + +GitHub Pages deployment will be configured to: +- Build from `docs/` directory +- Publish to: https://go-openapi.github.io/testify/ + +## Notes + +- **Versioning ignored for now** - Focus on basic structure first +- Edit links point to `internal/assertions/` source code +- Search enabled via Relearn theme +- Breadcrumbs and prev/next navigation enabled diff --git a/hack/doc-site/hugo/gendoc.sh b/hack/doc-site/hugo/gendoc.sh new file mode 100755 index 000000000..6b3956322 --- /dev/null +++ b/hack/doc-site/hugo/gendoc.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +# Local development script for Hugo documentation + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "${SCRIPT_DIR}" + +echo "==> Preparing Hugo documentation site..." + +# Extract version information +LATEST_RELEASE=$(git tag --list --sort -version:refname 'v*' 2>/dev/null | head -1 || echo "dev") +REQUIRED_GO_VERSION=$(grep "^go\s" ../../../go.mod | awk '{print $2}') +BUILD_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ") + +echo " Latest release: ${LATEST_RELEASE}" +echo " Go version: ${REQUIRED_GO_VERSION}" +echo " Build time: ${BUILD_TIME}" + +# Generate dynamic config +cat testify.yaml.template \ + | sed "s|{{ GO_VERSION }}|${REQUIRED_GO_VERSION}|g" \ + | sed "s|{{ LATEST_RELEASE }}|${LATEST_RELEASE}|g" \ + | sed "s|{{ BUILD_TIME }}|${BUILD_TIME}|g" \ + > testify.yaml + +echo "==> Generated testify.yaml" + +# Check if theme exists +if [ ! -d "themes/hugo-relearn" ]; then + echo "ERROR: Relearn theme not found at themes/hugo-relearn" + echo "Run: unzip ../../hugo-theme-relearn-main.zip -d themes/ && mv themes/hugo-theme-relearn-main themes/hugo-relearn" + exit 1 +fi + +# Check if generated docs exist +if [ ! -d "../../../docs/doc-site" ]; then + echo "WARNING: Generated docs not found at ../../../docs/doc-site" + echo "You may need to run: go generate ./..." + echo "" + echo "Creating placeholder content directory..." + mkdir -p content +fi + +echo "==> Starting Hugo development server..." +echo " Visit: http://localhost:1313/testify/" +echo "" + +# Start Hugo server with both configs +hugo server \ + --config hugo.yaml,testify.yaml \ + --buildDrafts \ + --disableFastRender \ + --navigateToChanged \ + --bind 0.0.0.0 \ + --port 1313 \ + --baseURL http://localhost:1313/testify/ \ + --appendPort=false \ + --logLevel info \ + --cleanDestinationDir diff --git a/hack/doc-site/hugo/hugo.yaml b/hack/doc-site/hugo/hugo.yaml new file mode 100644 index 000000000..0f0c25b0c --- /dev/null +++ b/hack/doc-site/hugo/hugo.yaml @@ -0,0 +1,137 @@ +baseURL: https://go-openapi.github.io/testify/ +title: Testify v2 +theme: hugo-relearn +languageCode: en-us + +# Enable features +enableEmoji: true +enableGitInfo: true +enableRobotsTXT: true + +# Content configuration +contentDir: content + +# Output formats +outputs: + home: + - HTML + - RSS + - SEARCH + +# Markup configuration +markup: + goldmark: + renderer: + unsafe: true # Allow raw HTML in markdown + parser: + attribute: + title: true + block: true + renderHooks: + image: + useEmbedded: always + highlight: + codeFences: true + guessSyntax: false + lineNos: false + lineNumbersInTable: false + noClasses: true + style: monokai + tabWidth: 4 + +# Taxonomies are disabled for now +disableKinds: + - taxonomy + - term +# Module mounts +# Following go-swagger pattern: simple mounts for content and assets +module: + mounts: + # Custom layouts (override theme if needed) + - source: layouts + target: layouts + + # Generated documentation content + - source: '../../../docs/doc-site' + target: content + + # Custom SCSS (for theme customization) + logo + - source: themes/testify-assets + target: assets + + # Custom static files (favicon) + - source: themes/testify-static + target: static + +# Relearn theme parameters +params: + # Edit links - point to source code since docs are generated + editURL: 'https://github.com/go-openapi/testify/edit/master/internal/assertions/' + externalLinkTarget: _blank + + # Repository info + sourceRepository: 'https://github.com/go-openapi/testify' + + # Branding + author: + name: 'go-openapi maintainers' + hideAuthorEmail: true + + # Theme customization + themeVariant: + - zen-dark + - relearn-dark + - relearn-light + showVisitedLinks: true + collapsibleMenu: true + disableBreadcrumb: false + disableNextPrev: false + disableLandingPageButton: true + titleSeparator: '|' + + # Features + search: + disable: false + index: + disable: false + page: + disable: false + adapter: + identifier: lunr + disableLanguageSwitchingButton: true + disableInlineCopyToClipBoard: false + + # Menu ordering + ordersectionsby: 'weight' + # Proposal for enhancement: configure versions + #versions: + #- baseURL: https://go-openapi.github.io/testify/ + # identifier: v2.1.0 + # isLatest: true + #version: 'v2.1.0' + + # Custom params (populated by testify.yaml) + testify: + goVersion: '' + latestRelease: '' + versionMessage: '' + +# Menu configuration +menu: + shortcuts: + - name: "GitHub" + identifier: github + url: "https://github.com/go-openapi/testify" + weight: 10 + + - name: "go-openapi toolkit" + identifier: go-openapi + url: "https://github.com/go-openapi" + weight: 20 + +# Privacy settings +privacy: + disqus: + disable: true + googleAnalytics: + disable: true diff --git a/hack/doc-site/hugo/layouts/partials/.gitkeep b/hack/doc-site/hugo/layouts/partials/.gitkeep new file mode 100644 index 000000000..780221121 --- /dev/null +++ b/hack/doc-site/hugo/layouts/partials/.gitkeep @@ -0,0 +1,2 @@ +# Custom partials directory +# Override theme partials here if needed diff --git a/hack/doc-site/hugo/layouts/partials/bodys/single.html b/hack/doc-site/hugo/layouts/partials/bodys/single.html new file mode 100644 index 000000000..50a81a647 --- /dev/null +++ b/hack/doc-site/hugo/layouts/partials/bodys/single.html @@ -0,0 +1,7 @@ +{{- .Store.Set "relearnIsNested" false }} +{{- if gt .ReadingTime 1 }} +
+📖 {{ .ReadingTime }} min read (~ {{ .FuzzyWordCount }} words). +
+{{- end }} +{{- .Render "article" }} diff --git a/hack/doc-site/hugo/layouts/partials/content-footer.html b/hack/doc-site/hugo/layouts/partials/content-footer.html new file mode 100644 index 000000000..1ff3a4818 --- /dev/null +++ b/hack/doc-site/hugo/layouts/partials/content-footer.html @@ -0,0 +1,37 @@ +{{- $LastModifierDisplayName := "" }} +{{- $LastModifierEmail := "" }} +{{- $Date := "" }} +{{- $dateFormat := site.Params.dateFormat | default ":date_medium" }} +{{- with .GitInfo }} + {{- with and (not site.Params.hideAuthorName) .AuthorName }} + {{- $LastModifierDisplayName = . }} + {{- end }} + {{- with and (not site.Params.hideAuthorEmail) .AuthorEmail }} + {{- $LastModifierEmail = . }} + {{- end }} + {{- with and (not site.Params.hideAuthorDate) .AuthorDate }} + {{- $Date = . | time.Format $dateFormat }} + {{- end }} +{{- else }} + {{- with and (not site.Params.hideAuthorName) .Params.LastModifierDisplayName }} + {{- $LastModifierDisplayName = . }} + {{- end }} + {{- with and (not site.Params.hideAuthorEmail) .Params.LastModifierEmail }} + {{- $LastModifierEmail = . }} + {{- end }} + {{- with and (not site.Params.hideAuthorDate) .Date }} + {{- $Date = . | time.Format $dateFormat }} + {{- end }} +{{- end }} +{{- if $LastModifierDisplayName }} + Last edited by: {{ with $LastModifierEmail }}{{ end }}{{ $LastModifierDisplayName }}{{ with $LastModifierEmail }}{{ end }} +{{- end }} +{{- with $Date }} + {{ . }} +{{- end }} +
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license. +{{- partial "term-list.html" (dict + "page" . + "taxonomy" "categories" + "icon" "layer-group" +) }} diff --git a/hack/doc-site/hugo/layouts/partials/custom-header.html b/hack/doc-site/hugo/layouts/partials/custom-header.html new file mode 100644 index 000000000..8bae1c490 --- /dev/null +++ b/hack/doc-site/hugo/layouts/partials/custom-header.html @@ -0,0 +1,9 @@ + diff --git a/hack/doc-site/hugo/layouts/shortcodes/.gitkeep b/hack/doc-site/hugo/layouts/shortcodes/.gitkeep new file mode 100644 index 000000000..f4bdeec44 --- /dev/null +++ b/hack/doc-site/hugo/layouts/shortcodes/.gitkeep @@ -0,0 +1,2 @@ +# Custom shortcodes directory +# Add custom Hugo shortcodes here diff --git a/hack/doc-site/hugo/testify.yaml b/hack/doc-site/hugo/testify.yaml new file mode 100644 index 000000000..049c0bc15 --- /dev/null +++ b/hack/doc-site/hugo/testify.yaml @@ -0,0 +1,16 @@ +# Dynamic configuration generated at build time +# This file provides version information extracted from the repository + +params: + testify: + # Go version requirement (from go.mod) + goVersion: '1.24.0' + + # Latest release tag (from git tags) + latestRelease: 'v2.0.2' + + # Version message for the documentation set + versionMessage: 'Documentation for Testify v2.0.2' + + # Build timestamp + buildTime: '2026-01-01T21:42:33Z' diff --git a/hack/doc-site/hugo/testify.yaml.template b/hack/doc-site/hugo/testify.yaml.template new file mode 100644 index 000000000..5ce1d68a8 --- /dev/null +++ b/hack/doc-site/hugo/testify.yaml.template @@ -0,0 +1,16 @@ +# Dynamic configuration generated at build time +# This file provides version information extracted from the repository + +params: + testify: + # Go version requirement (from go.mod) + goVersion: '{{ GO_VERSION }}' + + # Latest release tag (from git tags) + latestRelease: '{{ LATEST_RELEASE }}' + + # Version message for the documentation set + versionMessage: 'Documentation for Testify {{ LATEST_RELEASE }}' + + # Build timestamp + buildTime: '{{ BUILD_TIME }}' diff --git a/hack/gogenerate.sh b/hack/gogenerate.sh deleted file mode 100755 index 5b5642094..000000000 --- a/hack/gogenerate.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/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/hack/readme.fmt.sh b/hack/readme.fmt.sh deleted file mode 100755 index b3d6a1d06..000000000 --- a/hack/readme.fmt.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/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/internal/assertions/boolean.go b/internal/assertions/boolean.go index e990cde61..13b27fbde 100644 --- a/internal/assertions/boolean.go +++ b/internal/assertions/boolean.go @@ -5,15 +5,16 @@ package assertions // True asserts that the specified value is true. // -// Usage: +// # Usage // // assertions.True(t, myBool) // -// Examples: +// # Examples // // success: 1 == 1 // failure: 1 == 0 func True(t T, value bool, msgAndArgs ...any) bool { + // Domain: boolean if !value { if h, ok := t.(H); ok { h.Helper() @@ -26,15 +27,16 @@ func True(t T, value bool, msgAndArgs ...any) bool { // False asserts that the specified value is false. // -// Usage: +// # Usage // // assertions.False(t, myBool) // -// Examples: +// # Examples // // success: 1 == 0 // failure: 1 == 1 func False(t T, value bool, msgAndArgs ...any) bool { + // Domain: boolean if value { if h, ok := t.(H); ok { h.Helper() diff --git a/internal/assertions/collection.go b/internal/assertions/collection.go index bbbc00446..b13eb8bb4 100644 --- a/internal/assertions/collection.go +++ b/internal/assertions/collection.go @@ -18,17 +18,22 @@ import ( // // See also [reflect.Len]. // -// Usage: +// # Usage // // assertions.Len(t, mySlice, 3) // assertions.Len(t, myString, 4) // assertions.Len(t, myMap, 5) // -// Examples: +// # Examples // // success: []string{"A","B"}, 2 // failure: []string{"A","B"}, 1 func Len(t T, object any, length int, msgAndArgs ...any) bool { + // Domain: collection + // Maintainer: The implementation is based on [reflect.Len]. The potential panic is handled with recover. + // A better approach could be to check for the [reflect.Type] before calling [reflect.Len]. + // + // Note: (proposals) this does not currently support iterators, or collection objects that have a Len() method. if h, ok := t.(H); ok { h.Helper() } @@ -46,17 +51,18 @@ func Len(t T, object any, length int, msgAndArgs ...any) bool { // Contains asserts that the specified string, list(array, slice...) or map contains the // specified substring or element. // -// Usage: +// # Usage // // assertions.Contains(t, "Hello World", "World") -// assertions.Contains(t, ["Hello", "World"], "World") -// assertions.Contains(t, {"Hello": "World"}, "Hello") +// assertions.Contains(t, []string{"Hello", "World"}, "World") +// assertions.Contains(t, map[string]string{"Hello": "World"}, "Hello") // -// Examples: +// # Examples // // success: []string{"A","B"}, "A" // failure: []string{"A","B"}, "C" func Contains(t T, s, contains any, msgAndArgs ...any) bool { + // Domain: collection if h, ok := t.(H); ok { h.Helper() } @@ -75,17 +81,18 @@ func Contains(t T, s, contains any, msgAndArgs ...any) bool { // NotContains asserts that the specified string, list(array, slice...) or map does NOT contain the // specified substring or element. // -// Usage: +// # Usage // // assertions.NotContains(t, "Hello World", "Earth") // assertions.NotContains(t, ["Hello", "World"], "Earth") // assertions.NotContains(t, {"Hello": "World"}, "Earth") // -// Examples: +// # Examples // // success: []string{"A","B"}, "C" // failure: []string{"A","B"}, "B" func NotContains(t T, s, contains any, msgAndArgs ...any) bool { + // Domain: collection if h, ok := t.(H); ok { h.Helper() } @@ -107,16 +114,19 @@ func NotContains(t T, s, contains any, msgAndArgs ...any) bool { // 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"]) +// # Usage // -// Examples: +// assertions.Subset(t, [1, 2, 3], [1, 2]) +// assertions.Subset(t, {"x": 1, "y": 2}, {"x": 1}) +// assertions.Subset(t, [1, 2, 3], {1: "one", 2: "two"}) +// assertions.Subset(t, {"x": 1, "y": 2}, ["x"]) +// +// # Examples // // success: []int{1, 2, 3}, []int{1, 2} // failure: []int{1, 2, 3}, []int{4, 5} func Subset(t T, list, subset any, msgAndArgs ...any) (ok bool) { + // Domain: collection if h, ok := t.(H); ok { h.Helper() } @@ -181,16 +191,19 @@ func Subset(t T, list, subset any, msgAndArgs ...any) (ok bool) { // 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"]) +// # Usage +// +// assertions.NotSubset(t, [1, 3, 4], [1, 2]) +// assertions.NotSubset(t, {"x": 1, "y": 2}, {"z": 3}) +// assertions.NotSubset(t, [1, 3, 4], {1: "one", 2: "two"}) +// assertions.NotSubset(t, {"x": 1, "y": 2}, ["z"]) // -// Examples: +// # Examples // // success: []int{1, 2, 3}, []int{4, 5} // failure: []int{1, 2, 3}, []int{1, 2} func NotSubset(t T, list, subset any, msgAndArgs ...any) (ok bool) { + // Domain: collection if h, ok := t.(H); ok { h.Helper() } @@ -253,13 +266,16 @@ func NotSubset(t T, list, subset any, msgAndArgs ...any) (ok bool) { // listB(array, slice...) ignoring the order of the elements. If there are duplicate elements, // the number of appearances of each of them in both lists should match. // -// assert.ElementsMatch(t, [1, 3, 2, 3], [1, 3, 3, 2]). +// # Usage +// +// assertions.ElementsMatch(t, [1, 3, 2, 3], [1, 3, 3, 2]) // -// Examples: +// # Examples // // success: []int{1, 3, 2, 3}, []int{1, 3, 3, 2} // failure: []int{1, 2, 3}, []int{1, 2, 4} func ElementsMatch(t T, listA, listB any, msgAndArgs ...any) (ok bool) { + // Domain: collection if h, ok := t.(H); ok { h.Helper() } @@ -280,6 +296,45 @@ func ElementsMatch(t T, listA, listB any, msgAndArgs ...any) (ok bool) { return Fail(t, formatListDiff(listA, listB, extraA, extraB), msgAndArgs...) } +// 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. +// +// # Usage +// +// assertions.NotElementsMatch(t, [1, 1, 2, 3], [1, 1, 2, 3]) -> false +// assertions.NotElementsMatch(t, [1, 1, 2, 3], [1, 2, 3]) -> true +// assertions.NotElementsMatch(t, [1, 2, 3], [1, 2, 4]) -> true +// +// # Examples +// +// success: []int{1, 2, 3}, []int{1, 2, 4} +// failure: []int{1, 3, 2, 3}, []int{1, 3, 3, 2} +func NotElementsMatch(t T, listA, listB any, msgAndArgs ...any) (ok bool) { + // Domain: collection + if h, ok := t.(H); 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 +} + // containsElement tries to loop over the list check if the list includes the element. // // return (false, false) if impossible. @@ -393,44 +448,6 @@ func formatListDiff(listA, listB any, extraA, extraB []any) string { 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. -// -// Examples: -// -// success: []int{1, 2, 3}, []int{1, 2, 4} -// failure: []int{1, 3, 2, 3}, []int{1, 3, 3, 2} -func NotElementsMatch(t T, listA, listB any, msgAndArgs ...any) (ok bool) { - if h, ok := t.(H); 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 -} - // getLen tries to get the length of an object. // It returns (0, false) if impossible. func getLen(x any) (length int, ok bool) { diff --git a/internal/assertions/compare.go b/internal/assertions/compare.go index 13d324ecb..7d40bdd01 100644 --- a/internal/assertions/compare.go +++ b/internal/assertions/compare.go @@ -12,27 +12,26 @@ import ( "time" ) -// ComparisonAssertionFunc is a common function prototype when comparing two values. Can be useful -// for table driven tests. -type ComparisonAssertionFunc func(T, any, any, ...any) bool +type ( + // ComparisonAssertionFunc is a common function prototype when comparing two values. Can be useful + // for table driven tests. + ComparisonAssertionFunc func(T, any, any, ...any) bool -// ValueAssertionFunc is a common function prototype when validating a single value. Can be useful -// for table driven tests. -type ValueAssertionFunc func(T, any, ...any) bool + // ValueAssertionFunc is a common function prototype when validating a single value. Can be useful + // for table driven tests. + ValueAssertionFunc func(T, any, ...any) bool -// BoolAssertionFunc is a common function prototype when validating a bool value. Can be useful -// for table driven tests. -type BoolAssertionFunc func(T, bool, ...any) bool + // BoolAssertionFunc is a common function prototype when validating a bool value. Can be useful + // for table driven tests. + BoolAssertionFunc func(T, bool, ...any) bool -// ErrorAssertionFunc is a common function prototype when validating an error value. Can be useful -// for table driven tests. -type ErrorAssertionFunc func(T, error, ...any) bool + // ErrorAssertionFunc is a common function prototype when validating an error value. Can be useful + // for table driven tests. + ErrorAssertionFunc func(T, error, ...any) bool -// Comparison is a custom function that returns true on success and false on failure. -type Comparison func() (success bool) - -// 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 + // Comparison is a custom function that returns true on success and false on failure. + Comparison func() (success bool) +) type compareResult int @@ -42,6 +41,161 @@ const ( compareGreater ) +// Greater asserts that the first element is strictly greater than the second. +// +// # Usage +// +// assertions.Greater(t, 2, 1) +// assertions.Greater(t, float64(2), float64(1)) +// assertions.Greater(t, "b", "a") +// +// # Examples +// +// success: 2, 1 +// failure: 1, 2 +func Greater(t T, e1 any, e2 any, msgAndArgs ...any) bool { + // Domain: comparison + if h, ok := t.(H); 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. +// +// # Usage +// +// assertions.GreaterOrEqual(t, 2, 1) +// assertions.GreaterOrEqual(t, 2, 2) +// assertions.GreaterOrEqual(t, "b", "a") +// assertions.GreaterOrEqual(t, "b", "b") +// +// # Examples +// +// success: 2, 1 +// failure: 1, 2 +func GreaterOrEqual(t T, e1 any, e2 any, msgAndArgs ...any) bool { + // Domain: comparison + if h, ok := t.(H); 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 strictly less than the second. +// +// # Usage +// +// assertions.Less(t, 1, 2) +// assertions.Less(t, float64(1), float64(2)) +// assertions.Less(t, "a", "b") +// +// # Examples +// +// success: 1, 2 +// failure: 2, 1 +func Less(t T, e1 any, e2 any, msgAndArgs ...any) bool { + // Domain: comparison + if h, ok := t.(H); 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. +// +// # Usage +// +// assertions.LessOrEqual(t, 1, 2) +// assertions.LessOrEqual(t, 2, 2) +// assertions.LessOrEqual(t, "a", "b") +// assertions.LessOrEqual(t, "b", "b") +// +// # Examples +// +// success: 1, 2 +// failure: 2, 1 +func LessOrEqual(t T, e1 any, e2 any, msgAndArgs ...any) bool { + // Domain: comparison + if h, ok := t.(H); 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 strictly positive. +// +// # Usage +// +// assertions.Positive(t, 1) +// assertions.Positive(t, 1.23) +// +// # Examples +// +// success: 1 +// failure: -1 +func Positive(t T, e any, msgAndArgs ...any) bool { + // Domain: comparison + if h, ok := t.(H); 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 strictly negative. +// +// # Usage +// +// assertions.Negative(t, -1) +// assertions.Negative(t, -1.23) +// +// # Examples +// +// success: -1 +// failure: 1 +func Negative(t T, e any, msgAndArgs ...any) bool { + // Domain: comparison + if h, ok := t.(H); 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 T, e1 any, e2 any, allowedComparesResults []compareResult, failMessage string, msgAndArgs ...any) bool { + if h, ok := t.(H); 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 { + return slices.Contains(values, value) +} + func compare(obj1, obj2 any, kind reflect.Kind) (compareResult, bool) { obj1Value := reflect.ValueOf(obj1) obj2Value := reflect.ValueOf(obj2) @@ -186,142 +340,3 @@ func convertReflectValue[T any](obj any, value reflect.Value) T { //nolint:iretu return converted } - -// Greater asserts that the first element is strictly greater than the second. -// -// Usage: -// -// assertions.Greater(t, 2, 1) -// assertions.Greater(t, float64(2), float64(1)) -// assertions.Greater(t, "b", "a") -// -// Examples: -// -// success: 2, 1 -// failure: 1, 2 -func Greater(t T, e1 any, e2 any, msgAndArgs ...any) bool { - if h, ok := t.(H); 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") -// -// Examples: -// -// success: 2, 1 -// failure: 1, 2 -func GreaterOrEqual(t T, e1 any, e2 any, msgAndArgs ...any) bool { - if h, ok := t.(H); 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 strictly less than the second. -// -// assert.Less(t, 1, 2) -// assert.Less(t, float64(1), float64(2)) -// assert.Less(t, "a", "b") -// -// Examples: -// -// success: 1, 2 -// failure: 2, 1 -func Less(t T, e1 any, e2 any, msgAndArgs ...any) bool { - if h, ok := t.(H); 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") -// -// Examples: -// -// success: 1, 2 -// failure: 2, 1 -func LessOrEqual(t T, e1 any, e2 any, msgAndArgs ...any) bool { - if h, ok := t.(H); 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 strictly positive. -// -// assert.Positive(t, 1) -// assert.Positive(t, 1.23) -// -// Examples: -// -// success: 1 -// failure: -1 -func Positive(t T, e any, msgAndArgs ...any) bool { - if h, ok := t.(H); 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 strictly negative. -// -// assert.Negative(t, -1) -// assert.Negative(t, -1.23) -// -// Examples: -// -// success: -1 -// failure: 1 -func Negative(t T, e any, msgAndArgs ...any) bool { - if h, ok := t.(H); 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 T, e1 any, e2 any, allowedComparesResults []compareResult, failMessage string, msgAndArgs ...any) bool { - if h, ok := t.(H); 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 { - return slices.Contains(values, value) -} diff --git a/internal/assertions/condition.go b/internal/assertions/condition.go index bce0b47b2..fef478a76 100644 --- a/internal/assertions/condition.go +++ b/internal/assertions/condition.go @@ -11,11 +11,16 @@ import ( // Condition uses a Comparison to assert a complex condition. // -// Examples: +// # Usage +// +// assertions.Condition(t, func() bool { return myCondition }) +// +// # Examples // // success: func() bool { return true } // failure: func() bool { return false } func Condition(t T, comp Comparison, msgAndArgs ...any) bool { + // Domain: condition if h, ok := t.(H); ok { h.Helper() } @@ -29,13 +34,16 @@ func Condition(t T, comp Comparison, msgAndArgs ...any) bool { // 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) +// # Usage +// +// assertions.Eventually(t, func() bool { return true; }, time.Second, 10*time.Millisecond) // -// Examples: +// # Examples // // success: func() bool { return true }, 100*time.Millisecond, 20*time.Millisecond // failure: func() bool { return false }, 100*time.Millisecond, 20*time.Millisecond func Eventually(t T, condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...any) bool { + // Domain: condition if h, ok := t.(H); ok { h.Helper() } @@ -70,8 +78,14 @@ func Eventually(t T, condition func() bool, waitFor time.Duration, tick time.Dur } } -// CollectT implements the T interface and collects all errors. +// CollectT implements the [T] interface and collects all errors. type CollectT struct { + // Domain: condition + // + // Maintainer: + // 1. we should verify if the use of runtime.GoExit is correct in this context. + // 2. deprecated methods removed. + // 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. @@ -92,16 +106,6 @@ func (c *CollectT) FailNow() { 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(T) { - panic("Copy() is deprecated") -} - func (c *CollectT) fail() { if !c.failed() { c.errors = []error{} // Make it non-nil to mark a failure. @@ -121,21 +125,24 @@ func (c *CollectT) failed() bool { // If the condition is not met before waitFor, the collected errors of // the last tick are copied to t. // +// # Usage +// // externalValue := false // go func() { // time.Sleep(8*time.Second) // externalValue = true // }() -// assert.EventuallyWithT(t, func(c *assert.CollectT) { +// assertions.EventuallyWithT(t, func(c *assertions.CollectT) { // // add assertions as needed; any assertion failure will fail the current tick -// assert.True(c, externalValue, "expected 'externalValue' to be true") +// assertions.True(c, externalValue, "expected 'externalValue' to be true") // }, 10*time.Second, 1*time.Second, "external state has not changed to 'true'; still false") // -// Examples: +// # Examples // // success: func(c *CollectT) { True(c,true) }, 100*time.Millisecond, 20*time.Millisecond // failure: func(c *CollectT) { False(c,true) }, 100*time.Millisecond, 20*time.Millisecond func EventuallyWithT(t T, condition func(collect *CollectT), waitFor time.Duration, tick time.Duration, msgAndArgs ...any) bool { + // Domain: condition if h, ok := t.(H); ok { h.Helper() } @@ -186,13 +193,16 @@ func EventuallyWithT(t T, condition func(collect *CollectT), waitFor time.Durati // 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) +// # Usage +// +// assertions.Never(t, func() bool { return false; }, time.Second, 10*time.Millisecond) // -// Examples: +// # Examples // // success: func() bool { return false }, 100*time.Millisecond, 20*time.Millisecond // failure: func() bool { return true }, 100*time.Millisecond, 20*time.Millisecond func Never(t T, condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...any) bool { + // Domain: condition if h, ok := t.(H); ok { h.Helper() } diff --git a/internal/assertions/doc.go b/internal/assertions/doc.go index c45a34574..7ba6e3031 100644 --- a/internal/assertions/doc.go +++ b/internal/assertions/doc.go @@ -7,4 +7,24 @@ // It serves as a base to develop and test testify, // whereas the publicly exposed API (in packages assert and require) // is generated. +// +// # Domains +// +// - boolean: asserting boolean values +// - collection: asserting slices and maps +// - comparison: comparing ordered values +// - condition: expressing assertions using conditions +// - equality: asserting two things are equal +// - error: asserting errors +// - file: asserting OS files +// - http: asserting HTTP response and body +// - json: asserting JSON documents +// - number: asserting numbers +// - ordering: asserting how collections are ordered +// - panic: asserting a panic behavior +// - string: asserting strings +// - testing: mimicks methods from the testing standard library +// - time: asserting times and durations +// - type: asserting types rather than values +// - yaml: asserting yaml documents package assertions diff --git a/internal/assertions/enable/yaml/enable_yaml.go b/internal/assertions/enable/yaml/enable_yaml.go index 4b95a335c..7daa1289d 100644 --- a/internal/assertions/enable/yaml/enable_yaml.go +++ b/internal/assertions/enable/yaml/enable_yaml.go @@ -28,7 +28,6 @@ import ( ) `, ) - } return enableYAMLUnmarshal(in, out) } diff --git a/internal/assertions/equal.go b/internal/assertions/equal.go index 77c14d552..88384e94c 100644 --- a/internal/assertions/equal.go +++ b/internal/assertions/equal.go @@ -12,17 +12,21 @@ import ( // 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. +// referenced values (as opposed to the memory addresses). +// +// Function equality cannot be determined and will always fail. // -// Examples: +// # Usage +// +// assertions.Equal(t, 123, 123) +// +// # Examples // // success: 123, 123 // failure: 123, 456 func Equal(t T, expected, actual any, msgAndArgs ...any) bool { + // Domain: equality if h, ok := t.(H); ok { h.Helper() } @@ -44,16 +48,19 @@ func Equal(t T, expected, actual any, msgAndArgs ...any) bool { // 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. // -// Examples: +// # Usage +// +// assertions.Same(t, ptr1, ptr2) +// +// # Examples // // success: &staticVar, staticVarPtr // failure: &staticVar, ptr("static string") func Same(t T, expected, actual any, msgAndArgs ...any) bool { + // Domain: equality if h, ok := t.(H); ok { h.Helper() } @@ -75,16 +82,19 @@ func Same(t T, expected, actual any, msgAndArgs ...any) bool { // 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. // -// Examples: +// # Usage +// +// assertions.NotSame(t, ptr1, ptr2) +// +// # Examples // // success: &staticVar, ptr("static string") // failure: &staticVar, staticVarPtr func NotSame(t T, expected, actual any, msgAndArgs ...any) bool { + // Domain: equality if h, ok := t.(H); ok { h.Helper() } @@ -103,68 +113,19 @@ func NotSame(t T, expected, actual any, msgAndArgs ...any) bool { return true } -// validateEqualArgs checks whether provided arguments can be safely used in the -// Equal/NotEqual functions. -func validateEqualArgs(expected, actual any) error { - if expected == nil && actual == nil { - return nil - } - - if isFunction(expected) || isFunction(actual) { - return errors.New("cannot take func type as argument") - } - return nil -} - -// 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 any) (same bool, ok bool) { - firstPtr, secondPtr := reflect.ValueOf(first), reflect.ValueOf(second) - if firstPtr.Kind() != reflect.Pointer || secondPtr.Kind() != reflect.Pointer { - 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 any) (e string, a string) { - if reflect.TypeOf(expected) != reflect.TypeOf(actual) { - return fmt.Sprintf("%T(%s)", expected, truncatingFormat("%#v", expected)), - fmt.Sprintf("%T(%s)", actual, truncatingFormat("%#v", actual)) - } - switch expected.(type) { - case time.Duration, uint, uint8, uint16, uint32, uint64: - return fmt.Sprint(expected), fmt.Sprint(actual) - default: - return truncatingFormat("%#v", expected), truncatingFormat("%#v", actual) - } -} - // EqualValues asserts that two objects are equal or convertible to the larger // type and equal. // -// assert.EqualValues(t, uint32(123), int32(123)) +// # Usage +// +// assertions.EqualValues(t, uint32(123), int32(123)) // -// Examples: +// # Examples // // success: uint32(123), int32(123) // failure: uint32(123), int32(456) func EqualValues(t T, expected, actual any, msgAndArgs ...any) bool { + // Domain: equality if h, ok := t.(H); ok { h.Helper() } @@ -184,18 +145,21 @@ func EqualValues(t T, expected, actual any, msgAndArgs ...any) bool { // fields are also equal. This is useful for comparing structs that have private fields // that could potentially differ. // +// # Usage +// // 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 +// assertions.EqualExportedValues(t, S{1, 2}, S{1, 3}) => true +// assertions.EqualExportedValues(t, S{1, 2}, S{2, 3}) => false // -// Examples: +// # Examples // // success: &dummyStruct{A: "a", b: 1}, &dummyStruct{A: "a", b: 2} // failure: &dummyStruct{A: "a", b: 1}, &dummyStruct{A: "b", b: 1} func EqualExportedValues(t T, expected, actual any, msgAndArgs ...any) bool { + // Domain: equality if h, ok := t.(H); ok { h.Helper() } @@ -223,13 +187,16 @@ func EqualExportedValues(t T, expected, actual any, msgAndArgs ...any) bool { // Exactly asserts that two objects are equal in value and type. // -// assert.Exactly(t, int32(123), int64(123)) +// # Usage +// +// assertions.Exactly(t, int32(123), int64(123)) // -// Examples: +// # Examples // // success: int32(123), int32(123) // failure: int32(123), int64(123) func Exactly(t T, expected, actual any, msgAndArgs ...any) bool { + // Domain: equality if h, ok := t.(H); ok { h.Helper() } @@ -246,13 +213,16 @@ func Exactly(t T, expected, actual any, msgAndArgs ...any) bool { // NotNil asserts that the specified object is not nil. // -// assert.NotNil(t, err) +// # Usage // -// Examples: +// assertions.NotNil(t, err) +// +// # Examples // // success: "not nil" // failure: nil func NotNil(t T, object any, msgAndArgs ...any) bool { + // Domain: equality if !isNil(object) { return true } @@ -262,34 +232,18 @@ func NotNil(t T, object any, msgAndArgs ...any) bool { 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 any) 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() - default: - return false - } -} - // Nil asserts that the specified object is nil. // -// assert.Nil(t, err) +// # Usage +// +// assertions.Nil(t, err) // -// Examples: +// # Examples // // success: nil // failure: "not nil" func Nil(t T, object any, msgAndArgs ...any) bool { + // Domain: equality if isNil(object) { return true } @@ -301,7 +255,7 @@ func Nil(t T, object any, msgAndArgs ...any) bool { // Empty asserts that the given value is "empty". // -// [Zero values] are "empty". +// Zero values are "empty". // // Arrays are "empty" if every element is the zero value of the type (stricter than "empty"). // @@ -309,15 +263,18 @@ func Nil(t T, object any, msgAndArgs ...any) bool { // // Pointer values are "empty" if the pointer is nil or if the pointed value is "empty". // -// assert.Empty(t, obj) +// # Usage // -// Examples: +// assertions.Empty(t, obj) +// +// # Examples // // success: "" // failure: "not empty" // // [Zero values]: https://go.dev/ref/spec#The_zero_value func Empty(t T, object any, msgAndArgs ...any) bool { + // Domain: equality pass := isEmpty(object) if !pass { if h, ok := t.(H); ok { @@ -331,15 +288,18 @@ func Empty(t T, object any, msgAndArgs ...any) bool { // NotEmpty asserts that the specified object is NOT [Empty]. // +// # Usage +// // if assert.NotEmpty(t, obj) { -// assert.Equal(t, "two", obj[1]) +// assertions.Equal(t, "two", obj[1]) // } // -// Examples: +// # Examples // // success: "not empty" // failure: "" func NotEmpty(t T, object any, msgAndArgs ...any) bool { + // Domain: equality pass := !isEmpty(object) if !pass { if h, ok := t.(H); ok { @@ -353,16 +313,19 @@ func NotEmpty(t T, object any, msgAndArgs ...any) bool { // NotEqual asserts that the specified values are NOT equal. // -// assert.NotEqual(t, obj1, obj2) +// # Usage +// +// assertions.NotEqual(t, obj1, obj2) // // Pointer variable equality is determined based on the equality of the // referenced values (as opposed to the memory addresses). // -// Examples: +// # Examples // // success: 123, 456 // failure: 123, 123 func NotEqual(t T, expected, actual any, msgAndArgs ...any) bool { + // Domain: equality if h, ok := t.(H); ok { h.Helper() } @@ -380,13 +343,16 @@ func NotEqual(t T, expected, actual any, msgAndArgs ...any) bool { // NotEqualValues asserts that two objects are not equal even when converted to the same type. // -// assert.NotEqualValues(t, obj1, obj2) +// # Usage // -// Examples: +// assertions.NotEqualValues(t, obj1, obj2) +// +// # Examples // // success: uint32(123), int32(456) // failure: uint32(123), int32(123) func NotEqualValues(t T, expected, actual any, msgAndArgs ...any) bool { + // Domain: equality if h, ok := t.(H); ok { h.Helper() } @@ -398,6 +364,77 @@ func NotEqualValues(t T, expected, actual any, msgAndArgs ...any) bool { return true } +// isNil checks if a specified object is nil or not, without Failing. +func isNil(object any) 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() + default: + return false + } +} + +// validateEqualArgs checks whether provided arguments can be safely used in the +// Equal/NotEqual functions. +func validateEqualArgs(expected, actual any) error { + if expected == nil && actual == nil { + return nil + } + + if isFunction(expected) || isFunction(actual) { + return errors.New("cannot take func type as argument") + } + return nil +} + +// 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 any) (same bool, ok bool) { + firstPtr, secondPtr := reflect.ValueOf(first), reflect.ValueOf(second) + if firstPtr.Kind() != reflect.Pointer || secondPtr.Kind() != reflect.Pointer { + 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 any) (e string, a string) { + if reflect.TypeOf(expected) != reflect.TypeOf(actual) { + return fmt.Sprintf("%T(%s)", expected, truncatingFormat("%#v", expected)), + fmt.Sprintf("%T(%s)", actual, truncatingFormat("%#v", actual)) + } + switch expected.(type) { + case time.Duration, uint, uint8, uint16, uint32, uint64: + return fmt.Sprint(expected), fmt.Sprint(actual) + default: + return truncatingFormat("%#v", expected), truncatingFormat("%#v", actual) + } +} + // isEmpty gets whether the specified object is considered empty or not. func isEmpty(object any) bool { // get nil case out of the way diff --git a/internal/assertions/equal_test.go b/internal/assertions/equal_test.go index a8172692c..9d16be64c 100644 --- a/internal/assertions/equal_test.go +++ b/internal/assertions/equal_test.go @@ -140,9 +140,8 @@ func TestEqualSame(t *testing.T) { if Same(mock, dps, ps) { t.Error("Same should return false") } - expPat := - fmt.Sprintf(`expected: &%[1]s.s\{i:1\} \(%[1]s.sPtr\)\((0x[a-f0-9]+)\)\s*\n`, shortpkg) + - fmt.Sprintf(`\s+actual : &%[1]s.s\{i:1\} \(\*%[1]s.s\)\((0x[a-f0-9]+)\)`, shortpkg) + expPat := fmt.Sprintf(`expected: &%[1]s.s\{i:1\} \(%[1]s.sPtr\)\((0x[a-f0-9]+)\)\s*\n`, shortpkg) + + fmt.Sprintf(`\s+actual : &%[1]s.s\{i:1\} \(\*%[1]s.s\)\((0x[a-f0-9]+)\)`, shortpkg) Regexp(t, regexp.MustCompile(expPat), mock.errorString()) }) } @@ -216,7 +215,7 @@ func TestEqualValuesAndNotEqualValues(t *testing.T) { func TestEqualEmpty(t *testing.T) { t.Parallel() - // TODO(fredbi): redundant test context declaration + // Proposal for enhancement: redundant test context declaration chWithValue := make(chan struct{}, 1) chWithValue <- struct{}{} var tiP *time.Time @@ -592,20 +591,20 @@ func equalEmptyCases() iter.Seq[equalEmptyCase] { name: "string with only spaces is not empty", value: " ", expectedResult: false, - expectedErrMsg: "Should be empty, but was \n", // TODO FIX THIS strange error message + expectedErrMsg: "Should be empty, but was \n", // Proposal for enhancement: 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 + // Proposal for enhancement: This is the exact same error message as for an empty string + expectedErrMsg: "Should be empty, but was \n", // Proposal for enhancement: 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 + // Proposal for enhancement: 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", @@ -614,14 +613,14 @@ func equalEmptyCases() iter.Seq[equalEmptyCase] { 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 + // Proposal for enhancement: 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 + // Proposal for enhancement: The line feeds and tab are not helping to figure what is expected expectedErrMsg: "" + "Should be empty, but was \n" + "\n" + @@ -633,7 +632,7 @@ func equalEmptyCases() iter.Seq[equalEmptyCase] { 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 + // Proposal for enhancement: here you cannot figure out what is expected expectedErrMsg: "Should be empty, but was \u00a0\n", }, @@ -660,7 +659,7 @@ func equalNotEmptyCases() iter.Seq[equalNotEmptyCase] { name: "Empty string is empty", value: "", expectedResult: false, - expectedErrMsg: `Should NOT be empty, but was ` + "\n", // TODO FIX THIS strange error message + expectedErrMsg: `Should NOT be empty, but was ` + "\n", // Proposal for enhancement: FIX THIS strange error message }, { name: "Nil is empty", diff --git a/internal/assertions/error.go b/internal/assertions/error.go index d95043e0b..78b350a32 100644 --- a/internal/assertions/error.go +++ b/internal/assertions/error.go @@ -15,20 +15,23 @@ import ( // 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 ErrTest = errors.New("assert.ErrTest general error for testing") // TODO: make a type and a const. +var ErrTest = errors.New("assert.ErrTest general error for testing") // Proposal for enhancement: make a type and a const. // NoError asserts that a function returned a nil error (ie. no error). // -// actualObj, err := SomeFunction() -// if assert.NoError(t, err) { -// assert.Equal(t, expectedObj, actualObj) -// } +// # Usage // -// Examples: +// actualObj, err := SomeFunction() +// if assert.NoError(t, err) { +// assertions.Equal(t, expectedObj, actualObj) +// } +// +// # Examples // // success: nil // failure: ErrTest func NoError(t T, err error, msgAndArgs ...any) bool { + // Domain: error if err != nil { if h, ok := t.(H); ok { h.Helper() @@ -41,14 +44,17 @@ func NoError(t T, err error, msgAndArgs ...any) bool { // Error asserts that a function returned a non-nil error (ie. an error). // +// # Usage +// // actualObj, err := SomeFunction() -// assert.Error(t, err) +// assertions.Error(t, err) // -// Examples: +// # Examples // // success: ErrTest // failure: nil func Error(t T, err error, msgAndArgs ...any) bool { + // Domain: error if err == nil { if h, ok := t.(H); ok { h.Helper() @@ -62,14 +68,17 @@ func Error(t T, err error, msgAndArgs ...any) bool { // EqualError asserts that a function returned a non-nil error (i.e. an error) // and that it is equal to the provided error. // +// # Usage +// // actualObj, err := SomeFunction() -// assert.EqualError(t, err, expectedErrorString) +// assertions.EqualError(t, err, expectedErrorString) // -// Examples: +// # Examples // // success: ErrTest, "assert.ErrTest general error for testing" // failure: ErrTest, "wrong error message" func EqualError(t T, theError error, errString string, msgAndArgs ...any) bool { + // Domain: error if h, ok := t.(H); ok { h.Helper() } @@ -90,14 +99,17 @@ func EqualError(t T, theError error, errString string, msgAndArgs ...any) bool { // ErrorContains asserts that a function returned a non-nil error (i.e. an // error) and that the error contains the specified substring. // +// # Usage +// // actualObj, err := SomeFunction() -// assert.ErrorContains(t, err, expectedErrorSubString) +// assertions.ErrorContains(t, err, expectedErrorSubString) // -// Examples: +// # Examples // // success: ErrTest, "general error" // failure: ErrTest, "not in message" func ErrorContains(t T, theError error, contains string, msgAndArgs ...any) bool { + // Domain: error if h, ok := t.(H); ok { h.Helper() } @@ -114,13 +126,19 @@ func ErrorContains(t T, theError error, contains string, msgAndArgs ...any) bool } // ErrorIs asserts that at least one of the errors in err's chain matches target. -// This is a wrapper for errors.Is. // -// Examples: +// This is a wrapper for [errors.Is]. +// +// # Usage +// +// assertions.ErrorIs(t, err, io.EOF) +// +// # Examples // // success: fmt.Errorf("wrap: %w", io.EOF), io.EOF // failure: ErrTest, io.EOF func ErrorIs(t T, err, target error, msgAndArgs ...any) bool { + // Domain: error if h, ok := t.(H); ok { h.Helper() } @@ -145,13 +163,19 @@ func ErrorIs(t T, err, target error, msgAndArgs ...any) bool { } // NotErrorIs asserts that none of the errors in err's chain matches target. -// This is a wrapper for errors.Is. // -// Examples: +// This is a wrapper for [errors.Is]. +// +// # Usage +// +// assertions.NotErrorIs(t, err, io.EOF) +// +// # Examples // // success: ErrTest, io.EOF // failure: fmt.Errorf("wrap: %w", io.EOF), io.EOF func NotErrorIs(t T, err, target error, msgAndArgs ...any) bool { + // Domain: error if h, ok := t.(H); ok { h.Helper() } @@ -173,13 +197,19 @@ func NotErrorIs(t T, err, target error, msgAndArgs ...any) bool { } // 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. // -// Examples: +// This is a wrapper for [errors.As]. +// +// # Usage +// +// assertions.ErrorAs(t, err, &target) +// +// # Examples // // success: fmt.Errorf("wrap: %w", &dummyError{}), new(*dummyError) // failure: ErrTest, new(*dummyError) func ErrorAs(t T, err error, target any, msgAndArgs ...any) bool { + // Domain: error if h, ok := t.(H); ok { h.Helper() } @@ -204,11 +234,16 @@ func ErrorAs(t T, err error, target any, msgAndArgs ...any) bool { // NotErrorAs asserts that none of the errors in err's chain matches target, // but if so, sets target to that error value. // -// Examples: +// # Usage +// +// assertions.NotErrorAs(t, err, &target) +// +// # Examples // // success: ErrTest, new(*dummyError) // failure: fmt.Errorf("wrap: %w", &dummyError{}), new(*dummyError) func NotErrorAs(t T, err error, target any, msgAndArgs ...any) bool { + // Domain: error if h, ok := t.(H); ok { h.Helper() } diff --git a/internal/assertions/error_test.go b/internal/assertions/error_test.go index ba02ecadb..91ea008b8 100644 --- a/internal/assertions/error_test.go +++ b/internal/assertions/error_test.go @@ -213,11 +213,6 @@ func TestError(t *testing.T) { True(t, Error(mock, err), "Error with error should return True") - /* TODO(fred): testing formatted versions of method should be generated - // 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 diff --git a/internal/assertions/file.go b/internal/assertions/file.go index c4a6bab30..086b06e72 100644 --- a/internal/assertions/file.go +++ b/internal/assertions/file.go @@ -12,11 +12,16 @@ import ( // 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. // -// Examples: +// # Usage +// +// assertions.FileExists(t, "path/to/file") +// +// # Examples // // success: filepath.Join(testDataPath(),"existing_file") // failure: filepath.Join(testDataPath(),"non_existing_file") func FileExists(t T, path string, msgAndArgs ...any) bool { + // Domain: file if h, ok := t.(H); ok { h.Helper() } @@ -36,11 +41,16 @@ func FileExists(t T, path string, msgAndArgs ...any) bool { // NoFileExists checks whether a file does not exist in a given path. It fails // if the path points to an existing _file_ only. // -// Examples: +// # Usage +// +// assertions.NoFileExists(t, "path/to/file") +// +// # Examples // // success: filepath.Join(testDataPath(),"non_existing_file") // failure: filepath.Join(testDataPath(),"existing_file") func NoFileExists(t T, path string, msgAndArgs ...any) bool { + // Domain: file if h, ok := t.(H); ok { h.Helper() } @@ -57,11 +67,16 @@ func NoFileExists(t T, path string, msgAndArgs ...any) bool { // 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. // -// Examples: +// # Usage +// +// assertions.DirExists(t, "path/to/directory") +// +// # Examples // // success: filepath.Join(testDataPath(),"existing_dir") // failure: filepath.Join(testDataPath(),"non_existing_dir") func DirExists(t T, path string, msgAndArgs ...any) bool { + // Domain: file if h, ok := t.(H); ok { h.Helper() } @@ -80,11 +95,17 @@ func DirExists(t T, path string, msgAndArgs ...any) bool { // NoDirExists checks whether a directory does not exist in the given path. // It fails if the path points to an existing _directory_ only. -// Examples: +// +// # Usage +// +// assertions.NoDirExists(t, "path/to/directory") +// +// # Examples // // success: filepath.Join(testDataPath(),"non_existing_dir") // failure: filepath.Join(testDataPath(),"existing_dir") func NoDirExists(t T, path string, msgAndArgs ...any) bool { + // Domain: file if h, ok := t.(H); ok { h.Helper() } @@ -104,11 +125,16 @@ func NoDirExists(t T, path string, msgAndArgs ...any) bool { // FileEmpty checks whether a file exists in the given path and is empty. // It fails if the file is not empty, if the path points to a directory or there is an error when trying to check the file. // -// Examples: +// # Usage +// +// assertions.FileEmpty(t, "path/to/file") +// +// # Examples // // success: filepath.Join(testDataPath(),"empty_file") // failure: filepath.Join(testDataPath(),"existing_file") func FileEmpty(t T, path string, msgAndArgs ...any) bool { + // Domain: file if h, ok := t.(H); ok { h.Helper() } @@ -140,11 +166,16 @@ func FileEmpty(t T, path string, msgAndArgs ...any) bool { // FileNotEmpty checks whether a file exists in the given path and is not empty. // It fails if the file is empty, if the path points to a directory or there is an error when trying to check the file. // -// Examples: +// # Usage +// +// assertions.FileNotEmpty(t, "path/to/file") +// +// # Examples // // success: filepath.Join(testDataPath(),"existing_file") // failure: filepath.Join(testDataPath(),"empty_file") func FileNotEmpty(t T, path string, msgAndArgs ...any) bool { + // Domain: file if h, ok := t.(H); ok { h.Helper() } diff --git a/internal/assertions/http.go b/internal/assertions/http.go index 7350765c5..509162865 100644 --- a/internal/assertions/http.go +++ b/internal/assertions/http.go @@ -12,30 +12,20 @@ import ( "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.NewRequestWithContext(context.Background(), 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). // -// Examples: +// # Usage +// +// assertions.HTTPSuccess(t, myHandler, "POST", "http://www.google.com", nil) +// +// # Examples // // success: httpOK, "GET", "/", nil // failure: httpError, "GET", "/", nil func HTTPSuccess(t T, handler http.HandlerFunc, method, url string, values url.Values, msgAndArgs ...any) bool { + // Domain: http if h, ok := t.(H); ok { h.Helper() } @@ -54,15 +44,18 @@ func HTTPSuccess(t T, handler http.HandlerFunc, method, url string, values url.V // 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). // -// Examples: +// # Usage +// +// assertions.HTTPRedirect(t, myHandler, "GET", "/a/b/c", url.Values{"a": []string{"b", "c"}} +// +// # Examples // // success: httpRedirect, "GET", "/", nil // failure: httpError, "GET", "/", nil func HTTPRedirect(t T, handler http.HandlerFunc, method, url string, values url.Values, msgAndArgs ...any) bool { + // Domain: http if h, ok := t.(H); ok { h.Helper() } @@ -81,15 +74,18 @@ func HTTPRedirect(t T, handler http.HandlerFunc, method, url string, values url. // 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). // -// Examples: +// # Usage +// +// assertions.HTTPError(t, myHandler, "POST", "/a/b/c", url.Values{"a": []string{"b", "c"}} +// +// # Examples // // success: httpError, "GET", "/", nil // failure: httpOK, "GET", "/", nil func HTTPError(t T, handler http.HandlerFunc, method, url string, values url.Values, msgAndArgs ...any) bool { + // Domain: http if h, ok := t.(H); ok { h.Helper() } @@ -108,15 +104,18 @@ func HTTPError(t T, handler http.HandlerFunc, method, url string, values url.Val // 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). // -// Examples: +// # Usage +// +// assertions.HTTPStatusCode(t, myHandler, "GET", "/notImplemented", nil, 501) +// +// # Examples // // success: httpOK, "GET", "/", nil, http.StatusOK // failure: httpError, "GET", "/", nil, http.StatusOK func HTTPStatusCode(t T, handler http.HandlerFunc, method, url string, values url.Values, statuscode int, msgAndArgs ...any) bool { + // Domain: http if h, ok := t.(H); ok { h.Helper() } @@ -136,6 +135,7 @@ func HTTPStatusCode(t T, handler http.HandlerFunc, method, url string, values ur // HTTPBody is a helper that returns the HTTP body of the response. // It returns the empty string if building a new request fails. func HTTPBody(handler http.HandlerFunc, method, url string, values url.Values) string { + // Domain: http w := httptest.NewRecorder() if len(values) > 0 { url += "?" + values.Encode() @@ -150,15 +150,18 @@ func HTTPBody(handler http.HandlerFunc, method, url string, values url.Values) s // 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). // -// Examples: +// # Usage +// +// assertions.HTTPBodyContains(t, myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky") +// +// # Examples // // success: httpBody, "GET", "/", url.Values{"name": []string{"World"}}, "Hello, World!" // failure: httpBody, "GET", "/", url.Values{"name": []string{"Bob"}}, "Hello, World!" func HTTPBodyContains(t T, handler http.HandlerFunc, method, url string, values url.Values, str any, msgAndArgs ...any) bool { + // Domain: http if h, ok := t.(H); ok { h.Helper() } @@ -175,15 +178,18 @@ func HTTPBodyContains(t T, handler http.HandlerFunc, method, url string, values // 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). // -// Examples: +// # Usage +// +// assertions.HTTPBodyNotContains(t, myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky") +// +// # Examples // // success: httpBody, "GET", "/", url.Values{"name": []string{"World"}}, "Hello, Bob!" // failure: httpBody, "GET", "/", url.Values{"name": []string{"Bob"}}, "Hello, Bob!" func HTTPBodyNotContains(t T, handler http.HandlerFunc, method, url string, values url.Values, str any, msgAndArgs ...any) bool { + // Domain: http if h, ok := t.(H); ok { h.Helper() } @@ -196,3 +202,17 @@ func HTTPBodyNotContains(t T, handler http.HandlerFunc, method, url string, valu return !contains } + +// httpCode is a helper that returns the 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.NewRequestWithContext(context.Background(), method, url, http.NoBody) + if err != nil { + return -1, err + } + req.URL.RawQuery = values.Encode() + handler(w, req) + return w.Code, nil +} diff --git a/internal/assertions/http_test.go b/internal/assertions/http_test.go index 592742ac1..6a2156578 100644 --- a/internal/assertions/http_test.go +++ b/internal/assertions/http_test.go @@ -111,7 +111,7 @@ func TestHTTPStatusCode(t *testing.T) { False(t, mock4.Failed()) } -func TestHTTPStatusWrapper(t *testing.T) { // TODO(fredbi): check if redundant (wrappers tests are generated) +func TestHTTPStatusWrapper(t *testing.T) { t.Parallel() mock := new(mockT) diff --git a/internal/assertions/json.go b/internal/assertions/json.go index c7a3e5924..a343b66db 100644 --- a/internal/assertions/json.go +++ b/internal/assertions/json.go @@ -11,13 +11,16 @@ import ( // JSONEqBytes asserts that two JSON byte slices are equivalent. // -// assert.JSONEqBytes(t, []byte(`{"hello": "world", "foo": "bar"}`), []byte(`{"foo": "bar", "hello": "world"}`)) +// # Usage // -// Examples: +// assertions.JSONEqBytes(t, []byte(`{"hello": "world", "foo": "bar"}`), []byte(`{"foo": "bar", "hello": "world"}`)) +// +// # Examples // // success: []byte(`{"hello": "world", "foo": "bar"}`), []byte(`{"foo": "bar", "hello": "world"}`) // failure: []byte(`{"hello": "world", "foo": "bar"}`), []byte(`[{"foo": "bar"}, {"hello": "world"}]`) func JSONEqBytes(t T, expected, actual []byte, msgAndArgs ...any) bool { + // Domain: json if h, ok := t.(H); ok { h.Helper() } @@ -41,12 +44,15 @@ func JSONEqBytes(t T, expected, actual []byte, msgAndArgs ...any) bool { // JSONEq asserts that two JSON strings are equivalent. // -// assert.JSONEq(t, `{"hello": "world", "foo": "bar"}`, `{"foo": "bar", "hello": "world"}`) +// # Usage +// +// assertions.JSONEq(t, `{"hello": "world", "foo": "bar"}`, `{"foo": "bar", "hello": "world"}`) // -// Examples: +// # Examples // // success: `{"hello": "world", "foo": "bar"}`, `{"foo": "bar", "hello": "world"}` // failure: `{"hello": "world", "foo": "bar"}`, `[{"foo": "bar"}, {"hello": "world"}]` func JSONEq(t T, expected, actual string, msgAndArgs ...any) bool { + // Domain: json return JSONEqBytes(t, []byte(expected), []byte(actual), msgAndArgs) } diff --git a/internal/assertions/json_test.go b/internal/assertions/json_test.go index 49ff2fff4..2ed28f2ae 100644 --- a/internal/assertions/json_test.go +++ b/internal/assertions/json_test.go @@ -5,7 +5,7 @@ package assertions import "testing" -// TODO(fred): load fixtures and assertions from embedded testdata +// Proposal for enhancement: load fixtures and assertions from embedded testdata func TestJSONEq_EqualSONString(t *testing.T) { t.Parallel() diff --git a/internal/assertions/number.go b/internal/assertions/number.go index fdf60f963..e8cad406b 100644 --- a/internal/assertions/number.go +++ b/internal/assertions/number.go @@ -13,13 +13,16 @@ import ( // InDelta asserts that the two numerals are within delta of each other. // -// assert.InDelta(t, math.Pi, 22/7.0, 0.01) +// # Usage // -// Examples: +// assertions.InDelta(t, math.Pi, 22/7.0, 0.01) +// +// # Examples // // success: 1.0, 1.01, 0.02 // failure: 1.0, 1.1, 0.05 func InDelta(t T, expected, actual any, delta float64, msgAndArgs ...any) bool { + // Domain: number if h, ok := t.(H); ok { h.Helper() } @@ -53,11 +56,16 @@ func InDelta(t T, expected, actual any, delta float64, msgAndArgs ...any) bool { // InDeltaSlice is the same as InDelta, except it compares two slices. // -// Examples: +// # Usage +// +// assertions.InDeltaSlice(t, []float64{1.0, 2.0}, []float64{1.01, 2.01}, 0.02) +// +// # Examples // // success: []float64{1.0, 2.0}, []float64{1.01, 2.01}, 0.02 // failure: []float64{1.0, 2.0}, []float64{1.1, 2.1}, 0.05 func InDeltaSlice(t T, expected, actual any, delta float64, msgAndArgs ...any) bool { + // Domain: number if h, ok := t.(H); ok { h.Helper() } @@ -82,11 +90,16 @@ func InDeltaSlice(t T, expected, actual any, delta float64, msgAndArgs ...any) b // InDeltaMapValues is the same as InDelta, but it compares all values between two maps. Both maps must have exactly the same keys. // -// Examples: +// # Usage +// +// assertions.InDeltaMapValues(t, map[string]float64{"a": 1.0}, map[string]float64{"a": 1.01}, 0.02) +// +// # Examples // // success: map[string]float64{"a": 1.0}, map[string]float64{"a": 1.01}, 0.02 // failure: map[string]float64{"a": 1.0}, map[string]float64{"a": 1.1}, 0.05 func InDeltaMapValues(t T, expected, actual any, delta float64, msgAndArgs ...any) bool { + // Domain: number if h, ok := t.(H); ok { h.Helper() } @@ -129,35 +142,18 @@ func InDeltaMapValues(t T, expected, actual any, delta float64, msgAndArgs ...an return true } -func calcRelativeError(expected, actual any) (float64, error) { - af, aok := toFloat(expected) - bf, bok := toFloat(actual) - if !aok || !bok { - return 0, errors.New("parameters must be numerical") - } - 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, errors.New("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. // -// Examples: +// # Usage +// +// assertions.InEpsilon(t, 100.0, 101.0, 0.02) +// +// # Examples // // success: 100.0, 101.0, 0.02 // failure: 100.0, 110.0, 0.05 func InEpsilon(t T, expected, actual any, epsilon float64, msgAndArgs ...any) bool { + // Domain: number if h, ok := t.(H); ok { h.Helper() } @@ -181,11 +177,16 @@ func InEpsilon(t T, expected, actual any, epsilon float64, msgAndArgs ...any) bo // InEpsilonSlice is the same as InEpsilon, except it compares each value from two slices. // -// Examples: +// # Usage +// +// assertions.InEpsilonSlice(t, []float64{100.0, 200.0}, []float64{101.0, 202.0}, 0.02) +// +// # Examples // // success: []float64{100.0, 200.0}, []float64{101.0, 202.0}, 0.02 // failure: []float64{100.0, 200.0}, []float64{110.0, 220.0}, 0.05 func InEpsilonSlice(t T, expected, actual any, epsilon float64, msgAndArgs ...any) bool { + // Domain: number if h, ok := t.(H); ok { h.Helper() } @@ -215,6 +216,28 @@ func InEpsilonSlice(t T, expected, actual any, epsilon float64, msgAndArgs ...an return true } +func calcRelativeError(expected, actual any) (float64, error) { + af, aok := toFloat(expected) + bf, bok := toFloat(actual) + if !aok || !bok { + return 0, errors.New("parameters must be numerical") + } + 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, errors.New("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 +} + func toFloat(x any) (float64, bool) { var xf float64 xok := true diff --git a/internal/assertions/object.go b/internal/assertions/object.go index c760f95dc..0c796b892 100644 --- a/internal/assertions/object.go +++ b/internal/assertions/object.go @@ -33,19 +33,6 @@ func ObjectsAreEqual(expected, actual any) bool { return bytes.Equal(exp, act) } -// 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 any) 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 any) bool { diff --git a/internal/assertions/object_test.go b/internal/assertions/object_test.go index 5a50ea122..0f6349fef 100644 --- a/internal/assertions/object_test.go +++ b/internal/assertions/object_test.go @@ -41,20 +41,6 @@ func TestObjectsAreEqualValues(t *testing.T) { } } -func TestObjectsExportedFieldsAreEqual(t *testing.T) { - t.Parallel() - - for c := range objectExportedFieldsCases() { - 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 TestObjectsCopyExportedFields(t *testing.T) { t.Parallel() @@ -100,10 +86,6 @@ type S struct { notExported2 Nested } -type S2 struct { - foo any -} - type S3 struct { Exported1 *Nested Exported2 *Nested @@ -173,65 +155,6 @@ func objectEqualValuesCases() iter.Seq[objectEqualCase] { }) } -func objectExportedFieldsCases() iter.Seq[objectEqualCase] { - intValue := 1 - - return slices.Values([]objectEqualCase{ - {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[any]*Nested{nil: nil}, 2}, - Nested{map[any]*Nested{nil: nil}, 2}, - true, - }, - { - Nested{map[any]*Nested{"a": nil}, 2}, - Nested{map[any]*Nested{"a": nil}, 2}, - true, - }, - { - Nested{map[any]*Nested{"a": nil}, 2}, - Nested{map[any]*Nested{"a": {1, 2}}, 2}, - false, - }, - { - Nested{map[any]Nested{"a": {1, 2}, "b": {3, 4}}, 2}, - Nested{map[any]Nested{"a": {1, 5}, "b": {3, 7}}, 2}, - true, - }, - { - Nested{map[any]Nested{"a": {1, 2}, "b": {3, 4}}, 2}, - Nested{map[any]Nested{"a": {2, 2}, "b": {3, 4}}, 2}, - false, - }, - }) -} - type objectCopyFieldsCase struct { input any expected any diff --git a/internal/assertions/order.go b/internal/assertions/order.go index f01c94a57..c44b83666 100644 --- a/internal/assertions/order.go +++ b/internal/assertions/order.go @@ -8,56 +8,20 @@ import ( "reflect" ) -// isOrdered checks that collection contains orderable elements. -func isOrdered(t T, object any, allowedComparesResults []compareResult, failMessage string, msgAndArgs ...any) bool { - objKind := reflect.TypeOf(object).Kind() - if objKind != reflect.Slice && objKind != reflect.Array { - return Fail(t, fmt.Sprintf("object %T is not an ordered collection", object), msgAndArgs...) - } - - 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"}) +// # Usage // -// Examples: +// assertions.IsIncreasing(t, []int{1, 2, 3}) +// assertions.IsIncreasing(t, []float{1, 2}) +// assertions.IsIncreasing(t, []string{"a", "b"}) +// +// # Examples // // success: []int{1, 2, 3} // failure: []int{1, 1, 2} func IsIncreasing(t T, object any, msgAndArgs ...any) bool { + // Domain: ordering if h, ok := t.(H); ok { h.Helper() } @@ -66,15 +30,18 @@ func IsIncreasing(t T, object any, msgAndArgs ...any) bool { // 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"}) +// # Usage +// +// assertions.IsNonIncreasing(t, []int{2, 1, 1}) +// assertions.IsNonIncreasing(t, []float{2, 1}) +// assertions.IsNonIncreasing(t, []string{"b", "a"}) // -// Examples: +// # Examples // // success: []int{2, 1, 1} // failure: []int{1, 2, 3} func IsNonIncreasing(t T, object any, msgAndArgs ...any) bool { + // Domain: ordering if h, ok := t.(H); ok { h.Helper() } @@ -83,15 +50,18 @@ func IsNonIncreasing(t T, object any, msgAndArgs ...any) bool { // 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"}) +// # Usage // -// Examples: +// assertions.IsDecreasing(t, []int{2, 1, 0}) +// assertions.IsDecreasing(t, []float{2, 1}) +// assertions.IsDecreasing(t, []string{"b", "a"}) +// +// # Examples // // success: []int{3, 2, 1} // failure: []int{1, 2, 3} func IsDecreasing(t T, object any, msgAndArgs ...any) bool { + // Domain: ordering if h, ok := t.(H); ok { h.Helper() } @@ -100,17 +70,59 @@ func IsDecreasing(t T, object any, msgAndArgs ...any) bool { // 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"}) +// # Usage +// +// assertions.IsNonDecreasing(t, []int{1, 1, 2}) +// assertions.IsNonDecreasing(t, []float{1, 2}) +// assertions.IsNonDecreasing(t, []string{"a", "b"}) // -// Examples: +// # Examples // // success: []int{1, 1, 2} // failure: []int{2, 1, 1} func IsNonDecreasing(t T, object any, msgAndArgs ...any) bool { + // Domain: ordering if h, ok := t.(H); ok { h.Helper() } return isOrdered(t, object, []compareResult{compareLess, compareEqual}, "\"%v\" is not less than or equal to \"%v\"", msgAndArgs...) } + +// isOrdered checks that collection contains orderable elements. +func isOrdered(t T, object any, allowedComparesResults []compareResult, failMessage string, msgAndArgs ...any) bool { + objKind := reflect.TypeOf(object).Kind() + if objKind != reflect.Slice && objKind != reflect.Array { + return Fail(t, fmt.Sprintf("object %T is not an ordered collection", object), msgAndArgs...) + } + + 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 +} diff --git a/internal/assertions/panic.go b/internal/assertions/panic.go index f0b13a1ab..083ad46c5 100644 --- a/internal/assertions/panic.go +++ b/internal/assertions/panic.go @@ -16,40 +16,18 @@ type PanicTestFunc func() // for table driven tests. type PanicAssertionFunc func(t T, f PanicTestFunc, msgAndArgs ...any) bool -// didPanic returns true if the function passed to it panics. Otherwise, it returns false. -func didPanic(f PanicTestFunc) (didPanic bool, message any, stack string) { - didPanic = true - - defer func() { - message = recover() - if didPanic { - stack = string(debug.Stack()) - } - // Go 1.21 introduces runtime.PanicNilError on panic(nil), - // so maintain the same logic going forward (https://github.com/golang/go/issues/25448). - if err, ok := message.(error); ok { - if err.Error() == "panic called with nil argument" { - message = nil - } - } - }() - - // call the target function - f() - didPanic = false - - return -} - // Panics asserts that the code inside the specified PanicTestFunc panics. // -// assert.Panics(t, func(){ GoCrazy() }) +// # Usage +// +// assertions.Panics(t, func(){ GoCrazy() }) // -// Examples: +// # Examples // // success: func() { panic("panicking") } // failure: func() { } func Panics(t T, f PanicTestFunc, msgAndArgs ...any) bool { + // Domain: panic if h, ok := t.(H); ok { h.Helper() } @@ -64,13 +42,16 @@ func Panics(t T, f PanicTestFunc, msgAndArgs ...any) bool { // 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() }) +// # Usage +// +// assertions.PanicsWithValue(t, "crazy error", func(){ GoCrazy() }) // -// Examples: +// # Examples // // success: "panicking", func() { panic("panicking") } // failure: "panicking", func() { } func PanicsWithValue(t T, expected any, f PanicTestFunc, msgAndArgs ...any) bool { + // Domain: panic if h, ok := t.(H); ok { h.Helper() } @@ -90,13 +71,16 @@ func PanicsWithValue(t T, expected any, f PanicTestFunc, msgAndArgs ...any) bool // panics, and that the recovered panic value is an error that satisfies the // EqualError comparison. // -// assert.PanicsWithError(t, "crazy error", func(){ GoCrazy() }) +// # Usage // -// Examples: +// assertions.PanicsWithError(t, "crazy error", func(){ GoCrazy() }) +// +// # Examples // // success: ErrTest.Error(), func() { panic(ErrTest) } // failure: ErrTest.Error(), func() { } func PanicsWithError(t T, errString string, f PanicTestFunc, msgAndArgs ...any) bool { + // Domain: panic if h, ok := t.(H); ok { h.Helper() } @@ -121,13 +105,16 @@ func PanicsWithError(t T, errString string, f PanicTestFunc, msgAndArgs ...any) // NotPanics asserts that the code inside the specified PanicTestFunc does NOT panic. // -// assert.NotPanics(t, func(){ RemainCalm() }) +// # Usage +// +// assertions.NotPanics(t, func(){ RemainCalm() }) // -// Examples: +// # Examples // // success: func() { } // failure: func() { panic("panicking") } func NotPanics(t T, f PanicTestFunc, msgAndArgs ...any) bool { + // Domain: panic if h, ok := t.(H); ok { h.Helper() } @@ -138,3 +125,28 @@ func NotPanics(t T, f PanicTestFunc, msgAndArgs ...any) bool { return true } + +// didPanic returns true if the function passed to it panics. Otherwise, it returns false. +func didPanic(f PanicTestFunc) (didPanic bool, message any, stack string) { + didPanic = true + + defer func() { + message = recover() + if didPanic { + stack = string(debug.Stack()) + } + // Go 1.21 introduces runtime.PanicNilError on panic(nil), + // so maintain the same logic going forward (https://github.com/golang/go/issues/25448). + if err, ok := message.(error); ok { + if err.Error() == "panic called with nil argument" { + message = nil + } + } + }() + + // call the target function + f() + didPanic = false + + return +} diff --git a/internal/assertions/string.go b/internal/assertions/string.go index 0e8726554..097d72c59 100644 --- a/internal/assertions/string.go +++ b/internal/assertions/string.go @@ -10,14 +10,17 @@ import ( // 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") +// # Usage // -// Examples: +// assertions.Regexp(t, regexp.MustCompile("start"), "it's starting") +// assertions.Regexp(t, "start...$", "it's not starting") +// +// # Examples // // success: "^start", "starting" // failure: "^start", "not starting" func Regexp(t T, rx any, str any, msgAndArgs ...any) bool { + // Domain: string if h, ok := t.(H); ok { h.Helper() } @@ -38,14 +41,17 @@ func Regexp(t T, rx any, str any, msgAndArgs ...any) bool { // 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") +// # Usage +// +// assertions.NotRegexp(t, regexp.MustCompile("starts"), "it's starting") +// assertions.NotRegexp(t, "^start", "it's not starting") // -// Examples: +// # Examples // // success: "^start", "not starting" // failure: "^start", "starting" func NotRegexp(t T, rx any, str any, msgAndArgs ...any) bool { + // Domain: string if h, ok := t.(H); ok { h.Helper() } diff --git a/internal/assertions/testing.go b/internal/assertions/testing.go index d1b0a72e2..531866587 100644 --- a/internal/assertions/testing.go +++ b/internal/assertions/testing.go @@ -13,23 +13,29 @@ import ( "unicode/utf8" ) -/* 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 { + // 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.*/ + // + // Maintainer: it is not necessary to export CallerInfo. This should remain an internal implementation detail. return callerInfo(1) } // FailNow fails test. // -// Example: +// # Usage +// +// assertions.FailNow(t, "failed") +// +// # Examples // // failure: "failed" func FailNow(t T, failureMessage string, msgAndArgs ...any) bool { + // Domain: testing if h, ok := t.(H); ok { h.Helper() } @@ -52,10 +58,15 @@ func FailNow(t T, failureMessage string, msgAndArgs ...any) bool { // Fail reports a failure through. // -// Example: +// # Usage +// +// assertions.Fail(t, "failed") +// +// # Examples // // failure: "failed" func Fail(t T, failureMessage string, msgAndArgs ...any) bool { + // Domain: testing if h, ok := t.(H); ok { h.Helper() } diff --git a/internal/assertions/time.go b/internal/assertions/time.go index f82d44861..9f9574c14 100644 --- a/internal/assertions/time.go +++ b/internal/assertions/time.go @@ -10,13 +10,16 @@ import ( // WithinDuration asserts that the two times are within duration delta of each other. // -// assert.WithinDuration(t, time.Now(), 10*time.Second) +// # Usage // -// Examples: +// assertions.WithinDuration(t, time.Now(), 10*time.Second) +// +// # Examples // // success: time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC), time.Date(2024, 1, 1, 12, 0, 1, 0, time.UTC), 2*time.Second // failure: time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC), time.Date(2024, 1, 1, 12, 0, 10, 0, time.UTC), 1*time.Second func WithinDuration(t T, expected, actual time.Time, delta time.Duration, msgAndArgs ...any) bool { + // Domain: time if h, ok := t.(H); ok { h.Helper() } @@ -31,13 +34,16 @@ func WithinDuration(t T, expected, actual time.Time, delta time.Duration, msgAnd // 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)) +// # Usage +// +// assertions.WithinRange(t, time.Now(), time.Now().Add(-time.Second), time.Now().Add(time.Second)) // -// Examples: +// # Examples // // success: time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC), time.Date(2024, 1, 1, 11, 0, 0, 0, time.UTC), time.Date(2024, 1, 1, 13, 0, 0, 0, time.UTC) // failure: time.Date(2024, 1, 1, 14, 0, 0, 0, time.UTC), time.Date(2024, 1, 1, 11, 0, 0, 0, time.UTC), time.Date(2024, 1, 1, 13, 0, 0, 0, time.UTC) func WithinRange(t T, actual, start, end time.Time, msgAndArgs ...any) bool { + // Domain: time if h, ok := t.(H); ok { h.Helper() } diff --git a/internal/assertions/type.go b/internal/assertions/type.go index 663160d51..e916c64aa 100644 --- a/internal/assertions/type.go +++ b/internal/assertions/type.go @@ -10,13 +10,16 @@ import ( // Implements asserts that an object is implemented by the specified interface. // -// assert.Implements(t, (*MyInterface)(nil), new(MyObject)) +// # Usage // -// Examples: +// assertions.Implements(t, (*MyInterface)(nil), new(MyObject)) +// +// # Examples // // success: ptr(dummyInterface), new(testing.T) // failure: (*error)(nil), new(testing.T) func Implements(t T, interfaceObject any, object any, msgAndArgs ...any) bool { + // Domain: type if h, ok := t.(H); ok { h.Helper() } @@ -34,13 +37,16 @@ func Implements(t T, interfaceObject any, object any, msgAndArgs ...any) bool { // NotImplements asserts that an object does not implement the specified interface. // -// assert.NotImplements(t, (*MyInterface)(nil), new(MyObject)) +// # Usage +// +// assertions.NotImplements(t, (*MyInterface)(nil), new(MyObject)) // -// Examples: +// # Examples // // success: (*error)(nil), new(testing.T) // failure: ptr(dummyInterface), new(testing.T) func NotImplements(t T, interfaceObject any, object any, msgAndArgs ...any) bool { + // Domain: type if h, ok := t.(H); ok { h.Helper() } @@ -56,19 +62,18 @@ func NotImplements(t T, interfaceObject any, object any, msgAndArgs ...any) bool return true } -func isType(expectedType, object any) 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{}) +// # Usage // -// Examples: +// assertions.IsType(t, &MyStruct{}, &MyStruct{}) +// +// # Examples // // success: 123, 456 // failure: int32(123), int64(456) func IsType(t T, expectedType, object any, msgAndArgs ...any) bool { + // Domain: type if isType(expectedType, object) { return true } @@ -80,13 +85,16 @@ func IsType(t T, expectedType, object any, msgAndArgs ...any) bool { // IsNotType asserts that the specified objects are not of the same type. // -// assert.IsNotType(t, &NotMyStruct{}, &MyStruct{}) +// # Usage +// +// assertions.IsNotType(t, &NotMyStruct{}, &MyStruct{}) // -// Examples: +// # Examples // // success: int32(123), int64(456) // failure: 123, 456 func IsNotType(t T, theType, object any, msgAndArgs ...any) bool { + // Domain: type if !isType(theType, object) { return true } @@ -98,11 +106,16 @@ func IsNotType(t T, theType, object any, msgAndArgs ...any) bool { // Zero asserts that i is the zero value for its type. // -// Examples: +// # Usage +// +// assertions.Zero(t, obj) +// +// # Examples // // success: 0 // failure: 1 func Zero(t T, i any, msgAndArgs ...any) bool { + // Domain: type if h, ok := t.(H); ok { h.Helper() } @@ -114,11 +127,16 @@ func Zero(t T, i any, msgAndArgs ...any) bool { // NotZero asserts that i is not the zero value for its type. // -// Examples: +// # Usage +// +// assertions.NotZero(t, obj) +// +// # Examples // // success: 1 // failure: 0 func NotZero(t T, i any, msgAndArgs ...any) bool { + // Domain: type if h, ok := t.(H); ok { h.Helper() } @@ -127,3 +145,7 @@ func NotZero(t T, i any, msgAndArgs ...any) bool { } return true } + +func isType(expectedType, object any) bool { + return ObjectsAreEqual(reflect.TypeOf(object), reflect.TypeOf(expectedType)) +} diff --git a/internal/assertions/yaml.go b/internal/assertions/yaml.go index 79bae424e..8836d1f65 100644 --- a/internal/assertions/yaml.go +++ b/internal/assertions/yaml.go @@ -11,7 +11,7 @@ import ( // YAMLEq asserts that the first documents in the two YAML strings are equivalent. // -// Usage: +// # Usage // // expected := `--- // key: value @@ -25,11 +25,12 @@ import ( // ` // assertions.YAMLEq(t, expected, actual) // -// Example: +// # Examples // -// panic: "key: value", "key: value" -// should panic without the yaml feature enabled. +// panic: "key: value", "key: value" +// should panic without the yaml feature enabled. func YAMLEq(t T, expected string, actual string, msgAndArgs ...any) bool { + // Domain: yaml if h, ok := t.(H); ok { h.Helper() } diff --git a/package_test.go b/package_test.go index 3ce151303..943da5c0c 100644 --- a/package_test.go +++ b/package_test.go @@ -7,10 +7,15 @@ import ( "testing" "github.com/go-openapi/testify/v2/assert" + "github.com/go-openapi/testify/v2/require" ) -func TestImports(t *testing.T) { +func TestImportAssert(t *testing.T) { if assert.Equal(t, 1, 1) != true { t.Error("Something is wrong.") } } + +func TestImportRequire(t *testing.T) { + require.Equal(t, 1, 1) +} diff --git a/require/require_assertions.go b/require/require_assertions.go index 4d0a027a0..e509c8078 100644 --- a/require/require_assertions.go +++ b/require/require_assertions.go @@ -1,7 +1,8 @@ // SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 -// Code generated with github.com/go-openapi/testify/v2/codegen@master [sha: bb2c19fba6c03f46cb643b3bcdc1d647ea1453ab]; DO NOT EDIT. +// Code generated with github.com/go-openapi/testify/v2/codegen; DO NOT EDIT. +// Generated on 2026-01-02 (version v1.2.2-760-g97c29e3) using codegen version master [sha: 97c29e3dbfc40800a080863ceea81db0cfd6e858] package require @@ -15,7 +16,11 @@ import ( // Condition uses a Comparison to assert a complex condition. // -// Examples: +// # Usage +// +// assertions.Condition(t, func() bool { return myCondition }) +// +// # Examples // // success: func() bool { return true } // failure: func() bool { return false } @@ -35,13 +40,13 @@ func Condition(t T, comp Comparison, msgAndArgs ...any) { // Contains asserts that the specified string, list(array, slice...) or map contains the // specified substring or element. // -// Usage: +// # Usage // // assertions.Contains(t, "Hello World", "World") -// assertions.Contains(t, ["Hello", "World"], "World") -// assertions.Contains(t, {"Hello": "World"}, "Hello") +// assertions.Contains(t, []string{"Hello", "World"}, "World") +// assertions.Contains(t, map[string]string{"Hello": "World"}, "Hello") // -// Examples: +// # Examples // // success: []string{"A","B"}, "A" // failure: []string{"A","B"}, "C" @@ -61,7 +66,11 @@ func Contains(t T, s any, contains any, msgAndArgs ...any) { // 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. // -// Examples: +// # Usage +// +// assertions.DirExists(t, "path/to/directory") +// +// # Examples // // success: filepath.Join(testDataPath(),"existing_dir") // failure: filepath.Join(testDataPath(),"non_existing_dir") @@ -82,9 +91,11 @@ func DirExists(t T, path string, msgAndArgs ...any) { // listB(array, slice...) ignoring the order of the elements. If there are duplicate elements, // the number of appearances of each of them in both lists should match. // -// assert.ElementsMatch(t, [1, 3, 2, 3], [1, 3, 3, 2]). +// # Usage // -// Examples: +// assertions.ElementsMatch(t, [1, 3, 2, 3], [1, 3, 3, 2]) +// +// # Examples // // success: []int{1, 3, 2, 3}, []int{1, 3, 3, 2} // failure: []int{1, 2, 3}, []int{1, 2, 4} @@ -103,7 +114,7 @@ func ElementsMatch(t T, listA any, listB any, msgAndArgs ...any) { // Empty asserts that the given value is "empty". // -// [Zero values] are "empty". +// Zero values are "empty". // // Arrays are "empty" if every element is the zero value of the type (stricter than "empty"). // @@ -111,9 +122,11 @@ func ElementsMatch(t T, listA any, listB any, msgAndArgs ...any) { // // Pointer values are "empty" if the pointer is nil or if the pointed value is "empty". // -// assert.Empty(t, obj) +// # Usage +// +// assertions.Empty(t, obj) // -// Examples: +// # Examples // // success: "" // failure: "not empty" @@ -134,13 +147,16 @@ func Empty(t T, object any, msgAndArgs ...any) { // 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. +// referenced values (as opposed to the memory addresses). // -// Examples: +// Function equality cannot be determined and will always fail. +// +// # Usage +// +// assertions.Equal(t, 123, 123) +// +// # Examples // // success: 123, 123 // failure: 123, 456 @@ -160,10 +176,12 @@ func Equal(t T, expected any, actual any, msgAndArgs ...any) { // EqualError asserts that a function returned a non-nil error (i.e. an error) // and that it is equal to the provided error. // +// # Usage +// // actualObj, err := SomeFunction() -// assert.EqualError(t, err, expectedErrorString) +// assertions.EqualError(t, err, expectedErrorString) // -// Examples: +// # Examples // // success: ErrTest, "assert.ErrTest general error for testing" // failure: ErrTest, "wrong error message" @@ -184,14 +202,16 @@ func EqualError(t T, theError error, errString string, msgAndArgs ...any) { // fields are also equal. This is useful for comparing structs that have private fields // that could potentially differ. // +// # Usage +// // 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 +// assertions.EqualExportedValues(t, S{1, 2}, S{1, 3}) => true +// assertions.EqualExportedValues(t, S{1, 2}, S{2, 3}) => false // -// Examples: +// # Examples // // success: &dummyStruct{A: "a", b: 1}, &dummyStruct{A: "a", b: 2} // failure: &dummyStruct{A: "a", b: 1}, &dummyStruct{A: "b", b: 1} @@ -211,9 +231,11 @@ func EqualExportedValues(t T, expected any, actual any, msgAndArgs ...any) { // EqualValues asserts that two objects are equal or convertible to the larger // type and equal. // -// assert.EqualValues(t, uint32(123), int32(123)) +// # Usage +// +// assertions.EqualValues(t, uint32(123), int32(123)) // -// Examples: +// # Examples // // success: uint32(123), int32(123) // failure: uint32(123), int32(456) @@ -232,10 +254,12 @@ func EqualValues(t T, expected any, actual any, msgAndArgs ...any) { // Error asserts that a function returned a non-nil error (ie. an error). // +// # Usage +// // actualObj, err := SomeFunction() -// assert.Error(t, err) +// assertions.Error(t, err) // -// Examples: +// # Examples // // success: ErrTest // failure: nil @@ -253,9 +277,14 @@ func Error(t T, err error, msgAndArgs ...any) { } // 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. // -// Examples: +// This is a wrapper for [errors.As]. +// +// # Usage +// +// assertions.ErrorAs(t, err, &target) +// +// # Examples // // success: fmt.Errorf("wrap: %w", &dummyError{}), new(*dummyError) // failure: ErrTest, new(*dummyError) @@ -275,10 +304,12 @@ func ErrorAs(t T, err error, target any, msgAndArgs ...any) { // ErrorContains asserts that a function returned a non-nil error (i.e. an // error) and that the error contains the specified substring. // +// # Usage +// // actualObj, err := SomeFunction() -// assert.ErrorContains(t, err, expectedErrorSubString) +// assertions.ErrorContains(t, err, expectedErrorSubString) // -// Examples: +// # Examples // // success: ErrTest, "general error" // failure: ErrTest, "not in message" @@ -296,9 +327,14 @@ func ErrorContains(t T, theError error, contains string, msgAndArgs ...any) { } // ErrorIs asserts that at least one of the errors in err's chain matches target. -// This is a wrapper for errors.Is. // -// Examples: +// This is a wrapper for [errors.Is]. +// +// # Usage +// +// assertions.ErrorIs(t, err, io.EOF) +// +// # Examples // // success: fmt.Errorf("wrap: %w", io.EOF), io.EOF // failure: ErrTest, io.EOF @@ -318,9 +354,11 @@ func ErrorIs(t T, err error, target error, msgAndArgs ...any) { // 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) +// # Usage +// +// assertions.Eventually(t, func() bool { return true; }, time.Second, 10*time.Millisecond) // -// Examples: +// # Examples // // success: func() bool { return true }, 100*time.Millisecond, 20*time.Millisecond // failure: func() bool { return false }, 100*time.Millisecond, 20*time.Millisecond @@ -346,17 +384,19 @@ func Eventually(t T, condition func() bool, waitFor time.Duration, tick time.Dur // If the condition is not met before waitFor, the collected errors of // the last tick are copied to t. // +// # Usage +// // externalValue := false // go func() { // time.Sleep(8*time.Second) // externalValue = true // }() -// assert.EventuallyWithT(t, func(c *assert.CollectT) { +// assertions.EventuallyWithT(t, func(c *assertions.CollectT) { // // add assertions as needed; any assertion failure will fail the current tick -// assert.True(c, externalValue, "expected 'externalValue' to be true") +// assertions.True(c, externalValue, "expected 'externalValue' to be true") // }, 10*time.Second, 1*time.Second, "external state has not changed to 'true'; still false") // -// Examples: +// # Examples // // success: func(c *CollectT) { True(c,true) }, 100*time.Millisecond, 20*time.Millisecond // failure: func(c *CollectT) { False(c,true) }, 100*time.Millisecond, 20*time.Millisecond @@ -375,9 +415,11 @@ func EventuallyWithT(t T, condition func(collect *CollectT), waitFor time.Durati // Exactly asserts that two objects are equal in value and type. // -// assert.Exactly(t, int32(123), int64(123)) +// # Usage +// +// assertions.Exactly(t, int32(123), int64(123)) // -// Examples: +// # Examples // // success: int32(123), int32(123) // failure: int32(123), int64(123) @@ -396,7 +438,11 @@ func Exactly(t T, expected any, actual any, msgAndArgs ...any) { // Fail reports a failure through. // -// Example: +// # Usage +// +// assertions.Fail(t, "failed") +// +// # Examples // // failure: "failed" // @@ -412,7 +458,11 @@ func Fail(t T, failureMessage string, msgAndArgs ...any) { // FailNow fails test. // -// Example: +// # Usage +// +// assertions.FailNow(t, "failed") +// +// # Examples // // failure: "failed" // @@ -428,11 +478,11 @@ func FailNow(t T, failureMessage string, msgAndArgs ...any) { // False asserts that the specified value is false. // -// Usage: +// # Usage // // assertions.False(t, myBool) // -// Examples: +// # Examples // // success: 1 == 0 // failure: 1 == 1 @@ -452,7 +502,11 @@ func False(t T, value bool, msgAndArgs ...any) { // FileEmpty checks whether a file exists in the given path and is empty. // It fails if the file is not empty, if the path points to a directory or there is an error when trying to check the file. // -// Examples: +// # Usage +// +// assertions.FileEmpty(t, "path/to/file") +// +// # Examples // // success: filepath.Join(testDataPath(),"empty_file") // failure: filepath.Join(testDataPath(),"existing_file") @@ -472,7 +526,11 @@ func FileEmpty(t T, path string, msgAndArgs ...any) { // 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. // -// Examples: +// # Usage +// +// assertions.FileExists(t, "path/to/file") +// +// # Examples // // success: filepath.Join(testDataPath(),"existing_file") // failure: filepath.Join(testDataPath(),"non_existing_file") @@ -492,7 +550,11 @@ func FileExists(t T, path string, msgAndArgs ...any) { // FileNotEmpty checks whether a file exists in the given path and is not empty. // It fails if the file is empty, if the path points to a directory or there is an error when trying to check the file. // -// Examples: +// # Usage +// +// assertions.FileNotEmpty(t, "path/to/file") +// +// # Examples // // success: filepath.Join(testDataPath(),"existing_file") // failure: filepath.Join(testDataPath(),"empty_file") @@ -511,13 +573,13 @@ func FileNotEmpty(t T, path string, msgAndArgs ...any) { // Greater asserts that the first element is strictly greater than the second. // -// Usage: +// # Usage // // assertions.Greater(t, 2, 1) // assertions.Greater(t, float64(2), float64(1)) // assertions.Greater(t, "b", "a") // -// Examples: +// # Examples // // success: 2, 1 // failure: 1, 2 @@ -536,12 +598,14 @@ func Greater(t T, e1 any, e2 any, msgAndArgs ...any) { // 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") +// # Usage +// +// assertions.GreaterOrEqual(t, 2, 1) +// assertions.GreaterOrEqual(t, 2, 2) +// assertions.GreaterOrEqual(t, "b", "a") +// assertions.GreaterOrEqual(t, "b", "b") // -// Examples: +// # Examples // // success: 2, 1 // failure: 1, 2 @@ -560,11 +624,13 @@ func GreaterOrEqual(t T, e1 any, e2 any, msgAndArgs ...any) { // 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). // -// Examples: +// # Usage +// +// assertions.HTTPBodyContains(t, myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky") +// +// # Examples // // success: httpBody, "GET", "/", url.Values{"name": []string{"World"}}, "Hello, World!" // failure: httpBody, "GET", "/", url.Values{"name": []string{"Bob"}}, "Hello, World!" @@ -584,11 +650,13 @@ func HTTPBodyContains(t T, handler http.HandlerFunc, method string, url string, // 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). // -// Examples: +// # Usage +// +// assertions.HTTPBodyNotContains(t, myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky") +// +// # Examples // // success: httpBody, "GET", "/", url.Values{"name": []string{"World"}}, "Hello, Bob!" // failure: httpBody, "GET", "/", url.Values{"name": []string{"Bob"}}, "Hello, Bob!" @@ -607,11 +675,13 @@ func HTTPBodyNotContains(t T, handler http.HandlerFunc, method string, url strin // 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). // -// Examples: +// # Usage +// +// assertions.HTTPError(t, myHandler, "POST", "/a/b/c", url.Values{"a": []string{"b", "c"}} +// +// # Examples // // success: httpError, "GET", "/", nil // failure: httpOK, "GET", "/", nil @@ -630,11 +700,13 @@ func HTTPError(t T, handler http.HandlerFunc, method string, url string, values // 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). // -// Examples: +// # Usage +// +// assertions.HTTPRedirect(t, myHandler, "GET", "/a/b/c", url.Values{"a": []string{"b", "c"}} +// +// # Examples // // success: httpRedirect, "GET", "/", nil // failure: httpError, "GET", "/", nil @@ -653,11 +725,13 @@ func HTTPRedirect(t T, handler http.HandlerFunc, method string, url string, valu // 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). // -// Examples: +// # Usage +// +// assertions.HTTPStatusCode(t, myHandler, "GET", "/notImplemented", nil, 501) +// +// # Examples // // success: httpOK, "GET", "/", nil, http.StatusOK // failure: httpError, "GET", "/", nil, http.StatusOK @@ -676,11 +750,13 @@ func HTTPStatusCode(t T, handler http.HandlerFunc, method string, url string, va // 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). // -// Examples: +// # Usage +// +// assertions.HTTPSuccess(t, myHandler, "POST", "http://www.google.com", nil) +// +// # Examples // // success: httpOK, "GET", "/", nil // failure: httpError, "GET", "/", nil @@ -699,9 +775,11 @@ func HTTPSuccess(t T, handler http.HandlerFunc, method string, url string, value // Implements asserts that an object is implemented by the specified interface. // -// assert.Implements(t, (*MyInterface)(nil), new(MyObject)) +// # Usage // -// Examples: +// assertions.Implements(t, (*MyInterface)(nil), new(MyObject)) +// +// # Examples // // success: ptr(dummyInterface), new(testing.T) // failure: (*error)(nil), new(testing.T) @@ -720,9 +798,11 @@ func Implements(t T, interfaceObject any, object any, msgAndArgs ...any) { // InDelta asserts that the two numerals are within delta of each other. // -// assert.InDelta(t, math.Pi, 22/7.0, 0.01) +// # Usage +// +// assertions.InDelta(t, math.Pi, 22/7.0, 0.01) // -// Examples: +// # Examples // // success: 1.0, 1.01, 0.02 // failure: 1.0, 1.1, 0.05 @@ -741,7 +821,11 @@ func InDelta(t T, expected any, actual any, delta float64, msgAndArgs ...any) { // InDeltaMapValues is the same as InDelta, but it compares all values between two maps. Both maps must have exactly the same keys. // -// Examples: +// # Usage +// +// assertions.InDeltaMapValues(t, map[string]float64{"a": 1.0}, map[string]float64{"a": 1.01}, 0.02) +// +// # Examples // // success: map[string]float64{"a": 1.0}, map[string]float64{"a": 1.01}, 0.02 // failure: map[string]float64{"a": 1.0}, map[string]float64{"a": 1.1}, 0.05 @@ -760,7 +844,11 @@ func InDeltaMapValues(t T, expected any, actual any, delta float64, msgAndArgs . // InDeltaSlice is the same as InDelta, except it compares two slices. // -// Examples: +// # Usage +// +// assertions.InDeltaSlice(t, []float64{1.0, 2.0}, []float64{1.01, 2.01}, 0.02) +// +// # Examples // // success: []float64{1.0, 2.0}, []float64{1.01, 2.01}, 0.02 // failure: []float64{1.0, 2.0}, []float64{1.1, 2.1}, 0.05 @@ -779,7 +867,11 @@ func InDeltaSlice(t T, expected any, actual any, delta float64, msgAndArgs ...an // InEpsilon asserts that expected and actual have a relative error less than epsilon. // -// Examples: +// # Usage +// +// assertions.InEpsilon(t, 100.0, 101.0, 0.02) +// +// # Examples // // success: 100.0, 101.0, 0.02 // failure: 100.0, 110.0, 0.05 @@ -798,7 +890,11 @@ func InEpsilon(t T, expected any, actual any, epsilon float64, msgAndArgs ...any // InEpsilonSlice is the same as InEpsilon, except it compares each value from two slices. // -// Examples: +// # Usage +// +// assertions.InEpsilonSlice(t, []float64{100.0, 200.0}, []float64{101.0, 202.0}, 0.02) +// +// # Examples // // success: []float64{100.0, 200.0}, []float64{101.0, 202.0}, 0.02 // failure: []float64{100.0, 200.0}, []float64{110.0, 220.0}, 0.05 @@ -817,11 +913,13 @@ func InEpsilonSlice(t T, expected any, actual any, epsilon float64, 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"}) +// # Usage +// +// assertions.IsDecreasing(t, []int{2, 1, 0}) +// assertions.IsDecreasing(t, []float{2, 1}) +// assertions.IsDecreasing(t, []string{"b", "a"}) // -// Examples: +// # Examples // // success: []int{3, 2, 1} // failure: []int{1, 2, 3} @@ -840,11 +938,13 @@ func IsDecreasing(t T, object any, msgAndArgs ...any) { // 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"}) +// # Usage // -// Examples: +// assertions.IsIncreasing(t, []int{1, 2, 3}) +// assertions.IsIncreasing(t, []float{1, 2}) +// assertions.IsIncreasing(t, []string{"a", "b"}) +// +// # Examples // // success: []int{1, 2, 3} // failure: []int{1, 1, 2} @@ -863,11 +963,13 @@ func IsIncreasing(t T, object any, msgAndArgs ...any) { // 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"}) +// # Usage +// +// assertions.IsNonDecreasing(t, []int{1, 1, 2}) +// assertions.IsNonDecreasing(t, []float{1, 2}) +// assertions.IsNonDecreasing(t, []string{"a", "b"}) // -// Examples: +// # Examples // // success: []int{1, 1, 2} // failure: []int{2, 1, 1} @@ -886,11 +988,13 @@ func IsNonDecreasing(t T, object any, msgAndArgs ...any) { // 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"}) +// # Usage // -// Examples: +// assertions.IsNonIncreasing(t, []int{2, 1, 1}) +// assertions.IsNonIncreasing(t, []float{2, 1}) +// assertions.IsNonIncreasing(t, []string{"b", "a"}) +// +// # Examples // // success: []int{2, 1, 1} // failure: []int{1, 2, 3} @@ -909,9 +1013,11 @@ func IsNonIncreasing(t T, object any, msgAndArgs ...any) { // IsNotType asserts that the specified objects are not of the same type. // -// assert.IsNotType(t, &NotMyStruct{}, &MyStruct{}) +// # Usage +// +// assertions.IsNotType(t, &NotMyStruct{}, &MyStruct{}) // -// Examples: +// # Examples // // success: int32(123), int64(456) // failure: 123, 456 @@ -930,9 +1036,11 @@ func IsNotType(t T, theType any, object any, msgAndArgs ...any) { // IsType asserts that the specified objects are of the same type. // -// assert.IsType(t, &MyStruct{}, &MyStruct{}) +// # Usage // -// Examples: +// assertions.IsType(t, &MyStruct{}, &MyStruct{}) +// +// # Examples // // success: 123, 456 // failure: int32(123), int64(456) @@ -951,9 +1059,11 @@ func IsType(t T, expectedType any, object any, msgAndArgs ...any) { // JSONEq asserts that two JSON strings are equivalent. // -// assert.JSONEq(t, `{"hello": "world", "foo": "bar"}`, `{"foo": "bar", "hello": "world"}`) +// # Usage +// +// assertions.JSONEq(t, `{"hello": "world", "foo": "bar"}`, `{"foo": "bar", "hello": "world"}`) // -// Examples: +// # Examples // // success: `{"hello": "world", "foo": "bar"}`, `{"foo": "bar", "hello": "world"}` // failure: `{"hello": "world", "foo": "bar"}`, `[{"foo": "bar"}, {"hello": "world"}]` @@ -972,9 +1082,11 @@ func JSONEq(t T, expected string, actual string, msgAndArgs ...any) { // JSONEqBytes asserts that two JSON byte slices are equivalent. // -// assert.JSONEqBytes(t, []byte(`{"hello": "world", "foo": "bar"}`), []byte(`{"foo": "bar", "hello": "world"}`)) +// # Usage // -// Examples: +// assertions.JSONEqBytes(t, []byte(`{"hello": "world", "foo": "bar"}`), []byte(`{"foo": "bar", "hello": "world"}`)) +// +// # Examples // // success: []byte(`{"hello": "world", "foo": "bar"}`), []byte(`{"foo": "bar", "hello": "world"}`) // failure: []byte(`{"hello": "world", "foo": "bar"}`), []byte(`[{"foo": "bar"}, {"hello": "world"}]`) @@ -999,13 +1111,13 @@ func JSONEqBytes(t T, expected []byte, actual []byte, msgAndArgs ...any) { // // See also [reflect.Len]. // -// Usage: +// # Usage // // assertions.Len(t, mySlice, 3) // assertions.Len(t, myString, 4) // assertions.Len(t, myMap, 5) // -// Examples: +// # Examples // // success: []string{"A","B"}, 2 // failure: []string{"A","B"}, 1 @@ -1024,11 +1136,13 @@ func Len(t T, object any, length int, msgAndArgs ...any) { // Less asserts that the first element is strictly less than the second. // -// assert.Less(t, 1, 2) -// assert.Less(t, float64(1), float64(2)) -// assert.Less(t, "a", "b") +// # Usage +// +// assertions.Less(t, 1, 2) +// assertions.Less(t, float64(1), float64(2)) +// assertions.Less(t, "a", "b") // -// Examples: +// # Examples // // success: 1, 2 // failure: 2, 1 @@ -1047,12 +1161,14 @@ func Less(t T, e1 any, e2 any, msgAndArgs ...any) { // 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") +// # Usage +// +// assertions.LessOrEqual(t, 1, 2) +// assertions.LessOrEqual(t, 2, 2) +// assertions.LessOrEqual(t, "a", "b") +// assertions.LessOrEqual(t, "b", "b") // -// Examples: +// # Examples // // success: 1, 2 // failure: 2, 1 @@ -1071,10 +1187,12 @@ func LessOrEqual(t T, e1 any, e2 any, msgAndArgs ...any) { // Negative asserts that the specified element is strictly negative. // -// assert.Negative(t, -1) -// assert.Negative(t, -1.23) +// # Usage // -// Examples: +// assertions.Negative(t, -1) +// assertions.Negative(t, -1.23) +// +// # Examples // // success: -1 // failure: 1 @@ -1094,9 +1212,11 @@ func Negative(t T, e any, msgAndArgs ...any) { // 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) +// # Usage +// +// assertions.Never(t, func() bool { return false; }, time.Second, 10*time.Millisecond) // -// Examples: +// # Examples // // success: func() bool { return false }, 100*time.Millisecond, 20*time.Millisecond // failure: func() bool { return true }, 100*time.Millisecond, 20*time.Millisecond @@ -1115,9 +1235,11 @@ func Never(t T, condition func() bool, waitFor time.Duration, tick time.Duration // Nil asserts that the specified object is nil. // -// assert.Nil(t, err) +// # Usage // -// Examples: +// assertions.Nil(t, err) +// +// # Examples // // success: nil // failure: "not nil" @@ -1136,7 +1258,12 @@ func Nil(t T, object any, msgAndArgs ...any) { // NoDirExists checks whether a directory does not exist in the given path. // It fails if the path points to an existing _directory_ only. -// Examples: +// +// # Usage +// +// assertions.NoDirExists(t, "path/to/directory") +// +// # Examples // // success: filepath.Join(testDataPath(),"non_existing_dir") // failure: filepath.Join(testDataPath(),"existing_dir") @@ -1155,12 +1282,14 @@ func NoDirExists(t T, path string, msgAndArgs ...any) { // NoError asserts that a function returned a nil error (ie. no error). // -// actualObj, err := SomeFunction() -// if assert.NoError(t, err) { -// assert.Equal(t, expectedObj, actualObj) -// } +// # Usage +// +// actualObj, err := SomeFunction() +// if assert.NoError(t, err) { +// assertions.Equal(t, expectedObj, actualObj) +// } // -// Examples: +// # Examples // // success: nil // failure: ErrTest @@ -1180,7 +1309,11 @@ func NoError(t T, err error, msgAndArgs ...any) { // NoFileExists checks whether a file does not exist in a given path. It fails // if the path points to an existing _file_ only. // -// Examples: +// # Usage +// +// assertions.NoFileExists(t, "path/to/file") +// +// # Examples // // success: filepath.Join(testDataPath(),"non_existing_file") // failure: filepath.Join(testDataPath(),"existing_file") @@ -1200,13 +1333,13 @@ func NoFileExists(t T, path string, msgAndArgs ...any) { // NotContains asserts that the specified string, list(array, slice...) or map does NOT contain the // specified substring or element. // -// Usage: +// # Usage // // assertions.NotContains(t, "Hello World", "Earth") // assertions.NotContains(t, ["Hello", "World"], "Earth") // assertions.NotContains(t, {"Hello": "World"}, "Earth") // -// Examples: +// # Examples // // success: []string{"A","B"}, "C" // failure: []string{"A","B"}, "B" @@ -1228,13 +1361,13 @@ func NotContains(t T, s any, contains any, msgAndArgs ...any) { // 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 +// # Usage // -// assert.NotElementsMatch(t, [1, 1, 2, 3], [1, 2, 3]) -> true +// assertions.NotElementsMatch(t, [1, 1, 2, 3], [1, 1, 2, 3]) -> false +// assertions.NotElementsMatch(t, [1, 1, 2, 3], [1, 2, 3]) -> true +// assertions.NotElementsMatch(t, [1, 2, 3], [1, 2, 4]) -> true // -// assert.NotElementsMatch(t, [1, 2, 3], [1, 2, 4]) -> true. -// -// Examples: +// # Examples // // success: []int{1, 2, 3}, []int{1, 2, 4} // failure: []int{1, 3, 2, 3}, []int{1, 3, 3, 2} @@ -1253,11 +1386,13 @@ func NotElementsMatch(t T, listA any, listB any, msgAndArgs ...any) { // NotEmpty asserts that the specified object is NOT [Empty]. // +// # Usage +// // if assert.NotEmpty(t, obj) { -// assert.Equal(t, "two", obj[1]) +// assertions.Equal(t, "two", obj[1]) // } // -// Examples: +// # Examples // // success: "not empty" // failure: "" @@ -1276,12 +1411,14 @@ func NotEmpty(t T, object any, msgAndArgs ...any) { // NotEqual asserts that the specified values are NOT equal. // -// assert.NotEqual(t, obj1, obj2) +// # Usage +// +// assertions.NotEqual(t, obj1, obj2) // // Pointer variable equality is determined based on the equality of the // referenced values (as opposed to the memory addresses). // -// Examples: +// # Examples // // success: 123, 456 // failure: 123, 123 @@ -1300,9 +1437,11 @@ func NotEqual(t T, expected any, actual any, msgAndArgs ...any) { // NotEqualValues asserts that two objects are not equal even when converted to the same type. // -// assert.NotEqualValues(t, obj1, obj2) +// # Usage // -// Examples: +// assertions.NotEqualValues(t, obj1, obj2) +// +// # Examples // // success: uint32(123), int32(456) // failure: uint32(123), int32(123) @@ -1322,7 +1461,11 @@ func NotEqualValues(t T, expected any, actual any, msgAndArgs ...any) { // NotErrorAs asserts that none of the errors in err's chain matches target, // but if so, sets target to that error value. // -// Examples: +// # Usage +// +// assertions.NotErrorAs(t, err, &target) +// +// # Examples // // success: ErrTest, new(*dummyError) // failure: fmt.Errorf("wrap: %w", &dummyError{}), new(*dummyError) @@ -1340,9 +1483,14 @@ func NotErrorAs(t T, err error, target any, msgAndArgs ...any) { } // NotErrorIs asserts that none of the errors in err's chain matches target. -// This is a wrapper for errors.Is. // -// Examples: +// This is a wrapper for [errors.Is]. +// +// # Usage +// +// assertions.NotErrorIs(t, err, io.EOF) +// +// # Examples // // success: ErrTest, io.EOF // failure: fmt.Errorf("wrap: %w", io.EOF), io.EOF @@ -1361,9 +1509,11 @@ func NotErrorIs(t T, err error, target error, msgAndArgs ...any) { // NotImplements asserts that an object does not implement the specified interface. // -// assert.NotImplements(t, (*MyInterface)(nil), new(MyObject)) +// # Usage // -// Examples: +// assertions.NotImplements(t, (*MyInterface)(nil), new(MyObject)) +// +// # Examples // // success: (*error)(nil), new(testing.T) // failure: ptr(dummyInterface), new(testing.T) @@ -1382,9 +1532,11 @@ func NotImplements(t T, interfaceObject any, object any, msgAndArgs ...any) { // NotNil asserts that the specified object is not nil. // -// assert.NotNil(t, err) +// # Usage +// +// assertions.NotNil(t, err) // -// Examples: +// # Examples // // success: "not nil" // failure: nil @@ -1403,9 +1555,11 @@ func NotNil(t T, object any, msgAndArgs ...any) { // NotPanics asserts that the code inside the specified PanicTestFunc does NOT panic. // -// assert.NotPanics(t, func(){ RemainCalm() }) +// # Usage // -// Examples: +// assertions.NotPanics(t, func(){ RemainCalm() }) +// +// # Examples // // success: func() { } // failure: func() { panic("panicking") } @@ -1424,10 +1578,12 @@ func NotPanics(t T, f assertions.PanicTestFunc, msgAndArgs ...any) { // 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") +// # Usage +// +// assertions.NotRegexp(t, regexp.MustCompile("starts"), "it's starting") +// assertions.NotRegexp(t, "^start", "it's not starting") // -// Examples: +// # Examples // // success: "^start", "not starting" // failure: "^start", "starting" @@ -1446,12 +1602,14 @@ func NotRegexp(t T, rx any, str any, msgAndArgs ...any) { // 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. // -// Examples: +// # Usage +// +// assertions.NotSame(t, ptr1, ptr2) +// +// # Examples // // success: &staticVar, ptr("static string") // failure: &staticVar, staticVarPtr @@ -1473,12 +1631,14 @@ func NotSame(t T, expected any, actual any, msgAndArgs ...any) { // 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"]) +// # Usage +// +// assertions.NotSubset(t, [1, 3, 4], [1, 2]) +// assertions.NotSubset(t, {"x": 1, "y": 2}, {"z": 3}) +// assertions.NotSubset(t, [1, 3, 4], {1: "one", 2: "two"}) +// assertions.NotSubset(t, {"x": 1, "y": 2}, ["z"]) // -// Examples: +// # Examples // // success: []int{1, 2, 3}, []int{4, 5} // failure: []int{1, 2, 3}, []int{1, 2} @@ -1497,7 +1657,11 @@ func NotSubset(t T, list any, subset any, msgAndArgs ...any) { // NotZero asserts that i is not the zero value for its type. // -// Examples: +// # Usage +// +// assertions.NotZero(t, obj) +// +// # Examples // // success: 1 // failure: 0 @@ -1516,9 +1680,11 @@ func NotZero(t T, i any, msgAndArgs ...any) { // Panics asserts that the code inside the specified PanicTestFunc panics. // -// assert.Panics(t, func(){ GoCrazy() }) +// # Usage +// +// assertions.Panics(t, func(){ GoCrazy() }) // -// Examples: +// # Examples // // success: func() { panic("panicking") } // failure: func() { } @@ -1539,9 +1705,11 @@ func Panics(t T, f assertions.PanicTestFunc, msgAndArgs ...any) { // panics, and that the recovered panic value is an error that satisfies the // EqualError comparison. // -// assert.PanicsWithError(t, "crazy error", func(){ GoCrazy() }) +// # Usage // -// Examples: +// assertions.PanicsWithError(t, "crazy error", func(){ GoCrazy() }) +// +// # Examples // // success: ErrTest.Error(), func() { panic(ErrTest) } // failure: ErrTest.Error(), func() { } @@ -1561,9 +1729,11 @@ func PanicsWithError(t T, errString string, f assertions.PanicTestFunc, msgAndAr // 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() }) +// # Usage +// +// assertions.PanicsWithValue(t, "crazy error", func(){ GoCrazy() }) // -// Examples: +// # Examples // // success: "panicking", func() { panic("panicking") } // failure: "panicking", func() { } @@ -1582,10 +1752,12 @@ func PanicsWithValue(t T, expected any, f assertions.PanicTestFunc, msgAndArgs . // Positive asserts that the specified element is strictly positive. // -// assert.Positive(t, 1) -// assert.Positive(t, 1.23) +// # Usage // -// Examples: +// assertions.Positive(t, 1) +// assertions.Positive(t, 1.23) +// +// # Examples // // success: 1 // failure: -1 @@ -1604,10 +1776,12 @@ func Positive(t T, e any, msgAndArgs ...any) { // 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") +// # Usage +// +// assertions.Regexp(t, regexp.MustCompile("start"), "it's starting") +// assertions.Regexp(t, "start...$", "it's not starting") // -// Examples: +// # Examples // // success: "^start", "starting" // failure: "^start", "not starting" @@ -1626,12 +1800,14 @@ func Regexp(t T, rx any, str any, msgAndArgs ...any) { // 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. // -// Examples: +// # Usage +// +// assertions.Same(t, ptr1, ptr2) +// +// # Examples // // success: &staticVar, staticVarPtr // failure: &staticVar, ptr("static string") @@ -1654,12 +1830,14 @@ func Same(t T, expected any, actual any, msgAndArgs ...any) { // 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"]) +// # Usage // -// Examples: +// assertions.Subset(t, [1, 2, 3], [1, 2]) +// assertions.Subset(t, {"x": 1, "y": 2}, {"x": 1}) +// assertions.Subset(t, [1, 2, 3], {1: "one", 2: "two"}) +// assertions.Subset(t, {"x": 1, "y": 2}, ["x"]) +// +// # Examples // // success: []int{1, 2, 3}, []int{1, 2} // failure: []int{1, 2, 3}, []int{4, 5} @@ -1678,11 +1856,11 @@ func Subset(t T, list any, subset any, msgAndArgs ...any) { // True asserts that the specified value is true. // -// Usage: +// # Usage // // assertions.True(t, myBool) // -// Examples: +// # Examples // // success: 1 == 1 // failure: 1 == 0 @@ -1701,9 +1879,11 @@ func True(t T, value bool, msgAndArgs ...any) { // WithinDuration asserts that the two times are within duration delta of each other. // -// assert.WithinDuration(t, time.Now(), 10*time.Second) +// # Usage +// +// assertions.WithinDuration(t, time.Now(), 10*time.Second) // -// Examples: +// # Examples // // success: time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC), time.Date(2024, 1, 1, 12, 0, 1, 0, time.UTC), 2*time.Second // failure: time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC), time.Date(2024, 1, 1, 12, 0, 10, 0, time.UTC), 1*time.Second @@ -1722,9 +1902,11 @@ func WithinDuration(t T, expected time.Time, actual time.Time, delta time.Durati // 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)) +// # Usage // -// Examples: +// assertions.WithinRange(t, time.Now(), time.Now().Add(-time.Second), time.Now().Add(time.Second)) +// +// # Examples // // success: time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC), time.Date(2024, 1, 1, 11, 0, 0, 0, time.UTC), time.Date(2024, 1, 1, 13, 0, 0, 0, time.UTC) // failure: time.Date(2024, 1, 1, 14, 0, 0, 0, time.UTC), time.Date(2024, 1, 1, 11, 0, 0, 0, time.UTC), time.Date(2024, 1, 1, 13, 0, 0, 0, time.UTC) @@ -1743,7 +1925,7 @@ func WithinRange(t T, actual time.Time, start time.Time, end time.Time, msgAndAr // YAMLEq asserts that the first documents in the two YAML strings are equivalent. // -// Usage: +// # Usage // // expected := `--- // key: value @@ -1757,10 +1939,10 @@ func WithinRange(t T, actual time.Time, start time.Time, end time.Time, msgAndAr // ` // assertions.YAMLEq(t, expected, actual) // -// Example: +// # Examples // -// panic: "key: value", "key: value" -// should panic without the yaml feature enabled +// panic: "key: value", "key: value" +// should panic without the yaml feature enabled. // // Upon failure, the test [T] is marked as failed and stops execution. func YAMLEq(t T, expected string, actual string, msgAndArgs ...any) { @@ -1776,7 +1958,11 @@ func YAMLEq(t T, expected string, actual string, msgAndArgs ...any) { // Zero asserts that i is the zero value for its type. // -// Examples: +// # Usage +// +// assertions.Zero(t, obj) +// +// # Examples // // success: 0 // failure: 1 diff --git a/require/require_assertions_test.go b/require/require_assertions_test.go index 97b7eb52c..5e6f53a5e 100644 --- a/require/require_assertions_test.go +++ b/require/require_assertions_test.go @@ -1,7 +1,8 @@ // SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 -// Code generated with github.com/go-openapi/testify/v2/codegen@master [sha: bb2c19fba6c03f46cb643b3bcdc1d647ea1453ab]; DO NOT EDIT. +// Code generated with github.com/go-openapi/testify/v2/codegen; DO NOT EDIT. +// Generated on 2026-01-02 (version v1.2.2-760-g97c29e3) using codegen version master [sha: 97c29e3dbfc40800a080863ceea81db0cfd6e858] package require @@ -1550,7 +1551,7 @@ func TestYAMLEq(t *testing.T) { Panics(t, func() { YAMLEq(t, "key: value", "key: value") - }, "should panic without the yaml feature enabled") + }, "should panic without the yaml feature enabled.") }) } diff --git a/require/require_examples_test.go b/require/require_examples_test.go index cd9ae59e6..bc1a05118 100644 --- a/require/require_examples_test.go +++ b/require/require_examples_test.go @@ -1,7 +1,8 @@ // SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 -// Code generated with github.com/go-openapi/testify/v2/codegen@master [sha: bb2c19fba6c03f46cb643b3bcdc1d647ea1453ab]; DO NOT EDIT. +// Code generated with github.com/go-openapi/testify/v2/codegen; DO NOT EDIT. +// Generated on 2026-01-02 (version v1.2.2-760-g97c29e3) using codegen version master [sha: 97c29e3dbfc40800a080863ceea81db0cfd6e858] package require_test @@ -14,12 +15,15 @@ import ( "testing" "time" + "github.com/go-openapi/testify/v2/assert" "github.com/go-openapi/testify/v2/require" ) func ExampleCondition() { t := new(testing.T) - require.Condition(t, func() bool { return true }) + require.Condition(t, func() bool { + return true + }) fmt.Println("passed") // Output: passed @@ -123,7 +127,9 @@ func ExampleErrorIs() { func ExampleEventually() { t := new(testing.T) - require.Eventually(t, func() bool { return true }, 100*time.Millisecond, 20*time.Millisecond) + require.Eventually(t, func() bool { + return true + }, 100*time.Millisecond, 20*time.Millisecond) fmt.Println("passed") // Output: passed @@ -131,7 +137,9 @@ func ExampleEventually() { func ExampleEventuallyWithT() { t := new(testing.T) - require.EventuallyWithT(t, func(c *require.CollectT) { require.True(c, true) }, 100*time.Millisecond, 20*time.Millisecond) + require.EventuallyWithT(t, func(c *assert.CollectT) { + assert.True(c, true) + }, 100*time.Millisecond, 20*time.Millisecond) fmt.Println("passed") // Output: passed @@ -395,7 +403,9 @@ func ExampleNegative() { func ExampleNever() { t := new(testing.T) - require.Never(t, func() bool { return false }, 100*time.Millisecond, 20*time.Millisecond) + require.Never(t, func() bool { + return false + }, 100*time.Millisecond, 20*time.Millisecond) fmt.Println("passed") // Output: passed @@ -507,7 +517,8 @@ func ExampleNotNil() { func ExampleNotPanics() { t := new(testing.T) - require.NotPanics(t, func() {}) + require.NotPanics(t, func() { + }) fmt.Println("passed") // Output: passed @@ -547,7 +558,9 @@ func ExampleNotZero() { func ExamplePanics() { t := new(testing.T) - require.Panics(t, func() { panic("panicking") }) + require.Panics(t, func() { + panic("panicking") + }) fmt.Println("passed") // Output: passed @@ -555,7 +568,9 @@ func ExamplePanics() { func ExamplePanicsWithError() { t := new(testing.T) - require.PanicsWithError(t, require.ErrTest.Error(), func() { panic(require.ErrTest) }) + require.PanicsWithError(t, assert.ErrTest.Error(), func() { + panic(assert.ErrTest) + }) fmt.Println("passed") // Output: passed @@ -563,7 +578,9 @@ func ExamplePanicsWithError() { func ExamplePanicsWithValue() { t := new(testing.T) - require.PanicsWithValue(t, "panicking", func() { panic("panicking") }) + require.PanicsWithValue(t, "panicking", func() { + panic("panicking") + }) fmt.Println("passed") // Output: passed diff --git a/require/require_format.go b/require/require_format.go index 826d4dba4..41fdeac1f 100644 --- a/require/require_format.go +++ b/require/require_format.go @@ -1,7 +1,8 @@ // SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 -// Code generated with github.com/go-openapi/testify/v2/codegen@master [sha: bb2c19fba6c03f46cb643b3bcdc1d647ea1453ab]; DO NOT EDIT. +// Code generated with github.com/go-openapi/testify/v2/codegen; DO NOT EDIT. +// Generated on 2026-01-02 (version v1.2.2-760-g97c29e3) using codegen version master [sha: 97c29e3dbfc40800a080863ceea81db0cfd6e858] package require diff --git a/require/require_format_test.go b/require/require_format_test.go index 219c9fb35..a4da38239 100644 --- a/require/require_format_test.go +++ b/require/require_format_test.go @@ -1,7 +1,8 @@ // SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 -// Code generated with github.com/go-openapi/testify/v2/codegen@master [sha: bb2c19fba6c03f46cb643b3bcdc1d647ea1453ab]; DO NOT EDIT. +// Code generated with github.com/go-openapi/testify/v2/codegen; DO NOT EDIT. +// Generated on 2026-01-02 (version v1.2.2-760-g97c29e3) using codegen version master [sha: 97c29e3dbfc40800a080863ceea81db0cfd6e858] package require @@ -1550,7 +1551,7 @@ func TestYAMLEqf(t *testing.T) { Panicsf(t, func() { YAMLEqf(t, "key: value", "key: value", "test message") - }, "should panic without the yaml feature enabled") + }, "should panic without the yaml feature enabled.") }) } diff --git a/require/require_forward.go b/require/require_forward.go index ed796027a..941b876e7 100644 --- a/require/require_forward.go +++ b/require/require_forward.go @@ -1,7 +1,8 @@ // SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 -// Code generated with github.com/go-openapi/testify/v2/codegen@master [sha: bb2c19fba6c03f46cb643b3bcdc1d647ea1453ab]; DO NOT EDIT. +// Code generated with github.com/go-openapi/testify/v2/codegen; DO NOT EDIT. +// Generated on 2026-01-02 (version v1.2.2-760-g97c29e3) using codegen version master [sha: 97c29e3dbfc40800a080863ceea81db0cfd6e858] package require diff --git a/require/require_forward_test.go b/require/require_forward_test.go index 4270cb15c..e43372a0c 100644 --- a/require/require_forward_test.go +++ b/require/require_forward_test.go @@ -1,7 +1,8 @@ // SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 -// Code generated with github.com/go-openapi/testify/v2/codegen@master [sha: bb2c19fba6c03f46cb643b3bcdc1d647ea1453ab]; DO NOT EDIT. +// Code generated with github.com/go-openapi/testify/v2/codegen; DO NOT EDIT. +// Generated on 2026-01-02 (version v1.2.2-760-g97c29e3) using codegen version master [sha: 97c29e3dbfc40800a080863ceea81db0cfd6e858] package require @@ -3764,7 +3765,7 @@ func TestAssertionsYAMLEq(t *testing.T) { a := New(t) a.Panics(func() { a.YAMLEq("key: value", "key: value") - }, "should panic without the yaml feature enabled") + }, "should panic without the yaml feature enabled.") }) } @@ -3776,7 +3777,7 @@ func TestAssertionsYAMLEqf(t *testing.T) { a := New(t) a.Panics(func() { a.YAMLEqf("key: value", "key: value", "test message") - }, "should panic without the yaml feature enabled") + }, "should panic without the yaml feature enabled.") }) } diff --git a/require/require_helpers.go b/require/require_helpers.go index 0b0a7f093..3bc26dfed 100644 --- a/require/require_helpers.go +++ b/require/require_helpers.go @@ -1,7 +1,8 @@ // SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 -// Code generated with github.com/go-openapi/testify/v2/codegen@master [sha: bb2c19fba6c03f46cb643b3bcdc1d647ea1453ab]; DO NOT EDIT. +// Code generated with github.com/go-openapi/testify/v2/codegen; DO NOT EDIT. +// Generated on 2026-01-02 (version v1.2.2-760-g97c29e3) using codegen version master [sha: 97c29e3dbfc40800a080863ceea81db0cfd6e858] package require @@ -37,14 +38,3 @@ func ObjectsAreEqual(expected any, actual any) bool { func ObjectsAreEqualValues(expected any, actual any) bool { return assertions.ObjectsAreEqualValues(expected, actual) } - -// 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 any, actual any) bool { - return assertions.ObjectsExportedFieldsAreEqual(expected, actual) -} diff --git a/require/require_helpers_test.go b/require/require_helpers_test.go index 1ef368de7..b507f90c3 100644 --- a/require/require_helpers_test.go +++ b/require/require_helpers_test.go @@ -1,7 +1,8 @@ // SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 -// Code generated with github.com/go-openapi/testify/v2/codegen@master [sha: bb2c19fba6c03f46cb643b3bcdc1d647ea1453ab]; DO NOT EDIT. +// Code generated with github.com/go-openapi/testify/v2/codegen; DO NOT EDIT. +// Generated on 2026-01-02 (version v1.2.2-760-g97c29e3) using codegen version master [sha: 97c29e3dbfc40800a080863ceea81db0cfd6e858] package require @@ -24,7 +25,3 @@ func TestObjectsAreEqualf(t *testing.T) { func TestObjectsAreEqualValuesf(t *testing.T) { t.Skip() // this function doesn't have tests yet } - -func TestObjectsExportedFieldsAreEqualf(t *testing.T) { - t.Skip() // this function doesn't have tests yet -} diff --git a/require/require_types.go b/require/require_types.go index 6c13db3cd..80e19f708 100644 --- a/require/require_types.go +++ b/require/require_types.go @@ -1,7 +1,8 @@ // SPDX-FileCopyrightText: Copyright 2025 go-swagger maintainers // SPDX-License-Identifier: Apache-2.0 -// Code generated with github.com/go-openapi/testify/v2/codegen@master [sha: bb2c19fba6c03f46cb643b3bcdc1d647ea1453ab]; DO NOT EDIT. +// Code generated with github.com/go-openapi/testify/v2/codegen; DO NOT EDIT. +// Generated on 2026-01-02 (version v1.2.2-760-g97c29e3) using codegen version master [sha: 97c29e3dbfc40800a080863ceea81db0cfd6e858] package require @@ -23,12 +24,9 @@ type ( // for table driven tests. BoolAssertionFunc func(T, bool, ...any) - // CollectT implements the T interface and collects all errors. + // CollectT implements the [T] interface and collects all errors. CollectT = assertions.CollectT - // Deprecated: CompareType has only ever been for internal use and has accidentally been published since v1.6.0. Do not use it. - CompareType = assertions.CompareType - // Comparison is a custom function that returns true on success and false on failure. Comparison = assertions.Comparison