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 }