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 }