You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
packer/packer_test/common/check/gadgets.go

233 lines
5.5 KiB

package check
import (
"fmt"
"reflect"
"strings"
"testing"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/go-version"
)
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
}
func GrepInverted(expression string, opts ...GrepOpts) Checker {
return Grep(expression, append(opts, GrepInvert)...)
}
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 {
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)
}