github.com/ezbuy/gauge@v0.9.4-0.20171013092048-7ac5bd3931cd/validation/validate.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 validation
    19  
    20  import (
    21  	"errors"
    22  	"fmt"
    23  	"os"
    24  	"strconv"
    25  	"strings"
    26  
    27  	"github.com/getgauge/common"
    28  	"github.com/getgauge/gauge/api"
    29  	"github.com/getgauge/gauge/config"
    30  	"github.com/getgauge/gauge/conn"
    31  	"github.com/getgauge/gauge/gauge"
    32  	gm "github.com/getgauge/gauge/gauge_messages"
    33  	"github.com/getgauge/gauge/logger"
    34  	"github.com/getgauge/gauge/manifest"
    35  	"github.com/getgauge/gauge/parser"
    36  	"github.com/getgauge/gauge/runner"
    37  )
    38  
    39  var TableRows = ""
    40  var HideSuggestion bool
    41  
    42  type validator struct {
    43  	manifest           *manifest.Manifest
    44  	specsToExecute     []*gauge.Specification
    45  	runner             runner.Runner
    46  	conceptsDictionary *gauge.ConceptDictionary
    47  }
    48  
    49  type specValidator struct {
    50  	specification       *gauge.Specification
    51  	runner              runner.Runner
    52  	conceptsDictionary  *gauge.ConceptDictionary
    53  	validationErrors    []error
    54  	stepValidationCache map[string]error
    55  }
    56  
    57  type StepValidationError struct {
    58  	step       *gauge.Step
    59  	message    string
    60  	fileName   string
    61  	errorType  *gm.StepValidateResponse_ErrorType
    62  	suggestion string
    63  }
    64  
    65  type SpecValidationError struct {
    66  	message  string
    67  	fileName string
    68  }
    69  
    70  func (s StepValidationError) Error() string {
    71  	return fmt.Sprintf("%s:%d %s => '%s'", s.fileName, s.step.LineNo, s.message, s.step.GetLineText())
    72  }
    73  
    74  func (s StepValidationError) Suggestion() string {
    75  	return s.suggestion
    76  }
    77  
    78  func (s SpecValidationError) Error() string {
    79  	return fmt.Sprintf("%s %s", s.fileName, s.message)
    80  }
    81  
    82  func NewSpecValidationError(m string, f string) SpecValidationError {
    83  	return SpecValidationError{message: m, fileName: f}
    84  }
    85  
    86  func NewStepValidationError(s *gauge.Step, m string, f string, e *gm.StepValidateResponse_ErrorType) StepValidationError {
    87  	return StepValidationError{step: s, message: m, fileName: f, errorType: e}
    88  }
    89  
    90  func Validate(args []string) {
    91  	if len(args) == 0 {
    92  		args = append(args, common.SpecsDirectoryName)
    93  	}
    94  	res := ValidateSpecs(args, false)
    95  	if len(res.Errs) > 0 {
    96  		os.Exit(1)
    97  	}
    98  	if res.SpecCollection.Size() < 1 {
    99  		logger.Infof("No specifications found in %s.", strings.Join(args, ", "))
   100  		res.Runner.Kill()
   101  		if res.ParseOk {
   102  			os.Exit(0)
   103  		}
   104  		os.Exit(1)
   105  	}
   106  	res.Runner.Kill()
   107  	if res.ErrMap.HasErrors() {
   108  		os.Exit(1)
   109  	}
   110  	logger.Infof("No error found.")
   111  }
   112  
   113  //TODO : duplicate in execute.go. Need to fix runner init.
   114  func startAPI(debug bool) runner.Runner {
   115  	sc := api.StartAPI(debug)
   116  	select {
   117  	case runner := <-sc.RunnerChan:
   118  		return runner
   119  	case err := <-sc.ErrorChan:
   120  		logger.Fatalf("Failed to start gauge API: %s", err.Error())
   121  	}
   122  	return nil
   123  }
   124  
   125  type ValidationResult struct {
   126  	SpecCollection *gauge.SpecCollection
   127  	ErrMap         *gauge.BuildErrors
   128  	Runner         runner.Runner
   129  	Errs           []error
   130  	ParseOk        bool
   131  }
   132  
   133  func NewValidationResult(s *gauge.SpecCollection, errMap *gauge.BuildErrors, r runner.Runner, parseOk bool, e ...error) *ValidationResult {
   134  	return &ValidationResult{SpecCollection: s, ErrMap: errMap, Runner: r, ParseOk: parseOk, Errs: e}
   135  }
   136  
   137  func ValidateSpecs(args []string, debug bool) *ValidationResult {
   138  	manifest, err := manifest.ProjectManifest()
   139  	if err != nil {
   140  		logger.Errorf(err.Error())
   141  		return NewValidationResult(nil, nil, nil, false, err)
   142  	}
   143  	conceptDict, res := parser.ParseConcepts()
   144  	errMap := gauge.NewBuildErrors()
   145  	s, specsFailed := parser.ParseSpecs(args, conceptDict, errMap)
   146  	r := startAPI(debug)
   147  	vErrs := newValidator(manifest, s, r, conceptDict).validate()
   148  	errMap = getErrMap(errMap, vErrs)
   149  	s = parser.GetSpecsForDataTableRows(s, errMap)
   150  	printValidationFailures(vErrs)
   151  	showSuggestion(vErrs)
   152  	if !res.Ok {
   153  		r.Kill()
   154  		return NewValidationResult(nil, nil, nil, false, errors.New("Parsing failed."))
   155  	}
   156  	if specsFailed {
   157  		return NewValidationResult(gauge.NewSpecCollection(s, false), errMap, r, false)
   158  	}
   159  	return NewValidationResult(gauge.NewSpecCollection(s, false), errMap, r, true)
   160  }
   161  
   162  func getErrMap(errMap *gauge.BuildErrors, validationErrors validationErrors) *gauge.BuildErrors {
   163  	for spec, valErrors := range validationErrors {
   164  		for _, err := range valErrors {
   165  			switch err.(type) {
   166  			case StepValidationError:
   167  				errMap.StepErrs[err.(StepValidationError).step] = err.(StepValidationError)
   168  			case SpecValidationError:
   169  				errMap.SpecErrs[spec] = append(errMap.SpecErrs[spec], err.(SpecValidationError))
   170  			}
   171  		}
   172  		skippedScnInSpec := 0
   173  		for _, scenario := range spec.Scenarios {
   174  			fillScenarioErrors(scenario, errMap, scenario.Steps)
   175  			if _, ok := errMap.ScenarioErrs[scenario]; ok {
   176  				skippedScnInSpec++
   177  			}
   178  		}
   179  		if len(spec.Scenarios) > 0 && skippedScnInSpec == len(spec.Scenarios) {
   180  			errMap.SpecErrs[spec] = append(errMap.SpecErrs[spec], errMap.ScenarioErrs[spec.Scenarios[0]]...)
   181  		}
   182  		fillSpecErrors(spec, errMap, append(spec.Contexts, spec.TearDownSteps...))
   183  	}
   184  	return errMap
   185  }
   186  
   187  func fillScenarioErrors(scenario *gauge.Scenario, errMap *gauge.BuildErrors, steps []*gauge.Step) {
   188  	for _, step := range steps {
   189  		if step.IsConcept {
   190  			fillScenarioErrors(scenario, errMap, step.ConceptSteps)
   191  		}
   192  		if err, ok := errMap.StepErrs[step]; ok {
   193  			errMap.ScenarioErrs[scenario] = append(errMap.ScenarioErrs[scenario], err)
   194  		}
   195  	}
   196  }
   197  
   198  func fillSpecErrors(spec *gauge.Specification, errMap *gauge.BuildErrors, steps []*gauge.Step) {
   199  	for _, context := range steps {
   200  		if context.IsConcept {
   201  			fillSpecErrors(spec, errMap, context.ConceptSteps)
   202  		}
   203  		if err, ok := errMap.StepErrs[context]; ok {
   204  			errMap.SpecErrs[spec] = append(errMap.SpecErrs[spec], err)
   205  			for _, scenario := range spec.Scenarios {
   206  				if _, ok := errMap.ScenarioErrs[scenario]; !ok {
   207  					errMap.ScenarioErrs[scenario] = append(errMap.ScenarioErrs[scenario], err)
   208  				}
   209  			}
   210  		}
   211  	}
   212  }
   213  
   214  func printValidationFailures(validationErrors validationErrors) {
   215  	for _, errs := range validationErrors {
   216  		for _, e := range errs {
   217  			logger.Errorf("[ValidationError] %s", e.Error())
   218  		}
   219  	}
   220  }
   221  
   222  type validationErrors map[*gauge.Specification][]error
   223  
   224  func newValidator(m *manifest.Manifest, s []*gauge.Specification, r runner.Runner, c *gauge.ConceptDictionary) *validator {
   225  	return &validator{manifest: m, specsToExecute: s, runner: r, conceptsDictionary: c}
   226  }
   227  
   228  func (v *validator) validate() validationErrors {
   229  	validationStatus := make(validationErrors)
   230  	specValidator := &specValidator{runner: v.runner, conceptsDictionary: v.conceptsDictionary, stepValidationCache: make(map[string]error)}
   231  	for _, spec := range v.specsToExecute {
   232  		specValidator.specification = spec
   233  		validationErrors := specValidator.validate()
   234  		if len(validationErrors) != 0 {
   235  			validationStatus[spec] = validationErrors
   236  		}
   237  	}
   238  	if len(validationStatus) > 0 {
   239  		return validationStatus
   240  	}
   241  	return nil
   242  }
   243  
   244  func (v *specValidator) validate() []error {
   245  	queue := &gauge.ItemQueue{Items: v.specification.AllItems()}
   246  	v.specification.Traverse(v, queue)
   247  	return v.validationErrors
   248  }
   249  
   250  func (v *specValidator) Step(s *gauge.Step) {
   251  	if s.IsConcept {
   252  		for _, c := range s.ConceptSteps {
   253  			v.Step(c)
   254  		}
   255  		return
   256  	}
   257  	val, ok := v.stepValidationCache[s.Value]
   258  	if !ok {
   259  		err := v.validateStep(s)
   260  		if err != nil {
   261  			v.validationErrors = append(v.validationErrors, err)
   262  		}
   263  		v.stepValidationCache[s.Value] = err
   264  		return
   265  	}
   266  	if val != nil {
   267  		valErr := val.(StepValidationError)
   268  		if s.Parent == nil {
   269  			v.validationErrors = append(v.validationErrors,
   270  				NewStepValidationError(s, valErr.message, v.specification.FileName, valErr.errorType))
   271  		} else {
   272  			cpt := v.conceptsDictionary.Search(s.Parent.Value)
   273  			v.validationErrors = append(v.validationErrors,
   274  				NewStepValidationError(s, valErr.message, cpt.FileName, valErr.errorType))
   275  		}
   276  	}
   277  }
   278  
   279  var invalidResponse gm.StepValidateResponse_ErrorType = -1
   280  
   281  var getResponseFromRunner = func(m *gm.Message, v *specValidator) (*gm.Message, error) {
   282  	return conn.GetResponseForMessageWithTimeout(m, v.runner.Connection(), config.RunnerRequestTimeout())
   283  }
   284  
   285  func (v *specValidator) validateStep(s *gauge.Step) error {
   286  	stepValue, _ := parser.ExtractStepValueAndParams(s.LineText, s.HasInlineTable)
   287  	protoStepValue := gauge.ConvertToProtoStepValue(stepValue)
   288  
   289  	m := &gm.Message{MessageType: gm.Message_StepValidateRequest,
   290  		StepValidateRequest: &gm.StepValidateRequest{StepText: s.Value, NumberOfParameters: int32(len(s.Args)), StepValue: protoStepValue}}
   291  
   292  	r, err := getResponseFromRunner(m, v)
   293  	if err != nil {
   294  		return NewStepValidationError(s, err.Error(), v.specification.FileName, nil)
   295  	}
   296  	if r.GetMessageType() == gm.Message_StepValidateResponse {
   297  		res := r.GetStepValidateResponse()
   298  		if !res.GetIsValid() {
   299  			msg := getMessage(res.GetErrorType().String())
   300  			suggestion := res.GetSuggestion()
   301  			if s.Parent == nil {
   302  				vErr := NewStepValidationError(s, msg, v.specification.FileName, &res.ErrorType)
   303  				vErr.suggestion = suggestion
   304  				return vErr
   305  			}
   306  			cpt := v.conceptsDictionary.Search(s.Parent.Value)
   307  			vErr := NewStepValidationError(s, msg, cpt.FileName, &res.ErrorType)
   308  			vErr.suggestion = suggestion
   309  			return vErr
   310  
   311  		}
   312  		return nil
   313  	}
   314  	return NewStepValidationError(s, "Invalid response from runner for Validation request", v.specification.FileName, &invalidResponse)
   315  }
   316  
   317  func getMessage(message string) string {
   318  	lower := strings.ToLower(strings.Replace(message, "_", " ", -1))
   319  	return strings.ToUpper(lower[:1]) + lower[1:]
   320  }
   321  
   322  func (v *specValidator) TearDown(step *gauge.TearDown) {
   323  }
   324  
   325  func (v *specValidator) Heading(heading *gauge.Heading) {
   326  }
   327  
   328  func (v *specValidator) Tags(tags *gauge.Tags) {
   329  }
   330  
   331  func (v *specValidator) Table(dataTable *gauge.Table) {
   332  
   333  }
   334  
   335  func (v *specValidator) Scenario(scenario *gauge.Scenario) {
   336  
   337  }
   338  
   339  func (v *specValidator) Comment(comment *gauge.Comment) {
   340  }
   341  
   342  func (v *specValidator) DataTable(dataTable *gauge.DataTable) {
   343  
   344  }
   345  
   346  func (v *specValidator) Specification(specification *gauge.Specification) {
   347  	v.validationErrors = make([]error, 0)
   348  	err := validateDataTableRange(specification.DataTable.Table.GetRowCount())
   349  	if err != nil {
   350  		v.validationErrors = append(v.validationErrors, NewSpecValidationError(err.Error(), specification.FileName))
   351  	}
   352  }
   353  
   354  func validateDataTableRange(rowCount int) error {
   355  	if TableRows == "" {
   356  		return nil
   357  	}
   358  	if strings.Contains(TableRows, "-") {
   359  		indexes := strings.Split(TableRows, "-")
   360  		if len(indexes) > 2 {
   361  			return fmt.Errorf("Table rows range '%s' is invalid => Table rows range should be of format rowNumber-rowNumber", TableRows)
   362  		}
   363  		if err := validateTableRow(indexes[0], rowCount); err != nil {
   364  			return err
   365  		}
   366  		if err := validateTableRow(indexes[1], rowCount); err != nil {
   367  			return err
   368  		}
   369  	} else {
   370  		indexes := strings.Split(TableRows, ",")
   371  		for _, i := range indexes {
   372  			if err := validateTableRow(i, rowCount); err != nil {
   373  				return err
   374  			}
   375  		}
   376  	}
   377  	return nil
   378  }
   379  
   380  func validateTableRow(rowNumber string, rowCount int) error {
   381  	if rowNumber = strings.TrimSpace(rowNumber); rowNumber == "" {
   382  		return fmt.Errorf("Table rows range validation failed => Row number cannot be empty")
   383  	}
   384  	row, err := strconv.Atoi(rowNumber)
   385  	if err != nil {
   386  		return fmt.Errorf("Table rows range validation failed => Failed to parse '%s' to row number", rowNumber)
   387  	}
   388  	if row < 1 || row > rowCount {
   389  		return fmt.Errorf("Table rows range validation failed => Table row number '%d' is out of range", row)
   390  	}
   391  	return nil
   392  }