-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
/
command.go
152 lines (126 loc) · 4.27 KB
/
command.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
package shell
import (
"bufio"
"errors"
"fmt"
"io"
"os"
"os/exec"
"strings"
"sync"
"syscall"
"testing"
"github.com/gruntwork-io/terratest/modules/logger"
)
// Command is a simpler struct for defining commands than Go's built-in Cmd.
type Command struct {
Command string // The command to run
Args []string // The args to pass to the command
WorkingDir string // The working directory
Env map[string]string // Additional environment variables to set
}
// RunCommand runs a shell command and redirects its stdout and stderr to the stdout of the atomic script itself.
func RunCommand(t *testing.T, command Command) {
err := RunCommandE(t, command)
if err != nil {
t.Fatal(err)
}
}
// RunCommandE runs a shell command and redirects its stdout and stderr to the stdout of the atomic script itself.
func RunCommandE(t *testing.T, command Command) error {
_, err := RunCommandAndGetOutputE(t, command)
return err
}
// RunCommandAndGetOutput runs a shell command and returns its stdout and stderr as a string. The stdout and stderr of that command will also
// be printed to the stdout and stderr of this Go program to make debugging easier.
func RunCommandAndGetOutput(t *testing.T, command Command) string {
out, err := RunCommandAndGetOutputE(t, command)
if err != nil {
t.Fatal(err)
}
return out
}
// RunCommandAndGetOutputE runs a shell command and returns its stdout and stderr as a string. The stdout and stderr of that command will also
// be printed to the stdout and stderr of this Go program to make debugging easier.
func RunCommandAndGetOutputE(t *testing.T, command Command) (string, error) {
logger.Logf(t, "Running command %s with args %s", command.Command, command.Args)
cmd := exec.Command(command.Command, command.Args...)
cmd.Dir = command.WorkingDir
cmd.Stdin = os.Stdin
cmd.Env = formatEnvVars(command)
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
}
output, err := readStdoutAndStderr(t, stdout, stderr)
if err != nil {
return output, err
}
if err := cmd.Wait(); err != nil {
return output, err
}
return output, nil
}
// This function captures stdout and stderr while still printing it to the stdout and stderr of this Go program
func readStdoutAndStderr(t *testing.T, stdout io.ReadCloser, stderr io.ReadCloser) (string, error) {
allOutput := []string{}
stdoutScanner := bufio.NewScanner(stdout)
stderrScanner := bufio.NewScanner(stderr)
wg := &sync.WaitGroup{}
mutex := &sync.Mutex{}
wg.Add(2)
go readData(t, stdoutScanner, wg, mutex, &allOutput)
go readData(t, stderrScanner, wg, mutex, &allOutput)
wg.Wait()
if err := stdoutScanner.Err(); err != nil {
return "", err
}
if err := stderrScanner.Err(); err != nil {
return "", err
}
return strings.Join(allOutput, "\n"), nil
}
func readData(t *testing.T, scanner *bufio.Scanner, wg *sync.WaitGroup, mutex *sync.Mutex, allOutput *[]string) {
defer wg.Done()
for scanner.Scan() {
logTextAndAppendToOutput(t, mutex, scanner.Text(), allOutput)
}
}
func logTextAndAppendToOutput(t *testing.T, mutex *sync.Mutex, text string, allOutput *[]string) {
defer mutex.Unlock()
logger.Log(t, text)
mutex.Lock()
*allOutput = append(*allOutput, text)
}
// GetExitCodeForRunCommandError tries to read the exit code for the error object returned from running a shell command. This is a bit tricky to do
// in a way that works across platforms.
func GetExitCodeForRunCommandError(err error) (int, error) {
// http://stackoverflow.com/a/10385867/483528
if exitErr, ok := err.(*exec.ExitError); ok {
// The program has exited with an exit code != 0
// This works on both Unix and Windows. Although package
// syscall is generally platform dependent, WaitStatus is
// defined for both Unix and Windows and in both cases has
// an ExitStatus() method with the same signature.
if status, ok := exitErr.Sys().(syscall.WaitStatus); ok {
return status.ExitStatus(), nil
}
return 1, errors.New("could not determine exit code")
}
return 0, nil
}
func formatEnvVars(command Command) []string {
env := os.Environ()
for key, value := range command.Env {
env = append(env, fmt.Sprintf("%s=%s", key, value))
}
return env
}