From d4c696c1e6de11d3ae9c26083fd2d99f1c23c82c Mon Sep 17 00:00:00 2001 From: Daniel Schreij Date: Sat, 5 Sep 2026 21:57:36 +0200 Subject: [PATCH 1/2] Add a test helper that runs the generator over ad hoc sources --- internal/gen/generator_support_test.go | 80 ++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 internal/gen/generator_support_test.go diff --git a/internal/gen/generator_support_test.go b/internal/gen/generator_support_test.go new file mode 100644 index 0000000..6c485f7 --- /dev/null +++ b/internal/gen/generator_support_test.go @@ -0,0 +1,80 @@ +package gen + +import ( + "io/fs" + "os" + "path/filepath" + "regexp" + "strings" + "testing" +) + +// generateFromSources writes files into a throwaway module, runs the generator over it and +// returns the concatenated generated code. Keys are file names relative to the module root. +func generateFromSources(t *testing.T, files map[string]string) string { + t.Helper() + + dir := t.TempDir() + if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module example.com/sample\n\ngo 1.21\n"), 0o644); err != nil { + t.Fatalf("write go.mod: %v", err) + } + for name, content := range files { + path := filepath.Join(dir, name) + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + t.Fatalf("create directory for %s: %v", name, err) + } + if err := os.WriteFile(path, []byte(content), 0o644); err != nil { + t.Fatalf("write %s: %v", name, err) + } + } + + out := filepath.Join(dir, "out") + g := &Generator{Files: map[string]*File{}, outPath: out} + if err := g.Process(dir); err != nil { + t.Fatalf("Process: %v", err) + } + if err := g.Gen(); err != nil { + t.Fatalf("Gen: %v", err) + } + return readGeneratedTree(t, out) +} + +// readGeneratedTree concatenates every generated file under dir, the nested packages included. +// readAllGeneratedGoFiles reads the top level only, which is enough for a fixture of one package +// but silently drops the output of one that spans several. +func readGeneratedTree(t *testing.T, dir string) string { + t.Helper() + + var b strings.Builder + err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if d.IsDir() || !strings.HasSuffix(path, ".go") { + return nil + } + content, err := os.ReadFile(path) + if err != nil { + return err + } + b.WriteString(string(content)) + b.WriteString("\n\n") + + return nil + }) + if err != nil { + t.Fatalf("read generated tree %s: %v", dir, err) + } + if b.Len() == 0 { + t.Fatalf("no .go files under %s", dir) + } + + return b.String() +} + +// containsField reports whether the generated code declares a helper named name with the given +// type, tolerating the column alignment gofmt applies to struct fields. +func containsField(content, name, typ string) bool { + re := regexp.MustCompile(`(?m)^\s*` + regexp.QuoteMeta(name) + `\s+` + regexp.QuoteMeta(typ) + `\s*$`) + return re.MatchString(content) +} From 8880f6853a5f37b9efe0b4f70f6798c514f5f8b3 Mon Sep 17 00:00:00 2001 From: Daniel Schreij Date: Fri, 4 Sep 2026 22:35:04 +0200 Subject: [PATCH 2/2] Skip _test.go files when collecting generator inputs --- internal/gen/generator.go | 2 +- internal/gen/skip_test_files_test.go | 25 +++++++++++++++++++++++++ internal/gen/utils.go | 6 ++++-- 3 files changed, 30 insertions(+), 3 deletions(-) create mode 100644 internal/gen/skip_test_files_test.go diff --git a/internal/gen/generator.go b/internal/gen/generator.go index f67eca0..bc97f04 100644 --- a/internal/gen/generator.go +++ b/internal/gen/generator.go @@ -233,7 +233,7 @@ func (g *Generator) processFile(inputFile, inputRoot string) error { } if shouldSkipFile(inputFile) { - fmt.Printf("Skipping generated file: %s\n", inputFile) + fmt.Printf("Skipping file: %s\n", inputFile) return nil } diff --git a/internal/gen/skip_test_files_test.go b/internal/gen/skip_test_files_test.go new file mode 100644 index 0000000..6022d55 --- /dev/null +++ b/internal/gen/skip_test_files_test.go @@ -0,0 +1,25 @@ +package gen + +import ( + "strings" + "testing" +) + +func TestProcessSkipsTestFiles(t *testing.T) { + content := generateFromSources(t, map[string]string{ + "model.go": "package sample\n\ntype Article struct {\n\tID uint\n\tTitle string\n}\n", + "model_test.go": "package sample\n\ntype Fixture struct {\n\tID uint\n}\n", + }) + + if !strings.Contains(content, "var Article = struct") { + t.Fatalf("expected helpers for Article, got:\n%s", content) + } + if strings.Contains(content, "var Fixture = struct") { + t.Fatalf("test-only type Fixture must not get a helper, got:\n%s", content) + } + // And the broad form as well: a leak could surface as a field type rather than as a helper + // block of its own, which a check for the block alone would not see. + if strings.Contains(content, "Fixture") { + t.Fatalf("no trace of a test-only type may reach the generated package, got:\n%s", content) + } +} diff --git a/internal/gen/utils.go b/internal/gen/utils.go index 88dc6d4..f2cf8df 100644 --- a/internal/gen/utils.go +++ b/internal/gen/utils.go @@ -175,9 +175,11 @@ func mergeImports(dst *[]Import, src []Import) { } } -// shouldSkipFile checks if a file contains the generated code header and should be skipped +// shouldSkipFile reports whether a file is not an input for generation: anything that is not Go +// source, test files (their types exist for tests only and would leak helpers into the output), +// and files the generator wrote itself. func shouldSkipFile(filePath string) bool { - if !strings.HasSuffix(filePath, ".go") { + if !strings.HasSuffix(filePath, ".go") || strings.HasSuffix(filePath, "_test.go") { return true }