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  }