github.com/ezbuy/gauge@v0.9.4-0.20171013092048-7ac5bd3931cd/api/infoGatherer/specDetails.go (about)

     1  // Copyright 2015 ThoughtWorks, Inc.
     2  
     3  // This file is part of Gauge.
     4  
     5  // Gauge is free software: you can redistribute it and/or modify
     6  // it under the terms of the GNU General Public License as published by
     7  // the Free Software Foundation, either version 3 of the License, or
     8  // (at your option) any later version.
     9  
    10  // Gauge is distributed in the hope that it will be useful,
    11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13  // GNU General Public License for more details.
    14  
    15  // You should have received a copy of the GNU General Public License
    16  // along with Gauge.  If not, see <http://www.gnu.org/licenses/>.
    17  
    18  package infoGatherer
    19  
    20  import (
    21  	"io/ioutil"
    22  	"path/filepath"
    23  	"sync"
    24  
    25  	"github.com/fsnotify/fsnotify"
    26  	"github.com/getgauge/common"
    27  	"github.com/getgauge/gauge/config"
    28  	"github.com/getgauge/gauge/gauge"
    29  	"github.com/getgauge/gauge/gauge_messages"
    30  	"github.com/getgauge/gauge/logger"
    31  	"github.com/getgauge/gauge/parser"
    32  	"github.com/getgauge/gauge/util"
    33  )
    34  
    35  // SpecInfoGatherer contains the caches for specs, concepts, and steps
    36  type SpecInfoGatherer struct {
    37  	waitGroup         sync.WaitGroup
    38  	conceptDictionary *gauge.ConceptDictionary
    39  	specsCache        specsCache
    40  	conceptsCache     conceptCache
    41  	stepsCache        stepsCache
    42  	paramsCache       paramsCache
    43  	SpecDirs          []string
    44  }
    45  
    46  type conceptCache struct {
    47  	mutex    sync.RWMutex
    48  	concepts map[string][]*gauge.Concept
    49  }
    50  
    51  type stepsCache struct {
    52  	mutex      sync.RWMutex
    53  	stepValues map[string][]*gauge.StepValue
    54  }
    55  
    56  type specsCache struct {
    57  	mutex       sync.RWMutex
    58  	specDetails map[string]*SpecDetail
    59  }
    60  
    61  type paramsCache struct {
    62  	mutex         sync.RWMutex
    63  	staticParams  map[string]map[string]gauge.StepArg
    64  	dynamicParams map[string]map[string]gauge.StepArg
    65  }
    66  
    67  type SpecDetail struct {
    68  	Spec *gauge.Specification
    69  	Errs []parser.ParseError
    70  }
    71  
    72  func (d *SpecDetail) HasSpec() bool {
    73  	return d.Spec != nil && d.Spec.Heading != nil
    74  }
    75  
    76  func NewSpecInfoGatherer(conceptDictionary *gauge.ConceptDictionary) *SpecInfoGatherer {
    77  	return &SpecInfoGatherer{conceptDictionary: conceptDictionary,conceptsCache:conceptCache{concepts:make(map[string][]*gauge.Concept, 0)}}
    78  }
    79  
    80  // Init initializes all the SpecInfoGatherer caches
    81  func (s *SpecInfoGatherer) Init() {
    82  	go s.watchForFileChanges()
    83  	s.waitGroup.Wait()
    84  
    85  	// Concepts parsed first because we need to create a concept dictionary that spec parsing can use
    86  	s.initConceptsCache()
    87  	s.initSpecsCache()
    88  	s.initStepsCache()
    89  	s.initParamsCache()
    90  }
    91  
    92  func (s *SpecInfoGatherer) initParamsCache() {
    93  	s.paramsCache.mutex.Lock()
    94  	defer s.paramsCache.mutex.Unlock()
    95  	s.specsCache.mutex.Lock()
    96  	s.paramsCache.staticParams = make(map[string]map[string]gauge.StepArg, 0)
    97  	s.paramsCache.dynamicParams = make(map[string]map[string]gauge.StepArg, 0)
    98  	for file, specDetail := range s.specsCache.specDetails {
    99  		s.updateParamCacheFromSpecs(file, specDetail)
   100  	}
   101  	s.specsCache.mutex.Unlock()
   102  	s.conceptsCache.mutex.Lock()
   103  	for file, concepts := range s.conceptsCache.concepts {
   104  		s.updateParamsCacheFromConcepts(file, concepts)
   105  	}
   106  	s.conceptsCache.mutex.Unlock()
   107  }
   108  
   109  func (s *SpecInfoGatherer) initSpecsCache() {
   110  	details := s.getParsedSpecs(getSpecFiles(s.SpecDirs))
   111  
   112  	s.specsCache.mutex.Lock()
   113  	defer s.specsCache.mutex.Unlock()
   114  
   115  	s.specsCache.specDetails = make(map[string]*SpecDetail, 0)
   116  
   117  	logger.APILog.Infof("Initializing specs cache with %d specs", len(details))
   118  	for _, d := range details {
   119  		logger.APILog.Debugf("Adding specs from %s", d.Spec.FileName)
   120  		s.addToSpecsCache(d.Spec.FileName, d)
   121  	}
   122  }
   123  
   124  func getSpecFiles(specs []string) []string {
   125  	var specFiles []string
   126  	for _, dir := range specs {
   127  		specFiles = append(specFiles, util.FindSpecFilesIn(dir)...)
   128  	}
   129  	return specFiles
   130  }
   131  
   132  func (s *SpecInfoGatherer) initConceptsCache() {
   133  	s.conceptsCache.mutex.Lock()
   134  	defer s.conceptsCache.mutex.Unlock()
   135  
   136  	parsedConcepts := s.getParsedConcepts()
   137  	s.conceptsCache.concepts = make(map[string][]*gauge.Concept, 0)
   138  	logger.APILog.Infof("Initializing concepts cache with %d concepts", len(parsedConcepts))
   139  	for _, concept := range parsedConcepts {
   140  		logger.APILog.Debug("Adding concepts from %s", concept.FileName)
   141  		s.addToConceptsCache(concept.FileName, concept)
   142  	}
   143  }
   144  
   145  func (s *SpecInfoGatherer) initStepsCache() {
   146  	s.stepsCache.mutex.Lock()
   147  	defer s.stepsCache.mutex.Unlock()
   148  
   149  	s.stepsCache.stepValues = make(map[string][]*gauge.StepValue, 0)
   150  	stepsFromSpecsMap := s.getStepsFromCachedSpecs()
   151  	stepsFromConceptsMap := s.getStepsFromCachedConcepts()
   152  
   153  	for filename, steps := range stepsFromConceptsMap {
   154  		s.addToStepsCache(filename, steps)
   155  	}
   156  	for filename, steps := range stepsFromSpecsMap {
   157  		s.addToStepsCache(filename, steps)
   158  	}
   159  	logger.APILog.Infof("Initializing steps cache with %d steps", len(stepsFromSpecsMap)+len(stepsFromConceptsMap))
   160  }
   161  
   162  func (s *SpecInfoGatherer) updateParamsCacheFromConcepts(file string, concepts []*gauge.Concept) {
   163  	s.paramsCache.staticParams[file] = make(map[string]gauge.StepArg, 0)
   164  	s.paramsCache.dynamicParams[file] = make(map[string]gauge.StepArg, 0)
   165  	for _, concept := range concepts {
   166  		s.addParamsFromSteps([]*gauge.Step{concept.ConceptStep}, file)
   167  		s.addParamsFromSteps(concept.ConceptStep.ConceptSteps, file)
   168  	}
   169  }
   170  
   171  func (s *SpecInfoGatherer) updateParamCacheFromSpecs(file string, specDetail *SpecDetail) {
   172  	s.paramsCache.staticParams[file] = make(map[string]gauge.StepArg, 0)
   173  	s.paramsCache.dynamicParams[file] = make(map[string]gauge.StepArg, 0)
   174  	s.addParamsFromSteps(specDetail.Spec.Contexts, file)
   175  	for _, sce := range specDetail.Spec.Scenarios {
   176  		s.addParamsFromSteps(sce.Steps, file)
   177  	}
   178  	s.addParamsFromSteps(specDetail.Spec.TearDownSteps, file)
   179  	if specDetail.Spec.DataTable.IsInitialized() {
   180  		for _, header := range specDetail.Spec.DataTable.Table.Headers {
   181  			s.paramsCache.dynamicParams[file][header] = gauge.StepArg{Value: header, ArgType: gauge.Dynamic}
   182  		}
   183  	}
   184  }
   185  
   186  func (s *SpecInfoGatherer) addParamsFromSteps(steps []*gauge.Step, file string) {
   187  	for _, step := range steps {
   188  		for _, arg := range step.Args {
   189  			if arg.ArgType == gauge.Static {
   190  				s.paramsCache.staticParams[file][arg.ArgValue()] = *arg
   191  			} else {
   192  				s.paramsCache.dynamicParams[file][arg.ArgValue()] = *arg
   193  			}
   194  		}
   195  	}
   196  }
   197  
   198  func (s *SpecInfoGatherer) addToSpecsCache(key string, value *SpecDetail) {
   199  	s.specsCache.specDetails[key] = value
   200  }
   201  
   202  func (s *SpecInfoGatherer) addToConceptsCache(key string, value *gauge.Concept) {
   203  	if s.conceptsCache.concepts[key] == nil {
   204  		s.conceptsCache.concepts[key] = make([]*gauge.Concept, 0)
   205  	}
   206  	s.conceptsCache.concepts[key] = append(s.conceptsCache.concepts[key], value)
   207  }
   208  
   209  func (s *SpecInfoGatherer) deleteFromConceptDictionary(file string) {
   210  	for _, c := range s.conceptsCache.concepts[file] {
   211  		delete(s.conceptDictionary.ConceptsMap, c.ConceptStep.Value)
   212  	}
   213  }
   214  
   215  func (s *SpecInfoGatherer) UpdateConceptCache(file, cptContent string) *parser.ParseResult {
   216  	s.deleteFromConceptDictionary(file)
   217  	cpts, res := new(parser.ConceptParser).Parse(cptContent, file)
   218  	if errs := parser.AddConcept(cpts, file, s.conceptDictionary); len(errs) > 0 {
   219  		res.ParseErrors = append(res.ParseErrors, errs...)
   220  	}
   221  	s.conceptsCache.concepts[file] = make([]*gauge.Concept, 0)
   222  	for _, concept := range s.conceptDictionary.ConceptsMap {
   223  		if file == concept.FileName {
   224  			s.addToConceptsCache(concept.FileName, concept)
   225  		}
   226  	}
   227  	return res
   228  }
   229  
   230  func (s *SpecInfoGatherer) addToStepsCache(fileName string, allSteps []*gauge.StepValue) {
   231  	s.stepsCache.stepValues[fileName] = allSteps
   232  }
   233  
   234  func (s *SpecInfoGatherer) getParsedSpecs(specFiles []string) []*SpecDetail {
   235  	if s.conceptDictionary == nil {
   236  		s.conceptDictionary = gauge.NewConceptDictionary()
   237  	}
   238  	parsedSpecs, parseResults := parser.ParseSpecFiles(specFiles, s.conceptDictionary, gauge.NewBuildErrors())
   239  	specs := make(map[string]*SpecDetail)
   240  
   241  	for _, spec := range parsedSpecs {
   242  		specs[spec.FileName] = &SpecDetail{Spec: spec}
   243  	}
   244  	for _, v := range parseResults {
   245  		_, ok := specs[v.FileName]
   246  		if !ok {
   247  			specs[v.FileName] = &SpecDetail{Spec: &gauge.Specification{FileName: v.FileName}, Errs: v.ParseErrors}
   248  		}
   249  	}
   250  	details := make([]*SpecDetail, 0)
   251  	for _, d := range specs {
   252  		details = append(details, d)
   253  	}
   254  	return details
   255  }
   256  
   257  func (s *SpecInfoGatherer) getParsedConcepts() map[string]*gauge.Concept {
   258  	var result *parser.ParseResult
   259  	s.conceptDictionary, result = parser.CreateConceptsDictionary()
   260  	handleParseFailures([]*parser.ParseResult{result})
   261  	return s.conceptDictionary.ConceptsMap
   262  }
   263  
   264  func (s *SpecInfoGatherer) getStepsFromCachedSpecs() map[string][]*gauge.StepValue {
   265  	s.specsCache.mutex.RLock()
   266  	defer s.specsCache.mutex.RUnlock()
   267  
   268  	var stepsFromSpecsMap = make(map[string][]*gauge.StepValue, 0)
   269  	for _, detail := range s.specsCache.specDetails {
   270  		stepsFromSpecsMap[detail.Spec.FileName] = append(stepsFromSpecsMap[detail.Spec.FileName], getStepsFromSpec(detail.Spec)...)
   271  	}
   272  	return stepsFromSpecsMap
   273  }
   274  
   275  func (s *SpecInfoGatherer) getStepsFromCachedConcepts() map[string][]*gauge.StepValue {
   276  	var stepsFromConceptMap = make(map[string][]*gauge.StepValue, 0)
   277  	s.conceptsCache.mutex.RLock()
   278  	defer s.conceptsCache.mutex.RUnlock()
   279  	for _, conceptList := range s.conceptsCache.concepts {
   280  		for _, concept := range conceptList {
   281  			stepsFromConceptMap[concept.FileName] = append(stepsFromConceptMap[concept.FileName], getStepsFromConcept(concept)...)
   282  		}
   283  	}
   284  	return stepsFromConceptMap
   285  }
   286  
   287  func (s *SpecInfoGatherer) OnSpecFileModify(file string) {
   288  	logger.APILog.Infof("Spec file added / modified: %s", file)
   289  
   290  	details := s.getParsedSpecs([]string{file})
   291  	s.specsCache.mutex.Lock()
   292  	s.addToSpecsCache(file, details[0])
   293  	s.specsCache.mutex.Unlock()
   294  
   295  	var steps []*gauge.StepValue
   296  	for _, step := range getStepsFromSpec(details[0].Spec) {
   297  		con := s.conceptDictionary.Search(step.StepValue)
   298  		if con == nil {
   299  			steps = append(steps, step)
   300  		}
   301  	}
   302  	s.stepsCache.mutex.Lock()
   303  	s.addToStepsCache(file, steps)
   304  	s.stepsCache.mutex.Unlock()
   305  
   306  	s.paramsCache.mutex.Lock()
   307  	defer s.paramsCache.mutex.Unlock()
   308  	s.updateParamCacheFromSpecs(file, details[0])
   309  }
   310  
   311  func (s *SpecInfoGatherer) OnConceptFileModify(file string) {
   312  	s.conceptsCache.mutex.Lock()
   313  	defer s.conceptsCache.mutex.Unlock()
   314  
   315  	logger.APILog.Infof("Concept file added / modified: %s", file)
   316  	s.deleteFromConceptDictionary(file)
   317  	concepts, parseErrors := parser.AddConcepts([]string{file}, s.conceptDictionary)
   318  	if len(parseErrors) > 0 {
   319  		res := &parser.ParseResult{}
   320  		res.ParseErrors = append(res.ParseErrors, parseErrors...)
   321  		res.Ok = false
   322  		handleParseFailures([]*parser.ParseResult{res})
   323  	}
   324  	s.conceptsCache.concepts[file] = make([]*gauge.Concept, 0)
   325  	for _, concept := range concepts {
   326  		c := gauge.Concept{ConceptStep: concept, FileName: file}
   327  		s.addToConceptsCache(file, &c)
   328  		stepsFromConcept := getStepsFromConcept(&c)
   329  		s.addToStepsCache(file, stepsFromConcept)
   330  	}
   331  	s.paramsCache.mutex.Lock()
   332  	defer s.paramsCache.mutex.Unlock()
   333  	s.updateParamsCacheFromConcepts(file, s.conceptsCache.concepts[file])
   334  }
   335  
   336  func (s *SpecInfoGatherer) onSpecFileRemove(file string) {
   337  	logger.APILog.Infof("Spec file removed: %s", file)
   338  	s.specsCache.mutex.Lock()
   339  	defer s.specsCache.mutex.Unlock()
   340  	delete(s.specsCache.specDetails, file)
   341  }
   342  
   343  func (s *SpecInfoGatherer) onConceptFileRemove(file string) {
   344  	logger.APILog.Infof("Concept file removed: %s", file)
   345  	s.conceptsCache.mutex.Lock()
   346  	defer s.conceptsCache.mutex.Unlock()
   347  	for _, c := range s.conceptsCache.concepts[file] {
   348  		delete(s.conceptDictionary.ConceptsMap, c.ConceptStep.Value)
   349  	}
   350  	delete(s.conceptsCache.concepts, file)
   351  }
   352  
   353  func (s *SpecInfoGatherer) onFileAdd(watcher *fsnotify.Watcher, file string) {
   354  	if util.IsDir(file) {
   355  		addDirToFileWatcher(watcher, file)
   356  	}
   357  	s.onFileModify(watcher, file)
   358  }
   359  
   360  func (s *SpecInfoGatherer) onFileModify(watcher *fsnotify.Watcher, file string) {
   361  	if util.IsSpec(file) {
   362  		s.OnSpecFileModify(file)
   363  	} else if util.IsConcept(file) {
   364  		s.OnConceptFileModify(file)
   365  	}
   366  }
   367  
   368  func (s *SpecInfoGatherer) onFileRemove(watcher *fsnotify.Watcher, file string) {
   369  	if util.IsSpec(file) {
   370  		s.onSpecFileRemove(file)
   371  	} else if util.IsConcept(file) {
   372  		s.onConceptFileRemove(file)
   373  	} else {
   374  		removeWatcherOn(watcher, file)
   375  	}
   376  }
   377  
   378  func (s *SpecInfoGatherer) onFileRename(watcher *fsnotify.Watcher, file string) {
   379  	s.onFileRemove(watcher, file)
   380  }
   381  
   382  func (s *SpecInfoGatherer) handleEvent(event fsnotify.Event, watcher *fsnotify.Watcher) {
   383  	s.waitGroup.Wait()
   384  
   385  	file, err := filepath.Abs(event.Name)
   386  	if err != nil {
   387  		logger.APILog.Errorf("Failed to get abs file path for %s: %s", event.Name, err)
   388  		return
   389  	}
   390  	if util.IsSpec(file) || util.IsConcept(file) || util.IsDir(file) {
   391  		switch event.Op {
   392  		case fsnotify.Create:
   393  			s.onFileAdd(watcher, file)
   394  		case fsnotify.Write:
   395  			s.onFileModify(watcher, file)
   396  		case fsnotify.Rename:
   397  			s.onFileRename(watcher, file)
   398  		case fsnotify.Remove:
   399  			s.onFileRemove(watcher, file)
   400  		}
   401  	}
   402  }
   403  
   404  func (s *SpecInfoGatherer) watchForFileChanges() {
   405  	s.waitGroup.Add(1)
   406  
   407  	watcher, err := fsnotify.NewWatcher()
   408  	if err != nil {
   409  		logger.APILog.Errorf("Error creating fileWatcher: %s", err)
   410  	}
   411  	defer watcher.Close()
   412  
   413  	done := make(chan bool)
   414  	go func() {
   415  		for {
   416  			select {
   417  			case event := <-watcher.Events:
   418  				s.handleEvent(event, watcher)
   419  			case err := <-watcher.Errors:
   420  				logger.APILog.Errorf("Error event while watching specs %s", err)
   421  			}
   422  		}
   423  	}()
   424  
   425  	var allDirsToWatch []string
   426  	var specDir string
   427  
   428  	for _, dir := range s.SpecDirs {
   429  		specDir = filepath.Join(config.ProjectRoot, dir)
   430  		allDirsToWatch = append(allDirsToWatch, specDir)
   431  		allDirsToWatch = append(allDirsToWatch, util.FindAllNestedDirs(specDir)...)
   432  	}
   433  
   434  	for _, dir := range allDirsToWatch {
   435  		addDirToFileWatcher(watcher, dir)
   436  	}
   437  	s.waitGroup.Done()
   438  	<-done
   439  }
   440  
   441  // GetAvailableSpecs returns the list of all the specs in the gauge project
   442  func (s *SpecInfoGatherer) GetAvailableSpecDetails(specs []string) []*SpecDetail {
   443  	if len(specs) < 1 {
   444  		specs = []string{common.SpecsDirectoryName}
   445  	}
   446  	specFiles := getSpecFiles(specs)
   447  	s.specsCache.mutex.RLock()
   448  	defer s.specsCache.mutex.RUnlock()
   449  	var details []*SpecDetail
   450  	for _, f := range specFiles {
   451  		if d, ok := s.specsCache.specDetails[f]; ok {
   452  			details = append(details, d)
   453  		}
   454  	}
   455  	return details
   456  }
   457  
   458  // Steps returns the list of all the steps in the gauge project
   459  func (s *SpecInfoGatherer) Steps() []*gauge.StepValue {
   460  	s.stepsCache.mutex.RLock()
   461  	defer s.stepsCache.mutex.RUnlock()
   462  	sValues := make(map[string]*gauge.StepValue)
   463  	for _, stepValues := range s.stepsCache.stepValues {
   464  		for _, sv := range stepValues {
   465  			sValues[sv.StepValue] = sv
   466  		}
   467  	}
   468  	var steps []*gauge.StepValue
   469  	for _, sv := range sValues {
   470  		steps = append(steps, sv)
   471  	}
   472  	return steps
   473  }
   474  
   475  // Steps returns the list of all the steps in the gauge project
   476  func (s *SpecInfoGatherer) Params(filePath string, argType gauge.ArgType) []gauge.StepArg {
   477  	s.paramsCache.mutex.RLock()
   478  	defer s.paramsCache.mutex.RUnlock()
   479  	var params []gauge.StepArg
   480  	if argType == gauge.Static {
   481  		for _, param := range s.paramsCache.staticParams[filePath] {
   482  			params = append(params, param)
   483  		}
   484  	} else {
   485  		for _, param := range s.paramsCache.dynamicParams[filePath] {
   486  			params = append(params, param)
   487  		}
   488  	}
   489  	return params
   490  }
   491  
   492  // Concepts returns an array containing information about all the concepts present in the Gauge project
   493  func (s *SpecInfoGatherer) Concepts() []*gauge_messages.ConceptInfo {
   494  	var conceptInfos []*gauge_messages.ConceptInfo
   495  	s.conceptsCache.mutex.RLock()
   496  	defer s.conceptsCache.mutex.RUnlock()
   497  	for _, conceptList := range s.conceptsCache.concepts {
   498  		for _, concept := range conceptList {
   499  			stepValue := parser.CreateStepValue(concept.ConceptStep)
   500  			conceptInfos = append(conceptInfos, &gauge_messages.ConceptInfo{StepValue: gauge.ConvertToProtoStepValue(&stepValue), Filepath: concept.FileName, LineNumber: int32(concept.ConceptStep.LineNo)})
   501  		}
   502  	}
   503  	return conceptInfos
   504  }
   505  
   506  func (s *SpecInfoGatherer) GetConceptDictionary() *gauge.ConceptDictionary {
   507  	return s.conceptDictionary
   508  }
   509  
   510  // SearchConceptDictionary searches for a concept in concept dictionary
   511  func (s *SpecInfoGatherer) SearchConceptDictionary(stepValue string) *gauge.Concept {
   512  	return s.conceptDictionary.Search(stepValue)
   513  }
   514  
   515  func getStepsFromSpec(spec *gauge.Specification) []*gauge.StepValue {
   516  	stepValues := getParsedStepValues(spec.Contexts)
   517  	for _, scenario := range spec.Scenarios {
   518  		stepValues = append(stepValues, getParsedStepValues(scenario.Steps)...)
   519  	}
   520  	return stepValues
   521  }
   522  
   523  func getStepsFromConcept(concept *gauge.Concept) []*gauge.StepValue {
   524  	return getParsedStepValues(concept.ConceptStep.ConceptSteps)
   525  }
   526  
   527  func getParsedStepValues(steps []*gauge.Step) []*gauge.StepValue {
   528  	var stepValues []*gauge.StepValue
   529  	for _, step := range steps {
   530  		if !step.IsConcept {
   531  			stepValue := parser.CreateStepValue(step)
   532  			stepValues = append(stepValues, &stepValue)
   533  		}
   534  	}
   535  	return stepValues
   536  }
   537  
   538  func handleParseFailures(parseResults []*parser.ParseResult) {
   539  	for _, result := range parseResults {
   540  		if !result.Ok {
   541  			logger.APILog.Errorf("Parse failure: %s", result.Errors())
   542  		}
   543  	}
   544  }
   545  
   546  func addDirToFileWatcher(watcher *fsnotify.Watcher, dir string) {
   547  	err := watcher.Add(dir)
   548  	if err != nil {
   549  		logger.APILog.Errorf("Unable to add directory %v to file watcher: %s", dir, err)
   550  	} else {
   551  		logger.APILog.Infof("Watching directory: %s", dir)
   552  		files, _ := ioutil.ReadDir(dir)
   553  		logger.APILog.Debugf("Found %d files", len(files))
   554  	}
   555  }
   556  
   557  func removeWatcherOn(watcher *fsnotify.Watcher, path string) {
   558  	logger.APILog.Infof("Removing watcher on : %s", path)
   559  	watcher.Remove(path)
   560  }