github.com/terraform-modules-krish/terratest@v0.29.0/modules/shell/command.go (about)

     1  package shell
     2  
     3  import (
     4  	"bufio"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"os"
     9  	"os/exec"
    10  	"strings"
    11  	"sync"
    12  	"syscall"
    13  
    14  	"github.com/terraform-modules-krish/terratest/modules/logger"
    15  	"github.com/terraform-modules-krish/terratest/modules/testing"
    16  	"github.com/stretchr/testify/require"
    17  )
    18  
    19  // Command is a simpler struct for defining commands than Go's built-in Cmd.
    20  type Command struct {
    21  	Command    string            // The command to run
    22  	Args       []string          // The args to pass to the command
    23  	WorkingDir string            // The working directory
    24  	Env        map[string]string // Additional environment variables to set
    25  	// Use the specified logger for the command's output. Use logger.Discard to not print the output while executing the command.
    26  	Logger *logger.Logger
    27  }
    28  
    29  // RunCommand runs a shell command and redirects its stdout and stderr to the stdout of the atomic script itself. If
    30  // there are any errors, fail the test.
    31  func RunCommand(t testing.TestingT, command Command) {
    32  	err := RunCommandE(t, command)
    33  	require.NoError(t, err)
    34  }
    35  
    36  // RunCommandE runs a shell command and redirects its stdout and stderr to the stdout of the atomic script itself. Any
    37  // returned error will be of type ErrWithCmdOutput, containing the output streams and the underlying error.
    38  func RunCommandE(t testing.TestingT, command Command) error {
    39  	output, err := runCommand(t, command)
    40  	if err != nil {
    41  		return &ErrWithCmdOutput{err, output}
    42  	}
    43  	return nil
    44  }
    45  
    46  // RunCommandAndGetOutput runs a shell command and returns its stdout and stderr as a string. The stdout and stderr of
    47  // that command will also be logged with Command.Log to make debugging easier. If there are any errors, fail the test.
    48  func RunCommandAndGetOutput(t testing.TestingT, command Command) string {
    49  	out, err := RunCommandAndGetOutputE(t, command)
    50  	require.NoError(t, err)
    51  	return out
    52  }
    53  
    54  // RunCommandAndGetOutputE runs a shell command and returns its stdout and stderr as a string. The stdout and stderr of
    55  // that command will also be logged with Command.Log to make debugging easier. Any returned error will be of type
    56  // ErrWithCmdOutput, containing the output streams and the underlying error.
    57  func RunCommandAndGetOutputE(t testing.TestingT, command Command) (string, error) {
    58  	output, err := runCommand(t, command)
    59  	if err != nil {
    60  		return output.Combined(), &ErrWithCmdOutput{err, output}
    61  	}
    62  
    63  	return output.Combined(), nil
    64  }
    65  
    66  // RunCommandAndGetStdOut runs a shell command and returns solely its stdout (but not stderr) as a string. The stdout and
    67  // stderr of that command will also be logged with Command.Log to make debugging easier. If there are any errors, fail
    68  // the test.
    69  func RunCommandAndGetStdOut(t testing.TestingT, command Command) string {
    70  	output, err := RunCommandAndGetStdOutE(t, command)
    71  	require.NoError(t, err)
    72  	return output
    73  }
    74  
    75  // RunCommandAndGetStdOutE runs a shell command and returns solely its stdout (but not stderr) as a string. The stdout
    76  // and stderr of that command will also be printed to the stdout and stderr of this Go program to make debugging easier.
    77  // Any returned error will be of type ErrWithCmdOutput, containing the output streams and the underlying error.
    78  func RunCommandAndGetStdOutE(t testing.TestingT, command Command) (string, error) {
    79  	output, err := runCommand(t, command)
    80  	if err != nil {
    81  		return output.Stdout(), &ErrWithCmdOutput{err, output}
    82  	}
    83  
    84  	return output.Stdout(), nil
    85  }
    86  
    87  type ErrWithCmdOutput struct {
    88  	Underlying error
    89  	Output     *output
    90  }
    91  
    92  func (e *ErrWithCmdOutput) Error() string {
    93  	return fmt.Sprintf("error while running command: %v; %s", e.Underlying, e.Output.Stderr())
    94  }
    95  
    96  // runCommand runs a shell command and stores each line from stdout and stderr in Output. Depending on the logger, the
    97  // stdout and stderr of that command will also be printed to the stdout and stderr of this Go program to make debugging
    98  // easier.
    99  func runCommand(t testing.TestingT, command Command) (*output, error) {
   100  	command.Logger.Logf(t, "Running command %s with args %s", command.Command, command.Args)
   101  
   102  	cmd := exec.Command(command.Command, command.Args...)
   103  	cmd.Dir = command.WorkingDir
   104  	cmd.Stdin = os.Stdin
   105  	cmd.Env = formatEnvVars(command)
   106  
   107  	stdout, err := cmd.StdoutPipe()
   108  	if err != nil {
   109  		return nil, err
   110  	}
   111  
   112  	stderr, err := cmd.StderrPipe()
   113  	if err != nil {
   114  		return nil, err
   115  	}
   116  
   117  	err = cmd.Start()
   118  	if err != nil {
   119  		return nil, err
   120  	}
   121  
   122  	output, err := readStdoutAndStderr(t, command.Logger, stdout, stderr)
   123  	if err != nil {
   124  		return output, err
   125  	}
   126  
   127  	return output, cmd.Wait()
   128  }
   129  
   130  // This function captures stdout and stderr into the given variables while still printing it to the stdout and stderr
   131  // of this Go program
   132  func readStdoutAndStderr(t testing.TestingT, log *logger.Logger, stdout, stderr io.ReadCloser) (*output, error) {
   133  	out := newOutput()
   134  	stdoutReader := bufio.NewReader(stdout)
   135  	stderrReader := bufio.NewReader(stderr)
   136  
   137  	wg := &sync.WaitGroup{}
   138  
   139  	wg.Add(2)
   140  	var stdoutErr, stderrErr error
   141  	go func() {
   142  		defer wg.Done()
   143  		stdoutErr = readData(t, log, stdoutReader, out.stdout)
   144  	}()
   145  	go func() {
   146  		defer wg.Done()
   147  		stderrErr = readData(t, log, stderrReader, out.stderr)
   148  	}()
   149  	wg.Wait()
   150  
   151  	if stdoutErr != nil {
   152  		return out, stdoutErr
   153  	}
   154  	if stderrErr != nil {
   155  		return out, stderrErr
   156  	}
   157  
   158  	return out, nil
   159  }
   160  
   161  func readData(t testing.TestingT, log *logger.Logger, reader *bufio.Reader, writer io.StringWriter) error {
   162  	var line string
   163  	var readErr error
   164  	for {
   165  		line, readErr = reader.ReadString('\n')
   166  
   167  		// remove newline, our output is in a slice,
   168  		// one element per line.
   169  		line = strings.TrimSuffix(line, "\n")
   170  
   171  		// only return early if the line does not have
   172  		// any contents. We could have a line that does
   173  		// not not have a newline before io.EOF, we still
   174  		// need to add it to the output.
   175  		if len(line) == 0 && readErr == io.EOF {
   176  			break
   177  		}
   178  
   179  		log.Logf(t, line)
   180  		if _, err := writer.WriteString(line); err != nil {
   181  			return err
   182  		}
   183  
   184  		if readErr != nil {
   185  			break
   186  		}
   187  	}
   188  	if readErr != io.EOF {
   189  		return readErr
   190  	}
   191  	return nil
   192  }
   193  
   194  // GetExitCodeForRunCommandError tries to read the exit code for the error object returned from running a shell command. This is a bit tricky to do
   195  // in a way that works across platforms.
   196  func GetExitCodeForRunCommandError(err error) (int, error) {
   197  	if errWithOutput, ok := err.(*ErrWithCmdOutput); ok {
   198  		err = errWithOutput.Underlying
   199  	}
   200  
   201  	// http://stackoverflow.com/a/10385867/483528
   202  	if exitErr, ok := err.(*exec.ExitError); ok {
   203  		// The program has exited with an exit code != 0
   204  
   205  		// This works on both Unix and Windows. Although package
   206  		// syscall is generally platform dependent, WaitStatus is
   207  		// defined for both Unix and Windows and in both cases has
   208  		// an ExitStatus() method with the same signature.
   209  		if status, ok := exitErr.Sys().(syscall.WaitStatus); ok {
   210  			return status.ExitStatus(), nil
   211  		}
   212  		return 1, errors.New("could not determine exit code")
   213  	}
   214  
   215  	return 0, nil
   216  }
   217  
   218  func formatEnvVars(command Command) []string {
   219  	env := os.Environ()
   220  	for key, value := range command.Env {
   221  		env = append(env, fmt.Sprintf("%s=%s", key, value))
   222  	}
   223  	return env
   224  }