From d9dd3c81e48db023a7065df55a40f37dcea1542c Mon Sep 17 00:00:00 2001 From: srgg Date: Mon, 3 Nov 2025 14:05:38 -0700 Subject: [PATCH] feat: generate header comments documenting suites, dependencies, and order (fixes #3) --- CHANGELOG.md | 3 + depend/cmd/dependgen/generator.go | 153 ++++++++++++++++++++++- depend/cmd/dependgen/generator_test.go | 34 ++++- depend/cmd/dependgen/integration_test.go | 10 +- 4 files changed, 191 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca669c2..49b8d8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [0.1.1] — Unreleased +### Added +- **dependgen**: Human-readable dependency documentation in generated files ([#3](https://github.com/srgg/testify/issues/3)) + ### Fixed - **dependgen**: Build tags (`//go:build` and `// +build` directives) are now correctly preserved from source test files to generated `*_depend_test.go` files ([#1](https://github.com/srgg/testify/issues/1)) - Scans all comment groups before package declaration to find build constraints diff --git a/depend/cmd/dependgen/generator.go b/depend/cmd/dependgen/generator.go index ecff08a..6676639 100644 --- a/depend/cmd/dependgen/generator.go +++ b/depend/cmd/dependgen/generator.go @@ -5,10 +5,149 @@ import ( "fmt" "os" "path/filepath" + "runtime/debug" "strings" "text/template" ) +// getVersion returns the module version from build info, or "(devel)" if unavailable. +// This matches the behavior of standard Go tools like stringer. +func getVersion() string { + info, ok := debug.ReadBuildInfo() + if !ok { + return "(devel)" + } + if info.Main.Version != "" && info.Main.Version != "(devel)" { + return info.Main.Version + } + return "(devel)" +} + +// formatSuiteDocs generates human-readable documentation for a single suite. +func formatSuiteDocs(suite SuiteInfo) string { + var buf strings.Builder + + fmt.Fprintf(&buf, "// • %s (%s)\n", suite.Name, suite.SourceFile) + + // Check if any tests have dependencies + hasDependencies := false + for _, test := range suite.Tests { + if len(test.DependsOn) > 0 { + hasDependencies = true + break + } + } + + if !hasDependencies { + buf.WriteString("// Dependencies: none\n") + } else { + buf.WriteString("// Dependencies:\n") + // Only list tests that have dependencies + for _, test := range suite.Tests { + if len(test.DependsOn) > 0 { + deps := strings.Join(test.DependsOn, ", ") + line := fmt.Sprintf("// %s: %s", test.Name, deps) + buf.WriteString(wrapLine(line, 80, "// ")) + buf.WriteString("\n") + } + } + } + + // Calculate and display execution order + order := calculateExecutionOrder(suite) + orderLine := "// Order: " + order + buf.WriteString(wrapLine(orderLine, 80, "// ")) + buf.WriteString("\n") + + return buf.String() +} + +// wrapLine wraps a line at maxLen characters, using the continuation prefix for subsequent lines. +func wrapLine(line string, maxLen int, continuationPrefix string) string { + if len(line) <= maxLen { + return line + } + + var result strings.Builder + remaining := line + + for len(remaining) > maxLen { + breakPoint := maxLen + + // Find a good break point (space, comma, or arrow) + foundBreak := false + for i := breakPoint; i > 0 && i < len(remaining); i-- { + if remaining[i] == ' ' || remaining[i] == ',' { + breakPoint = i + if remaining[i] == ',' { + breakPoint++ // Include the comma on this line + } + foundBreak = true + break + } + } + + if !foundBreak { + // No good break point found, break at maxLen + breakPoint = maxLen + } + + result.WriteString(remaining[:breakPoint]) + result.WriteString("\n") + remaining = strings.TrimSpace(remaining[breakPoint:]) + + if len(remaining) > 0 { + result.WriteString(continuationPrefix) + } + } + + if len(remaining) > 0 { + result.WriteString(remaining) + } + + return result.String() +} + +// calculateExecutionOrder determines the test execution order based on dependencies. +// Returns a string representation using arrows (→) to show the sequence. +func calculateExecutionOrder(suite SuiteInfo) string { + if len(suite.Tests) == 0 { + return "" + } + + // Build dependency map + deps := make(map[string][]string) + for _, test := range suite.Tests { + deps[test.Name] = test.DependsOn + } + + // Topological sort to get execution order + var order []string + visited := make(map[string]struct{}) + + var visit func(string) + visit = func(testName string) { + if _, exists := visited[testName]; exists { + return + } + visited[testName] = struct{}{} + + // Visit dependencies first + for _, dep := range deps[testName] { + visit(dep) + } + + order = append(order, testName) + } + + // Visit all tests + for _, test := range suite.Tests { + visit(test.Name) + } + + return strings.Join(order, " → ") +} + // Generator handles code generation for test suites. // It encapsulates the template and configuration needed to generate // dependency configuration code for testify/depend test suites. @@ -20,10 +159,20 @@ type Generator struct { // NewGenerator creates a new code generator with the given configuration. // The template is parsed at creation time and is safe for concurrent use. func NewGenerator(cfg *Config) *Generator { - tpl := template.Must(template.New("reg").Parse(`{{- range .BuildTags}} + // Register template helper functions + funcMap := template.FuncMap{ + "version": getVersion, + "suiteDocs": formatSuiteDocs, + } + + tpl := template.Must(template.New("reg").Funcs(funcMap).Parse(`{{- range .BuildTags}} {{.}} {{end -}} -// Code generated by dependgen — DO NOT EDIT. +// Code generated by testify/dependgen {{version}}. DO NOT EDIT. +// +{{range $i, $suite := .Suites -}} +{{if $i}}// +{{end}}{{suiteDocs $suite}}{{end}} package {{.Package}} import "github.com/srgg/testify/depend" diff --git a/depend/cmd/dependgen/generator_test.go b/depend/cmd/dependgen/generator_test.go index 14b5d0b..8573e55 100644 --- a/depend/cmd/dependgen/generator_test.go +++ b/depend/cmd/dependgen/generator_test.go @@ -96,7 +96,14 @@ func (s *GeneratorSuite) TestGenerateFile() { }, }, expectedFilename: "my_depend_test.go", - expectedContent: `// Code generated by dependgen — DO NOT EDIT. + expectedContent: `// Code generated by testify/dependgen (devel). DO NOT EDIT. +// +// • MySuite (my_test.go) +// Dependencies: +// TestB: TestA +// TestC: TestA, TestB +// Order: TestA → TestB → TestC + package testpkg import "github.com/srgg/testify/depend" @@ -147,7 +154,12 @@ func (s *MySuite) GeneratedDependConfig() *depend.SuiteConfig { }, }, expectedFilename: "simple_depend_test.go", - expectedContent: `// Code generated by dependgen — DO NOT EDIT. + expectedContent: `// Code generated by testify/dependgen (devel). DO NOT EDIT. +// +// • SimpleSuite (simple_test.go) +// Dependencies: none +// Order: TestA → TestB + package testpkg import "github.com/srgg/testify/depend" @@ -193,7 +205,12 @@ func (s *SimpleSuite) GeneratedDependConfig() *depend.SuiteConfig { }, }, expectedFilename: "http_handler_depend_test.go", - expectedContent: `// Code generated by dependgen — DO NOT EDIT. + expectedContent: `// Code generated by testify/dependgen (devel). DO NOT EDIT. +// +// • HTTPHandlerSuite (http_handler_test.go) +// Dependencies: none +// Order: TestSetup + package testpkg import "github.com/srgg/testify/depend" @@ -245,7 +262,16 @@ func (s *HTTPHandlerSuite) GeneratedDependConfig() *depend.SuiteConfig { }, }, expectedFilename: "multi_depend_test.go", - expectedContent: `// Code generated by dependgen — DO NOT EDIT. + expectedContent: `// Code generated by testify/dependgen (devel). DO NOT EDIT. +// +// • FirstSuite (multi_test.go) +// Dependencies: none +// Order: TestA +// +// • SecondSuite (multi_test.go) +// Dependencies: none +// Order: TestX + package testpkg import "github.com/srgg/testify/depend" diff --git a/depend/cmd/dependgen/integration_test.go b/depend/cmd/dependgen/integration_test.go index eea43a6..a7991e7 100644 --- a/depend/cmd/dependgen/integration_test.go +++ b/depend/cmd/dependgen/integration_test.go @@ -159,7 +159,11 @@ func (s *IntegrationSuite) TestAutoDetectSingleSuite() { s.Assert().NoError(err, "MUST generate successfully for single suite") s.assertGeneratedFile(testDir, "suite_depend_test.go", - "Code generated by dependgen", + "Code generated by testify/dependgen", + "DO NOT EDIT", + "• MySuite (suite_test.go)", + "Dependencies:", + "Order:", "MySuiteTestRegistry", "TestA", "TestB") @@ -610,11 +614,11 @@ func TestMySuite(t *testing.T) { generatedCode := s.assertGeneratedFile(testDir, "suite_depend_test.go", "//go:build test", "// +build test", - "Code generated by dependgen", + "Code generated by testify/dependgen", "MySuiteTestRegistry") // Verify build tags appear BEFORE the generated code comment (critical for Go toolchain) - s.Assert().Regexp(`(?s)//go:build test\s+// \+build test\s+// Code generated by dependgen`, generatedCode, + s.Assert().Regexp(`(?s)//go:build test\s+// \+build test\s+// Code generated by testify/dependgen`, generatedCode, "MUST have build tags before generated code comment") // Copyright should NOT be copied to generated file