├── features ├── empty.feature ├── lang.feature ├── load.feature ├── background.feature ├── formatter │ └── events.feature └── tags.feature ├── _examples ├── doc.go ├── api │ ├── screenshots │ │ ├── passed.png │ │ └── undefined.png │ ├── features │ │ └── version.feature │ ├── api.go │ └── api_test.go ├── assert-godogs │ ├── godogs.go │ ├── features │ │ └── godogs.feature │ └── godogs_test.go ├── custom-formatter │ ├── godogs.go │ ├── imgs │ │ └── emoji-output-example.png │ ├── README.md │ ├── features │ │ └── emoji.feature │ ├── godogs_test.go │ └── emoji.go ├── incorrect-project-structure │ ├── main.go │ ├── go.mod │ └── README.md ├── attachments │ ├── features │ │ └── attachments.feature │ ├── README.md │ └── attachments_test.go ├── godogs │ ├── features │ │ ├── godogs.feature │ │ └── nodogs.feature │ ├── godogs.go │ └── godogs_test.go ├── db │ ├── Makefile │ ├── README.md │ ├── features │ │ └── users.feature │ ├── api.go │ └── api_test.go └── go.mod ├── internal ├── formatters │ ├── formatter-tests │ │ ├── cucumber │ │ │ ├── empty │ │ │ ├── empty_with_description │ │ │ ├── scenario_without_steps_with_background │ │ │ ├── empty_with_single_scenario_without_steps │ │ │ ├── empty_with_single_scenario_without_steps_and_description │ │ │ ├── single_scenario_with_passing_step │ │ │ ├── with_few_empty_scenarios │ │ │ └── scenario_with_background │ │ ├── features │ │ │ ├── empty.feature │ │ │ ├── empty_with_description.feature │ │ │ ├── empty_with_single_scenario_without_steps.feature │ │ │ ├── scenario_without_steps_with_background.feature │ │ │ ├── empty_with_single_scenario_without_steps_and_description.feature │ │ │ ├── single_scenario_with_passing_step.feature │ │ │ ├── scenario_with_background.feature │ │ │ ├── scenario_with_attachment.feature │ │ │ ├── two_scenarios_with_background_fail.feature │ │ │ ├── stop_on_first_failure.feature │ │ │ ├── with_few_empty_scenarios.feature │ │ │ ├── some_scenarios_including_failing.feature │ │ │ ├── scenario_outline.feature │ │ │ └── rules_with_examples_with_backgrounds.feature │ │ ├── pretty │ │ │ ├── empty │ │ │ ├── empty_with_description │ │ │ ├── empty_with_single_scenario_without_steps │ │ │ ├── empty_with_single_scenario_without_steps_and_description │ │ │ ├── scenario_without_steps_with_background │ │ │ ├── single_scenario_with_passing_step │ │ │ ├── with_few_empty_scenarios │ │ │ ├── scenario_with_background │ │ │ ├── stop_on_first_failure │ │ │ ├── two_scenarios_with_background_fail │ │ │ ├── scenario_outline │ │ │ ├── rules_with_examples_with_backgrounds │ │ │ └── some_scenarios_including_failing │ │ ├── progress │ │ │ ├── empty │ │ │ ├── empty_with_description │ │ │ ├── with_few_empty_scenarios │ │ │ ├── empty_with_single_scenario_without_steps │ │ │ ├── scenario_without_steps_with_background │ │ │ ├── empty_with_single_scenario_without_steps_and_description │ │ │ ├── single_scenario_with_passing_step │ │ │ ├── scenario_with_background │ │ │ ├── two_scenarios_with_background_fail │ │ │ ├── some_scenarions_including_failing │ │ │ └── scenario_outline │ │ ├── junit │ │ │ ├── empty │ │ │ ├── empty_with_description │ │ │ ├── empty_with_single_scenario_without_steps │ │ │ ├── scenario_without_steps_with_background │ │ │ ├── scenario_with_background │ │ │ ├── single_scenario_with_passing_step │ │ │ ├── empty_with_single_scenario_without_steps_and_description │ │ │ ├── with_few_empty_scenarios │ │ │ ├── stop_on_first_failure │ │ │ ├── two_scenarios_with_background_fail │ │ │ ├── scenario_outline │ │ │ └── some_scenarios_including_failing │ │ ├── junit,pretty │ │ │ ├── empty │ │ │ ├── empty_with_description │ │ │ ├── empty_with_single_scenario_without_steps │ │ │ ├── empty_with_single_scenario_without_steps_and_description │ │ │ ├── scenario_without_steps_with_background │ │ │ ├── single_scenario_with_passing_step │ │ │ ├── scenario_with_background │ │ │ ├── with_few_empty_scenarios │ │ │ ├── stop_on_first_failure │ │ │ ├── two_scenarios_with_background_fail │ │ │ └── scenario_outline │ │ └── events │ │ │ ├── empty │ │ │ ├── empty_with_description │ │ │ ├── empty_with_single_scenario_without_steps │ │ │ ├── scenario_without_steps_with_background │ │ │ ├── empty_with_single_scenario_without_steps_and_description │ │ │ ├── single_scenario_with_passing_step │ │ │ ├── with_few_empty_scenarios │ │ │ └── scenario_with_background │ ├── utils_test.go │ ├── fmt_flushwrap_test.go │ ├── undefined_snippets_gen.go │ ├── fmt.go │ └── fmt_flushwrap.go ├── storage │ ├── fs.go │ └── fs_test.go ├── utils │ └── utils.go ├── builder │ ├── ast.go │ ├── builder_go113_test.go │ ├── builder_go112_test.go │ ├── ast_test.go │ └── builder_go_module_test.go ├── tags │ ├── tag_filter.go │ └── tag_filter_test.go ├── models │ ├── results_test.go │ ├── results.go │ └── feature_test.go ├── flags │ ├── flags_test.go │ ├── flags.go │ └── options.go └── testutils │ └── utils.go ├── logo.png ├── codecov.yml ├── .github ├── renovate.json └── workflows │ ├── gorelease.yml │ ├── test.yml │ └── release-assets.yml ├── .gitignore ├── mod_version.go ├── options.go ├── colors ├── ansi_others.go ├── no_colors.go ├── writer.go └── colors.go ├── cmd └── godog │ ├── main.go │ └── internal │ ├── cmd_version.go │ ├── cmd_build.go │ ├── cmd_root.go │ └── cmd_run.go ├── go.mod ├── utils_test.go ├── flags_v0110_test.go ├── stacktrace_test.go ├── attachment_test.go ├── flags_v0110.go ├── LICENSE ├── example_subtests_test.go ├── godog.go ├── CONTRIBUTING.md ├── formatters ├── fmt_test.go └── fmt.go ├── fmt_test.go ├── RELEASING.md ├── Makefile ├── release-notes └── v0.11.0.md └── stacktrace.go /features/empty.feature: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /_examples/doc.go: -------------------------------------------------------------------------------- 1 | package examples 2 | -------------------------------------------------------------------------------- /internal/formatters/formatter-tests/cucumber/empty: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cucumber/godog/HEAD/logo.png -------------------------------------------------------------------------------- /internal/formatters/formatter-tests/cucumber/empty_with_description: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /internal/formatters/formatter-tests/features/empty.feature: -------------------------------------------------------------------------------- 1 | Feature: empty feature 2 | -------------------------------------------------------------------------------- /internal/formatters/formatter-tests/pretty/empty: -------------------------------------------------------------------------------- 1 | 2 | No scenarios 3 | No steps 4 | 0s 5 | -------------------------------------------------------------------------------- /internal/formatters/formatter-tests/progress/empty: -------------------------------------------------------------------------------- 1 | 2 | 3 | No scenarios 4 | No steps 5 | 0s 6 | -------------------------------------------------------------------------------- /internal/formatters/formatter-tests/pretty/empty_with_description: -------------------------------------------------------------------------------- 1 | 2 | No scenarios 3 | No steps 4 | 0s 5 | -------------------------------------------------------------------------------- /internal/formatters/formatter-tests/progress/empty_with_description: -------------------------------------------------------------------------------- 1 | 2 | 3 | No scenarios 4 | No steps 5 | 0s 6 | -------------------------------------------------------------------------------- /_examples/api/screenshots/passed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cucumber/godog/HEAD/_examples/api/screenshots/passed.png -------------------------------------------------------------------------------- /_examples/api/screenshots/undefined.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cucumber/godog/HEAD/_examples/api/screenshots/undefined.png -------------------------------------------------------------------------------- /internal/formatters/formatter-tests/features/empty_with_description.feature: -------------------------------------------------------------------------------- 1 | Feature: empty feature 2 | describes 3 | an empty 4 | feature 5 | -------------------------------------------------------------------------------- /internal/formatters/formatter-tests/progress/with_few_empty_scenarios: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 scenarios (5 undefined) 4 | No steps 5 | 0s 6 | -------------------------------------------------------------------------------- /_examples/assert-godogs/godogs.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // Godogs available to eat 4 | var Godogs int 5 | 6 | func main() { /* usual main func */ } 7 | -------------------------------------------------------------------------------- /_examples/custom-formatter/godogs.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // Godogs available to eat 4 | var Godogs int 5 | 6 | func main() { /* usual main func */ } 7 | -------------------------------------------------------------------------------- /internal/formatters/formatter-tests/features/empty_with_single_scenario_without_steps.feature: -------------------------------------------------------------------------------- 1 | Feature: empty feature 2 | 3 | Scenario: without steps 4 | -------------------------------------------------------------------------------- /_examples/custom-formatter/imgs/emoji-output-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cucumber/godog/HEAD/_examples/custom-formatter/imgs/emoji-output-example.png -------------------------------------------------------------------------------- /internal/formatters/formatter-tests/progress/empty_with_single_scenario_without_steps: -------------------------------------------------------------------------------- 1 | 2 | 3 | 1 scenarios (1 undefined) 4 | No steps 5 | 0s 6 | -------------------------------------------------------------------------------- /internal/formatters/formatter-tests/progress/scenario_without_steps_with_background: -------------------------------------------------------------------------------- 1 | 2 | 3 | 1 scenarios (1 undefined) 4 | No steps 5 | 0s 6 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | threshold: 0.5% 6 | patch: 7 | default: 8 | threshold: 0.5% 9 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "github>cucumber/renovate-config" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /internal/formatters/formatter-tests/progress/empty_with_single_scenario_without_steps_and_description: -------------------------------------------------------------------------------- 1 | 2 | 3 | 1 scenarios (1 undefined) 4 | No steps 5 | 0s 6 | -------------------------------------------------------------------------------- /_examples/incorrect-project-structure/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/cucumber/godog" 4 | 5 | func InitializeScenario(ctx *godog.ScenarioContext) { 6 | 7 | } 8 | -------------------------------------------------------------------------------- /internal/formatters/formatter-tests/junit/empty: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /cmd/godog/godog 2 | /example/example 3 | **/vendor/* 4 | Gopkg.lock 5 | Gopkg.toml 6 | 7 | .DS_Store 8 | .idea 9 | .vscode 10 | 11 | _artifacts 12 | 13 | vendor 14 | -------------------------------------------------------------------------------- /internal/formatters/formatter-tests/progress/single_scenario_with_passing_step: -------------------------------------------------------------------------------- 1 | . 1 2 | 3 | 4 | 1 scenarios (1 passed) 5 | 1 steps (1 passed) 6 | 0s 7 | -------------------------------------------------------------------------------- /internal/formatters/formatter-tests/features/scenario_without_steps_with_background.feature: -------------------------------------------------------------------------------- 1 | Feature: empty feature 2 | 3 | Background: 4 | Given passing step 5 | 6 | Scenario: without steps 7 | -------------------------------------------------------------------------------- /internal/formatters/formatter-tests/junit/empty_with_description: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /_examples/incorrect-project-structure/go.mod: -------------------------------------------------------------------------------- 1 | module incorrect-project-structure 2 | 3 | go 1.13 4 | 5 | require github.com/cucumber/godog v0.15.1 6 | 7 | replace github.com/cucumber/godog => ../../ 8 | -------------------------------------------------------------------------------- /internal/formatters/formatter-tests/features/empty_with_single_scenario_without_steps_and_description.feature: -------------------------------------------------------------------------------- 1 | Feature: empty feature 2 | describes 3 | an empty 4 | feature 5 | 6 | Scenario: without steps 7 | -------------------------------------------------------------------------------- /internal/formatters/formatter-tests/progress/scenario_with_background: -------------------------------------------------------------------------------- 1 | .... 4 2 | 3 | 4 | 1 scenarios (1 passed) 5 | 4 steps (4 passed) 6 | 0s 7 | -------------------------------------------------------------------------------- /internal/formatters/formatter-tests/junit,pretty/empty: -------------------------------------------------------------------------------- 1 | 2 | 3 | No scenarios 4 | No steps 5 | 0s 6 | -------------------------------------------------------------------------------- /internal/formatters/formatter-tests/events/empty: -------------------------------------------------------------------------------- 1 | {"event":"TestRunStarted","version":"0.1.0","timestamp":-6795364578871,"suite":"events"} 2 | {"event":"TestRunFinished","status":"pending","timestamp":-6795364578871,"snippets":"","memory":""} 3 | -------------------------------------------------------------------------------- /internal/formatters/formatter-tests/features/single_scenario_with_passing_step.feature: -------------------------------------------------------------------------------- 1 | Feature: single passing scenario 2 | describes 3 | a single scenario 4 | feature 5 | 6 | Scenario: one step passing 7 | Given a passing step 8 | -------------------------------------------------------------------------------- /_examples/incorrect-project-structure/README.md: -------------------------------------------------------------------------------- 1 | This example is to help reproduce issue [#383](https://github.com/cucumber/godog/issues/383) 2 | 3 | To run the example: 4 | 5 | cd _examples/incorrect-project-structure 6 | go run ../../cmd/godog -------------------------------------------------------------------------------- /internal/formatters/formatter-tests/junit,pretty/empty_with_description: -------------------------------------------------------------------------------- 1 | 2 | 3 | No scenarios 4 | No steps 5 | 0s 6 | -------------------------------------------------------------------------------- /internal/formatters/formatter-tests/events/empty_with_description: -------------------------------------------------------------------------------- 1 | {"event":"TestRunStarted","version":"0.1.0","timestamp":-6795364578871,"suite":"events"} 2 | {"event":"TestRunFinished","status":"pending","timestamp":-6795364578871,"snippets":"","memory":""} 3 | -------------------------------------------------------------------------------- /internal/formatters/formatter-tests/features/scenario_with_background.feature: -------------------------------------------------------------------------------- 1 | Feature: single scenario with background 2 | 3 | Background: named 4 | Given passing step 5 | And passing step 6 | 7 | Scenario: scenario 8 | When passing step 9 | Then passing step 10 | -------------------------------------------------------------------------------- /internal/formatters/formatter-tests/features/scenario_with_attachment.feature: -------------------------------------------------------------------------------- 1 | Feature: feature with attachment 2 | describes 3 | an attachment 4 | feature 5 | 6 | Scenario: scenario with attachment 7 | Given a step with a single attachment call for multiple attachments 8 | And a step with multiple attachment calls 9 | -------------------------------------------------------------------------------- /internal/formatters/formatter-tests/features/two_scenarios_with_background_fail.feature: -------------------------------------------------------------------------------- 1 | Feature: two scenarios with background fail 2 | 3 | Background: 4 | Given passing step 5 | And failing step 6 | 7 | Scenario: one 8 | When passing step 9 | Then passing step 10 | 11 | Scenario: two 12 | Then passing step 13 | -------------------------------------------------------------------------------- /mod_version.go: -------------------------------------------------------------------------------- 1 | //go:build go1.12 2 | // +build go1.12 3 | 4 | package godog 5 | 6 | import ( 7 | "runtime/debug" 8 | ) 9 | 10 | func init() { 11 | if info, available := debug.ReadBuildInfo(); available { 12 | if Version == "v0.0.0-dev" && info.Main.Version != "(devel)" { 13 | Version = info.Main.Version 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /internal/formatters/formatter-tests/features/stop_on_first_failure.feature: -------------------------------------------------------------------------------- 1 | Feature: Stop on first failure 2 | 3 | Scenario: First scenario - should run and fail 4 | Given a passing step 5 | When a failing step 6 | Then a passing step 7 | 8 | Scenario: Second scenario - should be skipped 9 | Given a passing step 10 | Then a passing step -------------------------------------------------------------------------------- /internal/formatters/formatter-tests/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 | 1 scenarios (1 undefined) 6 | No steps 7 | 0s 8 | -------------------------------------------------------------------------------- /options.go: -------------------------------------------------------------------------------- 1 | package godog 2 | 3 | import "github.com/cucumber/godog/internal/flags" 4 | 5 | // Options are suite run options 6 | // flags are mapped to these options. 7 | // 8 | // It can also be used together with godog.RunWithOptions 9 | // to run test suite from go source directly 10 | // 11 | // See the flags for more details 12 | type Options = flags.Options 13 | -------------------------------------------------------------------------------- /_examples/attachments/features/attachments.feature: -------------------------------------------------------------------------------- 1 | Feature: Attaching content to the cucumber report 2 | The cucumber JSON and NDJSON support the inclusion of attachments. 3 | These can be text or images or any data really. 4 | 5 | Scenario: Attaching files to the report 6 | Given I have attached two documents in sequence 7 | And I have attached two documents at once 8 | -------------------------------------------------------------------------------- /internal/formatters/formatter-tests/features/with_few_empty_scenarios.feature: -------------------------------------------------------------------------------- 1 | Feature: few empty scenarios 2 | 3 | Scenario: one 4 | 5 | Scenario Outline: two 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 17 | -------------------------------------------------------------------------------- /internal/formatters/formatter-tests/junit/empty_with_single_scenario_without_steps: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /internal/formatters/formatter-tests/junit/scenario_without_steps_with_background: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /internal/formatters/formatter-tests/junit/scenario_with_background: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /internal/formatters/formatter-tests/junit/single_scenario_with_passing_step: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /internal/formatters/formatter-tests/junit/empty_with_single_scenario_without_steps_and_description: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /_examples/godogs/features/godogs.feature: -------------------------------------------------------------------------------- 1 | Feature: eat godogs 2 | In order to be happy 3 | As a hungry gopher 4 | I need to be able to eat godogs 5 | 6 | Scenario: Eat 5 out of 12 7 | Given there are 12 godogs 8 | When I eat 5 9 | Then there should be 7 remaining 10 | 11 | Scenario: Eat 12 out of 12 12 | Given there are 12 godogs 13 | When I eat 12 14 | Then there should be none remaining 15 | -------------------------------------------------------------------------------- /_examples/godogs/features/nodogs.feature: -------------------------------------------------------------------------------- 1 | Feature: do not eat godogs 2 | In order to be fit 3 | As a well-fed gopher 4 | I need to be able to avoid godogs 5 | 6 | Scenario: Eat 0 out of 12 7 | Given there are 12 godogs 8 | When I eat 0 9 | Then there should be 12 remaining 10 | 11 | Scenario: Eat 0 out of 0 12 | Given there are 0 godogs 13 | When I eat 0 14 | Then there should be none remaining 15 | -------------------------------------------------------------------------------- /internal/storage/fs.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "io/fs" 5 | "os" 6 | ) 7 | 8 | // FS is a wrapper that falls back to `os`. 9 | type FS struct { 10 | FS fs.FS 11 | } 12 | 13 | // Open a file in the provided `fs.FS`. If none provided, 14 | // open via `os.Open` 15 | func (f FS) Open(name string) (fs.File, error) { 16 | if f.FS == nil { 17 | return os.Open(name) 18 | } 19 | 20 | return f.FS.Open(name) 21 | } 22 | -------------------------------------------------------------------------------- /internal/formatters/formatter-tests/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 | 1 scenarios (1 undefined) 9 | No steps 10 | 0s 11 | -------------------------------------------------------------------------------- /internal/formatters/formatter-tests/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 | 1 scenarios (1 undefined) 9 | No steps 10 | 0s 11 | -------------------------------------------------------------------------------- /internal/formatters/formatter-tests/features/some_scenarios_including_failing.feature: -------------------------------------------------------------------------------- 1 | Feature: some scenarios 2 | 3 | Scenario: failing 4 | Given passing step 5 | When failing step 6 | Then passing step 7 | 8 | Scenario: pending 9 | When pending step 10 | Then passing step 11 | 12 | Scenario: undefined 13 | When undefined 14 | Then passing step 15 | 16 | Scenario: ambiguous 17 | When ambiguous step 18 | Then passing step 19 | -------------------------------------------------------------------------------- /internal/utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "strings" 5 | "time" 6 | ) 7 | 8 | // S repeats a space n times 9 | func S(n int) string { 10 | if n < 0 { 11 | n = 1 12 | } 13 | return strings.Repeat(" ", n) 14 | } 15 | 16 | // TimeNowFunc is a utility function to simply testing 17 | // by allowing TimeNowFunc to be defined to zero time 18 | // to remove the time domain from tests 19 | var TimeNowFunc = func() time.Time { 20 | return time.Now() 21 | } 22 | -------------------------------------------------------------------------------- /colors/ansi_others.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 | //go:build !windows 6 | // +build !windows 7 | 8 | package colors 9 | 10 | import "io" 11 | 12 | type ansiColorWriter struct { 13 | w io.Writer 14 | mode outputMode 15 | } 16 | 17 | func (cw *ansiColorWriter) Write(p []byte) (int, error) { 18 | return cw.w.Write(p) 19 | } 20 | -------------------------------------------------------------------------------- /_examples/assert-godogs/features/godogs.feature: -------------------------------------------------------------------------------- 1 | # file: $GOPATH/godogs/features/godogs.feature 2 | Feature: eat godogs 3 | In order to be happy 4 | As a hungry gopher 5 | I need to be able to eat godogs 6 | 7 | Scenario: Eat 5 out of 12 8 | Given there are 12 godogs 9 | When I eat 5 10 | Then there should be 7 remaining 11 | 12 | Scenario: Eat 12 out of 12 13 | Given there are 12 godogs 14 | When I eat 12 15 | Then there should be none remaining 16 | -------------------------------------------------------------------------------- /_examples/custom-formatter/README.md: -------------------------------------------------------------------------------- 1 | 2 | # Custom Formatter Example 3 | 4 | This example custom formatter demonstrates some ways to build and use custom formatters with godog 5 | 6 | 7 | ## Emoji Progress 8 | 9 | The first example is the Emoji formatter, built on top of the Progress formatter that is included with godog. 10 | 11 | To run it: 12 | 13 | ``` 14 | $ godog -f emoji 15 | ``` 16 | 17 | Which would output step progress as emojis rather than text: 18 | 19 | ![](imgs/emoji-output-example.png) -------------------------------------------------------------------------------- /cmd/godog/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/cucumber/godog/cmd/godog/internal" 8 | ) 9 | 10 | func main() { 11 | rootCmd := internal.CreateRootCmd() 12 | buildCmd := internal.CreateBuildCmd() 13 | runCmd := internal.CreateRunCmd() 14 | versionCmd := internal.CreateVersionCmd() 15 | 16 | rootCmd.AddCommand(&buildCmd, &runCmd, &versionCmd) 17 | 18 | if err := rootCmd.Execute(); err != nil { 19 | fmt.Println(err) 20 | os.Exit(1) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /internal/formatters/formatter-tests/features/scenario_outline.feature: -------------------------------------------------------------------------------- 1 | @outline @tag 2 | Feature: outline 3 | 4 | @scenario 5 | Scenario Outline: outline 6 | Given passing step 7 | When passing step 8 | Then odd and even 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 | --------------------------------------------------------------------------------