@@ -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