github.com/ezbuy/gauge@v0.9.4-0.20171013092048-7ac5bd3931cd/parser/conceptParser.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 parser 19 20 import ( 21 "fmt" 22 "strings" 23 24 "bytes" 25 26 "github.com/getgauge/common" 27 "github.com/getgauge/gauge/gauge" 28 "github.com/getgauge/gauge/logger" 29 "github.com/getgauge/gauge/util" 30 ) 31 32 type ConceptParser struct { 33 currentState int 34 currentConcept *gauge.Step 35 } 36 37 //concept file can have multiple concept headings 38 func (parser *ConceptParser) Parse(text, fileName string) ([]*gauge.Step, *ParseResult) { 39 defer parser.resetState() 40 41 specParser := new(SpecParser) 42 tokens, errs := specParser.GenerateTokens(text, fileName) 43 concepts, res := parser.createConcepts(tokens, fileName) 44 return concepts, &ParseResult{ParseErrors: append(errs, res.ParseErrors...), Warnings: res.Warnings} 45 } 46 47 func (parser *ConceptParser) ParseFile(file string) ([]*gauge.Step, *ParseResult) { 48 fileText, fileReadErr := common.ReadFileContents(file) 49 if fileReadErr != nil { 50 return nil, &ParseResult{ParseErrors: []ParseError{{Message: fmt.Sprintf("failed to read concept file %s", file)}}} 51 } 52 return parser.Parse(fileText, file) 53 } 54 55 func (parser *ConceptParser) resetState() { 56 parser.currentState = initial 57 parser.currentConcept = nil 58 } 59 60 func (parser *ConceptParser) createConcepts(tokens []*Token, fileName string) ([]*gauge.Step, *ParseResult) { 61 parser.currentState = initial 62 var concepts []*gauge.Step 63 parseRes := &ParseResult{ParseErrors: make([]ParseError, 0)} 64 var preComments []*gauge.Comment 65 addPreComments := false 66 for _, token := range tokens { 67 if parser.isConceptHeading(token) { 68 if isInState(parser.currentState, conceptScope, stepScope) { 69 if len(parser.currentConcept.ConceptSteps) < 1 { 70 parseRes.ParseErrors = append(parseRes.ParseErrors, ParseError{FileName: fileName, LineNo: parser.currentConcept.LineNo, Message: "Concept should have atleast one step", LineText: parser.currentConcept.LineText}) 71 continue 72 } 73 concepts = append(concepts, parser.currentConcept) 74 } 75 var res *ParseResult 76 parser.currentConcept, res = parser.processConceptHeading(token, fileName) 77 parser.currentState = initial 78 if len(res.ParseErrors) > 0 { 79 parseRes.ParseErrors = append(parseRes.ParseErrors, res.ParseErrors...) 80 continue 81 } 82 if addPreComments { 83 parser.currentConcept.PreComments = preComments 84 addPreComments = false 85 } 86 addStates(&parser.currentState, conceptScope) 87 } else if parser.isStep(token) { 88 if !isInState(parser.currentState, conceptScope) { 89 parseRes.ParseErrors = append(parseRes.ParseErrors, ParseError{FileName: fileName, LineNo: token.LineNo, Message: "Step is not defined inside a concept heading", LineText: token.LineText}) 90 continue 91 } 92 if errs := parser.processConceptStep(token, fileName); len(errs) > 0 { 93 parseRes.ParseErrors = append(parseRes.ParseErrors, errs...) 94 } 95 addStates(&parser.currentState, stepScope) 96 } else if parser.isTableHeader(token) { 97 if !isInState(parser.currentState, stepScope) { 98 parseRes.ParseErrors = append(parseRes.ParseErrors, ParseError{FileName: fileName, LineNo: token.LineNo, Message: "Table doesn't belong to any step", LineText: token.LineText}) 99 continue 100 } 101 parser.processTableHeader(token) 102 addStates(&parser.currentState, tableScope) 103 } else if parser.isScenarioHeading(token) { 104 parseRes.ParseErrors = append(parseRes.ParseErrors, ParseError{FileName: fileName, LineNo: token.LineNo, Message: "Scenario Heading is not allowed in concept file", LineText: token.LineText}) 105 continue 106 } else if parser.isTableDataRow(token) { 107 if areUnderlined(token.Args) && !isInState(parser.currentState, tableSeparatorScope) { 108 addStates(&parser.currentState, tableSeparatorScope) 109 } else if isInState(parser.currentState, stepScope) { 110 parser.processTableDataRow(token, &parser.currentConcept.Lookup, fileName) 111 } 112 } else { 113 retainStates(&parser.currentState, conceptScope) 114 addStates(&parser.currentState, commentScope) 115 comment := &gauge.Comment{Value: token.Value, LineNo: token.LineNo} 116 if parser.currentConcept == nil { 117 preComments = append(preComments, comment) 118 addPreComments = true 119 continue 120 } 121 parser.currentConcept.Items = append(parser.currentConcept.Items, comment) 122 } 123 } 124 if parser.currentConcept != nil && len(parser.currentConcept.ConceptSteps) < 1 { 125 parseRes.ParseErrors = append(parseRes.ParseErrors, ParseError{FileName: fileName, LineNo: parser.currentConcept.LineNo, Message: "Concept should have atleast one step", LineText: parser.currentConcept.LineText}) 126 return nil, parseRes 127 } 128 129 if parser.currentConcept != nil { 130 concepts = append(concepts, parser.currentConcept) 131 } 132 return concepts, parseRes 133 } 134 135 func (parser *ConceptParser) isConceptHeading(token *Token) bool { 136 return token.Kind == gauge.SpecKind 137 } 138 139 func (parser *ConceptParser) isStep(token *Token) bool { 140 return token.Kind == gauge.StepKind 141 } 142 143 func (parser *ConceptParser) isScenarioHeading(token *Token) bool { 144 return token.Kind == gauge.ScenarioKind 145 } 146 147 func (parser *ConceptParser) isTableHeader(token *Token) bool { 148 return token.Kind == gauge.TableHeader 149 } 150 151 func (parser *ConceptParser) isTableDataRow(token *Token) bool { 152 return token.Kind == gauge.TableRow 153 } 154 155 func (parser *ConceptParser) processConceptHeading(token *Token, fileName string) (*gauge.Step, *ParseResult) { 156 processStep(new(SpecParser), token) 157 token.LineText = strings.TrimSpace(strings.TrimLeft(strings.TrimSpace(token.LineText), "#")) 158 var concept *gauge.Step 159 var parseRes *ParseResult 160 concept, parseRes = CreateStepUsingLookup(token, nil, fileName) 161 if parseRes != nil && len(parseRes.ParseErrors) > 0 { 162 return nil, parseRes 163 } 164 if !parser.hasOnlyDynamicParams(concept) { 165 parseRes.ParseErrors = []ParseError{ParseError{FileName: fileName, LineNo: token.LineNo, Message: "Concept heading can have only Dynamic Parameters", LineText: token.LineText}} 166 return nil, parseRes 167 } 168 169 concept.IsConcept = true 170 parser.createConceptLookup(concept) 171 concept.Items = append(concept.Items, concept) 172 return concept, parseRes 173 } 174 175 func (parser *ConceptParser) processConceptStep(token *Token, fileName string) []ParseError { 176 processStep(new(SpecParser), token) 177 conceptStep, parseRes := CreateStepUsingLookup(token, &parser.currentConcept.Lookup, fileName) 178 if conceptStep != nil { 179 conceptStep.Suffix = token.Suffix 180 parser.currentConcept.ConceptSteps = append(parser.currentConcept.ConceptSteps, conceptStep) 181 parser.currentConcept.Items = append(parser.currentConcept.Items, conceptStep) 182 } 183 return parseRes.ParseErrors 184 } 185 186 func (parser *ConceptParser) processTableHeader(token *Token) { 187 steps := parser.currentConcept.ConceptSteps 188 currentStep := steps[len(steps)-1] 189 addInlineTableHeader(currentStep, token) 190 items := parser.currentConcept.Items 191 items[len(items)-1] = currentStep 192 } 193 194 func (parser *ConceptParser) processTableDataRow(token *Token, argLookup *gauge.ArgLookup, fileName string) { 195 steps := parser.currentConcept.ConceptSteps 196 currentStep := steps[len(steps)-1] 197 addInlineTableRow(currentStep, token, argLookup, fileName) 198 items := parser.currentConcept.Items 199 items[len(items)-1] = currentStep 200 } 201 202 func (parser *ConceptParser) hasOnlyDynamicParams(step *gauge.Step) bool { 203 for _, arg := range step.Args { 204 if arg.ArgType != gauge.Dynamic { 205 return false 206 } 207 } 208 return true 209 } 210 211 func (parser *ConceptParser) createConceptLookup(concept *gauge.Step) { 212 for _, arg := range concept.Args { 213 concept.Lookup.AddArgName(arg.Value) 214 } 215 } 216 217 func CreateConceptsDictionary() (*gauge.ConceptDictionary, *ParseResult) { 218 cptFilesMap := make(map[string]bool, 0) 219 for _, cpt := range util.GetConceptFiles() { 220 cptFilesMap[cpt] = true 221 } 222 var conceptFiles []string 223 for cpt := range cptFilesMap { 224 conceptFiles = append(conceptFiles, cpt) 225 } 226 conceptsDictionary := gauge.NewConceptDictionary() 227 res := &ParseResult{Ok: true} 228 if _, errs := AddConcepts(conceptFiles, conceptsDictionary); len(errs) > 0 { 229 for _, err := range errs { 230 logger.APILog.Errorf("Concept parse failure: %s %s", conceptFiles[0], err) 231 } 232 res.ParseErrors = append(res.ParseErrors, errs...) 233 res.Ok = false 234 } 235 vRes := ValidateConcepts(conceptsDictionary) 236 if len(vRes.ParseErrors) > 0 { 237 res.Ok = false 238 res.ParseErrors = append(res.ParseErrors, vRes.ParseErrors...) 239 } 240 return conceptsDictionary, res 241 } 242 243 func AddConcept(concepts []*gauge.Step, file string, conceptDictionary *gauge.ConceptDictionary) []ParseError { 244 var duplicateConcepts map[string][]string = make(map[string][]string) 245 for _, conceptStep := range concepts { 246 checkForDuplicateConcepts(conceptDictionary, conceptStep, duplicateConcepts, file) 247 } 248 conceptDictionary.UpdateLookupForNestedConcepts() 249 return mergeDuplicateConceptErrors(duplicateConcepts) 250 } 251 252 func AddConcepts(conceptFiles []string, conceptDictionary *gauge.ConceptDictionary) ([]*gauge.Step, []ParseError) { 253 var conceptSteps []*gauge.Step 254 var parseResults []*ParseResult 255 var duplicateConcepts map[string][]string = make(map[string][]string) 256 for _, conceptFile := range conceptFiles { 257 concepts, parseRes := new(ConceptParser).ParseFile(conceptFile) 258 if parseRes != nil && parseRes.Warnings != nil { 259 for _, warning := range parseRes.Warnings { 260 logger.Warningf(warning.String()) 261 } 262 } 263 for _, conceptStep := range concepts { 264 checkForDuplicateConcepts(conceptDictionary, conceptStep, duplicateConcepts, conceptFile) 265 } 266 conceptDictionary.UpdateLookupForNestedConcepts() 267 conceptSteps = append(conceptSteps, concepts...) 268 parseResults = append(parseResults, parseRes) 269 } 270 errs := collectAllPArseErrors(parseResults) 271 errs = append(errs, mergeDuplicateConceptErrors(duplicateConcepts)...) 272 return conceptSteps, errs 273 } 274 275 func mergeDuplicateConceptErrors(duplicateConcepts map[string][]string) []ParseError { 276 errs := []ParseError{} 277 if len(duplicateConcepts) > 0 { 278 for k, v := range duplicateConcepts { 279 var buffer bytes.Buffer 280 buffer.WriteString(fmt.Sprintf("Duplicate concept definition found => '%s' => at", k)) 281 for _, value := range v { 282 buffer.WriteString(value) 283 } 284 errs = append(errs, ParseError{Message: buffer.String()}) 285 } 286 } 287 return errs 288 } 289 290 func checkForDuplicateConcepts(conceptDictionary *gauge.ConceptDictionary, conceptStep *gauge.Step, duplicateConcepts map[string][]string, conceptFile string) { 291 var duplicateConceptStep *gauge.Step 292 if _, exists := conceptDictionary.ConceptsMap[conceptStep.Value]; exists { 293 duplicateConceptStep = conceptStep 294 duplicateConcepts[conceptStep.LineText] = append(duplicateConcepts[conceptStep.LineText], fmt.Sprintf("\n\t%s:%d", conceptFile, conceptStep.LineNo)) 295 } else { 296 conceptDictionary.ConceptsMap[conceptStep.Value] = &gauge.Concept{conceptStep, conceptFile} 297 } 298 conceptDictionary.ReplaceNestedConceptSteps(conceptStep) 299 if duplicateConceptStep != nil { 300 conceptInDictionary := conceptDictionary.ConceptsMap[duplicateConceptStep.Value] 301 errorInfo := fmt.Sprintf("\n\t%s:%d", conceptInDictionary.FileName, conceptInDictionary.ConceptStep.LineNo) 302 if !contains(duplicateConcepts[duplicateConceptStep.LineText], errorInfo) { 303 duplicateConcepts[duplicateConceptStep.LineText] = append(duplicateConcepts[duplicateConceptStep.LineText], errorInfo) 304 } 305 } 306 } 307 308 func contains(slice []string, item string) bool { 309 set := make(map[string]struct{}, len(slice)) 310 for _, s := range slice { 311 set[s] = struct{}{} 312 } 313 _, ok := set[item] 314 return ok 315 } 316 317 func collectAllPArseErrors(results []*ParseResult) (errs []ParseError) { 318 for _, res := range results { 319 errs = append(errs, res.ParseErrors...) 320 } 321 return 322 } 323 324 func ValidateConcepts(conceptDictionary *gauge.ConceptDictionary) *ParseResult { 325 res := &ParseResult{ParseErrors: []ParseError{}} 326 var conceptsWithError []*gauge.Concept 327 for _, concept := range conceptDictionary.ConceptsMap { 328 err := checkCircularReferencing(conceptDictionary, concept.ConceptStep, nil) 329 if err != nil { 330 delete(conceptDictionary.ConceptsMap, concept.ConceptStep.Value) 331 res.ParseErrors = append(res.ParseErrors, err.(ParseError)) 332 conceptsWithError = append(conceptsWithError, concept) 333 } 334 } 335 for _, con := range conceptsWithError { 336 removeAllReferences(conceptDictionary, con) 337 } 338 return res 339 } 340 341 func removeAllReferences(conceptDictionary *gauge.ConceptDictionary, concept *gauge.Concept) { 342 for _, cpt := range conceptDictionary.ConceptsMap { 343 var nestedSteps []*gauge.Step 344 for _, con := range cpt.ConceptStep.ConceptSteps { 345 if con.Value != concept.ConceptStep.Value { 346 nestedSteps = append(nestedSteps, con) 347 } 348 } 349 cpt.ConceptStep.ConceptSteps = nestedSteps 350 } 351 } 352 353 func checkCircularReferencing(conceptDictionary *gauge.ConceptDictionary, concept *gauge.Step, traversedSteps map[string]string) error { 354 if traversedSteps == nil { 355 traversedSteps = make(map[string]string, 0) 356 } 357 con := conceptDictionary.Search(concept.Value) 358 if con == nil { 359 return nil 360 } 361 currentConceptFileName := con.FileName 362 traversedSteps[concept.Value] = currentConceptFileName 363 for _, step := range concept.ConceptSteps { 364 if fileName, exists := traversedSteps[step.Value]; exists { 365 delete(conceptDictionary.ConceptsMap, step.Value) 366 return ParseError{ 367 FileName: fileName, 368 LineText: step.LineText, 369 LineNo: concept.LineNo, 370 Message: fmt.Sprintf("Circular reference found in concept. \"%s\" => %s:%d", concept.LineText, fileName, step.LineNo), 371 } 372 } 373 if step.IsConcept { 374 if err := checkCircularReferencing(conceptDictionary, step, traversedSteps); err != nil { 375 return err 376 } 377 } 378 } 379 delete(traversedSteps, concept.Value) 380 return nil 381 }