Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,5 +77,5 @@ that require external deps.
## Module Information

- Module path: `github.com/go-openapi/testify/v2`
- Go version: 1.24.0
- Go version: 1.25.0
- License: Apache-2.0
30 changes: 3 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ This is the go-openapi fork of the great [testify](https://github.com/stretchr/t
* 95% compatible with `stretchr/testify` — if you already use it, our migration tool automates the switch
* Actively maintained: regular fixes and evolutions, many PRs proposed upstream are already in
* Zero external dependencies — you import what you need, with opt-in modules for extras (e.g. YAML, colorized output)
* Modernized codebase targeting go1.24+
* Modernized codebase targeting go1.25+
* Go routine leak detection built in: zero-setup, no false positives, works with parallel tests (unlike `go.uber.org/goleak`)
* File descriptor leak detection (linux-only)
* Type-safe assertions with generics (see [a basic example][example-with-generics-url]) — migration to generics can be automated too. [Read the full story][doc-generics]
Expand All @@ -37,7 +37,7 @@ This is the go-openapi fork of the great [testify](https://github.com/stretchr/t
### This fork isn't for everyone

* You need the `mock` package — we removed it and won't bring it back. For suites, we're [open to discussion][suite-discussion] about a redesigned approach
* Your project must support Go versions older than 1.24
* Your project must support Go versions older than 1.25
* You rely on `testifylint` or other tooling that expects the `stretchr/testify` import path
* You need 100% API compatibility — we're at 95%, and the remaining 5% are intentional removals

Expand All @@ -60,31 +60,7 @@ Feedback, contributions and proposals are welcome.

> **Recent news**
>
> ✅ Stabibilized API
>
> ✅ Migration tool
>
> ✅ Fully refactored how assertions are generated and documented. Opt-in features with their dependencies.
>
> Fixes
>
> ✅ Fixed hangs & panics when using `spew`. Fuzzed `spew`. Fixed deterministic order of keys in diff.
>
> ✅ Fixed go routine leaks with `EventuallyWith` and co.
>
> ✅ Fixed wrong logic with `IsNonIncreasing`, `InNonDecreasing`
>
> ✅ Fixed edge cases with `InDelta`, `InEpsilon`
>
> ✅ Fixed edge cases with `EqualValues`
>
> Additions
>
> ✅ Introduced generics: ~ 40 new type-safe assertions with generic types (doc: added usage guide, examples and benchmark)
>
> ✅ Added `Kind` & `NotKind`, `Consistently`, `NoGoRoutineLeak`, `NoFileDescriptorLeak`
>
> ✅ Added opt-in support for colorized output
> ✅ Preparing v2.5.0: new features, a few fixes (`EventuallyWithT`)
>
> See also our [ROADMAP][doc-roadmap].

Expand Down
34 changes: 29 additions & 5 deletions assert/assert_assertions.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

149 changes: 145 additions & 4 deletions codegen/internal/generator/doc_generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,16 @@
package generator

import (
"cmp"
"errors"
"fmt"
"iter"
"os"
"path"
"path/filepath"
"slices"

yaml "go.yaml.in/yaml/v3"

"github.com/go-openapi/testify/codegen/v2/internal/generator/domains"
"github.com/go-openapi/testify/codegen/v2/internal/generator/funcmaps"
Expand All @@ -19,9 +23,12 @@ import (

const (
// index page metadata.
indexTitle = "Assertions index"
indexDescription = "Index of assertion domains"
indexFile = "_index.md"
indexTitle = "Assertions index"
indexDescription = "Index of assertion domains"
indexFile = "_index.md"
metricsTitle = "Quick API index"
metricsDescription = "API quick index & metrics"
metricsFile = "metrics.md"

// sensible default preallocated slots.
allocatedEntries = 15
Expand Down Expand Up @@ -78,7 +85,11 @@ func (d *DocGenerator) Generate(opts ...GenerateOption) error {
}
}

return nil
if err := d.generateMetricsPage(indexDoc); err != nil {
return err
}

return d.writeYAMLMetrics(indexDoc.Metrics)
}

type uniqueValues struct {
Expand Down Expand Up @@ -173,6 +184,8 @@ func (d *DocGenerator) buildIndexDocument(docsByDomain iter.Seq2[string, model.D
}

doc.RefCount = len(doc.Index)
doc.Metrics = buildMetrics(docsByDomain)
doc.QuickIndex = buildQuickIndex(docsByDomain)

return doc
}
Expand All @@ -196,6 +209,95 @@ func buildIndexEntries(docsByDomain iter.Seq2[string, model.Document]) []model.I
return entries
}

func buildMetrics(docsByDomain iter.Seq2[string, model.Document]) (metrics model.Metrics) {
metrics.ByDomain = make(map[string]model.DomainMetrics)

for domain, doc := range docsByDomain {
metrics.Domains++
var domainMetrics model.DomainMetrics
domainMetrics.Name = doc.Title
for _, fn := range doc.Package.Functions {
metrics.Functions++

if fn.IsHelper {
metrics.Helpers++
continue
}

if fn.IsConstructor {
metrics.Others++
continue
}

metrics.Assertions++
domainMetrics.Count++

if fn.IsGeneric {
metrics.Generics++
}
}

metrics.ByDomain[domain] = domainMetrics
}

metrics.NonGenerics = metrics.Functions - metrics.Generics

return metrics
}

func buildQuickIndex(docsByDomain iter.Seq2[string, model.Document]) model.QuickIndex {
const sensibleAlloc = 150
index := make(model.QuickIndex, 0, sensibleAlloc)
seen := make(map[string]struct{}, sensibleAlloc)
opposites := make(map[string]string, sensibleAlloc)

genericNames := make(map[string]string, sensibleAlloc)
for _, doc := range docsByDomain {
for _, fn := range doc.Package.Functions {
genericNames[fn.Name] = fn.GenericName()
for _, tag := range fn.ExtraComments {
opposite := tag.Opposite()
if opposite != "" {
seen[opposite] = struct{}{}
opposites[fn.Name] = opposite
break
}
}
}
}

for domain, doc := range docsByDomain {
for _, fn := range doc.Package.Functions {
_, isOpposite := seen[fn.Name]
if isOpposite {
continue
}

opposite := opposites[fn.Name]
entry := model.QuickIndexEntry{
Name: fn.GenericName(),
Anchor: funcmaps.Slugize(fn.GenericName()),
Opposite: opposite,
Domain: domain,
IsGeneric: fn.IsGeneric,
IsHelper: fn.IsHelper,
}
if opposite != "" {
if gn, ok := genericNames[opposite]; ok {
entry.OppositeAnchor = funcmaps.Slugize(gn)
}
}

index = append(index, entry)
}
}

slices.SortFunc(index, func(a, b model.QuickIndexEntry) int {
return cmp.Compare(a.Name, b.Name)
})
return index
}

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 {
Expand All @@ -213,6 +315,19 @@ func (d *DocGenerator) generateDomainPage(document model.Document) error {
return d.render("doc_page", filepath.Join(base, document.File), document)
}

func (d *DocGenerator) generateMetricsPage(document model.Document) error {
document.File = metricsFile
document.Title = metricsTitle
document.Description = metricsDescription

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_metrics", filepath.Join(base, document.File), document)
}

func (d *DocGenerator) loadTemplates() error {
const (
tplExt = ".md.gotmpl"
Expand All @@ -222,6 +337,7 @@ func (d *DocGenerator) loadTemplates() error {
index := make(map[string]string, expectedTemplates)
index["doc_index"] = "doc_index"
index["doc_page"] = "doc_page"
index["doc_metrics"] = "doc_metrics"

templates, err := loadTemplatesFromIndex(index, tplExt, templatesFS)
if err != nil {
Expand Down Expand Up @@ -331,3 +447,28 @@ func (d *DocGenerator) render(name string, target string, data any) error {
renderMD,
)
}

// writeYAMLMetrics writes the Metrics structure as a YAML file in the hugo generation folder.
//
// This allows doc pages to use metrics directly with shortcodes.
func (d *DocGenerator) writeYAMLMetrics(metrics model.Metrics) error {
base := filepath.Join(d.ctx.targetRoot, "hack", "doc-site", "hugo")
if err := os.MkdirAll(base, dirPermissions); err != nil {
return fmt.Errorf("can't make target folder: %w", err)
}
target := filepath.Join(base, "metrics.yaml")

type containerT struct {
Params struct {
Metrics model.Metrics `yaml:"metrics"`
} `yaml:"params"`
}
var container containerT
container.Params.Metrics = metrics
buf, err := yaml.Marshal(container)
if err != nil {
return err
}

return os.WriteFile(target, buf, filePermissions)
}
6 changes: 3 additions & 3 deletions codegen/internal/generator/funcmaps/funcmaps.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ func FuncMap() template.FuncMap {
"returns": PrintReturns,
"sourceLink": sourceLink,
"titleize": titleize,
"slugize": slugize,
"slugize": Slugize,
"blockquote": blockquote,
"hopen": hugoopen,
"hclose": hugoclose,
Expand Down Expand Up @@ -353,8 +353,8 @@ func printDate() string {
return time.Now().Format(time.DateOnly)
}

// slugize converts a name into a markdown ref inside a document.
func slugize(in string) string {
// Slugize converts a name into a markdown ref inside a document.
func Slugize(in string) string {
return strings.ToLower(
strings.Map(func(r rune) rune {
switch r {
Expand Down
4 changes: 2 additions & 2 deletions codegen/internal/generator/funcmaps/funcmaps_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -822,8 +822,8 @@ func TestSlugize(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

if got := slugize(tt.input); got != tt.expected {
t.Errorf("slugize(%q) = %q, want %q", tt.input, got, tt.expected)
if got := Slugize(tt.input); got != tt.expected {
t.Errorf("Slugize(%q) = %q, want %q", tt.input, got, tt.expected)
}
})
}
Expand Down
37 changes: 37 additions & 0 deletions codegen/internal/generator/templates/doc_metrics.md.gotmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
title: {{ .Title | quote }}
description: |
{{ titleize .Description }}.
weight: -1
---

{{- with .Metrics }}

## Domains

All assertions are classified into **{{ .Domains }}** domains to help navigate the API, depending on your use case.

## API metrics

Counts for core functionality, excluding variants (formatted, forward, forward-formatted).

| Kind | Count |
| ------------------------ | ----------------- |
| All functions | {{ .Functions }} |
| All core assertions | {{ .Assertions }} |
| Generic assertions | {{ .Generics }} |
| Helpers (not assertions) | {{ .Helpers }} |
| Others | {{ .Others }} |

{{- end }}

## Quick index

Table of core assertions, excluding variants. Each function is side by side with its logical opposite (when available).

| Assertion | Opposite | Domain | Kind |
| ------------------------ | ----------------- | ------ | ---- |
{{- range .QuickIndex }}
| [{{ .Name }}]({{ .Domain }}/#{{ .Anchor }}){{ if .IsGeneric }} {{ hopen }}% icon icon="star" color=orange %{{ hclose }}{{ end }} | {{ if .Opposite }}[{{ .Opposite }}]({{ .Domain }}/#{{ .OppositeAnchor }}){{ end }} | {{ .Domain }} | {{ if .IsHelper }}helper{{ end }} |
{{- end }}

Loading
Loading