github.com/getgauge/gauge@v1.6.9/parser/convert.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 parser 8 9 import ( 10 "fmt" 11 "regexp" 12 "strings" 13 14 "github.com/getgauge/gauge/env" 15 "github.com/getgauge/gauge/gauge" 16 "github.com/getgauge/gauge/util" 17 ) 18 19 func (parser *SpecParser) initializeConverters() []func(*Token, *int, *gauge.Specification) ParseResult { 20 specConverter := converterFn(func(token *Token, state *int) bool { 21 return token.Kind == gauge.SpecKind 22 }, func(token *Token, spec *gauge.Specification, state *int) ParseResult { 23 if spec.Heading != nil { 24 return ParseResult{Ok: false, ParseErrors: []ParseError{ParseError{spec.FileName, token.LineNo, token.SpanEnd, "Multiple spec headings found in same file", token.LineText()}}} 25 } 26 27 spec.AddHeading(&gauge.Heading{LineNo: token.LineNo, Value: token.Value, SpanEnd: token.SpanEnd}) 28 addStates(state, specScope) 29 return ParseResult{Ok: true} 30 }) 31 32 scenarioConverter := converterFn(func(token *Token, state *int) bool { 33 return token.Kind == gauge.ScenarioKind 34 }, func(token *Token, spec *gauge.Specification, state *int) ParseResult { 35 if spec.Heading == nil { 36 return ParseResult{Ok: false, ParseErrors: []ParseError{ParseError{spec.FileName, token.LineNo, token.SpanEnd, "Scenario should be defined after the spec heading", token.LineText()}}} 37 } 38 for _, scenario := range spec.Scenarios { 39 if strings.EqualFold(scenario.Heading.Value, token.Value) { 40 return ParseResult{Ok: false, ParseErrors: []ParseError{ParseError{spec.FileName, token.LineNo, token.SpanEnd, "Duplicate scenario definition '" + scenario.Heading.Value + "' found in the same specification", token.LineText()}}} 41 } 42 } 43 scenario := &gauge.Scenario{Span: &gauge.Span{Start: token.LineNo, End: token.LineNo}} 44 if len(spec.Scenarios) > 0 { 45 spec.LatestScenario().Span.End = token.LineNo - 1 46 } 47 scenario.AddHeading(&gauge.Heading{Value: token.Value, LineNo: token.LineNo, SpanEnd: token.SpanEnd}) 48 spec.AddScenario(scenario) 49 50 retainStates(state, specScope) 51 addStates(state, scenarioScope) 52 return ParseResult{Ok: true} 53 }) 54 55 stepConverter := converterFn(func(token *Token, state *int) bool { 56 return token.Kind == gauge.StepKind && isInState(*state, scenarioScope) 57 }, func(token *Token, spec *gauge.Specification, state *int) ParseResult { 58 latestScenario := spec.LatestScenario() 59 stepToAdd, parseDetails := createStep(spec, latestScenario, token) 60 if stepToAdd == nil { 61 return ParseResult{ParseErrors: parseDetails.ParseErrors, Ok: false, Warnings: parseDetails.Warnings} 62 } 63 latestScenario.AddStep(stepToAdd) 64 retainStates(state, specScope, scenarioScope) 65 addStates(state, stepScope) 66 if parseDetails != nil && len(parseDetails.ParseErrors) > 0 { 67 return ParseResult{ParseErrors: parseDetails.ParseErrors, Ok: false, Warnings: parseDetails.Warnings} 68 } 69 if parseDetails.Warnings != nil { 70 return ParseResult{Ok: false, Warnings: parseDetails.Warnings} 71 } 72 return ParseResult{Ok: true, Warnings: parseDetails.Warnings} 73 }) 74 75 contextConverter := converterFn(func(token *Token, state *int) bool { 76 return token.Kind == gauge.StepKind && !isInState(*state, scenarioScope) && isInState(*state, specScope) && !isInState(*state, tearDownScope) 77 }, func(token *Token, spec *gauge.Specification, state *int) ParseResult { 78 stepToAdd, parseDetails := createStep(spec, nil, token) 79 if stepToAdd == nil { 80 return ParseResult{ParseErrors: parseDetails.ParseErrors, Ok: false, Warnings: parseDetails.Warnings} 81 } 82 spec.AddContext(stepToAdd) 83 retainStates(state, specScope) 84 addStates(state, contextScope) 85 if parseDetails != nil && len(parseDetails.ParseErrors) > 0 { 86 parseDetails.Ok = false 87 return *parseDetails 88 } 89 if parseDetails.Warnings != nil { 90 return ParseResult{Ok: false, Warnings: parseDetails.Warnings} 91 } 92 return ParseResult{Ok: true, Warnings: parseDetails.Warnings} 93 }) 94 95 tearDownConverter := converterFn(func(token *Token, state *int) bool { 96 return token.Kind == gauge.TearDownKind 97 }, func(token *Token, spec *gauge.Specification, state *int) ParseResult { 98 retainStates(state, specScope) 99 addStates(state, tearDownScope) 100 spec.AddItem(&gauge.TearDown{LineNo: token.LineNo, Value: token.Value}) 101 return ParseResult{Ok: true} 102 }) 103 104 tearDownStepConverter := converterFn(func(token *Token, state *int) bool { 105 return token.Kind == gauge.StepKind && isInState(*state, tearDownScope) 106 }, func(token *Token, spec *gauge.Specification, state *int) ParseResult { 107 stepToAdd, parseDetails := createStep(spec, nil, token) 108 if stepToAdd == nil { 109 return ParseResult{ParseErrors: parseDetails.ParseErrors, Ok: false, Warnings: parseDetails.Warnings} 110 } 111 spec.TearDownSteps = append(spec.TearDownSteps, stepToAdd) 112 spec.AddItem(stepToAdd) 113 retainStates(state, specScope, tearDownScope) 114 if parseDetails != nil && len(parseDetails.ParseErrors) > 0 { 115 parseDetails.Ok = false 116 return *parseDetails 117 } 118 if parseDetails.Warnings != nil { 119 return ParseResult{Ok: false, Warnings: parseDetails.Warnings} 120 } 121 return ParseResult{Ok: true, Warnings: parseDetails.Warnings} 122 }) 123 124 commentConverter := converterFn(func(token *Token, state *int) bool { 125 return token.Kind == gauge.CommentKind 126 }, func(token *Token, spec *gauge.Specification, state *int) ParseResult { 127 comment := &gauge.Comment{Value: token.Value, LineNo: token.LineNo} 128 if isInState(*state, scenarioScope) { 129 spec.LatestScenario().AddComment(comment) 130 } else { 131 spec.AddComment(comment) 132 } 133 retainStates(state, specScope, scenarioScope, tearDownScope) 134 addStates(state, commentScope) 135 return ParseResult{Ok: true} 136 }) 137 138 keywordConverter := converterFn(func(token *Token, state *int) bool { 139 return token.Kind == gauge.DataTableKind 140 }, func(token *Token, spec *gauge.Specification, state *int) ParseResult { 141 resolvedArg, err := newSpecialTypeResolver().resolve(token.Value) 142 if resolvedArg == nil || err != nil { 143 errMessage := fmt.Sprintf("Could not resolve table. %s", err) 144 gaugeDataDir := env.GaugeDataDir() 145 if gaugeDataDir != "." { 146 errMessage = fmt.Sprintf("%s GAUGE_DATA_DIR property is set to '%s', Gauge will look for data files in this location.", errMessage, gaugeDataDir) 147 } 148 e := ParseError{FileName: spec.FileName, LineNo: token.LineNo, LineText: token.LineText(), Message: errMessage} 149 return ParseResult{ParseErrors: []ParseError{e}, Ok: false} 150 } 151 if isInAnyState(*state, scenarioScope) { 152 scn := spec.LatestScenario() 153 if !scn.DataTable.IsInitialized() && env.AllowScenarioDatatable() { 154 externalTable := &gauge.DataTable{} 155 externalTable.Table = &resolvedArg.Table 156 externalTable.LineNo = token.LineNo 157 externalTable.Value = token.Value 158 externalTable.IsExternal = true 159 scn.AddExternalDataTable(externalTable) 160 } else { 161 value := "Multiple data table present, ignoring table" 162 scn.AddComment(&gauge.Comment{Value: token.LineText(), LineNo: token.LineNo}) 163 return ParseResult{Ok: false, Warnings: []*Warning{&Warning{spec.FileName, token.LineNo, token.SpanEnd, value}}} 164 } 165 } else if isInState(*state, specScope) && !spec.DataTable.IsInitialized() { 166 externalTable := &gauge.DataTable{} 167 externalTable.Table = &resolvedArg.Table 168 externalTable.LineNo = token.LineNo 169 externalTable.Value = token.Value 170 externalTable.IsExternal = true 171 spec.AddExternalDataTable(externalTable) 172 } else if isInState(*state, specScope) && spec.DataTable.IsInitialized() { 173 value := "Multiple data table present, ignoring table" 174 spec.AddComment(&gauge.Comment{Value: token.LineText(), LineNo: token.LineNo}) 175 return ParseResult{Ok: false, Warnings: []*Warning{&Warning{spec.FileName, token.LineNo, token.SpanEnd, value}}} 176 } else { 177 value := "Data table not associated with spec or scenario" 178 spec.AddComment(&gauge.Comment{Value: token.LineText(), LineNo: token.LineNo}) 179 return ParseResult{Ok: false, Warnings: []*Warning{&Warning{spec.FileName, token.LineNo, token.SpanEnd, value}}} 180 } 181 retainStates(state, specScope, scenarioScope) 182 addStates(state, keywordScope) 183 return ParseResult{Ok: true} 184 }) 185 186 tableHeaderConverter := converterFn(func(token *Token, state *int) bool { 187 return token.Kind == gauge.TableHeader && isInAnyState(*state, specScope) 188 }, func(token *Token, spec *gauge.Specification, state *int) ParseResult { 189 if isInState(*state, stepScope) { 190 latestScenario := spec.LatestScenario() 191 latestStep := latestScenario.LatestStep() 192 addInlineTableHeader(latestStep, token) 193 } else if isInState(*state, contextScope) { 194 latestContext := spec.LatestContext() 195 addInlineTableHeader(latestContext, token) 196 } else if isInState(*state, tearDownScope) { 197 if len(spec.TearDownSteps) > 0 { 198 latestTeardown := spec.LatestTeardown() 199 addInlineTableHeader(latestTeardown, token) 200 } else { 201 spec.AddComment(&gauge.Comment{Value: token.LineText(), LineNo: token.LineNo}) 202 } 203 } else if isInState(*state, scenarioScope) { 204 scn := spec.LatestScenario() 205 if !scn.DataTable.Table.IsInitialized() && env.AllowScenarioDatatable() { 206 dataTable := &gauge.Table{LineNo: token.LineNo} 207 dataTable.AddHeaders(token.Args) 208 scn.AddDataTable(dataTable) 209 } else { 210 scn.AddComment(&gauge.Comment{Value: token.LineText(), LineNo: token.LineNo}) 211 return ParseResult{Ok: false, Warnings: []*Warning{ 212 &Warning{spec.FileName, token.LineNo, token.SpanEnd, "Multiple data table present, ignoring table"}}} 213 } 214 } else { 215 if !spec.DataTable.Table.IsInitialized() { 216 dataTable := &gauge.Table{LineNo: token.LineNo} 217 dataTable.AddHeaders(token.Args) 218 spec.AddDataTable(dataTable) 219 } else { 220 spec.AddComment(&gauge.Comment{Value: token.LineText(), LineNo: token.LineNo}) 221 return ParseResult{Ok: false, Warnings: []*Warning{&Warning{spec.FileName, 222 token.LineNo, token.SpanEnd, "Multiple data table present, ignoring table"}}} 223 } 224 } 225 retainStates(state, specScope, scenarioScope, stepScope, contextScope, tearDownScope) 226 addStates(state, tableScope) 227 return ParseResult{Ok: true} 228 }) 229 230 tableRowConverter := converterFn(func(token *Token, state *int) bool { 231 return token.Kind == gauge.TableRow 232 }, func(token *Token, spec *gauge.Specification, state *int) ParseResult { 233 var result ParseResult 234 //When table is to be treated as a comment 235 if !isInState(*state, tableScope) { 236 if isInState(*state, scenarioScope) { 237 spec.LatestScenario().AddComment(&gauge.Comment{Value: token.LineText(), LineNo: token.LineNo}) 238 } else { 239 spec.AddComment(&gauge.Comment{Value: token.LineText(), LineNo: token.LineNo}) 240 } 241 } else if areUnderlined(token.Args) && !isInState(*state, tableSeparatorScope) { 242 retainStates(state, specScope, scenarioScope, stepScope, contextScope, tearDownScope, tableScope) 243 addStates(state, tableSeparatorScope) 244 // skip table separator 245 result = ParseResult{Ok: true} 246 } else if isInState(*state, stepScope) { 247 latestScenario := spec.LatestScenario() 248 tables := []*gauge.Table{spec.DataTable.Table} 249 if latestScenario.DataTable.IsInitialized() { 250 tables = append(tables, latestScenario.DataTable.Table) 251 } 252 latestStep := latestScenario.LatestStep() 253 result = addInlineTableRow(latestStep, token, new(gauge.ArgLookup).FromDataTables(tables...), spec.FileName) 254 } else if isInState(*state, contextScope) { 255 latestContext := spec.LatestContext() 256 result = addInlineTableRow(latestContext, token, new(gauge.ArgLookup).FromDataTables(spec.DataTable.Table), spec.FileName) 257 } else if isInState(*state, tearDownScope) { 258 if len(spec.TearDownSteps) > 0 { 259 latestTeardown := spec.LatestTeardown() 260 result = addInlineTableRow(latestTeardown, token, new(gauge.ArgLookup).FromDataTables(spec.DataTable.Table), spec.FileName) 261 } else { 262 spec.AddComment(&gauge.Comment{Value: token.LineText(), LineNo: token.LineNo}) 263 } 264 } else { 265 t := spec.DataTable 266 if isInState(*state, scenarioScope) && env.AllowScenarioDatatable() { 267 t = spec.LatestScenario().DataTable 268 } 269 270 tableValues, warnings, err := validateTableRows(token, new(gauge.ArgLookup).FromDataTables(t.Table), spec.FileName) 271 if len(err) > 0 { 272 result = ParseResult{Ok: false, Warnings: warnings, ParseErrors: err} 273 } else { 274 t.Table.AddRowValues(tableValues) 275 result = ParseResult{Ok: true, Warnings: warnings} 276 } 277 } 278 retainStates(state, specScope, scenarioScope, stepScope, contextScope, tearDownScope, tableScope, tableSeparatorScope) 279 return result 280 }) 281 282 tagConverter := converterFn(func(token *Token, state *int) bool { 283 return (token.Kind == gauge.TagKind) 284 }, func(token *Token, spec *gauge.Specification, state *int) ParseResult { 285 tags := &gauge.Tags{RawValues: [][]string{token.Args}} 286 if isInState(*state, scenarioScope) { 287 if isInState(*state, tagsScope) { 288 spec.LatestScenario().Tags.Add(tags.RawValues[0]) 289 } else { 290 if spec.LatestScenario().NTags() != 0 { 291 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()}}} 292 } 293 spec.LatestScenario().AddTags(tags) 294 } 295 } else { 296 if isInState(*state, tagsScope) { 297 spec.Tags.Add(tags.RawValues[0]) 298 } else { 299 if spec.NTags() != 0 { 300 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()}}} 301 } 302 spec.AddTags(tags) 303 } 304 } 305 addStates(state, tagsScope) 306 return ParseResult{Ok: true} 307 }) 308 309 converter := []func(*Token, *int, *gauge.Specification) ParseResult{ 310 specConverter, scenarioConverter, stepConverter, contextConverter, commentConverter, tableHeaderConverter, tableRowConverter, tagConverter, keywordConverter, tearDownConverter, tearDownStepConverter, 311 } 312 313 return converter 314 } 315 316 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 { 317 return func(token *Token, state *int, spec *gauge.Specification) ParseResult { 318 if !predicate(token, state) { 319 return ParseResult{Ok: true} 320 } 321 return apply(token, spec, state) 322 } 323 } 324 325 //Step value is modified when inline table is found to account for the new parameter by appending {} 326 //todo validate headers for dynamic 327 func addInlineTableHeader(step *gauge.Step, token *Token) { 328 step.Value = fmt.Sprintf("%s %s", step.Value, gauge.ParameterPlaceholder) 329 step.HasInlineTable = true 330 step.AddInlineTableHeaders(token.Args) 331 } 332 333 func addInlineTableRow(step *gauge.Step, token *Token, argLookup *gauge.ArgLookup, fileName string) ParseResult { 334 tableValues, warnings, err := validateTableRows(token, argLookup, fileName) 335 if len(err) > 0 { 336 return ParseResult{Ok: false, Warnings: warnings, ParseErrors: err} 337 } 338 step.AddInlineTableRow(tableValues) 339 return ParseResult{Ok: true, Warnings: warnings} 340 } 341 342 func validateTableRows(token *Token, argLookup *gauge.ArgLookup, fileName string) ([]gauge.TableCell, []*Warning, []ParseError) { 343 dynamicArgMatcher := regexp.MustCompile("^<(.*)>$") 344 specialArgMatcher := regexp.MustCompile("^<(file:.*)>$") 345 tableValues := make([]gauge.TableCell, 0) 346 warnings := make([]*Warning, 0) 347 error := make([]ParseError, 0) 348 for _, tableValue := range token.Args { 349 if specialArgMatcher.MatchString(tableValue) { 350 match := specialArgMatcher.FindAllStringSubmatch(tableValue, -1) 351 param := match[0][1] 352 file := strings.TrimSpace(strings.TrimPrefix(param, "file:")) 353 tableValues = append(tableValues, gauge.TableCell{Value: param, CellType: gauge.SpecialString}) 354 if _, err := util.GetFileContents(file); err != nil { 355 error = append(error, ParseError{FileName: fileName, LineNo: token.LineNo, Message: fmt.Sprintf("Dynamic param <%s> could not be resolved, Missing file: %s", param, file), LineText: token.LineText()}) 356 } 357 } else if dynamicArgMatcher.MatchString(tableValue) { 358 match := dynamicArgMatcher.FindAllStringSubmatch(tableValue, -1) 359 param := match[0][1] 360 if !argLookup.ContainsArg(param) { 361 tableValues = append(tableValues, gauge.TableCell{Value: tableValue, CellType: gauge.Static}) 362 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)}) 363 } else { 364 tableValues = append(tableValues, gauge.TableCell{Value: param, CellType: gauge.Dynamic}) 365 } 366 } else { 367 tableValues = append(tableValues, gauge.TableCell{Value: tableValue, CellType: gauge.Static}) 368 } 369 } 370 return tableValues, warnings, error 371 }