github.com/maps90/godog@v0.7.5-0.20170923143419-0093943021d4/suite_context.go (about) 1 package godog 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "io" 8 "path/filepath" 9 "reflect" 10 "regexp" 11 "strconv" 12 "strings" 13 14 "github.com/DATA-DOG/godog/gherkin" 15 ) 16 17 // SuiteContext provides steps for godog suite execution and 18 // can be used for meta-testing of godog features/steps themselves. 19 // 20 // Beware, steps or their definitions might change without backward 21 // compatibility guarantees. A typical user of the godog library should never 22 // need this, rather it is provided for those developing add-on libraries for godog. 23 // 24 // For an example of how to use, see godog's own `features/` and `suite_test.go`. 25 func SuiteContext(s *Suite, additionalContextInitializers ...func(suite *Suite)) { 26 c := &suiteContext{ 27 extraCIs: additionalContextInitializers, 28 } 29 30 // apply any additional context intializers to modify the context that the 31 // meta-tests will be run in 32 for _, ci := range additionalContextInitializers { 33 ci(s) 34 } 35 36 s.BeforeScenario(c.ResetBeforeEachScenario) 37 38 s.Step(`^(?:a )?feature path "([^"]*)"$`, c.featurePath) 39 s.Step(`^I parse features$`, c.parseFeatures) 40 s.Step(`^I'm listening to suite events$`, c.iAmListeningToSuiteEvents) 41 s.Step(`^I run feature suite$`, c.iRunFeatureSuite) 42 s.Step(`^I run feature suite with formatter "([^"]*)"$`, c.iRunFeatureSuiteWithFormatter) 43 s.Step(`^(?:a )?feature "([^"]*)"(?: file)?:$`, c.aFeatureFile) 44 s.Step(`^the suite should have (passed|failed)$`, c.theSuiteShouldHave) 45 46 s.Step(`^I should have ([\d]+) features? files?:$`, c.iShouldHaveNumFeatureFiles) 47 s.Step(`^I should have ([\d]+) scenarios? registered$`, c.numScenariosRegistered) 48 s.Step(`^there (was|were) ([\d]+) "([^"]*)" events? fired$`, c.thereWereNumEventsFired) 49 s.Step(`^there was event triggered before scenario "([^"]*)"$`, c.thereWasEventTriggeredBeforeScenario) 50 s.Step(`^these events had to be fired for a number of times:$`, c.theseEventsHadToBeFiredForNumberOfTimes) 51 52 s.Step(`^(?:a )?failing step`, c.aFailingStep) 53 s.Step(`^this step should fail`, c.aFailingStep) 54 s.Step(`^the following steps? should be (passed|failed|skipped|undefined|pending):`, c.followingStepsShouldHave) 55 s.Step(`^all steps should (?:be|have|have been) (passed|failed|skipped|undefined|pending)$`, c.allStepsShouldHave) 56 s.Step(`^the undefined step snippets should be:$`, c.theUndefinedStepSnippetsShouldBe) 57 58 // event stream 59 s.Step(`^the following events should be fired:$`, c.thereShouldBeEventsFired) 60 61 // lt 62 s.Step(`^savybių aplankas "([^"]*)"$`, c.featurePath) 63 s.Step(`^aš išskaitau savybes$`, c.parseFeatures) 64 s.Step(`^aš turėčiau turėti ([\d]+) savybių failus:$`, c.iShouldHaveNumFeatureFiles) 65 66 s.Step(`^(?:a )?pending step$`, func() error { 67 return ErrPending 68 }) 69 s.Step(`^(?:a )?passing step$`, func() error { 70 return nil 71 }) 72 73 // Introduced to test formatter/cucumber.feature 74 s.Step(`^the rendered json will be as follows:$`, c.theRenderJSONWillBe) 75 76 s.Step(`^(?:a )?failing multistep$`, func() Steps { 77 return Steps{"passing step", "failing step"} 78 }) 79 80 s.Step(`^(?:a |an )?undefined multistep$`, func() Steps { 81 return Steps{"passing step", "undefined step", "passing step"} 82 }) 83 84 s.Step(`^(?:a )?passing multistep$`, func() Steps { 85 return Steps{"passing step", "passing step", "passing step"} 86 }) 87 88 s.Step(`^(?:a )?failing nested multistep$`, func() Steps { 89 return Steps{"passing step", "passing multistep", "failing multistep"} 90 }) 91 } 92 93 type firedEvent struct { 94 name string 95 args []interface{} 96 } 97 98 type suiteContext struct { 99 paths []string 100 testedSuite *Suite 101 extraCIs []func(suite *Suite) 102 events []*firedEvent 103 out bytes.Buffer 104 } 105 106 func (s *suiteContext) ResetBeforeEachScenario(interface{}) { 107 // reset whole suite with the state 108 s.out.Reset() 109 s.paths = []string{} 110 s.testedSuite = &Suite{} 111 // our tested suite will have the same context registered 112 SuiteContext(s.testedSuite, s.extraCIs...) 113 // reset all fired events 114 s.events = []*firedEvent{} 115 } 116 117 func (s *suiteContext) iRunFeatureSuiteWithFormatter(name string) error { 118 f := findFmt(name) 119 if f == nil { 120 return fmt.Errorf(`formatter "%s" is not available`, name) 121 } 122 s.testedSuite.fmt = f("godog", &s.out) 123 if err := s.parseFeatures(); err != nil { 124 return err 125 } 126 s.testedSuite.run() 127 s.testedSuite.fmt.Summary() 128 return nil 129 } 130 131 func (s *suiteContext) thereShouldBeEventsFired(doc *gherkin.DocString) error { 132 actual := strings.Split(strings.TrimSpace(s.out.String()), "\n") 133 expect := strings.Split(strings.TrimSpace(doc.Content), "\n") 134 if len(expect) != len(actual) { 135 return fmt.Errorf("expected %d events, but got %d", len(expect), len(actual)) 136 } 137 138 type ev struct { 139 Event string 140 } 141 142 for i, event := range actual { 143 exp := strings.TrimSpace(expect[i]) 144 var act ev 145 if err := json.Unmarshal([]byte(event), &act); err != nil { 146 return fmt.Errorf("failed to read event data: %v", err) 147 } 148 149 if act.Event != exp { 150 return fmt.Errorf(`expected event: "%s" at position: %d, but actual was "%s"`, exp, i, act.Event) 151 } 152 } 153 return nil 154 } 155 156 func (s *suiteContext) cleanupSnippet(snip string) string { 157 lines := strings.Split(strings.TrimSpace(snip), "\n") 158 for i := 0; i < len(lines); i++ { 159 lines[i] = strings.TrimSpace(lines[i]) 160 } 161 return strings.Join(lines, "\n") 162 } 163 164 func (s *suiteContext) theUndefinedStepSnippetsShouldBe(body *gherkin.DocString) error { 165 f, ok := s.testedSuite.fmt.(*testFormatter) 166 if !ok { 167 return fmt.Errorf("this step requires testFormatter, but there is: %T", s.testedSuite.fmt) 168 } 169 actual := s.cleanupSnippet(f.snippets()) 170 expected := s.cleanupSnippet(body.Content) 171 if actual != expected { 172 return fmt.Errorf("snippets do not match actual: %s", f.snippets()) 173 } 174 return nil 175 } 176 177 func (s *suiteContext) followingStepsShouldHave(status string, steps *gherkin.DocString) error { 178 var expected = strings.Split(steps.Content, "\n") 179 var actual, unmatched, matched []string 180 181 f, ok := s.testedSuite.fmt.(*testFormatter) 182 if !ok { 183 return fmt.Errorf("this step requires testFormatter, but there is: %T", s.testedSuite.fmt) 184 } 185 switch status { 186 case "passed": 187 for _, st := range f.passed { 188 actual = append(actual, st.step.Text) 189 } 190 case "failed": 191 for _, st := range f.failed { 192 actual = append(actual, st.step.Text) 193 } 194 case "skipped": 195 for _, st := range f.skipped { 196 actual = append(actual, st.step.Text) 197 } 198 case "undefined": 199 for _, st := range f.undefined { 200 actual = append(actual, st.step.Text) 201 } 202 case "pending": 203 for _, st := range f.pending { 204 actual = append(actual, st.step.Text) 205 } 206 default: 207 return fmt.Errorf("unexpected step status wanted: %s", status) 208 } 209 210 if len(expected) > len(actual) { 211 return fmt.Errorf("number of expected %s steps: %d is less than actual %s steps: %d", status, len(expected), status, len(actual)) 212 } 213 214 for _, a := range actual { 215 for _, e := range expected { 216 if a == e { 217 matched = append(matched, e) 218 break 219 } 220 } 221 } 222 223 if len(matched) >= len(expected) { 224 return nil 225 } 226 for _, s := range expected { 227 var found bool 228 for _, m := range matched { 229 if s == m { 230 found = true 231 break 232 } 233 } 234 if !found { 235 unmatched = append(unmatched, s) 236 } 237 } 238 239 return fmt.Errorf("the steps: %s - are not %s", strings.Join(unmatched, ", "), status) 240 } 241 242 func (s *suiteContext) allStepsShouldHave(status string) error { 243 f, ok := s.testedSuite.fmt.(*testFormatter) 244 if !ok { 245 return fmt.Errorf("this step requires testFormatter, but there is: %T", s.testedSuite.fmt) 246 } 247 248 total := len(f.passed) + len(f.failed) + len(f.skipped) + len(f.undefined) + len(f.pending) 249 var actual int 250 switch status { 251 case "passed": 252 actual = len(f.passed) 253 case "failed": 254 actual = len(f.failed) 255 case "skipped": 256 actual = len(f.skipped) 257 case "undefined": 258 actual = len(f.undefined) 259 case "pending": 260 actual = len(f.pending) 261 default: 262 return fmt.Errorf("unexpected step status wanted: %s", status) 263 } 264 265 if total > actual { 266 return fmt.Errorf("number of expected %s steps: %d is less than actual %s steps: %d", status, total, status, actual) 267 } 268 return nil 269 } 270 271 func (s *suiteContext) iAmListeningToSuiteEvents() error { 272 s.testedSuite.BeforeSuite(func() { 273 s.events = append(s.events, &firedEvent{"BeforeSuite", []interface{}{}}) 274 }) 275 s.testedSuite.AfterSuite(func() { 276 s.events = append(s.events, &firedEvent{"AfterSuite", []interface{}{}}) 277 }) 278 s.testedSuite.BeforeFeature(func(ft *gherkin.Feature) { 279 s.events = append(s.events, &firedEvent{"BeforeFeature", []interface{}{ft}}) 280 }) 281 s.testedSuite.AfterFeature(func(ft *gherkin.Feature) { 282 s.events = append(s.events, &firedEvent{"AfterFeature", []interface{}{ft}}) 283 }) 284 s.testedSuite.BeforeScenario(func(scenario interface{}) { 285 s.events = append(s.events, &firedEvent{"BeforeScenario", []interface{}{scenario}}) 286 }) 287 s.testedSuite.AfterScenario(func(scenario interface{}, err error) { 288 s.events = append(s.events, &firedEvent{"AfterScenario", []interface{}{scenario, err}}) 289 }) 290 s.testedSuite.BeforeStep(func(step *gherkin.Step) { 291 s.events = append(s.events, &firedEvent{"BeforeStep", []interface{}{step}}) 292 }) 293 s.testedSuite.AfterStep(func(step *gherkin.Step, err error) { 294 s.events = append(s.events, &firedEvent{"AfterStep", []interface{}{step, err}}) 295 }) 296 return nil 297 } 298 299 func (s *suiteContext) aFailingStep() error { 300 return fmt.Errorf("intentional failure") 301 } 302 303 // parse a given feature file body as a feature 304 func (s *suiteContext) aFeatureFile(name string, body *gherkin.DocString) error { 305 ft, err := gherkin.ParseFeature(strings.NewReader(body.Content)) 306 s.testedSuite.features = append(s.testedSuite.features, &feature{Feature: ft, Path: name}) 307 return err 308 } 309 310 func (s *suiteContext) featurePath(path string) error { 311 s.paths = append(s.paths, path) 312 return nil 313 } 314 315 func (s *suiteContext) parseFeatures() error { 316 fts, err := parseFeatures("", s.paths) 317 if err != nil { 318 return err 319 } 320 s.testedSuite.features = append(s.testedSuite.features, fts...) 321 return nil 322 } 323 324 func (s *suiteContext) theSuiteShouldHave(state string) error { 325 if s.testedSuite.failed && state == "passed" { 326 return fmt.Errorf("the feature suite has failed") 327 } 328 if !s.testedSuite.failed && state == "failed" { 329 return fmt.Errorf("the feature suite has passed") 330 } 331 return nil 332 } 333 334 func (s *suiteContext) iShouldHaveNumFeatureFiles(num int, files *gherkin.DocString) error { 335 if len(s.testedSuite.features) != num { 336 return fmt.Errorf("expected %d features to be parsed, but have %d", num, len(s.testedSuite.features)) 337 } 338 expected := strings.Split(files.Content, "\n") 339 var actual []string 340 for _, ft := range s.testedSuite.features { 341 actual = append(actual, ft.Path) 342 } 343 if len(expected) != len(actual) { 344 return fmt.Errorf("expected %d feature paths to be parsed, but have %d", len(expected), len(actual)) 345 } 346 for i := 0; i < len(expected); i++ { 347 var matched bool 348 split := strings.Split(expected[i], "/") 349 exp := filepath.Join(split...) 350 for j := 0; j < len(actual); j++ { 351 split = strings.Split(actual[j], "/") 352 act := filepath.Join(split...) 353 if exp == act { 354 matched = true 355 break 356 } 357 } 358 if !matched { 359 return fmt.Errorf(`expected feature path "%s" at position: %d, was not parsed, actual are %+v`, exp, i, actual) 360 } 361 } 362 return nil 363 } 364 365 func (s *suiteContext) iRunFeatureSuite() error { 366 if err := s.parseFeatures(); err != nil { 367 return err 368 } 369 s.testedSuite.fmt = testFormatterFunc("godog", &s.out) 370 s.testedSuite.run() 371 s.testedSuite.fmt.Summary() 372 373 return nil 374 } 375 376 func (s *suiteContext) numScenariosRegistered(expected int) (err error) { 377 var num int 378 for _, ft := range s.testedSuite.features { 379 num += len(ft.ScenarioDefinitions) 380 } 381 if num != expected { 382 err = fmt.Errorf("expected %d scenarios to be registered, but got %d", expected, num) 383 } 384 return 385 } 386 387 func (s *suiteContext) thereWereNumEventsFired(_ string, expected int, typ string) error { 388 var num int 389 for _, event := range s.events { 390 if event.name == typ { 391 num++ 392 } 393 } 394 if num != expected { 395 return fmt.Errorf("expected %d %s events to be fired, but got %d", expected, typ, num) 396 } 397 return nil 398 } 399 400 func (s *suiteContext) thereWasEventTriggeredBeforeScenario(expected string) error { 401 var found []string 402 for _, event := range s.events { 403 if event.name != "BeforeScenario" { 404 continue 405 } 406 407 var name string 408 switch t := event.args[0].(type) { 409 case *gherkin.Scenario: 410 name = t.Name 411 case *gherkin.ScenarioOutline: 412 name = t.Name 413 } 414 if name == expected { 415 return nil 416 } 417 418 found = append(found, name) 419 } 420 421 if len(found) == 0 { 422 return fmt.Errorf("before scenario event was never triggered or listened") 423 } 424 425 return fmt.Errorf(`expected "%s" scenario, but got these fired %s`, expected, `"`+strings.Join(found, `", "`)+`"`) 426 } 427 428 func (s *suiteContext) theseEventsHadToBeFiredForNumberOfTimes(tbl *gherkin.DataTable) error { 429 if len(tbl.Rows[0].Cells) != 2 { 430 return fmt.Errorf("expected two columns for event table row, got: %d", len(tbl.Rows[0].Cells)) 431 } 432 433 for _, row := range tbl.Rows { 434 num, err := strconv.ParseInt(row.Cells[1].Value, 10, 0) 435 if err != nil { 436 return err 437 } 438 if err := s.thereWereNumEventsFired("", int(num), row.Cells[0].Value); err != nil { 439 return err 440 } 441 } 442 return nil 443 } 444 445 func (s *suiteContext) theRenderJSONWillBe(docstring *gherkin.DocString) error { 446 loc := regexp.MustCompile(`"suite_context.go:\d+"`) 447 var expected []cukeFeatureJSON 448 if err := json.Unmarshal([]byte(loc.ReplaceAllString(docstring.Content, `"suite_context.go:0"`)), &expected); err != nil { 449 return err 450 } 451 452 var actual []cukeFeatureJSON 453 replaced := loc.ReplaceAllString(s.out.String(), `"suite_context.go:0"`) 454 if err := json.Unmarshal([]byte(replaced), &actual); err != nil { 455 return err 456 } 457 458 if !reflect.DeepEqual(expected, actual) { 459 return fmt.Errorf("expected json does not match actual: %s", replaced) 460 } 461 return nil 462 } 463 464 type testFormatter struct { 465 basefmt 466 scenarios []interface{} 467 } 468 469 func testFormatterFunc(suite string, out io.Writer) Formatter { 470 return &testFormatter{ 471 basefmt: basefmt{ 472 started: timeNowFunc(), 473 indent: 2, 474 out: out, 475 }, 476 } 477 } 478 479 func (f *testFormatter) Node(node interface{}) { 480 f.basefmt.Node(node) 481 switch t := node.(type) { 482 case *gherkin.Scenario: 483 f.scenarios = append(f.scenarios, t) 484 case *gherkin.ScenarioOutline: 485 f.scenarios = append(f.scenarios, t) 486 } 487 } 488 489 func (f *testFormatter) Summary() {}