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  }