github.com/getgauge/gauge@v1.6.9/api/infoGatherer/specDetails.go (about)

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