github.com/rainforestapp/rainforest-cli@v2.12.0+incompatible/rfml.go (about) 1 package main 2 3 import ( 4 "errors" 5 "fmt" 6 "log" 7 "os" 8 "path/filepath" 9 "regexp" 10 "strconv" 11 "strings" 12 13 "github.com/gyuho/goraph" 14 "github.com/rainforestapp/rainforest-cli/rainforest" 15 "github.com/satori/go.uuid" 16 "github.com/urfave/cli" 17 ) 18 19 // parseError is a custom error implementing error interface for reporting RFML parsing errors. 20 type fileParseError struct { 21 filePath string 22 parseError error 23 } 24 25 func (e fileParseError) Error() string { 26 return fmt.Sprintf("%v: %v", e.filePath, e.parseError.Error()) 27 } 28 29 // validateRFML is a wrapper around two other validation functions 30 // first one for the single file and the other for whole directory 31 func validateRFML(c cliContext, api rfmlAPI) error { 32 if path := c.Args().First(); path != "" { 33 err := validateSingleRFMLFile(path) 34 if err != nil { 35 return cli.NewExitError(err.Error(), 1) 36 } 37 return nil 38 } 39 tests, err := readRFMLFiles([]string{c.String("test-folder")}) 40 if err != nil { 41 return cli.NewExitError(err.Error(), 1) 42 } 43 err = validateRFMLFiles(tests, false, api) 44 if err != nil { 45 return cli.NewExitError(err.Error(), 1) 46 } 47 return nil 48 } 49 50 // readRFMLFiles takes in a list of files and/or directories and a list of tags 51 // and returns a list of the parsed tests, or an error if it is encountered. To 52 // allow all tags, pass in nil for tags. 53 func readRFMLFiles(files []string) ([]*rainforest.RFTest, error) { 54 fileList := []string{} 55 for _, file := range files { 56 stat, err := os.Stat(file) 57 if err != nil { 58 return nil, err 59 } 60 if !stat.IsDir() { 61 if strings.HasSuffix(file, ".rfml") { 62 fileList = append(fileList, file) 63 continue 64 } else { 65 log.Printf("%s is not a valid RFML file", file) 66 continue 67 } 68 } 69 70 // We have a directory, walk through and find RFML files 71 err = filepath.Walk(file, func(path string, f os.FileInfo, err error) error { 72 if strings.HasSuffix(path, ".rfml") { 73 fileList = append(fileList, path) 74 } 75 return nil 76 }) 77 if err != nil { 78 return nil, err 79 } 80 } 81 82 tests := []*rainforest.RFTest{} 83 seenPaths := map[string]bool{} 84 for _, filePath := range fileList { 85 // No dups! 86 if seenPaths[filePath] { 87 continue 88 } 89 seenPaths[filePath] = true 90 test, err := readRFMLFile(filePath) 91 if err != nil { 92 return nil, err 93 } 94 tests = append(tests, test) 95 } 96 return tests, nil 97 } 98 99 // anyMember is one of those things that would probably be in the stdlib if 100 // there were generics. I hate golang sometimes. In any case, it returns true if 101 // any of needles are in haystack. It's O(n*m), so only put small stuff in 102 // there! 103 func anyMember(haystack []string, needles []string) bool { 104 for _, n := range needles { 105 for _, h := range haystack { 106 if h == n { 107 return true 108 } 109 } 110 } 111 112 return false 113 } 114 115 func readRFMLFile(filePath string) (*rainforest.RFTest, error) { 116 f, err := os.Open(filePath) 117 if err != nil { 118 return nil, err 119 } 120 defer f.Close() 121 122 rfmlReader := rainforest.NewRFMLReader(f) 123 var pTest *rainforest.RFTest 124 pTest, err = rfmlReader.ReadAll() 125 if err != nil { 126 return nil, fileParseError{filePath, err} 127 } 128 129 pTest.RFMLPath = filePath 130 return pTest, err 131 } 132 133 // validateSingleRFMLFile validates RFML file syntax by 134 // trying to parse the file and sending any parse errors to the caller 135 func validateSingleRFMLFile(filePath string) error { 136 if !strings.Contains(filePath, ".rfml") { 137 return errors.New("RFML files should have .rfml extension") 138 } 139 f, err := os.Open(filePath) 140 if err != nil { 141 return err 142 } 143 defer f.Close() 144 rfmlReader := rainforest.NewRFMLReader(f) 145 _, err = rfmlReader.ReadAll() 146 if err != nil { 147 return fileParseError{filePath, err} 148 } 149 log.Printf("%v's syntax is valid", filePath) 150 return nil 151 } 152 153 var errValidation = errors.New("Validation failed") 154 155 // validateRFMLFiles validates RFML file syntax, embedded rfml ids, checks for 156 // circular dependiences and all other cool things in the specified directory 157 func validateRFMLFiles(parsedTests []*rainforest.RFTest, localOnly bool, api rfmlAPI) error { 158 // parse all of them files 159 var validationErrors []error 160 var err error 161 dependencyGraph := goraph.NewGraph() 162 163 // check for rfml_id uniqueness 164 rfmlIDToTest := make(map[string]*rainforest.RFTest) 165 for _, pTest := range parsedTests { 166 if conflictingTest, ok := rfmlIDToTest[pTest.RFMLID]; ok { 167 err = fmt.Errorf(" duplicate RFML id %v, also found in: %v", pTest.RFMLID, conflictingTest.RFMLPath) 168 validationErrors = append(validationErrors, fileParseError{pTest.RFMLPath, err}) 169 } else { 170 rfmlIDToTest[pTest.RFMLID] = pTest 171 dependencyGraph.AddNode(goraph.NewNode(pTest.RFMLID)) 172 } 173 } 174 175 // check for embedded tests id validity 176 // start with pulling the external test ids to validate against them as well 177 if !localOnly && api.ClientToken() != "" { 178 externalTests, err := api.GetTestIDs() 179 if err != nil { 180 return err 181 } 182 for _, externalTest := range externalTests { 183 if _, ok := rfmlIDToTest[externalTest.RFMLID]; !ok { 184 rfmlIDToTest[externalTest.RFMLID] = &rainforest.RFTest{} 185 dependencyGraph.AddNode(goraph.NewNode(externalTest.RFMLID)) 186 } 187 } 188 } 189 // go through all the tests 190 for _, pTest := range parsedTests { 191 // and steps... 192 for stepNum, step := range pTest.Steps { 193 // then check if it's embeddedTest 194 if embeddedTest, ok := step.(rainforest.RFEmbeddedTest); ok { 195 // if so, check if its rfml id exists 196 if _, ok := rfmlIDToTest[embeddedTest.RFMLID]; !ok { 197 if localOnly || api.ClientToken() != "" { 198 err = fmt.Errorf("step %v - embeddedTest RFML id %v not found", stepNum+1, embeddedTest.RFMLID) 199 } else { 200 err = fmt.Errorf("step %v - embeddedTest RFML id %v not found. Specify token_id to check against external tests", stepNum+1, embeddedTest.RFMLID) 201 } 202 validationErrors = append(validationErrors, fileParseError{pTest.RFMLPath, err}) 203 } else { 204 pNode := dependencyGraph.GetNode(goraph.StringID(pTest.RFMLID)) 205 eNode := dependencyGraph.GetNode(goraph.StringID(embeddedTest.RFMLID)) 206 dependencyGraph.AddEdge(pNode.ID(), eNode.ID(), 1) 207 } 208 } 209 } 210 } 211 212 // validate circular dependiences probably using Tarjan's strongly connected components 213 stronglyConnected := goraph.Tarjan(dependencyGraph) 214 for _, circularTests := range stronglyConnected { 215 if len(circularTests) > 1 { 216 err = fmt.Errorf("Found circular dependiences between: %v", circularTests) 217 validationErrors = append(validationErrors, err) 218 } 219 } 220 221 if len(validationErrors) > 0 { 222 for _, err := range validationErrors { 223 log.Print(err.Error()) 224 } 225 return errValidation 226 } 227 228 log.Print("All files are valid!") 229 return nil 230 } 231 232 func newRFMLTest(c cliContext) error { 233 testDirectory := c.String("test-folder") 234 235 absTestDirectory, err := prepareTestDirectory(testDirectory) 236 if err != nil { 237 return cli.NewExitError(err.Error(), 1) 238 } 239 240 fileName := c.Args().First() 241 title := fileName 242 243 if fileName == "" { 244 fileName = "Unnamed Test.rfml" 245 title = "Unnamed Test" 246 } else if strings.HasSuffix(fileName, ".rfml") { 247 title = strings.TrimSuffix(title, ".rfml") 248 } else { 249 fileName = fileName + ".rfml" 250 } 251 252 filePath := filepath.Join(absTestDirectory, fileName) 253 254 // Make sure that the file is unique 255 basePath := strings.TrimSuffix(filePath, ".rfml") 256 fileIdentifier := 0 257 var identStr string 258 for { 259 if fileIdentifier == 0 { 260 identStr = "" 261 } else { 262 identStr = fmt.Sprintf(" (%v)", strconv.Itoa(fileIdentifier)) 263 } 264 265 testPath := basePath + identStr + ".rfml" 266 267 _, err = os.Stat(testPath) 268 if !os.IsNotExist(err) { 269 fileIdentifier = fileIdentifier + 1 270 } else { 271 filePath = testPath 272 break 273 } 274 } 275 276 test := rainforest.RFTest{ 277 RFMLID: uuid.NewV4().String(), 278 Title: title, 279 StartURI: "/", 280 Execute: true, 281 Steps: []interface{}{ 282 rainforest.RFTestStep{ 283 Action: "This is a step action.", 284 Response: "This is a step question?", 285 Redirect: true, 286 }, 287 rainforest.RFTestStep{ 288 Action: "This is another step action.", 289 Response: "This is another step question?", 290 Redirect: true, 291 }, 292 }, 293 } 294 295 f, err := os.Create(filePath) 296 if err != nil { 297 return cli.NewExitError(err.Error(), 1) 298 } 299 300 writer := rainforest.NewRFMLWriter(f) 301 err = writer.WriteRFMLTest(&test) 302 if err != nil { 303 return cli.NewExitError(err.Error(), 1) 304 } 305 306 return nil 307 } 308 309 func deleteRFML(c cliContext) error { 310 filePath := c.Args().First() 311 if !strings.Contains(filePath, ".rfml") { 312 return cli.NewExitError("RFML files should have .rfml extension", 1) 313 } 314 f, err := os.Open(filePath) 315 if err != nil { 316 return cli.NewExitError(err.Error(), 1) 317 } 318 rfmlReader := rainforest.NewRFMLReader(f) 319 parsedRFML, err := rfmlReader.ReadAll() 320 if err != nil { 321 errMsg := fmt.Sprintf("Error removing test at '%v': %v", filePath, err.Error()) 322 return cli.NewExitError(errMsg, 1) 323 } 324 325 if parsedRFML.RFMLID == "" { 326 return cli.NewExitError("RFML file doesn't have RFML ID", 1) 327 } 328 329 // Close the file now so we can delete it 330 f.Close() 331 332 // Delete remote first 333 err = api.DeleteTestByRFMLID(parsedRFML.RFMLID) 334 if err != nil { 335 return cli.NewExitError(err.Error(), 1) 336 } 337 // Then delete local file 338 err = os.Remove(filePath) 339 if err != nil { 340 return cli.NewExitError(err.Error(), 1) 341 } 342 return nil 343 } 344 345 // uploadRFML is a wrapper around test creating/updating functions 346 func uploadRFML(c cliContext, api rfmlAPI) error { 347 if c.Bool("synchronous-upload") { 348 rfmlUploadConcurrency = 1 349 } 350 if path := c.Args().First(); path != "" { 351 err := uploadSingleRFMLFile(path) 352 if err != nil { 353 return cli.NewExitError(err.Error(), 1) 354 } 355 return nil 356 } 357 tests, err := readRFMLFiles([]string{c.String("test-folder")}) 358 if err != nil { 359 return cli.NewExitError(err.Error(), 1) 360 } 361 err = uploadRFMLFiles(tests, false, api) 362 if err != nil { 363 return cli.NewExitError(err.Error(), 1) 364 } 365 return nil 366 } 367 368 // uploadSingleRFMLFile uploads RFML file syntax by 369 // trying to parse the file and sending any parse errors to the caller 370 func uploadSingleRFMLFile(filePath string) error { 371 // Validate first before uploading 372 err := validateSingleRFMLFile(filePath) 373 if err != nil { 374 return err 375 } 376 377 f, err := os.Open(filePath) 378 if err != nil { 379 return err 380 } 381 defer f.Close() 382 rfmlReader := rainforest.NewRFMLReader(f) 383 parsedTest, err := rfmlReader.ReadAll() 384 if err != nil { 385 return fileParseError{filePath, err} 386 } 387 parsedTest.RFMLPath = filePath 388 389 // Check if the test already exists in RF so we can decide between updating and creating new one 390 testIDPairs, err := api.GetTestIDs() 391 if err != nil { 392 return err 393 } 394 395 testIDCollection := rainforest.NewTestIDCollection(testIDPairs) 396 testID, err := testIDCollection.GetTestID(parsedTest.RFMLID) 397 if err != nil { 398 // Create an empty test 399 log.Printf("Creating new test: %v", parsedTest.RFMLID) 400 401 emptyTest := rainforest.RFTest{ 402 RFMLID: parsedTest.RFMLID, 403 Title: parsedTest.Title, 404 } 405 406 err = emptyTest.PrepareToUploadFromRFML(*testIDCollection) 407 if err != nil { 408 return err 409 } 410 411 err = api.CreateTest(&emptyTest) 412 if err != nil { 413 return err 414 } 415 log.Printf("Created new test: %v", parsedTest.RFMLID) 416 // Refresh collection with new test IDs 417 testIDPairs, err = api.GetTestIDs() 418 if err != nil { 419 return err 420 } 421 testIDCollection = rainforest.NewTestIDCollection(testIDPairs) 422 423 // Assign test ID 424 testID, err = testIDCollection.GetTestID(parsedTest.RFMLID) 425 if err != nil { 426 panic(fmt.Sprintf("Unable to map RFML ID to a primary ID: %v", parsedTest.RFMLID)) 427 } else { 428 parsedTest.TestID = testID 429 } 430 } else { 431 parsedTest.TestID = testID 432 } 433 434 if parsedTest.HasUploadableFiles() { 435 err = api.ParseEmbeddedFiles(parsedTest) 436 if err != nil { 437 return err 438 } 439 } 440 441 err = parsedTest.PrepareToUploadFromRFML(*testIDCollection) 442 if err != nil { 443 return err 444 } 445 446 // Update the steps 447 log.Printf("Updating steps for test: %v", parsedTest.RFMLID) 448 err = api.UpdateTest(parsedTest) 449 if err != nil { 450 return err 451 } 452 return nil 453 } 454 455 func uploadRFMLFiles(tests []*rainforest.RFTest, localOnly bool, api rfmlAPI) error { 456 err := validateRFMLFiles(tests, localOnly, api) 457 if err != nil { 458 return err 459 } 460 461 // walk through the specifed directory (also subdirs) and pick the .rfml files 462 // This will be used over and over again 463 testIDs, err := api.GetTestIDs() 464 if err != nil { 465 return err 466 } 467 testIDCollection := rainforest.NewTestIDCollection(testIDs) 468 469 var newTests []*rainforest.RFTest 470 var parsedTests []*rainforest.RFTest 471 472 for _, pTest := range tests { 473 parsedTests = append(parsedTests, pTest) 474 // Check if it's a new test or an existing one, because they need different treatment 475 // to ensure we first add new ones and have IDs for potential embedds 476 _, err = testIDCollection.GetTestID(pTest.RFMLID) 477 if err != nil { 478 newTests = append(newTests, pTest) 479 } 480 } 481 // chan to gather errors from workers 482 errorsChan := make(chan error) 483 484 // prepare empty tests to upload, we will fill the steps later on in case there are some 485 // dependiences between them, we want all of the IDs in place 486 testsToCreate := make(chan *rainforest.RFTest, len(newTests)) 487 for _, newTest := range newTests { 488 emptyTest := rainforest.RFTest{ 489 RFMLID: newTest.RFMLID, 490 Description: newTest.Description, 491 Title: newTest.Title, 492 } 493 err = emptyTest.PrepareToUploadFromRFML(*testIDCollection) 494 if err != nil { 495 return err 496 } 497 testsToCreate <- &emptyTest 498 } 499 close(testsToCreate) 500 501 // spawn workers to create the tests 502 for i := 0; i < rfmlUploadConcurrency; i++ { 503 go testCreationWorker(api, testsToCreate, errorsChan) 504 } 505 506 // Read out the workers results 507 for i := 0; i < len(newTests); i++ { 508 if err = <-errorsChan; err != nil { 509 return err 510 } 511 } 512 513 // Refresh the collection with new test IDs so we have all of the new tests 514 testIDs, err = api.GetTestIDs() 515 if err != nil { 516 return err 517 } 518 testIDCollection = rainforest.NewTestIDCollection(testIDs) 519 520 // And here we update all of the tests 521 testsToUpdate := make(chan *rainforest.RFTest, len(parsedTests)) 522 for _, testToUpdate := range parsedTests { 523 testID, err := testIDCollection.GetTestID(testToUpdate.RFMLID) 524 if err != nil { 525 panic(fmt.Sprintf("Unable to map RFML ID to primary ID: %v", testToUpdate.RFMLID)) 526 } else { 527 testToUpdate.TestID = testID 528 } 529 530 if testToUpdate.HasUploadableFiles() { 531 err = api.ParseEmbeddedFiles(testToUpdate) 532 if err != nil { 533 return err 534 } 535 } 536 537 err = testToUpdate.PrepareToUploadFromRFML(*testIDCollection) 538 if err != nil { 539 return err 540 } 541 542 testsToUpdate <- testToUpdate 543 } 544 close(testsToUpdate) 545 546 // spawn workers to create the tests 547 for i := 0; i < rfmlUploadConcurrency; i++ { 548 go testUpdateWorker(api, testsToUpdate, errorsChan) 549 } 550 551 // Read out the workers results 552 for i := 0; i < len(parsedTests); i++ { 553 if err := <-errorsChan; err != nil { 554 return err 555 } 556 } 557 558 return nil 559 } 560 561 type rfmlAPI interface { 562 GetTestIDs() ([]rainforest.TestIDPair, error) 563 GetTests(*rainforest.RFTestFilters) ([]rainforest.RFTest, error) 564 GetTest(int) (*rainforest.RFTest, error) 565 CreateTest(*rainforest.RFTest) error 566 UpdateTest(*rainforest.RFTest) error 567 ParseEmbeddedFiles(*rainforest.RFTest) error 568 ClientToken() string 569 } 570 571 func downloadRFML(c cliContext, client rfmlAPI) error { 572 testDirectory := c.String("test-folder") 573 absTestDirectory, err := prepareTestDirectory(testDirectory) 574 if err != nil { 575 return cli.NewExitError(err.Error(), 1) 576 } 577 578 var testIDs []int 579 if len(c.Args()) > 0 { 580 var testID int 581 for _, arg := range c.Args() { 582 testID, err = strconv.Atoi(arg) 583 if err != nil { 584 return cli.NewExitError(err.Error(), 1) 585 } 586 587 testIDs = append(testIDs, testID) 588 } 589 } else { 590 var tests []rainforest.RFTest 591 filters := rainforest.RFTestFilters{ 592 Tags: c.StringSlice("tag"), 593 } 594 if c.Int("site-id") > 0 { 595 filters.SiteID = c.Int("site-id") 596 } 597 if c.Int("folder-id") > 0 { 598 filters.SmartFolderID = c.Int("folder-id") 599 } 600 if c.Int("feature-id") > 0 { 601 filters.FeatureID = c.Int("feature-id") 602 } 603 if c.Int("run-group-id") > 0 { 604 filters.RunGroupID = c.Int("run-group-id") 605 } 606 607 tests, err = client.GetTests(&filters) 608 if err != nil { 609 return cli.NewExitError(err.Error(), 1) 610 } 611 612 for _, t := range tests { 613 testID := t.TestID 614 testIDs = append(testIDs, testID) 615 } 616 } 617 618 errorsChan := make(chan error) 619 testIDChan := make(chan int, len(testIDs)) 620 testChan := make(chan *rainforest.RFTest, len(testIDs)) 621 622 for _, testID := range testIDs { 623 testIDChan <- testID 624 } 625 close(testIDChan) 626 627 for i := 0; i < rfmlDownloadConcurrency; i++ { 628 go downloadRFTestWorker(testIDChan, errorsChan, testChan, client) 629 } 630 631 testIDPairs, err := client.GetTestIDs() 632 if err != nil { 633 return cli.NewExitError(err.Error(), 1) 634 } 635 testIDCollection := rainforest.NewTestIDCollection(testIDPairs) 636 637 for i := 0; i < len(testIDs); i++ { 638 select { 639 case err = <-errorsChan: 640 return cli.NewExitError(err.Error(), 1) 641 case test := <-testChan: 642 err = test.PrepareToWriteAsRFML(*testIDCollection, c.Bool("flatten-steps")) 643 if err != nil { 644 return cli.NewExitError(err.Error(), 1) 645 } 646 647 paddedTestID := fmt.Sprintf("%010d", test.TestID) 648 sanitizedTitle := sanitizeTestTitle(test.Title) 649 fileName := fmt.Sprintf("%v_%v.rfml", paddedTestID, sanitizedTitle) 650 rfmlFilePath := filepath.Join(absTestDirectory, fileName) 651 652 var file *os.File 653 file, err = os.Create(rfmlFilePath) 654 if err != nil { 655 return cli.NewExitError(err.Error(), 1) 656 } 657 658 writer := rainforest.NewRFMLWriter(file) 659 err = writer.WriteRFMLTest(test) 660 file.Close() 661 if err != nil { 662 return cli.NewExitError(err.Error(), 1) 663 } 664 665 log.Printf("Downloaded RFML test to %v", rfmlFilePath) 666 } 667 } 668 669 return nil 670 } 671 672 func downloadRFTestWorker(testIDChan chan int, errorsChan chan error, testChan chan *rainforest.RFTest, client rfmlAPI) { 673 for testID := range testIDChan { 674 test, err := client.GetTest(testID) 675 if err != nil { 676 errorsChan <- err 677 return 678 } 679 testChan <- test 680 } 681 } 682 683 /* 684 Helper Functions 685 */ 686 687 func prepareTestDirectory(testDir string) (string, error) { 688 absTestDirectory, err := filepath.Abs(testDir) 689 if err != nil { 690 return "", err 691 } 692 693 dirStat, err := os.Stat(absTestDirectory) 694 if os.IsNotExist(err) { 695 log.Printf("Creating test directory: %v", absTestDirectory) 696 os.MkdirAll(absTestDirectory, os.ModePerm) 697 } else if err != nil { 698 return "", err 699 } else { 700 if !dirStat.IsDir() { 701 return "", fmt.Errorf("%v should be a directory", absTestDirectory) 702 } 703 } 704 705 return absTestDirectory, nil 706 } 707 708 func sanitizeTestTitle(title string) string { 709 title = strings.TrimSpace(title) 710 title = strings.ToLower(title) 711 712 // replace all non-alphanumeric character sequences with an underscore 713 rep := regexp.MustCompile(`[^[[:alnum:]]+`) 714 title = rep.ReplaceAllLiteralString(title, "_") 715 716 if len(title) > 30 { 717 return title[:30] 718 } 719 720 return title 721 } 722 723 func testCreationWorker(api rfmlAPI, 724 testsToCreate <-chan *rainforest.RFTest, errorsChan chan<- error) { 725 for test := range testsToCreate { 726 log.Printf("Creating new test: %v", test.RFMLID) 727 err := api.CreateTest(test) 728 errorsChan <- err 729 } 730 } 731 732 func testUpdateWorker(api rfmlAPI, 733 testsToUpdate <-chan *rainforest.RFTest, errorsChan chan<- error) { 734 for test := range testsToUpdate { 735 log.Printf("Updating existing test: %v", test.RFMLID) 736 err := api.UpdateTest(test) 737 errorsChan <- err 738 } 739 }