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  }