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  }