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  }