github.com/swaros/contxt/module/runner@v0.0.0-20240305083542-3dbd4436ac40/testrunner_test.go (about) 1 package runner_test 2 3 import ( 4 "bytes" 5 "fmt" 6 "io" 7 "os" 8 "path/filepath" 9 "runtime" 10 "strings" 11 "testing" 12 "time" 13 14 "github.com/swaros/contxt/module/configure" 15 "github.com/swaros/contxt/module/ctxout" 16 "github.com/swaros/contxt/module/dirhandle" 17 "github.com/swaros/contxt/module/runner" 18 "github.com/swaros/contxt/module/systools" 19 "github.com/swaros/contxt/module/tasks" 20 "github.com/swaros/contxt/module/yacl" 21 "github.com/swaros/contxt/module/yamc" 22 ) 23 24 var useLastDir = "./" 25 var lastExistCode = 0 26 var testDirectory = "" 27 28 type ExpectDef struct { 29 ExpectedInOutput []string "yaml:\"output\"" // what should be in the output (contains! not full match) 30 ExpectedInError []string "yaml:\"error\"" // what should be in the error (contains! not full match) 31 NotExpected []string "yaml:\"not\"" // what should not be in the output (contains! not full match) 32 } 33 34 type TestRunExpectation struct { 35 TestName string "yaml:\"testName\"" // the nameof the test 36 RunCmd string "yaml:\"runCmd\"" // the run command to execute 37 RunInteractive string "yaml:\"runInteractive\"" // the run command to execute in the interactive mode 38 Folder string "yaml:\"folder\"" // the folder where the test is located. empty to use the current directory 39 Systems []string "yaml:\"systems\"" // what system is ment like linux, windows, darwin etc. 40 Expectations ExpectDef "yaml:\"expect\"" 41 Disabled bool "yaml:\"disabled\"" // if the test is disabled 42 } 43 44 func TestAllIssues(t *testing.T) { 45 // walk on every file in the directory starting from ./testdata/issues 46 // and run the test if the file is prefixed with 'issue_' 47 48 // change to the testdata directory 49 path := ChangeToRuntimeDir(t) 50 51 // walk on every file in the directory 52 // look for files prefixed with 'issue_' 53 // and run the test 54 err := filepath.Walk(path, func(path string, info os.FileInfo, err error) error { 55 if err != nil { 56 return err 57 } 58 // check if we have an file and if the file is prefixed with 'issue_' and has the suffix '.yml' 59 // but use the filename for test prefix 60 61 baseName := filepath.Base(path) 62 if !info.IsDir() && strings.HasPrefix(baseName, "issue_") && strings.HasSuffix(path, ".yml") { 63 64 // load the test definition 65 testDef := TestRunExpectation{} 66 loader := yacl.New(&testDef, yamc.NewYamlReader()).SetFileAndPathsByFullFilePath(path) 67 if err := loader.Load(); err != nil { 68 t.Error(err) 69 } 70 if loader.GetLoadedFile() == "" { 71 t.Error("could not load the test definition file: " + path) 72 } else { 73 if testDef.Disabled { 74 t.Log("test " + testDef.TestName + " is disabled") 75 return nil 76 } 77 78 if testDef.Systems != nil && len(testDef.Systems) > 0 { 79 // check if the current system is in the list of systems 80 // if not, we skip the test 81 if !systools.StringInSlice(runtime.GOOS, testDef.Systems) { 82 return nil 83 } 84 } 85 86 testDef.Folder, _ = filepath.Abs(filepath.Dir(path)) 87 IssueTester(t, testDef) 88 } 89 } 90 return nil 91 92 }) 93 if err != nil { 94 panic(err) 95 } 96 } 97 98 // IssueTester is the main test function for the issues based of the test definition 99 // the test definition is loaded from a yaml file. 100 func IssueTester(t *testing.T, testDef TestRunExpectation) error { 101 t.Helper() 102 tasks.NewGlobalWatchman().ResetAllTaskInfos() 103 ChangeToRuntimeDir(t) 104 app, output, appErr := SetupTestApp("issues", "ctx_test_config.yml") 105 if appErr != nil { 106 t.Errorf("Expected no error, got '%v'", appErr) 107 return appErr 108 } 109 cleanAllFiles() 110 defer cleanAllFiles() 111 112 // set the log file with an timestamp 113 logFileName := testDef.TestName + "_" + time.Now().Format(time.RFC3339) + ".log" 114 output.SetLogFile(getAbsolutePath(logFileName)) 115 output.ClearAndLog() 116 currentDir := dirhandle.Pushd() 117 defer currentDir.Popd() 118 119 // change into the test directory 120 if testDef.Folder != "" { 121 if err := os.Chdir(testDef.Folder); err != nil { 122 t.Errorf("error by changing the directory. check test %s: '%v'", testDef.TestName, err) 123 return err 124 } 125 } 126 127 assertSomething := 0 128 runSomething := false 129 130 // command to run a cobracmd. 131 if testDef.RunCmd != "" { 132 runSomething = true 133 if err := runCobraCmd(app, testDef.RunCmd); err != nil { 134 if len(testDef.Expectations.ExpectedInError) > 0 { 135 for _, expected := range testDef.Expectations.ExpectedInError { 136 assertInMessage(t, output, expected) 137 assertSomething++ 138 } 139 } else { 140 t.Errorf("Expected no error, got '%v'", err) 141 return err 142 } 143 } 144 } 145 146 if testDef.RunInteractive != "" { 147 runSomething = true 148 if err := runCobraCmdNonSplits(app, testDef.RunInteractive); err != nil { 149 if len(testDef.Expectations.ExpectedInError) > 0 { 150 for _, expected := range testDef.Expectations.ExpectedInError { 151 assertInMessage(t, output, expected) 152 assertSomething++ 153 } 154 } else { 155 t.Errorf("Expected no error, got '%v'", err) 156 return err 157 } 158 } 159 } 160 161 if !runSomething { 162 t.Error("no run command was set. please check the test definition for test " + testDef.TestName) 163 return fmt.Errorf("no run command was set. please check the test definition for test " + testDef.TestName) 164 } 165 t.Log("test " + testDef.TestName + " was executed in: " + testDef.Folder) 166 if len(testDef.Expectations.ExpectedInOutput) > 0 { 167 for _, expected := range testDef.Expectations.ExpectedInOutput { 168 assertInMessage(t, output, expected) 169 assertSomething++ 170 } 171 } 172 173 // looking for the not expected 174 if len(testDef.Expectations.NotExpected) > 0 { 175 for _, expected := range testDef.Expectations.NotExpected { 176 assertNotInMessage(t, output, expected) 177 assertSomething++ 178 } 179 } 180 output.ClearAndLog() 181 if assertSomething == 0 { 182 t.Error("no expectations was set and tested. please check the test definition") 183 return fmt.Errorf("no expectations was set and tested. please check the test definition") 184 } 185 return nil 186 } 187 188 func RuntimeFileInfo(t *testing.T) string { 189 _, filename, _, _ := runtime.Caller(0) 190 return filename 191 } 192 193 func ChangeToRuntimeDir(t *testing.T) string { 194 _, filename, _, _ := runtime.Caller(0) 195 dir := filepath.Dir(filename) 196 if err := os.Chdir(dir); err != nil { 197 t.Error(err) 198 } 199 return dir 200 } 201 202 // shortcut for running a cobra command 203 // without any other setup 204 func runCobraCommand(runnCallback func(cobra *runner.SessionCobra, writer io.Writer)) string { 205 cobra := runner.NewCobraCmds() 206 cmpltn := new(bytes.Buffer) 207 if runnCallback != nil { 208 runnCallback(cobra, cmpltn) 209 } 210 return cmpltn.String() 211 } 212 213 // this are some helper functions especially for testing the runner 214 // Setup the test app 215 // create the application. set up the config folder name, and the name of the config file. 216 // the testapp bevavior is afterwards different, because it uses the config 217 // related to the current directory. 218 // 219 // if the file should remover automatically, it needs prefixed by 'ctx_'. 220 // 221 // thats why we have some special helper functions. 222 // - getAbsolutePath to get the absolute path to the testdata directory 223 // - backToWorkDir to go back to the testdata directory 224 // - cleanAllFiles to remove the config file 225 func SetupTestApp(dir, file string) (*runner.CmdSession, *TestOutHandler, error) { 226 tasks.NewGlobalWatchman().ResetAllTaskInfos() 227 file = strings.ReplaceAll(file, ":", "_") 228 file = strings.ReplaceAll(file, "-", "_") 229 file = strings.ReplaceAll(file, "+", "_") 230 231 // first we want to catch the exist codes 232 systools.AddExitListener("testing_prevent_exit", func(no int) systools.ExitBehavior { 233 lastExistCode = no 234 return systools.Interrupt 235 }) 236 237 configure.USE_SPECIAL_DIR = false // no special directory like userHome etc. 238 configure.CONTXT_FILE = file // set the configuration file name 239 configure.MIGRATION_ENABLED = false // disable the migration 240 configure.CONTEXT_DIR = dir // set the directory name 241 242 // save the current directory 243 // and also get back to them (next time) 244 popdTestDir() 245 // we need to stick to the testdata directory 246 // any other directory will not work 247 if err := os.Chdir("./testdata"); err != nil { 248 249 panic(err) 250 } 251 // check if the directory exists, that we want to use in the testdata directory. 252 // even if the config package is able to create them, we want avoid this here. 253 if _, err := os.Stat(dir); os.IsNotExist(err) { 254 panic(err.Error() + "| the directory " + dir + " does not exist in the testdata directory") 255 } 256 257 // build the absolute path to the testdata directory 258 // this is needed to go back to the testdata directory 259 // if needed 260 if pwd, derr := os.Getwd(); derr == nil { 261 useLastDir = pwd 262 configure.CONFIG_PATH_CALLBACK = func() string { 263 return useLastDir + "/" + configure.CONTEXT_DIR + "/" + configure.CONTXT_FILE 264 } 265 } else { 266 panic(derr) 267 } 268 269 app := runner.NewCmdSession() 270 271 // set the TemplateHndl OnLoad function to parse required files 272 // like it is done in the real application 273 onLoadFn := func(template *configure.RunConfig) error { 274 return app.SharedHelper.MergeRequiredPaths(template, app.TemplateHndl) 275 } 276 app.TemplateHndl.SetOnLoad(onLoadFn) 277 278 functions := runner.NewCmd(app) 279 // init the main functions 280 functions.MainInit() 281 282 // signs filter 283 signsFilter := ctxout.NewSignFilter(ctxout.NewBaseSignSet()) 284 ctxout.AddPostFilter(signsFilter) 285 // tabout filter 286 tabouOutFilter := ctxout.NewTabOut() 287 ctxout.AddPostFilter(tabouOutFilter) 288 info := ctxout.PostFilterInfo{ 289 Width: 800, // give us a big width so we can render the whole line 290 IsTerminal: false, //no terminal 291 Colored: false, // no colors 292 Height: 500, // give us a big height so we can render the whole line 293 Disabled: true, 294 } 295 tabouOutFilter.Update(info) 296 signsFilter.Update(info) 297 signsFilter.ForceEmpty(true) 298 299 if err := app.Cobra.Init(functions); err != nil { 300 panic(err) 301 } 302 ctxout.ForceFilterUpdate(info) 303 304 outputHdnl := NewTestOutHandler() 305 app.OutPutHdnl = outputHdnl 306 configure.GetGlobalConfig().ResetConfig() 307 return app, outputHdnl, nil 308 } 309 310 // helper function to verify the configuration file. 311 // if the testCallBack is not nil, we will call it with the configuration model 312 // so we can check the content of the configuration file. 313 // this is helpfull just because to double check the content of the file itself and 314 // the current state of the configuration. the configuration can be different from the file. 315 // just because the configuration is in memory and the file is on the disk. 316 // this functions is all about checking if the configuration is updated correctly, also in the file content. 317 func verifyConfigurationFile(t *testing.T, testCallBack func(CFG *configure.ConfigMetaV2)) { 318 t.Helper() 319 file := "" 320 if configure.CONFIG_PATH_CALLBACK != nil { 321 file = configure.CONFIG_PATH_CALLBACK() 322 } 323 324 if file == "" { 325 t.Error("configuration setup failed. could not determine the configuration file.") 326 return 327 } 328 329 if _, err := os.Stat(file); os.IsNotExist(err) { 330 t.Error("configuration file not found: ", file) 331 return 332 } 333 // if the testCallBack is nil, we dont need to check the content 334 if testCallBack == nil { 335 return 336 } 337 // model 338 var CFG configure.ConfigMetaV2 = configure.ConfigMetaV2{} 339 // load the configuration file 340 loader := yacl.New(&CFG, yamc.NewYamlReader()).SetFileAndPathsByFullFilePath(file) 341 if err := loader.Load(); err != nil { 342 t.Error(err) 343 } 344 testCallBack(&CFG) 345 346 } 347 348 // save and go back to the test folder 349 func popdTestDir() { 350 // if not set, we get the current directory 351 // and set them once. 352 // so the carefully use this function in the first place 353 if testDirectory == "" { 354 if pwd, derr := os.Getwd(); derr == nil { 355 testDirectory = pwd 356 } else { 357 panic(derr) 358 } 359 } 360 361 if err := os.Chdir(testDirectory); err != nil { 362 panic(err) 363 } 364 } 365 366 // helper function to change back to the testdata directory 367 func backToWorkDir() { 368 if err := os.Chdir(useLastDir); err != nil { 369 panic(err) 370 } 371 } 372 373 // helper function to get the absolute path to the testdata directory 374 func getAbsolutePath(dir string) string { 375 376 dir = useLastDir + "/" + dir 377 dir = filepath.Clean(dir) 378 if filepath.IsAbs(dir) { 379 return dir 380 } 381 abs, err := filepath.Abs(dir) 382 if err != nil { 383 panic(err) 384 } 385 return abs 386 } 387 388 // helper function to remove the config files 389 // from testdata/config folder 390 func cleanAllFiles() { 391 popdTestDir() 392 if err := os.Chdir("./testdata/config"); err != nil { 393 panic(err) 394 } 395 // walk on every file in the directory 396 // and remove it 397 err := filepath.Walk(".", func(path string, info os.FileInfo, err error) error { 398 if err != nil { 399 return err 400 } 401 if !info.IsDir() && strings.HasPrefix(path, "ctx_") && strings.HasSuffix(path, ".yml") { 402 return os.Remove(path) 403 } 404 return nil 405 406 }) 407 if err != nil { 408 panic(err) 409 } 410 popdTestDir() 411 } 412 413 // helper function to run a cobra command by argument line 414 func runCobraCmd(app *runner.CmdSession, cmd string) error { 415 app.Cobra.RootCmd.SetArgs(strings.Split(cmd, " ")) 416 return app.Cobra.RootCmd.Execute() 417 } 418 419 // helper function to run a cobra command by argument line 420 func runCobraCmdNonSplits(app *runner.CmdSession, cmd string) error { 421 app.Cobra.RootCmd.SetArgs([]string{cmd}) 422 return app.Cobra.RootCmd.Execute() 423 } 424 425 // checks if the given string is part of the output buffer 426 // if not, it will fail the test. 427 // the special thing about this function is, that it will split the string 428 // by new line and check if every line is part of the output buffer. 429 // example: 430 // 431 // output.ClearAndLog() 432 // if err := runCobraCmd(app, "workspace new ducktale"); err != nil { 433 // t.Errorf("Expected no error, got '%v'", err) 434 // } 435 // assertInMessage(t, output, "ducktale\ncreated\nproject") 436 func assertSplitTestInMessage(t *testing.T, output *TestOutHandler, msg string) { 437 t.Helper() 438 parts := strings.Split(msg, "\n") 439 errorHappen := false 440 for _, part := range parts { 441 if part == "" { 442 continue 443 } 444 if !output.Contains(part) { 445 errorHappen = true 446 t.Errorf("Expected [%s]not found in the output", part) 447 } 448 } 449 if errorHappen { 450 t.Error("this is the source output\n", output.String()) 451 } 452 } 453 454 // assert a string is part of the output buffer 455 func assertInMessage(t *testing.T, output *TestOutHandler, msg string) { 456 t.Helper() 457 if !output.Contains(msg) { 458 t.Errorf("Expected \n%s\n-- but instead we did not found it in --\n%v\n", msg, output.String()) 459 } 460 } 461 462 // assert a string is part of the output buffer as regex 463 func assertRegexmatchInMessage(t *testing.T, output *TestOutHandler, msg string) { 464 t.Helper() 465 if !output.TestRegexPattern(msg) { 466 t.Errorf("Expected \n%s\nbut instead we got\n%v", msg, output.String()) 467 } 468 } 469 470 // assert a string is not part of the output buffer 471 func assertNotInMessage(t *testing.T, output *TestOutHandler, msg string) { 472 t.Helper() 473 if output.Contains(msg) { 474 t.Errorf("Expected '%s' is not in the message, but got '%v'", msg, output.String()) 475 } 476 } 477 478 // assert a cobra command is returning an error 479 func assertCobraError(t *testing.T, app *runner.CmdSession, cmd string, expectedMessageContains string) { 480 t.Helper() 481 if err := runCobraCmd(app, cmd); err == nil { 482 t.Errorf("Expected error, but got none") 483 } else { 484 if !strings.Contains(err.Error(), expectedMessageContains) { 485 t.Errorf("Expected error message to contain '%s', but got '%s'", systools.PadString(expectedMessageContains, 80), err.Error()) 486 } 487 } 488 } 489 490 // checking if we are in the expected path in the operting system 491 func assertInOsPath(t *testing.T, path string) { 492 t.Helper() 493 if currentDir, err := os.Getwd(); err != nil { 494 t.Errorf("Expected no error, got '%v'", err) 495 } else { 496 if currentDir != path { 497 t.Errorf("Expected to be in '%v', got '%v'", path, currentDir) 498 } 499 } 500 } 501 502 var ( 503 AcceptFullMatch = 1 504 AcceptIgnoreLn = 2 505 AcceptContains = 3 506 AcceptContainsNoSpecials = 4 507 ) 508 509 func assertStringFind(search, searchIn string, acceptableLevel int) bool { 510 if search == "" || searchIn == "" { 511 return true 512 } 513 if acceptableLevel >= AcceptFullMatch && searchIn == search { 514 return true 515 } 516 if acceptableLevel >= AcceptIgnoreLn && searchIn == search+"\n" { 517 return true 518 } 519 if acceptableLevel >= AcceptContains && strings.Contains(searchIn, search) { 520 return true 521 } 522 523 if acceptableLevel >= AcceptContainsNoSpecials { 524 search = strings.Replace(search, " ", "", -1) 525 searchIn = strings.Replace(searchIn, " ", "", -1) 526 search = strings.Replace(search, "\n", "", -1) 527 searchIn = strings.Replace(searchIn, "\n", "", -1) 528 search = strings.Replace(search, "\t", "", -1) 529 searchIn = strings.Replace(searchIn, "\t", "", -1) 530 if strings.Contains(searchIn, search) { 531 return true 532 } 533 } 534 535 return false 536 } 537 538 func assertStringFindInArray(search string, searchIn []string, acceptableLevel int) int { 539 for index, s := range searchIn { 540 if assertStringFind(search, s, acceptableLevel) { 541 return index 542 } 543 } 544 return -1 545 } 546 547 func assertFileExists(t *testing.T, file string) { 548 t.Helper() 549 file, err := filepath.Abs(file) 550 if err != nil { 551 t.Errorf("Error while trying to get the absolute path, got '%v'", err) 552 } 553 if _, err := os.Stat(file); os.IsNotExist(err) { 554 t.Errorf("Expected file '%s' exists, but got '%v'", file, err) 555 } 556 } 557 558 func assertFileContent(t *testing.T, file string, expectedContent string, acceptableLevel int) { 559 t.Helper() 560 if content, err := os.ReadFile(file); err != nil { 561 t.Errorf("Expected no error, got '%v'", err) 562 } else { 563 fileSlice := strings.Split(string(content), "\n") 564 expectedSlice := strings.Split(expectedContent, "\n") 565 // we want to check anything from the expectations is in the file 566 // but we need to make sure if we also have this in order 567 lastHit := -1 568 for _, expected := range expectedSlice { 569 hitAtIndex := assertStringFindInArray(expected, fileSlice, acceptableLevel) 570 if hitAtIndex == -1 { 571 t.Errorf("Expected file '%s' should contains '%s' what seems not be the case", file, expected) 572 } 573 if hitAtIndex < lastHit { 574 t.Errorf("Expected file '%s' contains '%s' but not in the right order", file, expected) 575 } 576 // remove the hit from the file slice, so we can check if we have duplicates. 577 // this is also nessary to check if we have the same line multiple times and do 578 // not fail because we found it on the wrong index 579 if hitAtIndex != -1 { 580 systools.RemoveFromSliceOnce(fileSlice, fileSlice[hitAtIndex]) 581 } 582 lastHit = hitAtIndex 583 } 584 } 585 } 586 587 type find_flags int 588 589 const ( 590 FindFlagsNone find_flags = iota 591 IgnoreTabs // ignore all tabs in the content and the message 592 IgnoreSpaces // ignore all spaces in the content and the message 593 IgnoreNewLines // ignore all new lines in the content and the message 594 IgnoreMultiSpaces // ignore all repeated spaces and tabs in the content and the message 595 IgnoreCaseSensitive // ignore case sensitive 596 ) 597 598 // assert a string is part of the output buffer where we can ignore some flags 599 // like tabs, spaces, new lines, case sensitive 600 func assertInContent(t *testing.T, content string, msg string, flags ...find_flags) { 601 t.Helper() 602 603 if len(flags) > 0 { 604 for _, flag := range flags { 605 switch flag { 606 case IgnoreTabs: 607 content = strings.ReplaceAll(content, "\t", "") 608 msg = strings.ReplaceAll(msg, "\t", "") 609 case IgnoreSpaces: 610 content = strings.ReplaceAll(content, " ", "") 611 msg = strings.ReplaceAll(msg, " ", "") 612 case IgnoreNewLines: 613 content = strings.ReplaceAll(content, "\n", "") 614 msg = strings.ReplaceAll(msg, "\n", "") 615 case IgnoreMultiSpaces: 616 content = systools.TrimAllSpaces(content) 617 msg = systools.TrimAllSpaces(msg) 618 case IgnoreCaseSensitive: 619 content = strings.ToLower(content) 620 msg = strings.ToLower(msg) 621 } 622 } 623 } 624 if !strings.Contains(content, msg) { 625 t.Errorf("Expected to find '%s' in string. but did not found", msg) 626 } 627 } 628 629 func assertStringSliceInContent(t *testing.T, content string, msg []string, flags ...find_flags) { 630 t.Helper() 631 for _, line := range msg { 632 assertInContent(t, content, line, flags...) 633 } 634 }