github.com/ezbuy/gauge@v0.9.4-0.20171013092048-7ac5bd3931cd/parser/specparser.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 "bufio" 22 "fmt" 23 "regexp" 24 "strings" 25 26 "github.com/getgauge/common" 27 "github.com/getgauge/gauge/gauge" 28 "github.com/getgauge/gauge/gauge_messages" 29 ) 30 31 type SpecParser struct { 32 scanner *bufio.Scanner 33 lineNo int 34 tokens []*Token 35 currentState int 36 processors map[gauge.TokenKind]func(*SpecParser, *Token) ([]error, bool) 37 conceptDictionary *gauge.ConceptDictionary 38 } 39 40 const ( 41 initial = 1 << iota 42 specScope = 1 << iota 43 scenarioScope = 1 << iota 44 commentScope = 1 << iota 45 tableScope = 1 << iota 46 tableSeparatorScope = 1 << iota 47 tableDataScope = 1 << iota 48 stepScope = 1 << iota 49 contextScope = 1 << iota 50 tearDownScope = 1 << iota 51 conceptScope = 1 << iota 52 keywordScope = 1 << iota 53 tagsScope = 1 << iota 54 ) 55 56 func (parser *SpecParser) initialize() { 57 parser.processors = make(map[gauge.TokenKind]func(*SpecParser, *Token) ([]error, bool)) 58 parser.processors[gauge.SpecKind] = processSpec 59 parser.processors[gauge.ScenarioKind] = processScenario 60 parser.processors[gauge.CommentKind] = processComment 61 parser.processors[gauge.StepKind] = processStep 62 parser.processors[gauge.TagKind] = processTag 63 parser.processors[gauge.TableHeader] = processTable 64 parser.processors[gauge.TableRow] = processTable 65 parser.processors[gauge.DataTableKind] = processDataTable 66 parser.processors[gauge.TearDownKind] = processTearDown 67 } 68 69 func (parser *SpecParser) Parse(specText string, conceptDictionary *gauge.ConceptDictionary, specFile string) (*gauge.Specification, *ParseResult) { 70 tokens, errs := parser.GenerateTokens(specText, specFile) 71 spec, res := parser.CreateSpecification(tokens, conceptDictionary, specFile) 72 res.FileName = specFile 73 if len(errs) > 0 { 74 res.Ok = false 75 } 76 res.ParseErrors = append(errs, res.ParseErrors...) 77 return spec, res 78 } 79 80 // ParseSpecText without validating and replacing concepts. 81 func (parser *SpecParser) ParseSpecText(specText string, specFile string) (*gauge.Specification, *ParseResult) { 82 tokens, errs := parser.GenerateTokens(specText, specFile) 83 spec, res := parser.createSpecification(tokens, specFile) 84 res.FileName = specFile 85 if len(errs) > 0 { 86 res.Ok = false 87 } 88 res.ParseErrors = append(errs, res.ParseErrors...) 89 return spec, res 90 } 91 92 func (parser *SpecParser) GenerateTokens(specText, fileName string) ([]*Token, []ParseError) { 93 parser.initialize() 94 parser.scanner = bufio.NewScanner(strings.NewReader(specText)) 95 parser.currentState = initial 96 var errors []ParseError 97 var newToken *Token 98 for line, hasLine := parser.nextLine(); hasLine; line, hasLine = parser.nextLine() { 99 trimmedLine := strings.TrimSpace(line) 100 if len(trimmedLine) == 0 { 101 if newToken != nil && newToken.Kind == gauge.StepKind { 102 newToken.Suffix = "\n" 103 continue 104 } 105 newToken = &Token{Kind: gauge.CommentKind, LineNo: parser.lineNo, LineText: line, Value: "\n"} 106 } else if parser.isScenarioHeading(trimmedLine) { 107 newToken = &Token{Kind: gauge.ScenarioKind, LineNo: parser.lineNo, LineText: line, Value: strings.TrimSpace(trimmedLine[2:])} 108 } else if parser.isSpecHeading(trimmedLine) { 109 newToken = &Token{Kind: gauge.SpecKind, LineNo: parser.lineNo, LineText: line, Value: strings.TrimSpace(trimmedLine[1:])} 110 } else if parser.isSpecUnderline(trimmedLine) && (isInState(parser.currentState, commentScope)) { 111 newToken = parser.tokens[len(parser.tokens)-1] 112 newToken.Kind = gauge.SpecKind 113 parser.tokens = append(parser.tokens[:len(parser.tokens)-1]) 114 } else if parser.isScenarioUnderline(trimmedLine) && (isInState(parser.currentState, commentScope)) { 115 newToken = parser.tokens[len(parser.tokens)-1] 116 newToken.Kind = gauge.ScenarioKind 117 parser.tokens = append(parser.tokens[:len(parser.tokens)-1]) 118 } else if parser.isStep(trimmedLine) { 119 newToken = &Token{Kind: gauge.StepKind, LineNo: parser.lineNo, LineText: strings.TrimSpace(trimmedLine[1:]), Value: strings.TrimSpace(trimmedLine[1:])} 120 } else if found, startIndex := parser.checkTag(trimmedLine); found || isInState(parser.currentState, tagsScope) { 121 if isInState(parser.currentState, tagsScope) { 122 startIndex = 0 123 } 124 if parser.isTagEndingWithComma(trimmedLine) { 125 addStates(&parser.currentState, tagsScope) 126 } else { 127 parser.clearState() 128 } 129 newToken = &Token{Kind: gauge.TagKind, LineNo: parser.lineNo, LineText: line, Value: strings.TrimSpace(trimmedLine[startIndex:])} 130 } else if parser.isTableRow(trimmedLine) { 131 kind := parser.tokenKindBasedOnCurrentState(tableScope, gauge.TableRow, gauge.TableHeader) 132 newToken = &Token{Kind: kind, LineNo: parser.lineNo, LineText: line, Value: strings.TrimSpace(trimmedLine)} 133 } else if value, found := parser.isDataTable(trimmedLine); found { 134 newToken = &Token{Kind: gauge.DataTableKind, LineNo: parser.lineNo, LineText: line, Value: value} 135 } else if parser.isTearDown(trimmedLine) { 136 newToken = &Token{Kind: gauge.TearDownKind, LineNo: parser.lineNo, LineText: line, Value: trimmedLine} 137 } else { 138 newToken = &Token{Kind: gauge.CommentKind, LineNo: parser.lineNo, LineText: line, Value: common.TrimTrailingSpace(line)} 139 } 140 errors = append(errors, parser.accept(newToken, fileName)...) 141 } 142 return parser.tokens, errors 143 } 144 145 func (parser *SpecParser) tokenKindBasedOnCurrentState(state int, matchingToken gauge.TokenKind, alternateToken gauge.TokenKind) gauge.TokenKind { 146 if isInState(parser.currentState, state) { 147 return matchingToken 148 } else { 149 return alternateToken 150 } 151 } 152 153 func (parser *SpecParser) checkTag(text string) (bool, int) { 154 lowerCased := strings.ToLower 155 tagColon := "tags:" 156 tagSpaceColon := "tags :" 157 if tagStartIndex := strings.Index(lowerCased(text), tagColon); tagStartIndex == 0 { 158 return true, len(tagColon) 159 } else if tagStartIndex := strings.Index(lowerCased(text), tagSpaceColon); tagStartIndex == 0 { 160 return true, len(tagSpaceColon) 161 } 162 return false, -1 163 } 164 func (parser *SpecParser) isTagEndingWithComma(text string) bool { 165 return strings.HasSuffix(strings.ToLower(text), ",") 166 } 167 168 func (parser *SpecParser) isSpecHeading(text string) bool { 169 if len(text) > 1 { 170 return text[0] == '#' && text[1] != '#' 171 } else { 172 return text[0] == '#' 173 } 174 } 175 176 func (parser *SpecParser) isScenarioHeading(text string) bool { 177 if len(text) > 2 { 178 return text[0] == '#' && text[1] == '#' && text[2] != '#' 179 } else if len(text) == 2 { 180 return text[0] == '#' && text[1] == '#' 181 } 182 return false 183 } 184 185 func (parser *SpecParser) isStep(text string) bool { 186 if len(text) > 1 { 187 return text[0] == '*' && text[1] != '*' 188 } else { 189 return text[0] == '*' 190 } 191 } 192 193 func (parser *SpecParser) isScenarioUnderline(text string) bool { 194 return isUnderline(text, rune('-')) 195 } 196 197 func (parser *SpecParser) isTableRow(text string) bool { 198 return text[0] == '|' && text[len(text)-1] == '|' 199 } 200 201 func (parser *SpecParser) isTearDown(text string) bool { 202 return isUnderline(text, rune('_')) 203 } 204 205 func (parser *SpecParser) isSpecUnderline(text string) bool { 206 return isUnderline(text, rune('=')) 207 } 208 209 func (parser *SpecParser) isDataTable(text string) (string, bool) { 210 lowerCased := strings.ToLower 211 tableColon := "table:" 212 tableSpaceColon := "table :" 213 if strings.HasPrefix(lowerCased(text), tableColon) { 214 return tableColon + " " + strings.TrimSpace(strings.Replace(lowerCased(text), tableColon, "", 1)), true 215 } else if strings.HasPrefix(lowerCased(text), tableSpaceColon) { 216 return tableColon + " " + strings.TrimSpace(strings.Replace(lowerCased(text), tableSpaceColon, "", 1)), true 217 } 218 return "", false 219 } 220 221 //concept header will have dynamic param and should not be resolved through lookup, so passing nil lookup 222 func isConceptHeader(lookup *gauge.ArgLookup) bool { 223 return lookup == nil 224 } 225 226 func (parser *SpecParser) accept(token *Token, fileName string) []ParseError { 227 errs, _ := parser.processors[token.Kind](parser, token) 228 parser.tokens = append(parser.tokens, token) 229 var parseErrs []ParseError 230 for _, err := range errs { 231 parseErrs = append(parseErrs, ParseError{FileName: fileName, LineNo: token.LineNo, Message: err.Error(), LineText: token.Value}) 232 } 233 return parseErrs 234 } 235 236 func (parser *SpecParser) nextLine() (string, bool) { 237 scanned := parser.scanner.Scan() 238 if scanned { 239 parser.lineNo++ 240 return parser.scanner.Text(), true 241 } 242 if err := parser.scanner.Err(); err != nil { 243 panic(err) 244 } 245 246 return "", false 247 } 248 249 func (parser *SpecParser) clearState() { 250 parser.currentState = 0 251 } 252 253 func (parser *SpecParser) CreateSpecification(tokens []*Token, conceptDictionary *gauge.ConceptDictionary, specFile string) (*gauge.Specification, *ParseResult) { 254 parser.conceptDictionary = conceptDictionary 255 specification, finalResult := parser.createSpecification(tokens, specFile) 256 specification.ProcessConceptStepsFrom(conceptDictionary) 257 err := parser.validateSpec(specification) 258 if err != nil { 259 finalResult.Ok = false 260 finalResult.ParseErrors = append([]ParseError{err.(ParseError)}, finalResult.ParseErrors...) 261 } 262 return specification, finalResult 263 } 264 265 func (parser *SpecParser) createSpecification(tokens []*Token, specFile string) (*gauge.Specification, *ParseResult) { 266 finalResult := &ParseResult{ParseErrors: make([]ParseError, 0), Ok: true} 267 converters := parser.initializeConverters() 268 specification := &gauge.Specification{FileName: specFile} 269 state := initial 270 for _, token := range tokens { 271 for _, converter := range converters { 272 result := converter(token, &state, specification) 273 if !result.Ok { 274 if result.ParseErrors != nil { 275 finalResult.Ok = false 276 finalResult.ParseErrors = append(finalResult.ParseErrors, result.ParseErrors...) 277 } 278 } 279 if result.Warnings != nil { 280 if finalResult.Warnings == nil { 281 finalResult.Warnings = make([]*Warning, 0) 282 } 283 finalResult.Warnings = append(finalResult.Warnings, result.Warnings...) 284 } 285 } 286 } 287 if len(specification.Scenarios) > 0 { 288 specification.LatestScenario().Span.End = tokens[len(tokens)-1].LineNo 289 } 290 return specification, finalResult 291 } 292 293 func (parser *SpecParser) initializeConverters() []func(*Token, *int, *gauge.Specification) ParseResult { 294 specConverter := converterFn(func(token *Token, state *int) bool { 295 return token.Kind == gauge.SpecKind 296 }, func(token *Token, spec *gauge.Specification, state *int) ParseResult { 297 if spec.Heading != nil { 298 return ParseResult{Ok: false, ParseErrors: []ParseError{ParseError{spec.FileName, token.LineNo, "Multiple spec headings found in same file", token.LineText}}} 299 } 300 301 spec.AddHeading(&gauge.Heading{LineNo: token.LineNo, Value: token.Value}) 302 addStates(state, specScope) 303 return ParseResult{Ok: true} 304 }) 305 306 scenarioConverter := converterFn(func(token *Token, state *int) bool { 307 return token.Kind == gauge.ScenarioKind 308 }, func(token *Token, spec *gauge.Specification, state *int) ParseResult { 309 if spec.Heading == nil { 310 return ParseResult{Ok: false, ParseErrors: []ParseError{ParseError{spec.FileName, token.LineNo, "Scenario should be defined after the spec heading", token.LineText}}} 311 } 312 for _, scenario := range spec.Scenarios { 313 if strings.ToLower(scenario.Heading.Value) == strings.ToLower(token.Value) { 314 return ParseResult{Ok: false, ParseErrors: []ParseError{ParseError{spec.FileName, token.LineNo, "Duplicate scenario definition '" + scenario.Heading.Value + "' found in the same specification", token.LineText}}} 315 } 316 } 317 scenario := &gauge.Scenario{Span: &gauge.Span{Start: token.LineNo, End: token.LineNo}} 318 if len(spec.Scenarios) > 0 { 319 spec.LatestScenario().Span.End = token.LineNo - 1 320 } 321 scenario.AddHeading(&gauge.Heading{Value: token.Value, LineNo: token.LineNo}) 322 spec.AddScenario(scenario) 323 324 retainStates(state, specScope) 325 addStates(state, scenarioScope) 326 return ParseResult{Ok: true} 327 }) 328 329 stepConverter := converterFn(func(token *Token, state *int) bool { 330 return token.Kind == gauge.StepKind && isInState(*state, scenarioScope) 331 }, func(token *Token, spec *gauge.Specification, state *int) ParseResult { 332 latestScenario := spec.LatestScenario() 333 stepToAdd, parseDetails := createStep(spec, token) 334 if stepToAdd == nil { 335 return ParseResult{ParseErrors: parseDetails.ParseErrors, Ok: false, Warnings: parseDetails.Warnings} 336 } 337 latestScenario.AddStep(stepToAdd) 338 retainStates(state, specScope, scenarioScope) 339 addStates(state, stepScope) 340 if parseDetails != nil && len(parseDetails.ParseErrors) > 0 { 341 return ParseResult{ParseErrors: parseDetails.ParseErrors, Ok: false, Warnings: parseDetails.Warnings} 342 } 343 if parseDetails.Warnings != nil { 344 return ParseResult{Ok: false, Warnings: parseDetails.Warnings} 345 } 346 return ParseResult{Ok: true, Warnings: parseDetails.Warnings} 347 }) 348 349 contextConverter := converterFn(func(token *Token, state *int) bool { 350 return token.Kind == gauge.StepKind && !isInState(*state, scenarioScope) && isInState(*state, specScope) && !isInState(*state, tearDownScope) 351 }, func(token *Token, spec *gauge.Specification, state *int) ParseResult { 352 stepToAdd, parseDetails := createStep(spec, token) 353 if stepToAdd == nil { 354 return ParseResult{ParseErrors: parseDetails.ParseErrors, Ok: false, Warnings: parseDetails.Warnings} 355 } 356 spec.AddContext(stepToAdd) 357 retainStates(state, specScope) 358 addStates(state, contextScope) 359 if parseDetails != nil && len(parseDetails.ParseErrors) > 0 { 360 parseDetails.Ok = false 361 return *parseDetails 362 } 363 if parseDetails.Warnings != nil { 364 return ParseResult{Ok: false, Warnings: parseDetails.Warnings} 365 } 366 return ParseResult{Ok: true, Warnings: parseDetails.Warnings} 367 }) 368 369 tearDownConverter := converterFn(func(token *Token, state *int) bool { 370 return token.Kind == gauge.TearDownKind 371 }, func(token *Token, spec *gauge.Specification, state *int) ParseResult { 372 retainStates(state, specScope) 373 addStates(state, tearDownScope) 374 spec.AddItem(&gauge.TearDown{LineNo: token.LineNo, Value: token.Value}) 375 return ParseResult{Ok: true} 376 }) 377 378 tearDownStepConverter := converterFn(func(token *Token, state *int) bool { 379 return token.Kind == gauge.StepKind && isInState(*state, tearDownScope) 380 }, func(token *Token, spec *gauge.Specification, state *int) ParseResult { 381 stepToAdd, parseDetails := createStep(spec, token) 382 if stepToAdd == nil { 383 return ParseResult{ParseErrors: parseDetails.ParseErrors, Ok: false, Warnings: parseDetails.Warnings} 384 } 385 spec.TearDownSteps = append(spec.TearDownSteps, stepToAdd) 386 spec.AddItem(stepToAdd) 387 retainStates(state, specScope, tearDownScope) 388 if parseDetails != nil && len(parseDetails.ParseErrors) > 0 { 389 parseDetails.Ok = false 390 return *parseDetails 391 } 392 if parseDetails.Warnings != nil { 393 return ParseResult{Ok: false, Warnings: parseDetails.Warnings} 394 } 395 return ParseResult{Ok: true, Warnings: parseDetails.Warnings} 396 }) 397 398 commentConverter := converterFn(func(token *Token, state *int) bool { 399 return token.Kind == gauge.CommentKind 400 }, func(token *Token, spec *gauge.Specification, state *int) ParseResult { 401 comment := &gauge.Comment{token.Value, token.LineNo} 402 if isInState(*state, scenarioScope) { 403 spec.LatestScenario().AddComment(comment) 404 } else { 405 spec.AddComment(comment) 406 } 407 retainStates(state, specScope, scenarioScope, tearDownScope) 408 addStates(state, commentScope) 409 return ParseResult{Ok: true} 410 }) 411 412 keywordConverter := converterFn(func(token *Token, state *int) bool { 413 return token.Kind == gauge.DataTableKind 414 }, func(token *Token, spec *gauge.Specification, state *int) ParseResult { 415 resolvedArg, err := newSpecialTypeResolver().resolve(token.Value) 416 if resolvedArg == nil || err != nil { 417 e := ParseError{FileName: spec.FileName, LineNo: token.LineNo, LineText: token.LineText, Message: fmt.Sprintf("Could not resolve table from %s", token.LineText)} 418 return ParseResult{ParseErrors: []ParseError{e}, Ok: false} 419 } 420 if isInState(*state, specScope) && !spec.DataTable.IsInitialized() { 421 externalTable := &gauge.DataTable{} 422 externalTable.Table = resolvedArg.Table 423 externalTable.LineNo = token.LineNo 424 externalTable.Value = token.Value 425 externalTable.IsExternal = true 426 spec.AddExternalDataTable(externalTable) 427 } else if isInState(*state, specScope) && spec.DataTable.IsInitialized() { 428 value := "Multiple data table present, ignoring table" 429 spec.AddComment(&gauge.Comment{token.LineText, token.LineNo}) 430 return ParseResult{Ok: false, Warnings: []*Warning{&Warning{spec.FileName, token.LineNo, value}}} 431 } else { 432 value := "Data table not associated with spec" 433 spec.AddComment(&gauge.Comment{token.LineText, token.LineNo}) 434 return ParseResult{Ok: false, Warnings: []*Warning{&Warning{spec.FileName, token.LineNo, value}}} 435 } 436 retainStates(state, specScope) 437 addStates(state, keywordScope) 438 return ParseResult{Ok: true} 439 }) 440 441 tableHeaderConverter := converterFn(func(token *Token, state *int) bool { 442 return token.Kind == gauge.TableHeader && isInState(*state, specScope) 443 }, func(token *Token, spec *gauge.Specification, state *int) ParseResult { 444 if isInState(*state, stepScope) { 445 latestScenario := spec.LatestScenario() 446 latestStep := latestScenario.LatestStep() 447 addInlineTableHeader(latestStep, token) 448 } else if isInState(*state, contextScope) { 449 latestContext := spec.LatestContext() 450 addInlineTableHeader(latestContext, token) 451 } else if isInState(*state, tearDownScope) { 452 if len(spec.TearDownSteps) > 0 { 453 latestTeardown := spec.LatestTeardown() 454 addInlineTableHeader(latestTeardown, token) 455 } else { 456 spec.AddComment(&gauge.Comment{token.LineText, token.LineNo}) 457 } 458 } else if !isInState(*state, scenarioScope) { 459 if !spec.DataTable.Table.IsInitialized() { 460 dataTable := &gauge.Table{} 461 dataTable.LineNo = token.LineNo 462 dataTable.AddHeaders(token.Args) 463 spec.AddDataTable(dataTable) 464 } else { 465 value := "Multiple data table present, ignoring table" 466 spec.AddComment(&gauge.Comment{token.LineText, token.LineNo}) 467 return ParseResult{Ok: false, Warnings: []*Warning{&Warning{spec.FileName, token.LineNo, value}}} 468 } 469 } else { 470 value := "Table not associated with a step, ignoring table" 471 spec.LatestScenario().AddComment(&gauge.Comment{token.LineText, token.LineNo}) 472 return ParseResult{Ok: false, Warnings: []*Warning{&Warning{spec.FileName, token.LineNo, value}}} 473 } 474 retainStates(state, specScope, scenarioScope, stepScope, contextScope, tearDownScope) 475 addStates(state, tableScope) 476 return ParseResult{Ok: true} 477 }) 478 479 tableRowConverter := converterFn(func(token *Token, state *int) bool { 480 return token.Kind == gauge.TableRow 481 }, func(token *Token, spec *gauge.Specification, state *int) ParseResult { 482 var result ParseResult 483 //When table is to be treated as a comment 484 if !isInState(*state, tableScope) { 485 if isInState(*state, scenarioScope) { 486 spec.LatestScenario().AddComment(&gauge.Comment{token.LineText, token.LineNo}) 487 } else { 488 spec.AddComment(&gauge.Comment{token.LineText, token.LineNo}) 489 } 490 } else if areUnderlined(token.Args) && !isInState(*state, tableSeparatorScope) { 491 retainStates(state, specScope, scenarioScope, stepScope, contextScope, tearDownScope, tableScope) 492 addStates(state, tableSeparatorScope) 493 // skip table separator 494 result = ParseResult{Ok: true} 495 } else if isInState(*state, stepScope) { 496 latestScenario := spec.LatestScenario() 497 latestStep := latestScenario.LatestStep() 498 result = addInlineTableRow(latestStep, token, new(gauge.ArgLookup).FromDataTable(&spec.DataTable.Table), spec.FileName) 499 } else if isInState(*state, contextScope) { 500 latestContext := spec.LatestContext() 501 result = addInlineTableRow(latestContext, token, new(gauge.ArgLookup).FromDataTable(&spec.DataTable.Table), spec.FileName) 502 } else if isInState(*state, tearDownScope) { 503 if len(spec.TearDownSteps) > 0 { 504 latestTeardown := spec.LatestTeardown() 505 result = addInlineTableRow(latestTeardown, token, new(gauge.ArgLookup).FromDataTable(&spec.DataTable.Table), spec.FileName) 506 } else { 507 spec.AddComment(&gauge.Comment{token.LineText, token.LineNo}) 508 } 509 } else { 510 //todo validate datatable rows also 511 spec.DataTable.Table.AddRowValues(token.Args) 512 result = ParseResult{Ok: true} 513 } 514 retainStates(state, specScope, scenarioScope, stepScope, contextScope, tearDownScope, tableScope, tableSeparatorScope) 515 return result 516 }) 517 518 tagConverter := converterFn(func(token *Token, state *int) bool { 519 return (token.Kind == gauge.TagKind) 520 }, func(token *Token, spec *gauge.Specification, state *int) ParseResult { 521 tags := &gauge.Tags{RawValues: [][]string{token.Args}} 522 if isInState(*state, scenarioScope) { 523 if isInState(*state, tagsScope) { 524 spec.LatestScenario().Tags.Add(tags.RawValues[0]) 525 } else { 526 if spec.LatestScenario().NTags() != 0 { 527 return ParseResult{Ok: false, ParseErrors: []ParseError{ParseError{FileName: spec.FileName, LineNo: token.LineNo, Message: "Tags can be defined only once per scenario", LineText: token.LineText}}} 528 } 529 spec.LatestScenario().AddTags(tags) 530 } 531 } else { 532 if isInState(*state, tagsScope) { 533 spec.Tags.Add(tags.RawValues[0]) 534 } else { 535 if spec.NTags() != 0 { 536 return ParseResult{Ok: false, ParseErrors: []ParseError{ParseError{FileName: spec.FileName, LineNo: token.LineNo, Message: "Tags can be defined only once per specification", LineText: token.LineText}}} 537 } 538 spec.AddTags(tags) 539 } 540 } 541 addStates(state, tagsScope) 542 return ParseResult{Ok: true} 543 }) 544 545 converter := []func(*Token, *int, *gauge.Specification) ParseResult{ 546 specConverter, scenarioConverter, stepConverter, contextConverter, commentConverter, tableHeaderConverter, tableRowConverter, tagConverter, keywordConverter, tearDownConverter, tearDownStepConverter, 547 } 548 549 return converter 550 } 551 552 func (parser *SpecParser) validateSpec(specification *gauge.Specification) error { 553 if len(specification.Items) == 0 { 554 specification.AddHeading(&gauge.Heading{}) 555 return ParseError{FileName: specification.FileName, LineNo: 1, Message: "Spec does not have any elements"} 556 } 557 if specification.Heading == nil { 558 specification.AddHeading(&gauge.Heading{}) 559 return ParseError{FileName: specification.FileName, LineNo: 1, Message: "Spec heading not found"} 560 } 561 if len(strings.TrimSpace(specification.Heading.Value)) < 1 { 562 return ParseError{FileName: specification.FileName, LineNo: specification.Heading.LineNo, Message: "Spec heading should have at least one character"} 563 } 564 565 dataTable := specification.DataTable.Table 566 if dataTable.IsInitialized() && dataTable.GetRowCount() == 0 { 567 return ParseError{FileName: specification.FileName, LineNo: dataTable.LineNo, Message: "Data table should have at least 1 data row"} 568 } 569 if len(specification.Scenarios) == 0 { 570 return ParseError{FileName: specification.FileName, LineNo: specification.Heading.LineNo, Message: "Spec should have atleast one scenario"} 571 } 572 for _, sce := range specification.Scenarios { 573 if len(sce.Steps) == 0 { 574 return ParseError{FileName: specification.FileName, LineNo: sce.Heading.LineNo, Message: "Scenario should have atleast one step"} 575 } 576 } 577 return nil 578 } 579 580 func converterFn(predicate func(token *Token, state *int) bool, apply func(token *Token, spec *gauge.Specification, state *int) ParseResult) func(*Token, *int, *gauge.Specification) ParseResult { 581 return func(token *Token, state *int, spec *gauge.Specification) ParseResult { 582 if !predicate(token, state) { 583 return ParseResult{Ok: true} 584 } 585 return apply(token, spec, state) 586 } 587 } 588 589 func createStep(spec *gauge.Specification, stepToken *Token) (*gauge.Step, *ParseResult) { 590 dataTableLookup := new(gauge.ArgLookup).FromDataTable(&spec.DataTable.Table) 591 stepToAdd, parseDetails := CreateStepUsingLookup(stepToken, dataTableLookup, spec.FileName) 592 if stepToAdd != nil { 593 stepToAdd.Suffix = stepToken.Suffix 594 } 595 return stepToAdd, parseDetails 596 } 597 598 func CreateStepUsingLookup(stepToken *Token, lookup *gauge.ArgLookup, specFileName string) (*gauge.Step, *ParseResult) { 599 stepValue, argsType := extractStepValueAndParameterTypes(stepToken.Value) 600 if argsType != nil && len(argsType) != len(stepToken.Args) { 601 return nil, &ParseResult{ParseErrors: []ParseError{ParseError{specFileName, stepToken.LineNo, "Step text should not have '{static}' or '{dynamic}' or '{special}'", stepToken.LineText}}, Warnings: nil} 602 } 603 step := &gauge.Step{LineNo: stepToken.LineNo, Value: stepValue, LineText: strings.TrimSpace(stepToken.LineText)} 604 arguments := make([]*gauge.StepArg, 0) 605 var errors []ParseError 606 var warnings []*Warning 607 for i, argType := range argsType { 608 argument, parseDetails := createStepArg(stepToken.Args[i], argType, stepToken, lookup, specFileName) 609 if parseDetails != nil && len(parseDetails.ParseErrors) > 0 { 610 errors = append(errors, parseDetails.ParseErrors...) 611 } 612 arguments = append(arguments, argument) 613 if parseDetails != nil && parseDetails.Warnings != nil { 614 for _, warn := range parseDetails.Warnings { 615 warnings = append(warnings, warn) 616 } 617 } 618 } 619 step.AddArgs(arguments...) 620 return step, &ParseResult{ParseErrors: errors, Warnings: warnings} 621 } 622 623 func ExtractStepArgsFromToken(stepToken *Token) ([]gauge.StepArg, error) { 624 _, argsType := extractStepValueAndParameterTypes(stepToken.Value) 625 if argsType != nil && len(argsType) != len(stepToken.Args) { 626 return nil, fmt.Errorf("Step text should not have '{static}' or '{dynamic}' or '{special}'") 627 } 628 var args []gauge.StepArg 629 for i, argType := range argsType { 630 if gauge.ArgType(argType) == gauge.Static { 631 args = append(args, gauge.StepArg{ArgType: gauge.Static, Value: stepToken.Args[i]}) 632 } else { 633 args = append(args, gauge.StepArg{ArgType: gauge.Dynamic, Value: stepToken.Args[i]}) 634 } 635 } 636 return args, nil 637 } 638 639 func extractStepValueAndParameterTypes(stepTokenValue string) (string, []string) { 640 argsType := make([]string, 0) 641 r := regexp.MustCompile("{(dynamic|static|special)}") 642 /* 643 enter {dynamic} and {static} 644 returns 645 [ 646 ["{dynamic}","dynamic"] 647 ["{static}","static"] 648 ] 649 */ 650 args := r.FindAllStringSubmatch(stepTokenValue, -1) 651 652 if args == nil { 653 return stepTokenValue, nil 654 } 655 for _, arg := range args { 656 //arg[1] extracts the first group 657 argsType = append(argsType, arg[1]) 658 } 659 return r.ReplaceAllString(stepTokenValue, gauge.ParameterPlaceholder), argsType 660 } 661 662 func createStepArg(argValue string, typeOfArg string, token *Token, lookup *gauge.ArgLookup, fileName string) (*gauge.StepArg, *ParseResult) { 663 if typeOfArg == "special" { 664 resolvedArgValue, err := newSpecialTypeResolver().resolve(argValue) 665 if err != nil { 666 switch err.(type) { 667 case invalidSpecialParamError: 668 return treatArgAsDynamic(argValue, token, lookup, fileName) 669 default: 670 return &gauge.StepArg{ArgType: gauge.Dynamic, Value: argValue, Name: argValue}, &ParseResult{ParseErrors: []ParseError{ParseError{FileName: fileName, LineNo: token.LineNo, Message: fmt.Sprintf("Dynamic parameter <%s> could not be resolved", argValue), LineText: token.LineText}}} 671 } 672 } 673 return resolvedArgValue, nil 674 } else if typeOfArg == "static" { 675 return &gauge.StepArg{ArgType: gauge.Static, Value: argValue}, nil 676 } else { 677 return validateDynamicArg(argValue, token, lookup, fileName) 678 } 679 } 680 681 func treatArgAsDynamic(argValue string, token *Token, lookup *gauge.ArgLookup, fileName string) (*gauge.StepArg, *ParseResult) { 682 parseRes := &ParseResult{Warnings: []*Warning{&Warning{FileName: fileName, LineNo: token.LineNo, Message: fmt.Sprintf("Could not resolve special param type <%s>. Treating it as dynamic param.", argValue)}}} 683 stepArg, result := validateDynamicArg(argValue, token, lookup, fileName) 684 if result != nil { 685 if len(result.ParseErrors) > 0 { 686 parseRes.ParseErrors = result.ParseErrors 687 } 688 if result.Warnings != nil { 689 for _, warn := range result.Warnings { 690 parseRes.Warnings = append(parseRes.Warnings, warn) 691 } 692 } 693 } 694 return stepArg, parseRes 695 } 696 697 func validateDynamicArg(argValue string, token *Token, lookup *gauge.ArgLookup, fileName string) (*gauge.StepArg, *ParseResult) { 698 stepArgument := &gauge.StepArg{ArgType: gauge.Dynamic, Value: argValue, Name: argValue} 699 if !isConceptHeader(lookup) && !lookup.ContainsArg(argValue) { 700 return stepArgument, &ParseResult{ParseErrors: []ParseError{ParseError{FileName: fileName, LineNo: token.LineNo, Message: fmt.Sprintf("Dynamic parameter <%s> could not be resolved", argValue), LineText: token.LineText}}} 701 } 702 703 return stepArgument, nil 704 } 705 706 //Step value is modified when inline table is found to account for the new parameter by appending {} 707 //todo validate headers for dynamic 708 func addInlineTableHeader(step *gauge.Step, token *Token) { 709 step.Value = fmt.Sprintf("%s %s", step.Value, gauge.ParameterPlaceholder) 710 step.HasInlineTable = true 711 step.AddInlineTableHeaders(token.Args) 712 } 713 714 func addInlineTableRow(step *gauge.Step, token *Token, argLookup *gauge.ArgLookup, fileName string) ParseResult { 715 dynamicArgMatcher := regexp.MustCompile("^<(.*)>$") 716 tableValues := make([]gauge.TableCell, 0) 717 warnings := make([]*Warning, 0) 718 for _, tableValue := range token.Args { 719 if dynamicArgMatcher.MatchString(tableValue) { 720 match := dynamicArgMatcher.FindAllStringSubmatch(tableValue, -1) 721 param := match[0][1] 722 if !argLookup.ContainsArg(param) { 723 tableValues = append(tableValues, gauge.TableCell{Value: tableValue, CellType: gauge.Static}) 724 warnings = append(warnings, &Warning{FileName: fileName, LineNo: token.LineNo, Message: fmt.Sprintf("Dynamic param <%s> could not be resolved, Treating it as static param", param)}) 725 } else { 726 tableValues = append(tableValues, gauge.TableCell{Value: param, CellType: gauge.Dynamic}) 727 } 728 } else { 729 tableValues = append(tableValues, gauge.TableCell{Value: tableValue, CellType: gauge.Static}) 730 } 731 } 732 step.AddInlineTableRow(tableValues) 733 return ParseResult{Ok: true, Warnings: warnings} 734 } 735 736 func ConvertToStepText(fragments []*gauge_messages.Fragment) string { 737 stepText := "" 738 for _, fragment := range fragments { 739 value := "" 740 if fragment.GetFragmentType() == gauge_messages.Fragment_Text { 741 value = fragment.GetText() 742 } else { 743 switch fragment.GetParameter().GetParameterType() { 744 case gauge_messages.Parameter_Static: 745 value = fmt.Sprintf("\"%s\"", fragment.GetParameter().GetValue()) 746 break 747 case gauge_messages.Parameter_Dynamic: 748 value = fmt.Sprintf("<%s>", fragment.GetParameter().GetValue()) 749 break 750 } 751 } 752 stepText += value 753 } 754 return stepText 755 } 756 757 type Token struct { 758 Kind gauge.TokenKind 759 LineNo int 760 LineText string 761 Suffix string 762 Args []string 763 Value string 764 } 765 766 type ParseError struct { 767 FileName string 768 LineNo int 769 Message string 770 LineText string 771 } 772 773 func (se ParseError) Error() string { 774 if se.LineNo == 0 && se.FileName == "" { 775 return fmt.Sprintf("%s", se.Message) 776 } 777 return fmt.Sprintf("%s:%d %s => '%s'", se.FileName, se.LineNo, se.Message, se.LineText) 778 } 779 780 func (token *Token) String() string { 781 return fmt.Sprintf("kind:%d, lineNo:%d, value:%s, line:%s, args:%s", token.Kind, token.LineNo, token.Value, token.LineText, token.Args) 782 } 783 784 type ParseResult struct { 785 ParseErrors []ParseError 786 Warnings []*Warning 787 Ok bool 788 FileName string 789 } 790 791 func (result *ParseResult) Errors() (errors []string) { 792 for _, err := range result.ParseErrors { 793 errors = append(errors, fmt.Sprintf("[ParseError] %s", err.Error())) 794 } 795 return 796 } 797 798 type Warning struct { 799 FileName string 800 LineNo int 801 Message string 802 } 803 804 func (warning *Warning) String() string { 805 return fmt.Sprintf("%s:%d %s", warning.FileName, warning.LineNo, warning.Message) 806 }