github.com/mattdotmatt/gauge@v0.3.2-0.20160421115137-425a4cdccb62/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/getgauge/gauge/config"
    26  	"github.com/getgauge/gauge/gauge"
    27  	"github.com/getgauge/gauge/gauge_messages"
    28  	"github.com/getgauge/gauge/logger"
    29  	"github.com/getgauge/gauge/parser"
    30  	"github.com/getgauge/gauge/util"
    31  	"github.com/golang/protobuf/proto"
    32  	fsnotify "gopkg.in/fsnotify.v1"
    33  )
    34  
    35  // SpecInfoGatherer contains the caches for specs, concepts, and steps
    36  type SpecInfoGatherer struct {
    37  	waitGroup         sync.WaitGroup
    38  	mutex             sync.Mutex
    39  	conceptDictionary *gauge.ConceptDictionary
    40  	specsCache        map[string][]*gauge.Specification
    41  	conceptsCache     map[string][]*gauge.Concept
    42  	stepsCache        map[string]*gauge.StepValue
    43  	SpecDirs          []string
    44  }
    45  
    46  // MakeListOfAvailableSteps initializes all the SpecInfoGatherer caches
    47  func (s *SpecInfoGatherer) MakeListOfAvailableSteps() {
    48  	go s.watchForFileChanges()
    49  	s.waitGroup.Wait()
    50  
    51  	// Concepts parsed first because we need to create a concept dictionary that spec parsing can use
    52  	s.waitGroup.Add(3)
    53  	s.initConceptsCache()
    54  	s.initSpecsCache()
    55  	s.initStepsCache()
    56  }
    57  
    58  func (s *SpecInfoGatherer) initSpecsCache() {
    59  	defer s.waitGroup.Done()
    60  
    61  	var specFiles []string
    62  	s.specsCache = make(map[string][]*gauge.Specification, 0)
    63  
    64  	for _, dir := range s.SpecDirs {
    65  		specFiles = append(specFiles, util.FindSpecFilesIn(filepath.Join(config.ProjectRoot, dir))...)
    66  	}
    67  
    68  	parsedSpecs := s.getParsedSpecs(specFiles)
    69  
    70  	logger.APILog.Info("Initializing specs cache with %d specs", len(parsedSpecs))
    71  	for _, spec := range parsedSpecs {
    72  		logger.APILog.Debug("Adding specs from %s", spec.FileName)
    73  		s.addToSpecsCache(spec.FileName, spec)
    74  	}
    75  }
    76  
    77  func (s *SpecInfoGatherer) initConceptsCache() {
    78  	defer s.waitGroup.Done()
    79  
    80  	s.conceptsCache = make(map[string][]*gauge.Concept, 0)
    81  	parsedConcepts := s.getParsedConcepts()
    82  
    83  	logger.APILog.Info("Initializing concepts cache with %d concepts", len(parsedConcepts))
    84  	for _, concept := range parsedConcepts {
    85  		logger.APILog.Debug("Adding concepts from %s", concept.FileName)
    86  		s.addToConceptsCache(concept.FileName, concept)
    87  	}
    88  }
    89  
    90  func (s *SpecInfoGatherer) initStepsCache() {
    91  	defer s.waitGroup.Done()
    92  
    93  	s.stepsCache = make(map[string]*gauge.StepValue, 0)
    94  	stepsFromSpecs := s.getStepsFromCachedSpecs()
    95  	stepsFromConcepts := s.getStepsFromCachedConcepts()
    96  
    97  	allSteps := append(stepsFromSpecs, stepsFromConcepts...)
    98  
    99  	logger.APILog.Info("Initializing steps cache with %d steps", len(allSteps))
   100  	s.addToStepsCache(allSteps)
   101  }
   102  
   103  func (s *SpecInfoGatherer) addToSpecsCache(key string, value *gauge.Specification) {
   104  	s.mutex.Lock()
   105  	if s.specsCache[key] == nil {
   106  		s.specsCache[key] = make([]*gauge.Specification, 0)
   107  	}
   108  	s.specsCache[key] = append(s.specsCache[key], value)
   109  	s.mutex.Unlock()
   110  }
   111  
   112  func (s *SpecInfoGatherer) addToConceptsCache(key string, value *gauge.Concept) {
   113  	s.mutex.Lock()
   114  	if s.conceptsCache[key] == nil {
   115  		s.conceptsCache[key] = make([]*gauge.Concept, 0)
   116  	}
   117  	s.conceptsCache[key] = append(s.conceptsCache[key], value)
   118  	s.mutex.Unlock()
   119  }
   120  
   121  func (s *SpecInfoGatherer) addToStepsCache(allSteps []*gauge.StepValue) {
   122  	s.mutex.Lock()
   123  	for _, step := range allSteps {
   124  		if _, ok := s.stepsCache[step.StepValue]; !ok {
   125  			s.stepsCache[step.StepValue] = step
   126  		}
   127  	}
   128  	s.mutex.Unlock()
   129  }
   130  
   131  func (s *SpecInfoGatherer) getParsedSpecs(specFiles []string) []*gauge.Specification {
   132  	if s.conceptDictionary == nil {
   133  		s.conceptDictionary = gauge.NewConceptDictionary()
   134  	}
   135  	parsedSpecs, parseResults := parser.ParseSpecFiles(specFiles, s.conceptDictionary)
   136  	handleParseFailures(parseResults)
   137  	return parsedSpecs
   138  }
   139  
   140  func (s *SpecInfoGatherer) getParsedConcepts() map[string]*gauge.Concept {
   141  	var result *parser.ParseResult
   142  	s.conceptDictionary, result = parser.CreateConceptsDictionary(true, s.SpecDirs)
   143  	handleParseFailures([]*parser.ParseResult{result})
   144  	return s.conceptDictionary.ConceptsMap
   145  }
   146  
   147  func (s *SpecInfoGatherer) getStepsFromCachedSpecs() []*gauge.StepValue {
   148  	var stepValues []*gauge.StepValue
   149  	s.mutex.Lock()
   150  	for _, specList := range s.specsCache {
   151  		for _, spec := range specList {
   152  			stepValues = append(stepValues, getStepsFromSpec(spec)...)
   153  		}
   154  	}
   155  	s.mutex.Unlock()
   156  	return stepValues
   157  }
   158  
   159  func (s *SpecInfoGatherer) getStepsFromCachedConcepts() []*gauge.StepValue {
   160  	var stepValues []*gauge.StepValue
   161  	s.mutex.Lock()
   162  	for _, conceptList := range s.conceptsCache {
   163  		for _, concept := range conceptList {
   164  			stepValues = append(stepValues, getStepsFromConcept(concept)...)
   165  		}
   166  	}
   167  	s.mutex.Unlock()
   168  	return stepValues
   169  }
   170  
   171  func (s *SpecInfoGatherer) onSpecFileModify(file string) {
   172  	s.waitGroup.Add(1)
   173  	defer s.waitGroup.Done()
   174  
   175  	logger.APILog.Info("Spec file added / modified: %s", file)
   176  	parsedSpecs := s.getParsedSpecs([]string{file})
   177  	if len(parsedSpecs) != 0 {
   178  		parsedSpec := parsedSpecs[0]
   179  		s.addToSpecsCache(file, parsedSpec)
   180  		stepsFromSpec := getStepsFromSpec(parsedSpec)
   181  		s.addToStepsCache(stepsFromSpec)
   182  	}
   183  }
   184  
   185  func (s *SpecInfoGatherer) onConceptFileModify(file string) {
   186  	s.waitGroup.Add(1)
   187  	defer s.waitGroup.Done()
   188  
   189  	logger.APILog.Info("Concept file added / modified: %s", file)
   190  	conceptParser := new(parser.ConceptParser)
   191  	concepts, parseResults := conceptParser.ParseFile(file)
   192  	if parseResults != nil && parseResults.Error != nil {
   193  		logger.APILog.Error("Error parsing concepts: ", parseResults.Error)
   194  		return
   195  	}
   196  
   197  	for _, concept := range concepts {
   198  		c := gauge.Concept{ConceptStep: concept, FileName: file}
   199  		s.addToConceptsCache(file, &c)
   200  		stepsFromConcept := getStepsFromConcept(&c)
   201  		s.addToStepsCache(stepsFromConcept)
   202  	}
   203  }
   204  
   205  func (s *SpecInfoGatherer) onSpecFileRemove(file string) {
   206  	s.waitGroup.Add(1)
   207  	defer s.waitGroup.Done()
   208  
   209  	logger.APILog.Info("Spec file removed: %s", file)
   210  	s.mutex.Lock()
   211  	delete(s.specsCache, file)
   212  	s.mutex.Unlock()
   213  }
   214  
   215  func (s *SpecInfoGatherer) onConceptFileRemove(file string) {
   216  	s.waitGroup.Add(1)
   217  	defer s.waitGroup.Done()
   218  
   219  	logger.APILog.Info("Concept file removed: %s", file)
   220  	s.mutex.Lock()
   221  	delete(s.conceptsCache, file)
   222  	s.mutex.Unlock()
   223  }
   224  
   225  func (s *SpecInfoGatherer) onFileAdd(watcher *fsnotify.Watcher, file string) {
   226  	if util.IsDir(file) {
   227  		addDirToFileWatcher(watcher, file)
   228  	}
   229  	s.onFileModify(watcher, file)
   230  }
   231  
   232  func (s *SpecInfoGatherer) onFileModify(watcher *fsnotify.Watcher, file string) {
   233  	if util.IsSpec(file) {
   234  		s.onSpecFileModify(file)
   235  	} else if util.IsConcept(file) {
   236  		s.onConceptFileModify(file)
   237  	}
   238  }
   239  
   240  func (s *SpecInfoGatherer) onFileRemove(watcher *fsnotify.Watcher, file string) {
   241  	if util.IsSpec(file) {
   242  		s.onSpecFileRemove(file)
   243  	} else if util.IsConcept(file) {
   244  		s.onConceptFileRemove(file)
   245  	} else {
   246  		removeWatcherOn(watcher, file)
   247  	}
   248  }
   249  
   250  func (s *SpecInfoGatherer) onFileRename(watcher *fsnotify.Watcher, file string) {
   251  	s.onFileRemove(watcher, file)
   252  }
   253  
   254  func (s *SpecInfoGatherer) handleEvent(event fsnotify.Event, watcher *fsnotify.Watcher) {
   255  	s.waitGroup.Wait()
   256  
   257  	file, err := filepath.Abs(event.Name)
   258  	if err != nil {
   259  		logger.APILog.Error("Failed to get abs file path for %s: %s", event.Name, err)
   260  		return
   261  	}
   262  	if util.IsSpec(file) || util.IsConcept(file) || util.IsDir(file) {
   263  		switch event.Op {
   264  		case fsnotify.Create:
   265  			s.onFileAdd(watcher, file)
   266  		case fsnotify.Write:
   267  			s.onFileModify(watcher, file)
   268  		case fsnotify.Rename:
   269  			s.onFileRename(watcher, file)
   270  		case fsnotify.Remove:
   271  			s.onFileRemove(watcher, file)
   272  		}
   273  	}
   274  }
   275  
   276  func (s *SpecInfoGatherer) watchForFileChanges() {
   277  	s.waitGroup.Add(1)
   278  
   279  	watcher, err := fsnotify.NewWatcher()
   280  	if err != nil {
   281  		logger.APILog.Error("Error creating fileWatcher: %s", err)
   282  	}
   283  	defer watcher.Close()
   284  
   285  	done := make(chan bool)
   286  	go func() {
   287  		for {
   288  			select {
   289  			case event := <-watcher.Events:
   290  				s.handleEvent(event, watcher)
   291  			case err := <-watcher.Errors:
   292  				logger.APILog.Error("Error event while watching specs", err)
   293  			}
   294  		}
   295  	}()
   296  
   297  	var allDirsToWatch []string
   298  	var specDir string
   299  
   300  	for _, dir := range s.SpecDirs {
   301  		specDir = filepath.Join(config.ProjectRoot, dir)
   302  		allDirsToWatch = append(allDirsToWatch, specDir)
   303  		allDirsToWatch = append(allDirsToWatch, util.FindAllNestedDirs(specDir)...)
   304  	}
   305  
   306  	for _, dir := range allDirsToWatch {
   307  		addDirToFileWatcher(watcher, dir)
   308  	}
   309  	s.waitGroup.Done()
   310  	<-done
   311  }
   312  
   313  // GetAvailableSpecs returns the list of all the specs in the gauge project
   314  func (s *SpecInfoGatherer) GetAvailableSpecs() []*gauge.Specification {
   315  	s.waitGroup.Wait()
   316  
   317  	var allSpecs []*gauge.Specification
   318  	s.mutex.Lock()
   319  	for _, specs := range s.specsCache {
   320  		allSpecs = append(allSpecs, specs...)
   321  	}
   322  	s.mutex.Unlock()
   323  	return allSpecs
   324  }
   325  
   326  // GetAvailableSteps returns the list of all the steps in the gauge project
   327  func (s *SpecInfoGatherer) GetAvailableSteps() []*gauge.StepValue {
   328  	s.waitGroup.Wait()
   329  
   330  	var steps []*gauge.StepValue
   331  	s.mutex.Lock()
   332  	for _, stepValue := range s.stepsCache {
   333  		steps = append(steps, stepValue)
   334  	}
   335  	s.mutex.Unlock()
   336  	return steps
   337  }
   338  
   339  // GetConceptInfos returns an array containing information about all the concepts present in the Gauge project
   340  func (s *SpecInfoGatherer) GetConceptInfos() []*gauge_messages.ConceptInfo {
   341  	s.waitGroup.Wait()
   342  
   343  	var conceptInfos []*gauge_messages.ConceptInfo
   344  	s.mutex.Lock()
   345  	for _, conceptList := range s.conceptsCache {
   346  		for _, concept := range conceptList {
   347  			stepValue := parser.CreateStepValue(concept.ConceptStep)
   348  			conceptInfos = append(conceptInfos, &gauge_messages.ConceptInfo{StepValue: gauge.ConvertToProtoStepValue(&stepValue), Filepath: proto.String(concept.FileName), LineNumber: proto.Int(concept.ConceptStep.LineNo)})
   349  		}
   350  	}
   351  	s.mutex.Unlock()
   352  	return conceptInfos
   353  }
   354  
   355  func getStepsFromSpec(spec *gauge.Specification) []*gauge.StepValue {
   356  	stepValues := getParsedStepValues(spec.Contexts)
   357  	for _, scenario := range spec.Scenarios {
   358  		stepValues = append(stepValues, getParsedStepValues(scenario.Steps)...)
   359  	}
   360  	return stepValues
   361  }
   362  
   363  func getStepsFromConcept(concept *gauge.Concept) []*gauge.StepValue {
   364  	return getParsedStepValues(concept.ConceptStep.ConceptSteps)
   365  }
   366  
   367  func getParsedStepValues(steps []*gauge.Step) []*gauge.StepValue {
   368  	var stepValues []*gauge.StepValue
   369  	for _, step := range steps {
   370  		if !step.IsConcept {
   371  			stepValue := parser.CreateStepValue(step)
   372  			stepValues = append(stepValues, &stepValue)
   373  		}
   374  	}
   375  	return stepValues
   376  }
   377  
   378  func handleParseFailures(parseResults []*parser.ParseResult) {
   379  	for _, result := range parseResults {
   380  		if !result.Ok {
   381  			logger.APILog.Error("Spec Parse failure: %s", result.Error())
   382  		}
   383  	}
   384  }
   385  
   386  func addDirToFileWatcher(watcher *fsnotify.Watcher, dir string) {
   387  	err := watcher.Add(dir)
   388  	if err != nil {
   389  		logger.APILog.Error("Unable to add directory %v to file watcher: %s", dir, err)
   390  	} else {
   391  		logger.APILog.Info("Watching directory: %s", dir)
   392  		files, _ := ioutil.ReadDir(dir)
   393  		logger.APILog.Debug("Found %d files", len(files))
   394  	}
   395  }
   396  
   397  func removeWatcherOn(watcher *fsnotify.Watcher, path string) {
   398  	logger.APILog.Info("Removing watcher on : %s", path)
   399  	watcher.Remove(path)
   400  }