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