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 }