github.com/getgauge/gauge@v1.6.9/validation/validate.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  Validation invokes language runner for every step in serial fashion with the StepValidateRequest and runner gets back with the StepValidateResponse.
     9  
    10  Step Level validation
    11  	1. Duplicate step implementation
    12  	2. Step implementation not found : Prints a step implementation stub for every unimplemented step
    13  
    14  If there is a validation error it skips that scenario and executes other scenarios in the spec.
    15  */
    16  package validation
    17  
    18  import (
    19  	"errors"
    20  	"fmt"
    21  	"os"
    22  	"strconv"
    23  	"strings"
    24  
    25  	gm "github.com/getgauge/gauge-proto/go/gauge_messages"
    26  	"github.com/getgauge/gauge/api"
    27  	"github.com/getgauge/gauge/gauge"
    28  	"github.com/getgauge/gauge/logger"
    29  	"github.com/getgauge/gauge/parser"
    30  	"github.com/getgauge/gauge/runner"
    31  	"github.com/getgauge/gauge/util"
    32  )
    33  
    34  // TableRows is used to check for table rows range validation.
    35  var TableRows = ""
    36  
    37  // HideSuggestion is used decide whether suggestion should be given for the unimplemented step or not based on the flag : --hide-suggestion.
    38  var HideSuggestion bool
    39  
    40  type validator struct {
    41  	specsToExecute     []*gauge.Specification
    42  	runner             runner.Runner
    43  	conceptsDictionary *gauge.ConceptDictionary
    44  }
    45  
    46  type SpecValidator struct {
    47  	specification       *gauge.Specification
    48  	runner              runner.Runner
    49  	conceptsDictionary  *gauge.ConceptDictionary
    50  	validationErrors    []error
    51  	stepValidationCache map[string]error
    52  }
    53  
    54  type StepValidationError struct {
    55  	step       *gauge.Step
    56  	message    string
    57  	fileName   string
    58  	errorType  *gm.StepValidateResponse_ErrorType
    59  	suggestion string
    60  }
    61  
    62  type SpecValidationError struct {
    63  	message  string
    64  	fileName string
    65  }
    66  
    67  func (s StepValidationError) Message() string {
    68  	return s.message
    69  }
    70  
    71  func (s StepValidationError) Step() *gauge.Step {
    72  	return s.step
    73  }
    74  
    75  func (s StepValidationError) FileName() string {
    76  	return s.fileName
    77  }
    78  
    79  func (s StepValidationError) ErrorType() gm.StepValidateResponse_ErrorType {
    80  	return *s.errorType
    81  }
    82  
    83  // Error prints a step validation error with filename, line number, error message, step text and suggestion in case of step implementation not found.
    84  func (s StepValidationError) Error() string {
    85  	return fmt.Sprintf("%s:%d %s => '%s'", s.fileName, s.step.LineNo, s.message, s.step.GetLineText())
    86  }
    87  
    88  func (s StepValidationError) Suggestion() string {
    89  	return s.suggestion
    90  }
    91  
    92  // Error prints a spec validation error with filename and error message.
    93  func (s SpecValidationError) Error() string {
    94  	return fmt.Sprintf("%s %s", s.fileName, s.message)
    95  }
    96  
    97  // NewSpecValidationError generates new spec validation error with error message and filename.
    98  func NewSpecValidationError(m string, f string) SpecValidationError {
    99  	return SpecValidationError{message: m, fileName: f}
   100  }
   101  
   102  // NewStepValidationError generates new step validation error with error message, filename and error type.
   103  func NewStepValidationError(s *gauge.Step, m string, f string, e *gm.StepValidateResponse_ErrorType, suggestion string) StepValidationError {
   104  	return StepValidationError{step: s, message: m, fileName: f, errorType: e, suggestion: suggestion}
   105  }
   106  
   107  // Validate validates specs and if it has any errors, it exits.
   108  func Validate(args []string) {
   109  	if len(args) == 0 {
   110  		args = append(args, util.GetSpecDirs()...)
   111  	}
   112  	res := ValidateSpecs(args, false)
   113  	if len(res.Errs) > 0 {
   114  		os.Exit(1)
   115  	}
   116  	if res.SpecCollection.Size() < 1 {
   117  		logger.Infof(true, "No specifications found in %s.", strings.Join(args, ", "))
   118  		err := res.Runner.Kill()
   119  		if err != nil {
   120  			logger.Errorf(false, "unable to kill runner: %s", err.Error())
   121  		}
   122  		if res.ParseOk {
   123  			os.Exit(0)
   124  		}
   125  		os.Exit(1)
   126  	}
   127  	err := res.Runner.Kill()
   128  	if err != nil {
   129  		logger.Errorf(false, "unable to kill runner: %s", err.Error())
   130  	}
   131  
   132  	if res.ErrMap.HasErrors() {
   133  		os.Exit(1)
   134  	}
   135  	logger.Infof(true, "No errors found.")
   136  }
   137  
   138  //TODO : duplicate in execute.go. Need to fix runner init.
   139  func startAPI(debug bool) runner.Runner {
   140  	sc := api.StartAPI(debug)
   141  	select {
   142  	case runner := <-sc.RunnerChan:
   143  		return runner
   144  	case err := <-sc.ErrorChan:
   145  		logger.Fatalf(true, "Failed to start gauge API: %s", err.Error())
   146  	}
   147  	return nil
   148  }
   149  
   150  type ValidationResult struct {
   151  	SpecCollection *gauge.SpecCollection
   152  	ErrMap         *gauge.BuildErrors
   153  	Runner         runner.Runner
   154  	Errs           []error
   155  	ParseOk        bool
   156  }
   157  
   158  // NewValidationResult creates a new Validation result
   159  func NewValidationResult(s *gauge.SpecCollection, errMap *gauge.BuildErrors, r runner.Runner, parseOk bool, e ...error) *ValidationResult {
   160  	return &ValidationResult{SpecCollection: s, ErrMap: errMap, Runner: r, ParseOk: parseOk, Errs: e}
   161  }
   162  
   163  // ValidateSpecs parses the specs, creates a new validator and call the runner to get the validation result.
   164  func ValidateSpecs(specsToValidate []string, debug bool) *ValidationResult {
   165  	logger.Debug(true, "Parsing started.")
   166  	conceptDict, res, err := parser.ParseConcepts()
   167  	if err != nil {
   168  		logger.Fatalf(true, "Unable to parse : %s", err.Error())
   169  	}
   170  	errMap := gauge.NewBuildErrors()
   171  	specs, specsFailed := parser.ParseSpecs(specsToValidate, conceptDict, errMap)
   172  	logger.Debug(true, "Parsing completed.")
   173  	r := startAPI(debug)
   174  	validationErrors := NewValidator(specs, r, conceptDict).Validate()
   175  	errMap = getErrMap(errMap, validationErrors)
   176  	specs = parser.GetSpecsForDataTableRows(specs, errMap)
   177  	printValidationFailures(validationErrors)
   178  	showSuggestion(validationErrors)
   179  	if !res.Ok {
   180  		err := r.Kill()
   181  		if err != nil {
   182  			logger.Errorf(true, "unable to kill runner: %s", err.Error())
   183  		}
   184  		return NewValidationResult(nil, nil, nil, false, errors.New("Parsing failed"))
   185  	}
   186  	if specsFailed {
   187  		return NewValidationResult(gauge.NewSpecCollection(specs, false), errMap, r, false)
   188  	}
   189  	return NewValidationResult(gauge.NewSpecCollection(specs, false), errMap, r, true)
   190  }
   191  
   192  func getErrMap(errMap *gauge.BuildErrors, validationErrors validationErrors) *gauge.BuildErrors {
   193  	for spec, valErrors := range validationErrors {
   194  		for _, err := range valErrors {
   195  			switch e := err.(type) {
   196  			case StepValidationError:
   197  				errMap.StepErrs[e.step] = e
   198  			case SpecValidationError:
   199  				errMap.SpecErrs[spec] = append(errMap.SpecErrs[spec], err.(SpecValidationError))
   200  			}
   201  		}
   202  		skippedScnInSpec := 0
   203  		for _, scenario := range spec.Scenarios {
   204  			fillScenarioErrors(scenario, errMap, scenario.Steps)
   205  			if _, ok := errMap.ScenarioErrs[scenario]; ok {
   206  				skippedScnInSpec++
   207  			}
   208  		}
   209  		if len(spec.Scenarios) > 0 && skippedScnInSpec == len(spec.Scenarios) {
   210  			errMap.SpecErrs[spec] = append(errMap.SpecErrs[spec], errMap.ScenarioErrs[spec.Scenarios[0]]...)
   211  		}
   212  		fillSpecErrors(spec, errMap, append(spec.Contexts, spec.TearDownSteps...))
   213  	}
   214  	return errMap
   215  }
   216  
   217  func fillScenarioErrors(scenario *gauge.Scenario, errMap *gauge.BuildErrors, steps []*gauge.Step) {
   218  	for _, step := range steps {
   219  		if step.IsConcept {
   220  			fillScenarioErrors(scenario, errMap, step.ConceptSteps)
   221  		}
   222  		if err, ok := errMap.StepErrs[step]; ok { // nolint
   223  			errMap.ScenarioErrs[scenario] = append(errMap.ScenarioErrs[scenario], err)
   224  		}
   225  	}
   226  }
   227  
   228  func fillSpecErrors(spec *gauge.Specification, errMap *gauge.BuildErrors, steps []*gauge.Step) {
   229  	for _, context := range steps {
   230  		if context.IsConcept {
   231  			fillSpecErrors(spec, errMap, context.ConceptSteps)
   232  		}
   233  		if err, ok := errMap.StepErrs[context]; ok { // nolint
   234  			errMap.SpecErrs[spec] = append(errMap.SpecErrs[spec], err)
   235  			for _, scenario := range spec.Scenarios {
   236  				if _, ok := errMap.ScenarioErrs[scenario]; !ok {
   237  					errMap.ScenarioErrs[scenario] = append(errMap.ScenarioErrs[scenario], err)
   238  				}
   239  			}
   240  		}
   241  	}
   242  }
   243  
   244  func printValidationFailures(validationErrors validationErrors) {
   245  	for _, e := range FilterDuplicates(validationErrors) {
   246  		logger.Errorf(true, "[ValidationError] %s", e.Error())
   247  	}
   248  }
   249  
   250  func FilterDuplicates(validationErrors validationErrors) []error {
   251  	filteredErrs := make([]error, 0)
   252  	exists := make(map[string]bool)
   253  	for _, errs := range validationErrors {
   254  		for _, e := range errs {
   255  			var val string
   256  			if vErr, ok := e.(StepValidationError); ok {
   257  				val = vErr.step.Value + vErr.step.FileName + strconv.Itoa(e.(StepValidationError).step.LineNo)
   258  			} else if vErr, ok := e.(SpecValidationError); ok {
   259  				val = vErr.message + vErr.fileName
   260  			} else {
   261  				continue
   262  			}
   263  			if _, ok := exists[val]; !ok {
   264  				exists[val] = true
   265  				filteredErrs = append(filteredErrs, e)
   266  			}
   267  		}
   268  	}
   269  	return filteredErrs
   270  }
   271  
   272  type validationErrors map[*gauge.Specification][]error
   273  
   274  func NewValidator(s []*gauge.Specification, r runner.Runner, c *gauge.ConceptDictionary) *validator {
   275  	return &validator{specsToExecute: s, runner: r, conceptsDictionary: c}
   276  }
   277  
   278  func (v *validator) Validate() validationErrors {
   279  	validationStatus := make(validationErrors)
   280  	logger.Debug(true, "Validation started.")
   281  	specValidator := &SpecValidator{runner: v.runner, conceptsDictionary: v.conceptsDictionary, stepValidationCache: make(map[string]error)}
   282  	for _, spec := range v.specsToExecute {
   283  		specValidator.specification = spec
   284  		validationErrors := specValidator.validate()
   285  		if len(validationErrors) != 0 {
   286  			validationStatus[spec] = validationErrors
   287  		}
   288  	}
   289  	if len(validationStatus) > 0 {
   290  		return validationStatus
   291  	}
   292  	logger.Debug(true, "Validation completed.")
   293  	return nil
   294  }
   295  
   296  func (v *SpecValidator) validate() []error {
   297  	queue := &gauge.ItemQueue{Items: v.specification.AllItems()}
   298  	v.specification.Traverse(v, queue)
   299  	return v.validationErrors
   300  }
   301  
   302  // Validates a step. If validation result from runner is not valid then it creates a new validation error.
   303  // If the error type is StepValidateResponse_STEP_IMPLEMENTATION_NOT_FOUND then gives suggestion with step implementation stub.
   304  func (v *SpecValidator) Step(s *gauge.Step) {
   305  	if s.IsConcept {
   306  		for _, c := range s.ConceptSteps {
   307  			v.Step(c)
   308  		}
   309  		return
   310  	}
   311  	val, ok := v.stepValidationCache[s.Value]
   312  	if !ok {
   313  		err := v.validateStep(s)
   314  		if err != nil {
   315  			v.validationErrors = append(v.validationErrors, err)
   316  		}
   317  		v.stepValidationCache[s.Value] = err
   318  		return
   319  	}
   320  	if val != nil {
   321  		valErr := val.(StepValidationError)
   322  		if s.Parent == nil {
   323  			v.validationErrors = append(v.validationErrors,
   324  				NewStepValidationError(s, valErr.message, v.specification.FileName, valErr.errorType, valErr.suggestion))
   325  		} else {
   326  			cpt := v.conceptsDictionary.Search(s.Parent.Value)
   327  			v.validationErrors = append(v.validationErrors,
   328  				NewStepValidationError(s, valErr.message, cpt.FileName, valErr.errorType, valErr.suggestion))
   329  		}
   330  	}
   331  }
   332  
   333  var invalidResponse gm.StepValidateResponse_ErrorType = -1
   334  
   335  func (v *SpecValidator) validateStep(s *gauge.Step) error {
   336  	stepValue, err := parser.ExtractStepValueAndParams(s.LineText, s.HasInlineTable)
   337  	if err != nil {
   338  		return nil
   339  	}
   340  	protoStepValue := gauge.ConvertToProtoStepValue(stepValue)
   341  
   342  	m := &gm.Message{MessageType: gm.Message_StepValidateRequest,
   343  		StepValidateRequest: &gm.StepValidateRequest{StepText: s.Value, NumberOfParameters: int32(len(s.Args)), StepValue: protoStepValue}}
   344  
   345  	r, err := v.runner.ExecuteMessageWithTimeout(m)
   346  	if err != nil {
   347  		return NewStepValidationError(s, err.Error(), v.specification.FileName, &invalidResponse, "")
   348  	}
   349  	if r.GetMessageType() == gm.Message_StepValidateResponse {
   350  		res := r.GetStepValidateResponse()
   351  		if !res.GetIsValid() {
   352  			msg := getMessage(res.GetErrorType().String())
   353  			suggestion := res.GetSuggestion()
   354  			if s.Parent == nil {
   355  				vErr := NewStepValidationError(s, msg, v.specification.FileName, &res.ErrorType, suggestion)
   356  				return vErr
   357  			}
   358  			cpt := v.conceptsDictionary.Search(s.Parent.Value)
   359  			vErr := NewStepValidationError(s, msg, cpt.FileName, &res.ErrorType, suggestion)
   360  			return vErr
   361  
   362  		}
   363  		return nil
   364  	}
   365  	return NewStepValidationError(s, "Invalid response from runner for Validation request", v.specification.FileName, &invalidResponse, "")
   366  }
   367  
   368  func getMessage(message string) string {
   369  	lower := strings.ToLower(strings.Replace(message, "_", " ", -1))
   370  	return strings.ToUpper(lower[:1]) + lower[1:]
   371  }
   372  
   373  func (v *SpecValidator) TearDown(step *gauge.TearDown) {
   374  }
   375  
   376  func (v *SpecValidator) Heading(heading *gauge.Heading) {
   377  }
   378  
   379  func (v *SpecValidator) Tags(tags *gauge.Tags) {
   380  }
   381  
   382  func (v *SpecValidator) Table(dataTable *gauge.Table) {
   383  
   384  }
   385  
   386  func (v *SpecValidator) Scenario(scenario *gauge.Scenario) {
   387  
   388  }
   389  
   390  func (v *SpecValidator) Comment(comment *gauge.Comment) {
   391  }
   392  
   393  func (v *SpecValidator) DataTable(dataTable *gauge.DataTable) {
   394  
   395  }
   396  
   397  // Validates data table for the range, if any error found append to the validation errors
   398  func (v *SpecValidator) Specification(specification *gauge.Specification) {
   399  	v.validationErrors = make([]error, 0)
   400  	err := validateDataTableRange(specification.DataTable.Table.GetRowCount())
   401  	if err != nil {
   402  		v.validationErrors = append(v.validationErrors, NewSpecValidationError(err.Error(), specification.FileName))
   403  	}
   404  }
   405  
   406  func validateDataTableRange(rowCount int) error {
   407  	if TableRows == "" {
   408  		return nil
   409  	}
   410  	if strings.Contains(TableRows, "-") {
   411  		indexes := strings.Split(TableRows, "-")
   412  		if len(indexes) > 2 {
   413  			return fmt.Errorf("Table rows range '%s' is invalid => Table rows range should be of format rowNumber-rowNumber", TableRows)
   414  		}
   415  		if err := validateTableRow(indexes[0], rowCount); err != nil {
   416  			return err
   417  		}
   418  		if err := validateTableRow(indexes[1], rowCount); err != nil {
   419  			return err
   420  		}
   421  	} else {
   422  		indexes := strings.Split(TableRows, ",")
   423  		for _, i := range indexes {
   424  			if err := validateTableRow(i, rowCount); err != nil {
   425  				return err
   426  			}
   427  		}
   428  	}
   429  	return nil
   430  }
   431  
   432  func validateTableRow(rowNumber string, rowCount int) error {
   433  	if rowNumber = strings.TrimSpace(rowNumber); rowNumber == "" {
   434  		return fmt.Errorf("Table rows range validation failed => Row number cannot be empty")
   435  	}
   436  	row, err := strconv.Atoi(rowNumber)
   437  	if err != nil {
   438  		return fmt.Errorf("Table rows range validation failed => Failed to parse '%s' to row number", rowNumber)
   439  	}
   440  	if row < 1 || row > rowCount {
   441  		return fmt.Errorf("Table rows range validation failed => Table row number '%d' is out of range", row)
   442  	}
   443  	return nil
   444  }