Skip to content

Commit f6b0b47

Browse files
authored
feat(librariangen): add execv package (#3932)
This will be used for executing commands like protoc. Copied from https://github.com/googleapis/google-cloud-go/tree/main/internal/librariangen/execv.
1 parent 4c5a1c4 commit f6b0b47

File tree

2 files changed

+130
-0
lines changed

2 files changed

+130
-0
lines changed
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
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 execv
16+
17+
import (
18+
"context"
19+
"errors"
20+
"fmt"
21+
"log/slog"
22+
"os"
23+
"os/exec"
24+
"strings"
25+
)
26+
27+
// Run executes a command in a specified working directory and logs its output.
28+
func Run(ctx context.Context, args []string, workingDir string) error {
29+
cmd := exec.CommandContext(ctx, args[0], args[1:]...)
30+
cmd.Env = os.Environ()
31+
cmd.Dir = workingDir
32+
slog.Debug("librariangen: running command", "command", strings.Join(cmd.Args, " "), "dir", cmd.Dir)
33+
34+
output, err := cmd.Output()
35+
if len(output) > 0 {
36+
slog.Debug("librariangen: command stdout", "output", string(output))
37+
}
38+
if err != nil {
39+
var exitErr *exec.ExitError
40+
if errors.As(err, &exitErr) {
41+
// The command ran and exited with a non-zero exit code.
42+
if len(exitErr.Stderr) > 0 {
43+
slog.Debug("librariangen: command stderr", "output", string(exitErr.Stderr))
44+
}
45+
return fmt.Errorf("librariangen: command failed with exit error: %s: %w", exitErr.Stderr, err)
46+
}
47+
// Another error occurred (e.g., command not found).
48+
return fmt.Errorf("librariangen: command failed: %w", err)
49+
}
50+
return nil
51+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
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 execv
16+
17+
import (
18+
"context"
19+
"errors"
20+
"os/exec"
21+
"strings"
22+
"testing"
23+
)
24+
25+
func TestRun(t *testing.T) {
26+
tests := []struct {
27+
name string
28+
args []string
29+
wantErr bool
30+
wantExit int
31+
wantInStderr string
32+
}{
33+
{
34+
name: "valid command",
35+
args: []string{"echo", "hello"},
36+
wantErr: false,
37+
},
38+
{
39+
name: "invalid command",
40+
args: []string{"command-that-does-not-exist"},
41+
wantErr: true,
42+
},
43+
{
44+
name: "command with non-zero exit",
45+
args: []string{"sh", "-c", "exit 1"},
46+
wantErr: true,
47+
wantExit: 1,
48+
},
49+
{
50+
name: "command with stderr output",
51+
args: []string{"sh", "-c", "echo 'test error' >&2; exit 1"},
52+
wantErr: true,
53+
wantExit: 1,
54+
wantInStderr: "test error",
55+
},
56+
}
57+
for _, tt := range tests {
58+
t.Run(tt.name, func(t *testing.T) {
59+
err := Run(context.Background(), tt.args, ".")
60+
if (err != nil) != tt.wantErr {
61+
t.Fatalf("Run() error = %v, wantErr %v", err, tt.wantErr)
62+
}
63+
64+
if !tt.wantErr {
65+
return
66+
}
67+
68+
var exitErr *exec.ExitError
69+
if errors.As(err, &exitErr) {
70+
if tt.wantExit != 0 && exitErr.ExitCode() != tt.wantExit {
71+
t.Errorf("Run() exit code = %d, want %d", exitErr.ExitCode(), tt.wantExit)
72+
}
73+
if tt.wantInStderr != "" && !strings.Contains(string(exitErr.Stderr), tt.wantInStderr) {
74+
t.Errorf("Run() stderr = %q, want contains %q", string(exitErr.Stderr), tt.wantInStderr)
75+
}
76+
}
77+
})
78+
}
79+
}

0 commit comments

Comments
 (0)