diff --git a/doc/config-schema.md b/doc/config-schema.md index 8012b156fd..56abcc0be1 100644 --- a/doc/config-schema.md +++ b/doc/config-schema.md @@ -329,6 +329,7 @@ This document describes the schema for the librarian.yaml. | Field | Type | Description | | :--- | :--- | :--- | +| `alternate_headers` | string | Is the path to a file containing alternate license header text. | | `api_id_override` | string | Is the ID of the API (e.g., "pubsub.googleapis.com"), allows the "api_id" field in .repo-metadata.json to be overridden. Defaults to "{library.api_shortname}.googleapis.com". | | `api_reference` | string | Is the URL for the API reference documentation. | | `api_description_override` | string | Allows the "api_description" field in .repo-metadata.json to be overridden. | diff --git a/internal/config/language.go b/internal/config/language.go index c457f9244b..2f340b97fa 100644 --- a/internal/config/language.go +++ b/internal/config/language.go @@ -464,6 +464,9 @@ type DartPackage struct { // TODO(https://github.com/googleapis/librarian/issues/4130): // add fill defaults for fields with default. type JavaModule struct { + // AlternateHeaders is the path to a file containing alternate license header text. + AlternateHeaders string `yaml:"alternate_headers,omitempty"` + // APIIDOverride is the ID of the API (e.g., "pubsub.googleapis.com"), // allows the "api_id" field in .repo-metadata.json to be overridden. // Defaults to "{library.api_shortname}.googleapis.com". diff --git a/internal/librarian/java/postprocess.go b/internal/librarian/java/postprocess.go index 94cb101a0e..91f53f0871 100644 --- a/internal/librarian/java/postprocess.go +++ b/internal/librarian/java/postprocess.go @@ -67,45 +67,51 @@ type libraryPostProcessParams struct { transports map[string]serviceconfig.Transport } -func postProcessLibrary(ctx context.Context, p libraryPostProcessParams) error { - if err := createOrVerifyOwlbotPy(p.outDir); err != nil { +func postProcessLibrary(ctx context.Context, params libraryPostProcessParams) error { + if err := createOrVerifyOwlbotPy(params.outDir); err != nil { return err } - bomVersion, err := findBOMVersion(p.cfg) + bomVersion, err := findBOMVersion(params.cfg) if err != nil { return err } - if err := removeKeptFilesFromStaging(p.library, p.outDir); err != nil { + if err := removeKeptFilesFromStaging(params.library, params.outDir); err != nil { return fmt.Errorf("failed to remove kept files from staging: %w", err) } - if err := runOwlBot(ctx, p.library, p.outDir, bomVersion); err != nil { + if err := runOwlBot(ctx, params.library, params.outDir, bomVersion); err != nil { return fmt.Errorf("%w: %w", errRunOwlBot, err) } - monorepoVersion, err := findMonorepoVersion(p.cfg) + monorepoVersion, err := findMonorepoVersion(params.cfg) if err != nil { return err } - if err := syncPOMs(p.library, p.outDir, monorepoVersion, p.metadata, p.transports); err != nil { + if err := syncPOMs(params.library, params.outDir, monorepoVersion, params.metadata, params.transports); err != nil { return fmt.Errorf("%w: %w", errSyncPOMs, err) } return nil } -func (p postProcessParams) gapicDir() string { return filepath.Join(p.outDir, p.apiBase, "gapic") } -func (p postProcessParams) gRPCDir() string { return filepath.Join(p.outDir, p.apiBase, "grpc") } -func (p postProcessParams) protoDir() string { return filepath.Join(p.outDir, p.apiBase, "proto") } -func (p postProcessParams) coords() APICoordinate { - return DeriveAPICoordinates(DeriveLibraryCoordinates(p.library), p.apiBase, p.javaAPI) +func (params postProcessParams) gapicDir() string { + return filepath.Join(params.outDir, params.apiBase, "gapic") +} +func (params postProcessParams) gRPCDir() string { + return filepath.Join(params.outDir, params.apiBase, "grpc") +} +func (params postProcessParams) protoDir() string { + return filepath.Join(params.outDir, params.apiBase, "proto") +} +func (params postProcessParams) coords() APICoordinate { + return DeriveAPICoordinates(DeriveLibraryCoordinates(params.library), params.apiBase, params.javaAPI) } func stagingDir(outDir string) string { return filepath.Join(outDir, owlbotStagingDir) } -func postProcessAPI(ctx context.Context, p postProcessParams) error { - gapicDir := p.gapicDir() - gRPCDir := p.gRPCDir() - protoDir := p.protoDir() +func postProcessAPI(ctx context.Context, params postProcessParams) error { + gapicDir := params.gapicDir() + gRPCDir := params.gRPCDir() + protoDir := params.protoDir() // Unzip the temp-codegen.srcjar into temporary {gapicDir} directory. srcjarPath := filepath.Join(gapicDir, "temp-codegen.srcjar") if _, err := os.Stat(srcjarPath); err == nil { @@ -113,45 +119,45 @@ func postProcessAPI(ctx context.Context, p postProcessParams) error { return fmt.Errorf("failed to unzip %s: %w", srcjarPath, err) } } - if err := addHeadersIfRequired(p, []string{gRPCDir, protoDir}); err != nil { + if err := addHeaders(params, []string{gRPCDir, protoDir}); err != nil { return err } - if err := copyFiles(p); err != nil { + if err := copyFiles(params); err != nil { return fmt.Errorf("failed to copy files: %w", err) } - if err := restructureToStaging(p); err != nil { + if err := restructureToStaging(params); err != nil { return fmt.Errorf("failed to restructure to staging: %w", err) } // Generate clirr-ignored-differences.xml for the proto module. // We target the staging directory because runOwlBot hasn't moved the files // to their final destination yet. - coords := p.coords() - protoModuleRepoRoot := filepath.Join(p.outDir, coords.Proto.ArtifactID) - shouldGenerate, err := clirrIgnoreShouldGenerate(coords.Proto.ArtifactID, protoModuleRepoRoot, p.javaAPI.Monolithic) + coords := params.coords() + protoModuleRepoRoot := filepath.Join(params.outDir, coords.Proto.ArtifactID) + shouldGenerate, err := clirrIgnoreShouldGenerate(coords.Proto.ArtifactID, protoModuleRepoRoot, params.javaAPI.Monolithic) if err != nil { return fmt.Errorf("failed to check for clirr ignore file: %w", err) } if shouldGenerate { - protoModuleStagingRoot := filepath.Join(stagingDir(p.outDir), p.apiBase, coords.Proto.ArtifactID) + protoModuleStagingRoot := filepath.Join(stagingDir(params.outDir), params.apiBase, coords.Proto.ArtifactID) if err := generateClirrIgnore(protoModuleStagingRoot); err != nil { return fmt.Errorf("failed to generate clirr ignore file: %w", err) } } // Cleanup intermediate protoc output directory after restructuring - if err := os.RemoveAll(filepath.Join(p.outDir, p.apiBase)); err != nil { + if err := os.RemoveAll(filepath.Join(params.outDir, params.apiBase)); err != nil { return fmt.Errorf("failed to cleanup intermediate files: %w", err) } return nil } -func addHeadersIfRequired(p postProcessParams, dirs []string) error { - if p.javaAPI.Monolithic { +func addHeaders(params postProcessParams, dirs []string) error { + if params.javaAPI.Monolithic { return nil } for _, dir := range dirs { - if err := addMissingHeaders(dir); err != nil { + if err := addMissingHeaders(params, dir); err != nil { return fmt.Errorf("failed to fix headers in %s: %w", dir, err) } } @@ -160,9 +166,11 @@ func addHeadersIfRequired(p postProcessParams, dirs []string) error { // addMissingHeaders prepends the license header to all Java files in the given directory // if they don't already have one. -func addMissingHeaders(dir string) error { - year := time.Now().Year() - licenseText := buildLicenseText(year) +func addMissingHeaders(params postProcessParams, dir string) error { + headerText, err := getLicenseText(params) + if err != nil { + return err + } return filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) error { if err != nil || !d.Type().IsRegular() || filepath.Ext(path) != ".java" { return err @@ -174,16 +182,36 @@ func addMissingHeaders(dir string) error { if license.HasHeader(content) { return nil } - return os.WriteFile(path, append([]byte(licenseText), content...), 0644) + return os.WriteFile(path, append(headerText, content...), 0644) }) } -func copyFiles(p postProcessParams) error { - if p.javaAPI == nil || len(p.javaAPI.CopyFiles) == 0 { +// getLicenseText reads the contents of the alternate_header property (a filepath) +// if a library has an alternate header file. Otherwise it will grab the default license +// header. +func getLicenseText(params postProcessParams) ([]byte, error) { + if params.library == nil || params.library.Java == nil || params.library.Java.AlternateHeaders == "" { + year := time.Now().Year() + return []byte(buildLicenseText(year)), nil + } + headerPath := filepath.Join(params.outDir, params.library.Java.AlternateHeaders) + b, err := os.ReadFile(headerPath) + if err != nil { + return nil, fmt.Errorf("failed to read alternate header file %s: %w", headerPath, err) + } + // Ensure the alternate header ends with a newline before it is prepended. + if len(b) > 0 && b[len(b)-1] != '\n' { + b = append(b, '\n') + } + return b, nil +} + +func copyFiles(params postProcessParams) error { + if params.javaAPI == nil || len(params.javaAPI.CopyFiles) == 0 { return nil } - gapicDir := p.gapicDir() - for _, c := range p.javaAPI.CopyFiles { + gapicDir := params.gapicDir() + for _, c := range params.javaAPI.CopyFiles { src := filepath.Join(gapicDir, c.Source) dest := filepath.Join(gapicDir, c.Destination) if _, err := os.Stat(src); err != nil { @@ -230,16 +258,16 @@ func removeConflictingFiles(protoSrcDir string) error { // that matches the structure expected by owlbot.py. It nests modules under the // {apiBase} directory (e.g., owl-bot-staging/v1/proto-google-cloud-chat-v1) to // ensure synthtool preserves the module structure. -func restructureToStaging(p postProcessParams) error { - stagingDir := stagingDir(p.outDir) - destRoot := filepath.Join(stagingDir, p.apiBase) - if p.javaAPI.Monolithic { +func restructureToStaging(params postProcessParams) error { + stagingDir := stagingDir(params.outDir) + destRoot := filepath.Join(stagingDir, params.apiBase) + if params.javaAPI.Monolithic { destRoot = filepath.Join(destRoot, "src") } if err := os.MkdirAll(destRoot, 0755); err != nil { return fmt.Errorf("failed to create staging directory: %w", err) } - return restructureModules(p, destRoot) + return restructureModules(params, destRoot) } type moveAction struct { @@ -264,10 +292,10 @@ func restructure(actions []moveAction) error { // restructureModules moves the generated code from the temporary versioned directory // tree into the destination root directory for GAPIC, Proto, gRPC, and samples. // It also copies the relevant proto files into the proto module. -func restructureModules(p postProcessParams, destRoot string) error { - coords := p.coords() - tempProtoSrcDir := p.protoDir() - if p.library.Name != commonProtosLibrary { +func restructureModules(params postProcessParams, destRoot string) error { + coords := params.coords() + tempProtoSrcDir := params.protoDir() + if params.library.Name != commonProtosLibrary { if err := removeConflictingFiles(tempProtoSrcDir); err != nil { return err } @@ -279,7 +307,7 @@ func restructureModules(p postProcessParams, destRoot string) error { gapicTestDest := filepath.Join(destRoot, coords.GAPIC.ArtifactID, "src", "test") protoFilesDestDir := filepath.Join(destRoot, coords.Proto.ArtifactID, "src", "main", "proto") - if p.javaAPI.Monolithic { + if params.javaAPI.Monolithic { protoDest = filepath.Join(destRoot, "main", "java") grpcDest = filepath.Join(destRoot, "main", "java") gapicMainDest = filepath.Join(destRoot, "main") @@ -288,44 +316,44 @@ func restructureModules(p postProcessParams, destRoot string) error { } var actions []moveAction - if shouldGenerateProto(p.javaAPI) { + if shouldGenerateProto(params.javaAPI) { actions = append(actions, moveAction{ src: tempProtoSrcDir, dest: protoDest, description: "proto source", }) } - if shouldGenerateGRPC(p.javaAPI) { + if shouldGenerateGRPC(params.javaAPI) { actions = append(actions, moveAction{ - src: p.gRPCDir(), + src: params.gRPCDir(), dest: grpcDest, description: "grpc source", }) } - if shouldGenerateGAPIC(p.javaAPI) { + if shouldGenerateGAPIC(params.javaAPI) { actions = append(actions, []moveAction{ { - src: filepath.Join(p.gapicDir(), "src", "main"), + src: filepath.Join(params.gapicDir(), "src", "main"), dest: gapicMainDest, description: "gapic source", }, { - src: filepath.Join(p.gapicDir(), "src", "test"), + src: filepath.Join(params.gapicDir(), "src", "test"), dest: gapicTestDest, description: "gapic test", }, }...) } - if shouldGenerateResourceNames(p.javaAPI) { + if shouldGenerateResourceNames(params.javaAPI) { actions = append(actions, moveAction{ - src: filepath.Join(p.gapicDir(), "proto", "src", "main", "java"), + src: filepath.Join(params.gapicDir(), "proto", "src", "main", "java"), dest: protoDest, description: "resource name source", }) } - if p.includeSamples && shouldGenerateGAPIC(p.javaAPI) { + if params.includeSamples && shouldGenerateGAPIC(params.javaAPI) { actions = append(actions, moveAction{ - src: filepath.Join(p.gapicDir(), "samples", "snippets", "generated", "src", "main", "java"), + src: filepath.Join(params.gapicDir(), "samples", "snippets", "generated", "src", "main", "java"), dest: filepath.Join(destRoot, "samples", "snippets", "generated"), description: "samples", }) @@ -334,8 +362,8 @@ func restructureModules(p postProcessParams, destRoot string) error { return err } // Copy proto files to proto-*/src/main/proto - if shouldGenerateProto(p.javaAPI) { - if err := copyProtos(p.protosToCopy, protoFilesDestDir); err != nil { + if shouldGenerateProto(params.javaAPI) { + if err := copyProtos(params.protosToCopy, protoFilesDestDir); err != nil { return fmt.Errorf("failed to copy proto files: %w", err) } } diff --git a/internal/librarian/java/postprocess_test.go b/internal/librarian/java/postprocess_test.go index 72ea58cf9e..970c90dcc0 100644 --- a/internal/librarian/java/postprocess_test.go +++ b/internal/librarian/java/postprocess_test.go @@ -23,6 +23,7 @@ import ( "os" "path/filepath" "strings" + "time" "testing" @@ -97,7 +98,7 @@ func TestPostProcessAPI(t *testing.T) { } apiProtos := []string{filepath.Join(googleapisDir, "google/cloud/secretmanager/v1/service.proto")} api := &config.API{Path: "google/cloud/secretmanager/v1"} - p := postProcessParams{ + params := postProcessParams{ cfg: &config.Config{ Libraries: []*config.Library{ {Name: "google-cloud-java", Version: "1.2.3"}, @@ -126,7 +127,7 @@ func TestPostProcessAPI(t *testing.T) { includeSamples: true, javaAPI: &config.JavaAPI{}, } - if err := postProcessAPI(t.Context(), p); err != nil { + if err := postProcessAPI(t.Context(), params); err != nil { t.Fatal(err) } @@ -198,7 +199,7 @@ func TestRestructureModules(t *testing.T) { protoPath := filepath.Join(googleapisDir, "google", "cloud", "secretmanager", "v1", "service.proto") additionalProtoPath := filepath.Join(googleapisDir, "google", "cloud", "oslogin", "common", "common.proto") - p := postProcessParams{ + params := postProcessParams{ outDir: tmpDir, library: &config.Library{ Name: libraryID, @@ -222,7 +223,7 @@ func TestRestructureModules(t *testing.T) { javaAPI: &config.JavaAPI{}, } destRoot := filepath.Join(tmpDir, "dest") - if err := restructureModules(p, destRoot); err != nil { + if err := restructureModules(params, destRoot); err != nil { t.Fatal(err) } @@ -253,7 +254,7 @@ func TestRestructureModules_CommonProtos(t *testing.T) { tmpDir := t.TempDir() apiBase := "v1" setupLocationProtoFile(t, tmpDir, apiBase) - p := postProcessParams{ + params := postProcessParams{ outDir: tmpDir, library: &config.Library{ Name: commonProtosLibrary, @@ -269,7 +270,7 @@ func TestRestructureModules_CommonProtos(t *testing.T) { }, } destRoot := filepath.Join(tmpDir, "dest") - if err := restructureModules(p, destRoot); err != nil { + if err := restructureModules(params, destRoot); err != nil { t.Fatal(err) } wantPath := filepath.Join(destRoot, "proto-google-common-protos", "src", "main", "java", "com", "google", "cloud", "location", "LocationsProto.java") @@ -283,7 +284,7 @@ func TestRestructureModules_ShouldRemoveClasses(t *testing.T) { tmpDir := t.TempDir() apiBase := "v1" setupLocationProtoFile(t, tmpDir, apiBase) - p := postProcessParams{ + params := postProcessParams{ outDir: tmpDir, library: &config.Library{ Name: "secretmanager", @@ -297,7 +298,7 @@ func TestRestructureModules_ShouldRemoveClasses(t *testing.T) { javaAPI: &config.JavaAPI{}, } destRoot := filepath.Join(tmpDir, "dest") - if err := restructureModules(p, destRoot); err != nil { + if err := restructureModules(params, destRoot); err != nil { t.Fatal(err) } wantPath := filepath.Join(destRoot, "proto-google-cloud-secretmanager-v1", "src", "main", "java", "com", "google", "cloud", "location", "LocationsProto.java") @@ -340,7 +341,7 @@ func TestRestructureModules_SamplesDisabled(t *testing.T) { t.Fatal(err) } - p := postProcessParams{ + params := postProcessParams{ outDir: tmpDir, library: &config.Library{ Name: libraryID, @@ -354,7 +355,7 @@ func TestRestructureModules_SamplesDisabled(t *testing.T) { javaAPI: &config.JavaAPI{}, } destRoot := filepath.Join(tmpDir, "dest") - if err := restructureModules(p, destRoot); err != nil { + if err := restructureModules(params, destRoot); err != nil { t.Fatal(err) } // Verify sample file location DOES NOT exist @@ -394,7 +395,7 @@ func TestRestructureModules_Monolithic(t *testing.T) { if err := os.WriteFile(protoFile, []byte("public class Proto {}"), 0644); err != nil { t.Fatal(err) } - p := postProcessParams{ + params := postProcessParams{ outDir: tmpDir, library: &config.Library{ Name: libraryID, @@ -410,7 +411,7 @@ func TestRestructureModules_Monolithic(t *testing.T) { }, } destRoot := filepath.Join(tmpDir, "dest", "src") - if err := restructureModules(p, destRoot); err != nil { + if err := restructureModules(params, destRoot); err != nil { t.Fatal(err) } @@ -432,12 +433,13 @@ func TestPostProcessAPI_SkipHeaders(t *testing.T) { for _, test := range []struct { name string monolithic bool - wantHeader bool + wantHeader string }{ - {"default - adds header", false, true}, - {"monolithic - skips header", true, false}, + {"default adds header", false, "/*\n * Copyright"}, + {"monolithic skips header", true, "package"}, } { t.Run(test.name, func(t *testing.T) { + t.Parallel() outdir := t.TempDir() apiBase := "v1" gRPCDir := filepath.Join(outdir, apiBase, "grpc") @@ -448,30 +450,63 @@ func TestPostProcessAPI_SkipHeaders(t *testing.T) { if err := os.WriteFile(grpcFile, []byte("package com.test;"), 0644); err != nil { t.Fatal(err) } - - p := postProcessParams{ + params := postProcessParams{ outDir: outdir, apiBase: apiBase, library: &config.Library{Java: &config.JavaModule{}}, javaAPI: &config.JavaAPI{Monolithic: test.monolithic}, } - // We only care about the skip logic before restructure/cleanup. - if err := addHeadersIfRequired(p, []string{gRPCDir}); err != nil { + if err := addHeaders(params, []string{gRPCDir}); err != nil { t.Fatal(err) } - got, err := os.ReadFile(grpcFile) if err != nil { t.Fatal(err) } - hasHeader := bytes.Contains(got, []byte("Copyright")) - if hasHeader != test.wantHeader { - t.Errorf("hasHeader = %v, want %v", hasHeader, test.wantHeader) + if !bytes.HasPrefix(got, []byte(test.wantHeader)) { + t.Errorf("mismatch got = %q, want %q", got, test.wantHeader) } }) } } +func TestPostProcessAPI_AlternateHeaders(t *testing.T) { + t.Parallel() + outdir := t.TempDir() + apiBase := "v1" + gRPCDir := filepath.Join(outdir, apiBase, "grpc") + if err := os.MkdirAll(gRPCDir, 0755); err != nil { + t.Fatal(err) + } + grpcFile := filepath.Join(gRPCDir, "GRPCFile.java") + if err := os.WriteFile(grpcFile, []byte("package com.test;"), 0644); err != nil { + t.Fatal(err) + } + params := postProcessParams{ + outDir: outdir, + apiBase: apiBase, + library: &config.Library{Java: &config.JavaModule{}}, + javaAPI: &config.JavaAPI{}, + } + altHeader := "/* Alternate */\n" + headerFile := filepath.Join(outdir, "header.txt") + if err := os.WriteFile(headerFile, []byte(altHeader), 0644); err != nil { + t.Fatal(err) + } + params.library.Java.AlternateHeaders = "header.txt" + if err := addHeaders(params, []string{gRPCDir}); err != nil { + t.Fatal(err) + } + got, err := os.ReadFile(grpcFile) + if err != nil { + t.Fatal(err) + } + wantHeader := "/* Alternate */" + if !bytes.HasPrefix(got, []byte(wantHeader)) { + t.Errorf("mismatch got = %q, want %q", got, wantHeader) + } +} + func TestCopyProtos_Success(t *testing.T) { t.Parallel() destDir := t.TempDir() @@ -494,8 +529,9 @@ func TestCopyProtos_Success(t *testing.T) { func TestCopyProtos_ErrorCase(t *testing.T) { t.Parallel() destDir := t.TempDir() - if err := copyProtos([]protoFileToCopy{{absolutePath: "/other/path/proto.proto", relativePath: "other/path/proto.proto"}}, destDir); err == nil { - t.Error("expected error for proto not in googleapisDir, got nil") + err := copyProtos([]protoFileToCopy{{absolutePath: "/other/path/proto.proto", relativePath: "other/path/proto.proto"}}, destDir) + if !errors.Is(err, fs.ErrNotExist) { + t.Errorf("copyProtos() error = %v, wantErr %v", err, fs.ErrNotExist) } } @@ -779,8 +815,9 @@ func TestRunOwlBot_Error(t *testing.T) { library := &config.Library{ Java: &config.JavaModule{}, } - if err := runOwlBot(t.Context(), library, outDir, ""); err == nil { - t.Error("expected error due to missing templates directory, got nil") + err := runOwlBot(t.Context(), library, outDir, "") + if !errors.Is(err, errTemplatesMissing) { + t.Errorf("runOwlBot() error = %v, wantErr %v", err, errTemplatesMissing) } if _, err := os.Stat(sDir); !errors.Is(err, fs.ErrNotExist) { t.Errorf("expected staging directory %s to be removed on error, but it still exists (err: %v)", sDir, err) @@ -788,59 +825,91 @@ func TestRunOwlBot_Error(t *testing.T) { } func TestAddMissingHeaders(t *testing.T) { + defaultHeader := buildLicenseText(time.Now().Year()) for _, test := range []struct { - name string - filename string - content string - wantModified bool + name string + params postProcessParams + filename string + content string + wantContent string }{ { - name: "file without header", - filename: "NoHeader.java", - content: "package com.example;", - wantModified: true, + name: "file without header", + filename: "NoHeader.java", + content: "package com.example;", + wantContent: defaultHeader + "package com.example;", }, { - name: "file with full header", - filename: "WithHeader.java", - content: "/* Licensed under the Apache License, Version 2.0 (the \"License\") */\npackage com.example;", + name: "file with full header", + filename: "WithHeader.java", + content: "/* Licensed under the Apache License, Version 2.0 (the \"License\") */\npackage com.example;", + wantContent: "/* Licensed under the Apache License, Version 2.0 (the \"License\") */\npackage com.example;", }, { - name: "file with partial header", - filename: "PartialHeader.java", - content: "/* Copyright 2024 Google LLC */\npackage com.example;", - wantModified: true, + name: "file with partial header", + filename: "PartialHeader.java", + content: "/* Copyright 2024 Google LLC */\npackage com.example;", + wantContent: defaultHeader + "/* Copyright 2024 Google LLC */\npackage com.example;", }, { - name: "non-java file", - filename: "test.txt", - content: "some text", + name: "non-java file", + filename: "test.txt", + content: "some text", + wantContent: "some text", }, } { t.Run(test.name, func(t *testing.T) { t.Parallel() tmpDir := t.TempDir() path := filepath.Join(tmpDir, test.filename) - originalContent := []byte(test.content) - if err := os.WriteFile(path, originalContent, 0644); err != nil { + if err := os.WriteFile(path, []byte(test.content), 0644); err != nil { t.Fatal(err) } - if err := addMissingHeaders(tmpDir); err != nil { + + params := test.params + params.outDir = tmpDir + if params.library != nil && params.library.Java != nil && params.library.Java.AlternateHeaders != "" { + headerPath := filepath.Join(tmpDir, params.library.Java.AlternateHeaders) + if err := os.MkdirAll(filepath.Dir(headerPath), 0755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(headerPath, []byte("/* Alternate Header */\n"), 0644); err != nil { + t.Fatal(err) + } + } + + if err := addMissingHeaders(params, tmpDir); err != nil { t.Fatal(err) } - newContent, err := os.ReadFile(path) + got, err := os.ReadFile(path) if err != nil { t.Fatal(err) } - wasModified := !bytes.Equal(originalContent, newContent) - if wasModified != test.wantModified { - t.Errorf("modification status = %v, want %v", wasModified, test.wantModified) + if diff := cmp.Diff(test.wantContent, string(got)); diff != "" { + t.Errorf("mismatch (-want +got):\n%s", diff) } }) } } +func TestAddMissingHeaders_AlternateHeaders_Error(t *testing.T) { + t.Parallel() + tmpDir := t.TempDir() + params := postProcessParams{ + outDir: tmpDir, + library: &config.Library{ + Java: &config.JavaModule{ + AlternateHeaders: "missing-header.txt", + }, + }, + } + err := addMissingHeaders(params, tmpDir) + if !errors.Is(err, fs.ErrNotExist) { + t.Errorf("addMissingHeaders() error = %v, wantErr %v", err, fs.ErrNotExist) + } +} + func TestCopyFiles(t *testing.T) { t.Parallel() outdir := t.TempDir() @@ -857,7 +926,7 @@ func TestCopyFiles(t *testing.T) { if err := os.WriteFile(fullSrcPath, []byte(content), 0644); err != nil { t.Fatal(err) } - p := postProcessParams{ + params := postProcessParams{ outDir: outdir, apiBase: apiBase, javaAPI: &config.JavaAPI{ @@ -869,7 +938,7 @@ func TestCopyFiles(t *testing.T) { }, }, } - if err := copyFiles(p); err != nil { + if err := copyFiles(params); err != nil { t.Fatal(err) } // Verify copy @@ -893,7 +962,7 @@ func TestCopyFiles_Error(t *testing.T) { t.Parallel() outdir := t.TempDir() apiBase := "v1" - p := postProcessParams{ + params := postProcessParams{ outDir: outdir, apiBase: apiBase, javaAPI: &config.JavaAPI{ @@ -905,8 +974,9 @@ func TestCopyFiles_Error(t *testing.T) { }, }, } - if err := copyFiles(p); err == nil { - t.Error("copyFiles() error = nil, want error for non-existent source") + err := copyFiles(params) + if !errors.Is(err, fs.ErrNotExist) { + t.Errorf("copyFiles() error = %v, wantErr %v", err, fs.ErrNotExist) } }