From e2602ecb229fd2202946dcd95e761d632e0a0028 Mon Sep 17 00:00:00 2001 From: Frederic BIDON Date: Thu, 16 Apr 2026 21:00:37 +0200 Subject: [PATCH] doc: enriched published agentic instructions (skills) Signed-off-by: Frederic BIDON --- .claude/.gitignore | 4 +- .claude/CLAUDE.md | 1 + .claude/skills/add-assertion/SKILL.md | 121 +++++++++ .claude/skills/codegen/SKILL.md | 85 +++++++ .claude/skills/doc-site/SKILL.md | 80 ++++++ .../skills/testing-generic-functions/SKILL.md | 238 ++++++++++++++++++ 6 files changed, 528 insertions(+), 1 deletion(-) create mode 100644 .claude/skills/add-assertion/SKILL.md create mode 100644 .claude/skills/codegen/SKILL.md create mode 100644 .claude/skills/doc-site/SKILL.md create mode 100644 .claude/skills/testing-generic-functions/SKILL.md diff --git a/.claude/.gitignore b/.claude/.gitignore index f830ad137..643020278 100644 --- a/.claude/.gitignore +++ b/.claude/.gitignore @@ -1,5 +1,7 @@ plans/ -skills/ +!skills/ +skills/.local-skills/ commands/ agents/ hooks/ +settings.local.json diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 0688e55fb..87f69a7ef 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -36,6 +36,7 @@ This is a mono-repo with multiple Go modules tied together by `go.work`. | `internal/difflib/` | Internalized go-difflib for generating diffs | | `internal/fdleak/` | File descriptor leak detection | | `internal/leak/` | Goroutine leak detection | +| `internal/tools` | Internal tools exposed | | `enable/stubs/` | Public API for enabling optional features (yaml, colors) | ### Code generation architecture diff --git a/.claude/skills/add-assertion/SKILL.md b/.claude/skills/add-assertion/SKILL.md new file mode 100644 index 000000000..5d002b72e --- /dev/null +++ b/.claude/skills/add-assertion/SKILL.md @@ -0,0 +1,121 @@ +# Adding a New Assertion + +Step-by-step workflow for adding a new assertion function to testify. + +## Workflow + +1. Add function to the appropriate domain file in `internal/assertions/` +2. Add `// Domain: ` as the first line inside the function body +3. Add `// Opposite: ` on the next line if a logical opposite exists +4. Add `Examples:` section to the doc comment +5. Add tests to the corresponding `*_test.go` file +6. Run `go generate ./...` to produce all 8 variants + docs +7. Run `go test work ./...` to verify everything + +## Function template + +```go +// FuncName asserts that . +// +// # Usage +// +// assertions.FuncName(t, arg1, arg2) +// +// # Examples +// +// success: arg1Value, arg2Value +// failure: arg1Value, arg2Value +func FuncName(t T, arg1, arg2 any, msgAndArgs ...any) bool { + // Domain: + // Opposite: (omit if none) + if h, ok := t.(H); ok { + h.Helper() + } + + // implementation + if !condition { + return Fail(t, "message", msgAndArgs...) + } + + return true +} +``` + +## Doc comment annotations + +### Domain tag (in doc comment header) + +```go +// domain: equality +``` + +Assigns the function to a documentation domain. Add domain descriptions in +`internal/assertions/doc.go` if creating a new domain. + +### Domain comment (inside function body) + +```go +// Domain: equality +``` + +First line inside the function body. Used by the codegen scanner. + +### Opposite annotation + +```go +// Opposite: NotEqual +``` + +Second line inside the body (after Domain). Only on the affirmative form +(e.g., on `Equal`, not on `NotEqual`). + +### Examples section + +```go +// # Examples +// +// success: 123, 123 +// failure: 123, 456 +``` + +Drives generated smoke tests for all 8 variants. Three case types: +- `success: ` -- test should pass +- `failure: ` -- test should fail +- `panic: ` followed by `` on next line + +For complex values that can't be represented inline, use `// NOT IMPLEMENTED`: +```go +// success: &customStruct{Field: "value"}, // NOT IMPLEMENTED +``` + +**Never use `// TODO`** -- it triggers false positives in code quality tools. + +## What gets generated + +From a single function, `go generate` produces: + +| Variant | Package | Example | +|---------|---------|---------| +| Package-level | `assert` | `assert.FuncName(t, ...)` | +| Formatted | `assert` | `assert.FuncNamef(t, ..., "msg")` | +| Forward method | `assert` | `a.FuncName(...)` | +| Forward formatted | `assert` | `a.FuncNamef(..., "msg")` | +| Package-level | `require` | `require.FuncName(t, ...)` | +| Formatted | `require` | `require.FuncNamef(t, ..., "msg")` | +| Forward method | `require` | `r.FuncName(...)` | +| Forward formatted | `require` | `r.FuncNamef(..., "msg")` | + +Plus: tests for all variants, documentation entry in `docs/doc-site/api/.md`. + +Generic assertions (with type params) produce 4 variants (no forward methods -- +Go limitation). + +## Checklist + +- [ ] Function in `internal/assertions/.go` +- [ ] `// Domain:` and optionally `// Opposite:` inside function body +- [ ] Doc comment with `# Usage`, `# Examples` sections +- [ ] Tests in `internal/assertions/_test.go` +- [ ] `go generate ./...` succeeds +- [ ] `go test work ./...` passes +- [ ] `golangci-lint run --new-from-rev master` clean diff --git a/.claude/skills/codegen/SKILL.md b/.claude/skills/codegen/SKILL.md new file mode 100644 index 000000000..7b613d7ff --- /dev/null +++ b/.claude/skills/codegen/SKILL.md @@ -0,0 +1,85 @@ +# Code Generation + +How the testify code and documentation generator works. + +## Running + +```bash +# Generate everything (code + docs) from internal/assertions/ +go generate ./... + +# Or run the generator directly +cd codegen && go run . -output-packages assert,require -include-doc + +# Code only (skip docs) +cd codegen && go run . -output-packages assert,require -include-doc=false +``` + +## Generator pipeline + +1. **Scanner** (`codegen/internal/scanner/`) parses `internal/assertions/` using + `go/packages` and `go/types`. Extracts function signatures, doc comments, + domain tags, examples, and metadata. + - `comments/` -- doc comment extraction + - `comments-parser/` -- domain tags, examples, metadata parsing + - `signature/` -- function signature analysis + +2. **Model** (`codegen/internal/model/`) holds the intermediate representation: + functions, type params, tests, documentation, metrics. + +3. **Generator** (`codegen/internal/generator/`) renders templates: + - **Code templates** produce `assert/` and `require/` packages with all variants + - **Doc templates** produce Hugo markdown in `docs/doc-site/api/` + - **Metrics** produces `metrics.yaml` for Hugo site params + +## Key templates + +Located in `codegen/internal/generator/templates/`: + +| Template | Produces | +|----------|----------| +| `assertion_assertions.gotmpl` | `assert` package-level functions | +| `assertion_format.gotmpl` | `assert` formatted variants (`*f`) | +| `assertion_forward.gotmpl` | `assert` forward methods | +| `requirement_*.gotmpl` | `require` equivalents (calls `FailNow`) | +| `doc_index.md.gotmpl` | API index page (`_index.md`) | +| `doc_page.md.gotmpl` | Per-domain doc pages | +| `doc_metrics.md.gotmpl` | Quick index & metrics page | + +## Template functions + +Custom template functions in `codegen/internal/generator/funcmaps/`: +- `Slugize(name)` -- converts function name to markdown anchor +- `Titleize(s)` -- title-cases a string +- `hopen` / `hclose` -- Hugo shortcode delimiters (`{{% ... %}}`) + +## Domain organization + +Functions are grouped by `// Domain: ` tags inside the function body. +Domain descriptions live in `internal/assertions/doc.go` as special comments. +The generator reorganizes package-based docs into domain-based pages +(19 domains currently). + +## Generated output + +**Never edit generated files directly.** They carry a `DO NOT EDIT` header. + +| Output | Location | +|--------|----------| +| `assert/` package | Generated functions + tests | +| `require/` package | Generated functions + tests | +| `docs/doc-site/api/*.md` | Domain-organized Hugo pages | +| `docs/doc-site/api/metrics.md` | Quick index + API metrics | +| `hack/doc-site/hugo/metrics.yaml` | Hugo site params (counts) | + +Exceptions (not generated): `assert/doc.go`, `require/doc.go`, ad-hoc testable examples. + +## Adding support for a new construct + +If the generator needs to support a new Go construct (e.g., new type param +pattern), the work is in: +1. `codegen/internal/scanner/` -- teach the scanner to extract it +2. `codegen/internal/model/` -- add fields to the model +3. `codegen/internal/generator/templates/` -- update templates to render it + +The scanner and generator have comprehensive tests (~1,400+ lines across test files). diff --git a/.claude/skills/doc-site/SKILL.md b/.claude/skills/doc-site/SKILL.md new file mode 100644 index 000000000..2cc53232f --- /dev/null +++ b/.claude/skills/doc-site/SKILL.md @@ -0,0 +1,80 @@ +# Documentation Site + +Hugo-based documentation site for testify, auto-generated from source code. + +## Running locally + +```bash +# 1. Generate docs from source +go generate ./... + +# 2. Start Hugo dev server +cd hack/doc-site/hugo +./gendoc.sh + +# Visit http://localhost:1313/testify/ +# Auto-reloads on changes to docs/doc-site/ +``` + +## Site structure + +``` +hack/doc-site/hugo/ + hugo.yaml # Main Hugo config + metrics.yaml # Generated metrics (codegen output, merged into site params) + testify.yaml # Version info + build metadata + gendoc.sh # Dev server launcher + layouts/ # Custom layout overrides + themes/hugo-relearn/ # Relearn documentation theme + +docs/doc-site/ # Content (mounted by Hugo) + api/ # Generated: domain pages, index, metrics (DO NOT EDIT) + usage/ # Hand-written: USAGE, GENERICS, CHANGES, MIGRATION, etc. + project/ # Hand-written: APPROACH, maintainer docs +``` + +## Generated vs hand-written content + +| Path | Generated? | Notes | +|------|-----------|-------| +| `docs/doc-site/api/*.md` | Yes | Domain pages, index, metrics. Regenerate with `go generate` | +| `docs/doc-site/api/metrics.md` | Yes | Quick index table + API counts | +| `docs/doc-site/usage/*.md` | No | Hand-written guides | +| `docs/doc-site/project/*.md` | No | Hand-written project docs | + +## Dynamic counts via Hugo params + +Function and assertion counts are generated into `metrics.yaml` and merged +into Hugo's `site.Params.metrics`. Use the relearn `siteparam` shortcode +to reference them in hand-written markdown: + +```markdown +We have {{% siteparam "metrics.assertions" %}} assertions across +{{% siteparam "metrics.domains" %}} domains. +``` + +Available params: `metrics.domains`, `metrics.functions`, `metrics.assertions`, +`metrics.generics`, `metrics.nongeneric_assertions`, `metrics.helpers`, `metrics.others`. + +Per-domain: `metrics.by_domain..count`, `metrics.by_domain..name`. + +Hugo math functions (`sub`, `mul`, `add`) are NOT available in markdown content. +For computed values, add them to the codegen `buildMetrics()` in +`codegen/internal/generator/doc_generator.go`. + +## Adding a new hand-written page + +1. Create `docs/doc-site/
/.md` with Hugo front matter +2. Set `weight:` to control ordering in the sidebar +3. Use relearn shortcodes: `{{% notice %}}`, `{{% expand %}}`, `{{< tabs >}}`, etc. +4. Reference API counts with `{{% siteparam "metrics." %}}` + +## Relearn theme features used + +- `{{% notice style="info" %}}` -- callout boxes +- `{{% expand title="..." %}}` -- collapsible sections +- `{{< tabs >}}` / `{{% tab %}}` -- tabbed content +- `{{< cards >}}` / `{{% card %}}` -- side-by-side cards +- `{{% icon icon="star" color=orange %}}` -- inline icons +- `{{% siteparam "key" %}}` -- site param substitution +- `{{< mermaid >}}` -- diagrams diff --git a/.claude/skills/testing-generic-functions/SKILL.md b/.claude/skills/testing-generic-functions/SKILL.md new file mode 100644 index 000000000..9fc0c1892 --- /dev/null +++ b/.claude/skills/testing-generic-functions/SKILL.md @@ -0,0 +1,238 @@ +# Testing Generic Functions with Table-Driven Tests + +## The Challenge + +Go generics require type parameters to be resolved at compile time. This creates a problem for traditional table-driven tests where test cases are stored in a slice of `any`: + +```go +// This DOES NOT work - type parameter cannot be inferred from 'any' +cases := []struct { + name string + value1 any + value2 any +}{ + {"int", 1, 2}, + {"string", "a", "b"}, +} + +for _, c := range cases { + GreaterT(mock, c.value1, c.value2) // Error: cannot infer type parameter +} +``` + +## The Solution: Test Function Closures + +Wrap each test case in a closure where the type parameter is resolved at slice construction time, not iteration time. + +### Step 1: Define a Test Case Struct + +```go +import "testing" + +// genericTestCase wraps a test function with its name for table-driven tests. +type genericTestCase struct { + name string + test func(*testing.T) +} +``` + +### Step 2: Create a Generic Test Helper + +The helper function is generic and returns a closure. The type parameter is resolved when the closure is created (at slice construction), not when it's executed. + +```go +import "testing" + +// testGreaterT creates a test function for GreaterT with specific type V. +// Type parameter V is resolved when this function is called, not when the +// returned closure executes. +func testGreaterT[V Ordered](successE1, successE2, failE1, failE2 V) func(*testing.T) { + return func(t *testing.T) { + t.Parallel() + mock := new(mockT) + + // Success case: e1 > e2 + True(t, GreaterT(mock, successE1, successE2)) + + // Failure case: e1 <= e2 + False(t, GreaterT(mock, failE1, failE2)) + + // Equal values should fail + False(t, GreaterT(mock, successE1, successE1)) + } +} +``` + +### Step 3: Create Iterator Function for Test Cases + +Use the iterator pattern (`iter.Seq`) to return test cases: + +```go +import ( + "iter" + "slices" +) + +func greaterTCases() iter.Seq[genericTestCase] { + return slices.Values([]genericTestCase{ + // Numeric types - type inferred from arguments + {"int", testGreaterT[int](2, 1, 1, 2)}, + {"int8", testGreaterT[int8](2, 1, 1, 2)}, + {"int16", testGreaterT[int16](2, 1, 1, 2)}, + {"int32", testGreaterT[int32](2, 1, 1, 2)}, + {"int64", testGreaterT[int64](2, 1, 1, 2)}, + {"uint", testGreaterT[uint](2, 1, 1, 2)}, + {"uint8", testGreaterT[uint8](2, 1, 1, 2)}, + {"uint16", testGreaterT[uint16](2, 1, 1, 2)}, + {"uint32", testGreaterT[uint32](2, 1, 1, 2)}, + {"uint64", testGreaterT[uint64](2, 1, 1, 2)}, + {"uintptr", testGreaterT[uintptr](2, 1, 1, 2)}, + {"float32", testGreaterT[float32](2.0, 1.0, 1.0, 2.0)}, + {"float64", testGreaterT[float64](2.0, 1.0, 1.0, 2.0)}, + {"string", testGreaterT[string]("b", "a", "a", "b")}, + + // Special types requiring dedicated setup functions + {"time.Time", testGreaterTTime()}, + {"[]byte", testGreaterTBytes()}, + + // Custom types to verify constraint satisfaction + {"custom int type", testGreaterTCustomInt()}, + }) +} +``` + +### Step 4: Handle Special Types + +Some types need dedicated test functions because they require specific setup: + +```go +import ( + "testing" + "time" +) + +// testGreaterTTime tests GreaterT with time.Time values. +func testGreaterTTime() func(*testing.T) { + return func(t *testing.T) { + t.Parallel() + mock := new(mockT) + + t0 := time.Now() + t1 := t0.Add(-time.Second) // t1 is before t0 + + True(t, GreaterT(mock, t0, t1)) // t0 > t1 + False(t, GreaterT(mock, t1, t0)) // t1 < t0 + False(t, GreaterT(mock, t0, t0)) // equal + } +} + +// testGreaterTBytes tests GreaterT with []byte values. +func testGreaterTBytes() func(*testing.T) { + return func(t *testing.T) { + t.Parallel() + mock := new(mockT) + + b1 := []byte("b") + b2 := []byte("a") + + True(t, GreaterT(mock, b1, b2)) + False(t, GreaterT(mock, b2, b1)) + False(t, GreaterT(mock, b1, b1)) + } +} + +// testGreaterTCustomInt verifies custom types satisfying the constraint work. +type myInt int + +func testGreaterTCustomInt() func(*testing.T) { + return func(t *testing.T) { + t.Parallel() + mock := new(mockT) + + True(t, GreaterT(mock, myInt(2), myInt(1))) + False(t, GreaterT(mock, myInt(1), myInt(2))) + } +} +``` + +### Step 5: Write the Test Function + +```go +import "testing" + +func TestCompareGreaterT(t *testing.T) { + t.Parallel() + + for tc := range greaterTCases() { + t.Run(tc.name, tc.test) + } +} +``` + +## Complete Pattern Summary + +```go +import ( + "iter" + "slices" + "testing" +) + +// 1. Test case wrapper struct +type genericTestCase struct { + name string + test func(*testing.T) +} + +// 2. Generic test helper returning closure +func testFunctionUnderTest[V Constraint](args V) func(*testing.T) { + return func(t *testing.T) { + t.Parallel() + // Test logic using args with type V + } +} + +// 3. Special type handlers +func testFunctionUnderTestSpecialType() func(*testing.T) { + return func(t *testing.T) { + t.Parallel() + // Test logic with special setup + } +} + +// 4. Iterator function +func functionUnderTestCases() iter.Seq[genericTestCase] { + return slices.Values([]genericTestCase{ + {"type1", testFunctionUnderTest[Type1](value1)}, + {"type2", testFunctionUnderTest[Type2](value2)}, + {"special", testFunctionUnderTestSpecialType()}, + }) +} + +// 5. Test function +func TestFunctionUnderTest(t *testing.T) { + t.Parallel() + for tc := range functionUnderTestCases() { + t.Run(tc.name, tc.test) + } +} +``` + +## Key Insights + +1. **Type resolution timing**: Generic type parameters are resolved at compile time. By creating closures at slice construction, each closure has its type parameter already resolved. + +2. **Closure captures**: The generic test helper captures the typed arguments in its closure, preserving type information for when the test executes. + +3. **Special type handling**: Types like `time.Time` and `[]byte` need dedicated functions because they require specific construction patterns that don't fit the simple value pattern. + +4. **Custom type testing**: Always include at least one custom type (e.g., `type myInt int`) to verify the constraint works with user-defined types, not just built-in types. + +5. **Parallel execution**: Each closure is independent, enabling parallel test execution with `t.Parallel()`. + +## When to Use This Pattern + +- Testing generic functions with type constraints +- When you need to verify behavior across multiple types satisfying a constraint +- When traditional table-driven tests fail due to type inference limitations +- Testing functions from `internal/assertions/` that have generic variants (e.g., `GreaterT`, `LessT`, `PositiveT`)