github.com/hashicorp/packer@v1.14.3/packer_test/common/commands.go (about)

     1  package common
     2  
     3  import (
     4  	"fmt"
     5  	"os"
     6  	"os/exec"
     7  	"strings"
     8  	"testing"
     9  
    10  	"github.com/hashicorp/packer/packer_test/common/check"
    11  )
    12  
    13  type packerCommand struct {
    14  	runs         int
    15  	packerPath   string
    16  	args         []string
    17  	env          map[string]string
    18  	stdin        string
    19  	stderr       *strings.Builder
    20  	stdout       *strings.Builder
    21  	workdir      string
    22  	err          error
    23  	t            *testing.T
    24  	fatalfAssert bool
    25  }
    26  
    27  // PackerCommand creates a skeleton of packer command with the ability to execute gadgets on the outputs of the command.
    28  func (ts *PackerTestSuite) PackerCommand() *packerCommand {
    29  	return &packerCommand{
    30  		packerPath: ts.packerPath,
    31  		runs:       1,
    32  		env: map[string]string{
    33  			"PACKER_LOG": "1",
    34  			// Required for Windows, otherwise since we overwrite all
    35  			// the envvars for the test and Go relies on that envvar
    36  			// being set in order to return another path than
    37  			// C:\Windows by default
    38  			//
    39  			// If we don't have it, Packer immediately errors upon
    40  			// invocation as the temporary logfile that we write in
    41  			// case of Panic will fail to be created (unless tests
    42  			// are running as Administrator, but please don't).
    43  			"TMP": os.TempDir(),
    44  			// Since those commands are used to run tests, we want to
    45  			// make them as self-contained and quick as possible.
    46  			// Removing telemetry here is probably for the best.
    47  			"CHECKPOINT_DISABLE": "1",
    48  		},
    49  		t: ts.T(),
    50  	}
    51  }
    52  
    53  // NoVerbose removes the `PACKER_LOG=1` environment variable from the command
    54  func (pc *packerCommand) NoVerbose() *packerCommand {
    55  	_, ok := pc.env["PACKER_LOG"]
    56  	if ok {
    57  		delete(pc.env, "PACKER_LOG")
    58  	}
    59  	return pc
    60  }
    61  
    62  // SetWD changes the directory Packer is invoked from
    63  func (pc *packerCommand) SetWD(dir string) *packerCommand {
    64  	pc.workdir = dir
    65  	return pc
    66  }
    67  
    68  // UsePluginDir sets the plugin directory in the environment to `dir`
    69  func (pc *packerCommand) UsePluginDir(dir *PluginDirSpec) *packerCommand {
    70  	return pc.UseRawPluginDir(dir.dirPath)
    71  }
    72  
    73  // UseRawPluginDir is meant to be used for setting the plugin directory with a
    74  // raw directory path instead of a PluginDirSpec.
    75  func (pc *packerCommand) UseRawPluginDir(dirPath string) *packerCommand {
    76  	return pc.AddEnv("PACKER_PLUGIN_PATH", dirPath)
    77  }
    78  
    79  func (pc *packerCommand) SetArgs(args ...string) *packerCommand {
    80  	pc.args = args
    81  	return pc
    82  }
    83  
    84  func (pc *packerCommand) AddEnv(key, val string) *packerCommand {
    85  	pc.env[key] = val
    86  	return pc
    87  }
    88  
    89  // Runs changes the number of times the command is run.
    90  //
    91  // This is useful for testing non-deterministic bugs, which we can reasonably
    92  // execute multiple times and expose a dysfunctional run.
    93  //
    94  // This is not necessarily a guarantee that the code is sound, but so long as
    95  // we run the test enough times, we can be decently confident the problem has
    96  // been solved.
    97  func (pc *packerCommand) Runs(runs int) *packerCommand {
    98  	if runs <= 0 {
    99  		panic(fmt.Sprintf("cannot set command runs to %d", runs))
   100  	}
   101  
   102  	pc.runs = runs
   103  	return pc
   104  }
   105  
   106  // Stdin changes the contents of the stdin for the command.
   107  //
   108  // Each run will be populated with a copy of this string, and wait for the
   109  // command to terminate.
   110  //
   111  // Note: this could lead to a deadlock if the command doesn't support stdin
   112  // closing after it's finished feeding the inputs.
   113  func (pc *packerCommand) Stdin(in string) *packerCommand {
   114  	pc.stdin = in
   115  	return pc
   116  }
   117  
   118  // SetAssertFatal allows changing how Assert behaves when reporting an error.
   119  //
   120  // By default Assert will invoke t.Errorf with the error details, but this can be
   121  // changed to a t.Fatalf so that if the assertion fails, the test invoking it will
   122  // also immediately fail and stop execution.
   123  func (pc *packerCommand) SetAssertFatal() *packerCommand {
   124  	pc.fatalfAssert = true
   125  	return pc
   126  }
   127  
   128  // Run executes the packer command with the args/env requested and returns the
   129  // output streams (stdout, stderr)
   130  //
   131  // Note: while originally "Run" was designed to be idempotent, with the
   132  // introduction of multiple runs for a command, this is not the case anymore
   133  // and the function should not be considered thread-safe anymore.
   134  func (pc *packerCommand) run() (string, string, error) {
   135  	if pc.runs <= 0 {
   136  		return pc.stdout.String(), pc.stderr.String(), pc.err
   137  	}
   138  	pc.runs--
   139  
   140  	pc.stdout = &strings.Builder{}
   141  	pc.stderr = &strings.Builder{}
   142  
   143  	cmd := exec.Command(pc.packerPath, pc.args...)
   144  	for key, val := range pc.env {
   145  		cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", key, val))
   146  	}
   147  	cmd.Stdout = pc.stdout
   148  	cmd.Stderr = pc.stderr
   149  
   150  	if pc.stdin != "" {
   151  		cmd.Stdin = strings.NewReader(pc.stdin)
   152  	}
   153  
   154  	if pc.workdir != "" {
   155  		cmd.Dir = pc.workdir
   156  	}
   157  
   158  	pc.err = cmd.Run()
   159  
   160  	// Check that the command didn't panic, and if it did, we can immediately error
   161  	panicErr := check.PanicCheck{}.Check(pc.stdout.String(), pc.stderr.String(), pc.err)
   162  	if panicErr != nil {
   163  		pc.t.Fatalf("Packer panicked during execution: %s", panicErr)
   164  	}
   165  
   166  	return pc.stdout.String(), pc.stderr.String(), pc.err
   167  }
   168  
   169  // Output returns the results of the latest Run that was executed.
   170  //
   171  // In general there is only one run of the command, but as it can be changed
   172  // through the Runs function, only the latest run will be returned.
   173  //
   174  // If the command was not run (through Assert), this will make the test fail
   175  // immediately.
   176  func (pc *packerCommand) Output() (string, string, error) {
   177  	if pc.runs > 0 {
   178  		pc.t.Fatalf("command was not run, invoke Assert first, then Output.")
   179  	}
   180  
   181  	return pc.stdout.String(), pc.stderr.String(), pc.err
   182  }
   183  
   184  func (pc *packerCommand) Assert(checks ...check.Checker) {
   185  	attempt := 0
   186  	for pc.runs > 0 {
   187  		attempt++
   188  		stdout, stderr, err := pc.run()
   189  
   190  		for _, checker := range checks {
   191  			checkErr := checker.Check(stdout, stderr, err)
   192  			if checkErr != nil {
   193  				checkerName := check.InferName(checker)
   194  
   195  				pc.t.Errorf("check %q failed: %s", checkerName, checkErr)
   196  			}
   197  		}
   198  
   199  		if pc.t.Failed() {
   200  			pc.t.Errorf("attempt %d failed validation", attempt)
   201  
   202  			pc.t.Logf("dumping stdout: %s", stdout)
   203  			pc.t.Logf("dumping stdout: %s", stderr)
   204  
   205  			if pc.fatalfAssert {
   206  				pc.t.Fatalf("stopping test now because of failures reported")
   207  			}
   208  
   209  			break
   210  		}
   211  	}
   212  }