github.com/vieux/docker@v0.6.3-0.20161004191708-e097c2a938c7/pkg/integration/cmd/command.go (about) 1 package cmd 2 3 import ( 4 "bytes" 5 "fmt" 6 "io" 7 "os/exec" 8 "path/filepath" 9 "runtime" 10 "strings" 11 "sync" 12 "syscall" 13 "time" 14 15 "github.com/go-check/check" 16 ) 17 18 type testingT interface { 19 Fatalf(string, ...interface{}) 20 } 21 22 const ( 23 // None is a token to inform Result.Assert that the output should be empty 24 None string = "<NOTHING>" 25 ) 26 27 // GetExitCode returns the ExitStatus of the specified error if its type is 28 // exec.ExitError, returns 0 and an error otherwise. 29 func GetExitCode(err error) (int, error) { 30 exitCode := 0 31 if exiterr, ok := err.(*exec.ExitError); ok { 32 if procExit, ok := exiterr.Sys().(syscall.WaitStatus); ok { 33 return procExit.ExitStatus(), nil 34 } 35 } 36 return exitCode, fmt.Errorf("failed to get exit code") 37 } 38 39 // ProcessExitCode process the specified error and returns the exit status code 40 // if the error was of type exec.ExitError, returns nothing otherwise. 41 func ProcessExitCode(err error) (exitCode int) { 42 if err != nil { 43 var exiterr error 44 if exitCode, exiterr = GetExitCode(err); exiterr != nil { 45 // TODO: Fix this so we check the error's text. 46 // we've failed to retrieve exit code, so we set it to 127 47 exitCode = 127 48 } 49 } 50 return 51 } 52 53 type lockedBuffer struct { 54 m sync.RWMutex 55 buf bytes.Buffer 56 } 57 58 func (buf *lockedBuffer) Write(b []byte) (int, error) { 59 buf.m.Lock() 60 defer buf.m.Unlock() 61 return buf.buf.Write(b) 62 } 63 64 func (buf *lockedBuffer) String() string { 65 buf.m.RLock() 66 defer buf.m.RUnlock() 67 return buf.buf.String() 68 } 69 70 // Result stores the result of running a command 71 type Result struct { 72 Cmd *exec.Cmd 73 ExitCode int 74 Error error 75 // Timeout is true if the command was killed because it ran for too long 76 Timeout bool 77 outBuffer *lockedBuffer 78 errBuffer *lockedBuffer 79 } 80 81 // Assert compares the Result against the Expected struct, and fails the test if 82 // any of the expcetations are not met. 83 func (r *Result) Assert(t testingT, exp Expected) { 84 err := r.Compare(exp) 85 if err == nil { 86 return 87 } 88 89 _, file, line, _ := runtime.Caller(1) 90 t.Fatalf("at %s:%d\n%s", filepath.Base(file), line, err.Error()) 91 } 92 93 // Compare returns an formatted error with the command, stdout, stderr, exit 94 // code, and any failed expectations 95 func (r *Result) Compare(exp Expected) error { 96 errors := []string{} 97 add := func(format string, args ...interface{}) { 98 errors = append(errors, fmt.Sprintf(format, args...)) 99 } 100 101 if exp.ExitCode != r.ExitCode { 102 add("ExitCode was %d expected %d", r.ExitCode, exp.ExitCode) 103 } 104 if exp.Timeout != r.Timeout { 105 if exp.Timeout { 106 add("Expected command to timeout") 107 } else { 108 add("Expected command to finish, but it hit the timeout") 109 } 110 } 111 if !matchOutput(exp.Out, r.Stdout()) { 112 add("Expected stdout to contain %q", exp.Out) 113 } 114 if !matchOutput(exp.Err, r.Stderr()) { 115 add("Expected stderr to contain %q", exp.Err) 116 } 117 switch { 118 // If a non-zero exit code is expected there is going to be an error. 119 // Don't require an error message as well as an exit code because the 120 // error message is going to be "exit status <code> which is not useful 121 case exp.Error == "" && exp.ExitCode != 0: 122 case exp.Error == "" && r.Error != nil: 123 add("Expected no error") 124 case exp.Error != "" && r.Error == nil: 125 add("Expected error to contain %q, but there was no error", exp.Error) 126 case exp.Error != "" && !strings.Contains(r.Error.Error(), exp.Error): 127 add("Expected error to contain %q", exp.Error) 128 } 129 130 if len(errors) == 0 { 131 return nil 132 } 133 return fmt.Errorf("%s\nFailures:\n%s\n", r, strings.Join(errors, "\n")) 134 } 135 136 func matchOutput(expected string, actual string) bool { 137 switch expected { 138 case None: 139 return actual == "" 140 default: 141 return strings.Contains(actual, expected) 142 } 143 } 144 145 func (r *Result) String() string { 146 var timeout string 147 if r.Timeout { 148 timeout = " (timeout)" 149 } 150 151 return fmt.Sprintf(` 152 Command: %s 153 ExitCode: %d%s, Error: %s 154 Stdout: %v 155 Stderr: %v 156 `, 157 strings.Join(r.Cmd.Args, " "), 158 r.ExitCode, 159 timeout, 160 r.Error, 161 r.Stdout(), 162 r.Stderr()) 163 } 164 165 // Expected is the expected output from a Command. This struct is compared to a 166 // Result struct by Result.Assert(). 167 type Expected struct { 168 ExitCode int 169 Timeout bool 170 Error string 171 Out string 172 Err string 173 } 174 175 // Success is the default expected result 176 var Success = Expected{} 177 178 // Stdout returns the stdout of the process as a string 179 func (r *Result) Stdout() string { 180 return r.outBuffer.String() 181 } 182 183 // Stderr returns the stderr of the process as a string 184 func (r *Result) Stderr() string { 185 return r.errBuffer.String() 186 } 187 188 // Combined returns the stdout and stderr combined into a single string 189 func (r *Result) Combined() string { 190 return r.outBuffer.String() + r.errBuffer.String() 191 } 192 193 // SetExitError sets Error and ExitCode based on Error 194 func (r *Result) SetExitError(err error) { 195 if err == nil { 196 return 197 } 198 r.Error = err 199 r.ExitCode = ProcessExitCode(err) 200 } 201 202 type matches struct{} 203 204 // Info returns the CheckerInfo 205 func (m *matches) Info() *check.CheckerInfo { 206 return &check.CheckerInfo{ 207 Name: "CommandMatches", 208 Params: []string{"result", "expected"}, 209 } 210 } 211 212 // Check compares a result against the expected 213 func (m *matches) Check(params []interface{}, names []string) (bool, string) { 214 result, ok := params[0].(*Result) 215 if !ok { 216 return false, fmt.Sprintf("result must be a *Result, not %T", params[0]) 217 } 218 expected, ok := params[1].(Expected) 219 if !ok { 220 return false, fmt.Sprintf("expected must be an Expected, not %T", params[1]) 221 } 222 223 err := result.Compare(expected) 224 if err == nil { 225 return true, "" 226 } 227 return false, err.Error() 228 } 229 230 // Matches is a gocheck.Checker for comparing a Result against an Expected 231 var Matches = &matches{} 232 233 // Cmd contains the arguments and options for a process to run as part of a test 234 // suite. 235 type Cmd struct { 236 Command []string 237 Timeout time.Duration 238 Stdin io.Reader 239 Stdout io.Writer 240 Dir string 241 Env []string 242 } 243 244 // RunCmd runs a command and returns a Result 245 func RunCmd(cmd Cmd) *Result { 246 result := StartCmd(cmd) 247 if result.Error != nil { 248 return result 249 } 250 return WaitOnCmd(cmd.Timeout, result) 251 } 252 253 // RunCommand parses a command line and runs it, returning a result 254 func RunCommand(command string, args ...string) *Result { 255 return RunCmd(Cmd{Command: append([]string{command}, args...)}) 256 } 257 258 // StartCmd starts a command, but doesn't wait for it to finish 259 func StartCmd(cmd Cmd) *Result { 260 result := buildCmd(cmd) 261 if result.Error != nil { 262 return result 263 } 264 result.SetExitError(result.Cmd.Start()) 265 return result 266 } 267 268 func buildCmd(cmd Cmd) *Result { 269 var execCmd *exec.Cmd 270 switch len(cmd.Command) { 271 case 1: 272 execCmd = exec.Command(cmd.Command[0]) 273 default: 274 execCmd = exec.Command(cmd.Command[0], cmd.Command[1:]...) 275 } 276 outBuffer := new(lockedBuffer) 277 errBuffer := new(lockedBuffer) 278 279 execCmd.Stdin = cmd.Stdin 280 execCmd.Dir = cmd.Dir 281 execCmd.Env = cmd.Env 282 if cmd.Stdout != nil { 283 execCmd.Stdout = io.MultiWriter(outBuffer, cmd.Stdout) 284 } else { 285 execCmd.Stdout = outBuffer 286 } 287 execCmd.Stderr = errBuffer 288 return &Result{ 289 Cmd: execCmd, 290 outBuffer: outBuffer, 291 errBuffer: errBuffer, 292 } 293 } 294 295 // WaitOnCmd waits for a command to complete. If timeout is non-nil then 296 // only wait until the timeout. 297 func WaitOnCmd(timeout time.Duration, result *Result) *Result { 298 if timeout == time.Duration(0) { 299 result.SetExitError(result.Cmd.Wait()) 300 return result 301 } 302 303 done := make(chan error, 1) 304 // Wait for command to exit in a goroutine 305 go func() { 306 done <- result.Cmd.Wait() 307 }() 308 309 select { 310 case <-time.After(timeout): 311 killErr := result.Cmd.Process.Kill() 312 if killErr != nil { 313 fmt.Printf("failed to kill (pid=%d): %v\n", result.Cmd.Process.Pid, killErr) 314 } 315 result.Timeout = true 316 case err := <-done: 317 result.SetExitError(err) 318 } 319 return result 320 }