Skip to content

Commit b762155

Browse files
committed
Add test coverage for generating many files (concurrently!)
1 parent 87ad511 commit b762155

File tree

1 file changed

+215
-0
lines changed

1 file changed

+215
-0
lines changed

codegen/generator/generate_merge_test.go

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,3 +68,218 @@ func TestGenerateMergesSamePathFiles(t *testing.T) {
6868
t.Fatalf("merged file missing method definition:\n%s", content)
6969
}
7070
}
71+
72+
// TestGenerateParallelManyFiles verifies that parallel file writing correctly
73+
// handles multiple files (more than runtime.NumCPU()) to exercise the worker
74+
// pool distribution. This ensures all workers process files and all files are
75+
// written correctly.
76+
func TestGenerateParallelManyFiles(t *testing.T) {
77+
t.Cleanup(func() { Generators = generators })
78+
79+
// Generate 20 files to ensure we exceed typical CPU counts and exercise
80+
// the worker pool's work distribution.
81+
const numFiles = 20
82+
Generators = func(cmd string) ([]Genfunc, error) {
83+
return []Genfunc{
84+
func(genpkg string, roots []eval.Root) ([]*codegen.File, error) {
85+
files := make([]*codegen.File, numFiles)
86+
for i := 0; i < numFiles; i++ {
87+
f := &codegen.File{
88+
Path: filepath.Join(codegen.Gendir, "types", filepath.Join("parallel", filepath.Join("file"+string(rune('a'+i%26)), "test"+string(rune('0'+i/26))+".go"))),
89+
}
90+
f.SectionTemplates = []*codegen.SectionTemplate{
91+
codegen.Header("Types", "types", nil),
92+
{
93+
Name: "type-def",
94+
Source: "type Test" + string(rune('A'+i)) + " struct{ ID int }\n",
95+
},
96+
}
97+
files[i] = f
98+
}
99+
return files, nil
100+
},
101+
}, nil
102+
}
103+
104+
dir := t.TempDir()
105+
outputs, err := Generate(dir, "gen", false)
106+
if err != nil {
107+
t.Fatalf("Generate failed: %v", err)
108+
}
109+
110+
// Verify all files were written
111+
if len(outputs) != numFiles {
112+
t.Fatalf("expected %d output files, got %d", numFiles, len(outputs))
113+
}
114+
115+
// Verify each file exists and has correct content
116+
for i := 0; i < numFiles; i++ {
117+
outpath := filepath.Join(dir, codegen.Gendir, "types", filepath.Join("parallel", filepath.Join("file"+string(rune('a'+i%26)), "test"+string(rune('0'+i/26))+".go")))
118+
bs, err := os.ReadFile(outpath)
119+
if err != nil {
120+
t.Fatalf("failed reading file %d: %v", i, err)
121+
}
122+
content := string(bs)
123+
expected := "type Test" + string(rune('A'+i)) + " struct"
124+
if !strings.Contains(content, expected) {
125+
t.Fatalf("file %d missing expected content %q:\n%s", i, expected, content)
126+
}
127+
}
128+
}
129+
130+
// TestGenerateParallelWithMerge verifies that parallel file writing correctly
131+
// handles file merging when multiple generators target the same path. This
132+
// tests the interaction between mergeFilesByPath and parallel rendering.
133+
func TestGenerateParallelWithMerge(t *testing.T) {
134+
t.Cleanup(func() { Generators = generators })
135+
136+
// Three generators: first two merge to same path, third is separate.
137+
// This exercises both merging and parallel writing with NumCPU workers.
138+
Generators = func(cmd string) ([]Genfunc, error) {
139+
return []Genfunc{
140+
func(genpkg string, roots []eval.Root) ([]*codegen.File, error) {
141+
f1 := &codegen.File{Path: filepath.Join(codegen.Gendir, "types", "merged.go")}
142+
f1.SectionTemplates = []*codegen.SectionTemplate{
143+
codegen.Header("Types", "types", nil),
144+
{Name: "type1", Source: "type Type1 struct{}\n"},
145+
}
146+
return []*codegen.File{f1}, nil
147+
},
148+
func(genpkg string, roots []eval.Root) ([]*codegen.File, error) {
149+
f2 := &codegen.File{Path: filepath.Join(codegen.Gendir, "types", "merged.go")}
150+
f2.SectionTemplates = []*codegen.SectionTemplate{
151+
codegen.Header("Types", "types", nil),
152+
{Name: "type2", Source: "type Type2 struct{}\n"},
153+
}
154+
return []*codegen.File{f2}, nil
155+
},
156+
func(genpkg string, roots []eval.Root) ([]*codegen.File, error) {
157+
f3 := &codegen.File{Path: filepath.Join(codegen.Gendir, "types", "separate.go")}
158+
f3.SectionTemplates = []*codegen.SectionTemplate{
159+
codegen.Header("Types", "types", nil),
160+
{Name: "type3", Source: "type Type3 struct{}\n"},
161+
}
162+
return []*codegen.File{f3}, nil
163+
},
164+
}, nil
165+
}
166+
167+
dir := t.TempDir()
168+
outputs, err := Generate(dir, "gen", false)
169+
if err != nil {
170+
t.Fatalf("Generate failed: %v", err)
171+
}
172+
173+
if len(outputs) != 2 {
174+
t.Fatalf("expected 2 output files, got %d", len(outputs))
175+
}
176+
177+
// Verify merged file contains both types
178+
mergedPath := filepath.Join(dir, codegen.Gendir, "types", "merged.go")
179+
bs, err := os.ReadFile(mergedPath)
180+
if err != nil {
181+
t.Fatalf("failed reading merged file: %v", err)
182+
}
183+
content := string(bs)
184+
if !strings.Contains(content, "type Type1 struct{}") {
185+
t.Fatalf("merged file missing Type1:\n%s", content)
186+
}
187+
if !strings.Contains(content, "type Type2 struct{}") {
188+
t.Fatalf("merged file missing Type2:\n%s", content)
189+
}
190+
191+
// Verify separate file
192+
separatePath := filepath.Join(dir, codegen.Gendir, "types", "separate.go")
193+
bs, err = os.ReadFile(separatePath)
194+
if err != nil {
195+
t.Fatalf("failed reading separate file: %v", err)
196+
}
197+
content = string(bs)
198+
if !strings.Contains(content, "type Type3 struct{}") {
199+
t.Fatalf("separate file missing Type3:\n%s", content)
200+
}
201+
}
202+
203+
// TestGenerateParallelErrorHandling verifies that when file rendering fails
204+
// in the parallel worker pool, the first error is captured and returned while
205+
// other workers continue processing.
206+
func TestGenerateParallelErrorHandling(t *testing.T) {
207+
t.Cleanup(func() { Generators = generators })
208+
209+
// Create multiple files where some will fail to render due to invalid paths.
210+
// Worker pool should capture first error but continue processing other files.
211+
Generators = func(cmd string) ([]Genfunc, error) {
212+
return []Genfunc{
213+
func(genpkg string, roots []eval.Root) ([]*codegen.File, error) {
214+
files := make([]*codegen.File, 5)
215+
for i := 0; i < 5; i++ {
216+
f := &codegen.File{
217+
Path: filepath.Join(codegen.Gendir, "types", "file"+string(rune('0'+i))+".go"),
218+
}
219+
f.SectionTemplates = []*codegen.SectionTemplate{
220+
codegen.Header("Types", "types", nil),
221+
{Name: "type", Source: "type T" + string(rune('0'+i)) + " struct{}\n"},
222+
}
223+
// Make file 2 fail by adding an invalid path character after writing starts
224+
if i == 2 {
225+
// Use a FinalizeFunc that returns an error
226+
f.FinalizeFunc = func(fp string) error {
227+
return os.ErrInvalid
228+
}
229+
}
230+
files[i] = f
231+
}
232+
return files, nil
233+
},
234+
}, nil
235+
}
236+
237+
dir := t.TempDir()
238+
_, err := Generate(dir, "gen", false)
239+
if err == nil {
240+
t.Fatal("expected error from parallel generation, got nil")
241+
}
242+
// Verify we got an error (the first one encountered)
243+
if !strings.Contains(err.Error(), "invalid") && !strings.Contains(err.Error(), "finalize") {
244+
t.Fatalf("unexpected error message: %v", err)
245+
}
246+
}
247+
248+
// TestGenerateParallelSingleFile verifies that parallel file writing works
249+
// correctly with just a single file (minimal parallelism edge case).
250+
func TestGenerateParallelSingleFile(t *testing.T) {
251+
t.Cleanup(func() { Generators = generators })
252+
253+
Generators = func(cmd string) ([]Genfunc, error) {
254+
return []Genfunc{
255+
func(genpkg string, roots []eval.Root) ([]*codegen.File, error) {
256+
f := &codegen.File{Path: filepath.Join(codegen.Gendir, "types", "single.go")}
257+
f.SectionTemplates = []*codegen.SectionTemplate{
258+
codegen.Header("Types", "types", nil),
259+
{Name: "type", Source: "type Single struct{}\n"},
260+
}
261+
return []*codegen.File{f}, nil
262+
},
263+
}, nil
264+
}
265+
266+
dir := t.TempDir()
267+
outputs, err := Generate(dir, "gen", false)
268+
if err != nil {
269+
t.Fatalf("Generate failed: %v", err)
270+
}
271+
272+
if len(outputs) != 1 {
273+
t.Fatalf("expected 1 output file, got %d", len(outputs))
274+
}
275+
276+
outpath := filepath.Join(dir, codegen.Gendir, "types", "single.go")
277+
bs, err := os.ReadFile(outpath)
278+
if err != nil {
279+
t.Fatalf("failed reading file: %v", err)
280+
}
281+
content := string(bs)
282+
if !strings.Contains(content, "type Single struct{}") {
283+
t.Fatalf("file missing expected content:\n%s", content)
284+
}
285+
}

0 commit comments

Comments
 (0)