Add new contextualized API for hooks and steps (#409)
* Add new contextualized API for hooks and steps * Make default context configurable * Run AfterStep hooks even after failed steps, fixes #370 * Update CHANGELOG and README * Add step result status to After hook, fixes #378 * Elaborate hooks documentation * Add test to pass state between contextualized steps * Update README with example of passing state between steps
Этот коммит содержится в:
		
							родитель
							
								
									7d343d4e35
								
							
						
					
					
						коммит
						b1728ff551
					
				
					 12 изменённых файлов: 495 добавлений и 154 удалений
				
			
		
							
								
								
									
										18
									
								
								CHANGELOG.md
									
										
									
									
									
								
							
							
						
						
									
										18
									
								
								CHANGELOG.md
									
										
									
									
									
								
							|  | @ -12,18 +12,27 @@ This document is formatted according to the principles of [Keep A CHANGELOG](htt | ||||||
| 
 | 
 | ||||||
| ### Added | ### Added | ||||||
| 
 | 
 | ||||||
| * Support for step definitions without return ([364](https://github.com/cucumber/godog/pull/364) -[titouanfreville]) | - Support for step definitions without return ([364](https://github.com/cucumber/godog/pull/364) - [titouanfreville]) | ||||||
|  | - Contextualized hooks for scenarios and steps ([409](https://github.com/cucumber/godog/pull/409)) - [vearutop]) | ||||||
|  | - Step result status in After hook ([409](https://github.com/cucumber/godog/pull/409)) - [vearutop]) | ||||||
| 
 | 
 | ||||||
| ### Changed | ### Changed | ||||||
| 
 | 
 | ||||||
| * Upgraded gherkin-go to v19 ([402](https://github.com/cucumber/godog/pull/402) - [mbow]) | - Upgraded gherkin-go to v19 ([402](https://github.com/cucumber/godog/pull/402) - [mbow]) | ||||||
| 
 | 
 | ||||||
| ### Deprecated | ### Deprecated | ||||||
| 
 | 
 | ||||||
|  | - `ScenarioContext.BeforeScenario`, use `ScenarioContext.Before` ([409](https://github.com/cucumber/godog/pull/409)) - [vearutop]) | ||||||
|  | - `ScenarioContext.AfterScenario`, use `ScenarioContext.After` ([409](https://github.com/cucumber/godog/pull/409)) - [vearutop]) | ||||||
|  | - `ScenarioContext.BeforeStep`, use `ScenarioContext.StepContext().Before` ([409](https://github.com/cucumber/godog/pull/409)) - [vearutop]) | ||||||
|  | - `ScenarioContext.AfterStep`, use `ScenarioContext.StepContext().After` ([409](https://github.com/cucumber/godog/pull/409)) - [vearutop]) | ||||||
|  | 
 | ||||||
| ### Removed | ### Removed | ||||||
| 
 | 
 | ||||||
| ### Fixed | ### Fixed | ||||||
| * Incorrect step definition output for Data Tables ([411](https://github.com/cucumber/godog/pull/411) - [karfrank]) | 
 | ||||||
|  | - Incorrect step definition output for Data Tables ([411](https://github.com/cucumber/godog/pull/411) - [karfrank]) | ||||||
|  | - `ScenarioContext.AfterStep` not invoked after a failed case (([409](https://github.com/cucumber/godog/pull/409)) - [vearutop])) | ||||||
| 
 | 
 | ||||||
| ## [v0.11.0] | ## [v0.11.0] | ||||||
| 
 | 
 | ||||||
|  | @ -175,7 +184,7 @@ This document is formatted according to the principles of [Keep A CHANGELOG](htt | ||||||
| 
 | 
 | ||||||
| <!-- Releases --> | <!-- Releases --> | ||||||
| 
 | 
 | ||||||
| [unreleased]: https://github.com/cucumber/godog/compare/v0.11.0...master | [unreleased]: https://github.com/cucumber/godog/compare/v0.11.0...main | ||||||
| [v0.11.0]: https://github.com/cucumber/godog/compare/v0.10.0...v0.11.0 | [v0.11.0]: https://github.com/cucumber/godog/compare/v0.10.0...v0.11.0 | ||||||
| [v0.10.0]: https://github.com/cucumber/godog/compare/v0.9.0...v0.10.0 | [v0.10.0]: https://github.com/cucumber/godog/compare/v0.9.0...v0.10.0 | ||||||
| [0.9.0]: https://github.com/cucumber/godog/compare/v0.8.1...v0.9.0 | [0.9.0]: https://github.com/cucumber/godog/compare/v0.8.1...v0.9.0 | ||||||
|  | @ -196,3 +205,4 @@ This document is formatted according to the principles of [Keep A CHANGELOG](htt | ||||||
| [hansbogert]: https://github.com/hansbogert | [hansbogert]: https://github.com/hansbogert | ||||||
| [rickardenglund]: https://github.com/rickardenglund | [rickardenglund]: https://github.com/rickardenglund | ||||||
| [mbow]: https://github.com/mbow | [mbow]: https://github.com/mbow | ||||||
|  | [vearutop]: https://github.com/vearutop | ||||||
|  |  | ||||||
							
								
								
									
										53
									
								
								README.md
									
										
									
									
									
								
							
							
						
						
									
										53
									
								
								README.md
									
										
									
									
									
								
							|  | @ -248,10 +248,12 @@ godogs | ||||||
| Now lets implement our step definitions to test our feature requirements: | Now lets implement our step definitions to test our feature requirements: | ||||||
| 
 | 
 | ||||||
| Replace the contents of `godogs_test.go` with the code below - `vim godogs_test.go` | Replace the contents of `godogs_test.go` with the code below - `vim godogs_test.go` | ||||||
|  | 
 | ||||||
| ```go | ```go | ||||||
| package main | package main | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"context" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 
 | 
 | ||||||
| 	"github.com/cucumber/godog" | 	"github.com/cucumber/godog" | ||||||
|  | @ -277,25 +279,51 @@ func thereShouldBeRemaining(remaining int) error { | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func InitializeTestSuite(ctx *godog.TestSuiteContext) { | func InitializeTestSuite(sc *godog.TestSuiteContext) { | ||||||
| 	ctx.BeforeSuite(func() { Godogs = 0 }) | 	sc.BeforeSuite(func() { Godogs = 0 }) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func InitializeScenario(ctx *godog.ScenarioContext) { | func InitializeScenario(sc *godog.ScenarioContext) { | ||||||
| 	ctx.BeforeScenario(func(*godog.Scenario) { | 	sc.Before(func(ctx context.Context, sc *godog.Scenario) (context.Context, error) { | ||||||
| 		Godogs = 0 // clean the state before every scenario | 		Godogs = 0 // clean the state before every scenario | ||||||
|  | 
 | ||||||
|  | 		return ctx, nil | ||||||
| 	}) | 	}) | ||||||
| 
 | 
 | ||||||
| 	ctx.Step(`^there are (\d+) godogs$`, thereAreGodogs) | 	sc.Step(`^there are (\d+) godogs$`, thereAreGodogs) | ||||||
| 	ctx.Step(`^I eat (\d+)$`, iEat) | 	sc.Step(`^I eat (\d+)$`, iEat) | ||||||
| 	ctx.Step(`^there should be (\d+) remaining$`, thereShouldBeRemaining) | 	sc.Step(`^there should be (\d+) remaining$`, thereShouldBeRemaining) | ||||||
| } | } | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
|  | You can also pass the state between steps and hooks of a scenario using `context.Context`.  | ||||||
|  | Step definitions can receive and return `context.Context`. | ||||||
|  | 
 | ||||||
|  | ```go | ||||||
|  | type cntCtxKey struct{} // Key for a particular context value type. | ||||||
|  | 
 | ||||||
|  | s.Step("^I have a random number of godogs$", func(ctx context.Context) context.Context { | ||||||
|  | 	// Creating a random number of godog and storing it in context for future reference. | ||||||
|  | 	cnt := rand.Int() | ||||||
|  | 	Godogs = cnt | ||||||
|  | 	return context.WithValue(ctx, cntCtxKey{}, cnt) | ||||||
|  | }) | ||||||
|  | 
 | ||||||
|  | s.Step("I eat all available godogs", func(ctx context.Context) error { | ||||||
|  | 	// Getting previously stored number of godogs from context. | ||||||
|  | 	cnt := ctx.Value(cntCtxKey{}).(uint32) | ||||||
|  | 	if Godogs < cnt { | ||||||
|  | 		return errors.New("can't eat more than I have") | ||||||
|  | 	} | ||||||
|  | 	Godogs -= cnt | ||||||
|  | 	return nil | ||||||
|  | }) | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
| When you run godog again - `godog` | When you run godog again - `godog` | ||||||
| 
 | 
 | ||||||
| You should see a passing run: | You should see a passing run: | ||||||
| ``` | ```gherkin | ||||||
| Feature: eat godogs | Feature: eat godogs | ||||||
|   In order to be happy |   In order to be happy | ||||||
|   As a hungry gopher |   As a hungry gopher | ||||||
|  | @ -305,13 +333,16 @@ Feature: eat godogs | ||||||
|     Given there are 12 godogs        # godogs_test.go:10 -> thereAreGodogs |     Given there are 12 godogs        # godogs_test.go:10 -> thereAreGodogs | ||||||
|     When I eat 5                     # godogs_test.go:14 -> iEat |     When I eat 5                     # godogs_test.go:14 -> iEat | ||||||
|     Then there should be 7 remaining # godogs_test.go:22 -> thereShouldBeRemaining |     Then there should be 7 remaining # godogs_test.go:22 -> thereShouldBeRemaining | ||||||
| 
 | ``` | ||||||
|  | ``` | ||||||
| 1 scenarios (1 passed) | 1 scenarios (1 passed) | ||||||
| 3 steps (3 passed) | 3 steps (3 passed) | ||||||
| 258.302µs | 258.302µs | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| We have hooked to **BeforeScenario** event in order to reset the application state before each scenario. You may hook into more events, like **AfterStep** to print all state in case of an error. Or **BeforeSuite** to prepare a database. | We have hooked to `ScenarioContext` **Before** event in order to reset the application state before each scenario.  | ||||||
|  | You may hook into more events, like `sc.StepContext()` **After** to print all state in case of an error.  | ||||||
|  | Or **BeforeSuite** to prepare a database. | ||||||
| 
 | 
 | ||||||
| By now, you should have figured out, how to use **godog**. Another advice is to make steps orthogonal, small and simple to read for a user. Whether the user is a dumb website user or an API developer, who may understand a little more technical context - it should target that user. | By now, you should have figured out, how to use **godog**. Another advice is to make steps orthogonal, small and simple to read for a user. Whether the user is a dumb website user or an API developer, who may understand a little more technical context - it should target that user. | ||||||
| 
 | 
 | ||||||
|  | @ -369,7 +400,7 @@ var opts = godog.Options{ | ||||||
| 
 | 
 | ||||||
| func init() { | func init() { | ||||||
| 	godog.BindFlags("godog.", pflag.CommandLine, &opts) // godog v0.10.0 and earlier | 	godog.BindFlags("godog.", pflag.CommandLine, &opts) // godog v0.10.0 and earlier | ||||||
| 	godog.BindCommandLineFlags("godog.", &opts)        // godog v0.11.0 (latest) | 	godog.BindCommandLineFlags("godog.", &opts)        // godog v0.11.0 and later | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func TestMain(m *testing.M) { | func TestMain(m *testing.M) { | ||||||
|  |  | ||||||
|  | @ -72,6 +72,9 @@ Feature: suite events | ||||||
| 
 | 
 | ||||||
|         Scenario: two |         Scenario: two | ||||||
|           Then passing step |           Then passing step | ||||||
|  |           And failing step | ||||||
|  |           And adding step state to context | ||||||
|  |           And having correct context | ||||||
| 
 | 
 | ||||||
|         Scenario Outline: three |         Scenario Outline: three | ||||||
|           Then passing step |           Then passing step | ||||||
|  | @ -84,7 +87,9 @@ Feature: suite events | ||||||
|     Then these events had to be fired for a number of times: |     Then these events had to be fired for a number of times: | ||||||
|       | BeforeSuite    | 1 | |       | BeforeSuite    | 1 | | ||||||
|       | BeforeScenario | 2 | |       | BeforeScenario | 2 | | ||||||
|       | BeforeStep     | 2 | |       | BeforeStep     | 5 | | ||||||
|       | AfterStep      | 2 | |       | AfterStep      | 5 | | ||||||
|       | AfterScenario  | 2 | |       | AfterScenario  | 2 | | ||||||
|       | AfterSuite     | 1 | |       | AfterSuite     | 1 | | ||||||
|  | 
 | ||||||
|  |     And the suite should have failed | ||||||
|  |  | ||||||
|  | @ -1,6 +1,7 @@ | ||||||
| package flags | package flags | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"context" | ||||||
| 	"io" | 	"io" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -56,4 +57,7 @@ type Options struct { | ||||||
| 
 | 
 | ||||||
| 	// Where it should print formatter output | 	// Where it should print formatter output | ||||||
| 	Output io.Writer | 	Output io.Writer | ||||||
|  | 
 | ||||||
|  | 	// DefaultContext is used as initial context instead of context.Background(). | ||||||
|  | 	DefaultContext context.Context | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,6 +1,7 @@ | ||||||
| package models | package models | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"context" | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"reflect" | 	"reflect" | ||||||
|  | @ -32,91 +33,104 @@ type StepDefinition struct { | ||||||
| 	Undefined []string | 	Undefined []string | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | var typeOfContext = reflect.TypeOf((*context.Context)(nil)).Elem() | ||||||
|  | 
 | ||||||
| // Run a step with the matched arguments using reflect | // Run a step with the matched arguments using reflect | ||||||
| func (sd *StepDefinition) Run() interface{} { | func (sd *StepDefinition) Run(ctx context.Context) (context.Context, interface{}) { | ||||||
|  | 	var values []reflect.Value | ||||||
|  | 
 | ||||||
| 	typ := sd.HandlerValue.Type() | 	typ := sd.HandlerValue.Type() | ||||||
| 	if len(sd.Args) < typ.NumIn() { | 	numIn := typ.NumIn() | ||||||
| 		return fmt.Errorf("%w: expected %d arguments, matched %d from step", ErrUnmatchedStepArgumentNumber, typ.NumIn(), len(sd.Args)) | 	hasCtxIn := numIn > 0 && typ.In(0).Implements(typeOfContext) | ||||||
|  | 	ctxOffset := 0 | ||||||
|  | 
 | ||||||
|  | 	if hasCtxIn { | ||||||
|  | 		values = append(values, reflect.ValueOf(ctx)) | ||||||
|  | 		ctxOffset = 1 | ||||||
|  | 		numIn-- | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	var values []reflect.Value | 	if len(sd.Args) < numIn { | ||||||
| 	for i := 0; i < typ.NumIn(); i++ { | 		return ctx, fmt.Errorf("%w: expected %d arguments, matched %d from step", ErrUnmatchedStepArgumentNumber, typ.NumIn(), len(sd.Args)) | ||||||
| 		param := typ.In(i) | 	} | ||||||
|  | 
 | ||||||
|  | 	for i := 0; i < numIn; i++ { | ||||||
|  | 		param := typ.In(i + ctxOffset) | ||||||
| 		switch param.Kind() { | 		switch param.Kind() { | ||||||
| 		case reflect.Int: | 		case reflect.Int: | ||||||
| 			s, err := sd.shouldBeString(i) | 			s, err := sd.shouldBeString(i) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				return err | 				return ctx, err | ||||||
| 			} | 			} | ||||||
| 			v, err := strconv.ParseInt(s, 10, 0) | 			v, err := strconv.ParseInt(s, 10, 0) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				return fmt.Errorf(`%w %d: "%s" to int: %s`, ErrCannotConvert, i, s, err) | 				return ctx, fmt.Errorf(`%w %d: "%s" to int: %s`, ErrCannotConvert, i, s, err) | ||||||
| 			} | 			} | ||||||
| 			values = append(values, reflect.ValueOf(int(v))) | 			values = append(values, reflect.ValueOf(int(v))) | ||||||
| 		case reflect.Int64: | 		case reflect.Int64: | ||||||
| 			s, err := sd.shouldBeString(i) | 			s, err := sd.shouldBeString(i) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				return err | 				return ctx, err | ||||||
| 			} | 			} | ||||||
| 			v, err := strconv.ParseInt(s, 10, 64) | 			v, err := strconv.ParseInt(s, 10, 64) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				return fmt.Errorf(`%w %d: "%s" to int64: %s`, ErrCannotConvert, i, s, err) | 				return ctx, fmt.Errorf(`%w %d: "%s" to int64: %s`, ErrCannotConvert, i, s, err) | ||||||
| 			} | 			} | ||||||
| 			values = append(values, reflect.ValueOf(v)) | 			values = append(values, reflect.ValueOf(v)) | ||||||
| 		case reflect.Int32: | 		case reflect.Int32: | ||||||
| 			s, err := sd.shouldBeString(i) | 			s, err := sd.shouldBeString(i) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				return err | 				return ctx, err | ||||||
| 			} | 			} | ||||||
| 			v, err := strconv.ParseInt(s, 10, 32) | 			v, err := strconv.ParseInt(s, 10, 32) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				return fmt.Errorf(`%w %d: "%s" to int32: %s`, ErrCannotConvert, i, s, err) | 				return ctx, fmt.Errorf(`%w %d: "%s" to int32: %s`, ErrCannotConvert, i, s, err) | ||||||
| 			} | 			} | ||||||
| 			values = append(values, reflect.ValueOf(int32(v))) | 			values = append(values, reflect.ValueOf(int32(v))) | ||||||
| 		case reflect.Int16: | 		case reflect.Int16: | ||||||
| 			s, err := sd.shouldBeString(i) | 			s, err := sd.shouldBeString(i) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				return err | 				return ctx, err | ||||||
| 			} | 			} | ||||||
| 			v, err := strconv.ParseInt(s, 10, 16) | 			v, err := strconv.ParseInt(s, 10, 16) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				return fmt.Errorf(`%w %d: "%s" to int16: %s`, ErrCannotConvert, i, s, err) | 				return ctx, fmt.Errorf(`%w %d: "%s" to int16: %s`, ErrCannotConvert, i, s, err) | ||||||
| 			} | 			} | ||||||
| 			values = append(values, reflect.ValueOf(int16(v))) | 			values = append(values, reflect.ValueOf(int16(v))) | ||||||
| 		case reflect.Int8: | 		case reflect.Int8: | ||||||
| 			s, err := sd.shouldBeString(i) | 			s, err := sd.shouldBeString(i) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				return err | 				return ctx, err | ||||||
| 			} | 			} | ||||||
| 			v, err := strconv.ParseInt(s, 10, 8) | 			v, err := strconv.ParseInt(s, 10, 8) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				return fmt.Errorf(`%w %d: "%s" to int8: %s`, ErrCannotConvert, i, s, err) | 				return ctx, fmt.Errorf(`%w %d: "%s" to int8: %s`, ErrCannotConvert, i, s, err) | ||||||
| 			} | 			} | ||||||
| 			values = append(values, reflect.ValueOf(int8(v))) | 			values = append(values, reflect.ValueOf(int8(v))) | ||||||
| 		case reflect.String: | 		case reflect.String: | ||||||
| 			s, err := sd.shouldBeString(i) | 			s, err := sd.shouldBeString(i) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				return err | 				return ctx, err | ||||||
| 			} | 			} | ||||||
| 			values = append(values, reflect.ValueOf(s)) | 			values = append(values, reflect.ValueOf(s)) | ||||||
| 		case reflect.Float64: | 		case reflect.Float64: | ||||||
| 			s, err := sd.shouldBeString(i) | 			s, err := sd.shouldBeString(i) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				return err | 				return ctx, err | ||||||
| 			} | 			} | ||||||
| 			v, err := strconv.ParseFloat(s, 64) | 			v, err := strconv.ParseFloat(s, 64) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				return fmt.Errorf(`%w %d: "%s" to float64: %s`, ErrCannotConvert, i, s, err) | 				return ctx, fmt.Errorf(`%w %d: "%s" to float64: %s`, ErrCannotConvert, i, s, err) | ||||||
| 			} | 			} | ||||||
| 			values = append(values, reflect.ValueOf(v)) | 			values = append(values, reflect.ValueOf(v)) | ||||||
| 		case reflect.Float32: | 		case reflect.Float32: | ||||||
| 			s, err := sd.shouldBeString(i) | 			s, err := sd.shouldBeString(i) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				return err | 				return ctx, err | ||||||
| 			} | 			} | ||||||
| 			v, err := strconv.ParseFloat(s, 32) | 			v, err := strconv.ParseFloat(s, 32) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				return fmt.Errorf(`%w %d: "%s" to float32: %s`, ErrCannotConvert, i, s, err) | 				return ctx, fmt.Errorf(`%w %d: "%s" to float32: %s`, ErrCannotConvert, i, s, err) | ||||||
| 			} | 			} | ||||||
| 			values = append(values, reflect.ValueOf(float32(v))) | 			values = append(values, reflect.ValueOf(float32(v))) | ||||||
| 		case reflect.Ptr: | 		case reflect.Ptr: | ||||||
|  | @ -133,7 +147,7 @@ func (sd *StepDefinition) Run() interface{} { | ||||||
| 					break | 					break | ||||||
| 				} | 				} | ||||||
| 
 | 
 | ||||||
| 				return fmt.Errorf(`%w %d: "%v" of type "%T" to *messages.PickleDocString`, ErrCannotConvert, i, arg, arg) | 				return ctx, fmt.Errorf(`%w %d: "%v" of type "%T" to *messages.PickleDocString`, ErrCannotConvert, i, arg, arg) | ||||||
| 			case "messages.PickleTable": | 			case "messages.PickleTable": | ||||||
| 				if v, ok := arg.(*messages.PickleStepArgument); ok { | 				if v, ok := arg.(*messages.PickleStepArgument); ok { | ||||||
| 					values = append(values, reflect.ValueOf(v.DataTable)) | 					values = append(values, reflect.ValueOf(v.DataTable)) | ||||||
|  | @ -145,32 +159,42 @@ func (sd *StepDefinition) Run() interface{} { | ||||||
| 					break | 					break | ||||||
| 				} | 				} | ||||||
| 
 | 
 | ||||||
| 				return fmt.Errorf(`%w %d: "%v" of type "%T" to *messages.PickleTable`, ErrCannotConvert, i, arg, arg) | 				return ctx, fmt.Errorf(`%w %d: "%v" of type "%T" to *messages.PickleTable`, ErrCannotConvert, i, arg, arg) | ||||||
| 			default: | 			default: | ||||||
| 				return fmt.Errorf("%w: the argument %d type %T is not supported %s", ErrUnsupportedArgumentType, i, arg, param.Elem().String()) | 				return ctx, fmt.Errorf("%w: the argument %d type %T is not supported %s", ErrUnsupportedArgumentType, i, arg, param.Elem().String()) | ||||||
| 			} | 			} | ||||||
| 		case reflect.Slice: | 		case reflect.Slice: | ||||||
| 			switch param { | 			switch param { | ||||||
| 			case typeOfBytes: | 			case typeOfBytes: | ||||||
| 				s, err := sd.shouldBeString(i) | 				s, err := sd.shouldBeString(i) | ||||||
| 				if err != nil { | 				if err != nil { | ||||||
| 					return err | 					return ctx, err | ||||||
| 				} | 				} | ||||||
| 				values = append(values, reflect.ValueOf([]byte(s))) | 				values = append(values, reflect.ValueOf([]byte(s))) | ||||||
| 			default: | 			default: | ||||||
| 				return fmt.Errorf("%w: the slice argument %d type %s is not supported", ErrUnsupportedArgumentType, i, param.Kind()) | 				return ctx, fmt.Errorf("%w: the slice argument %d type %s is not supported", ErrUnsupportedArgumentType, i, param.Kind()) | ||||||
| 			} | 			} | ||||||
| 		default: | 		default: | ||||||
| 			return fmt.Errorf("%w: the argument %d type %s is not supported", ErrUnsupportedArgumentType, i, param.Kind()) | 			return ctx, fmt.Errorf("%w: the argument %d type %s is not supported", ErrUnsupportedArgumentType, i, param.Kind()) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	res := sd.HandlerValue.Call(values) | 	res := sd.HandlerValue.Call(values) | ||||||
| 	if len(res) == 0 { | 	if len(res) == 0 { | ||||||
| 		return nil | 		return ctx, nil | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return res[0].Interface() | 	if len(res) == 1 { | ||||||
|  | 		r := res[0].Interface() | ||||||
|  | 
 | ||||||
|  | 		if ctx, ok := r.(context.Context); ok { | ||||||
|  | 			return ctx, nil | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		return ctx, res[0].Interface() | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return res[0].Interface().(context.Context), res[1].Interface() | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (sd *StepDefinition) shouldBeString(idx int) (string, error) { | func (sd *StepDefinition) shouldBeString(idx int) (string, error) { | ||||||
|  |  | ||||||
|  | @ -1,6 +1,7 @@ | ||||||
| package models_test | package models_test | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"context" | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"reflect" | 	"reflect" | ||||||
|  | @ -8,12 +9,59 @@ import ( | ||||||
| 	"testing" | 	"testing" | ||||||
| 
 | 
 | ||||||
| 	"github.com/cucumber/messages-go/v16" | 	"github.com/cucumber/messages-go/v16" | ||||||
|  | 	"github.com/stretchr/testify/assert" | ||||||
| 
 | 
 | ||||||
| 	"github.com/cucumber/godog" | 	"github.com/cucumber/godog" | ||||||
| 	"github.com/cucumber/godog/formatters" | 	"github.com/cucumber/godog/formatters" | ||||||
| 	"github.com/cucumber/godog/internal/models" | 	"github.com/cucumber/godog/internal/models" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | func TestShouldSupportContext(t *testing.T) { | ||||||
|  | 	ctx := context.WithValue(context.Background(), "original", 123) | ||||||
|  | 
 | ||||||
|  | 	fn := func(ctx context.Context, a int64, b int32, c int16, d int8) context.Context { | ||||||
|  | 		assert.Equal(t, 123, ctx.Value("original")) | ||||||
|  | 
 | ||||||
|  | 		return context.WithValue(ctx, "updated", 321) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	def := &models.StepDefinition{ | ||||||
|  | 		StepDefinition: formatters.StepDefinition{ | ||||||
|  | 			Handler: fn, | ||||||
|  | 		}, | ||||||
|  | 		HandlerValue: reflect.ValueOf(fn), | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	def.Args = []interface{}{"1", "1", "1", "1"} | ||||||
|  | 	ctx, err := def.Run(ctx) | ||||||
|  | 	assert.Nil(t, err) | ||||||
|  | 	assert.Equal(t, 123, ctx.Value("original")) | ||||||
|  | 	assert.Equal(t, 321, ctx.Value("updated")) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestShouldSupportContextAndError(t *testing.T) { | ||||||
|  | 	ctx := context.WithValue(context.Background(), "original", 123) | ||||||
|  | 
 | ||||||
|  | 	fn := func(ctx context.Context, a int64, b int32, c int16, d int8) (context.Context, error) { | ||||||
|  | 		assert.Equal(t, 123, ctx.Value("original")) | ||||||
|  | 
 | ||||||
|  | 		return context.WithValue(ctx, "updated", 321), nil | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	def := &models.StepDefinition{ | ||||||
|  | 		StepDefinition: formatters.StepDefinition{ | ||||||
|  | 			Handler: fn, | ||||||
|  | 		}, | ||||||
|  | 		HandlerValue: reflect.ValueOf(fn), | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	def.Args = []interface{}{"1", "1", "1", "1"} | ||||||
|  | 	ctx, err := def.Run(ctx) | ||||||
|  | 	assert.Nil(t, err) | ||||||
|  | 	assert.Equal(t, 123, ctx.Value("original")) | ||||||
|  | 	assert.Equal(t, 321, ctx.Value("updated")) | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func TestShouldSupportEmptyHandlerReturn(t *testing.T) { | func TestShouldSupportEmptyHandlerReturn(t *testing.T) { | ||||||
| 	fn := func(a int64, b int32, c int16, d int8) {} | 	fn := func(a int64, b int32, c int16, d int8) {} | ||||||
| 
 | 
 | ||||||
|  | @ -25,12 +73,12 @@ func TestShouldSupportEmptyHandlerReturn(t *testing.T) { | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	def.Args = []interface{}{"1", "1", "1", "1"} | 	def.Args = []interface{}{"1", "1", "1", "1"} | ||||||
| 	if err := def.Run(); err != nil { | 	if _, err := def.Run(context.Background()); err != nil { | ||||||
| 		t.Fatalf("unexpected error: %v", err) | 		t.Fatalf("unexpected error: %v", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	def.Args = []interface{}{"1", "1", "1", strings.Repeat("1", 9)} | 	def.Args = []interface{}{"1", "1", "1", strings.Repeat("1", 9)} | ||||||
| 	if err := def.Run(); err == nil { | 	if _, err := def.Run(context.Background()); err == nil { | ||||||
| 		t.Fatalf("expected convertion fail for int8, but got none") | 		t.Fatalf("expected convertion fail for int8, but got none") | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  | @ -46,12 +94,12 @@ func TestShouldSupportIntTypes(t *testing.T) { | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	def.Args = []interface{}{"1", "1", "1", "1"} | 	def.Args = []interface{}{"1", "1", "1", "1"} | ||||||
| 	if err := def.Run(); err != nil { | 	if _, err := def.Run(context.Background()); err != nil { | ||||||
| 		t.Fatalf("unexpected error: %v", err) | 		t.Fatalf("unexpected error: %v", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	def.Args = []interface{}{"1", "1", "1", strings.Repeat("1", 9)} | 	def.Args = []interface{}{"1", "1", "1", strings.Repeat("1", 9)} | ||||||
| 	if err := def.Run(); err == nil { | 	if _, err := def.Run(context.Background()); err == nil { | ||||||
| 		t.Fatalf("expected convertion fail for int8, but got none") | 		t.Fatalf("expected convertion fail for int8, but got none") | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  | @ -67,12 +115,12 @@ func TestShouldSupportFloatTypes(t *testing.T) { | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	def.Args = []interface{}{"1.1", "1.09"} | 	def.Args = []interface{}{"1.1", "1.09"} | ||||||
| 	if err := def.Run(); err != nil { | 	if _, err := def.Run(context.Background()); err != nil { | ||||||
| 		t.Fatalf("unexpected error: %v", err) | 		t.Fatalf("unexpected error: %v", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	def.Args = []interface{}{"1.08", strings.Repeat("1", 65) + ".67"} | 	def.Args = []interface{}{"1.08", strings.Repeat("1", 65) + ".67"} | ||||||
| 	if err := def.Run(); err == nil { | 	if _, err := def.Run(context.Background()); err == nil { | ||||||
| 		t.Fatalf("expected convertion fail for float32, but got none") | 		t.Fatalf("expected convertion fail for float32, but got none") | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  | @ -104,15 +152,15 @@ func TestShouldNotSupportOtherPointerTypesThanGherkin(t *testing.T) { | ||||||
| 		Args:         []interface{}{(*messages.PickleTable)(nil)}, | 		Args:         []interface{}{(*messages.PickleTable)(nil)}, | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if err := def1.Run(); err == nil { | 	if _, err := def1.Run(context.Background()); err == nil { | ||||||
| 		t.Fatalf("expected conversion error, but got none") | 		t.Fatalf("expected conversion error, but got none") | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if err := def2.Run(); err != nil { | 	if _, err := def2.Run(context.Background()); err != nil { | ||||||
| 		t.Fatalf("unexpected error: %v", err) | 		t.Fatalf("unexpected error: %v", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if err := def3.Run(); err != nil { | 	if _, err := def3.Run(context.Background()); err != nil { | ||||||
| 		t.Fatalf("unexpected error: %v", err) | 		t.Fatalf("unexpected error: %v", err) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  | @ -136,11 +184,11 @@ func TestShouldSupportOnlyByteSlice(t *testing.T) { | ||||||
| 		Args:         []interface{}{[]string{}}, | 		Args:         []interface{}{[]string{}}, | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if err := def1.Run(); err != nil { | 	if _, err := def1.Run(context.Background()); err != nil { | ||||||
| 		t.Fatalf("unexpected error: %v", err) | 		t.Fatalf("unexpected error: %v", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if err := def2.Run(); err == nil { | 	if _, err := def2.Run(context.Background()); err == nil { | ||||||
| 		t.Fatalf("expected conversion error, but got none") | 		t.Fatalf("expected conversion error, but got none") | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  | @ -156,7 +204,7 @@ func TestUnexpectedArguments(t *testing.T) { | ||||||
| 
 | 
 | ||||||
| 	def.Args = []interface{}{"1"} | 	def.Args = []interface{}{"1"} | ||||||
| 
 | 
 | ||||||
| 	res := def.Run() | 	_, res := def.Run(context.Background()) | ||||||
| 	if res == nil { | 	if res == nil { | ||||||
| 		t.Fatalf("expected an error due to wrong number of arguments, but got none") | 		t.Fatalf("expected an error due to wrong number of arguments, but got none") | ||||||
| 	} | 	} | ||||||
|  | @ -182,7 +230,7 @@ func TestStepDefinition_Run_StepShouldBeString(t *testing.T) { | ||||||
| 
 | 
 | ||||||
| 		def.Args = []interface{}{12} | 		def.Args = []interface{}{12} | ||||||
| 
 | 
 | ||||||
| 		res := def.Run() | 		_, res := def.Run(context.Background()) | ||||||
| 		if res == nil { | 		if res == nil { | ||||||
| 			t.Fatalf("expected a string convertion error, but got none") | 			t.Fatalf("expected a string convertion error, but got none") | ||||||
| 		} | 		} | ||||||
|  | @ -224,7 +272,7 @@ func TestStepDefinition_Run_InvalidHandlerParamConversion(t *testing.T) { | ||||||
| 
 | 
 | ||||||
| 		def.Args = []interface{}{12} | 		def.Args = []interface{}{12} | ||||||
| 
 | 
 | ||||||
| 		res := def.Run() | 		_, res := def.Run(context.Background()) | ||||||
| 		if res == nil { | 		if res == nil { | ||||||
| 			t.Fatalf("expected an unsupported argument type error, but got none") | 			t.Fatalf("expected an unsupported argument type error, but got none") | ||||||
| 		} | 		} | ||||||
|  | @ -280,7 +328,7 @@ func TestStepDefinition_Run_StringConversionToFunctionType(t *testing.T) { | ||||||
| 			Args:         args, | 			Args:         args, | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		res := def.Run() | 		_, res := def.Run(context.Background()) | ||||||
| 		if res == nil { | 		if res == nil { | ||||||
| 			t.Fatalf("expected a cannot convert argument type error, but got none") | 			t.Fatalf("expected a cannot convert argument type error, but got none") | ||||||
| 		} | 		} | ||||||
|  | @ -321,7 +369,7 @@ func TestStepDefinition_Run_StringConversionToFunctionType(t *testing.T) { | ||||||
| // def = &models.StepDefinition{Handler: fn2, HandlerValue: reflect.ValueOf(fn2)} | // def = &models.StepDefinition{Handler: fn2, HandlerValue: reflect.ValueOf(fn2)} | ||||||
| 
 | 
 | ||||||
| // def.Args = []interface{}{"1"} | // def.Args = []interface{}{"1"} | ||||||
| // if err := def.Run(); err == nil { | // if _, err := def.Run(context.Background()); err == nil { | ||||||
| // 	t.Fatalf("expected an error due to wrong argument type, but got none") | // 	t.Fatalf("expected an error due to wrong argument type, but got none") | ||||||
| // } | // } | ||||||
| 
 | 
 | ||||||
|  | @ -347,7 +395,7 @@ func TestShouldSupportDocStringToStringConversion(t *testing.T) { | ||||||
| 		}}, | 		}}, | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if err := def.Run(); err != nil { | 	if _, err := def.Run(context.Background()); err != nil { | ||||||
| 		t.Fatalf("unexpected error: %v", err) | 		t.Fatalf("unexpected error: %v", err) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
							
								
								
									
										5
									
								
								run.go
									
										
									
									
									
								
							
							
						
						
									
										5
									
								
								run.go
									
										
									
									
									
								
							|  | @ -1,6 +1,7 @@ | ||||||
| package godog | package godog | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"context" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"go/build" | 	"go/build" | ||||||
| 	"io" | 	"io" | ||||||
|  | @ -36,6 +37,8 @@ type runner struct { | ||||||
| 	randomSeed            int64 | 	randomSeed            int64 | ||||||
| 	stopOnFailure, strict bool | 	stopOnFailure, strict bool | ||||||
| 
 | 
 | ||||||
|  | 	defaultContext context.Context | ||||||
|  | 
 | ||||||
| 	features []*models.Feature | 	features []*models.Feature | ||||||
| 
 | 
 | ||||||
| 	testSuiteInitializer testSuiteInitializer | 	testSuiteInitializer testSuiteInitializer | ||||||
|  | @ -102,6 +105,7 @@ func (r *runner) concurrent(rate int) (failed bool) { | ||||||
| 					randomSeed:     r.randomSeed, | 					randomSeed:     r.randomSeed, | ||||||
| 					strict:         r.strict, | 					strict:         r.strict, | ||||||
| 					storage:        r.storage, | 					storage:        r.storage, | ||||||
|  | 					defaultContext: r.defaultContext, | ||||||
| 				} | 				} | ||||||
| 
 | 
 | ||||||
| 				if r.scenarioInitializer != nil { | 				if r.scenarioInitializer != nil { | ||||||
|  | @ -231,6 +235,7 @@ func runWithOptions(suiteName string, runner runner, opt Options) int { | ||||||
| 
 | 
 | ||||||
| 	runner.stopOnFailure = opt.StopOnFailure | 	runner.stopOnFailure = opt.StopOnFailure | ||||||
| 	runner.strict = opt.Strict | 	runner.strict = opt.Strict | ||||||
|  | 	runner.defaultContext = opt.DefaultContext | ||||||
| 
 | 
 | ||||||
| 	// store chosen seed in environment, so it could be seen in formatter summary report | 	// store chosen seed in environment, so it could be seen in formatter summary report | ||||||
| 	os.Setenv("GODOG_SEED", strconv.FormatInt(runner.randomSeed, 10)) | 	os.Setenv("GODOG_SEED", strconv.FormatInt(runner.randomSeed, 10)) | ||||||
|  |  | ||||||
|  | @ -414,11 +414,11 @@ func Test_AllFeaturesRun(t *testing.T) { | ||||||
| ...................................................................... 140 | ...................................................................... 140 | ||||||
| ...................................................................... 210 | ...................................................................... 210 | ||||||
| ...................................................................... 280 | ...................................................................... 280 | ||||||
| ............................                                           308 | .............................                                          309 | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| 81 scenarios (81 passed) | 81 scenarios (81 passed) | ||||||
| 308 steps (308 passed) | 309 steps (309 passed) | ||||||
| 0s | 0s | ||||||
| ` | ` | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										174
									
								
								suite.go
									
										
									
									
									
								
							
							
						
						
									
										174
									
								
								suite.go
									
										
									
									
									
								
							|  | @ -1,6 +1,7 @@ | ||||||
| package godog | package godog | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"context" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"reflect" | 	"reflect" | ||||||
| 	"strings" | 	"strings" | ||||||
|  | @ -13,7 +14,10 @@ import ( | ||||||
| 	"github.com/cucumber/godog/internal/utils" | 	"github.com/cucumber/godog/internal/utils" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| var errorInterface = reflect.TypeOf((*error)(nil)).Elem() | var ( | ||||||
|  | 	errorInterface   = reflect.TypeOf((*error)(nil)).Elem() | ||||||
|  | 	contextInterface = reflect.TypeOf((*context.Context)(nil)).Elem() | ||||||
|  | ) | ||||||
| 
 | 
 | ||||||
| // ErrUndefined is returned in case if step definition was not found | // ErrUndefined is returned in case if step definition was not found | ||||||
| var ErrUndefined = fmt.Errorf("step is undefined") | var ErrUndefined = fmt.Errorf("step is undefined") | ||||||
|  | @ -22,6 +26,22 @@ var ErrUndefined = fmt.Errorf("step is undefined") | ||||||
| // step implementation is pending | // step implementation is pending | ||||||
| var ErrPending = fmt.Errorf("step implementation is pending") | var ErrPending = fmt.Errorf("step implementation is pending") | ||||||
| 
 | 
 | ||||||
|  | // StepResultStatus describes step result. | ||||||
|  | type StepResultStatus = models.StepResultStatus | ||||||
|  | 
 | ||||||
|  | const ( | ||||||
|  | 	// StepPassed indicates step that passed. | ||||||
|  | 	StepPassed StepResultStatus = models.Passed | ||||||
|  | 	// StepFailed indicates step that failed. | ||||||
|  | 	StepFailed = models.Failed | ||||||
|  | 	// StepSkipped indicates step that was skipped. | ||||||
|  | 	StepSkipped = models.Skipped | ||||||
|  | 	// StepUndefined indicates undefined step. | ||||||
|  | 	StepUndefined = models.Undefined | ||||||
|  | 	// StepPending indicates step with pending implementation. | ||||||
|  | 	StepPending = models.Pending | ||||||
|  | ) | ||||||
|  | 
 | ||||||
| type suite struct { | type suite struct { | ||||||
| 	steps []*models.StepDefinition | 	steps []*models.StepDefinition | ||||||
| 
 | 
 | ||||||
|  | @ -33,11 +53,13 @@ type suite struct { | ||||||
| 	stopOnFailure bool | 	stopOnFailure bool | ||||||
| 	strict        bool | 	strict        bool | ||||||
| 
 | 
 | ||||||
|  | 	defaultContext context.Context | ||||||
|  | 
 | ||||||
| 	// suite event handlers | 	// suite event handlers | ||||||
| 	beforeScenarioHandlers []func(*Scenario) | 	beforeScenarioHandlers []BeforeScenarioHook | ||||||
| 	beforeStepHandlers     []func(*Step) | 	beforeStepHandlers     []BeforeStepHook | ||||||
| 	afterStepHandlers      []func(*Step, error) | 	afterStepHandlers      []AfterStepHook | ||||||
| 	afterScenarioHandlers  []func(*Scenario, error) | 	afterScenarioHandlers  []AfterScenarioHook | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (s *suite) matchStep(step *messages.PickleStep) *models.StepDefinition { | func (s *suite) matchStep(step *messages.PickleStep) *models.StepDefinition { | ||||||
|  | @ -48,15 +70,11 @@ func (s *suite) matchStep(step *messages.PickleStep) *models.StepDefinition { | ||||||
| 	return def | 	return def | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (s *suite) runStep(pickle *messages.Pickle, step *messages.PickleStep, prevStepErr error) (err error) { | func (s *suite) runStep(ctx context.Context, pickle *Scenario, step *Step, prevStepErr error) (rctx context.Context, err error) { | ||||||
| 	// run before step handlers | 	var ( | ||||||
| 	for _, f := range s.beforeStepHandlers { | 		match *models.StepDefinition | ||||||
| 		f(step) | 		sr    = models.PickleStepResult{Status: models.Undefined} | ||||||
| 	} | 	) | ||||||
| 
 |  | ||||||
| 	match := s.matchStep(step) |  | ||||||
| 	s.storage.MustInsertStepDefintionMatch(step.AstNodeIds[0], match) |  | ||||||
| 	s.fmt.Defined(pickle, step, match.GetInternalStepDefinition()) |  | ||||||
| 
 | 
 | ||||||
| 	// user multistep definitions may panic | 	// user multistep definitions may panic | ||||||
| 	defer func() { | 	defer func() { | ||||||
|  | @ -67,6 +85,24 @@ func (s *suite) runStep(pickle *messages.Pickle, step *messages.PickleStep, prev | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
|  | 		defer func() { | ||||||
|  | 			// run after step handlers | ||||||
|  | 			for _, f := range s.afterStepHandlers { | ||||||
|  | 				hctx, herr := f(rctx, step, sr.Status, err) | ||||||
|  | 
 | ||||||
|  | 				// Adding hook error to resulting error without breaking hooks loop. | ||||||
|  | 				if herr != nil { | ||||||
|  | 					if err == nil { | ||||||
|  | 						err = herr | ||||||
|  | 					} else { | ||||||
|  | 						err = fmt.Errorf("%v: %w", herr, err) | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				rctx = hctx | ||||||
|  | 			} | ||||||
|  | 		}() | ||||||
|  | 
 | ||||||
| 		if prevStepErr != nil { | 		if prevStepErr != nil { | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
|  | @ -75,7 +111,7 @@ func (s *suite) runStep(pickle *messages.Pickle, step *messages.PickleStep, prev | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		sr := models.NewStepResult(pickle.Id, step.Id, match) | 		sr = models.NewStepResult(pickle.Id, step.Id, match) | ||||||
| 
 | 
 | ||||||
| 		switch err { | 		switch err { | ||||||
| 		case nil: | 		case nil: | ||||||
|  | @ -95,15 +131,22 @@ func (s *suite) runStep(pickle *messages.Pickle, step *messages.PickleStep, prev | ||||||
| 
 | 
 | ||||||
| 			s.fmt.Failed(pickle, step, match.GetInternalStepDefinition(), err) | 			s.fmt.Failed(pickle, step, match.GetInternalStepDefinition(), err) | ||||||
| 		} | 		} | ||||||
| 
 |  | ||||||
| 		// run after step handlers |  | ||||||
| 		for _, f := range s.afterStepHandlers { |  | ||||||
| 			f(step, err) |  | ||||||
| 		} |  | ||||||
| 	}() | 	}() | ||||||
| 
 | 
 | ||||||
| 	if undef, err := s.maybeUndefined(step.Text, step.Argument); err != nil { | 	// run before step handlers | ||||||
| 		return err | 	for _, f := range s.beforeStepHandlers { | ||||||
|  | 		ctx, err = f(ctx, step) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return ctx, err | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	match = s.matchStep(step) | ||||||
|  | 	s.storage.MustInsertStepDefintionMatch(step.AstNodeIds[0], match) | ||||||
|  | 	s.fmt.Defined(pickle, step, match.GetInternalStepDefinition()) | ||||||
|  | 
 | ||||||
|  | 	if ctx, undef, err := s.maybeUndefined(ctx, step.Text, step.Argument); err != nil { | ||||||
|  | 		return ctx, err | ||||||
| 	} else if len(undef) > 0 { | 	} else if len(undef) > 0 { | ||||||
| 		if match != nil { | 		if match != nil { | ||||||
| 			match = &models.StepDefinition{ | 			match = &models.StepDefinition{ | ||||||
|  | @ -118,82 +161,85 @@ func (s *suite) runStep(pickle *messages.Pickle, step *messages.PickleStep, prev | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		sr := models.NewStepResult(pickle.Id, step.Id, match) | 		sr = models.NewStepResult(pickle.Id, step.Id, match) | ||||||
| 		sr.Status = models.Undefined | 		sr.Status = models.Undefined | ||||||
| 		s.storage.MustInsertPickleStepResult(sr) | 		s.storage.MustInsertPickleStepResult(sr) | ||||||
| 
 | 
 | ||||||
| 		s.fmt.Undefined(pickle, step, match.GetInternalStepDefinition()) | 		s.fmt.Undefined(pickle, step, match.GetInternalStepDefinition()) | ||||||
| 		return ErrUndefined | 		return ctx, ErrUndefined | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if prevStepErr != nil { | 	if prevStepErr != nil { | ||||||
| 		sr := models.NewStepResult(pickle.Id, step.Id, match) | 		sr = models.NewStepResult(pickle.Id, step.Id, match) | ||||||
| 		sr.Status = models.Skipped | 		sr.Status = models.Skipped | ||||||
| 		s.storage.MustInsertPickleStepResult(sr) | 		s.storage.MustInsertPickleStepResult(sr) | ||||||
| 
 | 
 | ||||||
| 		s.fmt.Skipped(pickle, step, match.GetInternalStepDefinition()) | 		s.fmt.Skipped(pickle, step, match.GetInternalStepDefinition()) | ||||||
| 		return nil | 		return ctx, nil | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	err = s.maybeSubSteps(match.Run()) | 	ctx, err = s.maybeSubSteps(match.Run(ctx)) | ||||||
| 	return | 
 | ||||||
|  | 	return ctx, err | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (s *suite) maybeUndefined(text string, arg interface{}) ([]string, error) { | func (s *suite) maybeUndefined(ctx context.Context, text string, arg interface{}) (context.Context, []string, error) { | ||||||
| 	step := s.matchStepText(text) | 	step := s.matchStepText(text) | ||||||
| 	if nil == step { | 	if nil == step { | ||||||
| 		return []string{text}, nil | 		return ctx, []string{text}, nil | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	var undefined []string | 	var undefined []string | ||||||
| 	if !step.Nested { | 	if !step.Nested { | ||||||
| 		return undefined, nil | 		return ctx, undefined, nil | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if arg != nil { | 	if arg != nil { | ||||||
| 		step.Args = append(step.Args, arg) | 		step.Args = append(step.Args, arg) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	for _, next := range step.Run().(Steps) { | 	ctx, steps := step.Run(ctx) | ||||||
|  | 
 | ||||||
|  | 	for _, next := range steps.(Steps) { | ||||||
| 		lines := strings.Split(next, "\n") | 		lines := strings.Split(next, "\n") | ||||||
| 		// @TODO: we cannot currently parse table or content body from nested steps | 		// @TODO: we cannot currently parse table or content body from nested steps | ||||||
| 		if len(lines) > 1 { | 		if len(lines) > 1 { | ||||||
| 			return undefined, fmt.Errorf("nested steps cannot be multiline and have table or content body argument") | 			return ctx, undefined, fmt.Errorf("nested steps cannot be multiline and have table or content body argument") | ||||||
| 		} | 		} | ||||||
| 		if len(lines[0]) > 0 && lines[0][len(lines[0])-1] == ':' { | 		if len(lines[0]) > 0 && lines[0][len(lines[0])-1] == ':' { | ||||||
| 			return undefined, fmt.Errorf("nested steps cannot be multiline and have table or content body argument") | 			return ctx, undefined, fmt.Errorf("nested steps cannot be multiline and have table or content body argument") | ||||||
| 		} | 		} | ||||||
| 		undef, err := s.maybeUndefined(next, nil) | 		ctx, undef, err := s.maybeUndefined(ctx, next, nil) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return undefined, err | 			return ctx, undefined, err | ||||||
| 		} | 		} | ||||||
| 		undefined = append(undefined, undef...) | 		undefined = append(undefined, undef...) | ||||||
| 	} | 	} | ||||||
| 	return undefined, nil | 	return ctx, undefined, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (s *suite) maybeSubSteps(result interface{}) error { | func (s *suite) maybeSubSteps(ctx context.Context, result interface{}) (context.Context, error) { | ||||||
| 	if nil == result { | 	if nil == result { | ||||||
| 		return nil | 		return ctx, nil | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if err, ok := result.(error); ok { | 	if err, ok := result.(error); ok { | ||||||
| 		return err | 		return ctx, err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	steps, ok := result.(Steps) | 	steps, ok := result.(Steps) | ||||||
| 	if !ok { | 	if !ok { | ||||||
| 		return fmt.Errorf("unexpected error, should have been []string: %T - %+v", result, result) | 		return ctx, fmt.Errorf("unexpected error, should have been []string: %T - %+v", result, result) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	for _, text := range steps { | 	for _, text := range steps { | ||||||
| 		if def := s.matchStepText(text); def == nil { | 		if def := s.matchStepText(text); def == nil { | ||||||
| 			return ErrUndefined | 			return ctx, ErrUndefined | ||||||
| 		} else if err := s.maybeSubSteps(def.Run()); err != nil { | 		} else if ctx, err := s.maybeSubSteps(def.Run(ctx)); err != nil { | ||||||
| 			return fmt.Errorf("%s: %+v", text, err) | 			return ctx, fmt.Errorf("%s: %+v", text, err) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 	return nil | 	return ctx, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (s *suite) matchStepText(text string) *models.StepDefinition { | func (s *suite) matchStepText(text string) *models.StepDefinition { | ||||||
|  | @ -220,9 +266,13 @@ func (s *suite) matchStepText(text string) *models.StepDefinition { | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (s *suite) runSteps(pickle *messages.Pickle, steps []*messages.PickleStep) (err error) { | func (s *suite) runSteps(ctx context.Context, pickle *Scenario, steps []*Step) (context.Context, error) { | ||||||
|  | 	var ( | ||||||
|  | 		stepErr, err error | ||||||
|  | 	) | ||||||
|  | 
 | ||||||
| 	for _, step := range steps { | 	for _, step := range steps { | ||||||
| 		stepErr := s.runStep(pickle, step, err) | 		ctx, stepErr = s.runStep(ctx, pickle, step, err) | ||||||
| 		switch stepErr { | 		switch stepErr { | ||||||
| 		case ErrUndefined: | 		case ErrUndefined: | ||||||
| 			// do not overwrite failed error | 			// do not overwrite failed error | ||||||
|  | @ -236,7 +286,8 @@ func (s *suite) runSteps(pickle *messages.Pickle, steps []*messages.PickleStep) | ||||||
| 			err = stepErr | 			err = stepErr | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 	return | 
 | ||||||
|  | 	return ctx, err | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (s *suite) shouldFail(err error) bool { | func (s *suite) shouldFail(err error) bool { | ||||||
|  | @ -262,6 +313,11 @@ func isEmptyFeature(pickles []*messages.Pickle) bool { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (s *suite) runPickle(pickle *messages.Pickle) (err error) { | func (s *suite) runPickle(pickle *messages.Pickle) (err error) { | ||||||
|  | 	ctx := s.defaultContext | ||||||
|  | 	if ctx == nil { | ||||||
|  | 		ctx = context.Background() | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	if len(pickle.Steps) == 0 { | 	if len(pickle.Steps) == 0 { | ||||||
| 		pr := models.PickleResult{PickleID: pickle.Id, StartedAt: utils.TimeNowFunc()} | 		pr := models.PickleResult{PickleID: pickle.Id, StartedAt: utils.TimeNowFunc()} | ||||||
| 		s.storage.MustInsertPickleResult(pr) | 		s.storage.MustInsertPickleResult(pr) | ||||||
|  | @ -272,7 +328,10 @@ func (s *suite) runPickle(pickle *messages.Pickle) (err error) { | ||||||
| 
 | 
 | ||||||
| 	// run before scenario handlers | 	// run before scenario handlers | ||||||
| 	for _, f := range s.beforeScenarioHandlers { | 	for _, f := range s.beforeScenarioHandlers { | ||||||
| 		f(pickle) | 		ctx, err = f(ctx, pickle) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	pr := models.PickleResult{PickleID: pickle.Id, StartedAt: utils.TimeNowFunc()} | 	pr := models.PickleResult{PickleID: pickle.Id, StartedAt: utils.TimeNowFunc()} | ||||||
|  | @ -281,12 +340,23 @@ func (s *suite) runPickle(pickle *messages.Pickle) (err error) { | ||||||
| 	s.fmt.Pickle(pickle) | 	s.fmt.Pickle(pickle) | ||||||
| 
 | 
 | ||||||
| 	// scenario | 	// scenario | ||||||
| 	err = s.runSteps(pickle, pickle.Steps) | 	ctx, err = s.runSteps(ctx, pickle, pickle.Steps) | ||||||
| 
 | 
 | ||||||
| 	// run after scenario handlers | 	// run after scenario handlers | ||||||
| 	for _, f := range s.afterScenarioHandlers { | 	for _, f := range s.afterScenarioHandlers { | ||||||
| 		f(pickle, err) | 		hctx, herr := f(ctx, pickle, err) | ||||||
|  | 
 | ||||||
|  | 		// Adding hook error to resulting error without breaking hooks loop. | ||||||
|  | 		if herr != nil { | ||||||
|  | 			if err == nil { | ||||||
|  | 				err = herr | ||||||
|  | 			} else { | ||||||
|  | 				err = fmt.Errorf("%v: %w", herr, err) | ||||||
|  | 			} | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 	return | 		ctx = hctx | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return err | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -2,8 +2,10 @@ package godog | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"bytes" | 	"bytes" | ||||||
|  | 	"context" | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| 	"encoding/xml" | 	"encoding/xml" | ||||||
|  | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"path/filepath" | 	"path/filepath" | ||||||
| 	"regexp" | 	"regexp" | ||||||
|  | @ -34,7 +36,7 @@ import ( | ||||||
| func InitializeScenario(ctx *ScenarioContext) { | func InitializeScenario(ctx *ScenarioContext) { | ||||||
| 	tc := &godogFeaturesScenario{} | 	tc := &godogFeaturesScenario{} | ||||||
| 
 | 
 | ||||||
| 	ctx.BeforeScenario(tc.ResetBeforeEachScenario) | 	ctx.Before(tc.ResetBeforeEachScenario) | ||||||
| 
 | 
 | ||||||
| 	ctx.Step(`^(?:a )?feature path "([^"]*)"$`, tc.featurePath) | 	ctx.Step(`^(?:a )?feature path "([^"]*)"$`, tc.featurePath) | ||||||
| 	ctx.Step(`^I parse features$`, tc.parseFeatures) | 	ctx.Step(`^I parse features$`, tc.parseFeatures) | ||||||
|  | @ -108,19 +110,42 @@ func InitializeScenario(ctx *ScenarioContext) { | ||||||
| 		return nil | 		return nil | ||||||
| 	}) | 	}) | ||||||
| 
 | 
 | ||||||
| 	ctx.Step(`^(?:a )?passing step without return$`, func() {}) | 	ctx.Step(`^passing step without return$`, func() {}) | ||||||
| 	ctx.BeforeStep(tc.inject) | 
 | ||||||
|  | 	ctx.Step(`^having correct context$`, func(ctx context.Context) (context.Context, error) { | ||||||
|  | 		if ctx.Value(ctxKey("BeforeScenario")) == nil { | ||||||
|  | 			return ctx, errors.New("missing BeforeScenario in context") | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| func (tc *godogFeaturesScenario) inject(step *Step) { | 		if ctx.Value(ctxKey("BeforeStep")) == nil { | ||||||
|  | 			return ctx, errors.New("missing BeforeStep in context") | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if ctx.Value(ctxKey("StepState")) == nil { | ||||||
|  | 			return ctx, errors.New("missing StepState in context") | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		return context.WithValue(ctx, ctxKey("Step"), true), nil | ||||||
|  | 	}) | ||||||
|  | 
 | ||||||
|  | 	ctx.Step(`^adding step state to context$`, func(ctx context.Context) context.Context { | ||||||
|  | 		return context.WithValue(ctx, ctxKey("StepState"), true) | ||||||
|  | 	}) | ||||||
|  | 
 | ||||||
|  | 	ctx.StepContext().Before(tc.inject) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type ctxKey string | ||||||
|  | 
 | ||||||
|  | func (tc *godogFeaturesScenario) inject(ctx context.Context, step *Step) (context.Context, error) { | ||||||
| 	if !tc.allowInjection { | 	if !tc.allowInjection { | ||||||
| 		return | 		return ctx, nil | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	step.Text = injectAll(step.Text) | 	step.Text = injectAll(step.Text) | ||||||
| 
 | 
 | ||||||
| 	if step.Argument == nil { | 	if step.Argument == nil { | ||||||
| 		return | 		return ctx, nil | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if table := step.Argument.DataTable; table != nil { | 	if table := step.Argument.DataTable; table != nil { | ||||||
|  | @ -134,6 +159,8 @@ func (tc *godogFeaturesScenario) inject(step *Step) { | ||||||
| 	if doc := step.Argument.DocString; doc != nil { | 	if doc := step.Argument.DocString; doc != nil { | ||||||
| 		doc.Content = injectAll(doc.Content) | 		doc.Content = injectAll(doc.Content) | ||||||
| 	} | 	} | ||||||
|  | 
 | ||||||
|  | 	return ctx, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func injectAll(src string) string { | func injectAll(src string) string { | ||||||
|  | @ -167,7 +194,7 @@ type godogFeaturesScenario struct { | ||||||
| 	allowInjection   bool | 	allowInjection   bool | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (tc *godogFeaturesScenario) ResetBeforeEachScenario(*Scenario) { | func (tc *godogFeaturesScenario) ResetBeforeEachScenario(ctx context.Context, sc *Scenario) (context.Context, error) { | ||||||
| 	// reset whole suite with the state | 	// reset whole suite with the state | ||||||
| 	tc.out.Reset() | 	tc.out.Reset() | ||||||
| 	tc.paths = []string{} | 	tc.paths = []string{} | ||||||
|  | @ -179,6 +206,8 @@ func (tc *godogFeaturesScenario) ResetBeforeEachScenario(*Scenario) { | ||||||
| 	// reset all fired events | 	// reset all fired events | ||||||
| 	tc.events = []*firedEvent{} | 	tc.events = []*firedEvent{} | ||||||
| 	tc.allowInjection = false | 	tc.allowInjection = false | ||||||
|  | 
 | ||||||
|  | 	return ctx, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (tc *godogFeaturesScenario) iSetVariableInjectionTo(to string) error { | func (tc *godogFeaturesScenario) iSetVariableInjectionTo(to string) error { | ||||||
|  | @ -391,20 +420,56 @@ func (tc *godogFeaturesScenario) iAmListeningToSuiteEvents() error { | ||||||
| 
 | 
 | ||||||
| 	scenarioContext := ScenarioContext{suite: tc.testedSuite} | 	scenarioContext := ScenarioContext{suite: tc.testedSuite} | ||||||
| 
 | 
 | ||||||
| 	scenarioContext.BeforeScenario(func(pickle *Scenario) { | 	scenarioContext.Before(func(ctx context.Context, pickle *Scenario) (context.Context, error) { | ||||||
| 		tc.events = append(tc.events, &firedEvent{"BeforeScenario", []interface{}{pickle}}) | 		tc.events = append(tc.events, &firedEvent{"BeforeScenario", []interface{}{pickle}}) | ||||||
|  | 
 | ||||||
|  | 		return context.WithValue(ctx, ctxKey("BeforeScenario"), true), nil | ||||||
| 	}) | 	}) | ||||||
| 
 | 
 | ||||||
| 	scenarioContext.AfterScenario(func(pickle *Scenario, err error) { | 	scenarioContext.After(func(ctx context.Context, pickle *Scenario, err error) (context.Context, error) { | ||||||
| 		tc.events = append(tc.events, &firedEvent{"AfterScenario", []interface{}{pickle, err}}) | 		tc.events = append(tc.events, &firedEvent{"AfterScenario", []interface{}{pickle, err}}) | ||||||
|  | 
 | ||||||
|  | 		if ctx.Value(ctxKey("BeforeScenario")) == nil { | ||||||
|  | 			return ctx, errors.New("missing BeforeScenario in context") | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if ctx.Value(ctxKey("AfterStep")) == nil { | ||||||
|  | 			return ctx, errors.New("missing AfterStep in context") | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		return context.WithValue(ctx, ctxKey("AfterScenario"), true), nil | ||||||
| 	}) | 	}) | ||||||
| 
 | 
 | ||||||
| 	scenarioContext.BeforeStep(func(step *Step) { | 	scenarioContext.StepContext().Before(func(ctx context.Context, step *Step) (context.Context, error) { | ||||||
| 		tc.events = append(tc.events, &firedEvent{"BeforeStep", []interface{}{step}}) | 		tc.events = append(tc.events, &firedEvent{"BeforeStep", []interface{}{step}}) | ||||||
|  | 
 | ||||||
|  | 		if ctx.Value(ctxKey("BeforeScenario")) == nil { | ||||||
|  | 			return ctx, errors.New("missing BeforeScenario in context") | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		return context.WithValue(ctx, ctxKey("BeforeStep"), true), nil | ||||||
| 	}) | 	}) | ||||||
| 
 | 
 | ||||||
| 	scenarioContext.AfterStep(func(step *Step, err error) { | 	scenarioContext.StepContext().After(func(ctx context.Context, step *Step, status StepResultStatus, err error) (context.Context, error) { | ||||||
| 		tc.events = append(tc.events, &firedEvent{"AfterStep", []interface{}{step, err}}) | 		tc.events = append(tc.events, &firedEvent{"AfterStep", []interface{}{step, err}}) | ||||||
|  | 
 | ||||||
|  | 		if ctx.Value(ctxKey("BeforeScenario")) == nil { | ||||||
|  | 			return ctx, errors.New("missing BeforeScenario in context") | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if ctx.Value(ctxKey("BeforeStep")) == nil { | ||||||
|  | 			return ctx, errors.New("missing BeforeStep in context") | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if step.Text == "having correct context" && ctx.Value(ctxKey("Step")) == nil { | ||||||
|  | 			if status != StepSkipped { | ||||||
|  | 				return ctx, fmt.Errorf("unexpected step result status: %s", status) | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			return ctx, errors.New("missing Step in context") | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		return context.WithValue(ctx, ctxKey("AfterStep"), true), nil | ||||||
| 	}) | 	}) | ||||||
| 
 | 
 | ||||||
| 	return nil | 	return nil | ||||||
|  |  | ||||||
							
								
								
									
										101
									
								
								test_context.go
									
										
									
									
									
								
							
							
						
						
									
										101
									
								
								test_context.go
									
										
									
									
									
								
							|  | @ -1,6 +1,7 @@ | ||||||
| package godog | package godog | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"context" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"reflect" | 	"reflect" | ||||||
| 	"regexp" | 	"regexp" | ||||||
|  | @ -12,9 +13,6 @@ import ( | ||||||
| 	"github.com/cucumber/godog/internal/models" | 	"github.com/cucumber/godog/internal/models" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // matchable errors |  | ||||||
| var () |  | ||||||
| 
 |  | ||||||
| // Scenario represents the executed scenario | // Scenario represents the executed scenario | ||||||
| type Scenario = messages.Pickle | type Scenario = messages.Pickle | ||||||
| 
 | 
 | ||||||
|  | @ -97,26 +95,101 @@ type ScenarioContext struct { | ||||||
| 	suite *suite | 	suite *suite | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // StepContext allows registering step hooks. | ||||||
|  | type StepContext struct { | ||||||
|  | 	suite *suite | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Before registers a a function or method | ||||||
|  | // to be run before every scenario. | ||||||
|  | // | ||||||
|  | // It is a good practice to restore the default state | ||||||
|  | // before every scenario so it would be isolated from | ||||||
|  | // any kind of state. | ||||||
|  | func (ctx ScenarioContext) Before(h BeforeScenarioHook) { | ||||||
|  | 	ctx.suite.beforeScenarioHandlers = append(ctx.suite.beforeScenarioHandlers, h) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // BeforeScenarioHook defines a hook before scenario. | ||||||
|  | type BeforeScenarioHook func(ctx context.Context, sc *Scenario) (context.Context, error) | ||||||
|  | 
 | ||||||
|  | // After registers an function or method | ||||||
|  | // to be run after every scenario. | ||||||
|  | func (ctx ScenarioContext) After(h AfterScenarioHook) { | ||||||
|  | 	ctx.suite.afterScenarioHandlers = append(ctx.suite.afterScenarioHandlers, h) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // AfterScenarioHook defines a hook after scenario. | ||||||
|  | type AfterScenarioHook func(ctx context.Context, sc *Scenario, err error) (context.Context, error) | ||||||
|  | 
 | ||||||
|  | // StepContext exposes StepContext of a scenario. | ||||||
|  | func (ctx *ScenarioContext) StepContext() StepContext { | ||||||
|  | 	return StepContext{suite: ctx.suite} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Before registers a function or method | ||||||
|  | // to be run before every step. | ||||||
|  | func (ctx StepContext) Before(h BeforeStepHook) { | ||||||
|  | 	ctx.suite.beforeStepHandlers = append(ctx.suite.beforeStepHandlers, h) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // BeforeStepHook defines a hook before step. | ||||||
|  | type BeforeStepHook func(ctx context.Context, st *Step) (context.Context, error) | ||||||
|  | 
 | ||||||
|  | // After registers an function or method | ||||||
|  | // to be run after every step. | ||||||
|  | // | ||||||
|  | // It may be convenient to return a different kind of error | ||||||
|  | // in order to print more state details which may help | ||||||
|  | // in case of step failure | ||||||
|  | // | ||||||
|  | // In some cases, for example when running a headless | ||||||
|  | // browser, to take a screenshot after failure. | ||||||
|  | func (ctx StepContext) After(h AfterStepHook) { | ||||||
|  | 	ctx.suite.afterStepHandlers = append(ctx.suite.afterStepHandlers, h) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // AfterStepHook defines a hook after step. | ||||||
|  | type AfterStepHook func(ctx context.Context, st *Step, status StepResultStatus, err error) (context.Context, error) | ||||||
|  | 
 | ||||||
| // BeforeScenario registers a function or method | // BeforeScenario registers a function or method | ||||||
| // to be run before every scenario. | // to be run before every scenario. | ||||||
| // | // | ||||||
| // It is a good practice to restore the default state | // It is a good practice to restore the default state | ||||||
| // before every scenario so it would be isolated from | // before every scenario so it would be isolated from | ||||||
| // any kind of state. | // any kind of state. | ||||||
|  | // | ||||||
|  | // Deprecated: use Before. | ||||||
| func (ctx *ScenarioContext) BeforeScenario(fn func(sc *Scenario)) { | func (ctx *ScenarioContext) BeforeScenario(fn func(sc *Scenario)) { | ||||||
| 	ctx.suite.beforeScenarioHandlers = append(ctx.suite.beforeScenarioHandlers, fn) | 	ctx.Before(func(ctx context.Context, sc *Scenario) (context.Context, error) { | ||||||
|  | 		fn(sc) | ||||||
|  | 
 | ||||||
|  | 		return ctx, nil | ||||||
|  | 	}) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // AfterScenario registers an function or method | // AfterScenario registers an function or method | ||||||
| // to be run after every scenario. | // to be run after every scenario. | ||||||
|  | // | ||||||
|  | // Deprecated: use After. | ||||||
| func (ctx *ScenarioContext) AfterScenario(fn func(sc *Scenario, err error)) { | func (ctx *ScenarioContext) AfterScenario(fn func(sc *Scenario, err error)) { | ||||||
| 	ctx.suite.afterScenarioHandlers = append(ctx.suite.afterScenarioHandlers, fn) | 	ctx.After(func(ctx context.Context, sc *Scenario, err error) (context.Context, error) { | ||||||
|  | 		fn(sc, err) | ||||||
|  | 
 | ||||||
|  | 		return ctx, nil | ||||||
|  | 	}) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // BeforeStep registers a function or method | // BeforeStep registers a function or method | ||||||
| // to be run before every step. | // to be run before every step. | ||||||
|  | // | ||||||
|  | // Deprecated: use ScenarioContext.StepContext() and StepContext.Before. | ||||||
| func (ctx *ScenarioContext) BeforeStep(fn func(st *Step)) { | func (ctx *ScenarioContext) BeforeStep(fn func(st *Step)) { | ||||||
| 	ctx.suite.beforeStepHandlers = append(ctx.suite.beforeStepHandlers, fn) | 	ctx.StepContext().Before(func(ctx context.Context, st *Step) (context.Context, error) { | ||||||
|  | 		fn(st) | ||||||
|  | 
 | ||||||
|  | 		return ctx, nil | ||||||
|  | 	}) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // AfterStep registers an function or method | // AfterStep registers an function or method | ||||||
|  | @ -128,8 +201,14 @@ func (ctx *ScenarioContext) BeforeStep(fn func(st *Step)) { | ||||||
| // | // | ||||||
| // In some cases, for example when running a headless | // In some cases, for example when running a headless | ||||||
| // browser, to take a screenshot after failure. | // browser, to take a screenshot after failure. | ||||||
|  | // | ||||||
|  | // Deprecated: use ScenarioContext.StepContext() and StepContext.After. | ||||||
| func (ctx *ScenarioContext) AfterStep(fn func(st *Step, err error)) { | func (ctx *ScenarioContext) AfterStep(fn func(st *Step, err error)) { | ||||||
| 	ctx.suite.afterStepHandlers = append(ctx.suite.afterStepHandlers, fn) | 	ctx.StepContext().After(func(ctx context.Context, st *Step, status StepResultStatus, err error) (context.Context, error) { | ||||||
|  | 		fn(st, err) | ||||||
|  | 
 | ||||||
|  | 		return ctx, nil | ||||||
|  | 	}) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Step allows to register a *StepDefinition in the | // Step allows to register a *StepDefinition in the | ||||||
|  | @ -179,8 +258,8 @@ func (ctx *ScenarioContext) Step(expr, stepFunc interface{}) { | ||||||
| 		panic(fmt.Sprintf("expected handler to be func, but got: %T", stepFunc)) | 		panic(fmt.Sprintf("expected handler to be func, but got: %T", stepFunc)) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if typ.NumOut() > 1 { | 	if typ.NumOut() > 2 { | ||||||
| 		panic(fmt.Sprintf("expected handler to return either zero or one value, but it has: %d", typ.NumOut())) | 		panic(fmt.Sprintf("expected handler to return either zero, one or two values, but it has: %d", typ.NumOut())) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	def := &models.StepDefinition{ | 	def := &models.StepDefinition{ | ||||||
|  | @ -195,8 +274,8 @@ func (ctx *ScenarioContext) Step(expr, stepFunc interface{}) { | ||||||
| 		typ = typ.Out(0) | 		typ = typ.Out(0) | ||||||
| 		switch typ.Kind() { | 		switch typ.Kind() { | ||||||
| 		case reflect.Interface: | 		case reflect.Interface: | ||||||
| 			if !typ.Implements(errorInterface) { | 			if !typ.Implements(errorInterface) && !typ.Implements(contextInterface) { | ||||||
| 				panic(fmt.Sprintf("expected handler to return an error, but got: %s", typ.Kind())) | 				panic(fmt.Sprintf("expected handler to return an error or context.Context, but got: %s", typ.Kind())) | ||||||
| 			} | 			} | ||||||
| 		case reflect.Slice: | 		case reflect.Slice: | ||||||
| 			if typ.Elem().Kind() != reflect.String { | 			if typ.Elem().Kind() != reflect.String { | ||||||
|  |  | ||||||
|  | @ -41,13 +41,13 @@ func TestScenarioContext_Step(t *testing.T) { | ||||||
| 				So(func() { ctx.Step(".*", 124) }, ShouldPanicWith, fmt.Sprintf("expected handler to be func, but got: %T", 12)) | 				So(func() { ctx.Step(".*", 124) }, ShouldPanicWith, fmt.Sprintf("expected handler to be func, but got: %T", 12)) | ||||||
| 			}) | 			}) | ||||||
| 
 | 
 | ||||||
| 			Convey("has more than 1 return value", func() { | 			Convey("has more than 2 return values", func() { | ||||||
| 				So(func() { ctx.Step(".*", nokLimitCase) }, ShouldPanicWith, fmt.Sprintf("expected handler to return either zero or one value, but it has: 2")) | 				So(func() { ctx.Step(".*", nokLimitCase) }, ShouldPanicWith, fmt.Sprintf("expected handler to return either zero, one or two values, but it has: 3")) | ||||||
| 				So(func() { ctx.Step(".*", nokMore) }, ShouldPanicWith, fmt.Sprintf("expected handler to return either zero or one value, but it has: 5")) | 				So(func() { ctx.Step(".*", nokMore) }, ShouldPanicWith, fmt.Sprintf("expected handler to return either zero, one or two values, but it has: 5")) | ||||||
| 			}) | 			}) | ||||||
| 
 | 
 | ||||||
| 			Convey("return type is not an error or string slice or void", func() { | 			Convey("return type is not an error or string slice or void", func() { | ||||||
| 				So(func() { ctx.Step(".*", nokInvalidReturnInterfaceType) }, ShouldPanicWith, "expected handler to return an error, but got: interface") | 				So(func() { ctx.Step(".*", nokInvalidReturnInterfaceType) }, ShouldPanicWith, "expected handler to return an error or context.Context, but got: interface") | ||||||
| 				So(func() { ctx.Step(".*", nokInvalidReturnSliceType) }, ShouldPanicWith, "expected handler to return []string for multistep, but got: []int") | 				So(func() { ctx.Step(".*", nokInvalidReturnSliceType) }, ShouldPanicWith, "expected handler to return []string for multistep, but got: []int") | ||||||
| 				So(func() { ctx.Step(".*", nokInvalidReturnOtherType) }, ShouldPanicWith, "expected handler to return an error or []string, but got: chan") | 				So(func() { ctx.Step(".*", nokInvalidReturnOtherType) }, ShouldPanicWith, "expected handler to return an error or []string, but got: chan") | ||||||
| 			}) | 			}) | ||||||
|  | @ -60,7 +60,7 @@ func TestScenarioContext_Step(t *testing.T) { | ||||||
| func okEmptyResult()                             {} | func okEmptyResult()                             {} | ||||||
| func okErrorResult() error                       { return nil } | func okErrorResult() error                       { return nil } | ||||||
| func okSliceResult() []string                    { return nil } | func okSliceResult() []string                    { return nil } | ||||||
| func nokLimitCase() (int, error)                 { return 0, nil } | func nokLimitCase() (string, int, error)         { return "", 0, nil } | ||||||
| func nokMore() (int, int, int, int, error)       { return 0, 0, 0, 0, nil } | func nokMore() (int, int, int, int, error)       { return 0, 0, 0, 0, nil } | ||||||
| func nokInvalidReturnInterfaceType() interface{} { return 0 } | func nokInvalidReturnInterfaceType() interface{} { return 0 } | ||||||
| func nokInvalidReturnSliceType() []int           { return nil } | func nokInvalidReturnSliceType() []int           { return nil } | ||||||
|  |  | ||||||
		Загрузка…
	
	Создание таблицы
		
		Сослаться в новой задаче
	
	 Viacheslav Poturaev
						Viacheslav Poturaev