Skip to content

Commit fb8c557

Browse files
authored
fix(internal/librarian): test logic should look for service and message/enums separately (#2809)
With this change, the test now tracks services separately from messages/enums in proto files and provides more coverage. Instead of only tracking a comment change for the first found message/enum/service, it now tracks a first service and first message/enum if both are present in the proto file. Fix #2623
1 parent 3e3b1cd commit fb8c557

File tree

2 files changed

+190
-105
lines changed

2 files changed

+190
-105
lines changed

internal/librarian/test_container_generate.go

Lines changed: 92 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -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.
137153
func (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

Comments
 (0)