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 }