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 }