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  }