github.com/hashicorp/packer@v1.14.3/packer_test/common/check/pipe_checker.go (about)

     1  package check
     2  
     3  import (
     4  	"fmt"
     5  	"regexp"
     6  	"strconv"
     7  	"strings"
     8  	"testing"
     9  )
    10  
    11  // Pipe is any command that allows piping two gadgets together
    12  //
    13  // There's always only one input and one output (stdout), mimicking essentially
    14  // how pipes work in the UNIX-world.
    15  type Pipe interface {
    16  	Process(input string) (string, error)
    17  }
    18  
    19  // CustomPipe allows providing a simple function for piping inputs together
    20  type CustomPipe func(string) (string, error)
    21  
    22  func (c CustomPipe) Process(input string) (string, error) {
    23  	return c(input)
    24  }
    25  
    26  // PipeGrep performs a grep on an input and returns the matches, one-per-line.
    27  //
    28  // The expression passed as parameter will be compiled to a POSIX extended regexp.
    29  func PipeGrep(expression string) Pipe {
    30  	re := regexp.MustCompilePOSIX(expression)
    31  	return CustomPipe(func(input string) (string, error) {
    32  		return strings.Join(re.FindAllString(input, -1), "\n"), nil
    33  	})
    34  }
    35  
    36  // LineCount counts the number of lines received
    37  //
    38  // This excludes empty lines.
    39  func LineCount() Pipe {
    40  	return CustomPipe(func(s string) (string, error) {
    41  		lines := strings.FieldsFunc(s, func(r rune) bool {
    42  			return r == '\n'
    43  		})
    44  		return fmt.Sprintf("%d\n", len(lines)), nil
    45  	})
    46  }
    47  
    48  // Tee pipes the output to stdout (as t.Logf) and forwards it, unaltered
    49  //
    50  // This is useful typically for troubleshooting a pipe that misbehaves
    51  func Tee(t *testing.T) Pipe {
    52  	return CustomPipe(func(s string) (string, error) {
    53  		t.Logf("%s", s)
    54  		return s, nil
    55  	})
    56  }
    57  
    58  // Tester is the end of a pipe for testing purposes.
    59  //
    60  // Once multiple commands have been piped together in a pipeline, we can
    61  // perform some checks on that input, and decide if a test is a success or a
    62  // failure.
    63  type Tester interface {
    64  	Check(input string) error
    65  }
    66  
    67  // CustomTester allows providing a function to check that the input is what we want
    68  type CustomTester func(string) error
    69  
    70  func (ct CustomTester) Check(input string) error {
    71  	return ct(input)
    72  }
    73  
    74  // ExpectNonEmptyInput errors if the result from the pipeline was empty
    75  //
    76  // Non-empty in this context means that the output contains characters that are
    77  // non-whitespace, i.e. anything that `TrimSpace` (aka unicode.IsSpace) recognizes
    78  // as whitespace.
    79  func ExpectNonEmptyInput() Tester {
    80  	return CustomTester(func(in string) error {
    81  		in = strings.TrimSpace(in)
    82  		if in == "" {
    83  			return fmt.Errorf("input is empty, expected it not to")
    84  		}
    85  		return nil
    86  	})
    87  }
    88  
    89  // ExpectEmptyInput errors if the result from the pipeline was not empty
    90  //
    91  // Non-empty in this context means that the output contains characters that are
    92  // non-whitespace, i.e. anything that `TrimSpace` (aka unicode.IsSpace) recognizes
    93  // as whitespace.
    94  func ExpectEmptyInput() Tester {
    95  	return CustomTester(func(in string) error {
    96  		in = strings.TrimSpace(in)
    97  		if in != "" {
    98  			return fmt.Errorf("input is not empty, expected it to be: %s", in)
    99  		}
   100  		return nil
   101  	})
   102  }
   103  
   104  type Op int
   105  
   106  const (
   107  	Eq Op = iota
   108  	Ne
   109  	Gt
   110  	Ge
   111  	Lt
   112  	Le
   113  )
   114  
   115  func (op Op) String() string {
   116  	switch op {
   117  	case Eq:
   118  		return "=="
   119  	case Ne:
   120  		return "!="
   121  	case Gt:
   122  		return ">"
   123  	case Ge:
   124  		return ">="
   125  	case Lt:
   126  		return "<"
   127  	case Le:
   128  		return "<="
   129  	}
   130  
   131  	panic(fmt.Sprintf("Unknown operator %d", op))
   132  }
   133  
   134  // IntCompare reads the input from the pipeline and compares it to a value using `op`
   135  //
   136  // If the input is not an int, this fails.
   137  func IntCompare(op Op, value int) Tester {
   138  	return CustomTester(func(in string) error {
   139  		n, err := strconv.Atoi(strings.TrimSpace(in))
   140  		if err != nil {
   141  			return fmt.Errorf("not an integer %q: %s", in, err)
   142  		}
   143  
   144  		var result bool
   145  		switch op {
   146  		case Eq:
   147  			result = n == value
   148  		case Ne:
   149  			result = n != value
   150  		case Gt:
   151  			result = n > value
   152  		case Ge:
   153  			result = n >= value
   154  		case Lt:
   155  			result = n < value
   156  		case Le:
   157  			result = n <= value
   158  		default:
   159  			panic(fmt.Sprintf("Unsupported operator %d, make sure the operation is implemented for IntCompare", op))
   160  		}
   161  
   162  		if !result {
   163  			return fmt.Errorf("comparison failed: %d %s %d -> %t", n, op, value, result)
   164  		}
   165  
   166  		return nil
   167  	})
   168  }
   169  
   170  // MkPipeCheck builds a new named PipeChecker
   171  //
   172  // Before it can be used, it will need a tester to be set, but this
   173  // function is meant to make initialisation simpler.
   174  func MkPipeCheck(name string, p ...Pipe) *PipeChecker {
   175  	return &PipeChecker{
   176  		name:   name,
   177  		pipers: p,
   178  	}
   179  }
   180  
   181  // PipeChecker is a kind of checker that essentially lets users write mini
   182  // gadgets that pipe inputs together, and compose those to end as a true/false
   183  // statement, which translates to an error.
   184  //
   185  // Unlike pipes in a real command-line context, since we're dealing with
   186  // finite gadgets to process data, we're sequentially running their Process
   187  // function, and any processor that ends in an error will make the pipeline
   188  // fail.
   189  //
   190  // Stream is provided so we know if we want to combine stdout/stderr for the
   191  // pipeline, or if we want only to focus on either.
   192  type PipeChecker struct {
   193  	name   string
   194  	stream Stream
   195  
   196  	pipers []Pipe
   197  	check  Tester
   198  }
   199  
   200  // SetTester sets the tester to use for a pipe checker
   201  //
   202  // This is required, otherwise running the pipe checker will fail
   203  func (pc *PipeChecker) SetTester(t Tester) *PipeChecker {
   204  	pc.check = t
   205  	return pc
   206  }
   207  
   208  // SetStream changes the stream(s) on which the PipeChecker will perform
   209  //
   210  // Defaults to BothStreams, i.e. stdout and stderr
   211  func (pc *PipeChecker) SetStream(s Stream) *PipeChecker {
   212  	pc.stream = s
   213  	return pc
   214  }
   215  
   216  func (pc PipeChecker) Check(stdout, stderr string, _ error) error {
   217  	if pc.check == nil {
   218  		return fmt.Errorf("%s - missing tester", pc.Name())
   219  	}
   220  
   221  	var pipeStr string
   222  	switch pc.stream {
   223  	case OnlyStdout:
   224  		pipeStr = stdout
   225  	case OnlyStderr:
   226  		pipeStr = stderr
   227  	case BothStreams:
   228  		pipeStr = fmt.Sprintf("%s\n%s", stdout, stderr)
   229  	}
   230  
   231  	var err error
   232  	for i, pp := range pc.pipers {
   233  		pipeStr, err = pp.Process(pipeStr)
   234  		if err != nil {
   235  			return fmt.Errorf("pipeline failed during execution (%d): %s", i, err)
   236  		}
   237  	}
   238  	return pc.check.Check(pipeStr)
   239  }
   240  
   241  func (pc PipeChecker) Name() string {
   242  	rawName := "|>?"
   243  	if pc.name != "" {
   244  		rawName = fmt.Sprintf("%s - %s", rawName, pc.name)
   245  	}
   246  	return rawName
   247  }