Skip to content

Commit dd12481

Browse files
authored
Merge branch 'main' into librariangen-grpc
2 parents a86dbb8 + a26a6d9 commit dd12481

File tree

9 files changed

+477
-99
lines changed

9 files changed

+477
-99
lines changed
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
// Package languagecontainer defines LanguageContainer interface and
16+
// the Run function to execute commands within the container.
17+
// This package should not have any language-specific implementation or
18+
// Librarian CLI's implementation.
19+
// TODO(b/447404382): Move this package to the https://github.com/googleapis/librarian
20+
// GitHub repository once the interface is finalized.
21+
package languagecontainer
22+
23+
import (
24+
"context"
25+
"encoding/json"
26+
"flag"
27+
"fmt"
28+
"log/slog"
29+
"os"
30+
"path/filepath"
31+
32+
"cloud.google.com/java/internal/librariangen/languagecontainer/release"
33+
"cloud.google.com/java/internal/librariangen/message"
34+
)
35+
36+
// LanguageContainer defines the functions for language-specific container operations.
37+
type LanguageContainer struct {
38+
ReleaseInit func(context.Context, *release.Config) (*message.ReleaseInitResponse, error)
39+
// Other container functions like Generate and Build will also be part of the struct.
40+
}
41+
42+
// Run accepts an implementation of the LanguageContainer.
43+
func Run(args []string, container *LanguageContainer) int {
44+
// Logic to parse args and call the appropriate method on the container.
45+
// For example, if args[1] is "generate":
46+
// request := ... // unmarshal the request from the expected location
47+
// err := container.Generate(context.Background(), request)
48+
// ...
49+
if len(args) < 1 {
50+
panic("args must not be empty")
51+
}
52+
cmd := args[0]
53+
flags := args[1:]
54+
switch cmd {
55+
case "generate":
56+
slog.Warn("librariangen: generate command is not yet implemented")
57+
return 1
58+
case "configure":
59+
slog.Warn("librariangen: configure command is not yet implemented")
60+
return 1
61+
case "release-init":
62+
return handleReleaseInit(flags, container)
63+
case "build":
64+
slog.Warn("librariangen: build command is not yet implemented")
65+
return 1
66+
default:
67+
slog.Error(fmt.Sprintf("librariangen: unknown command: %s (with flags %v)", cmd, flags))
68+
return 1
69+
}
70+
return 0
71+
}
72+
73+
func handleReleaseInit(flags []string, container *LanguageContainer) int {
74+
cfg := &release.Context{}
75+
releaseInitFlags := flag.NewFlagSet("release-init", flag.ContinueOnError)
76+
releaseInitFlags.StringVar(&cfg.LibrarianDir, "librarian", "/librarian", "Path to the librarian-tool input directory. Contains release-init-request.json.")
77+
releaseInitFlags.StringVar(&cfg.RepoDir, "repo", "/repo", "Path to the language repo.")
78+
releaseInitFlags.StringVar(&cfg.OutputDir, "output", "/output", "Path to the output directory.")
79+
if err := releaseInitFlags.Parse(flags); err != nil {
80+
slog.Error("failed to parse flags", "error", err)
81+
return 1
82+
}
83+
requestPath := filepath.Join(cfg.LibrarianDir, "release-init-request.json")
84+
bytes, err := os.ReadFile(requestPath)
85+
if err != nil {
86+
slog.Error("failed to read request file", "path", requestPath, "error", err)
87+
return 1
88+
}
89+
request := &message.ReleaseInitRequest{}
90+
if err := json.Unmarshal(bytes, request); err != nil {
91+
slog.Error("failed to parse request JSON", "error", err)
92+
return 1
93+
}
94+
config := &release.Config{
95+
Context: cfg,
96+
Request: request,
97+
}
98+
response, err := container.ReleaseInit(context.Background(), config)
99+
if err != nil {
100+
slog.Error("release-init failed", "error", err)
101+
return 1
102+
}
103+
bytes, err = json.MarshalIndent(response, "", " ")
104+
if err != nil {
105+
slog.Error("failed to marshal response JSON", "error", err)
106+
return 1
107+
}
108+
responsePath := filepath.Join(cfg.LibrarianDir, "release-init-response.json")
109+
if err := os.WriteFile(responsePath, bytes, 0644); err != nil {
110+
slog.Error("failed to write response file", "path", responsePath, "error", err)
111+
return 1
112+
}
113+
slog.Info("librariangen: release-init command executed successfully")
114+
return 0
115+
}
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package languagecontainer
16+
17+
import (
18+
"context"
19+
"encoding/json"
20+
"os"
21+
"path/filepath"
22+
"testing"
23+
24+
"cloud.google.com/java/internal/librariangen/languagecontainer/release"
25+
"cloud.google.com/java/internal/librariangen/message"
26+
"github.com/google/go-cmp/cmp"
27+
)
28+
29+
func TestRun(t *testing.T) {
30+
tmpDir := t.TempDir()
31+
if err := os.WriteFile(filepath.Join(tmpDir, "release-init-request.json"), []byte("{}"), 0644); err != nil {
32+
t.Fatal(err)
33+
}
34+
tests := []struct {
35+
name string
36+
args []string
37+
wantCode int
38+
wantErr bool
39+
}{
40+
{
41+
name: "unknown command",
42+
args: []string{"foo"},
43+
wantCode: 1,
44+
},
45+
{
46+
name: "build command",
47+
args: []string{"build"},
48+
wantCode: 1, // Not implemented yet
49+
},
50+
{
51+
name: "configure command",
52+
args: []string{"configure"},
53+
wantCode: 1, // Not implemented yet
54+
},
55+
{
56+
name: "generate command",
57+
args: []string{"generate"},
58+
wantCode: 1, // Not implemented yet
59+
},
60+
{
61+
name: "release-init command success",
62+
args: []string{"release-init", "-librarian", tmpDir},
63+
wantCode: 0,
64+
},
65+
{
66+
name: "release-init command failure",
67+
args: []string{"release-init", "-librarian", tmpDir},
68+
wantCode: 1,
69+
wantErr: true,
70+
},
71+
}
72+
for _, tt := range tests {
73+
t.Run(tt.name, func(t *testing.T) {
74+
container := LanguageContainer{
75+
ReleaseInit: func(ctx context.Context, c *release.Config) (*message.ReleaseInitResponse, error) {
76+
if tt.wantErr {
77+
return nil, os.ErrNotExist
78+
}
79+
return &message.ReleaseInitResponse{}, nil
80+
},
81+
}
82+
if gotCode := Run(tt.args, &container); gotCode != tt.wantCode {
83+
t.Errorf("Run() = %v, want %v", gotCode, tt.wantCode)
84+
}
85+
})
86+
}
87+
}
88+
89+
func TestRun_noArgs(t *testing.T) {
90+
defer func() {
91+
if r := recover(); r == nil {
92+
t.Errorf("The code did not panic")
93+
}
94+
}()
95+
Run([]string{}, &LanguageContainer{})
96+
}
97+
98+
func TestRun_ReleaseInitWritesResponse(t *testing.T) {
99+
tmpDir := t.TempDir()
100+
if err := os.WriteFile(filepath.Join(tmpDir, "release-init-request.json"), []byte("{}"), 0644); err != nil {
101+
t.Fatal(err)
102+
}
103+
args := []string{"release-init", "-librarian", tmpDir}
104+
want := &message.ReleaseInitResponse{Error: "test error"}
105+
container := LanguageContainer{
106+
ReleaseInit: func(ctx context.Context, c *release.Config) (*message.ReleaseInitResponse, error) {
107+
return want, nil
108+
},
109+
}
110+
111+
if code := Run(args, &container); code != 0 {
112+
t.Errorf("Run() = %v, want 0", code)
113+
}
114+
115+
responsePath := filepath.Join(tmpDir, "release-init-response.json")
116+
bytes, err := os.ReadFile(responsePath)
117+
if err != nil {
118+
t.Fatal(err)
119+
}
120+
got := &message.ReleaseInitResponse{}
121+
if err := json.Unmarshal(bytes, got); err != nil {
122+
t.Fatal(err)
123+
}
124+
if diff := cmp.Diff(want, got); diff != "" {
125+
t.Errorf("response mismatch (-want +got):\n%s", diff)
126+
}
127+
}
128+
129+
func TestRun_ReleaseInitReadsContextArgs(t *testing.T) {
130+
tmpDir := t.TempDir()
131+
librarianDir := filepath.Join(tmpDir, "librarian")
132+
if err := os.Mkdir(librarianDir, 0755); err != nil {
133+
t.Fatal(err)
134+
}
135+
if err := os.WriteFile(filepath.Join(librarianDir, "release-init-request.json"), []byte("{}"), 0644); err != nil {
136+
t.Fatal(err)
137+
}
138+
repoDir := filepath.Join(tmpDir, "repo")
139+
if err := os.Mkdir(repoDir, 0755); err != nil {
140+
t.Fatal(err)
141+
}
142+
outputDir := filepath.Join(tmpDir, "output")
143+
if err := os.Mkdir(outputDir, 0755); err != nil {
144+
t.Fatal(err)
145+
}
146+
args := []string{"release-init", "-librarian", librarianDir, "-repo", repoDir, "-output", outputDir}
147+
var gotConfig *release.Config
148+
container := LanguageContainer{
149+
ReleaseInit: func(ctx context.Context, c *release.Config) (*message.ReleaseInitResponse, error) {
150+
gotConfig = c
151+
return &message.ReleaseInitResponse{}, nil
152+
},
153+
}
154+
if code := Run(args, &container); code != 0 {
155+
t.Errorf("Run() = %v, want 0", code)
156+
}
157+
if got, want := gotConfig.Context.LibrarianDir, librarianDir; got != want {
158+
t.Errorf("gotConfig.Context.LibrarianDir = %q, want %q", got, want)
159+
}
160+
if got, want := gotConfig.Context.RepoDir, repoDir; got != want {
161+
t.Errorf("gotConfig.Context.RepoDir = %q, want %q", got, want)
162+
}
163+
if got, want := gotConfig.Context.OutputDir, outputDir; got != want {
164+
t.Errorf("gotConfig.Context.OutputDir = %q, want %q", got, want)
165+
}
166+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
// Package release contains types for language container's release command.
16+
package release
17+
18+
import "cloud.google.com/java/internal/librariangen/message"
19+
20+
// Context has the directory paths for the release-init command.
21+
// https://github.com/googleapis/librarian/blob/main/doc/language-onboarding.md#release-init
22+
type Context struct {
23+
LibrarianDir string
24+
RepoDir string
25+
OutputDir string
26+
}
27+
28+
// The Config for the release-init command. This holds the context (the directory paths)
29+
// and the request parsed from the release-init-request.json file.
30+
type Config struct {
31+
Context *Context
32+
// This request is parsed from the release-init-request.json file in
33+
// the LibrarianDir of the context.
34+
Request *message.ReleaseInitRequest
35+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package release
16+
17+
import (
18+
"encoding/json"
19+
"os"
20+
"path/filepath"
21+
"testing"
22+
23+
"cloud.google.com/java/internal/librariangen/message"
24+
"github.com/google/go-cmp/cmp"
25+
)
26+
27+
func TestReadReleaseInitRequest(t *testing.T) {
28+
want := &message.ReleaseInitRequest{
29+
Libraries: []*message.Library{
30+
{
31+
ID: "google-cloud-secretmanager-v1",
32+
Version: "1.3.0",
33+
Changes: []*message.Change{
34+
{
35+
Type: "feat",
36+
Subject: "add new UpdateRepository API",
37+
Body: "This adds the ability to update a repository's properties.",
38+
PiperCLNumber: "786353207",
39+
CommitHash: "9461532e7d19c8d71709ec3b502e5d81340fb661",
40+
},
41+
{
42+
Type: "docs",
43+
Subject: "fix typo in BranchRule comment",
44+
Body: "",
45+
PiperCLNumber: "786353207",
46+
CommitHash: "9461532e7d19c8d71709ec3b502e5d81340fb661",
47+
},
48+
},
49+
APIs: []message.API{
50+
{
51+
Path: "google/cloud/secretmanager/v1",
52+
},
53+
{
54+
Path: "google/cloud/secretmanager/v1beta",
55+
},
56+
},
57+
SourcePaths: []string{
58+
"secretmanager",
59+
"other/location/secretmanager",
60+
},
61+
ReleaseTriggered: true,
62+
},
63+
},
64+
}
65+
bytes, err := os.ReadFile(filepath.Join("..", "testdata", "release-init-request.json"))
66+
if err != nil {
67+
t.Fatal(err)
68+
}
69+
got := &message.ReleaseInitRequest{}
70+
if err := json.Unmarshal(bytes, got); err != nil {
71+
t.Fatal(err)
72+
}
73+
if diff := cmp.Diff(want, got); diff != "" {
74+
t.Errorf("Unmarshal() mismatch (-want +got):\n%s", diff)
75+
}
76+
}

0 commit comments

Comments
 (0)