From 6546d6f217afc51030d70b421a171b0cf68569a7 Mon Sep 17 00:00:00 2001 From: Viacheslav Poturaev Date: Wed, 18 Aug 2021 01:28:06 +0200 Subject: [PATCH] Add allure formatter (#2) --- .github/workflows/gorelease.yml | 4 +- .github/workflows/test-unit.yml | 12 +- README.md | 34 ++++ allure/allure_test.go | 33 ++++ allure/doc.go | 2 + allure/executor.go | 25 +++ allure/formatter.go | 281 ++++++++++++++++++++++++++++++++ allure/result.go | 148 +++++++++++++++++ go.mod | 5 +- go.sum | 10 +- pretty_failed.go | 2 +- 11 files changed, 541 insertions(+), 15 deletions(-) create mode 100644 allure/allure_test.go create mode 100644 allure/doc.go create mode 100644 allure/executor.go create mode 100644 allure/formatter.go create mode 100644 allure/result.go diff --git a/.github/workflows/gorelease.yml b/.github/workflows/gorelease.yml index 445ad3e..a733761 100644 --- a/.github/workflows/gorelease.yml +++ b/.github/workflows/gorelease.yml @@ -8,7 +8,7 @@ jobs: gorelease: strategy: matrix: - go-version: [ 1.16.x ] + go-version: [ 1.17.x ] runs-on: ubuntu-latest steps: - name: Install Go @@ -38,7 +38,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} header: gorelease message: | - ### Exported API Changes Report + ### API Changes
             ${{ steps.gorelease.outputs.report }}
diff --git a/.github/workflows/test-unit.yml b/.github/workflows/test-unit.yml
index 7304bfc..79ed0f6 100644
--- a/.github/workflows/test-unit.yml
+++ b/.github/workflows/test-unit.yml
@@ -13,7 +13,7 @@ jobs:
   test:
     strategy:
       matrix:
-        go-version: [ 1.13.x, 1.14.x, 1.15.x, 1.16.x ]
+        go-version: [ 1.13.x, 1.14.x, 1.15.x, 1.16.x, 1.17.x ]
     runs-on: ubuntu-latest
     steps:
       - name: Install Go
@@ -35,7 +35,7 @@ jobs:
           restore-keys: |
             ${{ runner.os }}-go-cache
       - name: Restore base test coverage
-        if: matrix.go-version == '1.16.x'
+        if: matrix.go-version == '1.17.x'
         uses: actions/cache@v2
         with:
           path: |
@@ -43,13 +43,13 @@ jobs:
           # Use base sha for PR or new commit hash for master/main push in test result key.
           key: ${{ runner.os }}-unit-test-coverage-${{ (github.event.pull_request.base.sha != github.event.after) && github.event.pull_request.base.sha || github.event.after }}
       - name: Checkout base code
-        if: matrix.go-version == '1.16.x' && env.RUN_BASE_COVERAGE == 'on' && steps.benchmark-base.outputs.cache-hit != 'true' && github.event.pull_request.base.sha != ''
+        if: matrix.go-version == '1.17.x' && env.RUN_BASE_COVERAGE == 'on' && steps.benchmark-base.outputs.cache-hit != 'true' && github.event.pull_request.base.sha != ''
         uses: actions/checkout@v2
         with:
           ref: ${{ github.event.pull_request.base.sha }}
           path: __base
       - name: Run test for base code
-        if: matrix.go-version == '1.16.x' && env.RUN_BASE_COVERAGE == 'on' && steps.benchmark-base.outputs.cache-hit != 'true' && github.event.pull_request.base.sha != ''
+        if: matrix.go-version == '1.17.x' && env.RUN_BASE_COVERAGE == 'on' && steps.benchmark-base.outputs.cache-hit != 'true' && github.event.pull_request.base.sha != ''
         run: |
           cd __base
           make | grep test-unit && (make test-unit && go tool cover -func=./unit.coverprofile | sed -e 's/.go:[0-9]*:\t/.go\t/g' | sed -e 's/\t\t*/\t/g'  > ../unit-base.txt) || echo "No test-unit in base"
@@ -69,7 +69,7 @@ jobs:
         if: ${{ github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main' }}
         run: cp unit.txt unit-base.txt
       - name: Comment Test Coverage
-        if: matrix.go-version == '1.16.x'
+        if: matrix.go-version == '1.17.x'
         uses: marocchino/sticky-pull-request-comment@v2
         with:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -85,7 +85,7 @@ jobs:
             
 
       - name: Upload code coverage
-        if: matrix.go-version == '1.16.x'
+        if: matrix.go-version == '1.17.x'
         uses: codecov/codecov-action@v1
         with:
           file: ./unit.coverprofile
diff --git a/README.md b/README.md
index b59eb76..437d583 100644
--- a/README.md
+++ b/README.md
@@ -18,3 +18,37 @@ failing scenarios.
 skipped steps after the failure was encountered.
 
 You can enable it by calling `godogx.RegisterPrettyFailedFormatter()`.
+
+## Allure Formatter
+
+[Allure](https://github.com/allure-framework/allure2) is convenient UI to expose test results.
+
+You can enable it by calling `allure.RegisterFormatter()`.
+
+Additional configuration can be added with env vars before test run.
+
+`ALLURE_ENV_*` are added to allure environment report.
+
+`ALLURE_EXECUTOR_*` configure `Executor` info.
+
+`ALLURE_RESULTS_PATH` can change default `./allure-results` destination.
+
+Example:
+```bash
+export ALLURE_ENV_TICKET=JIRA-1234
+export ALLURE_ENV_APP=todo-list
+export ALLURE_EXECUTOR_NAME=IntegrationTest
+export ALLURE_EXECUTOR_TYPE=github
+```
+
+Then you can run test with 
+```bash
+# Optionally clean up current result (if you have it).
+rm -rf ./allure-results/*
+# Optionally copy history from previous report.
+cp -r ./allure-report/history ./allure-results/history
+# Run suite with godog CLI tool or with go test.
+godog -f allure
+# Generate report with allure CLI tool.
+allure generate --clean
+```
\ No newline at end of file
diff --git a/allure/allure_test.go b/allure/allure_test.go
new file mode 100644
index 0000000..3074056
--- /dev/null
+++ b/allure/allure_test.go
@@ -0,0 +1,33 @@
+package allure_test
+
+import (
+	"bytes"
+	"errors"
+	"testing"
+
+	"github.com/bool64/godogx/allure"
+	"github.com/cucumber/godog"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestRegister(t *testing.T) {
+	allure.RegisterFormatter()
+
+	out := bytes.NewBuffer(nil)
+
+	suite := godog.TestSuite{
+		ScenarioInitializer: func(s *godog.ScenarioContext) {
+			s.Step("I pass", func() {})
+			s.Step("I fail", func() error { return errors.New("failed") })
+		},
+		Options: &godog.Options{
+			Format:   "allure",
+			Output:   out,
+			NoColors: true,
+			Paths:    []string{"../_testdata"},
+		},
+	}
+
+	st := suite.Run()
+	assert.Equal(t, 1, st) // Failed.
+}
diff --git a/allure/doc.go b/allure/doc.go
new file mode 100644
index 0000000..47ba2b0
--- /dev/null
+++ b/allure/doc.go
@@ -0,0 +1,2 @@
+// Package allure provides allure formatter for godog.
+package allure
diff --git a/allure/executor.go b/allure/executor.go
new file mode 100644
index 0000000..6ac5e7f
--- /dev/null
+++ b/allure/executor.go
@@ -0,0 +1,25 @@
+package allure
+
+// Executor describes execution context.
+type Executor struct {
+	Name string `json:"name,omitempty" example:"Jenkins"`
+	// Type may be one of [github, gitlab, teamcity, bamboo, jenkins] or a custom one.
+	Type       string `json:"type,omitempty" example:"jenkins"`
+	URL        string `json:"url,omitempty" example:"url"`
+	BuildOrder int    `json:"buildOrder,omitempty" example:"13"`
+	BuildName  string `json:"buildName,omitempty" example:"allure-report_deploy#13"`
+	BuildURL   string `json:"buildUrl,omitempty" example:"http://example.org/build#13"`
+	ReportURL  string `json:"reportUrl,omitempty" example:"http://example.org/build#13/AllureReport"`
+	ReportName string `json:"reportName,omitempty" example:"Demo allure report"`
+}
+
+//{
+//  "name": "Jenkins",
+//  "type": "jenkins",
+//  "url": "http://example.org",
+//  "buildOrder": 13,
+//  "buildName": "allure-report_deploy#13",
+//  "buildUrl": "http://example.org/build#13",
+//  "reportUrl": "http://example.org/build#13/AllureReport",
+//  "reportName": "Demo allure report"
+//}
diff --git a/allure/formatter.go b/allure/formatter.go
new file mode 100644
index 0000000..e0faa02
--- /dev/null
+++ b/allure/formatter.go
@@ -0,0 +1,281 @@
+package allure
+
+import (
+	"bytes"
+	"encoding/csv"
+	"encoding/json"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"log"
+	"os"
+	"strconv"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/cucumber/godog"
+	"github.com/cucumber/godog/formatters"
+	"github.com/google/uuid"
+)
+
+var (
+	// Exec allows configuring execution context information.
+	Exec Executor
+
+	// ResultsPath controls report destination.
+	ResultsPath = os.Getenv("ALLURE_RESULTS_PATH")
+
+	formatterRegister sync.Once
+)
+
+// RegisterFormatter adds allure to available formatters.
+func RegisterFormatter() {
+	formatterRegister.Do(func() {
+		if ResultsPath == "" {
+			ResultsPath = "./allure-results"
+		}
+
+		godog.Format("allure", "Allure formatter.",
+			func(suite string, writer io.Writer) formatters.Formatter {
+				if suite == "" {
+					suite = "Features"
+				}
+
+				return &formatter{
+					resultsPath: strings.TrimSuffix(ResultsPath, "/"),
+					container: &Container{
+						UUID:  uuid.New().String(),
+						Start: getTimestampMs(),
+						Name:  suite,
+					},
+					BaseFmt: godog.NewBaseFmt(suite, writer),
+				}
+			})
+	})
+}
+
+type formatter struct {
+	container   *Container
+	res         *Result
+	lastTime    TimestampMs
+	resultsPath string
+
+	*godog.BaseFmt
+}
+
+func (f *formatter) writeResult(r *Result) {
+	f.lastTime = getTimestampMs()
+
+	r.Stage = "finished"
+	r.Stop = f.lastTime
+	f.container.Children = append(f.container.Children, r.UUID)
+
+	f.writeJSON(fmt.Sprintf("%s-result.json", r.UUID), r)
+}
+
+// TestRunStarted prepares test result directory.
+func (f *formatter) TestRunStarted() {
+	err := os.MkdirAll(f.resultsPath, 0o700)
+	if err != nil {
+		log.Fatal("failed create allure results directory:", err)
+	}
+}
+
+// Pickle receives scenario.
+func (f *formatter) Pickle(scenario *godog.Scenario) {
+	if f.res != nil {
+		f.writeResult(f.res)
+	}
+
+	f.lastTime = getTimestampMs()
+
+	feature := f.Storage.MustGetFeature(scenario.Uri)
+
+	f.res = &Result{
+		UUID:        uuid.New().String(),
+		Name:        scenario.Name,
+		HistoryID:   feature.Feature.Name + ": " + scenario.Name,
+		FullName:    scenario.Uri + ":" + scenario.Name,
+		Description: scenario.Uri,
+		Start:       f.lastTime,
+		Labels: []Label{
+			{Name: "feature", Value: feature.Feature.Name},
+			{Name: "suite", Value: f.container.Name},
+			{Name: "framework", Value: "godog"},
+			{Name: "language", Value: "Go"},
+		},
+	}
+}
+
+func getTimestampMs() TimestampMs {
+	return TimestampMs(time.Now().UnixNano() / int64(time.Millisecond))
+}
+
+const (
+	csvMime = "text/csv"
+)
+
+func mediaType(t string) string {
+	switch t {
+	case "json":
+		return "application/json"
+	case "xml":
+		return "application/xml"
+	case "csv":
+		return csvMime
+	default:
+		return "text/plain"
+	}
+}
+
+func (f *formatter) argumentAttachment(st *godog.Step) *Attachment {
+	if st.Argument == nil {
+		return nil
+	}
+
+	if st.Argument.DocString != nil {
+		att, err := NewAttachment("Doc", mediaType(st.Argument.DocString.MediaType),
+			f.resultsPath, []byte(st.Argument.DocString.Content))
+		if err != nil {
+			log.Fatal("failed to create attachment:", err)
+		}
+
+		return att
+	} else if st.Argument.DataTable != nil {
+		mt := csvMime
+		buf := bytes.NewBuffer(nil)
+		c := csv.NewWriter(buf)
+
+		for _, r := range st.Argument.DataTable.Rows {
+			var rec []string
+			for _, cell := range r.Cells {
+				rec = append(rec, cell.Value)
+			}
+			if err := c.Write(rec); err != nil {
+				log.Fatal("failed write csv row:", err)
+			}
+		}
+		c.Flush()
+
+		att, err := NewAttachment("Table", mt, f.resultsPath, buf.Bytes())
+		if err != nil {
+			log.Fatal("failed create table attachment:", err)
+		}
+
+		return att
+	}
+
+	return nil
+}
+
+func (f *formatter) step(st *godog.Step) Step {
+	step := Step{
+		Name:  st.Text,
+		Stage: "finished",
+		Start: f.lastTime,
+	}
+
+	if att := f.argumentAttachment(st); att != nil {
+		step.Attachments = append(step.Attachments, *att)
+	}
+
+	f.lastTime = getTimestampMs()
+	step.Stop = f.lastTime
+
+	return step
+}
+
+// Passed captures passed step.
+func (f *formatter) Passed(_ *godog.Scenario, st *godog.Step, _ *godog.StepDefinition) {
+	step := f.step(st)
+	step.Status = Passed
+	f.res.Steps = append(f.res.Steps, step)
+	f.res.Status = Passed
+}
+
+// Skipped captures skipped step.
+func (f *formatter) Skipped(_ *godog.Scenario, st *godog.Step, _ *godog.StepDefinition) {
+	step := f.step(st)
+	step.Status = Skipped
+	f.res.Steps = append(f.res.Steps, step)
+}
+
+// Undefined captures undefined step.
+func (f *formatter) Undefined(_ *godog.Scenario, st *godog.Step, _ *godog.StepDefinition) {
+	step := f.step(st)
+	step.Status = Broken
+
+	f.res.Steps = append(f.res.Steps, step)
+}
+
+// Failed captures failed step.
+func (f *formatter) Failed(_ *godog.Scenario, st *godog.Step, _ *godog.StepDefinition, err error) {
+	details := &StatusDetails{
+		Message: err.Error(),
+	}
+
+	step := f.step(st)
+	step.Status = Failed
+	step.StatusDetails = details
+
+	f.res.Steps = append(f.res.Steps, step)
+	f.res.Status = Failed
+	f.res.StatusDetails = details
+}
+
+// Pending captures pending step.
+func (f *formatter) Pending(*godog.Scenario, *godog.Step, *godog.StepDefinition) {
+}
+
+func (f *formatter) writeJSON(name string, v interface{}) {
+	j, err := json.Marshal(v)
+	if err != nil {
+		log.Fatal("failed to marshal json value:", err)
+	}
+
+	if err := ioutil.WriteFile(f.resultsPath+"/"+name, j, 0o600); err != nil {
+		log.Fatal("failed to write a file:", err)
+	}
+}
+
+// Summary finishes report.
+func (f *formatter) Summary() {
+	if f.res != nil {
+		f.writeResult(f.res)
+	}
+
+	f.container.Stop = getTimestampMs()
+
+	f.writeJSON(f.container.UUID+"-container.json", f.container)
+
+	// Populate from env vars.
+	if Exec.Name == "" {
+		Exec.Name = os.Getenv("ALLURE_EXECUTOR_NAME")
+		Exec.Type = os.Getenv("ALLURE_EXECUTOR_TYPE")
+		Exec.URL = os.Getenv("ALLURE_EXECUTOR_URL")
+		Exec.BuildOrder, _ = strconv.Atoi(os.Getenv("ALLURE_EXECUTOR_BUILD_ORDER")) // nolint:errcheck
+		Exec.BuildName = os.Getenv("ALLURE_EXECUTOR_BUILD_NAME")
+		Exec.BuildURL = os.Getenv("ALLURE_EXECUTOR_BUILD_URL")
+		Exec.ReportName = os.Getenv("ALLURE_EXECUTOR_REPORT_NAME")
+		Exec.ReportURL = os.Getenv("ALLURE_EXECUTOR_REPORT_URL")
+	}
+
+	if Exec.Name != "" {
+		f.writeJSON("executor.json", Exec)
+	}
+
+	var env []byte
+
+	for _, l := range os.Environ() {
+		if strings.HasPrefix(l, "ALLURE_ENV_") {
+			env = append(env, []byte(strings.TrimPrefix(l, "ALLURE_ENV_")+"\n")...)
+		}
+	}
+
+	if len(env) > 0 {
+		if err := ioutil.WriteFile(f.resultsPath+"/environment.properties", env, 0o600); err != nil {
+			log.Fatal("failed to write a file:", err)
+		}
+	}
+}
diff --git a/allure/result.go b/allure/result.go
new file mode 100644
index 0000000..3c1524c
--- /dev/null
+++ b/allure/result.go
@@ -0,0 +1,148 @@
+package allure
+
+import (
+	"fmt"
+	"io/ioutil"
+
+	"github.com/google/uuid"
+)
+
+// Container lists all results.
+type Container struct {
+	UUID     string      `json:"uuid,omitempty"`
+	Name     string      `json:"name"`
+	Children []string    `json:"children"`
+	Start    TimestampMs `json:"start,omitempty"`
+	Stop     TimestampMs `json:"stop,omitempty"`
+}
+
+// Result is the top level report object for a test.
+//
+// 18 known properties: "start", "descriptionHtml", "parameters", "name", "historyId",
+// "statusDetails", "status", "links", "fullName", "uuid", "description", "testCaseId",
+// "stage", "labels", "stop", "steps", "rerunOf", "attachments".
+type Result struct {
+	UUID          string         `json:"uuid,omitempty"`
+	HistoryID     string         `json:"historyId,omitempty"`
+	Name          string         `json:"name,omitempty"`
+	Description   string         `json:"description,omitempty"`
+	Status        Status         `json:"status,omitempty"`
+	StatusDetails *StatusDetails `json:"statusDetails,omitempty"`
+	Stage         string         `json:"stage,omitempty"` // "finished"
+	Steps         []Step         `json:"steps,omitempty"`
+	Attachments   []Attachment   `json:"attachments,omitempty"`
+	Parameters    []Parameter    `json:"parameters,omitempty"`
+	Start         TimestampMs    `json:"start,omitempty"`
+	Stop          TimestampMs    `json:"stop,omitempty"`
+	Children      []string       `json:"children,omitempty"`
+	FullName      string         `json:"fullName,omitempty"`
+	Labels        []Label        `json:"labels,omitempty"`
+	Links         []Link         `json:"links,omitempty"`
+}
+
+// Available statuses.
+const (
+	Broken  = Status("broken")
+	Passed  = Status("passed")
+	Failed  = Status("failed")
+	Skipped = Status("skipped")
+	Unknown = Status("unknown")
+)
+
+// TimestampMs is a timestamp in milliseconds.
+type TimestampMs int64
+
+// LinkType is a type of link.
+type LinkType string
+
+// Types of links.
+const (
+	Issue  LinkType = "issue"
+	TMS    LinkType = "tms"
+	Custom LinkType = "custom"
+)
+
+// Link references additional resources.
+type Link struct {
+	Name string   `json:"name,omitempty"`
+	Type LinkType `json:"type,omitempty"`
+	URL  string   `json:"url,omitempty"`
+}
+
+// Status describes test result.
+type Status string
+
+// StatusDetails provides additional information on status.
+type StatusDetails struct {
+	Known   bool   `json:"known,omitempty"`
+	Muted   bool   `json:"muted,omitempty"`
+	Flaky   bool   `json:"flaky,omitempty"`
+	Message string `json:"message,omitempty"`
+	Trace   string `json:"trace,omitempty"`
+}
+
+// Step is a part of scenario result.
+type Step struct {
+	Name          string         `json:"name,omitempty"`
+	Status        Status         `json:"status,omitempty"`
+	StatusDetails *StatusDetails `json:"statusDetails,omitempty"`
+	Stage         string         `json:"stage"`
+	ChildrenSteps []Step         `json:"steps"`
+	Attachments   []Attachment   `json:"attachments"`
+	Parameters    []Parameter    `json:"parameters"`
+	Start         TimestampMs    `json:"start"`
+	Stop          TimestampMs    `json:"stop"`
+}
+
+// Attachment can be attached.
+type Attachment struct {
+	Name   string `json:"name"`
+	Source string `json:"source"`
+	Type   string `json:"type"`
+}
+
+// NewAttachment creates and stores attachment.
+func NewAttachment(name string, mimeType string, resultsPath string, content []byte) (*Attachment, error) {
+	var ext string
+
+	switch mimeType {
+	case "application/json":
+		ext = ".json"
+	case "image/png":
+		ext = ".png"
+	case "image/jpeg":
+		ext = ".jpg"
+	case "image/gif":
+		ext = ".gif"
+	case csvMime:
+		ext = ".csv"
+	case "application/xml":
+		ext = ".xml"
+	default:
+		ext = ".txt"
+	}
+
+	a := Attachment{
+		Name:   name,
+		Type:   mimeType,
+		Source: fmt.Sprintf("%s-attachment%s", uuid.New().String(), ext),
+	}
+
+	if err := ioutil.WriteFile(fmt.Sprintf("%s/%s", resultsPath, a.Source), content, 0o600); err != nil {
+		return nil, err
+	}
+
+	return &a, nil
+}
+
+// Parameter is a named value.
+type Parameter struct {
+	Name  string `json:"name,omitempty"`
+	Value string `json:"value,omitempty"`
+}
+
+// Label is a named value.
+type Label struct {
+	Name  string `json:"name,omitempty"`
+	Value string `json:"value,omitempty"`
+}
diff --git a/go.mod b/go.mod
index cdea0bd..fe0aed4 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,8 @@ module github.com/bool64/godogx
 go 1.13
 
 require (
-	github.com/bool64/dev v0.1.37
-	github.com/cucumber/godog v0.12.0-rc2.0.20210815195939-92ea38e7ce8d
+	github.com/bool64/dev v0.1.38
+	github.com/cucumber/godog v0.12.0
+	github.com/google/uuid v1.3.0
 	github.com/stretchr/testify v1.7.0
 )
diff --git a/go.sum b/go.sum
index f2b240d..cc445bf 100644
--- a/go.sum
+++ b/go.sum
@@ -23,8 +23,8 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24
 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
 github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
 github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
-github.com/bool64/dev v0.1.37 h1:/c8U4emt4xjMDx8Au+POOYo5LJIw+Mzi4e48j5CmGQo=
-github.com/bool64/dev v0.1.37/go.mod h1:cTHiTDNc8EewrQPy3p1obNilpMpdmlUesDkFTF2zRWU=
+github.com/bool64/dev v0.1.38 h1:RJZlSdbIDW/2RAQykcMziptk4zx/UcZD3n36HU4Zivo=
+github.com/bool64/dev v0.1.38/go.mod h1:cTHiTDNc8EewrQPy3p1obNilpMpdmlUesDkFTF2zRWU=
 github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
 github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
@@ -35,8 +35,8 @@ github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfc
 github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
 github.com/cucumber/gherkin-go/v19 v19.0.3 h1:mMSKu1077ffLbTJULUfM5HPokgeBcIGboyeNUof1MdE=
 github.com/cucumber/gherkin-go/v19 v19.0.3/go.mod h1:jY/NP6jUtRSArQQJ5h1FXOUgk5fZK24qtE7vKi776Vw=
-github.com/cucumber/godog v0.12.0-rc2.0.20210815195939-92ea38e7ce8d h1:dch/okb8/EavnI5TM3nzpYeuGtB1Rx3F15bIFkFGd6U=
-github.com/cucumber/godog v0.12.0-rc2.0.20210815195939-92ea38e7ce8d/go.mod h1:u6SD7IXC49dLpPN35kal0oYEjsXZWee4pW6Tm9t5pIc=
+github.com/cucumber/godog v0.12.0 h1:xVOc9ML+1joT0CqcdQTpfXiT7G1hOLbCmlUnYOyJ80w=
+github.com/cucumber/godog v0.12.0/go.mod h1:u6SD7IXC49dLpPN35kal0oYEjsXZWee4pW6Tm9t5pIc=
 github.com/cucumber/messages-go/v16 v16.0.0/go.mod h1:EJcyR5Mm5ZuDsKJnT2N9KRnBK30BGjtYotDKpwQ0v6g=
 github.com/cucumber/messages-go/v16 v16.0.1 h1:fvkpwsLgnIm0qugftrw2YwNlio+ABe2Iu94Ap8GMYIY=
 github.com/cucumber/messages-go/v16 v16.0.1/go.mod h1:EJcyR5Mm5ZuDsKJnT2N9KRnBK30BGjtYotDKpwQ0v6g=
@@ -73,6 +73,8 @@ github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXi
 github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
 github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
 github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
+github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
+github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
 github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
 github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
diff --git a/pretty_failed.go b/pretty_failed.go
index 7c03d6e..b1365d9 100644
--- a/pretty_failed.go
+++ b/pretty_failed.go
@@ -53,7 +53,7 @@ func (p *prettyFailedFormatter) Pickle(scenario *godog.Scenario) {
 	p.PrettyFmt.Pickle(scenario)
 }
 
-func (p *prettyFailedFormatter) Feature(f *godog.Feature, ps string, c []byte) {
+func (p *prettyFailedFormatter) Feature(f *godog.GherkinDocument, ps string, c []byte) {
 	p.Lock.Lock()
 	p.buf.Reset()
 	p.Lock.Unlock()