number
9 |
10 | @tagged
11 | Examples: tagged
12 | | odd | even |
13 | | 1 | 2 |
14 | | 2 | 0 |
15 | | 3 | 11 |
16 |
17 | @tag2
18 | Examples:
19 | | odd | even |
20 | | 1 | 14 |
21 | | 3 | 9 |
22 |
--------------------------------------------------------------------------------
/_examples/attachments/README.md:
--------------------------------------------------------------------------------
1 | # An example of Making attachments to the reports
2 |
3 | The JSON (and in future NDJSON) report formats allow the inclusion of data attachments.
4 |
5 | These attachments could be console logs or file data or images for instance.
6 |
7 | The example in this directory shows how the godog API is used to add attachments to the JSON report.
8 |
9 |
10 | ## Run the example
11 |
12 | You must use the '-v' flag or you will not see the cucumber JSON output.
13 |
14 | go test -v attachments_test.go
15 |
16 |
17 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/cucumber/godog
2 |
3 | go 1.16
4 |
5 | require (
6 | github.com/cucumber/gherkin/go/v26 v26.2.0
7 | github.com/hashicorp/go-memdb v1.3.4
8 | github.com/spf13/cobra v1.7.0
9 | github.com/spf13/pflag v1.0.10
10 | github.com/stretchr/testify v1.11.1
11 | )
12 |
13 | require (
14 | github.com/cucumber/messages/go/v21 v21.0.1
15 | github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
16 | github.com/hashicorp/go-uuid v1.0.2 // indirect
17 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
18 | )
19 |
--------------------------------------------------------------------------------
/utils_test.go:
--------------------------------------------------------------------------------
1 | package godog
2 |
3 | import (
4 | "testing"
5 | "time"
6 |
7 | "github.com/cucumber/godog/internal/utils"
8 | )
9 |
10 | // this zeroes the time throughout whole test suite
11 | // and makes it easier to assert output
12 | // activated only when godog tests are being run
13 | func init() {
14 | utils.TimeNowFunc = func() time.Time {
15 | return time.Time{}
16 | }
17 | }
18 |
19 | func TestTimeNowFunc(t *testing.T) {
20 | now := utils.TimeNowFunc()
21 | if !now.IsZero() {
22 | t.Fatalf("expected zeroed time, but got: %s", now.Format(time.RFC3339))
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/internal/formatters/formatter-tests/pretty/single_scenario_with_passing_step:
--------------------------------------------------------------------------------
1 | Feature: single passing scenario
2 | describes
3 | a single scenario
4 | feature
5 |
6 | Scenario: one step passing # formatter-tests/features/single_scenario_with_passing_step.feature:6
7 | Given a passing step # fmt_output_test.go:101 -> github.com/cucumber/godog/internal/formatters_test.passingStepDef
8 |
9 | 1 scenarios (1 passed)
10 | 1 steps (1 passed)
11 | 0s
12 |
--------------------------------------------------------------------------------
/internal/formatters/utils_test.go:
--------------------------------------------------------------------------------
1 | package formatters
2 |
3 | import (
4 | "testing"
5 | "time"
6 |
7 | "github.com/cucumber/godog/internal/utils"
8 | )
9 |
10 | // this zeroes the time throughout whole test suite
11 | // and makes it easier to assert output
12 | // activated only when godog tests are being run
13 | func init() {
14 | utils.TimeNowFunc = func() time.Time {
15 | return time.Time{}
16 | }
17 | }
18 |
19 | func TestTimeNowFunc(t *testing.T) {
20 | now := utils.TimeNowFunc()
21 | if !now.IsZero() {
22 | t.Fatalf("expected zeroed time, but got: %s", now.Format(time.RFC3339))
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/cmd/godog/internal/cmd_version.go:
--------------------------------------------------------------------------------
1 | package internal
2 |
3 | import (
4 | "fmt"
5 | "os"
6 |
7 | "github.com/spf13/cobra"
8 |
9 | "github.com/cucumber/godog"
10 | )
11 |
12 | // CreateVersionCmd creates the version subcommand.
13 | func CreateVersionCmd() cobra.Command {
14 | versionCmd := cobra.Command{
15 | Use: "version",
16 | Short: "Show current version",
17 | Run: versionCmdRunFunc,
18 | Version: godog.Version,
19 | }
20 |
21 | return versionCmd
22 | }
23 |
24 | func versionCmdRunFunc(cmd *cobra.Command, args []string) {
25 | fmt.Fprintln(os.Stdout, "Godog version is:", godog.Version)
26 | }
27 |
--------------------------------------------------------------------------------
/internal/formatters/formatter-tests/junit/with_few_empty_scenarios:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/_examples/db/Makefile:
--------------------------------------------------------------------------------
1 |
2 | define DB_SQL
3 | CREATE TABLE users (
4 | `id` BIGINT UNSIGNED AUTO_INCREMENT NOT NULL,
5 | `username` VARCHAR(32) NOT NULL,
6 | `email` VARCHAR(255) NOT NULL,
7 | PRIMARY KEY (`id`),
8 | UNIQUE INDEX `uniq_email` (`email`)
9 | ) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB;
10 | endef
11 |
12 | export DB_SQL
13 |
14 | SQL := "$$DB_SQL"
15 |
16 | test:
17 | mysql -u root -e 'DROP DATABASE IF EXISTS `godog_test`'
18 | mysql -u root -e 'CREATE DATABASE IF NOT EXISTS `godog_test`'
19 | @mysql -u root godog_test -e $(SQL)
20 | godog users.feature
21 |
22 | .PHONY: test
23 |
--------------------------------------------------------------------------------
/flags_v0110_test.go:
--------------------------------------------------------------------------------
1 | package godog
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/cucumber/godog/internal/flags"
7 | "github.com/stretchr/testify/assert"
8 | )
9 |
10 | func Test_BindFlagsShouldRespectFlagDefaults(t *testing.T) {
11 | opts := flags.Options{}
12 |
13 | BindCommandLineFlags("flagDefaults.", &opts)
14 |
15 | assert.Equal(t, "pretty", opts.Format)
16 | assert.Equal(t, "", opts.Tags)
17 | assert.Equal(t, 1, opts.Concurrency)
18 | assert.False(t, opts.ShowStepDefinitions)
19 | assert.False(t, opts.StopOnFailure)
20 | assert.False(t, opts.Strict)
21 | assert.False(t, opts.NoColors)
22 | assert.Equal(t, int64(0), opts.Randomize)
23 | }
24 |
--------------------------------------------------------------------------------
/internal/formatters/formatter-tests/junit/stop_on_first_failure:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/internal/formatters/formatter-tests/cucumber/scenario_without_steps_with_background:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "uri": "formatter-tests/features/scenario_without_steps_with_background.feature",
4 | "id": "empty-feature",
5 | "keyword": "Feature",
6 | "name": "empty feature",
7 | "description": "",
8 | "line": 1,
9 | "elements": [
10 | {
11 | "id": "empty-feature;without-steps",
12 | "keyword": "Scenario",
13 | "name": "without steps",
14 | "description": "",
15 | "line": 6,
16 | "type": "scenario"
17 | }
18 | ]
19 | }
20 | ]
21 |
--------------------------------------------------------------------------------
/internal/formatters/formatter-tests/junit,pretty/empty_with_single_scenario_without_steps:
--------------------------------------------------------------------------------
1 | Feature: empty feature
2 |
3 | Scenario: without steps # formatter-tests/features/empty_with_single_scenario_without_steps.feature:3
4 |
5 |
6 |
7 |
8 |
9 |
10 | 1 scenarios (1 undefined)
11 | No steps
12 | 0s
13 |
--------------------------------------------------------------------------------
/internal/formatters/formatter-tests/cucumber/empty_with_single_scenario_without_steps:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "uri": "formatter-tests/features/empty_with_single_scenario_without_steps.feature",
4 | "id": "empty-feature",
5 | "keyword": "Feature",
6 | "name": "empty feature",
7 | "description": "",
8 | "line": 1,
9 | "elements": [
10 | {
11 | "id": "empty-feature;without-steps",
12 | "keyword": "Scenario",
13 | "name": "without steps",
14 | "description": "",
15 | "line": 3,
16 | "type": "scenario"
17 | }
18 | ]
19 | }
20 | ]
21 |
--------------------------------------------------------------------------------
/_examples/api/features/version.feature:
--------------------------------------------------------------------------------
1 | # file: version.feature
2 | Feature: get version
3 | In order to know godog version
4 | As an API user
5 | I need to be able to request version
6 |
7 | Scenario: does not allow POST method
8 | When I send "POST" request to "/version"
9 | Then the response code should be 405
10 | And the response should match json:
11 | """
12 | {
13 | "error": "Method not allowed"
14 | }
15 | """
16 |
17 | Scenario: should get version number
18 | When I send "GET" request to "/version"
19 | Then the response code should be 200
20 | And the response should match json:
21 | """
22 | {
23 | "version": "v0.0.0-dev"
24 | }
25 | """
26 |
--------------------------------------------------------------------------------
/internal/formatters/formatter-tests/events/empty_with_single_scenario_without_steps:
--------------------------------------------------------------------------------
1 | {"event":"TestRunStarted","version":"0.1.0","timestamp":-6795364578871,"suite":"events"}
2 | {"event":"TestSource","location":"formatter-tests/features/empty_with_single_scenario_without_steps.feature:1","source":"Feature: empty feature\n\n Scenario: without steps\n"}
3 | {"event":"TestCaseStarted","location":"formatter-tests/features/empty_with_single_scenario_without_steps.feature:3","timestamp":-6795364578871}
4 | {"event":"TestCaseFinished","location":"formatter-tests/features/empty_with_single_scenario_without_steps.feature:3","timestamp":-6795364578871,"status":"undefined"}
5 | {"event":"TestRunFinished","status":"pending","timestamp":-6795364578871,"snippets":"","memory":""}
6 |
--------------------------------------------------------------------------------
/internal/formatters/formatter-tests/cucumber/empty_with_single_scenario_without_steps_and_description:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "uri": "formatter-tests/features/empty_with_single_scenario_without_steps_and_description.feature",
4 | "id": "empty-feature",
5 | "keyword": "Feature",
6 | "name": "empty feature",
7 | "description": " describes\n an empty\n feature",
8 | "line": 1,
9 | "elements": [
10 | {
11 | "id": "empty-feature;without-steps",
12 | "keyword": "Scenario",
13 | "name": "without steps",
14 | "description": "",
15 | "line": 6,
16 | "type": "scenario"
17 | }
18 | ]
19 | }
20 | ]
21 |
--------------------------------------------------------------------------------
/internal/formatters/formatter-tests/junit,pretty/empty_with_single_scenario_without_steps_and_description:
--------------------------------------------------------------------------------
1 | Feature: empty feature
2 | describes
3 | an empty
4 | feature
5 |
6 | Scenario: without steps # formatter-tests/features/empty_with_single_scenario_without_steps_and_description.feature:6
7 |
8 |
9 |
10 |
11 |
12 |
13 | 1 scenarios (1 undefined)
14 | No steps
15 | 0s
16 |
--------------------------------------------------------------------------------
/stacktrace_test.go:
--------------------------------------------------------------------------------
1 | package godog
2 |
3 | import (
4 | "fmt"
5 | "runtime"
6 | "testing"
7 |
8 | "github.com/stretchr/testify/assert"
9 | )
10 |
11 | func callstack1() *stack {
12 | return callstack2()
13 | }
14 |
15 | func callstack2() *stack {
16 | return callstack3()
17 | }
18 |
19 | func callstack3() *stack {
20 | const depth = 4
21 | var pcs [depth]uintptr
22 | n := runtime.Callers(1, pcs[:])
23 | var st stack = pcs[0:n]
24 | return &st
25 | }
26 |
27 | func Test_Stacktrace(t *testing.T) {
28 | err := &traceError{
29 | msg: "err msg",
30 | stack: callstack1(),
31 | }
32 |
33 | expected := "err msg"
34 | actual := fmt.Sprintf("%s", err)
35 |
36 | assert.Equal(t, expected, actual)
37 | assert.NotContains(t, actual, "stacktrace_test.go")
38 | }
39 |
--------------------------------------------------------------------------------
/internal/formatters/formatter-tests/features/rules_with_examples_with_backgrounds.feature:
--------------------------------------------------------------------------------
1 | Feature: rules with examples with backgrounds
2 |
3 | Rule: first rule
4 |
5 | Background: for first rule
6 | Given passing step
7 | And passing step
8 |
9 | Example: rule 1 example 1
10 | When passing step
11 | Then passing step
12 |
13 | Example: rule 1 example 2
14 | When passing step
15 | Then passing step
16 |
17 |
18 | Rule: second rule
19 |
20 | Background: for second rule
21 | Given passing step
22 | And passing step
23 |
24 | Example: rule 1 example 1
25 | When passing step
26 | Then passing step
27 |
28 | Example: rule 2 example 2
29 | When passing step
30 | Then passing step
31 |
--------------------------------------------------------------------------------
/internal/formatters/formatter-tests/events/scenario_without_steps_with_background:
--------------------------------------------------------------------------------
1 | {"event":"TestRunStarted","version":"0.1.0","timestamp":-6795364578871,"suite":"events"}
2 | {"event":"TestSource","location":"formatter-tests/features/scenario_without_steps_with_background.feature:1","source":"Feature: empty feature\n\n Background:\n Given passing step\n\n Scenario: without steps\n"}
3 | {"event":"TestCaseStarted","location":"formatter-tests/features/scenario_without_steps_with_background.feature:6","timestamp":-6795364578871}
4 | {"event":"TestCaseFinished","location":"formatter-tests/features/scenario_without_steps_with_background.feature:6","timestamp":-6795364578871,"status":"undefined"}
5 | {"event":"TestRunFinished","status":"pending","timestamp":-6795364578871,"snippets":"","memory":""}
6 |
--------------------------------------------------------------------------------
/internal/formatters/formatter-tests/junit,pretty/scenario_without_steps_with_background:
--------------------------------------------------------------------------------
1 | Feature: empty feature
2 |
3 | Background:
4 | Given passing step
5 |
6 | Scenario: without steps # formatter-tests/features/scenario_without_steps_with_background.feature:6
7 |
8 |
9 |
10 |
11 |
12 |
13 | 1 scenarios (1 undefined)
14 | No steps
15 | 0s
16 |
--------------------------------------------------------------------------------
/_examples/custom-formatter/features/emoji.feature:
--------------------------------------------------------------------------------
1 | # file: $GOPATH/godogs/features/godogs.feature
2 | Feature: Custom emoji formatter examples
3 | In order to be happy
4 | As a hungry gopher
5 | I need to be able to eat godogs
6 |
7 | Scenario: Passing test
8 | Given there are 12 godogs
9 | When I eat 5
10 | Then there should be 7 remaining
11 |
12 | Scenario: Failing and Skipped test
13 | Given there are 12 godogs
14 | When I eat 5
15 | Then there should be 6 remaining
16 | And there should be 4 remaining
17 |
18 | Scenario: Undefined steps
19 | Given there are 12 godogs
20 | When I eat 5
21 | Then this step is not defined
22 |
23 | Scenario: Pending step
24 | Given there are 12 godogs
25 | When I eat 5
26 | Then this step is pending
27 |
--------------------------------------------------------------------------------
/_examples/godogs/godogs.go:
--------------------------------------------------------------------------------
1 | package godogs
2 |
3 | import (
4 | "fmt"
5 | )
6 |
7 | // Godogs is an example behavior holder.
8 | type Godogs int
9 |
10 | // Add increments Godogs count.
11 | func (g *Godogs) Add(n int) {
12 | *g = *g + Godogs(n)
13 | }
14 |
15 | // Eat decrements Godogs count or fails if there is not enough available.
16 | func (g *Godogs) Eat(n int) error {
17 | ng := Godogs(n)
18 |
19 | if (g == nil && ng > 0) || ng > *g {
20 | return fmt.Errorf("you cannot eat %d godogs, there are %d available", n, g.Available())
21 | }
22 |
23 | if ng > 0 {
24 | *g = *g - ng
25 | }
26 |
27 | return nil
28 | }
29 |
30 | // Available returns the number of currently available Godogs.
31 | func (g *Godogs) Available() int {
32 | if g == nil {
33 | return 0
34 | }
35 |
36 | return int(*g)
37 | }
38 |
--------------------------------------------------------------------------------
/internal/formatters/formatter-tests/junit/two_scenarios_with_background_fail:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/internal/builder/ast.go:
--------------------------------------------------------------------------------
1 | package builder
2 |
3 | import "go/ast"
4 |
5 | func astContexts(f *ast.File, selectName string) []string {
6 | var contexts []string
7 | for _, d := range f.Decls {
8 | switch fun := d.(type) {
9 | case *ast.FuncDecl:
10 | for _, param := range fun.Type.Params.List {
11 | switch expr := param.Type.(type) {
12 | case *ast.StarExpr:
13 | switch x := expr.X.(type) {
14 | case *ast.Ident:
15 | if x.Name == selectName {
16 | contexts = append(contexts, fun.Name.Name)
17 | }
18 | case *ast.SelectorExpr:
19 | switch t := x.X.(type) {
20 | case *ast.Ident:
21 | if t.Name == "godog" && x.Sel.Name == selectName {
22 | contexts = append(contexts, fun.Name.Name)
23 | }
24 | }
25 | }
26 | }
27 | }
28 | }
29 | }
30 | return contexts
31 | }
32 |
--------------------------------------------------------------------------------
/internal/formatters/formatter-tests/events/empty_with_single_scenario_without_steps_and_description:
--------------------------------------------------------------------------------
1 | {"event":"TestRunStarted","version":"0.1.0","timestamp":-6795364578871,"suite":"events"}
2 | {"event":"TestSource","location":"formatter-tests/features/empty_with_single_scenario_without_steps_and_description.feature:1","source":"Feature: empty feature\n describes\n an empty\n feature\n\n Scenario: without steps\n"}
3 | {"event":"TestCaseStarted","location":"formatter-tests/features/empty_with_single_scenario_without_steps_and_description.feature:6","timestamp":-6795364578871}
4 | {"event":"TestCaseFinished","location":"formatter-tests/features/empty_with_single_scenario_without_steps_and_description.feature:6","timestamp":-6795364578871,"status":"undefined"}
5 | {"event":"TestRunFinished","status":"pending","timestamp":-6795364578871,"snippets":"","memory":""}
6 |
--------------------------------------------------------------------------------
/attachment_test.go:
--------------------------------------------------------------------------------
1 | package godog
2 |
3 | import (
4 | "context"
5 | "testing"
6 |
7 | "github.com/stretchr/testify/assert"
8 | )
9 |
10 | func TestAttach(t *testing.T) {
11 |
12 | ctx := context.Background()
13 |
14 | ctx = Attach(ctx, Attachment{Body: []byte("body1"), FileName: "fileName1", MediaType: "mediaType1"})
15 | ctx = Attach(ctx, Attachment{Body: []byte("body2"), FileName: "fileName2", MediaType: "mediaType2"})
16 |
17 | attachments := Attachments(ctx)
18 |
19 | assert.Equal(t, 2, len(attachments))
20 |
21 | assert.Equal(t, []byte("body1"), attachments[0].Body)
22 | assert.Equal(t, "fileName1", attachments[0].FileName)
23 | assert.Equal(t, "mediaType1", attachments[0].MediaType)
24 |
25 | assert.Equal(t, []byte("body2"), attachments[1].Body)
26 | assert.Equal(t, "fileName2", attachments[1].FileName)
27 | assert.Equal(t, "mediaType2", attachments[1].MediaType)
28 | }
29 |
--------------------------------------------------------------------------------
/features/lang.feature:
--------------------------------------------------------------------------------
1 | # language: lt
2 | @lang
3 | Savybė: užkrauti savybes
4 | Kad būtų galima paleisti savybių testus
5 | Kaip testavimo įrankis
6 | Aš turiu galėti užregistruoti savybes
7 |
8 | Scenarijus: savybių užkrovimas iš aplanko
9 | Duota savybių aplankas "features"
10 | Kai aš išskaitau savybes
11 | Tada aš turėčiau turėti 14 savybių failus:
12 | """
13 | features/background.feature
14 | features/events.feature
15 | features/formatter/cucumber.feature
16 | features/formatter/events.feature
17 | features/formatter/junit.feature
18 | features/formatter/pretty.feature
19 | features/lang.feature
20 | features/load.feature
21 | features/multistep.feature
22 | features/outline.feature
23 | features/run.feature
24 | features/snippets.feature
25 | features/tags.feature
26 | features/testingt.feature
27 | """
28 |
--------------------------------------------------------------------------------
/_examples/db/README.md:
--------------------------------------------------------------------------------
1 | # An example of API with DB
2 |
3 | The following example demonstrates steps how we describe and test our API with DB using **godog**.
4 | To start with, see [API example](https://github.com/cucumber/godog/tree/master/_examples/api) before.
5 | We have extended it to be used with database.
6 |
7 | The interesting point is, that we have [go-txdb](https://github.com/DATA-DOG/go-txdb) library,
8 | which has an implementation of custom sql.driver to allow execute every and each scenario
9 | within a **transaction**. After it completes, transaction is rolled back so the state could
10 | be clean for the next scenario.
11 |
12 | To run **users.feature** you need MySQL installed on your system with an anonymous root password.
13 | Then run:
14 |
15 | make test
16 |
17 | The json comparisom function should be improved and we should also have placeholders for primary
18 | keys when comparing a json result.
19 |
--------------------------------------------------------------------------------
/internal/formatters/formatter-tests/junit/scenario_outline:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/internal/formatters/formatter-tests/junit,pretty/single_scenario_with_passing_step:
--------------------------------------------------------------------------------
1 | Feature: single passing scenario
2 | describes
3 | a single scenario
4 | feature
5 |
6 | Scenario: one step passing # formatter-tests/features/single_scenario_with_passing_step.feature:6
7 | Given a passing step # fmt_output_test.go:101 -> github.com/cucumber/godog/internal/formatters_test.passingStepDef
8 |
9 |
10 |
11 |
12 |
13 |
14 | 1 scenarios (1 passed)
15 | 1 steps (1 passed)
16 | 0s
17 |
--------------------------------------------------------------------------------
/internal/builder/builder_go113_test.go:
--------------------------------------------------------------------------------
1 | //go:build go1.13
2 | // +build go1.13
3 |
4 | package builder_test
5 |
6 | import (
7 | "os"
8 | "os/exec"
9 | "path/filepath"
10 | "testing"
11 | )
12 |
13 | func testWithVendoredGodogAndMod(t *testing.T) {
14 | builderTC := builderTestCase{}
15 |
16 | gopath := filepath.Join(os.TempDir(), t.Name(), "_gpc")
17 | defer os.RemoveAll(gopath)
18 |
19 | builderTC.dir = filepath.Join(gopath, "src", "godogs")
20 | builderTC.files = map[string]string{
21 | "godogs.feature": builderFeatureFile,
22 | "godogs.go": builderMainCodeFile,
23 | "godogs_test.go": builderTestFile,
24 | "go.mod": builderModFile,
25 | }
26 |
27 | builderTC.goModCmds = make([]*exec.Cmd, 2)
28 | builderTC.goModCmds[0] = exec.Command("go", "mod", "tidy")
29 | builderTC.goModCmds[1] = exec.Command("go", "mod", "vendor")
30 | builderTC.testCmdEnv = append(envVarsWithoutGopath(), "GOPATH="+gopath)
31 |
32 | builderTC.run(t)
33 | }
34 |
--------------------------------------------------------------------------------
/internal/formatters/formatter-tests/pretty/with_few_empty_scenarios:
--------------------------------------------------------------------------------
1 | Feature: few empty scenarios
2 |
3 | Scenario: one # formatter-tests/features/with_few_empty_scenarios.feature:3
4 |
5 | Scenario Outline: two # formatter-tests/features/with_few_empty_scenarios.feature:5
6 |
7 | Examples: first group
8 | | one | two |
9 | | 1 | 2 |
10 | | 4 | 7 |
11 |
12 | Examples: second group
13 | | one | two |
14 | | 5 | 9 |
15 |
16 | Scenario: three # formatter-tests/features/with_few_empty_scenarios.feature:16
17 |
18 | 5 scenarios (5 undefined)
19 | No steps
20 | 0s
21 |
--------------------------------------------------------------------------------
/internal/formatters/formatter-tests/progress/two_scenarios_with_background_fail:
--------------------------------------------------------------------------------
1 | .F--.F- 7
2 |
3 |
4 | --- Failed steps:
5 |
6 | Scenario: one # formatter-tests/features/two_scenarios_with_background_fail.feature:7
7 | And failing step # formatter-tests/features/two_scenarios_with_background_fail.feature:5
8 | Error: step failed
9 |
10 | Scenario: two # formatter-tests/features/two_scenarios_with_background_fail.feature:11
11 | And failing step # formatter-tests/features/two_scenarios_with_background_fail.feature:5
12 | Error: step failed
13 |
14 |
15 | 2 scenarios (2 failed)
16 | 7 steps (2 passed, 2 failed, 3 skipped)
17 | 0s
18 |
--------------------------------------------------------------------------------
/flags_v0110.go:
--------------------------------------------------------------------------------
1 | package godog
2 |
3 | import (
4 | "errors"
5 | "flag"
6 | "math/rand"
7 | "time"
8 |
9 | "github.com/spf13/pflag"
10 |
11 | "github.com/cucumber/godog/internal/flags"
12 | )
13 |
14 | // Choose randomly assigns a convenient pseudo-random seed value.
15 | // The resulting seed will be between `1-99999` for later ease of specification.
16 | func makeRandomSeed() int64 {
17 | return rand.New(rand.NewSource(time.Now().UTC().UnixNano())).Int63n(99998) + 1
18 | }
19 |
20 | func flagSet(opt *Options) *pflag.FlagSet {
21 | set := pflag.NewFlagSet("godog", pflag.ExitOnError)
22 | flags.BindRunCmdFlags("", set, opt)
23 | pflag.ErrHelp = errors.New("godog: help requested")
24 | return set
25 | }
26 |
27 | // BindCommandLineFlags binds godog flags to given flag set prefixed
28 | // by given prefix, without overriding usage
29 | func BindCommandLineFlags(prefix string, opts *Options) {
30 | flagSet := pflag.CommandLine
31 | flags.BindRunCmdFlags(prefix, flagSet, opts)
32 | pflag.CommandLine.AddGoFlagSet(flag.CommandLine)
33 | }
34 |
--------------------------------------------------------------------------------
/internal/formatters/formatter-tests/pretty/scenario_with_background:
--------------------------------------------------------------------------------
1 | Feature: single scenario with background
2 |
3 | Background: named
4 | Given passing step # fmt_output_test.go:101 -> github.com/cucumber/godog/internal/formatters_test.passingStepDef
5 | And passing step # fmt_output_test.go:101 -> github.com/cucumber/godog/internal/formatters_test.passingStepDef
6 |
7 | Scenario: scenario # formatter-tests/features/scenario_with_background.feature:7
8 | When passing step # fmt_output_test.go:101 -> github.com/cucumber/godog/internal/formatters_test.passingStepDef
9 | Then passing step # fmt_output_test.go:101 -> github.com/cucumber/godog/internal/formatters_test.passingStepDef
10 |
11 | 1 scenarios (1 passed)
12 | 4 steps (4 passed)
13 | 0s
14 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) SmartBear
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/internal/builder/builder_go112_test.go:
--------------------------------------------------------------------------------
1 | //go:build go1.12 && !go1.13
2 | // +build go1.12,!go1.13
3 |
4 | package builder_test
5 |
6 | import (
7 | "os"
8 | "path/filepath"
9 | "testing"
10 |
11 | "github.com/stretchr/testify/require"
12 | )
13 |
14 | func testWithVendoredGodogAndMod(t *testing.T) {
15 | builderTC := builderTestCase{}
16 |
17 | gopath := filepath.Join(os.TempDir(), t.Name(), "_gpc")
18 | defer os.RemoveAll(gopath)
19 |
20 | builderTC.dir = filepath.Join(gopath, "src", "godogs")
21 | builderTC.files = map[string]string{
22 | "godogs.feature": builderFeatureFile,
23 | "godogs.go": builderMainCodeFile,
24 | "godogs_test.go": builderTestFile,
25 | "go.mod": builderModFile,
26 | }
27 |
28 | pkg := filepath.Join(builderTC.dir, "vendor", "github.com", "cucumber")
29 | err := os.MkdirAll(pkg, 0755)
30 | require.Nil(t, err)
31 |
32 | wd, err := os.Getwd()
33 | require.Nil(t, err)
34 |
35 | // symlink godog package
36 | err = os.Symlink(wd, filepath.Join(pkg, "godog"))
37 | require.Nil(t, err)
38 |
39 | builderTC.testCmdEnv = append(envVarsWithoutGopath(), "GOPATH="+gopath)
40 | builderTC.run(t)
41 | }
42 |
--------------------------------------------------------------------------------
/internal/formatters/formatter-tests/progress/some_scenarions_including_failing:
--------------------------------------------------------------------------------
1 | .F-P-U-A- 9
2 |
3 |
4 | --- Failed steps:
5 |
6 | Scenario: failing # formatter-tests/features/some_scenarios_including_failing.feature:3
7 | When failing step # formatter-tests/features/some_scenarios_including_failing.feature:5
8 | Error: step failed
9 |
10 |
11 | 4 scenarios (1 failed, 1 pending, 1 ambiguous, 1 undefined)
12 | 9 steps (1 passed, 1 failed, 1 pending, 1 ambiguous, 1 undefined, 4 skipped)
13 | 0s
14 |
15 | You can implement step definitions for undefined steps with these snippets:
16 |
17 | func undefined() error {
18 | return godog.ErrPending
19 | }
20 |
21 | func InitializeScenario(ctx *godog.ScenarioContext) {
22 | ctx.Step(`^undefined$`, undefined)
23 | }
24 |
25 |
--------------------------------------------------------------------------------
/example_subtests_test.go:
--------------------------------------------------------------------------------
1 | package godog_test
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/cucumber/godog"
7 | )
8 |
9 | func ExampleTestSuite_Run_subtests() {
10 | var t *testing.T // Comes from your test function, e.g. func TestFeatures(t *testing.T).
11 |
12 | suite := godog.TestSuite{
13 | ScenarioInitializer: func(s *godog.ScenarioContext) {
14 | // Add step definitions here.
15 | },
16 | Options: &godog.Options{
17 | Format: "pretty",
18 | Paths: []string{"features"},
19 | TestingT: t, // Testing instance that will run subtests.
20 | },
21 | }
22 |
23 | if suite.Run() != 0 {
24 | t.Fatal("non-zero status returned, failed to run feature tests")
25 | }
26 | }
27 |
28 | func TestFeatures(t *testing.T) {
29 | suite := godog.TestSuite{
30 | ScenarioInitializer: func(s *godog.ScenarioContext) {
31 | godog.InitializeScenario(s)
32 |
33 | // Add step definitions here.
34 | },
35 | Options: &godog.Options{
36 | Format: "pretty",
37 | Paths: []string{"features"},
38 | TestingT: t, // Testing instance that will run subtests.
39 | },
40 | }
41 |
42 | if suite.Run() != 0 {
43 | t.Fatal("non-zero status returned, failed to run feature tests")
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/internal/formatters/formatter-tests/junit/some_scenarios_including_failing:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/internal/formatters/formatter-tests/cucumber/single_scenario_with_passing_step:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "uri": "formatter-tests/features/single_scenario_with_passing_step.feature",
4 | "id": "single-passing-scenario",
5 | "keyword": "Feature",
6 | "name": "single passing scenario",
7 | "description": " describes\n a single scenario\n feature",
8 | "line": 1,
9 | "elements": [
10 | {
11 | "id": "single-passing-scenario;one-step-passing",
12 | "keyword": "Scenario",
13 | "name": "one step passing",
14 | "description": "",
15 | "line": 6,
16 | "type": "scenario",
17 | "steps": [
18 | {
19 | "keyword": "Given ",
20 | "name": "a passing step",
21 | "line": 7,
22 | "match": {
23 | "location": "fmt_output_test.go:101"
24 | },
25 | "result": {
26 | "status": "passed",
27 | "duration": 0
28 | }
29 | }
30 | ]
31 | }
32 | ]
33 | }
34 | ]
35 |
--------------------------------------------------------------------------------
/colors/no_colors.go:
--------------------------------------------------------------------------------
1 | package colors
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "io"
7 | )
8 |
9 | type noColors struct {
10 | out io.Writer
11 | lastbuf bytes.Buffer
12 | }
13 |
14 | // Uncolored will accept and io.Writer and return a
15 | // new io.Writer that won't include colors.
16 | func Uncolored(w io.Writer) io.Writer {
17 | return &noColors{out: w}
18 | }
19 |
20 | func (w *noColors) Write(data []byte) (n int, err error) {
21 | er := bytes.NewBuffer(data)
22 | loop:
23 | for {
24 | c1, _, err := er.ReadRune()
25 | if err != nil {
26 | break loop
27 | }
28 | if c1 != 0x1b {
29 | fmt.Fprint(w.out, string(c1))
30 | continue
31 | }
32 | c2, _, err := er.ReadRune()
33 | if err != nil {
34 | w.lastbuf.WriteRune(c1)
35 | break loop
36 | }
37 | if c2 != 0x5b {
38 | w.lastbuf.WriteRune(c1)
39 | w.lastbuf.WriteRune(c2)
40 | continue
41 | }
42 |
43 | var buf bytes.Buffer
44 | for {
45 | c, _, err := er.ReadRune()
46 | if err != nil {
47 | w.lastbuf.WriteRune(c1)
48 | w.lastbuf.WriteRune(c2)
49 | w.lastbuf.Write(buf.Bytes())
50 | break loop
51 | }
52 | if ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || c == '@' {
53 | break
54 | }
55 | buf.Write([]byte(string(c)))
56 | }
57 | }
58 | return len(data) - w.lastbuf.Len(), nil
59 | }
60 |
--------------------------------------------------------------------------------
/internal/formatters/formatter-tests/events/single_scenario_with_passing_step:
--------------------------------------------------------------------------------
1 | {"event":"TestRunStarted","version":"0.1.0","timestamp":-6795364578871,"suite":"events"}
2 | {"event":"TestSource","location":"formatter-tests/features/single_scenario_with_passing_step.feature:1","source":"Feature: single passing scenario\n describes\n a single scenario\n feature\n\n Scenario: one step passing\n Given a passing step\n"}
3 | {"event":"TestCaseStarted","location":"formatter-tests/features/single_scenario_with_passing_step.feature:6","timestamp":-6795364578871}
4 | {"event":"StepDefinitionFound","location":"formatter-tests/features/single_scenario_with_passing_step.feature:7","definition_id":"fmt_output_test.go:101 -\u003e github.com/cucumber/godog/internal/formatters_test.passingStepDef","arguments":[]}
5 | {"event":"TestStepStarted","location":"formatter-tests/features/single_scenario_with_passing_step.feature:7","timestamp":-6795364578871}
6 | {"event":"TestStepFinished","location":"formatter-tests/features/single_scenario_with_passing_step.feature:7","timestamp":-6795364578871,"status":"passed"}
7 | {"event":"TestCaseFinished","location":"formatter-tests/features/single_scenario_with_passing_step.feature:6","timestamp":-6795364578871,"status":"passed"}
8 | {"event":"TestRunFinished","status":"passed","timestamp":-6795364578871,"snippets":"","memory":""}
9 |
--------------------------------------------------------------------------------
/_examples/api/api.go:
--------------------------------------------------------------------------------
1 | // Example - demonstrates REST API server implementation tests.
2 | package main
3 |
4 | import (
5 | "encoding/json"
6 | "net/http"
7 |
8 | "github.com/cucumber/godog"
9 | )
10 |
11 | func getVersion(w http.ResponseWriter, r *http.Request) {
12 | if r.Method != http.MethodGet {
13 | fail(w, "Method not allowed", http.StatusMethodNotAllowed)
14 | return
15 | }
16 |
17 | data := struct {
18 | Version string `json:"version"`
19 | }{Version: godog.Version}
20 |
21 | ok(w, data)
22 | }
23 |
24 | // fail writes a json response with error msg and status header
25 | func fail(w http.ResponseWriter, msg string, status int) {
26 | w.WriteHeader(status)
27 |
28 | data := struct {
29 | Error string `json:"error"`
30 | }{Error: msg}
31 | resp, _ := json.Marshal(data)
32 |
33 | w.Header().Set("Content-Type", "application/json")
34 | w.Write(resp)
35 | }
36 |
37 | // ok writes data to response with 200 status
38 | func ok(w http.ResponseWriter, data interface{}) {
39 | resp, err := json.Marshal(data)
40 | if err != nil {
41 | fail(w, "Oops something evil has happened", http.StatusInternalServerError)
42 | return
43 | }
44 |
45 | w.Header().Set("Content-Type", "application/json")
46 | w.Write(resp)
47 | }
48 |
49 | func main() {
50 | http.HandleFunc("/version", getVersion)
51 | http.ListenAndServe(":8080", nil)
52 | }
53 |
--------------------------------------------------------------------------------
/internal/formatters/formatter-tests/progress/scenario_outline:
--------------------------------------------------------------------------------
1 | .....F..F.....F 15
2 |
3 |
4 | --- Failed steps:
5 |
6 | Scenario Outline: outline # formatter-tests/features/scenario_outline.feature:5
7 | Then odd 2 and even 0 number # formatter-tests/features/scenario_outline.feature:8
8 | Error: 2 is not odd
9 |
10 | Scenario Outline: outline # formatter-tests/features/scenario_outline.feature:5
11 | Then odd 3 and even 11 number # formatter-tests/features/scenario_outline.feature:8
12 | Error: 11 is not even
13 |
14 | Scenario Outline: outline # formatter-tests/features/scenario_outline.feature:5
15 | Then odd 3 and even 9 number # formatter-tests/features/scenario_outline.feature:8
16 | Error: 9 is not even
17 |
18 |
19 | 5 scenarios (2 passed, 3 failed)
20 | 15 steps (12 passed, 3 failed)
21 | 0s
22 |
--------------------------------------------------------------------------------
/_examples/db/features/users.feature:
--------------------------------------------------------------------------------
1 | Feature: users
2 | In order to use users api
3 | As an API user
4 | I need to be able to manage users
5 |
6 | Scenario: should get empty users
7 | When I send "GET" request to "/users"
8 | Then the response code should be 200
9 | And the response should match json:
10 | """
11 | {
12 | "users": []
13 | }
14 | """
15 |
16 | Scenario: should get users
17 | Given there are users:
18 | | username | email |
19 | | john | john.doe@mail.com |
20 | | jane | jane.doe@mail.com |
21 | When I send "GET" request to "/users"
22 | Then the response code should be 200
23 | And the response should match json:
24 | """
25 | {
26 | "users": [
27 | {
28 | "username": "john"
29 | },
30 | {
31 | "username": "jane"
32 | }
33 | ]
34 | }
35 | """
36 |
37 | Scenario: should get users when there is only one
38 | Given there are users:
39 | | username | email |
40 | | gopher | gopher@mail.com |
41 | When I send "GET" request to "/users"
42 | Then the response code should be 200
43 | And the response should match json:
44 | """
45 | {
46 | "users": [
47 | {
48 | "username": "gopher"
49 | }
50 | ]
51 | }
52 | """
53 |
--------------------------------------------------------------------------------
/colors/writer.go:
--------------------------------------------------------------------------------
1 | // Copyright 2014 shiena Authors. All rights reserved.
2 | // Use of this source code is governed by a MIT-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package colors
6 |
7 | import "io"
8 |
9 | type outputMode int
10 |
11 | // DiscardNonColorEscSeq supports the divided color escape sequence.
12 | // But non-color escape sequence is not output.
13 | // Please use the OutputNonColorEscSeq If you want to output a non-color
14 | // escape sequences such as ncurses. However, it does not support the divided
15 | // color escape sequence.
16 | const (
17 | _ outputMode = iota
18 | discardNonColorEscSeq
19 | outputNonColorEscSeq // unused
20 | )
21 |
22 | // Colored creates and initializes a new ansiColorWriter
23 | // using io.Writer w as its initial contents.
24 | // In the console of Windows, which change the foreground and background
25 | // colors of the text by the escape sequence.
26 | // In the console of other systems, which writes to w all text.
27 | func Colored(w io.Writer) io.Writer {
28 | return createModeAnsiColorWriter(w, discardNonColorEscSeq)
29 | }
30 |
31 | // NewModeAnsiColorWriter create and initializes a new ansiColorWriter
32 | // by specifying the outputMode.
33 | func createModeAnsiColorWriter(w io.Writer, mode outputMode) io.Writer {
34 | if _, ok := w.(*ansiColorWriter); !ok {
35 | return &ansiColorWriter{
36 | w: w,
37 | mode: mode,
38 | }
39 | }
40 | return w
41 | }
42 |
--------------------------------------------------------------------------------
/internal/formatters/formatter-tests/junit,pretty/scenario_with_background:
--------------------------------------------------------------------------------
1 | Feature: single scenario with background
2 |
3 | Background: named
4 | Given passing step # fmt_output_test.go:101 -> github.com/cucumber/godog/internal/formatters_test.passingStepDef
5 | And passing step # fmt_output_test.go:101 -> github.com/cucumber/godog/internal/formatters_test.passingStepDef
6 |
7 | Scenario: scenario # formatter-tests/features/scenario_with_background.feature:7
8 | When passing step # fmt_output_test.go:101 -> github.com/cucumber/godog/internal/formatters_test.passingStepDef
9 | Then passing step # fmt_output_test.go:101 -> github.com/cucumber/godog/internal/formatters_test.passingStepDef
10 |
11 |
12 |
13 |
14 |
15 |
16 | 1 scenarios (1 passed)
17 | 4 steps (4 passed)
18 | 0s
19 |
--------------------------------------------------------------------------------
/internal/tags/tag_filter.go:
--------------------------------------------------------------------------------
1 | package tags
2 |
3 | import (
4 | "strings"
5 |
6 | messages "github.com/cucumber/messages/go/v21"
7 | )
8 |
9 | // ApplyTagFilter will apply a filter string on the
10 | // array of pickles and returned the filtered list.
11 | func ApplyTagFilter(filter string, pickles []*messages.Pickle) []*messages.Pickle {
12 | if filter == "" {
13 | return pickles
14 | }
15 |
16 | var result = []*messages.Pickle{}
17 |
18 | for _, pickle := range pickles {
19 | if match(filter, pickle.Tags) {
20 | result = append(result, pickle)
21 | }
22 | }
23 |
24 | return result
25 | }
26 |
27 | // Based on http://behat.readthedocs.org/en/v2.5/guides/6.cli.html#gherkin-filters
28 | func match(filter string, tags []*messages.PickleTag) (ok bool) {
29 | ok = true
30 |
31 | for _, andTags := range strings.Split(filter, "&&") {
32 | var okComma bool
33 |
34 | for _, tag := range strings.Split(andTags, ",") {
35 | tag = strings.TrimSpace(tag)
36 | tag = strings.Replace(tag, "@", "", -1)
37 |
38 | okComma = contains(tags, tag) || okComma
39 |
40 | if tag[0] == '~' {
41 | tag = tag[1:]
42 | okComma = !contains(tags, tag) || okComma
43 | }
44 | }
45 |
46 | ok = ok && okComma
47 | }
48 |
49 | return
50 | }
51 |
52 | func contains(tags []*messages.PickleTag, tag string) bool {
53 | for _, t := range tags {
54 | tagName := strings.Replace(t.Name, "@", "", -1)
55 |
56 | if tagName == tag {
57 | return true
58 | }
59 | }
60 |
61 | return false
62 | }
63 |
--------------------------------------------------------------------------------
/.github/workflows/gorelease.yml:
--------------------------------------------------------------------------------
1 | # Gorelease comments public API changes to pull request.
2 | name: gorelease
3 | on:
4 | pull_request:
5 |
6 | # Cancel the workflow in progress in newer build is about to start.
7 | concurrency:
8 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
9 | cancel-in-progress: true
10 |
11 | env:
12 | GO_VERSION: stable
13 | jobs:
14 | gorelease:
15 | runs-on: ubuntu-latest
16 | steps:
17 | - name: Install Go stable
18 | uses: actions/setup-go@v6
19 | with:
20 | go-version: ${{ env.GO_VERSION }}
21 | - name: Checkout code
22 | uses: actions/checkout@v5
23 | - name: Gorelease cache
24 | uses: actions/cache@v4
25 | with:
26 | path: |
27 | ~/go/bin/gorelease
28 | key: ${{ runner.os }}-gorelease-generic
29 | - name: Gorelease
30 | id: gorelease
31 | run: |
32 | test -e ~/go/bin/gorelease || go install golang.org/x/exp/cmd/gorelease@latest
33 | OUTPUT=$(gorelease 2>&1 || exit 0)
34 | echo "${OUTPUT}"
35 | OUTPUT="${OUTPUT//$'\n'/%0A}"
36 | echo "report=$OUTPUT" >> $GITHUB_OUTPUT
37 | - name: Comment Report
38 | continue-on-error: true
39 | uses: marocchino/sticky-pull-request-comment@v2
40 | with:
41 | header: gorelease
42 | message: |
43 | ### Go API Changes
44 |
45 |
46 | ${{ steps.gorelease.outputs.report }}
47 |
48 |
--------------------------------------------------------------------------------
/internal/formatters/formatter-tests/junit,pretty/with_few_empty_scenarios:
--------------------------------------------------------------------------------
1 | Feature: few empty scenarios
2 |
3 | Scenario: one # formatter-tests/features/with_few_empty_scenarios.feature:3
4 |
5 | Scenario Outline: two # formatter-tests/features/with_few_empty_scenarios.feature:5
6 |
7 | Examples: first group
8 | | one | two |
9 | | 1 | 2 |
10 | | 4 | 7 |
11 |
12 | Examples: second group
13 | | one | two |
14 | | 5 | 9 |
15 |
16 | Scenario: three # formatter-tests/features/with_few_empty_scenarios.feature:16
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | 5 scenarios (5 undefined)
28 | No steps
29 | 0s
30 |
--------------------------------------------------------------------------------
/internal/tags/tag_filter_test.go:
--------------------------------------------------------------------------------
1 | package tags_test
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 |
8 | "github.com/cucumber/godog/internal/tags"
9 | messages "github.com/cucumber/messages/go/v21"
10 | )
11 |
12 | type tag = messages.PickleTag
13 | type pickle = messages.Pickle
14 |
15 | type testcase struct {
16 | filter string
17 | expected []*pickle
18 | }
19 |
20 | var testdata = []*pickle{p1, p2, p3}
21 | var p1 = &pickle{Id: "one", Tags: []*tag{{Name: "@one"}, {Name: "@wip"}}}
22 | var p2 = &pickle{Id: "two", Tags: []*tag{{Name: "@two"}, {Name: "@wip"}}}
23 | var p3 = &pickle{Id: "three", Tags: []*tag{{Name: "@hree"}, {Name: "@wip"}}}
24 |
25 | var testcases = []testcase{
26 | {filter: "", expected: testdata},
27 |
28 | {filter: "@one", expected: []*pickle{p1}},
29 | {filter: "~@one", expected: []*pickle{p2, p3}},
30 | {filter: "one", expected: []*pickle{p1}},
31 | {filter: " one ", expected: []*pickle{p1}},
32 |
33 | {filter: "@one,@two", expected: []*pickle{p1, p2}},
34 | {filter: "@one,~@two", expected: []*pickle{p1, p3}},
35 | {filter: " @one , @two ", expected: []*pickle{p1, p2}},
36 |
37 | {filter: "@one&&@two", expected: []*pickle{}},
38 | {filter: "@one&&~@two", expected: []*pickle{p1}},
39 | {filter: "@one&&@wip", expected: []*pickle{p1}},
40 |
41 | {filter: "@one&&@two,@wip", expected: []*pickle{p1}},
42 | }
43 |
44 | func Test_ApplyTagFilter(t *testing.T) {
45 | for _, tc := range testcases {
46 | t.Run("", func(t *testing.T) {
47 | actual := tags.ApplyTagFilter(tc.filter, testdata)
48 | assert.Equal(t, tc.expected, actual)
49 | })
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/_examples/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/cucumber/godog/_examples
2 |
3 | go 1.21
4 |
5 | replace github.com/cucumber/godog => ../
6 |
7 | require (
8 | github.com/DATA-DOG/go-txdb v0.2.1
9 | github.com/cucumber/godog v0.15.1
10 | github.com/go-sql-driver/mysql v1.7.1
11 | github.com/spf13/pflag v1.0.10
12 | github.com/stretchr/testify v1.11.1
13 | )
14 |
15 | require (
16 | github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
17 | github.com/creack/pty v1.1.9 // indirect
18 | github.com/cucumber/gherkin/go/v26 v26.2.0 // indirect
19 | github.com/cucumber/messages/go/v21 v21.0.1 // indirect
20 | github.com/cucumber/messages/go/v22 v22.0.0 // indirect
21 | github.com/davecgh/go-spew v1.1.1 // indirect
22 | github.com/gofrs/uuid v4.3.1+incompatible // indirect
23 | github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
24 | github.com/hashicorp/go-memdb v1.3.4 // indirect
25 | github.com/hashicorp/go-uuid v1.0.2 // indirect
26 | github.com/hashicorp/golang-lru v0.5.4 // indirect
27 | github.com/inconshreveable/mousetrap v1.1.0 // indirect
28 | github.com/kr/pretty v0.3.0 // indirect
29 | github.com/kr/pty v1.1.1 // indirect
30 | github.com/kr/text v0.2.0 // indirect
31 | github.com/lib/pq v1.10.3 // indirect
32 | github.com/pmezard/go-difflib v1.0.0 // indirect
33 | github.com/rogpeppe/go-internal v1.6.1 // indirect
34 | github.com/russross/blackfriday/v2 v2.1.0 // indirect
35 | github.com/spf13/cobra v1.7.0 // indirect
36 | github.com/stretchr/objx v0.5.2 // indirect
37 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
38 | gopkg.in/errgo.v2 v2.1.0 // indirect
39 | gopkg.in/yaml.v3 v3.0.1 // indirect
40 | )
41 |
--------------------------------------------------------------------------------
/godog.go:
--------------------------------------------------------------------------------
1 | /*
2 | Package godog is the official Cucumber BDD framework for Golang, it merges specification
3 | and test documentation into one cohesive whole.
4 |
5 | Godog does not intervene with the standard "go test" command and it's behavior.
6 | You can leverage both frameworks to functionally test your application while
7 | maintaining all test related source code in *_test.go files.
8 |
9 | Godog acts similar compared to go test command. It uses go
10 | compiler and linker tool in order to produce test executable. Godog
11 | contexts needs to be exported same as Test functions for go test.
12 |
13 | For example, imagine you're about to create the famous UNIX ls command.
14 | Before you begin, you describe how the feature should work, see the example below..
15 |
16 | Example:
17 |
18 | Feature: ls
19 | In order to see the directory structure
20 | As a UNIX user
21 | I need to be able to list the current directory's contents
22 |
23 | Scenario:
24 | Given I am in a directory "test"
25 | And I have a file named "foo"
26 | And I have a file named "bar"
27 | When I run ls
28 | Then I should get output:
29 | """
30 | bar
31 | foo
32 | """
33 |
34 | Now, wouldn't it be cool if something could read this sentence and use it to actually
35 | run a test against the ls command? Hey, that's exactly what this package does!
36 | As you'll see, Godog is easy to learn, quick to use, and will put the fun back into tests.
37 |
38 | Godog was inspired by Behat and Cucumber the above description is taken from it's documentation.
39 | */
40 | package godog
41 |
42 | // Version of package - based on Semantic Versioning 2.0.0 http://semver.org/
43 | var Version = "v0.0.0-dev"
44 |
--------------------------------------------------------------------------------
/cmd/godog/internal/cmd_build.go:
--------------------------------------------------------------------------------
1 | package internal
2 |
3 | import (
4 | "fmt"
5 | "go/build"
6 | "path/filepath"
7 |
8 | "github.com/cucumber/godog/colors"
9 | "github.com/cucumber/godog/internal/builder"
10 |
11 | "github.com/spf13/cobra"
12 | )
13 |
14 | var buildOutput string
15 | var buildOutputDefault = "godog.test"
16 |
17 | // CreateBuildCmd creates the build subcommand.
18 | func CreateBuildCmd() cobra.Command {
19 | if build.Default.GOOS == "windows" {
20 | buildOutputDefault += ".exe"
21 | }
22 |
23 | buildCmd := cobra.Command{
24 | Use: "build",
25 | Short: "Compiles a test runner",
26 | Long: `Compiles a test runner. Command should be run from the directory of tested
27 | package and contain buildable go source.
28 |
29 | The test runner can be executed with the same flags as when using godog run.`,
30 | Example: ` godog build
31 | godog build -o ` + buildOutputDefault,
32 | RunE: buildCmdRunFunc,
33 | }
34 |
35 | buildCmd.Flags().StringVarP(&buildOutput, "output", "o", buildOutputDefault, `compiles the test runner to the named file
36 | `)
37 |
38 | return buildCmd
39 | }
40 |
41 | func buildCmdRunFunc(cmd *cobra.Command, args []string) error {
42 | fmt.Println(colors.Yellow("Use of godog CLI is deprecated, please use *testing.T instead."))
43 | fmt.Println(colors.Yellow("See https://github.com/cucumber/godog/discussions/478 for details."))
44 |
45 | bin, err := filepath.Abs(buildOutput)
46 | if err != nil {
47 | return fmt.Errorf("could not locate absolute path for: %q. reason: %v", buildOutput, err)
48 | }
49 |
50 | if err = builder.Build(bin); err != nil {
51 | return fmt.Errorf("could not build binary at: %q. reason: %v", buildOutput, err)
52 | }
53 |
54 | return nil
55 | }
56 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Welcome 💖
2 |
3 | Before anything else, thank you for taking some of your precious time to help this project move forward. ❤️
4 |
5 | If you're new to open source and feeling a bit nervous 😳, we understand! We recommend watching [this excellent guide](https://egghead.io/talks/git-how-to-make-your-first-open-source-contribution)
6 | to give you a grounding in some of the basic concepts. You could also watch [this talk](https://www.youtube.com/watch?v=tuSk6dMoTIs) from our very own wonderful [Marit van Dijk](https://github.com/mlvandijk) on her experiences contributing to Cucumber.
7 |
8 | We want you to feel safe to make mistakes, and ask questions. If anything in this guide or anywhere else in the codebase doesn't make sense to you, please let us know! It's through your feedback that we can make this codebase more welcoming, so we'll be glad to hear thoughts.
9 |
10 | You can chat with us in the `#committers` channel in our [community Discord](https://cucumber.io/docs/community/get-in-touch/#discord), or feel free to [raise an issue] if you're experiencing any friction trying make your contribution.
11 |
12 | ## Setup
13 |
14 | To get your development environment set up, you'll need to [install Go]. We're currently using version 1.17 for development.
15 |
16 | Once that's done, try running the tests:
17 |
18 | make test
19 |
20 | If everything passes, you're ready to hack!
21 |
22 | [install go]: https://golang.org/doc/install
23 | [community Discord]: https://cucumber.io/community#discord
24 | [raise an issue]: https://github.com/cucumber/godog/issues/new/choose
25 |
26 | ## Changing dependencies
27 |
28 | If dependencies have changed, you will also need to update the _examples module. `go mod tidy` should be sufficient.
--------------------------------------------------------------------------------
/formatters/fmt_test.go:
--------------------------------------------------------------------------------
1 | package formatters_test
2 |
3 | import (
4 | "io"
5 | "testing"
6 |
7 | "github.com/stretchr/testify/assert"
8 | "github.com/stretchr/testify/require"
9 |
10 | "github.com/cucumber/godog"
11 | )
12 |
13 | func Test_FindFmt(t *testing.T) {
14 | cases := map[string]bool{
15 | "cucumber": true,
16 | "events": true,
17 | "junit": true,
18 | "pretty": true,
19 | "progress": true,
20 | "unknown": false,
21 | "undef": false,
22 | }
23 |
24 | for name, expected := range cases {
25 | t.Run(
26 | name,
27 | func(t *testing.T) {
28 | actual := godog.FindFmt(name)
29 |
30 | if expected {
31 | assert.NotNilf(t, actual, "expected %s formatter should be available", name)
32 | } else {
33 | assert.Nilf(t, actual, "expected %s formatter should be available", name)
34 | }
35 | },
36 | )
37 | }
38 | }
39 |
40 | func Test_AvailableFormatters(t *testing.T) {
41 | expected := map[string]string{
42 | "cucumber": "Produces cucumber JSON format output.",
43 | "events": "Produces JSON event stream, based on spec: 0.1.0.",
44 | "junit": "Prints junit compatible xml to stdout",
45 | "pretty": "Prints every feature with runtime statuses.",
46 | "progress": "Prints a character per step.",
47 | }
48 |
49 | actual := godog.AvailableFormatters()
50 | assert.Equal(t, expected, actual)
51 | }
52 |
53 | func Test_Format(t *testing.T) {
54 | actual := godog.FindFmt("Test_Format")
55 | require.Nil(t, actual)
56 |
57 | godog.Format("Test_Format", "...", testFormatterFunc)
58 | actual = godog.FindFmt("Test_Format")
59 |
60 | assert.NotNil(t, actual)
61 | }
62 |
63 | func testFormatterFunc(suiteName string, out io.Writer) godog.Formatter {
64 | return nil
65 | }
66 |
--------------------------------------------------------------------------------
/colors/colors.go:
--------------------------------------------------------------------------------
1 | package colors
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 | )
7 |
8 | const ansiEscape = "\x1b"
9 |
10 | // a color code type
11 | type color int
12 |
13 | // some ansi colors
14 | const (
15 | black color = iota + 30
16 | red
17 | green
18 | yellow
19 | blue // unused
20 | magenta // unused
21 | cyan
22 | white
23 | )
24 |
25 | func colorize(s interface{}, c color) string {
26 | return fmt.Sprintf("%s[%dm%v%s[0m", ansiEscape, c, s, ansiEscape)
27 | }
28 |
29 | // ColorFunc is a helper type to create colorized strings.
30 | type ColorFunc func(interface{}) string
31 |
32 | // Bold will accept a ColorFunc and return a new ColorFunc
33 | // that will make the string bold.
34 | func Bold(fn ColorFunc) ColorFunc {
35 | return ColorFunc(func(input interface{}) string {
36 | return strings.Replace(fn(input), ansiEscape+"[", ansiEscape+"[1;", 1)
37 | })
38 | }
39 |
40 | // Green will accept an interface and return a colorized green string.
41 | func Green(s interface{}) string {
42 | return colorize(s, green)
43 | }
44 |
45 | // Red will accept an interface and return a colorized red string.
46 | func Red(s interface{}) string {
47 | return colorize(s, red)
48 | }
49 |
50 | // Cyan will accept an interface and return a colorized cyan string.
51 | func Cyan(s interface{}) string {
52 | return colorize(s, cyan)
53 | }
54 |
55 | // Black will accept an interface and return a colorized black string.
56 | func Black(s interface{}) string {
57 | return colorize(s, black)
58 | }
59 |
60 | // Yellow will accept an interface and return a colorized yellow string.
61 | func Yellow(s interface{}) string {
62 | return colorize(s, yellow)
63 | }
64 |
65 | // White will accept an interface and return a colorized white string.
66 | func White(s interface{}) string {
67 | return colorize(s, white)
68 | }
69 |
--------------------------------------------------------------------------------
/features/load.feature:
--------------------------------------------------------------------------------
1 | Feature: load features
2 | In order to run features
3 | As a test suite
4 | I need to be able to load features
5 |
6 | Scenario: load features within path
7 | Given a feature path "features"
8 | When I parse features
9 | Then I should have 14 feature files:
10 | """
11 | features/background.feature
12 | features/events.feature
13 | features/formatter/cucumber.feature
14 | features/formatter/events.feature
15 | features/formatter/junit.feature
16 | features/formatter/pretty.feature
17 | features/lang.feature
18 | features/load.feature
19 | features/multistep.feature
20 | features/outline.feature
21 | features/run.feature
22 | features/snippets.feature
23 | features/tags.feature
24 | features/testingt.feature
25 | """
26 |
27 | Scenario: load a specific feature file
28 | Given a feature path "features/load.feature"
29 | When I parse features
30 | Then I should have 1 feature file:
31 | """
32 | features/load.feature
33 | """
34 |
35 | Scenario Outline: loaded feature should have a number of scenarios
36 | Given a feature path ""
37 | When I parse features
38 | Then I should have scenario registered
39 |
40 | Examples:
41 | | feature | number |
42 | | features/load.feature:3 | 0 |
43 | | features/load.feature:6 | 1 |
44 | | features/load.feature | 6 |
45 |
46 | Scenario: load a number of feature files
47 | Given a feature path "features/load.feature"
48 | And a feature path "features/events.feature"
49 | When I parse features
50 | Then I should have 2 feature files:
51 | """
52 | features/events.feature
53 | features/load.feature
54 | """
55 |
--------------------------------------------------------------------------------
/internal/formatters/fmt_flushwrap_test.go:
--------------------------------------------------------------------------------
1 | package formatters
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | )
8 |
9 | var flushMock = DummyFormatter{}
10 |
11 | func TestFlushWrapOnFormatter(t *testing.T) {
12 | flushMock.tt = t
13 |
14 | fmt := WrapOnFlush(&flushMock)
15 |
16 | fmt.Feature(document, str, byt)
17 | fmt.TestRunStarted()
18 | fmt.Pickle(pickle)
19 | fmt.Defined(pickle, step, definition)
20 | fmt.Passed(pickle, step, definition)
21 | fmt.Skipped(pickle, step, definition)
22 | fmt.Undefined(pickle, step, definition)
23 | fmt.Failed(pickle, step, definition, err)
24 | fmt.Pending(pickle, step, definition)
25 | fmt.Ambiguous(pickle, step, definition, err)
26 | fmt.Summary()
27 |
28 | assert.Equal(t, 0, flushMock.CountFeature)
29 | assert.Equal(t, 0, flushMock.CountTestRunStarted)
30 | assert.Equal(t, 0, flushMock.CountPickle)
31 | assert.Equal(t, 0, flushMock.CountDefined)
32 | assert.Equal(t, 0, flushMock.CountPassed)
33 | assert.Equal(t, 0, flushMock.CountSkipped)
34 | assert.Equal(t, 0, flushMock.CountUndefined)
35 | assert.Equal(t, 0, flushMock.CountFailed)
36 | assert.Equal(t, 0, flushMock.CountPending)
37 | assert.Equal(t, 0, flushMock.CountAmbiguous)
38 | assert.Equal(t, 0, flushMock.CountSummary)
39 |
40 | fmt.Flush()
41 |
42 | assert.Equal(t, 1, flushMock.CountFeature)
43 | assert.Equal(t, 1, flushMock.CountTestRunStarted)
44 | assert.Equal(t, 1, flushMock.CountPickle)
45 | assert.Equal(t, 1, flushMock.CountDefined)
46 | assert.Equal(t, 1, flushMock.CountPassed)
47 | assert.Equal(t, 1, flushMock.CountSkipped)
48 | assert.Equal(t, 1, flushMock.CountUndefined)
49 | assert.Equal(t, 1, flushMock.CountFailed)
50 | assert.Equal(t, 1, flushMock.CountPending)
51 | assert.Equal(t, 1, flushMock.CountAmbiguous)
52 | assert.Equal(t, 1, flushMock.CountSummary)
53 | }
54 |
--------------------------------------------------------------------------------
/internal/models/results_test.go:
--------------------------------------------------------------------------------
1 | package models_test
2 |
3 | import (
4 | "fmt"
5 | "testing"
6 |
7 | "github.com/stretchr/testify/assert"
8 |
9 | "github.com/cucumber/godog/colors"
10 | "github.com/cucumber/godog/internal/models"
11 | )
12 |
13 | type stepResultStatusTestCase struct {
14 | st models.StepResultStatus
15 | str string
16 | clr colors.ColorFunc
17 | }
18 |
19 | var stepResultStatusTestCases = []stepResultStatusTestCase{
20 | {st: models.Passed, str: "passed", clr: colors.Green},
21 | {st: models.Failed, str: "failed", clr: colors.Red},
22 | {st: models.Skipped, str: "skipped", clr: colors.Cyan},
23 | {st: models.Undefined, str: "undefined", clr: colors.Yellow},
24 | {st: models.Pending, str: "pending", clr: colors.Yellow},
25 | {st: models.Ambiguous, str: "ambiguous", clr: colors.Yellow},
26 | {st: -1, str: "unknown", clr: colors.Yellow},
27 | }
28 |
29 | func Test_StepResultStatus(t *testing.T) {
30 | for _, tc := range stepResultStatusTestCases {
31 | t.Run(tc.str, func(t *testing.T) {
32 | assert.Equal(t, tc.str, tc.st.String())
33 | assert.Equal(t, tc.clr(tc.str), tc.st.Color()(tc.str))
34 | })
35 | }
36 | }
37 |
38 | func Test_NewStepResuklt(t *testing.T) {
39 | status := models.StepResultStatus(123)
40 | pickleID := "pickleId"
41 | pickleStepID := "pickleStepID"
42 | match := &models.StepDefinition{}
43 | attachments := make([]models.PickleAttachment, 0)
44 | err := fmt.Errorf("intentional")
45 |
46 | results := models.NewStepResult(status, pickleID, pickleStepID, match, attachments, err)
47 |
48 | assert.Equal(t, status, results.Status)
49 | assert.Equal(t, pickleID, results.PickleID)
50 | assert.Equal(t, pickleStepID, results.PickleStepID)
51 | assert.Equal(t, match, results.Def)
52 | assert.Equal(t, attachments, results.Attachments)
53 | assert.Equal(t, err, results.Err)
54 | }
55 |
--------------------------------------------------------------------------------
/internal/formatters/formatter-tests/cucumber/with_few_empty_scenarios:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "uri": "formatter-tests/features/with_few_empty_scenarios.feature",
4 | "id": "few-empty-scenarios",
5 | "keyword": "Feature",
6 | "name": "few empty scenarios",
7 | "description": "",
8 | "line": 1,
9 | "elements": [
10 | {
11 | "id": "few-empty-scenarios;one",
12 | "keyword": "Scenario",
13 | "name": "one",
14 | "description": "",
15 | "line": 3,
16 | "type": "scenario"
17 | },
18 | {
19 | "id": "few-empty-scenarios;two;first-group;2",
20 | "keyword": "Scenario Outline",
21 | "name": "two",
22 | "description": "",
23 | "line": 9,
24 | "type": "scenario"
25 | },
26 | {
27 | "id": "few-empty-scenarios;two;first-group;3",
28 | "keyword": "Scenario Outline",
29 | "name": "two",
30 | "description": "",
31 | "line": 10,
32 | "type": "scenario"
33 | },
34 | {
35 | "id": "few-empty-scenarios;two;second-group;2",
36 | "keyword": "Scenario Outline",
37 | "name": "two",
38 | "description": "",
39 | "line": 14,
40 | "type": "scenario"
41 | },
42 | {
43 | "id": "few-empty-scenarios;three",
44 | "keyword": "Scenario",
45 | "name": "three",
46 | "description": "",
47 | "line": 16,
48 | "type": "scenario"
49 | }
50 | ]
51 | }
52 | ]
53 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: test
2 | on:
3 | push:
4 | branches:
5 | - main
6 | pull_request:
7 | jobs:
8 | test:
9 | strategy:
10 | matrix:
11 | go-version: [ 1.16.x, 1.17.x, oldstable, stable ] # Lowest supported and current stable versions.
12 | runs-on: ubuntu-latest
13 | steps:
14 | - name: Install Go
15 | uses: actions/setup-go@v6
16 | with:
17 | go-version: ${{ matrix.go-version }}
18 | - name: Checkout code
19 | uses: actions/checkout@v5
20 | - name: Go cache
21 | uses: actions/cache@v4
22 | with:
23 | path: |
24 | ~/go/pkg/mod
25 | ~/.cache/go-build
26 | key: ${{ runner.os }}-go-cache-${{ hashFiles('**/go.sum') }}
27 | restore-keys: |
28 | ${{ runner.os }}-go-cache
29 | - name: Run gofmt
30 | run: gofmt -d -e . 2>&1 | tee outfile && test -z "$(cat outfile)" && rm outfile
31 | - name: Run staticcheck
32 | if: matrix.go-version == 'stable'
33 | uses: dominikh/staticcheck-action@v1.4.0
34 | with:
35 | version: "latest"
36 | install-go: false
37 | cache-key: ${{ matrix.go }}
38 |
39 | - name: Run go vet
40 | run: |
41 | go vet ./...
42 | cd _examples && go vet ./... && cd ..
43 | - name: Run go test
44 | run: |
45 | go test -v -race -coverprofile=coverage.txt -covermode=atomic ./...
46 | cd _examples && go test -v -race ./... && cd ..
47 | - name: Run godog
48 | run: |
49 | go install ./cmd/godog
50 | godog -f progress --strict
51 | - name: Report on code coverage
52 | if: matrix.go-version == 'stable'
53 | uses: codecov/codecov-action@v4
54 | with:
55 | file: ./coverage.txt
56 |
--------------------------------------------------------------------------------
/fmt_test.go:
--------------------------------------------------------------------------------
1 | package godog_test
2 |
3 | import (
4 | "io"
5 | "testing"
6 |
7 | "github.com/stretchr/testify/assert"
8 | "github.com/stretchr/testify/require"
9 |
10 | "github.com/cucumber/godog"
11 | )
12 |
13 | func Test_FindFmt(t *testing.T) {
14 | cases := map[string]bool{
15 | "cucumber": true,
16 | "custom": true, // is available for test purposes only
17 | "events": true,
18 | "junit": true,
19 | "pretty": true,
20 | "progress": true,
21 | "unknown": false,
22 | "undef": false,
23 | }
24 |
25 | for name, expected := range cases {
26 | t.Run(
27 | name,
28 | func(t *testing.T) {
29 | actual := godog.FindFmt(name)
30 |
31 | if expected {
32 | assert.NotNilf(t, actual, "expected %s formatter should be available", name)
33 | } else {
34 | assert.Nilf(t, actual, "expected %s formatter should be available", name)
35 | }
36 | },
37 | )
38 | }
39 | }
40 |
41 | func Test_AvailableFormatters(t *testing.T) {
42 | expected := map[string]string{
43 | "cucumber": "Produces cucumber JSON format output.",
44 | "custom": "custom format description", // is available for test purposes only
45 | "events": "Produces JSON event stream, based on spec: 0.1.0.",
46 | "junit": "Prints junit compatible xml to stdout",
47 | "pretty": "Prints every feature with runtime statuses.",
48 | "progress": "Prints a character per step.",
49 | }
50 |
51 | actual := godog.AvailableFormatters()
52 | assert.Equal(t, expected, actual)
53 | }
54 |
55 | func Test_Format(t *testing.T) {
56 | actual := godog.FindFmt("Test_Format")
57 | require.Nil(t, actual)
58 |
59 | godog.Format("Test_Format", "...", testFormatterFunc)
60 | actual = godog.FindFmt("Test_Format")
61 |
62 | assert.NotNil(t, actual)
63 | }
64 |
65 | func testFormatterFunc(suiteName string, out io.Writer) godog.Formatter {
66 | return nil
67 | }
68 |
--------------------------------------------------------------------------------
/.github/workflows/release-assets.yml:
--------------------------------------------------------------------------------
1 | # This script uploads application binaries as GitHub release assets.
2 | name: release-assets
3 | on:
4 | release:
5 | types:
6 | - created
7 | env:
8 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
9 |
10 | jobs:
11 | build:
12 | name: Upload Release Assets
13 | runs-on: ubuntu-latest
14 | steps:
15 | - name: Install Go
16 | uses: actions/setup-go@v6
17 | with:
18 | go-version: stable
19 | - name: Checkout code
20 | uses: actions/checkout@v5
21 | - name: Build artifacts
22 | run: |
23 | make artifacts
24 | - name: Upload linux amd64 binary
25 | uses: actions/upload-release-asset@v1
26 | with:
27 | upload_url: ${{ github.event.release.upload_url }}
28 | asset_path: ./_artifacts/godog-${{ github.event.release.tag_name }}-linux-amd64.tar.gz
29 | asset_name: godog-${{ github.event.release.tag_name }}-linux-amd64.tar.gz
30 | asset_content_type: application/tar+gzip
31 | - name: Upload linux arm64 binary
32 | uses: actions/upload-release-asset@v1
33 | with:
34 | upload_url: ${{ github.event.release.upload_url }}
35 | asset_path: ./_artifacts/godog-${{ github.event.release.tag_name }}-linux-arm64.tar.gz
36 | asset_name: godog-${{ github.event.release.tag_name }}-linux-arm64.tar.gz
37 | asset_content_type: application/tar+gzip
38 | - name: Upload darwin amd64 binary
39 | uses: actions/upload-release-asset@v1
40 | with:
41 | upload_url: ${{ github.event.release.upload_url }}
42 | asset_path: ./_artifacts/godog-${{ github.event.release.tag_name }}-darwin-amd64.tar.gz
43 | asset_name: godog-${{ github.event.release.tag_name }}-darwin-amd64.tar.gz
44 | asset_content_type: application/tar+gzip
45 |
--------------------------------------------------------------------------------
/cmd/godog/internal/cmd_root.go:
--------------------------------------------------------------------------------
1 | package internal
2 |
3 | import (
4 | "github.com/spf13/cobra"
5 | "github.com/spf13/pflag"
6 |
7 | "github.com/cucumber/godog/internal/flags"
8 | )
9 |
10 | var version bool
11 | var output string
12 |
13 | // CreateRootCmd creates the root command.
14 | func CreateRootCmd() cobra.Command {
15 | rootCmd := cobra.Command{
16 | Use: "godog",
17 | Long: `Creates and runs test runner for the given feature files.
18 | Command should be run from the directory of tested package
19 | and contain buildable go source.`,
20 | Args: cobra.ArbitraryArgs,
21 | // Deprecated: Use godog build, godog run or godog version.
22 | // This is to support the legacy direct usage of the root command.
23 | RunE: runRootCmd,
24 | }
25 |
26 | bindRootCmdFlags(rootCmd.Flags())
27 |
28 | return rootCmd
29 | }
30 |
31 | func runRootCmd(cmd *cobra.Command, args []string) error {
32 | if version {
33 | versionCmdRunFunc(cmd, args)
34 | return nil
35 | }
36 |
37 | if len(output) > 0 {
38 | buildOutput = output
39 | if err := buildCmdRunFunc(cmd, args); err != nil {
40 | return err
41 | }
42 | }
43 |
44 | return runCmdRunFunc(cmd, args)
45 | }
46 |
47 | func bindRootCmdFlags(flagSet *pflag.FlagSet) {
48 | flagSet.StringVarP(&output, "output", "o", "", "compiles the test runner to the named file")
49 | flagSet.BoolVar(&version, "version", false, "show current version")
50 |
51 | flags.BindRunCmdFlags("", flagSet, &opts)
52 |
53 | // Since using the root command directly is deprecated.
54 | // All flags will be hidden
55 | flagSet.MarkHidden("output")
56 | flagSet.MarkHidden("version")
57 | flagSet.MarkHidden("no-colors")
58 | flagSet.MarkHidden("concurrency")
59 | flagSet.MarkHidden("tags")
60 | flagSet.MarkHidden("format")
61 | flagSet.MarkHidden("definitions")
62 | flagSet.MarkHidden("stop-on-failure")
63 | flagSet.MarkHidden("strict")
64 | flagSet.MarkHidden("random")
65 | }
66 |
--------------------------------------------------------------------------------
/internal/flags/flags_test.go:
--------------------------------------------------------------------------------
1 | package flags_test
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/spf13/pflag"
7 | "github.com/stretchr/testify/assert"
8 |
9 | "github.com/cucumber/godog/internal/flags"
10 | )
11 |
12 | func Test_BindFlagsShouldRespectFlagDefaults(t *testing.T) {
13 | opts := flags.Options{}
14 | flagSet := pflag.FlagSet{}
15 |
16 | flags.BindRunCmdFlags("optDefaults.", &flagSet, &opts)
17 |
18 | flagSet.Parse([]string{})
19 |
20 | assert.Equal(t, "pretty", opts.Format)
21 | assert.Equal(t, "", opts.Tags)
22 | assert.Equal(t, 1, opts.Concurrency)
23 | assert.False(t, opts.ShowStepDefinitions)
24 | assert.False(t, opts.StopOnFailure)
25 | assert.False(t, opts.Strict)
26 | assert.False(t, opts.NoColors)
27 | assert.Equal(t, int64(0), opts.Randomize)
28 | }
29 |
30 | func Test_BindFlagsShouldRespectFlagOverrides(t *testing.T) {
31 | opts := flags.Options{
32 | Format: "progress",
33 | Tags: "test",
34 | Concurrency: 2,
35 | ShowStepDefinitions: true,
36 | StopOnFailure: true,
37 | Strict: true,
38 | NoColors: true,
39 | Randomize: 11,
40 | }
41 | flagSet := pflag.FlagSet{}
42 |
43 | flags.BindRunCmdFlags("optOverrides.", &flagSet, &opts)
44 |
45 | flagSet.Parse([]string{
46 | "--optOverrides.format=junit",
47 | "--optOverrides.tags=test2",
48 | "--optOverrides.concurrency=3",
49 | "--optOverrides.definitions=false",
50 | "--optOverrides.stop-on-failure=false",
51 | "--optOverrides.strict=false",
52 | "--optOverrides.no-colors=false",
53 | "--optOverrides.random=2",
54 | })
55 |
56 | assert.Equal(t, "junit", opts.Format)
57 | assert.Equal(t, "test2", opts.Tags)
58 | assert.Equal(t, 3, opts.Concurrency)
59 | assert.False(t, opts.ShowStepDefinitions)
60 | assert.False(t, opts.StopOnFailure)
61 | assert.False(t, opts.Strict)
62 | assert.False(t, opts.NoColors)
63 | assert.Equal(t, int64(2), opts.Randomize)
64 | }
65 |
--------------------------------------------------------------------------------
/_examples/assert-godogs/godogs_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "os"
6 | "testing"
7 |
8 | "github.com/cucumber/godog"
9 | "github.com/cucumber/godog/colors"
10 | "github.com/spf13/pflag"
11 | "github.com/stretchr/testify/assert"
12 | )
13 |
14 | var opts = godog.Options{Output: colors.Colored(os.Stdout)}
15 |
16 | func init() {
17 | godog.BindCommandLineFlags("godog.", &opts)
18 | }
19 |
20 | func TestMain(m *testing.M) {
21 | pflag.Parse()
22 | opts.Paths = pflag.Args()
23 |
24 | status := godog.TestSuite{
25 | Name: "godogs",
26 | ScenarioInitializer: InitializeScenario,
27 | Options: &opts,
28 | }.Run()
29 |
30 | os.Exit(status)
31 | }
32 |
33 | func thereAreGodogs(available int) error {
34 | Godogs = available
35 | return nil
36 | }
37 |
38 | func iEat(ctx context.Context, num int) error {
39 | if !assert.GreaterOrEqual(godog.T(ctx), Godogs, num, "You cannot eat %d godogs, there are %d available", num, Godogs) {
40 | return nil
41 | }
42 | Godogs -= num
43 | return nil
44 | }
45 |
46 | func thereShouldBeRemaining(ctx context.Context, remaining int) error {
47 | assert.Equal(godog.T(ctx), Godogs, remaining, "Expected %d godogs to be remaining, but there is %d", remaining, Godogs)
48 | return nil
49 | }
50 |
51 | func thereShouldBeNoneRemaining(ctx context.Context) error {
52 | assert.Empty(godog.T(ctx), Godogs, "Expected none godogs to be remaining, but there is %d", Godogs)
53 | return nil
54 | }
55 |
56 | func InitializeScenario(ctx *godog.ScenarioContext) {
57 | ctx.Before(func(ctx context.Context, sc *godog.Scenario) (context.Context, error) {
58 | Godogs = 0 // clean the state before every scenario
59 | return ctx, nil
60 | })
61 |
62 | ctx.Step(`^there are (\d+) godogs$`, thereAreGodogs)
63 | ctx.Step(`^I eat (\d+)$`, iEat)
64 | ctx.Step(`^there should be (\d+) remaining$`, thereShouldBeRemaining)
65 | ctx.Step(`^there should be none remaining$`, thereShouldBeNoneRemaining)
66 | }
67 |
--------------------------------------------------------------------------------
/internal/builder/ast_test.go:
--------------------------------------------------------------------------------
1 | package builder
2 |
3 | import (
4 | "go/parser"
5 | "go/token"
6 | "testing"
7 | )
8 |
9 | var astContextSrc = `package main
10 |
11 | import (
12 | "github.com/cucumber/godog"
13 | )
14 |
15 | func MyContext(s *godog.Suite) {
16 | }`
17 |
18 | var astTwoContextSrc = `package lib
19 |
20 | import (
21 | "github.com/cucumber/godog"
22 | )
23 |
24 | func ApiContext(s *godog.Suite) {
25 | }
26 |
27 | func DBContext(s *godog.Suite) {
28 | }`
29 |
30 | func astContextParse(src string, t *testing.T) []string {
31 | fset := token.NewFileSet()
32 | f, err := parser.ParseFile(fset, "", []byte(src), 0)
33 | if err != nil {
34 | t.Fatalf("unexpected error while parsing ast: %v", err)
35 | }
36 |
37 | return astContexts(f, "Suite")
38 | }
39 |
40 | func TestShouldGetSingleContextFromSource(t *testing.T) {
41 | actual := astContextParse(astContextSrc, t)
42 | expect := []string{"MyContext"}
43 |
44 | if len(actual) != len(expect) {
45 | t.Fatalf("number of found contexts do not match, expected %d, but got %d", len(expect), len(actual))
46 | }
47 |
48 | for i, c := range expect {
49 | if c != actual[i] {
50 | t.Fatalf("expected context '%s' at pos %d, but got: '%s'", c, i, actual[i])
51 | }
52 | }
53 | }
54 |
55 | func TestShouldGetTwoContextsFromSource(t *testing.T) {
56 | actual := astContextParse(astTwoContextSrc, t)
57 | expect := []string{"ApiContext", "DBContext"}
58 |
59 | if len(actual) != len(expect) {
60 | t.Fatalf("number of found contexts do not match, expected %d, but got %d", len(expect), len(actual))
61 | }
62 |
63 | for i, c := range expect {
64 | if c != actual[i] {
65 | t.Fatalf("expected context '%s' at pos %d, but got: '%s'", c, i, actual[i])
66 | }
67 | }
68 | }
69 |
70 | func TestShouldNotFindAnyContextsInEmptyFile(t *testing.T) {
71 | actual := astContextParse(`package main`, t)
72 |
73 | if len(actual) != 0 {
74 | t.Fatalf("expected no contexts to be found, but there was some: %v", actual)
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/internal/formatters/formatter-tests/pretty/stop_on_first_failure:
--------------------------------------------------------------------------------
1 | Feature: Stop on first failure
2 |
3 | Scenario: First scenario - should run and fail # formatter-tests/features/stop_on_first_failure.feature:3
4 | Given a passing step # fmt_output_test.go:XXX -> github.com/cucumber/godog/internal/formatters_test.passingStepDef
5 | When a failing step # fmt_output_test.go:XXX -> github.com/cucumber/godog/internal/formatters_test.failingStepDef
6 | step failed
7 | Then a passing step # fmt_output_test.go:XXX -> github.com/cucumber/godog/internal/formatters_test.passingStepDef
8 |
9 | Scenario: Second scenario - should be skipped # formatter-tests/features/stop_on_first_failure.feature:8
10 | Given a passing step # fmt_output_test.go:XXX -> github.com/cucumber/godog/internal/formatters_test.passingStepDef
11 | Then a passing step # fmt_output_test.go:XXX -> github.com/cucumber/godog/internal/formatters_test.passingStepDef
12 |
13 | --- Failed steps:
14 |
15 | Scenario: First scenario - should run and fail # formatter-tests/features/stop_on_first_failure.feature:3
16 | When a failing step # formatter-tests/features/stop_on_first_failure.feature:5
17 | Error: step failed
18 |
19 |
20 | 2 scenarios (1 passed, 1 failed)
21 | 5 steps (3 passed, 1 failed, 1 skipped)
22 | 0s
23 |
--------------------------------------------------------------------------------
/internal/formatters/formatter-tests/events/with_few_empty_scenarios:
--------------------------------------------------------------------------------
1 | {"event":"TestRunStarted","version":"0.1.0","timestamp":-6795364578871,"suite":"events"}
2 | {"event":"TestSource","location":"formatter-tests/features/with_few_empty_scenarios.feature:1","source":"Feature: few empty scenarios\n\n Scenario: one\n\n Scenario Outline: two\n\n Examples: first group\n | one | two |\n | 1 | 2 |\n | 4 | 7 |\n\n Examples: second group\n | one | two |\n | 5 | 9 |\n\n Scenario: three\n"}
3 | {"event":"TestCaseStarted","location":"formatter-tests/features/with_few_empty_scenarios.feature:3","timestamp":-6795364578871}
4 | {"event":"TestCaseFinished","location":"formatter-tests/features/with_few_empty_scenarios.feature:3","timestamp":-6795364578871,"status":"undefined"}
5 | {"event":"TestCaseStarted","location":"formatter-tests/features/with_few_empty_scenarios.feature:9","timestamp":-6795364578871}
6 | {"event":"TestCaseFinished","location":"formatter-tests/features/with_few_empty_scenarios.feature:9","timestamp":-6795364578871,"status":"undefined"}
7 | {"event":"TestCaseStarted","location":"formatter-tests/features/with_few_empty_scenarios.feature:10","timestamp":-6795364578871}
8 | {"event":"TestCaseFinished","location":"formatter-tests/features/with_few_empty_scenarios.feature:10","timestamp":-6795364578871,"status":"undefined"}
9 | {"event":"TestCaseStarted","location":"formatter-tests/features/with_few_empty_scenarios.feature:14","timestamp":-6795364578871}
10 | {"event":"TestCaseFinished","location":"formatter-tests/features/with_few_empty_scenarios.feature:14","timestamp":-6795364578871,"status":"undefined"}
11 | {"event":"TestCaseStarted","location":"formatter-tests/features/with_few_empty_scenarios.feature:16","timestamp":-6795364578871}
12 | {"event":"TestCaseFinished","location":"formatter-tests/features/with_few_empty_scenarios.feature:16","timestamp":-6795364578871,"status":"undefined"}
13 | {"event":"TestRunFinished","status":"pending","timestamp":-6795364578871,"snippets":"","memory":""}
14 |
--------------------------------------------------------------------------------
/_examples/custom-formatter/godogs_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "os"
7 | "testing"
8 |
9 | "github.com/cucumber/godog"
10 | "github.com/cucumber/godog/colors"
11 | flag "github.com/spf13/pflag"
12 | )
13 |
14 | var opts = godog.Options{
15 | Output: colors.Colored(os.Stdout),
16 | Format: "emoji",
17 | }
18 |
19 | func init() {
20 | godog.BindCommandLineFlags("godog.", &opts)
21 | }
22 |
23 | func TestMain(m *testing.M) {
24 | flag.Parse()
25 | opts.Paths = flag.Args()
26 |
27 | status := godog.TestSuite{
28 | Name: "godogs",
29 | TestSuiteInitializer: InitializeTestSuite,
30 | ScenarioInitializer: InitializeScenario,
31 | Options: &opts,
32 | }.Run()
33 |
34 | // This example test is expected to fail to showcase custom formatting, suppressing status.
35 | if status != 1 {
36 | os.Exit(1)
37 | }
38 | }
39 |
40 | func thereAreGodogs(available int) error {
41 | Godogs = available
42 | return nil
43 | }
44 |
45 | func iEat(num int) error {
46 | if Godogs < num {
47 | return fmt.Errorf("you cannot eat %d godogs, there are %d available", num, Godogs)
48 | }
49 | Godogs -= num
50 | return nil
51 | }
52 |
53 | func thereShouldBeRemaining(remaining int) error {
54 | if Godogs != remaining {
55 | return fmt.Errorf("expected %d godogs to be remaining, but there is %d", remaining, Godogs)
56 | }
57 | return nil
58 | }
59 | func thisStepIsPending() error {
60 | return godog.ErrPending
61 | }
62 |
63 | func InitializeTestSuite(ctx *godog.TestSuiteContext) {
64 | ctx.BeforeSuite(func() { Godogs = 0 })
65 | }
66 |
67 | func InitializeScenario(ctx *godog.ScenarioContext) {
68 | ctx.Before(func(ctx context.Context, sc *godog.Scenario) (context.Context, error) {
69 | Godogs = 0 // clean the state before every scenario
70 |
71 | return ctx, nil
72 | })
73 |
74 | ctx.Step(`^there are (\d+) godogs$`, thereAreGodogs)
75 | ctx.Step(`^I eat (\d+)$`, iEat)
76 | ctx.Step(`^there should be (\d+) remaining$`, thereShouldBeRemaining)
77 | ctx.Step(`^this step is pending$`, thisStepIsPending)
78 | }
79 |
--------------------------------------------------------------------------------
/internal/formatters/formatter-tests/pretty/two_scenarios_with_background_fail:
--------------------------------------------------------------------------------
1 | Feature: two scenarios with background fail
2 |
3 | Background:
4 | Given passing step # fmt_output_test.go:101 -> github.com/cucumber/godog/internal/formatters_test.passingStepDef
5 | And failing step # fmt_output_test.go:117 -> github.com/cucumber/godog/internal/formatters_test.failingStepDef
6 | step failed
7 |
8 | Scenario: one # formatter-tests/features/two_scenarios_with_background_fail.feature:7
9 | When passing step # fmt_output_test.go:101 -> github.com/cucumber/godog/internal/formatters_test.passingStepDef
10 | Then passing step # fmt_output_test.go:101 -> github.com/cucumber/godog/internal/formatters_test.passingStepDef
11 |
12 | Scenario: two # formatter-tests/features/two_scenarios_with_background_fail.feature:11
13 | Then passing step # fmt_output_test.go:101 -> github.com/cucumber/godog/internal/formatters_test.passingStepDef
14 |
15 | --- Failed steps:
16 |
17 | Scenario: one # formatter-tests/features/two_scenarios_with_background_fail.feature:7
18 | And failing step # formatter-tests/features/two_scenarios_with_background_fail.feature:5
19 | Error: step failed
20 |
21 | Scenario: two # formatter-tests/features/two_scenarios_with_background_fail.feature:11
22 | And failing step # formatter-tests/features/two_scenarios_with_background_fail.feature:5
23 | Error: step failed
24 |
25 |
26 | 2 scenarios (2 failed)
27 | 7 steps (2 passed, 2 failed, 3 skipped)
28 | 0s
29 |
--------------------------------------------------------------------------------
/internal/flags/flags.go:
--------------------------------------------------------------------------------
1 | package flags
2 |
3 | import (
4 | "github.com/spf13/pflag"
5 | )
6 |
7 | // BindRunCmdFlags is an internal func to bind run subcommand flags.
8 | func BindRunCmdFlags(prefix string, flagSet *pflag.FlagSet, opts *Options) {
9 | if opts.Concurrency == 0 {
10 | opts.Concurrency = 1
11 | }
12 |
13 | if opts.Format == "" {
14 | opts.Format = "pretty"
15 | }
16 |
17 | flagSet.BoolVar(&opts.NoColors, prefix+"no-colors", opts.NoColors, "disable ansi colors")
18 | flagSet.IntVarP(&opts.Concurrency, prefix+"concurrency", "c", opts.Concurrency, "run the test suite with concurrency")
19 | flagSet.StringVarP(&opts.Tags, prefix+"tags", "t", opts.Tags, `filter scenarios by tags, expression can be:
20 | "@wip" run all scenarios with wip tag
21 | "~@wip" exclude all scenarios with wip tag
22 | "@wip && ~@new" run wip scenarios, but exclude new
23 | "@wip,@undone" run wip or undone scenarios`)
24 | flagSet.StringVarP(&opts.Format, prefix+"format", "f", opts.Format, `will write a report according to the selected formatter
25 |
26 | usage:
27 | -f
28 | will use the formatter and write the report on stdout
29 | -f :
30 | will use the formatter and write the report to the file path
31 |
32 | built-in formatters are:
33 | progress prints a character per step
34 | cucumber produces a Cucumber JSON report
35 | events produces JSON event stream, based on spec: 0.1.0
36 | junit produces JUnit compatible XML report
37 | pretty prints every feature with runtime statuses
38 | `)
39 |
40 | flagSet.BoolVarP(&opts.ShowStepDefinitions, prefix+"definitions", "d", opts.ShowStepDefinitions, "print all available step definitions")
41 | flagSet.BoolVar(&opts.StopOnFailure, prefix+"stop-on-failure", opts.StopOnFailure, "stop processing on first failed scenario")
42 | flagSet.BoolVar(&opts.Strict, prefix+"strict", opts.Strict, "fail suite when there are pending or undefined or ambiguous steps")
43 |
44 | flagSet.Int64Var(&opts.Randomize, prefix+"random", opts.Randomize, `randomly shuffle the scenario execution order
45 | --random
46 | specify SEED to reproduce the shuffling from a previous run
47 | --random=5738`)
48 | flagSet.Lookup(prefix + "random").NoOptDefVal = "-1"
49 | }
50 |
--------------------------------------------------------------------------------
/features/background.feature:
--------------------------------------------------------------------------------
1 | Feature: run background
2 | In order to test application behavior
3 | As a test suite
4 | I need to be able to run background correctly
5 |
6 | Scenario: should run background steps
7 | Given a feature "normal.feature" file:
8 | """
9 | Feature: with background
10 |
11 | Background:
12 | Given a feature path "features/load.feature:6"
13 |
14 | Scenario: parse a scenario
15 | When I parse features
16 | Then I should have 1 scenario registered
17 | """
18 | When I run feature suite
19 | Then the suite should have passed
20 | And the following steps should be passed:
21 | """
22 | a feature path "features/load.feature:6"
23 | I parse features
24 | I should have 1 scenario registered
25 | """
26 |
27 | Scenario: should skip all consequent steps on failure
28 | Given a feature "normal.feature" file:
29 | """
30 | Feature: with background
31 |
32 | Background:
33 | Given a failing step
34 | And a feature path "features/load.feature:6"
35 |
36 | Scenario: parse a scenario
37 | When I parse features
38 | Then I should have 1 scenario registered
39 | """
40 | When I run feature suite
41 | Then the suite should have failed
42 | And the following steps should be failed:
43 | """
44 | a failing step
45 | """
46 | And the following steps should be skipped:
47 | """
48 | a feature path "features/load.feature:6"
49 | I parse features
50 | I should have 1 scenario registered
51 | """
52 |
53 | Scenario: should continue undefined steps
54 | Given a feature "normal.feature" file:
55 | """
56 | Feature: with background
57 |
58 | Background:
59 | Given an undefined step
60 |
61 | Scenario: parse a scenario
62 | When I do undefined action
63 | Then I should have 1 scenario registered
64 | """
65 | When I run feature suite
66 | Then the suite should have passed
67 | And the following steps should be undefined:
68 | """
69 | an undefined step
70 | I do undefined action
71 | """
72 | And the following steps should be skipped:
73 | """
74 | I should have 1 scenario registered
75 | """
76 |
--------------------------------------------------------------------------------
/internal/models/results.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/cucumber/godog/colors"
7 | "github.com/cucumber/godog/internal/utils"
8 | )
9 |
10 | // TestRunStarted ...
11 | type TestRunStarted struct {
12 | StartedAt time.Time
13 | }
14 |
15 | // PickleResult ...
16 | type PickleResult struct {
17 | PickleID string
18 | StartedAt time.Time
19 | }
20 |
21 | // PickleAttachment ...
22 | type PickleAttachment struct {
23 | Name string
24 | MimeType string
25 | Data []byte
26 | }
27 |
28 | // PickleStepResult ...
29 | type PickleStepResult struct {
30 | Status StepResultStatus
31 | FinishedAt time.Time
32 | Err error
33 |
34 | PickleID string
35 | PickleStepID string
36 |
37 | Def *StepDefinition
38 |
39 | Attachments []PickleAttachment
40 | }
41 |
42 | // NewStepResult ...
43 | func NewStepResult(
44 | status StepResultStatus,
45 | pickleID, pickleStepID string,
46 | match *StepDefinition,
47 | attachments []PickleAttachment,
48 | err error,
49 | ) PickleStepResult {
50 | return PickleStepResult{
51 | Status: status,
52 | FinishedAt: utils.TimeNowFunc(),
53 | Err: err,
54 | PickleID: pickleID,
55 | PickleStepID: pickleStepID,
56 | Def: match,
57 | Attachments: attachments,
58 | }
59 | }
60 |
61 | // StepResultStatus ...
62 | type StepResultStatus int
63 |
64 | const (
65 | // Passed ...
66 | Passed StepResultStatus = iota
67 | // Failed ...
68 | Failed
69 | // Skipped ...
70 | Skipped
71 | // Undefined ...
72 | Undefined
73 | // Pending ...
74 | Pending
75 | // Ambiguous ...
76 | Ambiguous
77 | )
78 |
79 | // Color ...
80 | func (st StepResultStatus) Color() colors.ColorFunc {
81 | switch st {
82 | case Passed:
83 | return colors.Green
84 | case Failed:
85 | return colors.Red
86 | case Skipped:
87 | return colors.Cyan
88 | default:
89 | return colors.Yellow
90 | }
91 | }
92 |
93 | // String ...
94 | func (st StepResultStatus) String() string {
95 | switch st {
96 | case Passed:
97 | return "passed"
98 | case Failed:
99 | return "failed"
100 | case Skipped:
101 | return "skipped"
102 | case Undefined:
103 | return "undefined"
104 | case Pending:
105 | return "pending"
106 | case Ambiguous:
107 | return "ambiguous"
108 | default:
109 | return "unknown"
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/_examples/db/api.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "database/sql"
5 | "encoding/json"
6 | "fmt"
7 | "net/http"
8 |
9 | _ "github.com/go-sql-driver/mysql"
10 | )
11 |
12 | type server struct {
13 | db *sql.DB
14 | }
15 |
16 | type user struct {
17 | ID int64 `json:"-"`
18 | Username string `json:"username"`
19 | Email string `json:"-"`
20 | }
21 |
22 | func (s *server) users(w http.ResponseWriter, r *http.Request) {
23 | if r.Method != "GET" {
24 | fail(w, "Method not allowed", http.StatusMethodNotAllowed)
25 | return
26 | }
27 |
28 | var users []*user
29 | rows, err := s.db.Query("SELECT id, email, username FROM users")
30 | defer rows.Close()
31 | switch err {
32 | case nil:
33 | for rows.Next() {
34 | user := &user{}
35 | if err := rows.Scan(&user.ID, &user.Email, &user.Username); err != nil {
36 | fail(w, fmt.Sprintf("failed to scan an user: %s", err), http.StatusInternalServerError)
37 | return
38 | }
39 | users = append(users, user)
40 | }
41 | if len(users) == 0 {
42 | users = make([]*user, 0) // an empty array in this case
43 | }
44 | default:
45 | fail(w, fmt.Sprintf("failed to fetch users: %s", err), http.StatusInternalServerError)
46 | return
47 | }
48 |
49 | data := struct {
50 | Users []*user `json:"users"`
51 | }{Users: users}
52 |
53 | ok(w, data)
54 | }
55 |
56 | func main() {
57 | db, err := sql.Open("mysql", "root@/godog")
58 | if err != nil {
59 | panic(err)
60 | }
61 | s := &server{db: db}
62 | http.HandleFunc("/users", s.users)
63 | http.ListenAndServe(":8080", nil)
64 | }
65 |
66 | // fail writes a json response with error msg and status header
67 | func fail(w http.ResponseWriter, msg string, status int) {
68 | w.Header().Set("Content-Type", "application/json")
69 |
70 | data := struct {
71 | Error string `json:"error"`
72 | }{Error: msg}
73 |
74 | resp, _ := json.Marshal(data)
75 | w.WriteHeader(status)
76 |
77 | fmt.Fprintf(w, string(resp))
78 | }
79 |
80 | // ok writes data to response with 200 status
81 | func ok(w http.ResponseWriter, data interface{}) {
82 | w.Header().Set("Content-Type", "application/json")
83 |
84 | if s, ok := data.(string); ok {
85 | fmt.Fprintf(w, s)
86 | return
87 | }
88 |
89 | resp, err := json.Marshal(data)
90 | if err != nil {
91 | w.WriteHeader(http.StatusInternalServerError)
92 | fail(w, "oops something evil has happened", 500)
93 | return
94 | }
95 |
96 | fmt.Fprintf(w, string(resp))
97 | }
98 |
--------------------------------------------------------------------------------
/features/formatter/events.feature:
--------------------------------------------------------------------------------
1 | Feature: event stream formatter
2 | In order to have universal cucumber formatter
3 | As a test suite
4 | I need to be able to support event stream formatter
5 |
6 | Scenario: should fire only suite events without any scenario
7 | Given a feature path "features/load.feature:4"
8 | When I run feature suite with formatter "events"
9 | Then the following events should be fired:
10 | """
11 | TestRunStarted
12 | TestRunFinished
13 | """
14 |
15 | Scenario: should process simple scenario
16 | Given a feature path "features/load.feature:27"
17 | When I run feature suite with formatter "events"
18 | Then the following events should be fired:
19 | """
20 | TestRunStarted
21 | TestSource
22 | TestCaseStarted
23 | StepDefinitionFound
24 | TestStepStarted
25 | TestStepFinished
26 | StepDefinitionFound
27 | TestStepStarted
28 | TestStepFinished
29 | StepDefinitionFound
30 | TestStepStarted
31 | TestStepFinished
32 | TestCaseFinished
33 | TestRunFinished
34 | """
35 |
36 | Scenario: should process outline scenario
37 | Given a feature path "features/load.feature:35"
38 | When I run feature suite with formatter "events"
39 | Then the following events should be fired:
40 | """
41 | TestRunStarted
42 | TestSource
43 | TestCaseStarted
44 | StepDefinitionFound
45 | TestStepStarted
46 | TestStepFinished
47 | StepDefinitionFound
48 | TestStepStarted
49 | TestStepFinished
50 | StepDefinitionFound
51 | TestStepStarted
52 | TestStepFinished
53 | TestCaseFinished
54 | TestCaseStarted
55 | StepDefinitionFound
56 | TestStepStarted
57 | TestStepFinished
58 | StepDefinitionFound
59 | TestStepStarted
60 | TestStepFinished
61 | StepDefinitionFound
62 | TestStepStarted
63 | TestStepFinished
64 | TestCaseFinished
65 | TestCaseStarted
66 | StepDefinitionFound
67 | TestStepStarted
68 | TestStepFinished
69 | StepDefinitionFound
70 | TestStepStarted
71 | TestStepFinished
72 | StepDefinitionFound
73 | TestStepStarted
74 | TestStepFinished
75 | TestCaseFinished
76 | TestRunFinished
77 | """
78 |
--------------------------------------------------------------------------------
/internal/builder/builder_go_module_test.go:
--------------------------------------------------------------------------------
1 | package builder_test
2 |
3 | import (
4 | "os"
5 | "os/exec"
6 | "path/filepath"
7 | "testing"
8 | )
9 |
10 | func testOutsideGopathAndHavingOnlyFeature(t *testing.T) {
11 | builderTC := builderTestCase{}
12 |
13 | builderTC.dir = filepath.Join(os.TempDir(), t.Name(), "godogs")
14 | builderTC.files = map[string]string{
15 | "godogs.feature": builderFeatureFile,
16 | }
17 |
18 | builderTC.goModCmds = make([]*exec.Cmd, 2)
19 | builderTC.goModCmds[0] = exec.Command("go", "mod", "init", "godogs")
20 |
21 | builderTC.goModCmds[1] = exec.Command("go", "mod", "tidy")
22 |
23 | builderTC.run(t)
24 | }
25 |
26 | func testOutsideGopath(t *testing.T) {
27 | builderTC := builderTestCase{}
28 |
29 | builderTC.dir = filepath.Join(os.TempDir(), t.Name(), "godogs")
30 | builderTC.files = map[string]string{
31 | "godogs.feature": builderFeatureFile,
32 | "godogs.go": builderMainCodeFile,
33 | "godogs_test.go": builderTestFile,
34 | }
35 |
36 | builderTC.goModCmds = make([]*exec.Cmd, 1)
37 | builderTC.goModCmds[0] = exec.Command("go", "mod", "init", "godogs")
38 |
39 | builderTC.run(t)
40 | }
41 |
42 | func testOutsideGopathWithXTest(t *testing.T) {
43 | builderTC := builderTestCase{}
44 |
45 | builderTC.dir = filepath.Join(os.TempDir(), t.Name(), "godogs")
46 | builderTC.files = map[string]string{
47 | "godogs.feature": builderFeatureFile,
48 | "godogs.go": builderMainCodeFile,
49 | "godogs_test.go": builderXTestFile,
50 | }
51 |
52 | builderTC.goModCmds = make([]*exec.Cmd, 1)
53 | builderTC.goModCmds[0] = exec.Command("go", "mod", "init", "godogs")
54 |
55 | builderTC.run(t)
56 | }
57 |
58 | func testInsideGopath(t *testing.T) {
59 | builderTC := builderTestCase{}
60 |
61 | gopath := filepath.Join(os.TempDir(), t.Name(), "_gp")
62 | defer os.RemoveAll(gopath)
63 |
64 | builderTC.dir = filepath.Join(gopath, "src", "godogs")
65 | builderTC.files = map[string]string{
66 | "godogs.feature": builderFeatureFile,
67 | "godogs.go": builderMainCodeFile,
68 | "godogs_test.go": builderTestFile,
69 | }
70 |
71 | builderTC.goModCmds = make([]*exec.Cmd, 1)
72 | builderTC.goModCmds[0] = exec.Command("go", "mod", "init", "godogs")
73 | builderTC.goModCmds[0].Env = os.Environ()
74 | builderTC.goModCmds[0].Env = append(builderTC.goModCmds[0].Env, "GOPATH="+gopath)
75 | builderTC.goModCmds[0].Env = append(builderTC.goModCmds[0].Env, "GO111MODULE=on")
76 |
77 | builderTC.run(t)
78 | }
79 |
--------------------------------------------------------------------------------
/internal/formatters/formatter-tests/pretty/scenario_outline:
--------------------------------------------------------------------------------
1 | Feature: outline
2 |
3 | Scenario Outline: outline # formatter-tests/features/scenario_outline.feature:5
4 | Given passing step # fmt_output_test.go:101 -> github.com/cucumber/godog/internal/formatters_test.passingStepDef
5 | When passing step # fmt_output_test.go:101 -> github.com/cucumber/godog/internal/formatters_test.passingStepDef
6 | Then odd and even number # fmt_output_test.go:103 -> github.com/cucumber/godog/internal/formatters_test.oddEvenStepDef
7 |
8 | Examples: tagged
9 | | odd | even |
10 | | 1 | 2 |
11 | | 2 | 0 |
12 | 2 is not odd
13 | | 3 | 11 |
14 | 11 is not even
15 |
16 | Examples:
17 | | odd | even |
18 | | 1 | 14 |
19 | | 3 | 9 |
20 | 9 is not even
21 |
22 | --- Failed steps:
23 |
24 | Scenario Outline: outline # formatter-tests/features/scenario_outline.feature:5
25 | Then odd 2 and even 0 number # formatter-tests/features/scenario_outline.feature:8
26 | Error: 2 is not odd
27 |
28 | Scenario Outline: outline # formatter-tests/features/scenario_outline.feature:5
29 | Then odd 3 and even 11 number # formatter-tests/features/scenario_outline.feature:8
30 | Error: 11 is not even
31 |
32 | Scenario Outline: outline # formatter-tests/features/scenario_outline.feature:5
33 | Then odd 3 and even 9 number # formatter-tests/features/scenario_outline.feature:8
34 | Error: 9 is not even
35 |
36 |
37 | 5 scenarios (2 passed, 3 failed)
38 | 15 steps (12 passed, 3 failed)
39 | 0s
40 |
--------------------------------------------------------------------------------
/internal/formatters/formatter-tests/junit,pretty/stop_on_first_failure:
--------------------------------------------------------------------------------
1 | Feature: Stop on first failure
2 |
3 | Scenario: First scenario - should run and fail # formatter-tests/features/stop_on_first_failure.feature:3
4 | Given a passing step # fmt_output_test.go:XXX -> github.com/cucumber/godog/internal/formatters_test.passingStepDef
5 | When a failing step # fmt_output_test.go:XXX -> github.com/cucumber/godog/internal/formatters_test.failingStepDef
6 | step failed
7 | Then a passing step # fmt_output_test.go:XXX -> github.com/cucumber/godog/internal/formatters_test.passingStepDef
8 |
9 | Scenario: Second scenario - should be skipped # formatter-tests/features/stop_on_first_failure.feature:8
10 | Given a passing step # fmt_output_test.go:XXX -> github.com/cucumber/godog/internal/formatters_test.passingStepDef
11 | Then a passing step # fmt_output_test.go:XXX -> github.com/cucumber/godog/internal/formatters_test.passingStepDef
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | --- Failed steps:
23 |
24 | Scenario: First scenario - should run and fail # formatter-tests/features/stop_on_first_failure.feature:3
25 | When a failing step # formatter-tests/features/stop_on_first_failure.feature:5
26 | Error: step failed
27 |
28 |
29 | 2 scenarios (1 passed, 1 failed)
30 | 5 steps (3 passed, 1 failed, 1 skipped)
31 | 0s
32 |
--------------------------------------------------------------------------------
/internal/flags/options.go:
--------------------------------------------------------------------------------
1 | package flags
2 |
3 | import (
4 | "context"
5 | "io"
6 | "io/fs"
7 | "testing"
8 | )
9 |
10 | // Options are suite run options
11 | // flags are mapped to these options.
12 | //
13 | // It can also be used together with godog.RunWithOptions
14 | // to run test suite from go source directly
15 | //
16 | // See the flags for more details
17 | type Options struct {
18 | // Print step definitions found and exit
19 | ShowStepDefinitions bool
20 |
21 | // Randomize, if not `0`, will be used to run scenarios in a random order.
22 | //
23 | // Randomizing scenario order is especially helpful for detecting
24 | // situations where you have state leaking between scenarios, which can
25 | // cause flickering or fragile tests.
26 | //
27 | // The default value of `0` means "do not randomize".
28 | //
29 | // The magic value of `-1` means "pick a random seed for me", and godog will
30 | // assign a seed on it's own during the `RunWithOptions` phase, similar to if
31 | // you specified `--random` on the command line.
32 | //
33 | // Any other value will be used as the random seed for shuffling. Re-using the
34 | // same seed will allow you to reproduce the shuffle order of a previous run
35 | // to isolate an error condition.
36 | Randomize int64
37 |
38 | // Stops on the first failure
39 | StopOnFailure bool
40 |
41 | // Fail suite when there are pending or undefined or ambiguous steps
42 | Strict bool
43 |
44 | // Forces ansi color stripping
45 | NoColors bool
46 |
47 | // Various filters for scenarios parsed
48 | // from feature files
49 | Tags string
50 |
51 | // Dialect to be used to parse feature files. If not set, default to "en".
52 | Dialect string
53 |
54 | // The formatter name
55 | Format string
56 |
57 | // Concurrency rate, not all formatters accepts this
58 | Concurrency int
59 |
60 | // All feature file paths
61 | Paths []string
62 |
63 | // Where it should print formatter output
64 | Output io.Writer
65 |
66 | // DefaultContext is used as initial context instead of context.Background().
67 | DefaultContext context.Context
68 |
69 | // TestingT runs scenarios as subtests.
70 | TestingT *testing.T
71 |
72 | // FeatureContents allows passing in each feature manually
73 | // where the contents of each feature is stored as a byte slice
74 | // in a map entry
75 | FeatureContents []Feature
76 |
77 | // FS allows passing in an `fs.FS` to read features from, such as an `embed.FS`
78 | // or os.DirFS(string).
79 | FS fs.FS
80 |
81 | // ShowHelp enables suite to show CLI flags usage help and exit.
82 | ShowHelp bool
83 | }
84 |
85 | type Feature struct {
86 | Name string
87 | Contents []byte
88 | }
89 |
--------------------------------------------------------------------------------
/internal/formatters/formatter-tests/cucumber/scenario_with_background:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "uri": "formatter-tests/features/scenario_with_background.feature",
4 | "id": "single-scenario-with-background",
5 | "keyword": "Feature",
6 | "name": "single scenario with background",
7 | "description": "",
8 | "line": 1,
9 | "elements": [
10 | {
11 | "id": "single-scenario-with-background;scenario",
12 | "keyword": "Scenario",
13 | "name": "scenario",
14 | "description": "",
15 | "line": 7,
16 | "type": "scenario",
17 | "steps": [
18 | {
19 | "keyword": "Given ",
20 | "name": "passing step",
21 | "line": 4,
22 | "match": {
23 | "location": "fmt_output_test.go:101"
24 | },
25 | "result": {
26 | "status": "passed",
27 | "duration": 0
28 | }
29 | },
30 | {
31 | "keyword": "And ",
32 | "name": "passing step",
33 | "line": 5,
34 | "match": {
35 | "location": "fmt_output_test.go:101"
36 | },
37 | "result": {
38 | "status": "passed",
39 | "duration": 0
40 | }
41 | },
42 | {
43 | "keyword": "When ",
44 | "name": "passing step",
45 | "line": 8,
46 | "match": {
47 | "location": "fmt_output_test.go:101"
48 | },
49 | "result": {
50 | "status": "passed",
51 | "duration": 0
52 | }
53 | },
54 | {
55 | "keyword": "Then ",
56 | "name": "passing step",
57 | "line": 9,
58 | "match": {
59 | "location": "fmt_output_test.go:101"
60 | },
61 | "result": {
62 | "status": "passed",
63 | "duration": 0
64 | }
65 | }
66 | ]
67 | }
68 | ]
69 | }
70 | ]
71 |
--------------------------------------------------------------------------------
/_examples/api/api_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "encoding/json"
6 | "fmt"
7 | "net/http"
8 | "net/http/httptest"
9 | "reflect"
10 | "testing"
11 |
12 | "github.com/cucumber/godog"
13 | )
14 |
15 | type apiFeature struct {
16 | resp *httptest.ResponseRecorder
17 | }
18 |
19 | func (a *apiFeature) resetResponse(*godog.Scenario) {
20 | a.resp = httptest.NewRecorder()
21 | }
22 |
23 | func (a *apiFeature) iSendrequestTo(method, endpoint string) (err error) {
24 | req, err := http.NewRequest(method, endpoint, nil)
25 | if err != nil {
26 | return
27 | }
28 |
29 | // handle panic
30 | defer func() {
31 | switch t := recover().(type) {
32 | case string:
33 | err = fmt.Errorf(t)
34 | case error:
35 | err = t
36 | }
37 | }()
38 |
39 | switch endpoint {
40 | case "/version":
41 | getVersion(a.resp, req)
42 | default:
43 | err = fmt.Errorf("unknown endpoint: %s", endpoint)
44 | }
45 | return
46 | }
47 |
48 | func (a *apiFeature) theResponseCodeShouldBe(code int) error {
49 | if code != a.resp.Code {
50 | return fmt.Errorf("expected response code to be: %d, but actual is: %d", code, a.resp.Code)
51 | }
52 | return nil
53 | }
54 |
55 | func (a *apiFeature) theResponseShouldMatchJSON(body *godog.DocString) (err error) {
56 | var expected, actual interface{}
57 |
58 | // re-encode expected response
59 | if err = json.Unmarshal([]byte(body.Content), &expected); err != nil {
60 | return
61 | }
62 |
63 | // re-encode actual response too
64 | if err = json.Unmarshal(a.resp.Body.Bytes(), &actual); err != nil {
65 | return
66 | }
67 |
68 | // the matching may be adapted per different requirements.
69 | if !reflect.DeepEqual(expected, actual) {
70 | return fmt.Errorf("expected JSON does not match actual, %v vs. %v", expected, actual)
71 | }
72 | return nil
73 | }
74 |
75 | func TestFeatures(t *testing.T) {
76 | suite := godog.TestSuite{
77 | ScenarioInitializer: InitializeScenario,
78 | Options: &godog.Options{
79 | Format: "pretty",
80 | Paths: []string{"features"},
81 | TestingT: t, // Testing instance that will run subtests.
82 | },
83 | }
84 |
85 | if suite.Run() != 0 {
86 | t.Fatal("non-zero status returned, failed to run feature tests")
87 | }
88 | }
89 |
90 | func InitializeScenario(ctx *godog.ScenarioContext) {
91 | api := &apiFeature{}
92 |
93 | ctx.Before(func(ctx context.Context, sc *godog.Scenario) (context.Context, error) {
94 | api.resetResponse(sc)
95 | return ctx, nil
96 | })
97 | ctx.Step(`^I send "(GET|POST|PUT|DELETE)" request to "([^"]*)"$`, api.iSendrequestTo)
98 | ctx.Step(`^the response code should be (\d+)$`, api.theResponseCodeShouldBe)
99 | ctx.Step(`^the response should match json:$`, api.theResponseShouldMatchJSON)
100 | }
101 |
--------------------------------------------------------------------------------
/internal/formatters/formatter-tests/events/scenario_with_background:
--------------------------------------------------------------------------------
1 | {"event":"TestRunStarted","version":"0.1.0","timestamp":-6795364578871,"suite":"events"}
2 | {"event":"TestSource","location":"formatter-tests/features/scenario_with_background.feature:1","source":"Feature: single scenario with background\n\n Background: named\n Given passing step\n And passing step\n\n Scenario: scenario\n When passing step\n Then passing step\n"}
3 | {"event":"TestCaseStarted","location":"formatter-tests/features/scenario_with_background.feature:7","timestamp":-6795364578871}
4 | {"event":"StepDefinitionFound","location":"formatter-tests/features/scenario_with_background.feature:4","definition_id":"fmt_output_test.go:101 -\u003e github.com/cucumber/godog/internal/formatters_test.passingStepDef","arguments":[]}
5 | {"event":"TestStepStarted","location":"formatter-tests/features/scenario_with_background.feature:4","timestamp":-6795364578871}
6 | {"event":"TestStepFinished","location":"formatter-tests/features/scenario_with_background.feature:4","timestamp":-6795364578871,"status":"passed"}
7 | {"event":"StepDefinitionFound","location":"formatter-tests/features/scenario_with_background.feature:5","definition_id":"fmt_output_test.go:101 -\u003e github.com/cucumber/godog/internal/formatters_test.passingStepDef","arguments":[]}
8 | {"event":"TestStepStarted","location":"formatter-tests/features/scenario_with_background.feature:5","timestamp":-6795364578871}
9 | {"event":"TestStepFinished","location":"formatter-tests/features/scenario_with_background.feature:5","timestamp":-6795364578871,"status":"passed"}
10 | {"event":"StepDefinitionFound","location":"formatter-tests/features/scenario_with_background.feature:8","definition_id":"fmt_output_test.go:101 -\u003e github.com/cucumber/godog/internal/formatters_test.passingStepDef","arguments":[]}
11 | {"event":"TestStepStarted","location":"formatter-tests/features/scenario_with_background.feature:8","timestamp":-6795364578871}
12 | {"event":"TestStepFinished","location":"formatter-tests/features/scenario_with_background.feature:8","timestamp":-6795364578871,"status":"passed"}
13 | {"event":"StepDefinitionFound","location":"formatter-tests/features/scenario_with_background.feature:9","definition_id":"fmt_output_test.go:101 -\u003e github.com/cucumber/godog/internal/formatters_test.passingStepDef","arguments":[]}
14 | {"event":"TestStepStarted","location":"formatter-tests/features/scenario_with_background.feature:9","timestamp":-6795364578871}
15 | {"event":"TestStepFinished","location":"formatter-tests/features/scenario_with_background.feature:9","timestamp":-6795364578871,"status":"passed"}
16 | {"event":"TestCaseFinished","location":"formatter-tests/features/scenario_with_background.feature:7","timestamp":-6795364578871,"status":"passed"}
17 | {"event":"TestRunFinished","status":"passed","timestamp":-6795364578871,"snippets":"","memory":""}
18 |
--------------------------------------------------------------------------------
/internal/storage/fs_test.go:
--------------------------------------------------------------------------------
1 | package storage_test
2 |
3 | import (
4 | "errors"
5 | "io/fs"
6 | "io/ioutil"
7 | "os"
8 | "path/filepath"
9 | "strings"
10 | "testing"
11 | "testing/fstest"
12 |
13 | "github.com/cucumber/godog/internal/storage"
14 | "github.com/stretchr/testify/assert"
15 | "github.com/stretchr/testify/require"
16 | )
17 |
18 | func TestStorage_Open_FS(t *testing.T) {
19 | tests := map[string]struct {
20 | fs fs.FS
21 |
22 | expData []byte
23 | expError error
24 | }{
25 | "normal open": {
26 | fs: fstest.MapFS{
27 | "testfile": {
28 | Data: []byte("hello worlds"),
29 | },
30 | },
31 | expData: []byte("hello worlds"),
32 | },
33 | "file not found": {
34 | fs: fstest.MapFS{},
35 | expError: errors.New("open testfile: file does not exist"),
36 | },
37 | "nil fs falls back on os": {
38 | expError: errors.New("open testfile: no such file or directory"),
39 | },
40 | }
41 |
42 | for name, test := range tests {
43 | test := test
44 | t.Run(name, func(t *testing.T) {
45 | t.Parallel()
46 |
47 | f, err := (storage.FS{FS: test.fs}).Open("testfile")
48 | if test.expError != nil {
49 | assert.Error(t, err)
50 | assert.EqualError(t, err, test.expError.Error())
51 | return
52 | }
53 |
54 | assert.NoError(t, err)
55 |
56 | bb := make([]byte, len(test.expData))
57 | _, _ = f.Read(bb)
58 | assert.Equal(t, test.expData, bb)
59 | })
60 | }
61 | }
62 |
63 | func TestStorage_Open_OS(t *testing.T) {
64 | tests := map[string]struct {
65 | files map[string][]byte
66 | expData []byte
67 | expError error
68 | }{
69 | "normal open": {
70 | files: map[string][]byte{
71 | "testfile": []byte("hello worlds"),
72 | },
73 | expData: []byte("hello worlds"),
74 | },
75 | "nil fs falls back on os": {
76 | expError: errors.New("open %baseDir%/testfile: no such file or directory"),
77 | },
78 | }
79 |
80 | for name, test := range tests {
81 | test := test
82 | t.Run(name, func(t *testing.T) {
83 | t.Parallel()
84 |
85 | baseDir := filepath.Join(os.TempDir(), t.Name(), "godogs")
86 | err := os.MkdirAll(baseDir+"/a", 0755)
87 | defer os.RemoveAll(baseDir)
88 |
89 | require.Nil(t, err)
90 |
91 | for name, data := range test.files {
92 | err := ioutil.WriteFile(filepath.Join(baseDir, name), data, 0644)
93 | require.NoError(t, err)
94 | }
95 |
96 | f, err := (storage.FS{}).Open(filepath.Join(baseDir, "testfile"))
97 | if test.expError != nil {
98 | assert.Error(t, err)
99 | assert.EqualError(t, err, strings.ReplaceAll(test.expError.Error(), "%baseDir%", baseDir))
100 | return
101 | }
102 |
103 | assert.NoError(t, err)
104 |
105 | bb := make([]byte, len(test.expData))
106 | _, _ = f.Read(bb)
107 | assert.Equal(t, test.expData, bb)
108 | })
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/internal/formatters/formatter-tests/junit,pretty/two_scenarios_with_background_fail:
--------------------------------------------------------------------------------
1 | Feature: two scenarios with background fail
2 |
3 | Background:
4 | Given passing step # fmt_output_test.go:101 -> github.com/cucumber/godog/internal/formatters_test.passingStepDef
5 | And failing step # fmt_output_test.go:117 -> github.com/cucumber/godog/internal/formatters_test.failingStepDef
6 | step failed
7 |
8 | Scenario: one # formatter-tests/features/two_scenarios_with_background_fail.feature:7
9 | When passing step # fmt_output_test.go:101 -> github.com/cucumber/godog/internal/formatters_test.passingStepDef
10 | Then passing step # fmt_output_test.go:101 -> github.com/cucumber/godog/internal/formatters_test.passingStepDef
11 |
12 | Scenario: two # formatter-tests/features/two_scenarios_with_background_fail.feature:11
13 | Then passing step # fmt_output_test.go:101 -> github.com/cucumber/godog/internal/formatters_test.passingStepDef
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | --- Failed steps:
29 |
30 | Scenario: one # formatter-tests/features/two_scenarios_with_background_fail.feature:7
31 | And failing step # formatter-tests/features/two_scenarios_with_background_fail.feature:5
32 | Error: step failed
33 |
34 | Scenario: two # formatter-tests/features/two_scenarios_with_background_fail.feature:11
35 | And failing step # formatter-tests/features/two_scenarios_with_background_fail.feature:5
36 | Error: step failed
37 |
38 |
39 | 2 scenarios (2 failed)
40 | 7 steps (2 passed, 2 failed, 3 skipped)
41 | 0s
42 |
--------------------------------------------------------------------------------
/RELEASING.md:
--------------------------------------------------------------------------------
1 | # Releasing Guidelines for Cucumber Godog
2 |
3 | This document provides guidelines for releasing new versions of Cucumber Godog. Follow these steps to ensure a smooth and consistent release process.
4 |
5 | ## Versioning
6 |
7 | Cucumber Godog follows [Semantic Versioning]. Version numbers are in the format `MAJOR.MINOR.PATCH`.
8 |
9 | ### Current (for v0.MINOR.PATCH)
10 |
11 | - **MINOR**: Incompatible API changes.
12 | - **PATCH**: Backward-compatible new features and bug fixes.
13 |
14 | ### After v1.X.X release
15 |
16 | - **MAJOR**: Incompatible API changes.
17 | - **MINOR**: Backward-compatible new features.
18 | - **PATCH**: Backward-compatible bug fixes.
19 |
20 | ## Release Process
21 |
22 | 1. **Update Changelog:**
23 | - Open `CHANGELOG.md` and add an entry for the upcoming release formatting according to the principles of [Keep A CHANGELOG].
24 | - Include details about new features, enhancements, and bug fixes.
25 |
26 | 2. **Run Tests:**
27 | - Run the test suite to ensure all existing features are working as expected.
28 |
29 | 3. **Manual Testing for Backwards Compatibility:**
30 | - Manually test the new release with external libraries that depend on Cucumber Godog.
31 | - Look for any potential backwards compatibility issues, especially with widely-used libraries.
32 | - Address any identified issues before proceeding.
33 |
34 | 4. **Create Release on GitHub:**
35 | - Go to the [Releases] page on GitHub.
36 | - Click on "Draft a new release."
37 | - Tag version should be set to the new tag vMAJOR.MINOR.PATCH
38 | - Title the release using the version number (e.g., "vMAJOR.MINOR.PATCH").
39 | - Click 'Generate release notes'
40 |
41 | 5. **Publish Release:**
42 | - Click "Publish release" to make the release public.
43 |
44 | 6. **Announce the Release:**
45 | - Make an announcement on relevant communication channels (e.g., [community Discord]) about the new release.
46 |
47 | ## Additional Considerations
48 |
49 | - **Documentation:**
50 | - Update the project documentation on the [website], if applicable.
51 |
52 | - **Deprecation Notices:**
53 | - If any features are deprecated, clearly document them in the release notes and provide guidance on migration.
54 |
55 | - **Compatibility:**
56 | - Clearly state any compatibility requirements or changes in the release notes.
57 |
58 | - **Feedback:**
59 | - Encourage users to provide feedback and report any issues with the new release.
60 |
61 | Following these guidelines, including manual testing with external libraries, will help ensure a thorough release process for Cucumber Godog, allowing detection and resolution of potential backwards compatibility issues before tagging the release.
62 |
63 | [community Discord]: https://cucumber.io/community#discord
64 | [website]: https://cucumber.github.io/godog/
65 | [Releases]: https://github.com/cucumber/godog/releases
66 | [Semantic Versioning]: http://semver.org
67 | [Keep A CHANGELOG]: http://keepachangelog.com
--------------------------------------------------------------------------------
/internal/formatters/undefined_snippets_gen.go:
--------------------------------------------------------------------------------
1 | package formatters
2 |
3 | import (
4 | "fmt"
5 | "reflect"
6 | "regexp"
7 | "strings"
8 | "text/template"
9 |
10 | messages "github.com/cucumber/messages/go/v21"
11 | )
12 |
13 | // some snippet formatting regexps
14 | var snippetExprCleanup = regexp.MustCompile(`([\/\[\]\(\)\\^\$\.\|\?\*\+\'])`)
15 | var snippetExprQuoted = regexp.MustCompile(`(\W|^)"(?:[^"]*)"(\W|$)`)
16 | var snippetMethodName = regexp.MustCompile(`[^a-zA-Z\_\ ]`)
17 | var snippetNumbers = regexp.MustCompile(`(\d+)`)
18 |
19 | var snippetHelperFuncs = template.FuncMap{
20 | "backticked": func(s string) string {
21 | return "`" + s + "`"
22 | },
23 | }
24 |
25 | var undefinedSnippetsTpl = template.Must(template.New("snippets").Funcs(snippetHelperFuncs).Parse(`
26 | {{ range . }}func {{ .Method }}({{ .Args }}) error {
27 | return godog.ErrPending
28 | }
29 |
30 | {{end}}func InitializeScenario(ctx *godog.ScenarioContext) { {{ range . }}
31 | ctx.Step({{ backticked .Expr }}, {{ .Method }}){{end}}
32 | }
33 | `))
34 |
35 | type undefinedSnippet struct {
36 | Method string
37 | Expr string
38 | argument *messages.PickleStepArgument
39 | }
40 |
41 | func (s undefinedSnippet) Args() (ret string) {
42 | var (
43 | args []string
44 | pos int
45 | breakLoop bool
46 | )
47 |
48 | for !breakLoop {
49 | part := s.Expr[pos:]
50 | ipos := strings.Index(part, "(\\d+)")
51 | spos := strings.Index(part, "\"([^\"]*)\"")
52 |
53 | switch {
54 | case spos == -1 && ipos == -1:
55 | breakLoop = true
56 | case spos == -1:
57 | pos += ipos + len("(\\d+)")
58 | args = append(args, reflect.Int.String())
59 | case ipos == -1:
60 | pos += spos + len("\"([^\"]*)\"")
61 | args = append(args, reflect.String.String())
62 | case ipos < spos:
63 | pos += ipos + len("(\\d+)")
64 | args = append(args, reflect.Int.String())
65 | case spos < ipos:
66 | pos += spos + len("\"([^\"]*)\"")
67 | args = append(args, reflect.String.String())
68 | }
69 | }
70 |
71 | if s.argument != nil {
72 | if s.argument.DocString != nil {
73 | args = append(args, "*godog.DocString")
74 | }
75 |
76 | if s.argument.DataTable != nil {
77 | args = append(args, "*godog.Table")
78 | }
79 | }
80 |
81 | var last string
82 |
83 | for i, arg := range args {
84 | if last == "" || last == arg {
85 | ret += fmt.Sprintf("arg%d, ", i+1)
86 | } else {
87 | ret = strings.TrimRight(ret, ", ") + fmt.Sprintf(" %s, arg%d, ", last, i+1)
88 | }
89 |
90 | last = arg
91 | }
92 |
93 | return strings.TrimSpace(strings.TrimRight(ret, ", ") + " " + last)
94 | }
95 |
96 | type snippetSortByMethod []undefinedSnippet
97 |
98 | func (s snippetSortByMethod) Len() int {
99 | return len(s)
100 | }
101 |
102 | func (s snippetSortByMethod) Swap(i, j int) {
103 | s[i], s[j] = s[j], s[i]
104 | }
105 |
106 | func (s snippetSortByMethod) Less(i, j int) bool {
107 | return s[i].Method < s[j].Method
108 | }
109 |
--------------------------------------------------------------------------------
/cmd/godog/internal/cmd_run.go:
--------------------------------------------------------------------------------
1 | package internal
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "os/exec"
7 | "path/filepath"
8 | "syscall"
9 |
10 | "github.com/spf13/cobra"
11 |
12 | "github.com/cucumber/godog/colors"
13 | "github.com/cucumber/godog/internal/builder"
14 | "github.com/cucumber/godog/internal/flags"
15 | )
16 |
17 | var opts flags.Options
18 |
19 | // CreateRunCmd creates the run subcommand.
20 | func CreateRunCmd() cobra.Command {
21 | runCmd := cobra.Command{
22 | Use: "run [features]",
23 | Short: "Compiles and runs a test runner",
24 | Long: `Compiles and runs test runner for the given feature files.
25 | Command should be run from the directory of tested package and contain
26 | buildable go source.`,
27 | Example: ` godog run
28 | godog run
29 | godog run
30 |
31 | Optional feature(s) to run:
32 | dir (features/)
33 | feature (*.feature)
34 | scenario at specific line (*.feature:10)
35 | If no feature arguments are supplied, godog will use "features/" by default.`,
36 | RunE: runCmdRunFunc,
37 | SilenceUsage: true,
38 | }
39 |
40 | flags.BindRunCmdFlags("", runCmd.Flags(), &opts)
41 |
42 | return runCmd
43 | }
44 |
45 | func runCmdRunFunc(cmd *cobra.Command, args []string) error {
46 | fmt.Println(colors.Yellow("Use of godog CLI is deprecated, please use *testing.T instead."))
47 | fmt.Println(colors.Yellow("See https://github.com/cucumber/godog/discussions/478 for details."))
48 |
49 | osArgs := os.Args[1:]
50 |
51 | if len(osArgs) > 0 && osArgs[0] == "run" {
52 | osArgs = osArgs[1:]
53 | }
54 |
55 | if err := buildAndRunGodog(osArgs); err != nil {
56 | return err
57 | }
58 |
59 | return nil
60 | }
61 |
62 | func buildAndRunGodog(args []string) (err error) {
63 | bin, err := filepath.Abs(buildOutputDefault)
64 | if err != nil {
65 | return err
66 | }
67 |
68 | if err = builder.Build(bin); err != nil {
69 | return err
70 | }
71 |
72 | defer os.Remove(bin)
73 |
74 | return runGodog(bin, args)
75 | }
76 |
77 | func runGodog(bin string, args []string) (err error) {
78 | cmd := exec.Command(bin, args...)
79 | cmd.Stdout = os.Stdout
80 | cmd.Stderr = os.Stderr
81 | cmd.Stdin = os.Stdin
82 | cmd.Env = os.Environ()
83 |
84 | if err = cmd.Start(); err != nil {
85 | return err
86 | }
87 |
88 | if err = cmd.Wait(); err == nil {
89 | return nil
90 | }
91 |
92 | exiterr, ok := err.(*exec.ExitError)
93 | if !ok {
94 | return err
95 | }
96 |
97 | st, ok := exiterr.Sys().(syscall.WaitStatus)
98 | if !ok {
99 | return fmt.Errorf("failed to convert error to syscall wait status. original error: %w", exiterr)
100 | }
101 |
102 | // This works on both Unix and Windows. Although package
103 | // syscall is generally platform dependent, WaitStatus is
104 | // defined for both Unix and Windows and in both cases has
105 | // an ExitStatus() method with the same signature.
106 | if st.ExitStatus() > 0 {
107 | return err
108 | }
109 |
110 | return nil
111 | }
112 |
--------------------------------------------------------------------------------
/internal/formatters/formatter-tests/pretty/rules_with_examples_with_backgrounds:
--------------------------------------------------------------------------------
1 | Feature: rules with examples with backgrounds
2 |
3 | Background: for first rule
4 | Given passing step # fmt_output_test.go:101 -> github.com/cucumber/godog/internal/formatters_test.passingStepDef
5 | And passing step # fmt_output_test.go:101 -> github.com/cucumber/godog/internal/formatters_test.passingStepDef
6 |
7 | Example: rule 1 example 1 # formatter-tests/features/rules_with_examples_with_backgrounds.feature:9
8 | When passing step # fmt_output_test.go:101 -> github.com/cucumber/godog/internal/formatters_test.passingStepDef
9 | Then passing step # fmt_output_test.go:101 -> github.com/cucumber/godog/internal/formatters_test.passingStepDef
10 |
11 | Example: rule 1 example 2 # formatter-tests/features/rules_with_examples_with_backgrounds.feature:13
12 | When passing step # fmt_output_test.go:101 -> github.com/cucumber/godog/internal/formatters_test.passingStepDef
13 | Then passing step # fmt_output_test.go:101 -> github.com/cucumber/godog/internal/formatters_test.passingStepDef
14 |
15 | Background: for second rule
16 | Given passing step # fmt_output_test.go:101 -> github.com/cucumber/godog/internal/formatters_test.passingStepDef
17 | And passing step # fmt_output_test.go:101 -> github.com/cucumber/godog/internal/formatters_test.passingStepDef
18 |
19 | Example: rule 1 example 1 # formatter-tests/features/rules_with_examples_with_backgrounds.feature:24
20 | When passing step # fmt_output_test.go:101 -> github.com/cucumber/godog/internal/formatters_test.passingStepDef
21 | Then passing step # fmt_output_test.go:101 -> github.com/cucumber/godog/internal/formatters_test.passingStepDef
22 |
23 | Example: rule 2 example 2 # formatter-tests/features/rules_with_examples_with_backgrounds.feature:28
24 | When passing step # fmt_output_test.go:101 -> github.com/cucumber/godog/internal/formatters_test.passingStepDef
25 | Then passing step # fmt_output_test.go:101 -> github.com/cucumber/godog/internal/formatters_test.passingStepDef
26 |
27 | 4 scenarios (4 passed)
28 | 16 steps (16 passed)
29 | 0s
30 |
--------------------------------------------------------------------------------
/internal/formatters/formatter-tests/pretty/some_scenarios_including_failing:
--------------------------------------------------------------------------------
1 | Feature: some scenarios
2 |
3 | Scenario: failing # formatter-tests/features/some_scenarios_including_failing.feature:3
4 | Given passing step # fmt_output_test.go:101 -> github.com/cucumber/godog/internal/formatters_test.passingStepDef
5 | When failing step # fmt_output_test.go:117 -> github.com/cucumber/godog/internal/formatters_test.failingStepDef
6 | step failed
7 | Then passing step # fmt_output_test.go:101 -> github.com/cucumber/godog/internal/formatters_test.passingStepDef
8 |
9 | Scenario: pending # formatter-tests/features/some_scenarios_including_failing.feature:8
10 | When pending step # fmt_output_test.go:115 -> github.com/cucumber/godog/internal/formatters_test.pendingStepDef
11 | TODO: write pending definition
12 | Then passing step # fmt_output_test.go:101 -> github.com/cucumber/godog/internal/formatters_test.passingStepDef
13 |
14 | Scenario: undefined # formatter-tests/features/some_scenarios_including_failing.feature:12
15 | When undefined
16 | Then passing step # fmt_output_test.go:101 -> github.com/cucumber/godog/internal/formatters_test.passingStepDef
17 |
18 | Scenario: ambiguous # formatter-tests/features/some_scenarios_including_failing.feature:16
19 | When ambiguous step
20 | ambiguous step definition, step text: ambiguous step
21 | matches:
22 | ^ambiguous step.*$
23 | ^ambiguous step$
24 | Then passing step # fmt_output_test.go:XXX -> github.com/cucumber/godog/internal/formatters_test.passingStepDef
25 |
26 | --- Failed steps:
27 |
28 | Scenario: failing # formatter-tests/features/some_scenarios_including_failing.feature:3
29 | When failing step # formatter-tests/features/some_scenarios_including_failing.feature:5
30 | Error: step failed
31 |
32 |
33 | 4 scenarios (1 failed, 1 pending, 1 ambiguous, 1 undefined)
34 | 9 steps (1 passed, 1 failed, 1 pending, 1 ambiguous, 1 undefined, 4 skipped)
35 | 0s
36 |
37 | You can implement step definitions for undefined steps with these snippets:
38 |
39 | func undefined() error {
40 | return godog.ErrPending
41 | }
42 |
43 | func InitializeScenario(ctx *godog.ScenarioContext) {
44 | ctx.Step(`^undefined$`, undefined)
45 | }
46 |
47 |
--------------------------------------------------------------------------------
/_examples/attachments/attachments_test.go:
--------------------------------------------------------------------------------
1 | package attachments_test
2 |
3 | // This "demo" doesn't actually get run as a test by the build.
4 |
5 | // This "example" shows how to attach data to the cucumber reports
6 | // Run the sample with : go test -v attachments_test.go
7 | // Then review the "embeddings" within the JSON emitted on the console.
8 |
9 | import (
10 | "context"
11 | "os"
12 | "testing"
13 |
14 | "github.com/cucumber/godog"
15 | "github.com/cucumber/godog/colors"
16 | )
17 |
18 | var opts = godog.Options{
19 | Output: colors.Colored(os.Stdout),
20 | Format: "cucumber", // cucumber json format
21 | }
22 |
23 | func TestFeatures(t *testing.T) {
24 | o := opts
25 | o.TestingT = t
26 |
27 | status := godog.TestSuite{
28 | Name: "attachments",
29 | Options: &o,
30 | ScenarioInitializer: InitializeScenario,
31 | }.Run()
32 |
33 | if status == 2 {
34 | t.SkipNow()
35 | }
36 |
37 | if status != 0 {
38 | t.Fatalf("zero status code expected, %d received", status)
39 | }
40 | }
41 |
42 | func InitializeScenario(ctx *godog.ScenarioContext) {
43 |
44 | ctx.Before(func(ctx context.Context, sc *godog.Scenario) (context.Context, error) {
45 | ctx = godog.Attach(ctx,
46 | godog.Attachment{Body: []byte("BeforeScenarioAttachment"), FileName: "Step Attachment 1", MediaType: "text/plain"},
47 | )
48 | return ctx, nil
49 | })
50 | ctx.After(func(ctx context.Context, sc *godog.Scenario, err error) (context.Context, error) {
51 | ctx = godog.Attach(ctx,
52 | godog.Attachment{Body: []byte("AfterScenarioAttachment"), FileName: "Step Attachment 2", MediaType: "text/plain"},
53 | )
54 | return ctx, nil
55 | })
56 |
57 | ctx.StepContext().Before(func(ctx context.Context, st *godog.Step) (context.Context, error) {
58 | ctx = godog.Attach(ctx,
59 | godog.Attachment{Body: []byte("BeforeStepAttachment"), FileName: "Step Attachment 3", MediaType: "text/plain"},
60 | )
61 | return ctx, nil
62 | })
63 | ctx.StepContext().After(func(ctx context.Context, st *godog.Step, status godog.StepResultStatus, err error) (context.Context, error) {
64 | ctx = godog.Attach(ctx,
65 | godog.Attachment{Body: []byte("AfterStepAttachment"), FileName: "Step Attachment 4", MediaType: "text/plain"},
66 | )
67 | return ctx, nil
68 | })
69 |
70 | ctx.Step(`^I have attached two documents in sequence$`, func(ctx context.Context) (context.Context, error) {
71 | // the attached bytes will be base64 encoded by the framework and placed in the embeddings section of the cuke report
72 | ctx = godog.Attach(ctx,
73 | godog.Attachment{Body: []byte("TheData1"), FileName: "Step Attachment 5", MediaType: "text/plain"},
74 | )
75 | ctx = godog.Attach(ctx,
76 | godog.Attachment{Body: []byte("{ \"a\" : 1 }"), FileName: "Step Attachment 6", MediaType: "application/json"},
77 | )
78 |
79 | return ctx, nil
80 | })
81 | ctx.Step(`^I have attached two documents at once$`, func(ctx context.Context) (context.Context, error) {
82 | ctx = godog.Attach(ctx,
83 | godog.Attachment{Body: []byte("TheData1"), FileName: "Step Attachment 7", MediaType: "text/plain"},
84 | godog.Attachment{Body: []byte("TheData2"), FileName: "Step Attachment 8", MediaType: "text/plain"},
85 | )
86 |
87 | return ctx, nil
88 | })
89 | }
90 |
--------------------------------------------------------------------------------
/internal/formatters/fmt.go:
--------------------------------------------------------------------------------
1 | package formatters
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "path/filepath"
7 | "regexp"
8 | "runtime"
9 | "strconv"
10 | "strings"
11 |
12 | messages "github.com/cucumber/messages/go/v21"
13 |
14 | "github.com/cucumber/godog/colors"
15 | "github.com/cucumber/godog/internal/models"
16 | "github.com/cucumber/godog/internal/utils"
17 | )
18 |
19 | var (
20 | red = colors.Red
21 | redb = colors.Bold(colors.Red)
22 | green = colors.Green
23 | blackb = colors.Bold(colors.Black)
24 | yellow = colors.Yellow
25 | cyan = colors.Cyan
26 | cyanb = colors.Bold(colors.Cyan)
27 | whiteb = colors.Bold(colors.White)
28 | )
29 |
30 | // repeats a space n times
31 | var s = utils.S
32 |
33 | var (
34 | passed = models.Passed
35 | failed = models.Failed
36 | skipped = models.Skipped
37 | undefined = models.Undefined
38 | pending = models.Pending
39 | ambiguous = models.Ambiguous
40 | )
41 |
42 | type sortFeaturesByName []*models.Feature
43 |
44 | func (s sortFeaturesByName) Len() int { return len(s) }
45 | func (s sortFeaturesByName) Less(i, j int) bool { return s[i].Feature.Name < s[j].Feature.Name }
46 | func (s sortFeaturesByName) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
47 |
48 | type sortPicklesByID []*messages.Pickle
49 |
50 | func (s sortPicklesByID) Len() int { return len(s) }
51 | func (s sortPicklesByID) Less(i, j int) bool {
52 | iID := mustConvertStringToInt(s[i].Id)
53 | jID := mustConvertStringToInt(s[j].Id)
54 | return iID < jID
55 | }
56 | func (s sortPicklesByID) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
57 |
58 | type sortPickleStepResultsByPickleStepID []models.PickleStepResult
59 |
60 | func (s sortPickleStepResultsByPickleStepID) Len() int { return len(s) }
61 | func (s sortPickleStepResultsByPickleStepID) Less(i, j int) bool {
62 | iID := mustConvertStringToInt(s[i].PickleStepID)
63 | jID := mustConvertStringToInt(s[j].PickleStepID)
64 | return iID < jID
65 | }
66 | func (s sortPickleStepResultsByPickleStepID) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
67 |
68 | func mustConvertStringToInt(s string) int {
69 | i, err := strconv.Atoi(s)
70 | if err != nil {
71 | panic(err)
72 | }
73 |
74 | return i
75 | }
76 |
77 | // DefinitionID ...
78 | func DefinitionID(sd *models.StepDefinition) string {
79 | ptr := sd.HandlerValue.Pointer()
80 | f := runtime.FuncForPC(ptr)
81 | dir := filepath.Dir(sd.File)
82 | fn := strings.Replace(f.Name(), dir, "", -1)
83 | var parts []string
84 | for _, gr := range matchFuncDefRef.FindAllStringSubmatch(fn, -1) {
85 | parts = append(parts, strings.Trim(gr[1], "_."))
86 | }
87 | if len(parts) > 0 {
88 | // case when suite is a structure with methods
89 | fn = strings.Join(parts, ".")
90 | } else {
91 | // case when steps are just plain funcs
92 | fn = strings.Trim(fn, "_.")
93 | }
94 |
95 | if pkg := os.Getenv("GODOG_TESTED_PACKAGE"); len(pkg) > 0 {
96 | fn = strings.Replace(fn, pkg, "", 1)
97 | fn = strings.TrimLeft(fn, ".")
98 | fn = strings.Replace(fn, "..", ".", -1)
99 | }
100 |
101 | return fmt.Sprintf("%s:%d -> %s", filepath.Base(sd.File), sd.Line, fn)
102 | }
103 |
104 | var matchFuncDefRef = regexp.MustCompile(`\(([^\)]+)\)`)
105 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: test gherkin bump cover
2 |
3 | VERS ?= $(shell git symbolic-ref -q --short HEAD || git describe --tags --exact-match)
4 |
5 | GO_MAJOR_VERSION = $(shell go version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f1)
6 | GO_MINOR_VERSION = $(shell go version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f2)
7 | MINIMUM_SUPPORTED_GO_MAJOR_VERSION = 1
8 | MINIMUM_SUPPORTED_GO_MINOR_VERSION = 16
9 | GO_VERSION_VALIDATION_ERR_MSG = Go version $(GO_MAJOR_VERSION).$(GO_MINOR_VERSION) is not supported, please update to at least $(MINIMUM_SUPPORTED_GO_MAJOR_VERSION).$(MINIMUM_SUPPORTED_GO_MINOR_VERSION)
10 |
11 | .PHONY: check-go-version
12 | check-go-version:
13 | @if [ $(GO_MAJOR_VERSION) -gt $(MINIMUM_SUPPORTED_GO_MAJOR_VERSION) ]; then \
14 | exit 0 ;\
15 | elif [ $(GO_MAJOR_VERSION) -lt $(MINIMUM_SUPPORTED_GO_MAJOR_VERSION) ]; then \
16 | echo '$(GO_VERSION_VALIDATION_ERR_MSG)';\
17 | exit 1; \
18 | elif [ $(GO_MINOR_VERSION) -lt $(MINIMUM_SUPPORTED_GO_MINOR_VERSION) ] ; then \
19 | echo '$(GO_VERSION_VALIDATION_ERR_MSG)';\
20 | exit 1; \
21 | fi
22 |
23 | test: check-go-version
24 | @echo "running all tests"
25 | @go fmt ./...
26 | @go run honnef.co/go/tools/cmd/staticcheck@v0.5.1 github.com/cucumber/godog
27 | @go run honnef.co/go/tools/cmd/staticcheck@v0.5.1 github.com/cucumber/godog/cmd/godog
28 | go vet ./...
29 | go test -race ./...
30 | go run ./cmd/godog -f progress -c 4
31 |
32 | gherkin:
33 | @if [ -z "$(VERS)" ]; then echo "Provide gherkin version like: 'VERS=commit-hash'"; exit 1; fi
34 | @rm -rf gherkin
35 | @mkdir gherkin
36 | @curl -s -L https://github.com/cucumber/gherkin-go/tarball/$(VERS) | tar -C gherkin -zx --strip-components 1
37 | @rm -rf gherkin/{.travis.yml,.gitignore,*_test.go,gherkin-generate*,*.razor,*.jq,Makefile,CONTRIBUTING.md}
38 |
39 | bump:
40 | @if [ -z "$(VERSION)" ]; then echo "Provide version like: 'VERSION=$(VERS) make bump'"; exit 1; fi
41 | @echo "bumping version from: $(VERS) to $(VERSION)"
42 | @sed -i.bak 's/$(VERS)/$(VERSION)/g' godog.go
43 | @sed -i.bak 's/$(VERS)/$(VERSION)/g' _examples/api/features/version.feature
44 | @find . -name '*.bak' | xargs rm
45 |
46 | cover:
47 | go test -race -coverprofile=coverage.txt
48 | go tool cover -html=coverage.txt
49 | rm coverage.txt
50 |
51 | ARTIFACT_DIR := _artifacts
52 |
53 | # To upload artifacts for the current version;
54 | # execute: make upload
55 | #
56 | # Check https://github.com/tcnksm/ghr for usage of ghr
57 | upload: artifacts
58 | ghr -replace $(VERS) $(ARTIFACT_DIR)
59 |
60 | # To build artifacts for the current version;
61 | # execute: make artifacts
62 | artifacts:
63 | rm -rf $(ARTIFACT_DIR)
64 | mkdir $(ARTIFACT_DIR)
65 |
66 | $(call _build,darwin,amd64)
67 | $(call _build,linux,amd64)
68 | $(call _build,linux,arm64)
69 |
70 | define _build
71 | mkdir $(ARTIFACT_DIR)/godog-$(VERS)-$1-$2
72 | env GOOS=$1 GOARCH=$2 go build -ldflags "-X github.com/cucumber/godog.Version=$(VERS)" -o $(ARTIFACT_DIR)/godog-$(VERS)-$1-$2/godog ./cmd/godog
73 | cp README.md $(ARTIFACT_DIR)/godog-$(VERS)-$1-$2/README.md
74 | cp LICENSE $(ARTIFACT_DIR)/godog-$(VERS)-$1-$2/LICENSE
75 | cd $(ARTIFACT_DIR) && tar -c --use-compress-program="pigz --fast" -f godog-$(VERS)-$1-$2.tar.gz godog-$(VERS)-$1-$2 && cd ..
76 | rm -rf $(ARTIFACT_DIR)/godog-$(VERS)-$1-$2
77 | endef
78 |
--------------------------------------------------------------------------------
/formatters/fmt.go:
--------------------------------------------------------------------------------
1 | package formatters
2 |
3 | import (
4 | "io"
5 | "regexp"
6 |
7 | messages "github.com/cucumber/messages/go/v21"
8 | )
9 |
10 | type registeredFormatter struct {
11 | name string
12 | description string
13 | fmt FormatterFunc
14 | }
15 |
16 | var registeredFormatters []*registeredFormatter
17 |
18 | // FindFmt searches available formatters registered
19 | // and returns FormaterFunc matched by given
20 | // format name or nil otherwise
21 | func FindFmt(name string) FormatterFunc {
22 | for _, el := range registeredFormatters {
23 | if el.name == name {
24 | return el.fmt
25 | }
26 | }
27 |
28 | return nil
29 | }
30 |
31 | // Format registers a feature suite output
32 | // formatter by given name, description and
33 | // FormatterFunc constructor function, to initialize
34 | // formatter with the output recorder.
35 | func Format(name, description string, f FormatterFunc) {
36 | registeredFormatters = append(registeredFormatters, ®isteredFormatter{
37 | name: name,
38 | fmt: f,
39 | description: description,
40 | })
41 | }
42 |
43 | // AvailableFormatters gives a map of all
44 | // formatters registered with their name as key
45 | // and description as value
46 | func AvailableFormatters() map[string]string {
47 | fmts := make(map[string]string, len(registeredFormatters))
48 |
49 | for _, f := range registeredFormatters {
50 | fmts[f.name] = f.description
51 | }
52 |
53 | return fmts
54 | }
55 |
56 | // Formatter is an interface for feature runner
57 | // output summary presentation.
58 | //
59 | // New formatters may be created to represent
60 | // suite results in different ways. These new
61 | // formatters needs to be registered with a
62 | // godog.Format function call
63 | type Formatter interface {
64 | TestRunStarted()
65 | Feature(*messages.GherkinDocument, string, []byte)
66 | Pickle(*messages.Pickle)
67 | Defined(*messages.Pickle, *messages.PickleStep, *StepDefinition)
68 | Failed(*messages.Pickle, *messages.PickleStep, *StepDefinition, error)
69 | Passed(*messages.Pickle, *messages.PickleStep, *StepDefinition)
70 | Skipped(*messages.Pickle, *messages.PickleStep, *StepDefinition)
71 | Undefined(*messages.Pickle, *messages.PickleStep, *StepDefinition)
72 | Pending(*messages.Pickle, *messages.PickleStep, *StepDefinition)
73 | Ambiguous(*messages.Pickle, *messages.PickleStep, *StepDefinition, error)
74 | Summary()
75 | }
76 |
77 | // FlushFormatter is a `Formatter` but can be flushed.
78 | type FlushFormatter interface {
79 | Formatter
80 | Flush()
81 | }
82 |
83 | // FormatterFunc builds a formatter with given
84 | // suite name and io.Writer to record output
85 | type FormatterFunc func(string, io.Writer) Formatter
86 |
87 | // StepDefinition is a registered step definition
88 | // contains a StepHandler and regexp which
89 | // is used to match a step. Args which
90 | // were matched by last executed step
91 | //
92 | // This structure is passed to the formatter
93 | // when step is matched and is either failed
94 | // or successful
95 | type StepDefinition struct {
96 | Expr *regexp.Regexp
97 | Handler interface{}
98 | Keyword Keyword
99 | }
100 |
101 | type Keyword int64
102 |
103 | const (
104 | Given Keyword = iota
105 | When
106 | Then
107 | None
108 | )
109 |
--------------------------------------------------------------------------------
/internal/testutils/utils.go:
--------------------------------------------------------------------------------
1 | package testutils
2 |
3 | import (
4 | "strings"
5 | "testing"
6 |
7 | gherkin "github.com/cucumber/gherkin/go/v26"
8 | messages "github.com/cucumber/messages/go/v21"
9 | "github.com/stretchr/testify/require"
10 |
11 | "github.com/cucumber/godog/internal/models"
12 | )
13 |
14 | // BuildTestFeature creates a feature for testing purpose.
15 | //
16 | // The created feature includes:
17 | // - a background
18 | // - one normal scenario with three steps
19 | // - one outline scenario with one example and three steps
20 | func BuildTestFeature(t *testing.T) models.Feature {
21 | newIDFunc := (&messages.Incrementing{}).NewId
22 |
23 | gherkinDocument, err := gherkin.ParseGherkinDocument(strings.NewReader(featureContent), newIDFunc)
24 | require.NoError(t, err)
25 |
26 | path := t.Name()
27 | gherkinDocument.Uri = path
28 | pickles := gherkin.Pickles(*gherkinDocument, path, newIDFunc)
29 |
30 | ft := models.Feature{GherkinDocument: gherkinDocument, Pickles: pickles, Content: []byte(featureContent)}
31 | require.Len(t, ft.Pickles, 2)
32 |
33 | require.Len(t, ft.Pickles[0].AstNodeIds, 1)
34 | require.Len(t, ft.Pickles[0].Steps, 3)
35 |
36 | require.Len(t, ft.Pickles[1].AstNodeIds, 2)
37 | require.Len(t, ft.Pickles[1].Steps, 3)
38 |
39 | return ft
40 | }
41 |
42 | const featureContent = `Feature: eat godogs
43 | In order to be happy
44 | As a hungry gopher
45 | I need to be able to eat godogs
46 |
47 | Background:
48 | Given there are godogs
49 |
50 | Scenario: Eat 5 out of 12
51 | When I eat 5
52 | Then there should be 7 remaining
53 |
54 | Scenario Outline: Eat out of
55 | When I eat
56 | Then there should be remaining
57 |
58 | Examples:
59 | | begin | dec | remain |
60 | | 12 | 5 | 7 |`
61 |
62 | // BuildTestFeature creates a feature with rules for testing purpose.
63 | //
64 | // The created feature includes:
65 | // - a background
66 | // - one normal scenario with three steps
67 | // - one outline scenario with one example and three steps
68 | func BuildTestFeatureWithRules(t *testing.T) models.Feature {
69 | newIDFunc := (&messages.Incrementing{}).NewId
70 |
71 | gherkinDocument, err := gherkin.ParseGherkinDocument(strings.NewReader(featureWithRuleContent), newIDFunc)
72 | require.NoError(t, err)
73 |
74 | path := t.Name()
75 | gherkinDocument.Uri = path
76 | pickles := gherkin.Pickles(*gherkinDocument, path, newIDFunc)
77 |
78 | ft := models.Feature{GherkinDocument: gherkinDocument, Pickles: pickles, Content: []byte(featureWithRuleContent)}
79 | require.Len(t, ft.Pickles, 2)
80 |
81 | require.Len(t, ft.Pickles[0].AstNodeIds, 1)
82 | require.Len(t, ft.Pickles[0].Steps, 3)
83 |
84 | require.Len(t, ft.Pickles[1].AstNodeIds, 2)
85 | require.Len(t, ft.Pickles[1].Steps, 3)
86 |
87 | return ft
88 | }
89 |
90 | const featureWithRuleContent = `Feature: eat godogs
91 | In order to be happy
92 | As a hungry gopher
93 | I need to be able to eat godogs
94 |
95 | Rule: eating godogs
96 |
97 | Background:
98 | Given there are godogs
99 |
100 | Scenario: Eat 5 out of 12
101 | When I eat 5
102 | Then there should be 7 remaining
103 |
104 | Scenario Outline: Eat out of
105 | When I eat
106 | Then there should be remaining
107 |
108 | Examples:
109 | | begin | dec | remain |
110 | | 12 | 5 | 7 |`
111 |
--------------------------------------------------------------------------------
/internal/models/feature_test.go:
--------------------------------------------------------------------------------
1 | package models_test
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/cucumber/godog/internal/models"
7 | "github.com/cucumber/godog/internal/testutils"
8 | "github.com/stretchr/testify/assert"
9 | )
10 |
11 | func Test_Find(t *testing.T) {
12 | ft := testutils.BuildTestFeature(t)
13 |
14 | t.Run("scenario", func(t *testing.T) {
15 | sc := ft.FindScenario(ft.Pickles[0].AstNodeIds[0])
16 | assert.NotNilf(t, sc, "expected scenario to not be nil")
17 | })
18 |
19 | t.Run("background", func(t *testing.T) {
20 | bg := ft.FindBackground(ft.Pickles[0].AstNodeIds[0])
21 | assert.NotNilf(t, bg, "expected background to not be nil")
22 | })
23 |
24 | t.Run("example", func(t *testing.T) {
25 | example, row := ft.FindExample(ft.Pickles[1].AstNodeIds[1])
26 | assert.NotNilf(t, example, "expected example to not be nil")
27 | assert.NotNilf(t, row, "expected table row to not be nil")
28 | })
29 |
30 | t.Run("step", func(t *testing.T) {
31 | for _, ps := range ft.Pickles[0].Steps {
32 | step := ft.FindStep(ps.AstNodeIds[0])
33 | assert.NotNilf(t, step, "expected step to not be nil")
34 | }
35 | })
36 |
37 | t.Run("rule", func(t *testing.T) {
38 | sc := ft.FindRule(ft.Pickles[0].AstNodeIds[0])
39 | assert.Nilf(t, sc, "expected rule to be nil")
40 | })
41 | }
42 |
43 | func Test_FindInRule(t *testing.T) {
44 |
45 | ft := testutils.BuildTestFeatureWithRules(t)
46 |
47 | t.Run("rule", func(t *testing.T) {
48 | sc := ft.FindRule(ft.Pickles[0].AstNodeIds[0])
49 | assert.NotNilf(t, sc, "expected rule to not be nil")
50 | })
51 |
52 | t.Run("scenario", func(t *testing.T) {
53 | sc := ft.FindScenario(ft.Pickles[0].AstNodeIds[0])
54 | assert.NotNilf(t, sc, "expected scenario to not be nil")
55 | })
56 |
57 | t.Run("background", func(t *testing.T) {
58 | bg := ft.FindBackground(ft.Pickles[0].AstNodeIds[0])
59 | assert.NotNilf(t, bg, "expected background to not be nil")
60 | })
61 |
62 | t.Run("example", func(t *testing.T) {
63 | example, row := ft.FindExample(ft.Pickles[1].AstNodeIds[1])
64 | assert.NotNilf(t, example, "expected example to not be nil")
65 | assert.NotNilf(t, row, "expected table row to not be nil")
66 | })
67 |
68 | t.Run("step", func(t *testing.T) {
69 | for _, ps := range ft.Pickles[0].Steps {
70 | step := ft.FindStep(ps.AstNodeIds[0])
71 | assert.NotNilf(t, step, "expected step to not be nil")
72 | }
73 | })
74 | }
75 |
76 | func Test_NotFind(t *testing.T) {
77 | testCases := []struct {
78 | Feature models.Feature
79 | }{
80 | {testutils.BuildTestFeature(t)},
81 | {testutils.BuildTestFeatureWithRules(t)},
82 | }
83 |
84 | for _, tc := range testCases {
85 |
86 | ft := tc.Feature
87 | t.Run("scenario", func(t *testing.T) {
88 | sc := ft.FindScenario("-")
89 | assert.Nilf(t, sc, "expected scenario to be nil")
90 | })
91 |
92 | t.Run("background", func(t *testing.T) {
93 | bg := ft.FindBackground("-")
94 | assert.Nilf(t, bg, "expected background to be nil")
95 | })
96 |
97 | t.Run("example", func(t *testing.T) {
98 | example, row := ft.FindExample("-")
99 | assert.Nilf(t, example, "expected example to be nil")
100 | assert.Nilf(t, row, "expected table row to be nil")
101 | })
102 |
103 | t.Run("step", func(t *testing.T) {
104 | step := ft.FindStep("-")
105 | assert.Nilf(t, step, "expected step to be nil")
106 | })
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/internal/formatters/formatter-tests/junit,pretty/scenario_outline:
--------------------------------------------------------------------------------
1 | Feature: outline
2 |
3 | Scenario Outline: outline # formatter-tests/features/scenario_outline.feature:5
4 | Given passing step # fmt_output_test.go:101 -> github.com/cucumber/godog/internal/formatters_test.passingStepDef
5 | When passing step # fmt_output_test.go:101 -> github.com/cucumber/godog/internal/formatters_test.passingStepDef
6 | Then odd and even number # fmt_output_test.go:103 -> github.com/cucumber/godog/internal/formatters_test.oddEvenStepDef
7 |
8 | Examples: tagged
9 | | odd | even |
10 | | 1 | 2 |
11 | | 2 | 0 |
12 | 2 is not odd
13 | | 3 | 11 |
14 | 11 is not even
15 |
16 | Examples:
17 | | odd | even |
18 | | 1 | 14 |
19 | | 3 | 9 |
20 | 9 is not even
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | --- Failed steps:
38 |
39 | Scenario Outline: outline # formatter-tests/features/scenario_outline.feature:5
40 | Then odd 2 and even 0 number # formatter-tests/features/scenario_outline.feature:8
41 | Error: 2 is not odd
42 |
43 | Scenario Outline: outline # formatter-tests/features/scenario_outline.feature:5
44 | Then odd 3 and even 11 number # formatter-tests/features/scenario_outline.feature:8
45 | Error: 11 is not even
46 |
47 | Scenario Outline: outline # formatter-tests/features/scenario_outline.feature:5
48 | Then odd 3 and even 9 number # formatter-tests/features/scenario_outline.feature:8
49 | Error: 9 is not even
50 |
51 |
52 | 5 scenarios (2 passed, 3 failed)
53 | 15 steps (12 passed, 3 failed)
54 | 0s
55 |
--------------------------------------------------------------------------------
/internal/formatters/fmt_flushwrap.go:
--------------------------------------------------------------------------------
1 | package formatters
2 |
3 | import (
4 | "sync"
5 |
6 | "github.com/cucumber/godog/formatters"
7 | messages "github.com/cucumber/messages/go/v21"
8 | )
9 |
10 | // WrapOnFlush wrap a `formatters.Formatter` in a `formatters.FlushFormatter`, which only
11 | // executes when `Flush` is called
12 | func WrapOnFlush(fmt formatters.Formatter) formatters.FlushFormatter {
13 | return &onFlushFormatter{
14 | fmt: fmt,
15 | fns: make([]func(), 0),
16 | mu: &sync.Mutex{},
17 | }
18 | }
19 |
20 | type onFlushFormatter struct {
21 | fmt formatters.Formatter
22 | fns []func()
23 | mu *sync.Mutex
24 | }
25 |
26 | func (o *onFlushFormatter) Pickle(pickle *messages.Pickle) {
27 | o.fns = append(o.fns, func() {
28 | o.fmt.Pickle(pickle)
29 | })
30 | }
31 |
32 | func (o *onFlushFormatter) Passed(pickle *messages.Pickle, step *messages.PickleStep, definition *formatters.StepDefinition) {
33 | o.fns = append(o.fns, func() {
34 | o.fmt.Passed(pickle, step, definition)
35 | })
36 | }
37 |
38 | // Ambiguous implements formatters.Formatter.
39 | func (o *onFlushFormatter) Ambiguous(pickle *messages.Pickle, step *messages.PickleStep, definition *formatters.StepDefinition, err error) {
40 | o.fns = append(o.fns, func() {
41 | o.fmt.Ambiguous(pickle, step, definition, err)
42 | })
43 | }
44 |
45 | // Defined implements formatters.Formatter.
46 | func (o *onFlushFormatter) Defined(pickle *messages.Pickle, step *messages.PickleStep, definition *formatters.StepDefinition) {
47 | o.fns = append(o.fns, func() {
48 | o.fmt.Defined(pickle, step, definition)
49 | })
50 | }
51 |
52 | // Failed implements formatters.Formatter.
53 | func (o *onFlushFormatter) Failed(pickle *messages.Pickle, step *messages.PickleStep, definition *formatters.StepDefinition, err error) {
54 | o.fns = append(o.fns, func() {
55 | o.fmt.Failed(pickle, step, definition, err)
56 | })
57 | }
58 |
59 | // Feature implements formatters.Formatter.
60 | func (o *onFlushFormatter) Feature(pickle *messages.GherkinDocument, p string, c []byte) {
61 | o.fns = append(o.fns, func() {
62 | o.fmt.Feature(pickle, p, c)
63 | })
64 | }
65 |
66 | // Pending implements formatters.Formatter.
67 | func (o *onFlushFormatter) Pending(pickle *messages.Pickle, step *messages.PickleStep, definition *formatters.StepDefinition) {
68 | o.fns = append(o.fns, func() {
69 | o.fmt.Pending(pickle, step, definition)
70 | })
71 | }
72 |
73 | // Skipped implements formatters.Formatter.
74 | func (o *onFlushFormatter) Skipped(pickle *messages.Pickle, step *messages.PickleStep, definition *formatters.StepDefinition) {
75 | o.fns = append(o.fns, func() {
76 | o.fmt.Skipped(pickle, step, definition)
77 | })
78 | }
79 |
80 | // Summary implements formatters.Formatter.
81 | func (o *onFlushFormatter) Summary() {
82 | o.fns = append(o.fns, func() {
83 | o.fmt.Summary()
84 | })
85 | }
86 |
87 | // TestRunStarted implements formatters.Formatter.
88 | func (o *onFlushFormatter) TestRunStarted() {
89 | o.fns = append(o.fns, func() {
90 | o.fmt.TestRunStarted()
91 | })
92 | }
93 |
94 | // Undefined implements formatters.Formatter.
95 | func (o *onFlushFormatter) Undefined(pickle *messages.Pickle, step *messages.PickleStep, definition *formatters.StepDefinition) {
96 | o.fns = append(o.fns, func() {
97 | o.fmt.Undefined(pickle, step, definition)
98 | })
99 | }
100 |
101 | // Flush the logs.
102 | func (o *onFlushFormatter) Flush() {
103 | o.mu.Lock()
104 | defer o.mu.Unlock()
105 | for _, fn := range o.fns {
106 | fn()
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/release-notes/v0.11.0.md:
--------------------------------------------------------------------------------
1 | We are excited to announce the release of godog v0.11.0.
2 |
3 | Here follows a summary of Notable Changes, the Non Backward Compatible Changes and Deprecation Notices.
4 | The full change log is available [here](https://github.com/cucumber/godog/blob/master/CHANGELOG.md#v0110-rc1).
5 |
6 |
7 | Notable Changes
8 | ---------------
9 |
10 | ### Write test report to file
11 | godog is now able to write the report to a file.
12 |
13 | - `--format cucumber` will continue to write the report to `stdout`
14 |
15 | - `--format cucumber:report.json` will instead write the report to a file named `report.json`
16 |
17 | **Note**, godog still only support the use of one formatter.
18 |
19 | ### Executing godog from the Command Line
20 | godog is now using [Cobra](https://pkg.go.dev/github.com/spf13/cobra) to run godog from the command line. With this update, godog has received sub-commands: (build, help, run, version)
21 |
22 | To run tests with godog, `godog []` has been replaced with `godog run []`.
23 |
24 | To build a test binary, `godog --output g.test []`has been replaced with `godog build --output g.test []`.
25 |
26 | ### Upload artifacts to the github release
27 | The releases on github now include prebuilt binaries for:
28 | - Linux for amd64 and arm64
29 | - macOs (Darwin) for amd64
30 |
31 | ### Restructure of the codebase with internal packages
32 | A lot of the internal code that used to be in the main godog package has been moved to internal packages.
33 |
34 | The reason for this is mainly for decoupling to allow for simpler tests and to make the codebase easier to work with in general.
35 |
36 | ### Added official support for go1.15 and removed support for go1.12
37 | With the introduction of go1.15, go1.15 is now officially supported and go1.12 has been removed, this is since godog supports the 3 latest versions of golang.
38 |
39 | Non Backward Compatible Changes
40 | -------------------------------
41 |
42 | ### Concurrency Formatter
43 | `ConcurrencyFormatter` is now removed since the deprecation in [v0.10.0](./v0.10.0.md).
44 |
45 | ### Run and RunWithOptions
46 | `Run` and `RunWithOptions` are now removed since the deprecation in [v0.10.0](./v0.10.0.md).
47 |
48 | ### Suite and SuiteContext
49 | `Suite` and `SuiteContext` are now removed since the deprecation in [v0.10.0](./v0.10.0.md).
50 |
51 | Deprecation Notices
52 | -------------------
53 |
54 | ### BindFlags
55 | `BindFlags(prefix, flag.CommandLine, &opts)` has been replaced by `BindCommandLineFlags(prefix, &opts)` and will be removed in `v0.12.0`.
56 |
57 | Using `BindCommandLineFlags(prefix, &opts)` also requires you to use `"github.com/spf13/pflag"` to parse the flags.
58 | ```go
59 | package main
60 |
61 | import (
62 | "fmt"
63 | "os"
64 | "testing"
65 |
66 | "github.com/cucumber/godog"
67 | "github.com/cucumber/godog/colors"
68 | "github.com/spf13/pflag"
69 | )
70 |
71 | var opts = godog.Options{Output: colors.Colored(os.Stdout)}
72 |
73 | func init() {
74 | godog.BindCommandLineFlags("godog.", &opts)
75 | }
76 |
77 | func TestMain(m *testing.M) {
78 | pflag.Parse()
79 | opts.Paths = pflag.Args()
80 |
81 | // ...
82 | ```
83 |
84 | ### Executing the godog CLI
85 | godog has received sub-commands: (build, help, run, version)
86 |
87 | To run tests with godog, `godog []` has been replaced with `godog run []`.
88 |
89 | To build a test binary, `godog --output g.test []`has been replaced with `godog build --output g.test []`.
90 |
91 | Full change log
92 | ---------------
93 |
94 | See [CHANGELOG.md](https://github.com/cucumber/godog/blob/master/CHANGELOG.md#v0110-rc1).
95 |
--------------------------------------------------------------------------------
/stacktrace.go:
--------------------------------------------------------------------------------
1 | package godog
2 |
3 | import (
4 | "fmt"
5 | "go/build"
6 | "io"
7 | "path"
8 | "path/filepath"
9 | "runtime"
10 | "strings"
11 | )
12 |
13 | // Frame represents a program counter inside a stack frame.
14 | type stackFrame uintptr
15 |
16 | // pc returns the program counter for this frame;
17 | // multiple frames may have the same PC value.
18 | func (f stackFrame) pc() uintptr { return uintptr(f) - 1 }
19 |
20 | // file returns the full path to the file that contains the
21 | // function for this Frame's pc.
22 | func (f stackFrame) file() string {
23 | fn := runtime.FuncForPC(f.pc())
24 | if fn == nil {
25 | return "unknown"
26 | }
27 | file, _ := fn.FileLine(f.pc())
28 | return file
29 | }
30 |
31 | func trimGoPath(file string) string {
32 | for _, p := range filepath.SplitList(build.Default.GOPATH) {
33 | file = strings.Replace(file, filepath.Join(p, "src")+string(filepath.Separator), "", 1)
34 | }
35 | return file
36 | }
37 |
38 | // line returns the line number of source code of the
39 | // function for this Frame's pc.
40 | func (f stackFrame) line() int {
41 | fn := runtime.FuncForPC(f.pc())
42 | if fn == nil {
43 | return 0
44 | }
45 | _, line := fn.FileLine(f.pc())
46 | return line
47 | }
48 |
49 | // Format formats the frame according to the fmt.Formatter interface.
50 | //
51 | // %s source file
52 | // %d source line
53 | // %n function name
54 | // %v equivalent to %s:%d
55 | //
56 | // Format accepts flags that alter the printing of some verbs, as follows:
57 | //
58 | // %+s path of source file relative to the compile time GOPATH
59 | // %+v equivalent to %+s:%d
60 | func (f stackFrame) Format(s fmt.State, verb rune) {
61 | funcname := func(name string) string {
62 | i := strings.LastIndex(name, "/")
63 | name = name[i+1:]
64 | i = strings.Index(name, ".")
65 | return name[i+1:]
66 | }
67 |
68 | switch verb {
69 | case 's':
70 | switch {
71 | case s.Flag('+'):
72 | pc := f.pc()
73 | fn := runtime.FuncForPC(pc)
74 | if fn == nil {
75 | io.WriteString(s, "unknown")
76 | } else {
77 | file, _ := fn.FileLine(pc)
78 | fmt.Fprintf(s, "%s\n\t%s", fn.Name(), trimGoPath(file))
79 | }
80 | default:
81 | io.WriteString(s, path.Base(f.file()))
82 | }
83 | case 'd':
84 | fmt.Fprintf(s, "%d", f.line())
85 | case 'n':
86 | name := runtime.FuncForPC(f.pc()).Name()
87 | io.WriteString(s, funcname(name))
88 | case 'v':
89 | f.Format(s, 's')
90 | io.WriteString(s, ":")
91 | f.Format(s, 'd')
92 | }
93 | }
94 |
95 | // stack represents a stack of program counters.
96 | type stack []uintptr
97 |
98 | func (s *stack) Format(st fmt.State, verb rune) {
99 | switch verb {
100 | case 'v':
101 | switch {
102 | case st.Flag('+'):
103 | for _, pc := range *s {
104 | f := stackFrame(pc)
105 | fmt.Fprintf(st, "\n%+v", f)
106 | }
107 | }
108 | }
109 | }
110 |
111 | func callStack() *stack {
112 | const depth = 32
113 | var pcs [depth]uintptr
114 | n := runtime.Callers(3, pcs[:])
115 | var st stack = pcs[0:n]
116 | return &st
117 | }
118 |
119 | // fundamental is an error that has a message and a stack, but no caller.
120 | type traceError struct {
121 | msg string
122 | *stack
123 | }
124 |
125 | func (f *traceError) Error() string { return f.msg }
126 |
127 | func (f *traceError) Format(s fmt.State, verb rune) {
128 | switch verb {
129 | case 'v':
130 | if s.Flag('+') {
131 | io.WriteString(s, f.msg)
132 | f.stack.Format(s, verb)
133 | return
134 | }
135 | fallthrough
136 | case 's':
137 | io.WriteString(s, f.msg)
138 | case 'q':
139 | fmt.Fprintf(s, "%q", f.msg)
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/_examples/custom-formatter/emoji.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "io"
6 | "math"
7 |
8 | "github.com/cucumber/godog"
9 | )
10 |
11 | const (
12 | passedEmoji = "✅"
13 | skippedEmoji = "➖"
14 | failedEmoji = "❌"
15 | undefinedEmoji = "❓"
16 | pendingEmoji = "🚧"
17 | )
18 |
19 | func init() {
20 | godog.Format("emoji", "Progress formatter with emojis", emojiFormatterFunc)
21 | }
22 |
23 | func emojiFormatterFunc(suite string, out io.Writer) godog.Formatter {
24 | return newEmojiFmt(suite, out)
25 | }
26 |
27 | func newEmojiFmt(suite string, out io.Writer) *emojiFmt {
28 | return &emojiFmt{
29 | ProgressFmt: godog.NewProgressFmt(suite, out),
30 | out: out,
31 | }
32 | }
33 |
34 | type emojiFmt struct {
35 | *godog.ProgressFmt
36 |
37 | out io.Writer
38 | }
39 |
40 | func (f *emojiFmt) TestRunStarted() {}
41 |
42 | func (f *emojiFmt) Passed(scenario *godog.Scenario, step *godog.Step, match *godog.StepDefinition) {
43 | f.ProgressFmt.Base.Passed(scenario, step, match)
44 |
45 | f.ProgressFmt.Base.Lock.Lock()
46 | defer f.ProgressFmt.Base.Lock.Unlock()
47 |
48 | f.step(step.Id)
49 | }
50 |
51 | func (f *emojiFmt) Skipped(scenario *godog.Scenario, step *godog.Step, match *godog.StepDefinition) {
52 | f.ProgressFmt.Base.Skipped(scenario, step, match)
53 |
54 | f.ProgressFmt.Base.Lock.Lock()
55 | defer f.ProgressFmt.Base.Lock.Unlock()
56 |
57 | f.step(step.Id)
58 | }
59 |
60 | func (f *emojiFmt) Undefined(scenario *godog.Scenario, step *godog.Step, match *godog.StepDefinition) {
61 | f.ProgressFmt.Base.Undefined(scenario, step, match)
62 |
63 | f.ProgressFmt.Base.Lock.Lock()
64 | defer f.ProgressFmt.Base.Lock.Unlock()
65 |
66 | f.step(step.Id)
67 | }
68 |
69 | func (f *emojiFmt) Failed(scenario *godog.Scenario, step *godog.Step, match *godog.StepDefinition, err error) {
70 | f.ProgressFmt.Base.Failed(scenario, step, match, err)
71 |
72 | f.ProgressFmt.Base.Lock.Lock()
73 | defer f.ProgressFmt.Base.Lock.Unlock()
74 |
75 | f.step(step.Id)
76 | }
77 |
78 | func (f *emojiFmt) Pending(scenario *godog.Scenario, step *godog.Step, match *godog.StepDefinition) {
79 | f.ProgressFmt.Base.Pending(scenario, step, match)
80 |
81 | f.ProgressFmt.Base.Lock.Lock()
82 | defer f.ProgressFmt.Base.Lock.Unlock()
83 |
84 | f.step(step.Id)
85 | }
86 |
87 | func (f *emojiFmt) Summary() {
88 | f.printSummaryLegend()
89 | f.ProgressFmt.Summary()
90 | }
91 |
92 | func (f *emojiFmt) printSummaryLegend() {
93 | fmt.Fprint(f.out, "\n\nOutput Legend:\n")
94 | fmt.Fprint(f.out, fmt.Sprintf("\t%s Passed\n", passedEmoji))
95 | fmt.Fprint(f.out, fmt.Sprintf("\t%s Failed\n", failedEmoji))
96 | fmt.Fprint(f.out, fmt.Sprintf("\t%s Skipped\n", skippedEmoji))
97 | fmt.Fprint(f.out, fmt.Sprintf("\t%s Undefined\n", undefinedEmoji))
98 | fmt.Fprint(f.out, fmt.Sprintf("\t%s Pending\n", pendingEmoji))
99 | }
100 |
101 | func (f *emojiFmt) step(pickleStepID string) {
102 | pickleStepResult := f.Storage.MustGetPickleStepResult(pickleStepID)
103 |
104 | switch pickleStepResult.Status {
105 | case godog.StepPassed:
106 | fmt.Fprint(f.out, fmt.Sprintf(" %s", passedEmoji))
107 | case godog.StepSkipped:
108 | fmt.Fprint(f.out, fmt.Sprintf(" %s", skippedEmoji))
109 | case godog.StepFailed:
110 | fmt.Fprint(f.out, fmt.Sprintf(" %s", failedEmoji))
111 | case godog.StepUndefined:
112 | fmt.Fprint(f.out, fmt.Sprintf(" %s", undefinedEmoji))
113 | case godog.StepPending:
114 | fmt.Fprint(f.out, fmt.Sprintf(" %s", pendingEmoji))
115 | }
116 |
117 | *f.Steps++
118 |
119 | if math.Mod(float64(*f.Steps), float64(f.StepsPerRow)) == 0 {
120 | fmt.Fprintf(f.out, " %d\n", *f.Steps)
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/features/tags.feature:
--------------------------------------------------------------------------------
1 | Feature: tag filters
2 | In order to test application behavior
3 | As a test suite
4 | I need to be able to filter features and scenarios by tags
5 |
6 | Scenario: should filter outline examples by tags
7 | Given a feature "normal.feature" file:
8 | """
9 | Feature: outline
10 |
11 | Background:
12 | Given passing step
13 | And passing step without return
14 |
15 | Scenario Outline: parse a scenario
16 | Given a feature path ""
17 | When I parse features
18 | Then I should have scenario registered
19 |
20 | Examples:
21 | | path | num |
22 | | features/load.feature:3 | 0 |
23 |
24 | @used
25 | Examples:
26 | | path | num |
27 | | features/load.feature:6 | 1 |
28 | """
29 | When I run feature suite with tags "@used"
30 | Then the suite should have passed
31 | And the following steps should be passed:
32 | """
33 | I parse features
34 | a feature path "features/load.feature:6"
35 | I should have 1 scenario registered
36 | """
37 | And I should have 1 scenario registered
38 |
39 | Scenario: should filter scenarios by X tag
40 | Given a feature "normal.feature" file:
41 | """
42 | Feature: tagged
43 |
44 | @x
45 | Scenario: one
46 | Given a feature path "one"
47 |
48 | @x
49 | Scenario: two
50 | Given a feature path "two"
51 |
52 | @x @y
53 | Scenario: three
54 | Given a feature path "three"
55 |
56 | @y
57 | Scenario: four
58 | Given a feature path "four"
59 | """
60 | When I run feature suite with tags "@x"
61 | Then the suite should have passed
62 | And I should have 3 scenario registered
63 | And the following steps should be passed:
64 | """
65 | a feature path "one"
66 | a feature path "two"
67 | a feature path "three"
68 | """
69 |
70 | Scenario: should filter scenarios by X tag not having Y
71 | Given a feature "normal.feature" file:
72 | """
73 | Feature: tagged
74 |
75 | @x
76 | Scenario: one
77 | Given a feature path "one"
78 |
79 | @x
80 | Scenario: two
81 | Given a feature path "two"
82 |
83 | @x @y
84 | Scenario: three
85 | Given a feature path "three"
86 |
87 | @y @z
88 | Scenario: four
89 | Given a feature path "four"
90 | """
91 | When I run feature suite with tags "@x && ~@y"
92 | Then the suite should have passed
93 | And I should have 2 scenario registered
94 | And the following steps should be passed:
95 | """
96 | a feature path "one"
97 | a feature path "two"
98 | """
99 |
100 | Scenario: should filter scenarios having Y and Z tags
101 | Given a feature "normal.feature" file:
102 | """
103 | Feature: tagged
104 |
105 | @x
106 | Scenario: one
107 | Given a feature path "one"
108 |
109 | @x
110 | Scenario: two
111 | Given a feature path "two"
112 |
113 | @x @y
114 | Scenario: three
115 | Given a feature path "three"
116 |
117 | @y @z
118 | Scenario: four
119 | Given a feature path "four"
120 | """
121 | When I run feature suite with tags "@y && @z"
122 | Then the suite should have passed
123 | And I should have 1 scenario registered
124 | And the following steps should be passed:
125 | """
126 | a feature path "four"
127 | """
128 |
--------------------------------------------------------------------------------
/_examples/godogs/godogs_test.go:
--------------------------------------------------------------------------------
1 | package godogs_test
2 |
3 | // This example shows how to set up test suite runner with Go subtests and godog command line parameters.
4 | // Sample commands:
5 | // * run all scenarios from default directory (features): go test -test.run "^TestFeatures/"
6 | // * run all scenarios and list subtest names: go test -test.v -test.run "^TestFeatures/"
7 | // * run all scenarios from one feature file: go test -test.v -godog.paths features/nodogs.feature -test.run "^TestFeatures/"
8 | // * run all scenarios from multiple feature files: go test -test.v -godog.paths features/nodogs.feature,features/godogs.feature -test.run "^TestFeatures/"
9 | // * run single scenario as a subtest: go test -test.v -test.run "^TestFeatures/Eat_5_out_of_12$"
10 | // * show usage help: go test -godog.help
11 | // * show usage help if there were other test files in directory: go test -godog.help godogs_test.go
12 | // * run scenarios with multiple formatters: go test -test.v -godog.format cucumber:cuc.json,pretty -test.run "^TestFeatures/"
13 |
14 | import (
15 | "context"
16 | "flag"
17 | "fmt"
18 | "github.com/cucumber/godog/_examples/godogs"
19 | "os"
20 | "testing"
21 |
22 | "github.com/cucumber/godog"
23 | "github.com/cucumber/godog/colors"
24 | )
25 |
26 | var opts = godog.Options{
27 | Output: colors.Colored(os.Stdout),
28 | Concurrency: 4,
29 | }
30 |
31 | func init() {
32 | godog.BindFlags("godog.", flag.CommandLine, &opts)
33 | }
34 |
35 | func TestFeatures(t *testing.T) {
36 | o := opts
37 | o.TestingT = t
38 |
39 | status := godog.TestSuite{
40 | Name: "godogs",
41 | Options: &o,
42 | TestSuiteInitializer: InitializeTestSuite,
43 | ScenarioInitializer: InitializeScenario,
44 | }.Run()
45 |
46 | if status == 2 {
47 | t.SkipNow()
48 | }
49 |
50 | if status != 0 {
51 | t.Fatalf("zero status code expected, %d received", status)
52 | }
53 | }
54 |
55 | type godogsCtxKey struct{}
56 |
57 | func godogsToContext(ctx context.Context, g godogs.Godogs) context.Context {
58 | return context.WithValue(ctx, godogsCtxKey{}, &g)
59 | }
60 |
61 | func godogsFromContext(ctx context.Context) *godogs.Godogs {
62 | g, _ := ctx.Value(godogsCtxKey{}).(*godogs.Godogs)
63 |
64 | return g
65 | }
66 |
67 | // Concurrent execution of scenarios may lead to race conditions on shared resources.
68 | // Use context to maintain data separation and avoid data races.
69 |
70 | // Step definition can optionally receive context as a first argument.
71 |
72 | func thereAreGodogs(ctx context.Context, available int) {
73 | godogsFromContext(ctx).Add(available)
74 | }
75 |
76 | // Step definition can return error, context, context and error, or nothing.
77 |
78 | func iEat(ctx context.Context, num int) error {
79 | return godogsFromContext(ctx).Eat(num)
80 | }
81 |
82 | func thereShouldBeRemaining(ctx context.Context, remaining int) error {
83 | available := godogsFromContext(ctx).Available()
84 | if available != remaining {
85 | return fmt.Errorf("expected %d godogs to be remaining, but there is %d", remaining, available)
86 | }
87 |
88 | return nil
89 | }
90 |
91 | func thereShouldBeNoneRemaining(ctx context.Context) error {
92 | return thereShouldBeRemaining(ctx, 0)
93 | }
94 |
95 | func InitializeTestSuite(ctx *godog.TestSuiteContext) {
96 | ctx.BeforeSuite(func() { fmt.Println("Get the party started!") })
97 | }
98 |
99 | func InitializeScenario(ctx *godog.ScenarioContext) {
100 | ctx.Before(func(ctx context.Context, sc *godog.Scenario) (context.Context, error) {
101 | // Add initial godogs to context.
102 | return godogsToContext(ctx, 0), nil
103 | })
104 |
105 | ctx.Step(`^there are (\d+) godogs$`, thereAreGodogs)
106 | ctx.Step(`^I eat (\d+)$`, iEat)
107 | ctx.Step(`^there should be (\d+) remaining$`, thereShouldBeRemaining)
108 | ctx.Step(`^there should be none remaining$`, thereShouldBeNoneRemaining)
109 | }
110 |
--------------------------------------------------------------------------------
/_examples/db/api_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "database/sql"
6 | "encoding/json"
7 | "fmt"
8 | "net/http"
9 | "net/http/httptest"
10 | "reflect"
11 | "strings"
12 |
13 | txdb "github.com/DATA-DOG/go-txdb"
14 | "github.com/cucumber/godog"
15 | )
16 |
17 | func init() {
18 | // we register an sql driver txdb
19 | txdb.Register("txdb", "mysql", "root@/godog_test")
20 | }
21 |
22 | type apiFeature struct {
23 | server
24 | resp *httptest.ResponseRecorder
25 | }
26 |
27 | func (a *apiFeature) resetResponse(*godog.Scenario) {
28 | a.resp = httptest.NewRecorder()
29 | if a.db != nil {
30 | a.db.Close()
31 | }
32 | db, err := sql.Open("txdb", "api")
33 | if err != nil {
34 | panic(err)
35 | }
36 | a.db = db
37 | }
38 |
39 | func (a *apiFeature) iSendrequestTo(method, endpoint string) (err error) {
40 | req, err := http.NewRequest(method, endpoint, nil)
41 | if err != nil {
42 | return
43 | }
44 |
45 | // handle panic
46 | defer func() {
47 | switch t := recover().(type) {
48 | case string:
49 | err = fmt.Errorf(t)
50 | case error:
51 | err = t
52 | }
53 | }()
54 |
55 | switch endpoint {
56 | case "/users":
57 | a.users(a.resp, req)
58 | default:
59 | err = fmt.Errorf("unknown endpoint: %s", endpoint)
60 | }
61 | return
62 | }
63 |
64 | func (a *apiFeature) theResponseCodeShouldBe(code int) error {
65 | if code != a.resp.Code {
66 | if a.resp.Code >= 400 {
67 | return fmt.Errorf("expected response code to be: %d, but actual is: %d, response message: %s", code, a.resp.Code, string(a.resp.Body.Bytes()))
68 | }
69 | return fmt.Errorf("expected response code to be: %d, but actual is: %d", code, a.resp.Code)
70 | }
71 | return nil
72 | }
73 |
74 | func (a *apiFeature) theResponseShouldMatchJSON(body *godog.DocString) (err error) {
75 | var expected, actual interface{}
76 |
77 | // re-encode expected response
78 | if err = json.Unmarshal([]byte(body.Content), &expected); err != nil {
79 | return
80 | }
81 |
82 | // re-encode actual response too
83 | if err = json.Unmarshal(a.resp.Body.Bytes(), &actual); err != nil {
84 | return
85 | }
86 |
87 | // the matching may be adapted per different requirements.
88 | if !reflect.DeepEqual(expected, actual) {
89 | return fmt.Errorf("expected JSON does not match actual, %v vs. %v", expected, actual)
90 | }
91 | return nil
92 | }
93 |
94 | func (a *apiFeature) thereAreUsers(users *godog.Table) error {
95 | var fields []string
96 | var marks []string
97 | head := users.Rows[0].Cells
98 | for _, cell := range head {
99 | fields = append(fields, cell.Value)
100 | marks = append(marks, "?")
101 | }
102 |
103 | stmt, err := a.db.Prepare("INSERT INTO users (" + strings.Join(fields, ", ") + ") VALUES(" + strings.Join(marks, ", ") + ")")
104 | if err != nil {
105 | return err
106 | }
107 | for i := 1; i < len(users.Rows); i++ {
108 | var vals []interface{}
109 | for n, cell := range users.Rows[i].Cells {
110 | switch head[n].Value {
111 | case "username":
112 | vals = append(vals, cell.Value)
113 | case "email":
114 | vals = append(vals, cell.Value)
115 | default:
116 | return fmt.Errorf("unexpected column name: %s", head[n].Value)
117 | }
118 | }
119 | if _, err = stmt.Exec(vals...); err != nil {
120 | return err
121 | }
122 | }
123 | return nil
124 | }
125 |
126 | func InitializeScenario(ctx *godog.ScenarioContext) {
127 | api := &apiFeature{}
128 |
129 | ctx.Before(func(ctx context.Context, sc *godog.Scenario) (context.Context, error) {
130 | api.resetResponse(sc)
131 | return ctx, nil
132 | })
133 |
134 | ctx.Step(`^I send "(GET|POST|PUT|DELETE)" request to "([^"]*)"$`, api.iSendrequestTo)
135 | ctx.Step(`^the response code should be (\d+)$`, api.theResponseCodeShouldBe)
136 | ctx.Step(`^the response should match json:$`, api.theResponseShouldMatchJSON)
137 | ctx.Step(`^there are users:$`, api.thereAreUsers)
138 | }
139 |
--------------------------------------------------------------------------------