Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 10 additions & 31 deletions utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,40 +192,19 @@ func TruncateString(str string, maxLength int) string {

func RunCmdAndWait(name string, args ...string) (string, error) {
cmd := exec.Command(name, args...)

stdout, err := cmd.StdoutPipe()
if err != nil {
return "", err
}
stderr, err := cmd.StderrPipe()
if err != nil {
return "", err
}

err = cmd.Start()
if err != nil {
return "", err
}

resp, err := io.ReadAll(stdout)
resp, err := cmd.Output()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This changes a bit what ends up in the returned string because resp will now contain the stderr as well (if exit is 0), while previously we only had the stdout from the command if I'm not mistaken.
I think it's better but I'd like to hear from @dapr/maintainers-cli

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that what you are describing is CombinedOutput. From https://pkg.go.dev/os/exec#Cmd.Output

Output runs the command and returns its standard output. Any returned error will usually be of type *ExitError. If c.Stderr was nil and the returned error is of type *ExitError, Output populates the Stderr field of the returned error.

FWIW, I had the same understanding as you until I checked the docs a minute ago :-)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah cool, I was wrong then. In this case, I think the behavior is the same as previously.

if err != nil {
return "", err
}
errB, err := io.ReadAll(stderr)
if err != nil {
//nolint
return "", nil
}

err = cmd.Wait()
if err != nil {
// in case of error, capture the exact message.
if len(errB) > 0 {
return "", errors.New(string(errB))
cmdStr := strings.Join(append([]string{name}, args...), " ")
var exitErr *exec.ExitError
if errors.As(err, &exitErr) {
output := strings.TrimSpace(string(exitErr.Stderr))
if output == "" {
output = err.Error()
}
return "", fmt.Errorf("error running \"%s\" (exit code %d): %s", cmdStr, exitErr.ExitCode(), output)
}
return "", err
return "", fmt.Errorf("error running \"%s\": %w", cmdStr, err)
}

return string(resp), nil
}

Expand Down
51 changes: 49 additions & 2 deletions utils/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ func TestGetVersionAndImageVariant(t *testing.T) {

func TestValidateFilePaths(t *testing.T) {
dirName := createTempDir(t, "test_validate_paths")
defer cleanupTempDir(t, dirName)
t.Cleanup(func() { cleanupTempDir(t, dirName) })
validFile := createTempFile(t, dirName, "valid_test_file.yaml")
testcases := []struct {
name string
Expand Down Expand Up @@ -311,7 +311,7 @@ func TestResolveHomeDir(t *testing.T) {

func TestReadFile(t *testing.T) {
fileName := createTempFile(t, "", "test_read_file")
defer cleanupTempDir(t, fileName)
t.Cleanup(func() { cleanupTempDir(t, fileName) })
testcases := []struct {
name string
input string
Expand Down Expand Up @@ -504,6 +504,53 @@ func cleanupTempDir(t *testing.T, fileName string) {
assert.NoError(t, err)
}

func TestRunCmdAndWait(t *testing.T) {
t.Run("successful command returns stdout", func(t *testing.T) {
t.Parallel()
var out string
var err error
if runtime.GOOS == "windows" {
out, err = RunCmdAndWait("cmd", "/c", "echo", "hello")
} else {
out, err = RunCmdAndWait("echo", "hello")
}
assert.NoError(t, err)
assert.Contains(t, out, "hello")
})

t.Run("failed command with stderr includes command name and stderr in error", func(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("skipping on Windows")
}
t.Parallel()
_, err := RunCmdAndWait("sh", "-c", "echo myerror >&2; exit 1")
assert.Error(t, err)
assert.Contains(t, err.Error(), `error running "sh -c echo myerror >&2; exit 1" (exit code 1)`)
assert.Contains(t, err.Error(), "myerror")
})

t.Run("failed command with no stderr falls back to stdout in error", func(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("skipping on Windows")
}
t.Parallel()
_, err := RunCmdAndWait("sh", "-c", "echo myoutput; exit 1")
assert.Error(t, err)
assert.Contains(t, err.Error(), `error running "sh -c echo myoutput; exit 1" (exit code 1)`)
assert.Contains(t, err.Error(), "myoutput")
})

t.Run("failed command with no output wraps original error with command name", func(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("skipping on Windows")
}
t.Parallel()
_, err := RunCmdAndWait("sh", "-c", "exit 1")
assert.Error(t, err)
assert.Contains(t, err.Error(), `error running "sh -c exit 1"`)
})
}

func TestSanitizeDir(t *testing.T) {
testcases := []struct {
name string
Expand Down
Loading