mirror of https://github.com/hashicorp/packer
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.
248 lines
5.9 KiB
248 lines
5.9 KiB
package check
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// Pipe is any command that allows piping two gadgets together
|
|
//
|
|
// There's always only one input and one output (stdout), mimicking essentially
|
|
// how pipes work in the UNIX-world.
|
|
type Pipe interface {
|
|
Process(input string) (string, error)
|
|
}
|
|
|
|
// CustomPipe allows providing a simple function for piping inputs together
|
|
type CustomPipe func(string) (string, error)
|
|
|
|
func (c CustomPipe) Process(input string) (string, error) {
|
|
return c(input)
|
|
}
|
|
|
|
// PipeGrep performs a grep on an input and returns the matches, one-per-line.
|
|
//
|
|
// The expression passed as parameter will be compiled to a POSIX extended regexp.
|
|
func PipeGrep(expression string) Pipe {
|
|
re := regexp.MustCompilePOSIX(expression)
|
|
return CustomPipe(func(input string) (string, error) {
|
|
return strings.Join(re.FindAllString(input, -1), "\n"), nil
|
|
})
|
|
}
|
|
|
|
// LineCount counts the number of lines received
|
|
//
|
|
// This excludes empty lines.
|
|
func LineCount() Pipe {
|
|
return CustomPipe(func(s string) (string, error) {
|
|
lines := strings.FieldsFunc(s, func(r rune) bool {
|
|
return r == '\n'
|
|
})
|
|
return fmt.Sprintf("%d\n", len(lines)), nil
|
|
})
|
|
}
|
|
|
|
// Tee pipes the output to stdout (as t.Logf) and forwards it, unaltered
|
|
//
|
|
// This is useful typically for troubleshooting a pipe that misbehaves
|
|
func Tee(t *testing.T) Pipe {
|
|
return CustomPipe(func(s string) (string, error) {
|
|
t.Logf("%s", s)
|
|
return s, nil
|
|
})
|
|
}
|
|
|
|
// Tester is the end of a pipe for testing purposes.
|
|
//
|
|
// Once multiple commands have been piped together in a pipeline, we can
|
|
// perform some checks on that input, and decide if a test is a success or a
|
|
// failure.
|
|
type Tester interface {
|
|
Check(input string) error
|
|
}
|
|
|
|
// CustomTester allows providing a function to check that the input is what we want
|
|
type CustomTester func(string) error
|
|
|
|
func (ct CustomTester) Check(input string) error {
|
|
return ct(input)
|
|
}
|
|
|
|
// ExpectNonEmptyInput errors if the result from the pipeline was empty
|
|
//
|
|
// Non-empty in this context means that the output contains characters that are
|
|
// non-whitespace, i.e. anything that `TrimSpace` (aka unicode.IsSpace) recognizes
|
|
// as whitespace.
|
|
func ExpectNonEmptyInput() Tester {
|
|
return CustomTester(func(in string) error {
|
|
in = strings.TrimSpace(in)
|
|
if in == "" {
|
|
return fmt.Errorf("input is empty, expected it not to")
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// ExpectEmptyInput errors if the result from the pipeline was not empty
|
|
//
|
|
// Non-empty in this context means that the output contains characters that are
|
|
// non-whitespace, i.e. anything that `TrimSpace` (aka unicode.IsSpace) recognizes
|
|
// as whitespace.
|
|
func ExpectEmptyInput() Tester {
|
|
return CustomTester(func(in string) error {
|
|
in = strings.TrimSpace(in)
|
|
if in != "" {
|
|
return fmt.Errorf("input is not empty, expected it to be: %s", in)
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
type Op int
|
|
|
|
const (
|
|
Eq Op = iota
|
|
Ne
|
|
Gt
|
|
Ge
|
|
Lt
|
|
Le
|
|
)
|
|
|
|
func (op Op) String() string {
|
|
switch op {
|
|
case Eq:
|
|
return "=="
|
|
case Ne:
|
|
return "!="
|
|
case Gt:
|
|
return ">"
|
|
case Ge:
|
|
return ">="
|
|
case Lt:
|
|
return "<"
|
|
case Le:
|
|
return "<="
|
|
}
|
|
|
|
panic(fmt.Sprintf("Unknown operator %d", op))
|
|
}
|
|
|
|
// IntCompare reads the input from the pipeline and compares it to a value using `op`
|
|
//
|
|
// If the input is not an int, this fails.
|
|
func IntCompare(op Op, value int) Tester {
|
|
return CustomTester(func(in string) error {
|
|
n, err := strconv.Atoi(strings.TrimSpace(in))
|
|
if err != nil {
|
|
return fmt.Errorf("not an integer %q: %s", in, err)
|
|
}
|
|
|
|
var result bool
|
|
switch op {
|
|
case Eq:
|
|
result = n == value
|
|
case Ne:
|
|
result = n != value
|
|
case Gt:
|
|
result = n > value
|
|
case Ge:
|
|
result = n >= value
|
|
case Lt:
|
|
result = n < value
|
|
case Le:
|
|
result = n <= value
|
|
default:
|
|
panic(fmt.Sprintf("Unsupported operator %d, make sure the operation is implemented for IntCompare", op))
|
|
}
|
|
|
|
if !result {
|
|
return fmt.Errorf("comparison failed: %d %s %d -> %t", n, op, value, result)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// MkPipeCheck builds a new named PipeChecker
|
|
//
|
|
// Before it can be used, it will need a tester to be set, but this
|
|
// function is meant to make initialisation simpler.
|
|
func MkPipeCheck(name string, p ...Pipe) *PipeChecker {
|
|
return &PipeChecker{
|
|
name: name,
|
|
pipers: p,
|
|
}
|
|
}
|
|
|
|
// PipeChecker is a kind of checker that essentially lets users write mini
|
|
// gadgets that pipe inputs together, and compose those to end as a true/false
|
|
// statement, which translates to an error.
|
|
//
|
|
// Unlike pipes in a real command-line context, since we're dealing with
|
|
// finite gadgets to process data, we're sequentially running their Process
|
|
// function, and any processor that ends in an error will make the pipeline
|
|
// fail.
|
|
//
|
|
// Stream is provided so we know if we want to combine stdout/stderr for the
|
|
// pipeline, or if we want only to focus on either.
|
|
type PipeChecker struct {
|
|
name string
|
|
stream Stream
|
|
|
|
pipers []Pipe
|
|
check Tester
|
|
}
|
|
|
|
// SetTester sets the tester to use for a pipe checker
|
|
//
|
|
// This is required, otherwise running the pipe checker will fail
|
|
func (pc *PipeChecker) SetTester(t Tester) *PipeChecker {
|
|
pc.check = t
|
|
return pc
|
|
}
|
|
|
|
// SetStream changes the stream(s) on which the PipeChecker will perform
|
|
//
|
|
// Defaults to BothStreams, i.e. stdout and stderr
|
|
func (pc *PipeChecker) SetStream(s Stream) *PipeChecker {
|
|
pc.stream = s
|
|
return pc
|
|
}
|
|
|
|
func (pc PipeChecker) Check(stdout, stderr string, _ error) error {
|
|
if pc.check == nil {
|
|
return fmt.Errorf("%s - missing tester", pc.Name())
|
|
}
|
|
|
|
var pipeStr string
|
|
switch pc.stream {
|
|
case OnlyStdout:
|
|
pipeStr = stdout
|
|
case OnlyStderr:
|
|
pipeStr = stderr
|
|
case BothStreams:
|
|
pipeStr = fmt.Sprintf("%s\n%s", stdout, stderr)
|
|
}
|
|
|
|
var err error
|
|
for i, pp := range pc.pipers {
|
|
pipeStr, err = pp.Process(pipeStr)
|
|
if err != nil {
|
|
return fmt.Errorf("pipeline failed during execution (%d): %s", i, err)
|
|
}
|
|
}
|
|
return pc.check.Check(pipeStr)
|
|
}
|
|
|
|
func (pc PipeChecker) Name() string {
|
|
rawName := "|>?"
|
|
if pc.name != "" {
|
|
rawName = fmt.Sprintf("%s - %s", rawName, pc.name)
|
|
}
|
|
return rawName
|
|
}
|