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