github.com/getgauge/gauge@v1.6.9/parser/parse.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 /* 8 Package parser parses all the specs in the list of directories given and also de-duplicates all specs passed through `specDirs` before parsing specs. 9 10 Gets all the specs files in the given directory and generates token for each spec file. 11 While parsing a concept file, concepts are inlined i.e. concept in the spec file is replaced with steps that concept has in the concept file. 12 While creating a specification file parser applies the converter functions. 13 Parsing a spec file gives a specification with parseresult. ParseResult contains ParseErrors, CriticalErrors, Warnings and FileName 14 15 Errors can be generated, While 16 - Generating tokens 17 - Applying converters 18 - After Applying converters 19 20 If a parse error is found in a spec, only that spec is ignored and others will continue execution. 21 This doesn't invoke the language runner. 22 Eg : Multiple spec headings found in same file. 23 Scenario should be defined after the spec heading. 24 25 Critical error : 26 Circular reference of concepts - Doesn't parse specs becz it goes in recursion and crashes 27 */ 28 package parser 29 30 import ( 31 "strings" 32 "sync" 33 34 "regexp" 35 "strconv" 36 37 "github.com/getgauge/common" 38 "github.com/getgauge/gauge/filter" 39 "github.com/getgauge/gauge/gauge" 40 "github.com/getgauge/gauge/logger" 41 "github.com/getgauge/gauge/order" 42 "github.com/getgauge/gauge/util" 43 ) 44 45 // ParseSpecFiles gets all the spec files and parse each spec file. 46 // Generates specifications and parse results. 47 // TODO: Use single channel instead of one for spec and another for result, so that mapping is consistent 48 49 type parseInfo struct { 50 parseResult *ParseResult 51 spec *gauge.Specification 52 } 53 54 func newParseInfo(spec *gauge.Specification, pr *ParseResult) *parseInfo { 55 return &parseInfo{spec: spec, parseResult: pr} 56 } 57 58 func parse(wg *sync.WaitGroup, sfc *specFileCollection, cpt *gauge.ConceptDictionary, piChan chan *parseInfo) { 59 defer wg.Done() 60 for { 61 if s, err := sfc.Next(); err == nil { 62 piChan <- newParseInfo(parseSpec(s, cpt)) 63 } else { 64 return 65 } 66 } 67 } 68 69 func parseSpecFiles(sfc *specFileCollection, conceptDictionary *gauge.ConceptDictionary, piChan chan *parseInfo, limit int) { 70 wg := &sync.WaitGroup{} 71 for i := 0; i < limit; i++ { 72 wg.Add(1) 73 go parse(wg, sfc, conceptDictionary, piChan) 74 } 75 wg.Wait() 76 close(piChan) 77 } 78 79 func ParseSpecFiles(specFiles []string, conceptDictionary *gauge.ConceptDictionary, buildErrors *gauge.BuildErrors) ([]*gauge.Specification, []*ParseResult) { 80 sfc := NewSpecFileCollection(specFiles) 81 piChan := make(chan *parseInfo) 82 limit := len(specFiles) 83 rLimit, e := util.RLimit() 84 if e == nil && rLimit < limit { 85 logger.Debugf(true, "No of specifcations %d is higher than Max no of open file descriptors %d.\n"+ 86 "Starting %d routines for parallel parsing.", limit, rLimit, rLimit/2) 87 limit = rLimit / 2 88 } 89 go parseSpecFiles(sfc, conceptDictionary, piChan, limit) 90 var parseResults []*ParseResult 91 var specs []*gauge.Specification 92 for r := range piChan { 93 if r.spec != nil { 94 specs = append(specs, r.spec) 95 var parseErrs []error 96 for _, e := range r.parseResult.ParseErrors { 97 parseErrs = append(parseErrs, e) 98 } 99 if len(parseErrs) != 0 { 100 buildErrors.SpecErrs[r.spec] = parseErrs 101 } 102 } 103 parseResults = append(parseResults, r.parseResult) 104 } 105 return specs, parseResults 106 } 107 108 // ParseSpecs parses specs in the give directory and gives specification and pass/fail status, used in validation. 109 func ParseSpecs(specsToParse []string, conceptsDictionary *gauge.ConceptDictionary, buildErrors *gauge.BuildErrors) ([]*gauge.Specification, bool) { 110 specs, failed := parseSpecsInDirs(conceptsDictionary, specsToParse, buildErrors) 111 specsToExecute := order.Sort(filter.FilterSpecs(specs)) 112 return specsToExecute, failed 113 } 114 115 // ParseConcepts creates concept dictionary and concept parse result. 116 func ParseConcepts() (*gauge.ConceptDictionary, *ParseResult, error) { 117 logger.Debug(true, "Started concepts parsing.") 118 conceptsDictionary, conceptParseResult, err := CreateConceptsDictionary() 119 if err != nil { 120 return nil, nil, err 121 } 122 HandleParseResult(conceptParseResult) 123 logger.Debugf(true, "%d concepts parsing completed.", len(conceptsDictionary.ConceptsMap)) 124 return conceptsDictionary, conceptParseResult, nil 125 } 126 127 func parseSpec(specFile string, conceptDictionary *gauge.ConceptDictionary) (*gauge.Specification, *ParseResult) { 128 specFileContent, err := common.ReadFileContents(specFile) 129 if err != nil { 130 return nil, &ParseResult{ParseErrors: []ParseError{ParseError{FileName: specFile, Message: err.Error()}}, Ok: false} 131 } 132 spec, parseResult, err := new(SpecParser).Parse(specFileContent, conceptDictionary, specFile) 133 if err != nil { 134 logger.Fatal(true, err.Error()) 135 } 136 return spec, parseResult 137 } 138 139 type specFile struct { 140 filePath string 141 indices []int 142 } 143 144 // parseSpecsInDirs parses all the specs in list of dirs given. 145 // It also de-duplicates all specs passed through `specDirs` before parsing specs. 146 func parseSpecsInDirs(conceptDictionary *gauge.ConceptDictionary, specDirs []string, buildErrors *gauge.BuildErrors) ([]*gauge.Specification, bool) { 147 passed := true 148 givenSpecs, specFiles := getAllSpecFiles(specDirs) 149 var specs []*gauge.Specification 150 var specParseResults []*ParseResult 151 allSpecs := make([]*gauge.Specification, len(specFiles)) 152 logger.Debug(true, "Started specifications parsing.") 153 specs, specParseResults = ParseSpecFiles(givenSpecs, conceptDictionary, buildErrors) 154 passed = !HandleParseResult(specParseResults...) && passed 155 logger.Debugf(true, "%d specifications parsing completed.", len(specFiles)) 156 for _, spec := range specs { 157 i, _ := getIndexFor(specFiles, spec.FileName) 158 specFile := specFiles[i] 159 if len(specFile.indices) > 0 { 160 s, _ := spec.Filter(filter.NewScenarioFilterBasedOnSpan(specFile.indices)) 161 allSpecs[i] = s 162 } else { 163 allSpecs[i] = spec 164 } 165 } 166 return allSpecs, !passed 167 } 168 169 func getAllSpecFiles(specDirs []string) (givenSpecs []string, specFiles []*specFile) { 170 for _, specSource := range specDirs { 171 if isIndexedSpec(specSource) { 172 var specName string 173 specName, index := getIndexedSpecName(specSource) 174 files := util.GetSpecFiles([]string{specName}) 175 if len(files) < 1 { 176 continue 177 } 178 specificationFile, created := addSpecFile(&specFiles, files[0]) 179 if created || len(specificationFile.indices) > 0 { 180 specificationFile.indices = append(specificationFile.indices, index) 181 } 182 givenSpecs = append(givenSpecs, files[0]) 183 } else { 184 files := util.GetSpecFiles([]string{specSource}) 185 for _, file := range files { 186 specificationFile, _ := addSpecFile(&specFiles, file) 187 specificationFile.indices = specificationFile.indices[0:0] 188 } 189 givenSpecs = append(givenSpecs, files...) 190 } 191 } 192 return 193 } 194 195 func addSpecFile(specFiles *[]*specFile, file string) (*specFile, bool) { 196 i, exists := getIndexFor(*specFiles, file) 197 if !exists { 198 specificationFile := &specFile{filePath: file} 199 *specFiles = append(*specFiles, specificationFile) 200 return specificationFile, true 201 } 202 return (*specFiles)[i], false 203 } 204 205 func getIndexFor(files []*specFile, file string) (int, bool) { 206 for index, f := range files { 207 if f.filePath == file { 208 return index, true 209 } 210 } 211 return -1, false 212 } 213 214 func isIndexedSpec(specSource string) bool { 215 re := regexp.MustCompile(`(?i).(spec|md):[0-9]+$`) 216 index := re.FindStringIndex(specSource) 217 if index != nil { 218 return index[0] != 0 219 } 220 return false 221 } 222 223 func getIndexedSpecName(indexedSpec string) (string, int) { 224 index := getIndex(indexedSpec) 225 specName := indexedSpec[:index] 226 scenarioNum := indexedSpec[index+1:] 227 scenarioNumber, _ := strconv.Atoi(scenarioNum) 228 return specName, scenarioNumber 229 } 230 231 func getIndex(specSource string) int { 232 re, _ := regexp.Compile(":[0-9]+$") 233 index := re.FindStringSubmatchIndex(specSource) 234 if index != nil { 235 return index[0] 236 } 237 return 0 238 } 239 240 // ExtractStepValueAndParams parses a stepText string into a StepValue struct 241 func ExtractStepValueAndParams(stepText string, hasInlineTable bool) (*gauge.StepValue, error) { 242 stepValueWithPlaceHolders, args, err := processStepText(stepText) 243 if err != nil { 244 return nil, err 245 } 246 247 extractedStepValue, _ := extractStepValueAndParameterTypes(stepValueWithPlaceHolders) 248 if hasInlineTable { 249 extractedStepValue += " " + gauge.ParameterPlaceholder 250 args = append(args, string(gauge.TableArg)) 251 } 252 parameterizedStepValue := getParameterizeStepValue(extractedStepValue, args) 253 254 return &gauge.StepValue{Args: args, StepValue: extractedStepValue, ParameterizedStepValue: parameterizedStepValue}, nil 255 256 } 257 258 // CreateStepValue converts a Step to StepValue 259 func CreateStepValue(step *gauge.Step) gauge.StepValue { 260 stepValue := gauge.StepValue{StepValue: step.Value} 261 args := make([]string, 0) 262 for _, arg := range step.Args { 263 args = append(args, arg.ArgValue()) 264 } 265 stepValue.Args = args 266 stepValue.ParameterizedStepValue = getParameterizeStepValue(stepValue.StepValue, args) 267 return stepValue 268 } 269 270 func getParameterizeStepValue(stepValue string, params []string) string { 271 for _, param := range params { 272 stepValue = strings.Replace(stepValue, gauge.ParameterPlaceholder, "<"+param+">", 1) 273 } 274 return stepValue 275 } 276 277 // HandleParseResult collates list of parse result and determines if gauge has to break flow. 278 func HandleParseResult(results ...*ParseResult) bool { 279 var failed = false 280 for _, result := range results { 281 if !result.Ok { 282 for _, err := range result.Errors() { 283 logger.Error(true, err) 284 } 285 failed = true 286 } 287 if result.Warnings != nil { 288 for _, warning := range result.Warnings { 289 logger.Warningf(true, "[ParseWarning] %s", warning) 290 } 291 } 292 } 293 return failed 294 }