Files
Packer-Cn/packer_test/lib/pipe_checker.go
Lucas Bajolet 47a4ad201b packer_test: make packer test suite modular
Having only one test suite for the whole of Packer makes it harder to
segregate between test types, and makes for a longer runtime as no tests
run in parallel by default.

This commit splits the packer_test suite into several components in
order to make extension easier.

First we have `lib`: this package embeds the core for running Packer
test suites. This ships facilities to build your own test suite for
Packer core, and exposes convenience methods and structures for building
plugins, packer core, and use it to run a test suite in a temporary
directory.

Then we have two separate test suites: one for plugins, and one for core
itself, the latter of which does not depend on plugins being compiled at
all.

This sets the stage for more specialised test suites in the future, each
of which can run in parallel on different parts of the code.
2024-08-06 11:09:31 -04:00

248 lines
5.9 KiB
Go

package lib
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)
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
}