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 }