@@ -130,10 +130,26 @@ func (r *testGenerateRunner) cleanup() error {
130130}
131131
132132// testSingleLibrary runs a generation test for a single library.
133- // It prepares the source repository, runs generation, validates the output,
134- // and cleans up the source repository by checking out the original commit.
135- // It does not cleanup the source repository branch or worktree in code repository
136- // created during test.
133+ // The test performs the following steps:
134+ //
135+ // 1. Prepares the source repository:
136+ // - Checks out the `last_generated_commit` from the source repository.
137+ // - Injects unique GUIDs as comments into the first `message`/`enum` definition
138+ // and the first `service` definition found in each proto file to simulate a change.
139+ // - Commits these temporary changes to a new branch.
140+ //
141+ // 2. Runs the `generate` command for the specified library.
142+ // 3. Validates the output:
143+ // - Ensures that the generation command did not fail.
144+ // - Verifies that every injected GUID appears in the generated output,
145+ // confirming that the simulated changes triggered a corresponding update.
146+ // - Optionally, checks for any unexpected file additions, deletions, or modifications.
147+ //
148+ // 4. Cleans up the source repository by checking out the original commit.
149+ //
150+ // Note: This function does not delete the temporary branch created in the source
151+ // repository or reset the worktree in the code repository; these cleanup actions
152+ // are handled by the caller.
137153func (r * testGenerateRunner ) testSingleLibrary (ctx context.Context , libraryID , sourceRepoHead , outputDir string ) error {
138154 defer func () {
139155 slog .Debug ("resetting source repo to original commit" , "library" , libraryID )
@@ -153,15 +169,15 @@ func (r *testGenerateRunner) testSingleLibrary(ctx context.Context, libraryID, s
153169 return errGenerateBlocked
154170 }
155171 }
156- protoFileToGUID , err := r .prepareForGenerateTest (libraryState , libraryID )
172+ protoFileToGUIDs , err := r .prepareForGenerateTest (libraryState , libraryID )
157173 if err != nil {
158174 return fmt .Errorf ("failed in test preparing steps: %w" , err )
159175 }
160176
161177 // We capture the error here and pass it to the validation step.
162178 generateErr := generateSingleLibrary (ctx , r .containerClient , r .state , libraryState , r .repo , r .sourceRepo , outputDir )
163179
164- if err := r .validateGenerateTest (generateErr , protoFileToGUID , libraryState ); err != nil {
180+ if err := r .validateGenerateTest (generateErr , protoFileToGUIDs , libraryState ); err != nil {
165181 return fmt .Errorf ("failed in test validation steps: %w" , err )
166182 }
167183
@@ -171,9 +187,9 @@ func (r *testGenerateRunner) testSingleLibrary(ctx context.Context, libraryID, s
171187// prepareForGenerateTest sets up the source repository for a generation test. It
172188// checks out a new branch from the library's last generated commit, injects unique
173189// GUIDs as comments into the relevant proto files, and commits these temporary
174- // changes. It returns a map of the modified proto file paths to the GUIDs that
175- // were injected.
176- func (r * testGenerateRunner ) prepareForGenerateTest (libraryState * config.LibraryState , libraryID string ) (map [string ]string , error ) {
190+ // changes. It returns a map of the modified proto file paths to the slice of
191+ // GUIDs that were injected.
192+ func (r * testGenerateRunner ) prepareForGenerateTest (libraryState * config.LibraryState , libraryID string ) (map [string ][] string , error ) {
177193 if libraryState .LastGeneratedCommit == "" {
178194 return nil , fmt .Errorf ("last_generated_commit is not set for library %q" , libraryID )
179195 }
@@ -189,12 +205,12 @@ func (r *testGenerateRunner) prepareForGenerateTest(libraryState *config.Library
189205 return nil , fmt .Errorf ("failed finding proto files: %w" , err )
190206 }
191207
192- protoFileToGUID , err := injectTestGUIDsIntoProtoFiles (protoFiles , r .sourceRepo .GetDir ())
208+ protoFileToGUIDs , err := injectTestGUIDsIntoProtoFiles (protoFiles , r .sourceRepo .GetDir ())
193209 if err != nil {
194210 return nil , fmt .Errorf ("failed to inject test GUIDs into proto files: %w" , err )
195211 }
196212
197- if len (protoFileToGUID ) == 0 {
213+ if len (protoFileToGUIDs ) == 0 {
198214 return nil , fmt .Errorf ("library %q configured to generate, but nothing to generate" , libraryID )
199215 }
200216
@@ -205,7 +221,7 @@ func (r *testGenerateRunner) prepareForGenerateTest(libraryState *config.Library
205221 return nil , err
206222 }
207223
208- return protoFileToGUID , nil
224+ return protoFileToGUIDs , nil
209225}
210226
211227// findProtoFiles recursively finds all .proto files within the API paths specified in
@@ -237,62 +253,85 @@ func findProtoFiles(libraryState *config.LibraryState, repo gitrepo.Repository)
237253 return protoFiles , nil
238254}
239255
240- // injectTestGUIDsIntoProtoFiles injects a unique GUID into each one proto file
241- // provided. It returns a map of file paths to the GUIDs that were successfully injected.
242- func injectTestGUIDsIntoProtoFiles (protoFiles []string , repoPath string ) (map [string ]string , error ) {
243- protoFileToGUID := make (map [string ]string )
256+ // injectTestGUIDsIntoProtoFiles injects unique GUIDs into each proto file
257+ // provided. It returns a map of file paths to the GUIDs that were successfully
258+ // injected.
259+ func injectTestGUIDsIntoProtoFiles (protoFiles []string , repoPath string ) (map [string ][]string , error ) {
260+ protoFileToGUIDs := make (map [string ][]string )
244261 for _ , protoFile := range protoFiles {
245- guid , err := injectGUIDIntoProto (filepath .Join (repoPath , protoFile ))
262+ guids , err := injectGUIDsIntoProto (filepath .Join (repoPath , protoFile ))
246263 if err != nil {
247264 return nil , fmt .Errorf ("failed to inject GUID into %s: %w" , protoFile , err )
248265 }
249- if guid != "" {
250- protoFileToGUID [protoFile ] = guid
266+ if len ( guids ) > 0 {
267+ protoFileToGUIDs [protoFile ] = guids
251268 }
252269 }
253- return protoFileToGUID , nil
270+ return protoFileToGUIDs , nil
254271}
255272
256- // injectGUIDIntoProto adds a unique GUID comment to a single proto file to simulate
257- // a change. It finds a suitable insertion point (e.g., before a message, enum, or
258- // service definition) and writes the modified content back to the file. It returns
259- // the GUID that was injected or an empty string if no suitable insertion point was
260- // found.
261- func injectGUIDIntoProto (absPath string ) (string , error ) {
273+ // injectGUIDsIntoProto adds unique GUID comments to a single proto file to
274+ // simulate a change. It finds suitable insertion points (before a message, enum,
275+ // or service definition) and writes the modified content back to the file. It
276+ // returns the GUIDs that were injected.
277+ func injectGUIDsIntoProto (absPath string ) ([]string , error ) {
262278 content , err := os .ReadFile (absPath )
263279 if err != nil {
264- return "" , err
280+ return nil , err
265281 }
266282 lines := strings .Split (string (content ), "\n " )
267283 if len (content ) == 0 {
268- return "" , nil
284+ return nil , nil
285+ }
286+
287+ commentsByLine := make (map [int ][]string )
288+ var injectedGUIDs []string
289+ // find the first occurrence of message/enum, and the first occurrence of service separately
290+ // because they usually correspond to separate generated files.
291+ if guid := prepareGUIDInjection (lines , []string {"message " , "enum " }, commentsByLine ); guid != "" {
292+ injectedGUIDs = append (injectedGUIDs , guid )
293+ }
294+ if guid := prepareGUIDInjection (lines , []string {"service " }, commentsByLine ); guid != "" {
295+ injectedGUIDs = append (injectedGUIDs , guid )
269296 }
270297
271- insertionLine := findProtoInsertionLine (lines )
272- if insertionLine == - 1 {
273- // No suitable line found to inject the comment.
274- return "" , nil
298+ if len (injectedGUIDs ) == 0 {
299+ return nil , nil
275300 }
276301
277- guid := uuid .New ().String ()
278- comment := "// test-change-" + guid
279302 var newLines []string
280- newLines = append (newLines , lines [:insertionLine ]... )
281- newLines = append (newLines , comment )
282- newLines = append (newLines , lines [insertionLine :]... )
303+ for i , line := range lines {
304+ if comments , ok := commentsByLine [i ]; ok {
305+ newLines = append (newLines , comments ... )
306+ }
307+ newLines = append (newLines , line )
308+ }
283309
284310 output := strings .Join (newLines , "\n " )
285311 if err := os .WriteFile (absPath , []byte (output ), 0644 ); err != nil {
286- return "" , err
312+ return nil , err
287313 }
288- return guid , nil
314+ return injectedGUIDs , nil
315+ }
316+
317+ // prepareGUIDInjection finds the first occurrence of any of the provided search
318+ // terms and, if found, injects a new GUID comment into the commentsByLine map and
319+ // returns the generated GUID.
320+ func prepareGUIDInjection (lines []string , searchTerms []string , commentsByLine map [int ][]string ) string {
321+ insertionLine := findProtoInsertionLine (lines , searchTerms )
322+ if insertionLine != - 1 {
323+ guid := uuid .New ().String ()
324+ comment := "// test-change-" + guid
325+ commentsByLine [insertionLine ] = append (commentsByLine [insertionLine ], comment )
326+ return guid
327+ }
328+ return ""
289329}
290330
291331// findProtoInsertionLine determines the best line number to inject a test comment
292- // in a proto file. It searches for the first occurrence of a top-level message,
293- // enum, or service definition.
294- func findProtoInsertionLine (lines []string ) int {
295- searchTerms := []string {"message " , "enum " , "service " }
332+ // in a proto file. It searches for the first occurrence of a top-level definition
333+ // matching one of the search terms.
334+ func findProtoInsertionLine (lines []string , searchTerms []string ) int {
296335 for i , line := range lines {
297336 for _ , term := range searchTerms {
298337 if strings .HasPrefix (strings .TrimSpace (line ), term ) {
@@ -307,7 +346,7 @@ func findProtoInsertionLine(lines []string) int {
307346// that the generation command did not fail, that every injected proto change
308347// resulted in a corresponding change in the generated code, and optionally
309348// verifies that no other unexpected files were added, deleted, or modified.
310- func (r * testGenerateRunner ) validateGenerateTest (generateErr error , protoFileToGUID map [string ]string , libraryState * config.LibraryState ) error {
349+ func (r * testGenerateRunner ) validateGenerateTest (generateErr error , protoFileToGUIDs map [string ][ ]string , libraryState * config.LibraryState ) error {
311350 slog .Debug ("validating generation results" )
312351 if generateErr != nil {
313352 return fmt .Errorf ("the generation command failed: %w" , generateErr )
@@ -334,8 +373,10 @@ func (r *testGenerateRunner) validateGenerateTest(generateErr error, protoFileTo
334373 }
335374
336375 guidsToFind := make (map [string ]bool )
337- for _ , guid := range protoFileToGUID {
338- guidsToFind [guid ] = false
376+ for _ , guids := range protoFileToGUIDs {
377+ for _ , guid := range guids {
378+ guidsToFind [guid ] = false
379+ }
339380 }
340381 filesWithGUIDs := make (map [string ]bool )
341382 repoDir := r .repo .GetDir ()
@@ -363,9 +404,11 @@ func (r *testGenerateRunner) validateGenerateTest(generateErr error, protoFileTo
363404 }
364405 }
365406
366- for protoFile , guid := range protoFileToGUID {
367- if ! guidsToFind [guid ] {
368- return fmt .Errorf ("change in proto file %s (GUID %s) produced no corresponding generated file changes" , protoFile , guid )
407+ for protoFile , guids := range protoFileToGUIDs {
408+ for _ , guid := range guids {
409+ if ! guidsToFind [guid ] {
410+ return fmt .Errorf ("change in proto file %s (GUID %s) produced no corresponding generated file changes" , protoFile , guid )
411+ }
369412 }
370413 }
371414 slog .Debug ("validation succeeded: all proto changes resulted in generated file changes" )
0 commit comments