Skip to content

Commit

Permalink
Merge pull request #13054 from hashicorp/backport/acc_test_logic/wron…
Browse files Browse the repository at this point in the history
…gly-witty-moccasin

This pull request was automerged via backport-assistant
  • Loading branch information
hc-github-team-packer authored Jun 17, 2024
2 parents 7f407b1 + abf86c8 commit b58ceab
Show file tree
Hide file tree
Showing 55 changed files with 4,349 additions and 0 deletions.
36 changes: 36 additions & 0 deletions packer_test/base_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package packer_test

import (
"fmt"
"math/rand"
"os"
"os/exec"
"path/filepath"
"runtime"
"testing"
)

// BuildTestPacker builds a new Packer binary based on the current state of the repository.
//
// If for some reason the binary cannot be built, we will immediately exit with an error.
func BuildTestPacker(t *testing.T) (string, error) {
testDir, err := currentDir()
if err != nil {
return "", fmt.Errorf("failed to compile packer binary: %s", err)
}

packerCoreDir := filepath.Dir(testDir)

outBin := filepath.Join(os.TempDir(), fmt.Sprintf("packer_core-%d", rand.Int()))
if runtime.GOOS == "windows" {
outBin = fmt.Sprintf("%s.exe", outBin)
}

compileCommand := exec.Command("go", "build", "-C", packerCoreDir, "-o", outBin)
logs, err := compileCommand.CombinedOutput()
if err != nil {
t.Fatalf("failed to compile Packer core: %s\ncompilation logs: %s", err, logs)
}

return outBin, nil
}
118 changes: 118 additions & 0 deletions packer_test/commands_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package packer_test

import (
"fmt"
"os"
"os/exec"
"strings"
"sync"
"testing"
)

type packerCommand struct {
once sync.Once
packerPath string
args []string
env map[string]string
stderr *strings.Builder
stdout *strings.Builder
workdir string
err error
t *testing.T
}

// PackerCommand creates a skeleton of packer command with the ability to execute gadgets on the outputs of the command.
func (ts *PackerTestSuite) PackerCommand() *packerCommand {
stderr := &strings.Builder{}
stdout := &strings.Builder{}

return &packerCommand{
packerPath: ts.packerPath,
env: map[string]string{
"PACKER_LOG": "1",
// Required for Windows, otherwise since we overwrite all
// the envvars for the test and Go relies on that envvar
// being set in order to return another path than
// C:\Windows by default
//
// If we don't have it, Packer immediately errors upon
// invocation as the temporary logfile that we write in
// case of Panic will fail to be created (unless tests
// are running as Administrator, but please don't).
"TMP": os.TempDir(),
},
stderr: stderr,
stdout: stdout,
t: ts.T(),
}
}

// NoVerbose removes the `PACKER_LOG=1` environment variable from the command
func (pc *packerCommand) NoVerbose() *packerCommand {
_, ok := pc.env["PACKER_LOG"]
if ok {
delete(pc.env, "PACKER_LOG")
}
return pc
}

// SetWD changes the directory Packer is invoked from
func (pc *packerCommand) SetWD(dir string) *packerCommand {
pc.workdir = dir
return pc
}

// UsePluginDir sets the plugin directory in the environment to `dir`
func (pc *packerCommand) UsePluginDir(dir string) *packerCommand {
return pc.AddEnv("PACKER_PLUGIN_PATH", dir)
}

func (pc *packerCommand) SetArgs(args ...string) *packerCommand {
pc.args = args
return pc
}

func (pc *packerCommand) AddEnv(key, val string) *packerCommand {
pc.env[key] = val
return pc
}

// Run executes the packer command with the args/env requested and returns the
// output streams (stdout, stderr)
//
// Note: "Run" will only execute the command once, and return the streams and
// error from the only execution for every subsequent call
func (pc *packerCommand) Run() (string, string, error) {
pc.once.Do(pc.doRun)

return pc.stdout.String(), pc.stderr.String(), pc.err
}

func (pc *packerCommand) doRun() {
cmd := exec.Command(pc.packerPath, pc.args...)
for key, val := range pc.env {
cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", key, val))
}
cmd.Stdout = pc.stdout
cmd.Stderr = pc.stderr

if pc.workdir != "" {
cmd.Dir = pc.workdir
}

pc.err = cmd.Run()
}

func (pc *packerCommand) Assert(checks ...Checker) {
stdout, stderr, err := pc.Run()

checks = append(checks, PanicCheck{})

for _, check := range checks {
checkErr := check.Check(stdout, stderr, err)
if checkErr != nil {
checkerName := InferName(check)
pc.t.Errorf("check %q failed: %s", checkerName, checkErr)
}
}
}
170 changes: 170 additions & 0 deletions packer_test/gadgets_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
package packer_test

import (
"fmt"
"reflect"
"strings"
"testing"
)

type Stream int

const (
// BothStreams will use both stdout and stderr for performing a check
BothStreams Stream = iota
// OnlyStdout will only use stdout for performing a check
OnlyStdout
// OnlySterr will only use stderr for performing a check
OnlyStderr
)

func (s Stream) String() string {
switch s {
case BothStreams:
return "Both streams"
case OnlyStdout:
return "Stdout"
case OnlyStderr:
return "Stderr"
}

panic(fmt.Sprintf("Unknown stream value: %d", s))
}

// Checker represents anything that can be used in conjunction with Assert.
//
// The role of a checker is performing a test on a command's outputs/error, and
// return an error if the test fails.
//
// Note: the Check method is the only required, however during tests the name
// of the checker is printed out in case it fails, so it may be useful to have
// a custom string for this: the `Name() string` method is exactly what to
// implement for this kind of customization.
type Checker interface {
Check(stdout, stderr string, err error) error
}

func InferName(c Checker) string {
if c == nil {
panic("nil checker - malformed test?")
}

checkerType := reflect.TypeOf(c)
_, ok := checkerType.MethodByName("Name")
if !ok {
return checkerType.String()
}

retVals := reflect.ValueOf(c).MethodByName("Name").Call([]reflect.Value{})
if len(retVals) != 1 {
panic(fmt.Sprintf("Name function called - returned %d values. Must be one string only.", len(retVals)))
}

return retVals[0].String()
}

func MustSucceed() Checker {
return mustSucceed{}
}

type mustSucceed struct{}

func (_ mustSucceed) Check(stdout, stderr string, err error) error {
return err
}

func MustFail() Checker {
return mustFail{}
}

type mustFail struct{}

func (_ mustFail) Check(stdout, stderr string, err error) error {
if err == nil {
return fmt.Errorf("unexpected command success")
}
return nil
}

type grepOpts int

const (
// Invert the check, i.e. by default an empty grep fails, if this is set, a non-empty grep fails
grepInvert grepOpts = iota
// Only grep stderr
grepStderr
// Only grep stdout
grepStdout
)

// Grep returns a checker that performs a regexp match on the command's output and returns an error if it failed
//
// Note: by default both streams will be checked by the grep
func Grep(expression string, opts ...grepOpts) Checker {
pc := PipeChecker{
name: fmt.Sprintf("command | grep -E %q", expression),
stream: BothStreams,
pipers: []Pipe{
PipeGrep(expression),
},
check: ExpectNonEmptyInput(),
}
for _, opt := range opts {
switch opt {
case grepInvert:
pc.check = ExpectEmptyInput()
case grepStderr:
pc.stream = OnlyStderr
case grepStdout:
pc.stream = OnlyStdout
}
}
return pc
}

type Dump struct {
t *testing.T
}

func (d Dump) Check(stdout, stderr string, err error) error {
d.t.Logf("Dumping command result.")
d.t.Logf("Stdout: %s", stdout)
d.t.Logf("stderr: %s", stderr)
return nil
}

type PanicCheck struct{}

func (_ PanicCheck) Check(stdout, stderr string, _ error) error {
if strings.Contains(stdout, "= PACKER CRASH =") || strings.Contains(stderr, "= PACKER CRASH =") {
return fmt.Errorf("packer has crashed: this is never normal and should be investigated")
}
return nil
}

// CustomCheck is meant to be a one-off checker with a user-provided function.
//
// Use this if none of the existing checkers match your use case, and it is not
// reusable/generic enough for use in other tests.
type CustomCheck struct {
name string
checkFunc func(stdout, stderr string, err error) error
}

func (c CustomCheck) Check(stdout, stderr string, err error) error {
return c.checkFunc(stdout, stderr, err)
}

func (c CustomCheck) Name() string {
return fmt.Sprintf("custom check - %s", c.name)
}

// LineCountCheck builds a pipe checker to count the number of lines on stdout by default
//
// To change the stream(s) on which to perform the check, you can call SetStream on the
// returned pipe checker.
func LineCountCheck(lines int) *PipeChecker {
return MkPipeCheck(fmt.Sprintf("line count (%d)", lines), LineCount()).
SetTester(IntCompare(eq, lines)).
SetStream(OnlyStdout)
}
Loading

0 comments on commit b58ceab

Please sign in to comment.