123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307 |
- package cmd
- import (
- "bytes"
- "fmt"
- "io"
- "os/exec"
- "path/filepath"
- "runtime"
- "strings"
- "sync"
- "time"
- "github.com/docker/docker/pkg/system"
- "github.com/go-check/check"
- )
- type testingT interface {
- Fatalf(string, ...interface{})
- }
- const (
- // None is a token to inform Result.Assert that the output should be empty
- None string = "<NOTHING>"
- )
- type lockedBuffer struct {
- m sync.RWMutex
- buf bytes.Buffer
- }
- func (buf *lockedBuffer) Write(b []byte) (int, error) {
- buf.m.Lock()
- defer buf.m.Unlock()
- return buf.buf.Write(b)
- }
- func (buf *lockedBuffer) String() string {
- buf.m.RLock()
- defer buf.m.RUnlock()
- return buf.buf.String()
- }
- // Result stores the result of running a command
- type Result struct {
- Cmd *exec.Cmd
- ExitCode int
- Error error
- // Timeout is true if the command was killed because it ran for too long
- Timeout bool
- outBuffer *lockedBuffer
- errBuffer *lockedBuffer
- }
- // Assert compares the Result against the Expected struct, and fails the test if
- // any of the expcetations are not met.
- func (r *Result) Assert(t testingT, exp Expected) *Result {
- err := r.Compare(exp)
- if err == nil {
- return r
- }
- _, file, line, ok := runtime.Caller(1)
- if ok {
- t.Fatalf("at %s:%d - %s", filepath.Base(file), line, err.Error())
- } else {
- t.Fatalf("(no file/line info) - %s", err.Error())
- }
- return nil
- }
- // Compare returns a formatted error with the command, stdout, stderr, exit
- // code, and any failed expectations
- func (r *Result) Compare(exp Expected) error {
- errors := []string{}
- add := func(format string, args ...interface{}) {
- errors = append(errors, fmt.Sprintf(format, args...))
- }
- if exp.ExitCode != r.ExitCode {
- add("ExitCode was %d expected %d", r.ExitCode, exp.ExitCode)
- }
- if exp.Timeout != r.Timeout {
- if exp.Timeout {
- add("Expected command to timeout")
- } else {
- add("Expected command to finish, but it hit the timeout")
- }
- }
- if !matchOutput(exp.Out, r.Stdout()) {
- add("Expected stdout to contain %q", exp.Out)
- }
- if !matchOutput(exp.Err, r.Stderr()) {
- add("Expected stderr to contain %q", exp.Err)
- }
- switch {
- // If a non-zero exit code is expected there is going to be an error.
- // Don't require an error message as well as an exit code because the
- // error message is going to be "exit status <code> which is not useful
- case exp.Error == "" && exp.ExitCode != 0:
- case exp.Error == "" && r.Error != nil:
- add("Expected no error")
- case exp.Error != "" && r.Error == nil:
- add("Expected error to contain %q, but there was no error", exp.Error)
- case exp.Error != "" && !strings.Contains(r.Error.Error(), exp.Error):
- add("Expected error to contain %q", exp.Error)
- }
- if len(errors) == 0 {
- return nil
- }
- return fmt.Errorf("%s\nFailures:\n%s\n", r, strings.Join(errors, "\n"))
- }
- func matchOutput(expected string, actual string) bool {
- switch expected {
- case None:
- return actual == ""
- default:
- return strings.Contains(actual, expected)
- }
- }
- func (r *Result) String() string {
- var timeout string
- if r.Timeout {
- timeout = " (timeout)"
- }
- return fmt.Sprintf(`
- Command: %s
- ExitCode: %d%s
- Error: %v
- Stdout: %v
- Stderr: %v
- `,
- strings.Join(r.Cmd.Args, " "),
- r.ExitCode,
- timeout,
- r.Error,
- r.Stdout(),
- r.Stderr())
- }
- // Expected is the expected output from a Command. This struct is compared to a
- // Result struct by Result.Assert().
- type Expected struct {
- ExitCode int
- Timeout bool
- Error string
- Out string
- Err string
- }
- // Success is the default expected result
- var Success = Expected{}
- // Stdout returns the stdout of the process as a string
- func (r *Result) Stdout() string {
- return r.outBuffer.String()
- }
- // Stderr returns the stderr of the process as a string
- func (r *Result) Stderr() string {
- return r.errBuffer.String()
- }
- // Combined returns the stdout and stderr combined into a single string
- func (r *Result) Combined() string {
- return r.outBuffer.String() + r.errBuffer.String()
- }
- // SetExitError sets Error and ExitCode based on Error
- func (r *Result) SetExitError(err error) {
- if err == nil {
- return
- }
- r.Error = err
- r.ExitCode = system.ProcessExitCode(err)
- }
- type matches struct{}
- // Info returns the CheckerInfo
- func (m *matches) Info() *check.CheckerInfo {
- return &check.CheckerInfo{
- Name: "CommandMatches",
- Params: []string{"result", "expected"},
- }
- }
- // Check compares a result against the expected
- func (m *matches) Check(params []interface{}, names []string) (bool, string) {
- result, ok := params[0].(*Result)
- if !ok {
- return false, fmt.Sprintf("result must be a *Result, not %T", params[0])
- }
- expected, ok := params[1].(Expected)
- if !ok {
- return false, fmt.Sprintf("expected must be an Expected, not %T", params[1])
- }
- err := result.Compare(expected)
- if err == nil {
- return true, ""
- }
- return false, err.Error()
- }
- // Matches is a gocheck.Checker for comparing a Result against an Expected
- var Matches = &matches{}
- // Cmd contains the arguments and options for a process to run as part of a test
- // suite.
- type Cmd struct {
- Command []string
- Timeout time.Duration
- Stdin io.Reader
- Stdout io.Writer
- Dir string
- Env []string
- }
- // Command create a simple Cmd with the specified command and arguments
- func Command(command string, args ...string) Cmd {
- return Cmd{Command: append([]string{command}, args...)}
- }
- // RunCmd runs a command and returns a Result
- func RunCmd(cmd Cmd, cmdOperators ...func(*Cmd)) *Result {
- for _, op := range cmdOperators {
- op(&cmd)
- }
- result := StartCmd(cmd)
- if result.Error != nil {
- return result
- }
- return WaitOnCmd(cmd.Timeout, result)
- }
- // RunCommand parses a command line and runs it, returning a result
- func RunCommand(command string, args ...string) *Result {
- return RunCmd(Command(command, args...))
- }
- // StartCmd starts a command, but doesn't wait for it to finish
- func StartCmd(cmd Cmd) *Result {
- result := buildCmd(cmd)
- if result.Error != nil {
- return result
- }
- result.SetExitError(result.Cmd.Start())
- return result
- }
- func buildCmd(cmd Cmd) *Result {
- var execCmd *exec.Cmd
- switch len(cmd.Command) {
- case 1:
- execCmd = exec.Command(cmd.Command[0])
- default:
- execCmd = exec.Command(cmd.Command[0], cmd.Command[1:]...)
- }
- outBuffer := new(lockedBuffer)
- errBuffer := new(lockedBuffer)
- execCmd.Stdin = cmd.Stdin
- execCmd.Dir = cmd.Dir
- execCmd.Env = cmd.Env
- if cmd.Stdout != nil {
- execCmd.Stdout = io.MultiWriter(outBuffer, cmd.Stdout)
- } else {
- execCmd.Stdout = outBuffer
- }
- execCmd.Stderr = errBuffer
- return &Result{
- Cmd: execCmd,
- outBuffer: outBuffer,
- errBuffer: errBuffer,
- }
- }
- // WaitOnCmd waits for a command to complete. If timeout is non-nil then
- // only wait until the timeout.
- func WaitOnCmd(timeout time.Duration, result *Result) *Result {
- if timeout == time.Duration(0) {
- result.SetExitError(result.Cmd.Wait())
- return result
- }
- done := make(chan error, 1)
- // Wait for command to exit in a goroutine
- go func() {
- done <- result.Cmd.Wait()
- }()
- select {
- case <-time.After(timeout):
- killErr := result.Cmd.Process.Kill()
- if killErr != nil {
- fmt.Printf("failed to kill (pid=%d): %v\n", result.Cmd.Process.Pid, killErr)
- }
- result.Timeout = true
- case err := <-done:
- result.SetExitError(err)
- }
- return result
- }
|