github.phpd.cn/cilium/cilium@v1.6.12/test/helpers/cmd.go (about) 1 // Copyright 2017 Authors of Cilium 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package helpers 16 17 import ( 18 "bytes" 19 "encoding/json" 20 "fmt" 21 "reflect" 22 "regexp" 23 "strconv" 24 "strings" 25 "sync" 26 "time" 27 28 "github.com/cilium/cilium/test/config" 29 30 "github.com/onsi/gomega" 31 "github.com/onsi/gomega/types" 32 "k8s.io/client-go/util/jsonpath" 33 ) 34 35 // CmdRes contains a variety of data which results from running a command. 36 type CmdRes struct { 37 cmd string // Command to run 38 params []string // Parameters to provide to command 39 stdout *Buffer // Stdout from running cmd 40 stderr *Buffer // Stderr from running cmd 41 success bool // Whether command successfully executed 42 exitcode int // The exit code of cmd 43 duration time.Duration // Is the representation of the the time that command took to execute. 44 wg *sync.WaitGroup // Used to wait until the command has finished running when used in conjunction with a Context 45 err error // If the command had any error being executed, the error will be written here. 46 } 47 48 // GetCmd returns res's cmd. 49 func (res *CmdRes) GetCmd() string { 50 return res.cmd 51 } 52 53 // GetExitCode returns res's exitcode. 54 func (res *CmdRes) GetExitCode() int { 55 return res.exitcode 56 } 57 58 // GetStdOut returns the contents of the stdout buffer of res as a string. 59 func (res *CmdRes) GetStdOut() string { 60 return res.stdout.String() 61 } 62 63 // GetStdErr returns the contents of the stderr buffer of res as a string. 64 func (res *CmdRes) GetStdErr() string { 65 return res.stderr.String() 66 } 67 68 // SendToLog writes to `TestLogWriter` the debug message for the running 69 // command, if the quietMode argument is true will print only the command and 70 // the exitcode. 71 func (res *CmdRes) SendToLog(quietMode bool) { 72 if quietMode { 73 logformat := "cmd: %q exitCode: %d duration: %s\n" 74 fmt.Fprintf(&config.TestLogWriter, logformat, res.cmd, res.GetExitCode(), res.duration) 75 return 76 } 77 78 logformat := "cmd: %q exitCode: %d duration: %s stdout:\n%s\n" 79 log := fmt.Sprintf(logformat, res.cmd, res.GetExitCode(), res.duration, res.stdout.String()) 80 if res.stderr.Len() > 0 { 81 log = fmt.Sprintf("%sstderr:\n%s\n", log, res.stderr.String()) 82 } 83 fmt.Fprint(&config.TestLogWriter, log) 84 } 85 86 // WasSuccessful returns true if cmd completed successfully. 87 func (res *CmdRes) WasSuccessful() bool { 88 return res.success 89 } 90 91 // ExpectFail asserts whether res failed to execute. It accepts an optional 92 // parameter that can be used to annotate failure messages. 93 func (res *CmdRes) ExpectFail(optionalDescription ...interface{}) bool { 94 return gomega.ExpectWithOffset(1, res).ShouldNot( 95 CMDSuccess(), optionalDescription...) 96 } 97 98 // ExpectSuccess asserts whether res executed successfully. It accepts an optional 99 // parameter that can be used to annotate failure messages. 100 func (res *CmdRes) ExpectSuccess(optionalDescription ...interface{}) bool { 101 return gomega.ExpectWithOffset(1, res).Should( 102 CMDSuccess(), optionalDescription...) 103 } 104 105 // ExpectContains asserts a string into the stdout of the response of executed 106 // command. It accepts an optional parameter that can be used to annotate 107 // failure messages. 108 func (res *CmdRes) ExpectContains(data string, optionalDescription ...interface{}) bool { 109 return gomega.ExpectWithOffset(1, res.Output().String()).To( 110 gomega.ContainSubstring(data), optionalDescription...) 111 } 112 113 // ExpectDoesNotContain asserts that a string is not contained in the stdout of 114 // the executed command. It accepts an optional parameter that can be used to 115 // annotate failure messages. 116 func (res *CmdRes) ExpectDoesNotContain(data string, optionalDescription ...interface{}) bool { 117 return gomega.ExpectWithOffset(1, res.Output().String()).ToNot( 118 gomega.ContainSubstring(data), optionalDescription...) 119 } 120 121 // ExpectDoesNotMatchRegexp asserts that the stdout of the executed command 122 // doesn't match the regexp. It accepts an optional parameter that can be used 123 // to annotate failure messages. 124 func (res *CmdRes) ExpectDoesNotMatchRegexp(regexp string, optionalDescription ...interface{}) bool { 125 return gomega.ExpectWithOffset(1, res.Output().String()).ToNot( 126 gomega.MatchRegexp(regexp), optionalDescription...) 127 } 128 129 // CountLines return the number of lines in the stdout of res. 130 func (res *CmdRes) CountLines() int { 131 return strings.Count(res.stdout.String(), "\n") 132 } 133 134 // CombineOutput returns the combined output of stdout and stderr for res. 135 func (res *CmdRes) CombineOutput() *bytes.Buffer { 136 result := new(bytes.Buffer) 137 result.WriteString(res.stdout.String()) 138 result.WriteString(res.stderr.String()) 139 return result 140 } 141 142 // IntOutput returns the stdout of res as an integer 143 func (res *CmdRes) IntOutput() (int, error) { 144 return strconv.Atoi(strings.Trim(res.stdout.String(), "\n\r")) 145 } 146 147 // FindResults filters res's stdout using the provided JSONPath filter. It 148 // returns an array of the values that match the filter, and an error if 149 // the unmarshalling of the stdout of res fails. 150 // TODO - what exactly is the need for this vs. Filter function below? 151 func (res *CmdRes) FindResults(filter string) ([]reflect.Value, error) { 152 153 var data interface{} 154 var result []reflect.Value 155 156 err := json.Unmarshal(res.stdout.Bytes(), &data) 157 if err != nil { 158 return nil, err 159 } 160 parser := jsonpath.New("").AllowMissingKeys(true) 161 parser.Parse(filter) 162 fullResults, _ := parser.FindResults(data) 163 for _, res := range fullResults { 164 for _, val := range res { 165 result = append(result, val) 166 } 167 } 168 return result, nil 169 } 170 171 // Filter returns the contents of res's stdout filtered using the provided 172 // JSONPath filter in a buffer. Returns an error if the unmarshalling of the 173 // contents of res's stdout fails. 174 func (res *CmdRes) Filter(filter string) (*FilterBuffer, error) { 175 var data interface{} 176 result := new(bytes.Buffer) 177 178 err := json.Unmarshal(res.stdout.Bytes(), &data) 179 if err != nil { 180 return nil, fmt.Errorf("could not parse JSON from command %q", 181 res.cmd) 182 } 183 parser := jsonpath.New("").AllowMissingKeys(true) 184 parser.Parse(filter) 185 err = parser.Execute(result, data) 186 if err != nil { 187 return nil, err 188 } 189 return &FilterBuffer{result}, nil 190 } 191 192 // ByLines returns res's stdout split by the newline character and, if the stdout 193 // contains `\r\n`, it will be split by carriage return and new line characters. 194 func (res *CmdRes) ByLines() []string { 195 stdoutStr := res.stdout.String() 196 sep := "\n" 197 if strings.Contains(stdoutStr, "\r\n") { 198 sep = "\r\n" 199 } 200 stdoutStr = strings.TrimRight(stdoutStr, sep) 201 return strings.Split(stdoutStr, sep) 202 } 203 204 // KVOutput returns a map of the stdout of res split based on 205 // the separator '='. 206 // For example, the following strings would be split as follows: 207 // a=1 208 // b=2 209 // c=3 210 func (res *CmdRes) KVOutput() map[string]string { 211 result := make(map[string]string) 212 for _, line := range res.ByLines() { 213 vals := strings.Split(line, "=") 214 if len(vals) == 2 { 215 result[vals[0]] = vals[1] 216 } 217 } 218 return result 219 } 220 221 // Output returns res's stdout. 222 func (res *CmdRes) Output() *Buffer { 223 return res.stdout 224 } 225 226 // OutputPrettyPrint returns a string with the ExitCode, stdout and stderr in a 227 // pretty format. 228 func (res *CmdRes) OutputPrettyPrint() string { 229 format := func(message string) string { 230 result := []string{} 231 for _, line := range strings.Split(message, "\n") { 232 result = append(result, fmt.Sprintf("\t %s", line)) 233 } 234 return strings.Join(result, "\n") 235 236 } 237 return fmt.Sprintf( 238 "Exitcode: %d \nStdout:\n %s\nStderr:\n %s\n", 239 res.GetExitCode(), 240 format(res.GetStdOut()), 241 format(res.GetStdErr())) 242 } 243 244 // ExpectEqual asserts whether cmdRes.Output().String() and expected are equal. 245 // It accepts an optional parameter that can be used to annotate failure 246 // messages. 247 func (res *CmdRes) ExpectEqual(expected string, optionalDescription ...interface{}) bool { 248 return gomega.ExpectWithOffset(1, res.Output().String()).Should( 249 gomega.Equal(expected), optionalDescription...) 250 } 251 252 // Reset resets res's stdout buffer to be empty. 253 func (res *CmdRes) Reset() { 254 res.stdout.Reset() 255 return 256 } 257 258 // SingleOut returns res's stdout as a string without any newline characters 259 func (res *CmdRes) SingleOut() string { 260 strstdout := res.stdout.String() 261 strstdoutSingle := strings.Replace(strstdout, "\n", "", -1) 262 return strings.Replace(strstdoutSingle, "\r", "", -1) 263 } 264 265 // Unmarshal unmarshalls res's stdout into data. It assumes that the stdout of 266 // res is in JSON format. Returns an error if the unmarshalling fails. 267 func (res *CmdRes) Unmarshal(data interface{}) error { 268 err := json.Unmarshal(res.stdout.Bytes(), data) 269 return err 270 } 271 272 // GetDebugMessage returns executed command and its output 273 func (res *CmdRes) GetDebugMessage() string { 274 return fmt.Sprintf("cmd: %s\n%s", res.GetCmd(), res.OutputPrettyPrint()) 275 } 276 277 // WaitUntilMatch waits until the given substring is present in the `CmdRes.stdout` 278 // If the timeout is reached it will return an error. 279 func (res *CmdRes) WaitUntilMatch(substr string) error { 280 body := func() bool { 281 return strings.Contains(res.Output().String(), substr) 282 } 283 284 return WithTimeout( 285 body, 286 fmt.Sprintf("%s is not in the output after timeout", substr), 287 &TimeoutConfig{Timeout: HelperTimeout}) 288 } 289 290 // WaitUntilMatchRegexp waits until the `CmdRes.stdout` matches the given regexp. 291 // If the timeout is reached it will return an error. 292 func (res *CmdRes) WaitUntilMatchRegexp(expr string) error { 293 r := regexp.MustCompile(expr) 294 body := func() bool { 295 return r.Match(res.Output().Bytes()) 296 } 297 298 return WithTimeout( 299 body, 300 fmt.Sprintf("The output doesn't match regexp %q after timeout", expr), 301 &TimeoutConfig{Timeout: HelperTimeout}) 302 } 303 304 // WaitUntilFinish waits until the command context completes correctly 305 func (res *CmdRes) WaitUntilFinish() { 306 if res.wg == nil { 307 return 308 } 309 res.wg.Wait() 310 } 311 312 // GetErr returns error created from program output if command is not successful 313 func (res *CmdRes) GetErr(context string) error { 314 if res.WasSuccessful() { 315 return nil 316 } 317 return &cmdError{fmt.Sprintf("%s (%s) output: %s", context, res.err, res.GetDebugMessage())} 318 } 319 320 // GetError returns the error for this CmdRes. 321 func (res *CmdRes) GetError() error { 322 return res.err 323 } 324 325 // BeSuccesfulMatcher a new Ginkgo matcher for CmdRes struct 326 type BeSuccesfulMatcher struct{} 327 328 // Match validates that the given interface will be a `*CmdRes` struct and it 329 // was successful. In case of not a valid CmdRes will return an error. If the 330 // command was not successful it returns false. 331 func (matcher *BeSuccesfulMatcher) Match(actual interface{}) (success bool, err error) { 332 res, ok := actual.(*CmdRes) 333 if !ok { 334 return false, fmt.Errorf("%q is not a valid *CmdRes type", actual) 335 } 336 return res.WasSuccessful(), nil 337 } 338 339 // FailureMessage it returns a pretty printed error message in the case of the 340 // command was not successful. 341 func (matcher *BeSuccesfulMatcher) FailureMessage(actual interface{}) (message string) { 342 res, _ := actual.(*CmdRes) 343 return fmt.Sprintf("Expected command: %s \nTo succeed, but it failed:\n%s", 344 res.GetCmd(), res.OutputPrettyPrint()) 345 } 346 347 // NegatedFailureMessage returns a pretty printed error message in case of the 348 // command is tested with a negative 349 func (matcher *BeSuccesfulMatcher) NegatedFailureMessage(actual interface{}) (message string) { 350 res, _ := actual.(*CmdRes) 351 return fmt.Sprintf("Expected command: %s\nTo have failed, but it was successful:\n%s", 352 res.GetCmd(), res.OutputPrettyPrint()) 353 } 354 355 // CMDSuccess return a new Matcher that expects a CmdRes is a successful run command. 356 func CMDSuccess() types.GomegaMatcher { 357 return &BeSuccesfulMatcher{} 358 } 359 360 // cmdError is a implementation of error with String method to improve the debugging. 361 type cmdError struct { 362 s string 363 } 364 365 func (e *cmdError) Error() string { 366 return e.s 367 } 368 369 func (e *cmdError) String() string { 370 return e.s 371 }