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 }