Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement unambiguous keywords #509

Merged
merged 10 commits into from
Jan 23, 2023
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ This project adheres to [Semantic Versioning](http://semver.org).
This document is formatted according to the principles of [Keep A CHANGELOG](http://keepachangelog.com).

## Unreleased
### Added
- Added keyword functions. ([509](https://github.com/cucumber/godog/pull/509) - [otrava7](https://github.com/otrava7))

## [v0.12.6]
### Changed
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,15 @@ func InitializeScenario(ctx *godog.ScenarioContext) {
}
```

Alternatively, you can also specify the keyword (Given, When, Then...) when creating the step definitions:
``` go
func InitializeScenario(ctx *godog.ScenarioContext) {
ctx.Given(`^I eat (\d+)$`, iEat)
ctx.When(`^there are (\d+) godogs$`, thereAreGodogs)
ctx.Then(`^there should be (\d+) remaining$`, thereShouldBeRemaining)
}
```

Our module should now look like this:
```
godogs
Expand Down
6 changes: 3 additions & 3 deletions features/events.feature
Original file line number Diff line number Diff line change
Expand Up @@ -121,17 +121,17 @@ Feature: suite events
Feature: scenario hook errors

Scenario: failing before and after scenario # normal.feature:3
Then adding step state to context # suite_context_test.go:0 -> InitializeScenario.func12
Then adding step state to context # suite_context_test.go:0 -> InitializeScenario.func17
after scenario hook failed: failed in after scenario hook, step error: before scenario hook failed: failed in before scenario hook
And passing step # suite_context_test.go:0 -> InitializeScenario.func2

Scenario: failing before scenario # normal.feature:7
Then adding step state to context # suite_context_test.go:0 -> InitializeScenario.func12
Then adding step state to context # suite_context_test.go:0 -> InitializeScenario.func17
before scenario hook failed: failed in before scenario hook
And passing step # suite_context_test.go:0 -> InitializeScenario.func2

Scenario: failing after scenario # normal.feature:11
Then adding step state to context # suite_context_test.go:0 -> InitializeScenario.func12
Then adding step state to context # suite_context_test.go:0 -> InitializeScenario.func17
And passing step # suite_context_test.go:0 -> InitializeScenario.func2
after scenario hook failed: failed in after scenario hook

Expand Down
144 changes: 140 additions & 4 deletions features/formatter/pretty.feature
Original file line number Diff line number Diff line change
Expand Up @@ -332,11 +332,11 @@ Feature: pretty formatter
Feature: inject long value

Scenario: test scenario # features/inject.feature:3
Given Ignore I save some value X under key Y # suite_context.go:0 -> SuiteContext.func7
Given Ignore I save some value X under key Y # suite_context.go:0 -> SuiteContext.func12
And I allow variable injection # suite_context.go:0 -> *suiteContext
When Ignore I use value someverylonginjectionsoweacanbesureitsurpasstheinitiallongeststeplenghtanditwillhelptestsmethodsafety # suite_context.go:0 -> SuiteContext.func7
Then Ignore Godog rendering should not break # suite_context.go:0 -> SuiteContext.func7
And Ignore test # suite_context.go:0 -> SuiteContext.func7
When Ignore I use value someverylonginjectionsoweacanbesureitsurpasstheinitiallongeststeplenghtanditwillhelptestsmethodsafety # suite_context.go:0 -> SuiteContext.func12
Then Ignore Godog rendering should not break # suite_context.go:0 -> SuiteContext.func12
And Ignore test # suite_context.go:0 -> SuiteContext.func12
| key | val |
| 1 | 2 |
| 3 | 4 |
Expand Down Expand Up @@ -548,3 +548,139 @@ Feature: pretty formatter
2 steps (1 passed, 1 failed)
0s
"""

Scenario: Use 'given' keyword on a declared 'when' step
Given a feature "features/simple.feature" file:
"""
Feature: simple feature with a rule
simple feature description
Rule: simple rule
simple rule description
Example: simple scenario
simple scenario description
Given a when step
"""
When I run feature suite with formatter "pretty"
Then the rendered output will be as follows:
"""
Feature: simple feature with a rule
simple feature description

Example: simple scenario # features/simple.feature:5
Given a when step

1 scenarios (1 undefined)
1 steps (1 undefined)
0s

You can implement step definitions for undefined steps with these snippets:

func aWhenStep() error {
return godog.ErrPending
}

func InitializeScenario(ctx *godog.ScenarioContext) {
ctx.Step(`^a when step$`, aWhenStep)
}
"""

Scenario: Use 'when' keyword on a declared 'then' step
Given a feature "features/simple.feature" file:
"""
Feature: simple feature with a rule
simple feature description
Rule: simple rule
simple rule description
Example: simple scenario
simple scenario description
When a then step
"""
When I run feature suite with formatter "pretty"
Then the rendered output will be as follows:
"""
Feature: simple feature with a rule
simple feature description

Example: simple scenario # features/simple.feature:5
When a then step

1 scenarios (1 undefined)
1 steps (1 undefined)
0s

You can implement step definitions for undefined steps with these snippets:

func aThenStep() error {
return godog.ErrPending
}

func InitializeScenario(ctx *godog.ScenarioContext) {
ctx.Step(`^a then step$`, aThenStep)
}
"""

Scenario: Use 'then' keyword on a declared 'given' step
Given a feature "features/simple.feature" file:
"""
Feature: simple feature with a rule
simple feature description
Rule: simple rule
simple rule description
Example: simple scenario
simple scenario description
Then a given step
"""
When I run feature suite with formatter "pretty"
Then the rendered output will be as follows:
"""
Feature: simple feature with a rule
simple feature description

Example: simple scenario # features/simple.feature:5
Then a given step

1 scenarios (1 undefined)
1 steps (1 undefined)
0s

You can implement step definitions for undefined steps with these snippets:

func aGivenStep() error {
return godog.ErrPending
}

func InitializeScenario(ctx *godog.ScenarioContext) {
ctx.Step(`^a given step$`, aGivenStep)
}
"""

Scenario: Match keyword functions correctly
Given a feature "features/simple.feature" file:
"""
Feature: simple feature with a rule
simple feature description
Rule: simple rule
simple rule description
Example: simple scenario
simple scenario description
Given a given step
When a when step
Then a then step
And a then step
"""
When I run feature suite with formatter "pretty"
Then the rendered output will be as follows:
"""
Feature: simple feature with a rule
simple feature description

Example: simple scenario # features/simple.feature:5
Given a given step # suite_context_test.go:0 -> InitializeScenario.func3
When a when step # suite_context_test.go:0 -> InitializeScenario.func4
Then a then step # suite_context_test.go:0 -> InitializeScenario.func5
And a then step # suite_context_test.go:0 -> InitializeScenario.func5

1 scenarios (1 passed)
4 steps (4 passed)
0s
"""
37 changes: 37 additions & 0 deletions features/multistep.feature
Original file line number Diff line number Diff line change
Expand Up @@ -161,3 +161,40 @@ Feature: run features with nested steps
"""
When I run feature suite
Then the suite should have passed

Scenario: should run passing multistep using keyword function successfully
Given a feature "normal.feature" file:
"""
Feature: normal feature

Scenario: run passing multistep
Given passing step
Then passing multistep using 'then' function
"""
When I run feature suite
Then the suite should have passed
And the following steps should be passed:
"""
passing step
passing multistep using 'then' function
"""

Scenario: should identify undefined multistep using keyword function
Given a feature "normal.feature" file:
"""
Feature: normal feature

Scenario: run passing multistep
Given passing step
Then undefined multistep using 'then' function
"""
When I run feature suite
Then the suite should have passed
And the following steps should be passed:
"""
passing step
"""
And the following step should be undefined:
"""
undefined multistep using 'then' function
"""
10 changes: 10 additions & 0 deletions formatters/fmt.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,14 @@ type FormatterFunc func(string, io.Writer) Formatter
type StepDefinition struct {
Expr *regexp.Regexp
Handler interface{}
Keyword Keyword
}

type Keyword int64

const (
Given Keyword = iota
When
Then
None
)
14 changes: 8 additions & 6 deletions run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -518,11 +518,12 @@ func Test_AllFeaturesRun(t *testing.T) {
...................................................................... 140
...................................................................... 210
...................................................................... 280
....................................................... 335
...................................................................... 350
...... 356


88 scenarios (88 passed)
335 steps (335 passed)
94 scenarios (94 passed)
356 steps (356 passed)
0s
`

Expand All @@ -545,11 +546,12 @@ func Test_AllFeaturesRunAsSubtests(t *testing.T) {
...................................................................... 140
...................................................................... 210
...................................................................... 280
....................................................... 335
...................................................................... 350
...... 356


88 scenarios (88 passed)
335 steps (335 passed)
94 scenarios (94 passed)
356 steps (356 passed)
0s
`

Expand Down
35 changes: 28 additions & 7 deletions suite.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ type suite struct {
}

func (s *suite) matchStep(step *messages.PickleStep) *models.StepDefinition {
def := s.matchStepText(step.Text)
def := s.matchStepTextAndType(step.Text, step.Type)
if def != nil && step.Argument != nil {
def.Args = append(def.Args, step.Argument)
}
Expand Down Expand Up @@ -147,14 +147,15 @@ func (s *suite) runStep(ctx context.Context, pickle *Scenario, step *Step, prevS
return ctx, err
}

if ctx, undef, err := s.maybeUndefined(ctx, step.Text, step.Argument); err != nil {
if ctx, undef, err := s.maybeUndefined(ctx, step.Text, step.Argument, step.Type); err != nil {
return ctx, err
} else if len(undef) > 0 {
if match != nil {
match = &models.StepDefinition{
StepDefinition: formatters.StepDefinition{
Expr: match.Expr,
Handler: match.Handler,
Keyword: match.Keyword,
},
Args: match.Args,
HandlerValue: match.HandlerValue,
Expand Down Expand Up @@ -297,8 +298,8 @@ func (s *suite) runAfterScenarioHooks(ctx context.Context, pickle *messages.Pick
return ctx, err
}

func (s *suite) maybeUndefined(ctx context.Context, text string, arg interface{}) (context.Context, []string, error) {
step := s.matchStepText(text)
func (s *suite) maybeUndefined(ctx context.Context, text string, arg interface{}, stepType messages.PickleStepType) (context.Context, []string, error) {
step := s.matchStepTextAndType(text, stepType)
if nil == step {
return ctx, []string{text}, nil
}
Expand All @@ -323,7 +324,7 @@ func (s *suite) maybeUndefined(ctx context.Context, text string, arg interface{}
if len(lines[0]) > 0 && lines[0][len(lines[0])-1] == ':' {
return ctx, undefined, fmt.Errorf("nested steps cannot be multiline and have table or content body argument")
}
ctx, undef, err := s.maybeUndefined(ctx, next, nil)
ctx, undef, err := s.maybeUndefined(ctx, next, nil, messages.PickleStepType_UNKNOWN)
if err != nil {
return ctx, undefined, err
}
Expand All @@ -349,7 +350,7 @@ func (s *suite) maybeSubSteps(ctx context.Context, result interface{}) (context.
var err error

for _, text := range steps {
if def := s.matchStepText(text); def == nil {
if def := s.matchStepTextAndType(text, messages.PickleStepType_UNKNOWN); def == nil {
return ctx, ErrUndefined
} else if ctx, err = s.maybeSubSteps(def.Run(ctx)); err != nil {
return ctx, fmt.Errorf("%s: %+v", text, err)
Expand All @@ -358,9 +359,12 @@ func (s *suite) maybeSubSteps(ctx context.Context, result interface{}) (context.
return ctx, nil
}

func (s *suite) matchStepText(text string) *models.StepDefinition {
func (s *suite) matchStepTextAndType(text string, stepType messages.PickleStepType) *models.StepDefinition {
for _, h := range s.steps {
if m := h.Expr.FindStringSubmatch(text); len(m) > 0 {
if !keywordMatches(h.Keyword, stepType) {
continue
}
var args []interface{}
for _, m := range m[1:] {
args = append(args, m)
Expand All @@ -372,6 +376,7 @@ func (s *suite) matchStepText(text string) *models.StepDefinition {
StepDefinition: formatters.StepDefinition{
Expr: h.Expr,
Handler: h.Handler,
Keyword: h.Keyword,
},
Args: args,
HandlerValue: h.HandlerValue,
Expand All @@ -382,6 +387,22 @@ func (s *suite) matchStepText(text string) *models.StepDefinition {
return nil
}

func keywordMatches(k formatters.Keyword, stepType messages.PickleStepType) bool {
if k == formatters.None {
return true
}
switch stepType {
case messages.PickleStepType_CONTEXT:
return k == formatters.Given
case messages.PickleStepType_ACTION:
return k == formatters.When
case messages.PickleStepType_OUTCOME:
return k == formatters.Then
default:
return true
}
}

func (s *suite) runSteps(ctx context.Context, pickle *Scenario, steps []*Step) (context.Context, error) {
var (
stepErr, err error
Expand Down
Loading