diff --git a/CHANGELOG.md b/CHANGELOG.md index fe91409..ca669c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,15 @@ All notable changes to this project will be documented in this file. This project follows the [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) format and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [0.1.0] — Unreleased +## [0.1.1] — Unreleased + +### 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 + - Handles files with copyright notices or other comments before build tags + - Ensures generated files maintain the same build constraints as source files + +## [0.1.0] — 2024-11-03 Initial public release of `testify/depend`, bringing first-class dependency management to Go test suites built with `stretchr/testify`. diff --git a/depend/cmd/dependgen/generator.go b/depend/cmd/dependgen/generator.go index f5bbe38..ecff08a 100644 --- a/depend/cmd/dependgen/generator.go +++ b/depend/cmd/dependgen/generator.go @@ -20,7 +20,10 @@ 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(`// Code generated by dependgen — DO NOT EDIT. + tpl := template.Must(template.New("reg").Parse(`{{- range .BuildTags}} +{{.}} +{{end -}} +// Code generated by dependgen — DO NOT EDIT. package {{.Package}} import "github.com/srgg/testify/depend" @@ -109,13 +112,15 @@ func (g *Generator) generateFile(sourceFile string, suites []SuiteInfo) error { return nil } - // All suites from the same file have the same package + // All suites from the same file have the same package and build tags packageName := suites[0].Package + buildTags := suites[0].BuildTags var buf bytes.Buffer if err := g.tpl.Execute(&buf, map[string]any{ - "Package": packageName, - "Suites": suites, + "Package": packageName, + "BuildTags": buildTags, + "Suites": suites, }); err != nil { return err } diff --git a/depend/cmd/dependgen/integration_test.go b/depend/cmd/dependgen/integration_test.go index 4734463..eea43a6 100644 --- a/depend/cmd/dependgen/integration_test.go +++ b/depend/cmd/dependgen/integration_test.go @@ -530,6 +530,101 @@ func TestSuite(t *testing.T) { } } +func (s *IntegrationSuite) TestBuildTagsPreservedInGeneratedFile() { + // GOAL: Verify build tags are preserved from source file to generated file + // Fixes GitHub issue #1: https://github.com/srgg/testify/issues/1 + // + // TEST SCENARIO: Create test file with //go:build and // +build tags → run dependgen → + // generated file preserves both build tags before the "Code generated" comment + + testCases := []struct { + name string + fileContent string + }{ + { + name: "Build tags as first comment group", + fileContent: `//go:build test +// +build test + +package test + +import ( + "testing" + "github.com/srgg/testify/depend" + "github.com/stretchr/testify/suite" +) + +type MySuite struct { + suite.Suite +} + +func (s *MySuite) TestA() {} +func (s *MySuite) TestB() {} + +func TestMySuite(t *testing.T) { + depend.RunSuite(t, new(MySuite)) +}`, + }, + { + name: "Build tags after copyright comment", + fileContent: `// Copyright 2024 Example Corp +// All rights reserved + +//go:build test +// +build test + +package test + +import ( + "testing" + "github.com/srgg/testify/depend" + "github.com/stretchr/testify/suite" +) + +type MySuite struct { + suite.Suite +} + +func (s *MySuite) TestA() {} +func (s *MySuite) TestB() {} + +func TestMySuite(t *testing.T) { + depend.RunSuite(t, new(MySuite)) +}`, + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + testDir := s.TempDir().Build() + + testFile := filepath.Join(testDir, "suite_test.go") + err := os.WriteFile(testFile, []byte(tc.fileContent), 0644) + s.Require().NoError(err, "MUST write test file") + + // Run dependgen in auto-detect mode + output, err := s.runDependgen(testDir, []string{"-v"}, map[string]string{"GOFILE": "suite_test.go"}) + s.Assert().NoError(err, "MUST generate successfully: %s", output) + + // Verify generated file contains both build tags + generatedCode := s.assertGeneratedFile(testDir, "suite_depend_test.go", + "//go:build test", + "// +build test", + "Code generated by 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, + "MUST have build tags before generated code comment") + + // Copyright should NOT be copied to generated file + if tc.name == "Build tags after copyright comment" { + s.Assert().NotContains(generatedCode, "Copyright", "MUST NOT copy copyright to generated file") + } + }) + } +} + func TestDependgenSuite(t *testing.T) { suite.Run(t, new(IntegrationSuite)) } diff --git a/depend/cmd/dependgen/scanner.go b/depend/cmd/dependgen/scanner.go index 5f588f0..8e1b41c 100644 --- a/depend/cmd/dependgen/scanner.go +++ b/depend/cmd/dependgen/scanner.go @@ -22,18 +22,21 @@ type SuiteInfo struct { Package string // Package name (e.g., "depend") Tests []TestMethodInfo // Test methods in this suite SourceFile string // Source file where suite was defined (e.g., "runtime_test.go") + BuildTags []string // Build tags from source file (e.g., ["//go:build test", "// +build test"]) } // suiteCollector collects suite information during scanning. // It encapsulates the logic for tracking suites and their test methods. type suiteCollector struct { - suites map[string]SuiteInfo + suites map[string]SuiteInfo + srcFileBuildTags map[string][]string // sourceFile -> buildTags } // newSuiteCollector creates a new suite collector. func newSuiteCollector() *suiteCollector { return &suiteCollector{ - suites: make(map[string]SuiteInfo), + suites: make(map[string]SuiteInfo), + srcFileBuildTags: make(map[string][]string), } } @@ -46,6 +49,11 @@ func (c *suiteCollector) addSuiteName(name string) { } } +// setBuildTags stores build tags for a source file. +func (c *suiteCollector) setBuildTags(sourceFile string, buildTags []string) { + c.srcFileBuildTags[sourceFile] = buildTags +} + // addTest adds a test method to a suite. // If the suite doesn't have metadata set, it sets them. func (c *suiteCollector) addTest(suiteName, packageName, sourceFile string, test TestMethodInfo) { @@ -59,6 +67,10 @@ func (c *suiteCollector) addTest(suiteName, packageName, sourceFile string, test if info.SourceFile == "" { info.SourceFile = sourceFile } + // Set build tags from the map if not already set + if info.BuildTags == nil { + info.BuildTags = c.srcFileBuildTags[sourceFile] + } info.Tests = append(info.Tests, test) c.suites[suiteName] = info } @@ -141,8 +153,12 @@ func (s *SuiteScanner) processFile(f *ast.File, sourceFile string) (bool, error) return false, nil } - // Extract package name from the AST file + // Extract package name and build tags from the AST file packageName := f.Name.Name + buildTags := extractBuildTags(f) + + // Store build tags for this source file + s.collector.setBuildTags(sourceFile, buildTags) // Single-pass AST traversal: process both type declarations and method declarations in one iteration for _, decl := range f.Decls { @@ -313,3 +329,34 @@ func parseDependsOnComment(commentText string) []string { } return deps } + +// extractBuildTags extracts build constraint comments from an AST file. +// It looks for //go:build and // +build directives at the top of the file. +// These comments appear before the package declaration and must be preserved +// in generated files to maintain build constraints. +// +// Returns a slice of complete comment lines including the "//" prefix. +// Example output: ["//go:build test", "// +build test"] +func extractBuildTags(f *ast.File) []string { + var buildTags []string + + // Build tags can appear in any comment group before the package declaration. + // Scan all comment groups that appear before the package keyword. + for _, commentGroup := range f.Comments { + // Stop if we've reached comments after the package declaration + if commentGroup.Pos() >= f.Package { + break + } + + // Check each comment in the group for build tags + for _, comment := range commentGroup.List { + text := comment.Text + // Look for build constraint comments + if strings.HasPrefix(text, "//go:build") || strings.HasPrefix(text, "// +build") { + buildTags = append(buildTags, text) + } + } + } + + return buildTags +}