github.com/crowdsecurity/crowdsec@v1.6.1/cmd/crowdsec-cli/hubtest.go (about) 1 package main 2 3 import ( 4 "encoding/json" 5 "errors" 6 "fmt" 7 "math" 8 "os" 9 "path/filepath" 10 "strings" 11 "text/template" 12 13 "github.com/AlecAivazis/survey/v2" 14 "github.com/fatih/color" 15 log "github.com/sirupsen/logrus" 16 "github.com/spf13/cobra" 17 "gopkg.in/yaml.v2" 18 19 "github.com/crowdsecurity/crowdsec/pkg/dumps" 20 "github.com/crowdsecurity/crowdsec/pkg/emoji" 21 "github.com/crowdsecurity/crowdsec/pkg/hubtest" 22 ) 23 24 var ( 25 HubTest hubtest.HubTest 26 HubAppsecTests hubtest.HubTest 27 hubPtr *hubtest.HubTest 28 isAppsecTest bool 29 ) 30 31 type cliHubTest struct { 32 cfg configGetter 33 } 34 35 func NewCLIHubTest(cfg configGetter) *cliHubTest { 36 return &cliHubTest{ 37 cfg: cfg, 38 } 39 } 40 41 func (cli *cliHubTest) NewCommand() *cobra.Command { 42 var ( 43 hubPath string 44 crowdsecPath string 45 cscliPath string 46 ) 47 48 cmd := &cobra.Command{ 49 Use: "hubtest", 50 Short: "Run functional tests on hub configurations", 51 Long: "Run functional tests on hub configurations (parsers, scenarios, collections...)", 52 Args: cobra.ExactArgs(0), 53 DisableAutoGenTag: true, 54 PersistentPreRunE: func(_ *cobra.Command, _ []string) error { 55 var err error 56 HubTest, err = hubtest.NewHubTest(hubPath, crowdsecPath, cscliPath, false) 57 if err != nil { 58 return fmt.Errorf("unable to load hubtest: %+v", err) 59 } 60 61 HubAppsecTests, err = hubtest.NewHubTest(hubPath, crowdsecPath, cscliPath, true) 62 if err != nil { 63 return fmt.Errorf("unable to load appsec specific hubtest: %+v", err) 64 } 65 66 // commands will use the hubPtr, will point to the default hubTest object, or the one dedicated to appsec tests 67 hubPtr = &HubTest 68 if isAppsecTest { 69 hubPtr = &HubAppsecTests 70 } 71 72 return nil 73 }, 74 } 75 76 cmd.PersistentFlags().StringVar(&hubPath, "hub", ".", "Path to hub folder") 77 cmd.PersistentFlags().StringVar(&crowdsecPath, "crowdsec", "crowdsec", "Path to crowdsec") 78 cmd.PersistentFlags().StringVar(&cscliPath, "cscli", "cscli", "Path to cscli") 79 cmd.PersistentFlags().BoolVar(&isAppsecTest, "appsec", false, "Command relates to appsec tests") 80 81 cmd.AddCommand(cli.NewCreateCmd()) 82 cmd.AddCommand(cli.NewRunCmd()) 83 cmd.AddCommand(cli.NewCleanCmd()) 84 cmd.AddCommand(cli.NewInfoCmd()) 85 cmd.AddCommand(cli.NewListCmd()) 86 cmd.AddCommand(cli.NewCoverageCmd()) 87 cmd.AddCommand(cli.NewEvalCmd()) 88 cmd.AddCommand(cli.NewExplainCmd()) 89 90 return cmd 91 } 92 93 func (cli *cliHubTest) NewCreateCmd() *cobra.Command { 94 var ( 95 ignoreParsers bool 96 labels map[string]string 97 logType string 98 ) 99 100 parsers := []string{} 101 postoverflows := []string{} 102 scenarios := []string{} 103 104 cmd := &cobra.Command{ 105 Use: "create", 106 Short: "create [test_name]", 107 Example: `cscli hubtest create my-awesome-test --type syslog 108 cscli hubtest create my-nginx-custom-test --type nginx 109 cscli hubtest create my-scenario-test --parsers crowdsecurity/nginx --scenarios crowdsecurity/http-probing`, 110 Args: cobra.ExactArgs(1), 111 DisableAutoGenTag: true, 112 RunE: func(_ *cobra.Command, args []string) error { 113 testName := args[0] 114 testPath := filepath.Join(hubPtr.HubTestPath, testName) 115 if _, err := os.Stat(testPath); os.IsExist(err) { 116 return fmt.Errorf("test '%s' already exists in '%s', exiting", testName, testPath) 117 } 118 119 if isAppsecTest { 120 logType = "appsec" 121 } 122 123 if logType == "" { 124 return errors.New("please provide a type (--type) for the test") 125 } 126 127 if err := os.MkdirAll(testPath, os.ModePerm); err != nil { 128 return fmt.Errorf("unable to create folder '%s': %+v", testPath, err) 129 } 130 131 configFilePath := filepath.Join(testPath, "config.yaml") 132 133 configFileData := &hubtest.HubTestItemConfig{} 134 if logType == "appsec" { 135 // create empty nuclei template file 136 nucleiFileName := fmt.Sprintf("%s.yaml", testName) 137 nucleiFilePath := filepath.Join(testPath, nucleiFileName) 138 nucleiFile, err := os.OpenFile(nucleiFilePath, os.O_RDWR|os.O_CREATE, 0755) 139 if err != nil { 140 return err 141 } 142 143 ntpl := template.Must(template.New("nuclei").Parse(hubtest.TemplateNucleiFile)) 144 if ntpl == nil { 145 return errors.New("unable to parse nuclei template") 146 } 147 ntpl.ExecuteTemplate(nucleiFile, "nuclei", struct{ TestName string }{TestName: testName}) 148 nucleiFile.Close() 149 configFileData.AppsecRules = []string{"./appsec-rules/<author>/your_rule_here.yaml"} 150 configFileData.NucleiTemplate = nucleiFileName 151 fmt.Println() 152 fmt.Printf(" Test name : %s\n", testName) 153 fmt.Printf(" Test path : %s\n", testPath) 154 fmt.Printf(" Config File : %s\n", configFilePath) 155 fmt.Printf(" Nuclei Template : %s\n", nucleiFilePath) 156 } else { 157 // create empty log file 158 logFileName := fmt.Sprintf("%s.log", testName) 159 logFilePath := filepath.Join(testPath, logFileName) 160 logFile, err := os.Create(logFilePath) 161 if err != nil { 162 return err 163 } 164 logFile.Close() 165 166 // create empty parser assertion file 167 parserAssertFilePath := filepath.Join(testPath, hubtest.ParserAssertFileName) 168 parserAssertFile, err := os.Create(parserAssertFilePath) 169 if err != nil { 170 return err 171 } 172 parserAssertFile.Close() 173 // create empty scenario assertion file 174 scenarioAssertFilePath := filepath.Join(testPath, hubtest.ScenarioAssertFileName) 175 scenarioAssertFile, err := os.Create(scenarioAssertFilePath) 176 if err != nil { 177 return err 178 } 179 scenarioAssertFile.Close() 180 181 parsers = append(parsers, "crowdsecurity/syslog-logs") 182 parsers = append(parsers, "crowdsecurity/dateparse-enrich") 183 184 if len(scenarios) == 0 { 185 scenarios = append(scenarios, "") 186 } 187 188 if len(postoverflows) == 0 { 189 postoverflows = append(postoverflows, "") 190 } 191 configFileData.Parsers = parsers 192 configFileData.Scenarios = scenarios 193 configFileData.PostOverflows = postoverflows 194 configFileData.LogFile = logFileName 195 configFileData.LogType = logType 196 configFileData.IgnoreParsers = ignoreParsers 197 configFileData.Labels = labels 198 fmt.Println() 199 fmt.Printf(" Test name : %s\n", testName) 200 fmt.Printf(" Test path : %s\n", testPath) 201 fmt.Printf(" Log file : %s (please fill it with logs)\n", logFilePath) 202 fmt.Printf(" Parser assertion file : %s (please fill it with assertion)\n", parserAssertFilePath) 203 fmt.Printf(" Scenario assertion file : %s (please fill it with assertion)\n", scenarioAssertFilePath) 204 fmt.Printf(" Configuration File : %s (please fill it with parsers, scenarios...)\n", configFilePath) 205 } 206 207 fd, err := os.Create(configFilePath) 208 if err != nil { 209 return fmt.Errorf("open: %w", err) 210 } 211 data, err := yaml.Marshal(configFileData) 212 if err != nil { 213 return fmt.Errorf("marshal: %w", err) 214 } 215 _, err = fd.Write(data) 216 if err != nil { 217 return fmt.Errorf("write: %w", err) 218 } 219 if err := fd.Close(); err != nil { 220 return fmt.Errorf("close: %w", err) 221 } 222 223 return nil 224 }, 225 } 226 227 cmd.PersistentFlags().StringVarP(&logType, "type", "t", "", "Log type of the test") 228 cmd.Flags().StringSliceVarP(&parsers, "parsers", "p", parsers, "Parsers to add to test") 229 cmd.Flags().StringSliceVar(&postoverflows, "postoverflows", postoverflows, "Postoverflows to add to test") 230 cmd.Flags().StringSliceVarP(&scenarios, "scenarios", "s", scenarios, "Scenarios to add to test") 231 cmd.PersistentFlags().BoolVar(&ignoreParsers, "ignore-parsers", false, "Don't run test on parsers") 232 233 return cmd 234 } 235 236 func (cli *cliHubTest) NewRunCmd() *cobra.Command { 237 var ( 238 noClean bool 239 runAll bool 240 forceClean bool 241 NucleiTargetHost string 242 AppSecHost string 243 ) 244 245 cmd := &cobra.Command{ 246 Use: "run", 247 Short: "run [test_name]", 248 DisableAutoGenTag: true, 249 RunE: func(cmd *cobra.Command, args []string) error { 250 cfg := cli.cfg() 251 252 if !runAll && len(args) == 0 { 253 printHelp(cmd) 254 return errors.New("please provide test to run or --all flag") 255 } 256 hubPtr.NucleiTargetHost = NucleiTargetHost 257 hubPtr.AppSecHost = AppSecHost 258 if runAll { 259 if err := hubPtr.LoadAllTests(); err != nil { 260 return fmt.Errorf("unable to load all tests: %+v", err) 261 } 262 } else { 263 for _, testName := range args { 264 _, err := hubPtr.LoadTestItem(testName) 265 if err != nil { 266 return fmt.Errorf("unable to load test '%s': %w", testName, err) 267 } 268 } 269 } 270 271 // set timezone to avoid DST issues 272 os.Setenv("TZ", "UTC") 273 for _, test := range hubPtr.Tests { 274 if cfg.Cscli.Output == "human" { 275 log.Infof("Running test '%s'", test.Name) 276 } 277 err := test.Run() 278 if err != nil { 279 log.Errorf("running test '%s' failed: %+v", test.Name, err) 280 } 281 } 282 283 return nil 284 }, 285 PersistentPostRunE: func(_ *cobra.Command, _ []string) error { 286 cfg := cli.cfg() 287 288 success := true 289 testResult := make(map[string]bool) 290 for _, test := range hubPtr.Tests { 291 if test.AutoGen && !isAppsecTest { 292 if test.ParserAssert.AutoGenAssert { 293 log.Warningf("Assert file '%s' is empty, generating assertion:", test.ParserAssert.File) 294 fmt.Println() 295 fmt.Println(test.ParserAssert.AutoGenAssertData) 296 } 297 if test.ScenarioAssert.AutoGenAssert { 298 log.Warningf("Assert file '%s' is empty, generating assertion:", test.ScenarioAssert.File) 299 fmt.Println() 300 fmt.Println(test.ScenarioAssert.AutoGenAssertData) 301 } 302 if !noClean { 303 if err := test.Clean(); err != nil { 304 return fmt.Errorf("unable to clean test '%s' env: %w", test.Name, err) 305 } 306 } 307 fmt.Printf("\nPlease fill your assert file(s) for test '%s', exiting\n", test.Name) 308 os.Exit(1) 309 } 310 testResult[test.Name] = test.Success 311 if test.Success { 312 if cfg.Cscli.Output == "human" { 313 log.Infof("Test '%s' passed successfully (%d assertions)\n", test.Name, test.ParserAssert.NbAssert+test.ScenarioAssert.NbAssert) 314 } 315 if !noClean { 316 if err := test.Clean(); err != nil { 317 return fmt.Errorf("unable to clean test '%s' env: %w", test.Name, err) 318 } 319 } 320 } else { 321 success = false 322 cleanTestEnv := false 323 if cfg.Cscli.Output == "human" { 324 if len(test.ParserAssert.Fails) > 0 { 325 fmt.Println() 326 log.Errorf("Parser test '%s' failed (%d errors)\n", test.Name, len(test.ParserAssert.Fails)) 327 for _, fail := range test.ParserAssert.Fails { 328 fmt.Printf("(L.%d) %s => %s\n", fail.Line, emoji.RedCircle, fail.Expression) 329 fmt.Printf(" Actual expression values:\n") 330 for key, value := range fail.Debug { 331 fmt.Printf(" %s = '%s'\n", key, strings.TrimSuffix(value, "\n")) 332 } 333 fmt.Println() 334 } 335 } 336 if len(test.ScenarioAssert.Fails) > 0 { 337 fmt.Println() 338 log.Errorf("Scenario test '%s' failed (%d errors)\n", test.Name, len(test.ScenarioAssert.Fails)) 339 for _, fail := range test.ScenarioAssert.Fails { 340 fmt.Printf("(L.%d) %s => %s\n", fail.Line, emoji.RedCircle, fail.Expression) 341 fmt.Printf(" Actual expression values:\n") 342 for key, value := range fail.Debug { 343 fmt.Printf(" %s = '%s'\n", key, strings.TrimSuffix(value, "\n")) 344 } 345 fmt.Println() 346 } 347 } 348 if !forceClean && !noClean { 349 prompt := &survey.Confirm{ 350 Message: fmt.Sprintf("\nDo you want to remove runtime folder for test '%s'? (default: Yes)", test.Name), 351 Default: true, 352 } 353 if err := survey.AskOne(prompt, &cleanTestEnv); err != nil { 354 return fmt.Errorf("unable to ask to remove runtime folder: %w", err) 355 } 356 } 357 } 358 359 if cleanTestEnv || forceClean { 360 if err := test.Clean(); err != nil { 361 return fmt.Errorf("unable to clean test '%s' env: %w", test.Name, err) 362 } 363 } 364 } 365 } 366 367 switch cfg.Cscli.Output { 368 case "human": 369 hubTestResultTable(color.Output, testResult) 370 case "json": 371 jsonResult := make(map[string][]string, 0) 372 jsonResult["success"] = make([]string, 0) 373 jsonResult["fail"] = make([]string, 0) 374 for testName, success := range testResult { 375 if success { 376 jsonResult["success"] = append(jsonResult["success"], testName) 377 } else { 378 jsonResult["fail"] = append(jsonResult["fail"], testName) 379 } 380 } 381 jsonStr, err := json.Marshal(jsonResult) 382 if err != nil { 383 return fmt.Errorf("unable to json test result: %w", err) 384 } 385 fmt.Println(string(jsonStr)) 386 default: 387 return errors.New("only human/json output modes are supported") 388 } 389 390 if !success { 391 os.Exit(1) 392 } 393 394 return nil 395 }, 396 } 397 398 cmd.Flags().BoolVar(&noClean, "no-clean", false, "Don't clean runtime environment if test succeed") 399 cmd.Flags().BoolVar(&forceClean, "clean", false, "Clean runtime environment if test fail") 400 cmd.Flags().StringVar(&NucleiTargetHost, "target", hubtest.DefaultNucleiTarget, "Target for AppSec Test") 401 cmd.Flags().StringVar(&AppSecHost, "host", hubtest.DefaultAppsecHost, "Address to expose AppSec for hubtest") 402 cmd.Flags().BoolVar(&runAll, "all", false, "Run all tests") 403 404 return cmd 405 } 406 407 func (cli *cliHubTest) NewCleanCmd() *cobra.Command { 408 var cmd = &cobra.Command{ 409 Use: "clean", 410 Short: "clean [test_name]", 411 Args: cobra.MinimumNArgs(1), 412 DisableAutoGenTag: true, 413 RunE: func(_ *cobra.Command, args []string) error { 414 for _, testName := range args { 415 test, err := hubPtr.LoadTestItem(testName) 416 if err != nil { 417 return fmt.Errorf("unable to load test '%s': %w", testName, err) 418 } 419 if err := test.Clean(); err != nil { 420 return fmt.Errorf("unable to clean test '%s' env: %w", test.Name, err) 421 } 422 } 423 424 return nil 425 }, 426 } 427 428 return cmd 429 } 430 431 func (cli *cliHubTest) NewInfoCmd() *cobra.Command { 432 cmd := &cobra.Command{ 433 Use: "info", 434 Short: "info [test_name]", 435 Args: cobra.MinimumNArgs(1), 436 DisableAutoGenTag: true, 437 RunE: func(_ *cobra.Command, args []string) error { 438 for _, testName := range args { 439 test, err := hubPtr.LoadTestItem(testName) 440 if err != nil { 441 return fmt.Errorf("unable to load test '%s': %w", testName, err) 442 } 443 fmt.Println() 444 fmt.Printf(" Test name : %s\n", test.Name) 445 fmt.Printf(" Test path : %s\n", test.Path) 446 if isAppsecTest { 447 fmt.Printf(" Nuclei Template : %s\n", test.Config.NucleiTemplate) 448 fmt.Printf(" Appsec Rules : %s\n", strings.Join(test.Config.AppsecRules, ", ")) 449 } else { 450 fmt.Printf(" Log file : %s\n", filepath.Join(test.Path, test.Config.LogFile)) 451 fmt.Printf(" Parser assertion file : %s\n", filepath.Join(test.Path, hubtest.ParserAssertFileName)) 452 fmt.Printf(" Scenario assertion file : %s\n", filepath.Join(test.Path, hubtest.ScenarioAssertFileName)) 453 } 454 fmt.Printf(" Configuration File : %s\n", filepath.Join(test.Path, "config.yaml")) 455 } 456 457 return nil 458 }, 459 } 460 461 return cmd 462 } 463 464 func (cli *cliHubTest) NewListCmd() *cobra.Command { 465 cmd := &cobra.Command{ 466 Use: "list", 467 Short: "list", 468 DisableAutoGenTag: true, 469 RunE: func(_ *cobra.Command, _ []string) error { 470 cfg := cli.cfg() 471 472 if err := hubPtr.LoadAllTests(); err != nil { 473 return fmt.Errorf("unable to load all tests: %w", err) 474 } 475 476 switch cfg.Cscli.Output { 477 case "human": 478 hubTestListTable(color.Output, hubPtr.Tests) 479 case "json": 480 j, err := json.MarshalIndent(hubPtr.Tests, " ", " ") 481 if err != nil { 482 return err 483 } 484 fmt.Println(string(j)) 485 default: 486 return errors.New("only human/json output modes are supported") 487 } 488 489 return nil 490 }, 491 } 492 493 return cmd 494 } 495 496 func (cli *cliHubTest) NewCoverageCmd() *cobra.Command { 497 var ( 498 showParserCov bool 499 showScenarioCov bool 500 showOnlyPercent bool 501 showAppsecCov bool 502 ) 503 504 cmd := &cobra.Command{ 505 Use: "coverage", 506 Short: "coverage", 507 DisableAutoGenTag: true, 508 RunE: func(_ *cobra.Command, _ []string) error { 509 cfg := cli.cfg() 510 511 // for this one we explicitly don't do for appsec 512 if err := HubTest.LoadAllTests(); err != nil { 513 return fmt.Errorf("unable to load all tests: %+v", err) 514 } 515 var err error 516 scenarioCoverage := []hubtest.Coverage{} 517 parserCoverage := []hubtest.Coverage{} 518 appsecRuleCoverage := []hubtest.Coverage{} 519 scenarioCoveragePercent := 0 520 parserCoveragePercent := 0 521 appsecRuleCoveragePercent := 0 522 523 // if both are false (flag by default), show both 524 showAll := !showScenarioCov && !showParserCov && !showAppsecCov 525 526 if showParserCov || showAll { 527 parserCoverage, err = HubTest.GetParsersCoverage() 528 if err != nil { 529 return fmt.Errorf("while getting parser coverage: %w", err) 530 } 531 parserTested := 0 532 for _, test := range parserCoverage { 533 if test.TestsCount > 0 { 534 parserTested++ 535 } 536 } 537 parserCoveragePercent = int(math.Round((float64(parserTested) / float64(len(parserCoverage)) * 100))) 538 } 539 540 if showScenarioCov || showAll { 541 scenarioCoverage, err = HubTest.GetScenariosCoverage() 542 if err != nil { 543 return fmt.Errorf("while getting scenario coverage: %w", err) 544 } 545 546 scenarioTested := 0 547 for _, test := range scenarioCoverage { 548 if test.TestsCount > 0 { 549 scenarioTested++ 550 } 551 } 552 553 scenarioCoveragePercent = int(math.Round((float64(scenarioTested) / float64(len(scenarioCoverage)) * 100))) 554 } 555 556 if showAppsecCov || showAll { 557 appsecRuleCoverage, err = HubTest.GetAppsecCoverage() 558 if err != nil { 559 return fmt.Errorf("while getting scenario coverage: %w", err) 560 } 561 562 appsecRuleTested := 0 563 for _, test := range appsecRuleCoverage { 564 if test.TestsCount > 0 { 565 appsecRuleTested++ 566 } 567 } 568 appsecRuleCoveragePercent = int(math.Round((float64(appsecRuleTested) / float64(len(appsecRuleCoverage)) * 100))) 569 } 570 571 if showOnlyPercent { 572 switch { 573 case showAll: 574 fmt.Printf("parsers=%d%%\nscenarios=%d%%\nappsec_rules=%d%%", parserCoveragePercent, scenarioCoveragePercent, appsecRuleCoveragePercent) 575 case showParserCov: 576 fmt.Printf("parsers=%d%%", parserCoveragePercent) 577 case showScenarioCov: 578 fmt.Printf("scenarios=%d%%", scenarioCoveragePercent) 579 case showAppsecCov: 580 fmt.Printf("appsec_rules=%d%%", appsecRuleCoveragePercent) 581 } 582 os.Exit(0) 583 } 584 585 switch cfg.Cscli.Output { 586 case "human": 587 if showParserCov || showAll { 588 hubTestParserCoverageTable(color.Output, parserCoverage) 589 } 590 591 if showScenarioCov || showAll { 592 hubTestScenarioCoverageTable(color.Output, scenarioCoverage) 593 } 594 595 if showAppsecCov || showAll { 596 hubTestAppsecRuleCoverageTable(color.Output, appsecRuleCoverage) 597 } 598 599 fmt.Println() 600 if showParserCov || showAll { 601 fmt.Printf("PARSERS : %d%% of coverage\n", parserCoveragePercent) 602 } 603 if showScenarioCov || showAll { 604 fmt.Printf("SCENARIOS : %d%% of coverage\n", scenarioCoveragePercent) 605 } 606 if showAppsecCov || showAll { 607 fmt.Printf("APPSEC RULES : %d%% of coverage\n", appsecRuleCoveragePercent) 608 } 609 case "json": 610 dump, err := json.MarshalIndent(parserCoverage, "", " ") 611 if err != nil { 612 return err 613 } 614 fmt.Printf("%s", dump) 615 dump, err = json.MarshalIndent(scenarioCoverage, "", " ") 616 if err != nil { 617 return err 618 } 619 fmt.Printf("%s", dump) 620 dump, err = json.MarshalIndent(appsecRuleCoverage, "", " ") 621 if err != nil { 622 return err 623 } 624 fmt.Printf("%s", dump) 625 default: 626 return errors.New("only human/json output modes are supported") 627 } 628 629 return nil 630 }, 631 } 632 633 cmd.PersistentFlags().BoolVar(&showOnlyPercent, "percent", false, "Show only percentages of coverage") 634 cmd.PersistentFlags().BoolVar(&showParserCov, "parsers", false, "Show only parsers coverage") 635 cmd.PersistentFlags().BoolVar(&showScenarioCov, "scenarios", false, "Show only scenarios coverage") 636 cmd.PersistentFlags().BoolVar(&showAppsecCov, "appsec", false, "Show only appsec coverage") 637 638 return cmd 639 } 640 641 func (cli *cliHubTest) NewEvalCmd() *cobra.Command { 642 var evalExpression string 643 644 cmd := &cobra.Command{ 645 Use: "eval", 646 Short: "eval [test_name]", 647 Args: cobra.ExactArgs(1), 648 DisableAutoGenTag: true, 649 RunE: func(_ *cobra.Command, args []string) error { 650 for _, testName := range args { 651 test, err := hubPtr.LoadTestItem(testName) 652 if err != nil { 653 return fmt.Errorf("can't load test: %+v", err) 654 } 655 656 err = test.ParserAssert.LoadTest(test.ParserResultFile) 657 if err != nil { 658 return fmt.Errorf("can't load test results from '%s': %+v", test.ParserResultFile, err) 659 } 660 661 output, err := test.ParserAssert.EvalExpression(evalExpression) 662 if err != nil { 663 return err 664 } 665 666 fmt.Print(output) 667 } 668 669 return nil 670 }, 671 } 672 673 cmd.PersistentFlags().StringVarP(&evalExpression, "expr", "e", "", "Expression to eval") 674 675 return cmd 676 } 677 678 func (cli *cliHubTest) NewExplainCmd() *cobra.Command { 679 cmd := &cobra.Command{ 680 Use: "explain", 681 Short: "explain [test_name]", 682 Args: cobra.ExactArgs(1), 683 DisableAutoGenTag: true, 684 RunE: func(_ *cobra.Command, args []string) error { 685 for _, testName := range args { 686 test, err := HubTest.LoadTestItem(testName) 687 if err != nil { 688 return fmt.Errorf("can't load test: %+v", err) 689 } 690 err = test.ParserAssert.LoadTest(test.ParserResultFile) 691 if err != nil { 692 if err = test.Run(); err != nil { 693 return fmt.Errorf("running test '%s' failed: %+v", test.Name, err) 694 } 695 696 if err = test.ParserAssert.LoadTest(test.ParserResultFile); err != nil { 697 return fmt.Errorf("unable to load parser result after run: %w", err) 698 } 699 } 700 701 err = test.ScenarioAssert.LoadTest(test.ScenarioResultFile, test.BucketPourResultFile) 702 if err != nil { 703 if err = test.Run(); err != nil { 704 return fmt.Errorf("running test '%s' failed: %+v", test.Name, err) 705 } 706 707 if err = test.ScenarioAssert.LoadTest(test.ScenarioResultFile, test.BucketPourResultFile); err != nil { 708 return fmt.Errorf("unable to load scenario result after run: %w", err) 709 } 710 } 711 opts := dumps.DumpOpts{} 712 dumps.DumpTree(*test.ParserAssert.TestData, *test.ScenarioAssert.PourData, opts) 713 } 714 715 return nil 716 }, 717 } 718 719 return cmd 720 }