Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

packer_test: make packer test suite modular #13041

Merged
merged 5 commits into from
Aug 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package packer_test
package core_test
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why core_test? What information does it provide that packer_test may not?


import "fmt"
import (
"fmt"

func (ts *PackerTestSuite) TestEvalLocalsOrder() {
"github.com/hashicorp/packer/packer_test/lib"
)

func (ts *PackerCoreTestSuite) TestEvalLocalsOrder() {
ts.SkipNoAcc()

pluginDir, cleanup := ts.MakePluginDir()
Expand All @@ -12,20 +16,21 @@ func (ts *PackerTestSuite) TestEvalLocalsOrder() {
Runs(10).
Stdin("local.test_local\n").
SetArgs("console", "./templates/locals_no_order.pkr.hcl").
Assert(MustSucceed(), Grep("\\[\\]", grepStdout, grepInvert))
Assert(lib.MustSucceed(),
lib.Grep("\\[\\]", lib.GrepStdout, lib.GrepInvert))
}

func (ts *PackerTestSuite) TestLocalDuplicates() {
func (ts *PackerCoreTestSuite) TestLocalDuplicates() {
pluginDir, cleanup := ts.MakePluginDir()
defer cleanup()

for _, cmd := range []string{"console", "validate", "build"} {
ts.Run(fmt.Sprintf("duplicate local detection with %s command - expect error", cmd), func() {
ts.PackerCommand().UsePluginDir(pluginDir).
SetArgs(cmd, "./templates/locals_duplicate.pkr.hcl").
Assert(MustFail(),
Grep("Duplicate local definition"),
Grep("Local variable \"test\" is defined twice"))
Assert(lib.MustFail(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My first question here is what is lib? Why not just MustFail(). The generic name lib gives no information on the relation of MustFail() to the test suite.

What do you think if we renamed Assert to RunAndAssert to make it clear that Assert runs the test suite and succeeds if the asserts are true?

RunAndAssert(
  MustFail(), 
  Grep("Duplicate local definition"), 
  Grep("Local variable \"test\" is defined twice"))

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lib is the common parts, so the base TestSuite that you can extend in other packages, and the gadgets for testing, so all the checkers/pipe infrastructure.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can name them common too if you prefer? I went with lib because I like the conciseness, but I don't mind renaming this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As for renaming Assert to RunAndAssert, I agree it's clearer, but it's also longer, I like terse function names :p
But I'm not fundamentally against the renaming tbh

lib.Grep("Duplicate local definition"),
lib.Grep("Local variable \"test\" is defined twice"))
})
}
}
23 changes: 23 additions & 0 deletions packer_test/core_tests/suite_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package core_test

import (
"testing"

"github.com/hashicorp/packer/packer_test/lib"
"github.com/stretchr/testify/suite"
)

type PackerCoreTestSuite struct {
*lib.PackerTestSuite
}

func Test_PackerPluginSuite(t *testing.T) {
baseSuite, cleanup := lib.PackerCoreSuite(t)
Comment on lines +10 to +15
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand the need for PackerCoreTestSuite over lib.PackerTestSuite and how they relate or differ from lib.PackerCoreSuite(). Is this level of abstraction offering a benefit that may not be immediately noticeable?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the difference between the two is mostly that the tests run are not the same, being two separate packages/instances of TestSuite means that both can run in parallel. They also may not have the same needs for plugins for example, so one will have to compile a bunch of plugins, and others don't.
As for the names right now, I picked something somewhat randomly, I'm completely open to rename them to something else if you think it's confusing, no problems here!

defer cleanup()

ts := &PackerCoreTestSuite{
baseSuite,
}

suite.Run(t, ts)
}
4 changes: 2 additions & 2 deletions packer_test/base_test.go → packer_test/lib/base.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package packer_test
package lib
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have to dive into these changes further. But I hesitate to treat this as a separate package. I changed the name to packer_test, as that is the Go idiom for denoting that this is a test package, which does not have direct access to anything in the packer package and also to ensure that the package is excluded from the final binary.

Previously, the go files ended with _test. go, which are excluded from the binary. It looks like this refactor changes that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would think the code will not be part of the final binary still, as it's not transitively imported from main.go, that said if you think that the package name is a problem, we can still move the lib files back to the upper package so that packages using the test suite will still include something called _test.


import (
"fmt"
Expand All @@ -19,7 +19,7 @@ func BuildTestPacker(t *testing.T) (string, error) {
return "", fmt.Errorf("failed to compile packer binary: %s", err)
}

packerCoreDir := filepath.Dir(testDir)
packerCoreDir := filepath.Dir(filepath.Dir(testDir))

outBin := filepath.Join(os.TempDir(), fmt.Sprintf("packer_core-%d", rand.Int()))
if runtime.GOOS == "windows" {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package packer_test
package lib

import (
"fmt"
Expand Down
127 changes: 127 additions & 0 deletions packer_test/lib/fs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package lib

import (
"crypto/sha256"
"fmt"
"io"
"os"
"path/filepath"
"runtime"
"testing"
)

// CopyFile essentially replicates the `cp` command, for a file only.
//
// # Permissions are copied over from the source to destination
//
// The function detects if destination is a directory or a file (existent or not).
//
// If this is the former, we append the source file's basename to the
// directory and create the file from that inferred path.
func CopyFile(t *testing.T, dest, src string) {
st, err := os.Stat(src)
if err != nil {
t.Fatalf("failed to stat origin file %q: %s", src, err)
}

// If the stat call fails, we assume dest is the destination file.
dstStat, err := os.Stat(dest)
if err == nil && dstStat.IsDir() {
dest = filepath.Join(dest, filepath.Base(src))
}

destFD, err := os.OpenFile(dest, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, st.Mode().Perm())
if err != nil {
t.Fatalf("failed to create cp destination file %q: %s", dest, err)
}
defer destFD.Close()

srcFD, err := os.Open(src)
if err != nil {
t.Fatalf("failed to open source file to copy: %s", err)
}
defer srcFD.Close()

_, err = io.Copy(destFD, srcFD)
if err != nil {
t.Fatalf("failed to copy from %q -> %q: %s", src, dest, err)
}
}

// WriteFile writes `content` to a file `dest`
//
// The default permissions of that file is 0644
func WriteFile(t *testing.T, dest string, content string) {
outFile, err := os.OpenFile(dest, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
if err != nil {
t.Fatalf("failed to open/create %q: %s", dest, err)
}
defer outFile.Close()

_, err = fmt.Fprintf(outFile, content)
if err != nil {
t.Fatalf("failed to write to file %q: %s", dest, err)
}
}

// TempWorkdir creates a working directory for a Packer test with the list of files
// given as input.
//
// The files should either have a path relative to the test that invokes it, or should
// be absolute.
// Each file will be copied to the root of the workdir being created.
//
// If any file cannot be found, this function will fail
func TempWorkdir(t *testing.T, files ...string) (string, func()) {
var err error
tempDir, err := os.MkdirTemp("", "packer-test-workdir-")
if err != nil {
t.Fatalf("failed to create temporary working directory: %s", err)
}

defer func() {
if err != nil {
os.RemoveAll(tempDir)
t.Errorf("failed to create temporary workdir: %s", err)
}
}()

for _, file := range files {
CopyFile(t, tempDir, file)
}

return tempDir, func() {
err := os.RemoveAll(tempDir)
if err != nil {
t.Logf("failed to remove temporary workdir %q: %s. This will need manual action.", tempDir, err)
}
}
}

// SHA256Sum computes the SHA256 digest for an input file
//
// The digest is returned as a hexstring
func SHA256Sum(t *testing.T, file string) string {
fl, err := os.ReadFile(file)
if err != nil {
t.Fatalf("failed to compute sha256sum for %q: %s", file, err)
}
sha := sha256.New()
sha.Write(fl)
return fmt.Sprintf("%x", sha.Sum([]byte{}))
}

// currentDir returns the directory in which the current file is located.
//
// Since we're in tests it's reliable as they're supposed to run on the same
// machine the binary's compiled from, but goes to say it's not meant for use
// in distributed binaries.
func currentDir() (string, error) {
// pc uintptr, file string, line int, ok bool
_, testDir, _, ok := runtime.Caller(0)
if !ok {
return "", fmt.Errorf("couldn't get the location of the test suite file")
}

return filepath.Dir(testDir), nil
}
82 changes: 70 additions & 12 deletions packer_test/gadgets_test.go → packer_test/lib/gadgets.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package packer_test
package lib

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

"github.com/hashicorp/go-multierror"
"github.com/hashicorp/go-version"
)

type Stream int
Expand Down Expand Up @@ -86,21 +89,21 @@ func (_ mustFail) Check(stdout, stderr string, err error) error {
return nil
}

type grepOpts int
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
GrepInvert GrepOpts = iota
// Only grep stderr
grepStderr
GrepStderr
// Only grep stdout
grepStdout
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 {
func Grep(expression string, opts ...GrepOpts) Checker {
pc := PipeChecker{
name: fmt.Sprintf("command | grep -E %q", expression),
stream: BothStreams,
Expand All @@ -111,22 +114,77 @@ func Grep(expression string, opts ...grepOpts) Checker {
}
for _, opt := range opts {
switch opt {
case grepInvert:
case GrepInvert:
pc.check = ExpectEmptyInput()
case grepStderr:
case GrepStderr:
pc.stream = OnlyStderr
case grepStdout:
case GrepStdout:
pc.stream = OnlyStdout
}
}
return pc
}

type Dump struct {
type PluginVersionTuple struct {
Source string
Version *version.Version
}

func NewPluginVersionTuple(src, pluginVersion string) PluginVersionTuple {
ver := version.Must(version.NewVersion(pluginVersion))
return PluginVersionTuple{
Source: src,
Version: ver,
}
}

type pluginsUsed struct {
invert bool
plugins []PluginVersionTuple
}

func (pu pluginsUsed) Check(stdout, stderr string, _ error) error {
var opts []GrepOpts
if !pu.invert {
opts = append(opts, GrepInvert)
}

var retErr error

for _, pvt := range pu.plugins {
// `error` is ignored for Grep, so we can pass in nil
err := Grep(
fmt.Sprintf("%s_v%s[^:]+\\\\s*plugin process exited", pvt.Source, pvt.Version.Core()),
opts...,
).Check(stdout, stderr, nil)
if err != nil {
retErr = multierror.Append(retErr, err)
}
}

return retErr
}

// PluginsUsed is a glorified `Grep` checker that looks for a bunch of plugins
// used from the logs of a packer build or packer validate.
//
// Each tuple passed as parameter is looked for in the logs using Grep
func PluginsUsed(invert bool, plugins ...PluginVersionTuple) Checker {
return pluginsUsed{
invert: invert,
plugins: plugins,
}
}

func Dump(t *testing.T) Checker {
return &dump{t}
}

type dump struct {
t *testing.T
}

func (d Dump) Check(stdout, stderr string, err error) error {
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)
Expand Down Expand Up @@ -165,6 +223,6 @@ func (c CustomCheck) Name() string {
// returned pipe checker.
func LineCountCheck(lines int) *PipeChecker {
return MkPipeCheck(fmt.Sprintf("line count (%d)", lines), LineCount()).
SetTester(IntCompare(eq, lines)).
SetTester(IntCompare(Eq, lines)).
SetStream(OnlyStdout)
}
Loading