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