Skip to content

Commit ed60d34

Browse files
committed
doc: generated testable examples with link to pkg.go.dev
Signed-off-by: Frederic BIDON <fredbi@yahoo.com>
1 parent 9bc7ba6 commit ed60d34

33 files changed

+4513
-607
lines changed

assert/assert_examples_test.go

Lines changed: 117 additions & 117 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

assert/assert_forward.go

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

codegen/internal/generator/doc_generator.go

Lines changed: 93 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/go-openapi/testify/codegen/v2/internal/generator/domains"
1515
"github.com/go-openapi/testify/codegen/v2/internal/generator/funcmaps"
1616
"github.com/go-openapi/testify/codegen/v2/internal/model"
17+
exparser "github.com/go-openapi/testify/codegen/v2/internal/scanner/examples-parser"
1718
)
1819

1920
const (
@@ -53,8 +54,11 @@ func (d *DocGenerator) Generate(opts ...GenerateOption) error {
5354
return err
5455
}
5556

56-
// Proposal for enhancement: other fun stuff
57-
// - capture testable examples and render their source code
57+
// capture testable examples from generated packages and attach them to
58+
// the model so templates may render their source code.
59+
if err := d.populateExamples(); err != nil {
60+
return err
61+
}
5862

5963
// reorganize accumulated package-based docs into domain-based docs
6064
//
@@ -223,6 +227,93 @@ func (d *DocGenerator) loadTemplates() error {
223227
return nil
224228
}
225229

230+
// populateExamples runs the examples-parser against all generated packages in the
231+
// merged Documentation and attaches the discovered testable examples to the
232+
// corresponding Function and Ident objects.
233+
//
234+
// This must run before [reorganizeByDomain] because domain discovery copies
235+
// functions and types into domain entries.
236+
func (d *DocGenerator) populateExamples() error {
237+
if !d.ctx.runnableExamples {
238+
return nil
239+
}
240+
241+
docs := domains.FlattenDocumentation(d.doc)
242+
243+
// derive the module root from the assertions import that every generated
244+
// package carries (e.g. "github.com/go-openapi/testify/v2").
245+
var rootPkg string
246+
for _, doc := range docs {
247+
if doc.Package != nil && doc.Package.Imports != nil {
248+
if assertionsPath, ok := doc.Package.Imports[assertions]; ok {
249+
rootPkg = path.Dir(path.Dir(assertionsPath))
250+
251+
break
252+
}
253+
}
254+
}
255+
if rootPkg == "" {
256+
return nil // nothing to do
257+
}
258+
259+
workDir, err := filepath.Abs(d.ctx.targetRoot)
260+
if err != nil {
261+
return fmt.Errorf("resolving target root: %w", err)
262+
}
263+
264+
for _, doc := range docs {
265+
pkg := doc.Package
266+
if pkg == nil {
267+
continue
268+
}
269+
270+
// Skip the internal assertions package: testable examples live in the
271+
// generated packages (assert, require), not in the source package.
272+
if path.Base(pkg.Package) == assertions {
273+
continue
274+
}
275+
276+
importPath := rootPkg + "/" + pkg.Package
277+
examples, parseErr := exparser.New(importPath, exparser.WithWorkDir(workDir)).Parse()
278+
if parseErr != nil {
279+
return fmt.Errorf("parsing examples for %s: %w", pkg.Package, parseErr)
280+
}
281+
282+
populateFunctionExamples(pkg, examples)
283+
populateIdentExamples(pkg.Types, examples)
284+
}
285+
286+
return nil
287+
}
288+
289+
func populateFunctionExamples(pkg *model.AssertionPackage, examples exparser.Examples) {
290+
for i, fn := range pkg.Functions {
291+
exs, ok := examples[fn.Name]
292+
if !ok {
293+
continue
294+
}
295+
renderables := make([]model.Renderable, len(exs))
296+
for j := range exs {
297+
renderables[j] = exs[j]
298+
}
299+
pkg.Functions[i].Examples = renderables
300+
}
301+
}
302+
303+
func populateIdentExamples(idents []model.Ident, examples exparser.Examples) {
304+
for i, id := range idents {
305+
exs, ok := examples[id.Name]
306+
if !ok {
307+
continue
308+
}
309+
renderables := make([]model.Renderable, len(exs))
310+
for j := range exs {
311+
renderables[j] = exs[j]
312+
}
313+
idents[i].Examples = renderables
314+
}
315+
}
316+
226317
func (d *DocGenerator) render(name string, target string, data any) error {
227318
return renderTemplate(
228319
d.ctx.index,

codegen/internal/generator/funcmaps/markdown.go

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import (
77
"fmt"
88
"regexp"
99
"strings"
10+
11+
"github.com/go-openapi/testify/codegen/v2/internal/model"
1012
)
1113

1214
var (
@@ -24,7 +26,18 @@ const sensiblePrealloc = 20
2426
//
2527
// 1. Reference-style markdown links: [text]: url
2628
// 2. Godoc-style links: [errors.Is], [testing.T], etc.
27-
func FormatMarkdown(in string) string {
29+
//
30+
//nolint:gocognit,gocyclo,cyclop // will refactor later this highly complex function
31+
func FormatMarkdown(in string, object any) string {
32+
var (
33+
testableExamples []model.Renderable
34+
funcName string
35+
)
36+
if function, ok := (object).(model.Function); ok {
37+
testableExamples = function.Examples
38+
funcName = function.Name
39+
}
40+
2841
// Step 1: Extract reference-style link definitions
2942
// Pattern: [text]: url (at start of line or after whitespace)
3043
refLinks := make(map[string]string)
@@ -112,6 +125,7 @@ func FormatMarkdown(in string) string {
112125
tabsCollection := false
113126
expanded := false
114127
tab := false
128+
hasTestableExamples := false
115129

116130
for line := range strings.SplitSeq(processed, "\n") {
117131
if expanded && len(strings.TrimSpace(line)) == 0 {
@@ -147,12 +161,16 @@ func FormatMarkdown(in string) string {
147161
tabsCollection = true
148162
}
149163

164+
if strings.EqualFold(section, "Examples") && len(testableExamples) > 0 {
165+
hasTestableExamples = true
166+
continue // skip : we'll add testable examples below
167+
}
168+
150169
title := titleize(section)
151170
if tab {
152171
trailer = append(trailer, "```")
153172
trailer = append(trailer, `{{< /tab >}}`)
154173
}
155-
156174
trailer = append(trailer, fmt.Sprintf(`{{%% tab title="%s" %%}}`, title))
157175
trailer = append(trailer, "```go")
158176
tab = true
@@ -163,6 +181,28 @@ func FormatMarkdown(in string) string {
163181
trailer = append(trailer, `{{< /tab >}}`)
164182
}
165183

184+
if hasTestableExamples {
185+
trailer = append(trailer, `{{% tab title="Testable Examples" %}}`)
186+
trailer = append(trailer, `{{% cards %}}`)
187+
tabsCollection = true
188+
189+
for _, example := range testableExamples {
190+
trailer = append(trailer, `{{% card href="https://go.dev/play/" %}}`)
191+
trailer = append(trailer, "\n")
192+
trailer = append(trailer, `*Copy and click to open Go Playground*`)
193+
trailer = append(trailer, "\n")
194+
trailer = append(trailer, "```go")
195+
trailer = append(trailer, fmt.Sprintf("// real-world test would inject *testing.T from Test%s(t *testing.T)", funcName))
196+
trailer = append(trailer, example.Render())
197+
trailer = append(trailer, "```")
198+
trailer = append(trailer, `{{% /card %}}`)
199+
trailer = append(trailer, "\n")
200+
}
201+
trailer = append(trailer, `{{% /cards %}}`)
202+
trailer = append(trailer, `{{< /tab >}}`)
203+
trailer = append(trailer, "\n")
204+
}
205+
166206
if tabsCollection {
167207
trailer = append(trailer, `{{< /tabs >}}`)
168208
trailer = append(trailer, `{{% /expand %}}`)

codegen/internal/generator/funcmaps/markdown_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import (
1313
func TestMarkdownFormatEnhanced(t *testing.T) {
1414
for tt := range markdownTestCases() {
1515
t.Run(tt.name, func(t *testing.T) {
16-
result := FormatMarkdown(tt.input)
16+
result := FormatMarkdown(tt.input, nil)
1717

1818
for _, want := range tt.contains {
1919
if !strings.Contains(result, want) {
@@ -47,7 +47,7 @@ Values can be of type [strings.Builder] or [Boolean].
4747
failure: "not empty"
4848
[Zero values]: https://go.dev/ref/spec#The_zero_value`
4949

50-
result := FormatMarkdown(input)
50+
result := FormatMarkdown(input, nil)
5151
t.Logf("Output:\n%s", result)
5252
}
5353

codegen/internal/generator/generator.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@ func (g *Generator) transformModel() error {
268268
tgt.EnableForward = g.ctx.enableForward
269269
tgt.EnableGenerics = g.ctx.enableGenerics
270270
tgt.EnableExamples = g.ctx.generateExamples
271-
tgt.RunnableExamples = g.ctx.runnableExamples
271+
tgt.RunnableExamples = g.ctx.runnableExamples /// instructs the doc generator to scan the generated packages to collect runnable examples
272272
if tgt.Imports == nil {
273273
tgt.Imports = make(model.ImportMap, 1)
274274
}

codegen/internal/generator/templates/assertion_examples_test.gotmpl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ func Example{{ .Name }}() {
3232
{{- range .Tests }}
3333
{{- if eq .ExpectedOutcome 1 }}{{/* TestSuccess */}}
3434
{{- if (not $first) }}{{ println "" }}{{- end }}{{ $first = false }}
35-
t := new(testing.T)
35+
t := new(testing.T) // should come from testing, e.g. func Test{{ $fn.Name }}(t *testing.T)
3636
{{- if $isRequire }}
3737
{{ $pkg }}.{{ $fn.Name }}{{ if hasSuffix $fn.Name "OfTypeT" }}[myType]{{ end }}(t, {{ relocate .TestedValues $pkg }})
3838
fmt.Println("passed")

codegen/internal/generator/templates/assertion_forward.gotmpl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ func New(t T) *{{ .Receiver }} {
2727
}
2828
}
2929

30+
func (a *{{ $.Receiver }}) T() T {
31+
return a.t
32+
}
33+
3034
{{- range .Functions }}
3135
{{- if and (not .IsGeneric) (not .IsHelper) (not .IsConstructor) }}{{/* generics can't be added to the receiver */}}
3236

codegen/internal/generator/templates/doc_page.md.gotmpl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ Generic assertions are marked with a {{ print "{{" }}% icon icon="star" color=or
6060
<div style="background-color: #fceea5;">{{/* like godoc TODO: use css */}}
6161
{{- end }}
6262

63-
{{ mdformat .DocString }}{{/* reformat inner markdown and reformat */}}
63+
{{ mdformat .DocString . }}{{/* reformat inner markdown and reformat */}}
6464

6565
{{- $funcName := .Name }}
6666
{{- with $extraPackages }}
@@ -133,7 +133,7 @@ Generic assertions are marked with a {{ print "{{" }}% icon icon="star" color=or
133133
<div style="background-color: #fceea5;">{{/* like godoc */}}
134134
{{- end }}
135135

136-
{{ mdformat .DocString }}
136+
{{ mdformat .DocString . }}
137137

138138
{{- $funcName := .Name }}
139139
{{- with $extraPackages }}

codegen/internal/generator/templates/requirement_forward.gotmpl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ func New(t T) *{{ .Receiver }} {
2727
}
2828
}
2929

30+
func (a *{{ $.Receiver }}) T() T {
31+
return a.t
32+
}
33+
3034
{{- range .Functions }}
3135
{{- if and (not .IsGeneric) (not .IsHelper) (not .IsConstructor) }}{{/* generics can't be added to the receiver */}}
3236

0 commit comments

Comments
 (0)