gitlab.com/SkynetLabs/skyd@v1.6.9/cmd/skyc/helpers_test.go (about)

     1  package main
     2  
     3  import (
     4  	"bytes"
     5  	"io"
     6  	"os"
     7  	"regexp"
     8  	"strings"
     9  	"testing"
    10  
    11  	"github.com/spf13/cobra"
    12  	"gitlab.com/NebulousLabs/errors"
    13  	"gitlab.com/SkynetLabs/skyd/node"
    14  	"gitlab.com/SkynetLabs/skyd/node/api/client"
    15  	"gitlab.com/SkynetLabs/skyd/siatest"
    16  	"go.sia.tech/siad/persist"
    17  )
    18  
    19  // outputCatcher is a helper struct enabling to catch stdout and stderr during
    20  // tests
    21  type outputCatcher struct {
    22  	origStdout *os.File
    23  	origStderr *os.File
    24  	outW       *os.File
    25  	outC       chan string
    26  }
    27  
    28  // skycCmdSubTest is a helper struct for running skyc Cobra commands subtests
    29  // when subtests need command to run and expected output
    30  type skycCmdSubTest struct {
    31  	name               string
    32  	test               skycCmdTestFn
    33  	cmd                *cobra.Command
    34  	cmdStrs            []string
    35  	expectedOutPattern string
    36  }
    37  
    38  // skycCmdTestFn is a type of function to pass to skycCmdSubTest
    39  type skycCmdTestFn func(*testing.T, *cobra.Command, []string, string)
    40  
    41  // subTest is a helper struct for running subtests when tests can use the same
    42  // test http client
    43  type subTest struct {
    44  	name string
    45  	test func(*testing.T, client.Client)
    46  }
    47  
    48  // escapeRegexChars takes string and escapes all special regex characters
    49  func escapeRegexChars(s string) string {
    50  	res := s
    51  	chars := `\+*?^$.[]{}()|/`
    52  	for _, c := range chars {
    53  		res = strings.ReplaceAll(res, string(c), `\`+string(c))
    54  	}
    55  	return res
    56  }
    57  
    58  // executeSkycCommand is a pass-through function to execute skyc cobra command
    59  func executeSkycCommand(root *cobra.Command, args ...string) (output string, err error) {
    60  	// Recover from expected die() panic, rethrow any not expected panic
    61  	defer func() {
    62  		if rec := recover(); rec != nil {
    63  			// We are recovering from panic
    64  			if err, ok := rec.(error); !ok || err.Error() != errors.New("die panic for testing").Error() {
    65  				// This is not our expected die() panic, rethrow panic
    66  				panic(rec)
    67  			}
    68  		}
    69  	}()
    70  	_, output, err = executeSkycCommandC(root, args...)
    71  	return output, err
    72  }
    73  
    74  // executeSkycCommandC executes cobra command
    75  func executeSkycCommandC(root *cobra.Command, args ...string) (c *cobra.Command, output string, err error) {
    76  	buf := new(bytes.Buffer)
    77  	root.SetOut(buf)
    78  	root.SetErr(buf)
    79  	root.SetArgs(args)
    80  
    81  	c, err = root.ExecuteC()
    82  
    83  	return c, buf.String(), err
    84  }
    85  
    86  // getRootCmdForSkycCmdsTests creates and initializes a new instance of skyc Cobra
    87  // command
    88  func getRootCmdForSkycCmdsTests(dir string) *cobra.Command {
    89  	// create new instance of skyc cobra command
    90  	root := initCmds()
    91  
    92  	// initialize a skyc cobra command
    93  	initClient(root, &verbose, &httpClient, &dir, &alertSuppress)
    94  
    95  	return root
    96  }
    97  
    98  // newOutputCatcher starts catching stdout and stderr in tests
    99  func newOutputCatcher() (outputCatcher, error) {
   100  	// redirect stdout, stderr
   101  	origStdout := os.Stdout
   102  	origStderr := os.Stderr
   103  	r, w, err := os.Pipe()
   104  	if err != nil {
   105  		return outputCatcher{}, errors.New("Error opening pipe")
   106  	}
   107  	os.Stdout = w
   108  	os.Stderr = w
   109  
   110  	// capture redirected output
   111  	outC := make(chan string)
   112  	go func() {
   113  		var b bytes.Buffer
   114  		io.Copy(&b, r)
   115  		outC <- b.String()
   116  	}()
   117  
   118  	c := outputCatcher{
   119  		origStdout: origStdout,
   120  		origStderr: origStderr,
   121  		outW:       w,
   122  		outC:       outC,
   123  	}
   124  
   125  	return c, nil
   126  }
   127  
   128  // newTestNode creates a new Sia node for a test
   129  func newTestNode(dir string) (*siatest.TestNode, error) {
   130  	n, err := siatest.NewNode(node.AllModules(dir))
   131  	if err != nil {
   132  		return nil, errors.AddContext(err, "Error creating a new test node")
   133  	}
   134  	return n, nil
   135  }
   136  
   137  // runSkycCmdSubTests is a helper function to run skyc Cobra command subtests
   138  // when subtests need command to run and expected output
   139  func runSkycCmdSubTests(t *testing.T, tests []skycCmdSubTest) error {
   140  	// Run subtests
   141  	for _, test := range tests {
   142  		t.Run(test.name, func(t *testing.T) {
   143  			test.test(t, test.cmd, test.cmdStrs, test.expectedOutPattern)
   144  		})
   145  	}
   146  	return nil
   147  }
   148  
   149  // runSubTests is a helper function to run the subtests when tests can use the
   150  // same test http client
   151  func runSubTests(t *testing.T, directory string, tests []subTest) error {
   152  	// Create a test node/client for this test group
   153  	n, err := newTestNode(directory)
   154  	if err != nil {
   155  		t.Fatal(err)
   156  	}
   157  	defer func() {
   158  		if err := n.Close(); err != nil {
   159  			t.Fatal(err)
   160  		}
   161  	}()
   162  	// Run subtests
   163  	for _, test := range tests {
   164  		t.Run(test.name, func(t *testing.T) {
   165  			test.test(t, n.Client)
   166  		})
   167  	}
   168  	return nil
   169  }
   170  
   171  // skycTestDir creates a temporary Sia testing directory for a cmd/skyc test,
   172  // removing any files or directories that previously existed at that location.
   173  // This should only every be called once per test. Otherwise it will delete the
   174  // directory again.
   175  func skycTestDir(testName string) string {
   176  	path := siatest.TestDir("cmd/skyc", testName)
   177  	if err := os.MkdirAll(path, persist.DefaultDiskPermissionsTest); err != nil {
   178  		panic(err)
   179  	}
   180  	return path
   181  }
   182  
   183  // testGenericSkycCmd is a helper function to test skyc cobra commands
   184  // specified in cmds for expected output regex pattern
   185  func testGenericSkycCmd(t *testing.T, root *cobra.Command, cmds []string, expOutPattern string) {
   186  	// catch stdout and stderr
   187  	c, err := newOutputCatcher()
   188  	if err != nil {
   189  		t.Fatal("Error starting catching stdout/stderr", err)
   190  	}
   191  
   192  	// execute command
   193  	cobraOutput, _ := executeSkycCommand(root, cmds...)
   194  
   195  	// stop catching stdout/stderr, get catched outputs
   196  	siaOutput, err := c.stop()
   197  	if err != nil {
   198  		t.Fatal("Error stopping catching stdout/stderr", err)
   199  	}
   200  
   201  	// check output
   202  	// There are 2 types of output:
   203  	// 1) Output generated by Cobra commands (e.g. when using -h) or Cobra
   204  	//    errors (e.g. unknown cobra commands or flags).
   205  	// 2) Output generated by skyc to stdout and to stderr
   206  	var output string
   207  
   208  	if cobraOutput != "" {
   209  		output = cobraOutput
   210  	} else if siaOutput != "" {
   211  		output = siaOutput
   212  	} else {
   213  		t.Fatal("There was no output")
   214  	}
   215  
   216  	// check regex pattern by increasing rows so it is easier to spot the regex
   217  	// match issues, do not split on regex pattern rows with open regex groups
   218  	regexErr := false
   219  	regexRows := strings.Split(expOutPattern, "\n")
   220  	offsetFromLastOKRow := 0
   221  	for i := 0; i < len(regexRows); i++ {
   222  		// test only first i+1 rows from regex pattern
   223  		expSubPattern := strings.Join(regexRows[0:i+1], "\n")
   224  		// do not split on open regex group "("
   225  		openRegexGroups := strings.Count(expSubPattern, "(") - strings.Count(expSubPattern, `\(`)
   226  		closedRegexGroups := strings.Count(expSubPattern, ")") - strings.Count(expSubPattern, `\)`)
   227  		if openRegexGroups != closedRegexGroups {
   228  			offsetFromLastOKRow++
   229  			continue
   230  		}
   231  		validPattern := regexp.MustCompile(expSubPattern)
   232  		if !validPattern.MatchString(output) {
   233  			t.Logf("Regex pattern didn't match between row %v, and row %v", i+1-offsetFromLastOKRow, i+1)
   234  			t.Logf("Regex pattern part that didn't match:\n%s", strings.Join(regexRows[i-offsetFromLastOKRow:i+1], "\n"))
   235  			regexErr = true
   236  			break
   237  		}
   238  		offsetFromLastOKRow = 0
   239  	}
   240  
   241  	if regexErr {
   242  		t.Log("----- Expected output pattern: -----")
   243  		t.Log(expOutPattern)
   244  
   245  		t.Log("----- Actual Cobra output: -----")
   246  		t.Log(cobraOutput)
   247  
   248  		t.Log("----- Actual Sia output: -----")
   249  		t.Log(siaOutput)
   250  
   251  		t.Fatal()
   252  	}
   253  }
   254  
   255  // stop stops catching stdout and stderr, catched output is
   256  // returned
   257  func (c outputCatcher) stop() (string, error) {
   258  	// stop Stdout
   259  	err := c.outW.Close()
   260  	if err != nil {
   261  		return "", err
   262  	}
   263  	os.Stdout = c.origStdout
   264  	os.Stderr = c.origStderr
   265  	output := <-c.outC
   266  
   267  	return output, nil
   268  }