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 }