Revert "Migrated to github.com/cucumber/gherkin-go - v9.2.0"

Этот коммит содержится в:
Fredrik Lönnblad 2020-03-05 19:37:39 -03:00 коммит произвёл GitHub
родитель dba5f4e28f
коммит 84b42bdd4d
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
44 изменённых файлов: 13351 добавлений и 1344 удалений

Просмотреть файл

@ -23,6 +23,7 @@ commands:
description: "Run go vet"
steps:
- run: go vet github.com/cucumber/godog
- run: go vet github.com/cucumber/godog/gherkin
- run: go vet github.com/cucumber/godog/colors
fmt:
description: "Run go fmt"

Просмотреть файл

@ -56,8 +56,8 @@ need to store state within steps (a response), we should introduce a structure w
package main
import (
"github.com/cucumber/gherkin-go/v9"
"github.com/cucumber/godog"
"github.com/cucumber/godog/gherkin"
)
type apiFeature struct {
@ -71,7 +71,7 @@ func (a *apiFeature) theResponseCodeShouldBe(code int) error {
return godog.ErrPending
}
func (a *apiFeature) theResponseShouldMatchJSON(body *messages.PickleStepArgument_PickleDocString) error {
func (a *apiFeature) theResponseShouldMatchJSON(body *gherkin.DocString) error {
return godog.ErrPending
}
@ -98,8 +98,8 @@ import (
"net/http"
"net/http/httptest"
"github.com/cucumber/gherkin-go/v9"
"github.com/cucumber/godog"
"github.com/cucumber/godog/gherkin"
)
type apiFeature struct {
@ -142,7 +142,7 @@ func (a *apiFeature) theResponseCodeShouldBe(code int) error {
return nil
}
func (a *apiFeature) theResponseShouldMatchJSON(body *messages.PickleStepArgument_PickleDocString) error {
func (a *apiFeature) theResponseShouldMatchJSON(body *gherkin.DocString) (err error) {
var expected, actual []byte
var data interface{}
if err = json.Unmarshal([]byte(body.Content), &data); err != nil {

Просмотреть файл

@ -8,14 +8,14 @@ import (
"reflect"
"github.com/cucumber/godog"
"github.com/cucumber/messages-go/v9"
"github.com/cucumber/godog/gherkin"
)
type apiFeature struct {
resp *httptest.ResponseRecorder
}
func (a *apiFeature) resetResponse(*messages.Pickle) {
func (a *apiFeature) resetResponse(interface{}) {
a.resp = httptest.NewRecorder()
}
@ -51,7 +51,7 @@ func (a *apiFeature) theResponseCodeShouldBe(code int) error {
return nil
}
func (a *apiFeature) theResponseShouldMatchJSON(body *messages.PickleStepArgument_PickleDocString) (err error) {
func (a *apiFeature) theResponseShouldMatchJSON(body *gherkin.DocString) (err error) {
var expected, actual interface{}
// re-encode expected response

Просмотреть файл

@ -11,7 +11,7 @@ import (
txdb "github.com/DATA-DOG/go-txdb"
"github.com/cucumber/godog"
"github.com/cucumber/messages-go/v9"
"github.com/cucumber/godog/gherkin"
)
func init() {
@ -24,7 +24,7 @@ type apiFeature struct {
resp *httptest.ResponseRecorder
}
func (a *apiFeature) resetResponse(*messages.Pickle) {
func (a *apiFeature) resetResponse(interface{}) {
a.resp = httptest.NewRecorder()
if a.db != nil {
a.db.Close()
@ -71,7 +71,7 @@ func (a *apiFeature) theResponseCodeShouldBe(code int) error {
return nil
}
func (a *apiFeature) theResponseShouldMatchJSON(body *messages.PickleStepArgument_PickleDocString) (err error) {
func (a *apiFeature) theResponseShouldMatchJSON(body *gherkin.DocString) (err error) {
var expected, actual interface{}
// re-encode expected response
@ -91,7 +91,7 @@ func (a *apiFeature) theResponseShouldMatchJSON(body *messages.PickleStepArgumen
return nil
}
func (a *apiFeature) thereAreUsers(users *messages.PickleStepArgument_PickleTable) error {
func (a *apiFeature) thereAreUsers(users *gherkin.DataTable) error {
var fields []string
var marks []string
head := users.Rows[0].Cells

Просмотреть файл

@ -9,7 +9,6 @@ import (
"github.com/cucumber/godog"
"github.com/cucumber/godog/colors"
messages "github.com/cucumber/messages-go/v9"
)
var opt = godog.Options{Output: colors.Colored(os.Stdout)}
@ -57,7 +56,7 @@ func FeatureContext(s *godog.Suite) {
s.Step(`^I eat (\d+)$`, iEat)
s.Step(`^there should be (\d+) remaining$`, thereShouldBeRemaining)
s.BeforeScenario(func(*messages.Pickle) {
s.BeforeScenario(func(interface{}) {
Godogs = 0 // clean the state before every scenario
})
}

Просмотреть файл

@ -1,60 +0,0 @@
// +build go1.12
// +build !go1.13
package godog
import (
"bytes"
"os"
"path/filepath"
"testing"
)
func TestGodogBuildWithVendoredGodogAndMod(t *testing.T) {
gopath := filepath.Join(os.TempDir(), "_gpc")
dir := filepath.Join(gopath, "src", "godogs")
err := buildTestPackage(dir, map[string]string{
"godogs.feature": builderFeatureFile,
"godogs.go": builderMainCodeFile,
"godogs_test.go": builderTestFile,
"go.mod": builderModFile,
})
if err != nil {
os.RemoveAll(gopath)
t.Fatal(err)
}
defer os.RemoveAll(gopath)
pkg := filepath.Join(dir, "vendor", "github.com", "cucumber")
if err := os.MkdirAll(pkg, 0755); err != nil {
t.Fatal(err)
}
prevDir, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
// symlink godog package
if err := os.Symlink(prevDir, filepath.Join(pkg, "godog")); err != nil {
t.Fatal(err)
}
if err := os.Chdir(dir); err != nil {
t.Fatal(err)
}
defer os.Chdir(prevDir)
cmd := buildTestCommand(t, "godogs.feature")
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
cmd.Env = append(envVarsWithoutGopath(), "GOPATH="+gopath)
if err := cmd.Run(); err != nil {
t.Log(stdout.String())
t.Log(stderr.String())
t.Fatal(err)
}
}

Просмотреть файл

@ -1,54 +0,0 @@
// +build go1.13
package godog
import (
"bytes"
"os"
"os/exec"
"path/filepath"
"testing"
)
func TestGodogBuildWithVendoredGodogAndMod(t *testing.T) {
gopath := filepath.Join(os.TempDir(), "_gpc")
dir := filepath.Join(gopath, "src", "godogs")
err := buildTestPackage(dir, map[string]string{
"godogs.feature": builderFeatureFile,
"godogs.go": builderMainCodeFile,
"godogs_test.go": builderTestFile,
"go.mod": builderModFile,
})
if err != nil {
os.RemoveAll(gopath)
t.Fatal(err)
}
defer os.RemoveAll(gopath)
prevDir, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
if err = exec.Command("go", "mod", "vendor").Run(); err != nil {
t.Fatal(err)
}
if err := os.Chdir(dir); err != nil {
t.Fatal(err)
}
defer os.Chdir(prevDir)
cmd := buildTestCommand(t, "godogs.feature")
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
cmd.Env = append(envVarsWithoutGopath(), "GOPATH="+gopath)
if err := cmd.Run(); err != nil {
t.Log(stdout.String())
t.Log(stderr.String())
t.Fatal(err)
}
}

Просмотреть файл

@ -310,6 +310,55 @@ func TestGodogBuildWithinGopath(t *testing.T) {
}
}
func TestGodogBuildWithVendoredGodogAndMod(t *testing.T) {
gopath := filepath.Join(os.TempDir(), "_gpc")
dir := filepath.Join(gopath, "src", "godogs")
err := buildTestPackage(dir, map[string]string{
"godogs.feature": builderFeatureFile,
"godogs.go": builderMainCodeFile,
"godogs_test.go": builderTestFile,
"go.mod": builderModFile,
})
if err != nil {
os.RemoveAll(gopath)
t.Fatal(err)
}
defer os.RemoveAll(gopath)
pkg := filepath.Join(dir, "vendor", "github.com", "cucumber")
if err := os.MkdirAll(pkg, 0755); err != nil {
t.Fatal(err)
}
prevDir, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
// symlink godog package
if err := os.Symlink(prevDir, filepath.Join(pkg, "godog")); err != nil {
t.Fatal(err)
}
if err := os.Chdir(dir); err != nil {
t.Fatal(err)
}
defer os.Chdir(prevDir)
cmd := buildTestCommand(t, "godogs.feature")
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
cmd.Env = append(envVarsWithoutGopath(), "GOPATH="+gopath)
if err := cmd.Run(); err != nil {
t.Log(stdout.String())
t.Log(stderr.String())
t.Fatal(err)
}
}
func TestGodogBuildWithVendoredGodogWithoutModule(t *testing.T) {
gopath := filepath.Join(os.TempDir(), "_gp")
dir := filepath.Join(gopath, "src", "godogs")

Просмотреть файл

@ -181,7 +181,7 @@ func (cw *tagColorWriter) Write(p []byte) (int, error) {
}
if cw.state == outsideCsiCode {
nw, err = cw.w.Write(p[first:])
nw, err = cw.w.Write(p[first:len(p)])
r += nw
}

Просмотреть файл

@ -409,7 +409,7 @@ func (cw *ansiColorWriter) Write(p []byte) (int, error) {
}
if cw.mode != discardNonColorEscSeq || cw.state == outsideCsiCode {
nw, err = cw.w.Write(p[first:])
nw, err = cw.w.Write(p[first:len(p)])
r += nw
}

Просмотреть файл

@ -40,7 +40,7 @@ Feature: load features
| feature | number |
| features/load.feature:3 | 0 |
| features/load.feature:6 | 1 |
| features/load.feature | 6 |
| features/load.feature | 4 |
Scenario: load a number of feature files
Given a feature path "features/load.feature"

Просмотреть файл

@ -48,7 +48,7 @@ Feature: undefined step snippets
When I run feature suite
Then the undefined step snippets should be:
"""
func iSendRequestToWith(arg1, arg2 string, arg3 *messages.PickleStepArgument_PickleTable) error {
func iSendRequestToWith(arg1, arg2 string, arg3 *gherkin.DataTable) error {
return godog.ErrPending
}

5640
fixtures/cucumber_output.json Обычный файл

Различия файлов не показаны, т.к. их слишком много Показать различия

433
fmt.go
Просмотреть файл

@ -15,8 +15,7 @@ import (
"unicode"
"github.com/cucumber/godog/colors"
"github.com/cucumber/messages-go/v9"
"github.com/cucumber/godog/gherkin"
)
// some snippet formatting regexps
@ -44,7 +43,7 @@ var undefinedSnippetsTpl = template.Must(template.New("snippets").Funcs(snippetH
type undefinedSnippet struct {
Method string
Expr string
argument *messages.PickleStepArgument
argument interface{} // gherkin step argument
}
type registeredFormatter struct {
@ -98,14 +97,14 @@ func AvailableFormatters() map[string]string {
// formatters needs to be registered with a
// godog.Format function call
type Formatter interface {
Feature(*messages.GherkinDocument, string, []byte)
Pickle(*messages.Pickle)
Defined(*messages.Pickle, *messages.Pickle_PickleStep, *StepDefinition)
Failed(*messages.Pickle, *messages.Pickle_PickleStep, *StepDefinition, error)
Passed(*messages.Pickle, *messages.Pickle_PickleStep, *StepDefinition)
Skipped(*messages.Pickle, *messages.Pickle_PickleStep, *StepDefinition)
Undefined(*messages.Pickle, *messages.Pickle_PickleStep, *StepDefinition)
Pending(*messages.Pickle, *messages.Pickle_PickleStep, *StepDefinition)
Feature(*gherkin.Feature, string, []byte)
Node(interface{})
Defined(*gherkin.Step, *StepDef)
Failed(*gherkin.Step, *StepDef, error)
Passed(*gherkin.Step, *StepDef)
Skipped(*gherkin.Step, *StepDef)
Undefined(*gherkin.Step, *StepDef)
Pending(*gherkin.Step, *StepDef)
Summary()
}
@ -121,17 +120,17 @@ type ConcurrentFormatter interface {
// suite name and io.Writer to record output
type FormatterFunc func(string, io.Writer) Formatter
type stepResultStatus int
type stepType int
const (
passed stepResultStatus = iota
passed stepType = iota
failed
skipped
undefined
pending
)
func (st stepResultStatus) clr() colors.ColorFunc {
func (st stepType) clr() colors.ColorFunc {
switch st {
case passed:
return green
@ -144,7 +143,7 @@ func (st stepResultStatus) clr() colors.ColorFunc {
}
}
func (st stepResultStatus) String() string {
func (st stepType) String() string {
switch st {
case passed:
return "passed"
@ -162,17 +161,65 @@ func (st stepResultStatus) String() string {
}
type stepResult struct {
status stepResultStatus
typ stepType
feature *feature
owner interface{}
step *gherkin.Step
time time.Time
def *StepDef
err error
owner *messages.Pickle
step *messages.Pickle_PickleStep
def *StepDefinition
}
func newStepResult(pickle *messages.Pickle, step *messages.Pickle_PickleStep, match *StepDefinition) *stepResult {
return &stepResult{time: timeNowFunc(), owner: pickle, step: step, def: match}
func (f stepResult) line() string {
return fmt.Sprintf("%s:%d", f.feature.Path, f.step.Location.Line)
}
func (f stepResult) scenarioDesc() string {
if sc, ok := f.owner.(*gherkin.Scenario); ok {
return fmt.Sprintf("%s: %s", sc.Keyword, sc.Name)
}
if row, ok := f.owner.(*gherkin.TableRow); ok {
for _, def := range f.feature.Feature.ScenarioDefinitions {
out, ok := def.(*gherkin.ScenarioOutline)
if !ok {
continue
}
for _, ex := range out.Examples {
for _, rw := range ex.TableBody {
if rw.Location.Line == row.Location.Line {
return fmt.Sprintf("%s: %s", out.Keyword, out.Name)
}
}
}
}
}
return f.line() // was not expecting different owner
}
func (f stepResult) scenarioLine() string {
if sc, ok := f.owner.(*gherkin.Scenario); ok {
return fmt.Sprintf("%s:%d", f.feature.Path, sc.Location.Line)
}
if row, ok := f.owner.(*gherkin.TableRow); ok {
for _, def := range f.feature.Feature.ScenarioDefinitions {
out, ok := def.(*gherkin.ScenarioOutline)
if !ok {
continue
}
for _, ex := range out.Examples {
for _, rw := range ex.TableBody {
if rw.Location.Line == row.Location.Line {
return fmt.Sprintf("%s:%d", f.feature.Path, out.Location.Line)
}
}
}
}
}
return f.line() // was not expecting different owner
}
func newBaseFmt(suite string, out io.Writer) *basefmt {
@ -194,203 +241,215 @@ type basefmt struct {
started time.Time
features []*feature
failed []*stepResult
passed []*stepResult
skipped []*stepResult
undefined []*stepResult
pending []*stepResult
lock *sync.Mutex
}
func (f *basefmt) lastFeature() *feature {
return f.features[len(f.features)-1]
}
func (f *basefmt) lastStepResult() *stepResult {
return f.lastFeature().lastStepResult()
}
func (f *basefmt) findScenario(scenarioAstID string) *messages.GherkinDocument_Feature_Scenario {
for _, ft := range f.features {
if sc := ft.findScenario(scenarioAstID); sc != nil {
return sc
}
}
panic("Couldn't find scenario for AST ID: " + scenarioAstID)
}
func (f *basefmt) findBackground(scenarioAstID string) *messages.GherkinDocument_Feature_Background {
for _, ft := range f.features {
if bg := ft.findBackground(scenarioAstID); bg != nil {
return bg
}
}
return nil
}
func (f *basefmt) findExample(exampleAstID string) (*messages.GherkinDocument_Feature_Scenario_Examples, *messages.GherkinDocument_Feature_TableRow) {
for _, ft := range f.features {
if es, rs := ft.findExample(exampleAstID); es != nil && rs != nil {
return es, rs
}
}
return nil, nil
}
func (f *basefmt) findStep(stepAstID string) *messages.GherkinDocument_Feature_Step {
for _, ft := range f.features {
if st := ft.findStep(stepAstID); st != nil {
return st
}
}
panic("Couldn't find step for AST ID: " + stepAstID)
}
func (f *basefmt) Pickle(p *messages.Pickle) {
func (f *basefmt) Node(n interface{}) {
f.lock.Lock()
defer f.lock.Unlock()
switch t := n.(type) {
case *gherkin.Scenario:
f.owner = t
feature := f.features[len(f.features)-1]
feature.Scenarios = append(feature.Scenarios, &scenario{Name: t.Name, time: timeNowFunc()})
case *gherkin.ScenarioOutline:
feature := f.features[len(f.features)-1]
feature.Scenarios = append(feature.Scenarios, &scenario{OutlineName: t.Name})
case *gherkin.TableRow:
f.owner = t
feature := f.features[len(f.features)-1]
feature.pickleResults = append(feature.pickleResults, &pickleResult{Name: p.Name, time: timeNowFunc()})
lastExample := feature.Scenarios[len(feature.Scenarios)-1]
newExample := scenario{OutlineName: lastExample.OutlineName, ExampleNo: lastExample.ExampleNo + 1, time: timeNowFunc()}
newExample.Name = fmt.Sprintf("%s #%d", newExample.OutlineName, newExample.ExampleNo)
const firstExample = 1
if newExample.ExampleNo == firstExample {
feature.Scenarios[len(feature.Scenarios)-1] = &newExample
} else {
feature.Scenarios = append(feature.Scenarios, &newExample)
}
}
}
func (f *basefmt) Defined(*messages.Pickle, *messages.Pickle_PickleStep, *StepDefinition) {}
func (f *basefmt) Defined(*gherkin.Step, *StepDef) {
f.lock.Lock()
defer f.lock.Unlock()
}
func (f *basefmt) Feature(ft *messages.GherkinDocument, p string, c []byte) {
func (f *basefmt) Feature(ft *gherkin.Feature, p string, c []byte) {
f.lock.Lock()
defer f.lock.Unlock()
f.features = append(f.features, &feature{Path: p, GherkinDocument: ft, time: timeNowFunc()})
f.features = append(f.features, &feature{Path: p, Feature: ft, time: timeNowFunc()})
}
func (f *basefmt) Passed(pickle *messages.Pickle, step *messages.Pickle_PickleStep, match *StepDefinition) {
func (f *basefmt) Passed(step *gherkin.Step, match *StepDef) {
f.lock.Lock()
defer f.lock.Unlock()
s := newStepResult(pickle, step, match)
s.status = passed
f.lastFeature().appendStepResult(s)
s := &stepResult{
owner: f.owner,
feature: f.features[len(f.features)-1],
step: step,
def: match,
typ: passed,
time: timeNowFunc(),
}
f.passed = append(f.passed, s)
f.features[len(f.features)-1].appendStepResult(s)
}
func (f *basefmt) Skipped(pickle *messages.Pickle, step *messages.Pickle_PickleStep, match *StepDefinition) {
func (f *basefmt) Skipped(step *gherkin.Step, match *StepDef) {
f.lock.Lock()
defer f.lock.Unlock()
s := newStepResult(pickle, step, match)
s.status = skipped
f.lastFeature().appendStepResult(s)
s := &stepResult{
owner: f.owner,
feature: f.features[len(f.features)-1],
step: step,
def: match,
typ: skipped,
time: timeNowFunc(),
}
f.skipped = append(f.skipped, s)
f.features[len(f.features)-1].appendStepResult(s)
}
func (f *basefmt) Undefined(pickle *messages.Pickle, step *messages.Pickle_PickleStep, match *StepDefinition) {
func (f *basefmt) Undefined(step *gherkin.Step, match *StepDef) {
f.lock.Lock()
defer f.lock.Unlock()
s := newStepResult(pickle, step, match)
s.status = undefined
f.lastFeature().appendStepResult(s)
s := &stepResult{
owner: f.owner,
feature: f.features[len(f.features)-1],
step: step,
def: match,
typ: undefined,
time: timeNowFunc(),
}
f.undefined = append(f.undefined, s)
f.features[len(f.features)-1].appendStepResult(s)
}
func (f *basefmt) Failed(pickle *messages.Pickle, step *messages.Pickle_PickleStep, match *StepDefinition, err error) {
func (f *basefmt) Failed(step *gherkin.Step, match *StepDef, err error) {
f.lock.Lock()
defer f.lock.Unlock()
s := newStepResult(pickle, step, match)
s.status = failed
s.err = err
f.lastFeature().appendStepResult(s)
s := &stepResult{
owner: f.owner,
feature: f.features[len(f.features)-1],
step: step,
def: match,
err: err,
typ: failed,
time: timeNowFunc(),
}
f.failed = append(f.failed, s)
f.features[len(f.features)-1].appendStepResult(s)
}
func (f *basefmt) Pending(pickle *messages.Pickle, step *messages.Pickle_PickleStep, match *StepDefinition) {
func (f *basefmt) Pending(step *gherkin.Step, match *StepDef) {
f.lock.Lock()
defer f.lock.Unlock()
s := newStepResult(pickle, step, match)
s.status = pending
f.lastFeature().appendStepResult(s)
s := &stepResult{
owner: f.owner,
feature: f.features[len(f.features)-1],
step: step,
def: match,
typ: pending,
time: timeNowFunc(),
}
f.pending = append(f.pending, s)
f.features[len(f.features)-1].appendStepResult(s)
}
func (f *basefmt) Summary() {
var totalSc, passedSc, undefinedSc int
var totalSt, passedSt, failedSt, skippedSt, pendingSt, undefinedSt int
for _, feat := range f.features {
for _, pr := range feat.pickleResults {
var prStatus stepResultStatus
totalSc++
if len(pr.stepResults) == 0 {
prStatus = undefined
var total, passed, undefined int
for _, ft := range f.features {
for _, def := range ft.ScenarioDefinitions {
switch t := def.(type) {
case *gherkin.Scenario:
total++
if len(t.Steps) == 0 {
undefined++
}
for _, sr := range pr.stepResults {
totalSt++
switch sr.status {
case passed:
prStatus = passed
passedSt++
case failed:
prStatus = failed
failedSt++
case skipped:
skippedSt++
case undefined:
prStatus = undefined
undefinedSt++
case pending:
prStatus = pending
pendingSt++
case *gherkin.ScenarioOutline:
for _, ex := range t.Examples {
total += len(ex.TableBody)
if len(t.Steps) == 0 {
undefined += len(ex.TableBody)
}
}
if prStatus == passed {
passedSc++
} else if prStatus == undefined {
undefinedSc++
}
}
}
passed = total - undefined
var owner interface{}
for _, undef := range f.undefined {
if owner != undef.owner {
undefined++
owner = undef.owner
}
}
var steps, parts, scenarios []string
if passedSt > 0 {
steps = append(steps, green(fmt.Sprintf("%d passed", passedSt)))
nsteps := len(f.passed) + len(f.failed) + len(f.skipped) + len(f.undefined) + len(f.pending)
if len(f.passed) > 0 {
steps = append(steps, green(fmt.Sprintf("%d passed", len(f.passed))))
}
if failedSt > 0 {
parts = append(parts, red(fmt.Sprintf("%d failed", failedSt)))
steps = append(steps, red(fmt.Sprintf("%d failed", failedSt)))
if len(f.failed) > 0 {
passed -= len(f.failed)
parts = append(parts, red(fmt.Sprintf("%d failed", len(f.failed))))
steps = append(steps, parts[len(parts)-1])
}
if pendingSt > 0 {
parts = append(parts, yellow(fmt.Sprintf("%d pending", pendingSt)))
steps = append(steps, yellow(fmt.Sprintf("%d pending", pendingSt)))
if len(f.pending) > 0 {
passed -= len(f.pending)
parts = append(parts, yellow(fmt.Sprintf("%d pending", len(f.pending))))
steps = append(steps, yellow(fmt.Sprintf("%d pending", len(f.pending))))
}
if undefinedSt > 0 {
parts = append(parts, yellow(fmt.Sprintf("%d undefined", undefinedSc)))
steps = append(steps, yellow(fmt.Sprintf("%d undefined", undefinedSt)))
} else if undefinedSc > 0 {
if len(f.undefined) > 0 {
passed -= undefined
parts = append(parts, yellow(fmt.Sprintf("%d undefined", undefined)))
steps = append(steps, yellow(fmt.Sprintf("%d undefined", len(f.undefined))))
} else if undefined > 0 {
// there may be some scenarios without steps
parts = append(parts, yellow(fmt.Sprintf("%d undefined", undefinedSc)))
parts = append(parts, yellow(fmt.Sprintf("%d undefined", undefined)))
}
if skippedSt > 0 {
steps = append(steps, cyan(fmt.Sprintf("%d skipped", skippedSt)))
if len(f.skipped) > 0 {
steps = append(steps, cyan(fmt.Sprintf("%d skipped", len(f.skipped))))
}
if passedSc > 0 {
scenarios = append(scenarios, green(fmt.Sprintf("%d passed", passedSc)))
if passed > 0 {
scenarios = append(scenarios, green(fmt.Sprintf("%d passed", passed)))
}
scenarios = append(scenarios, parts...)
elapsed := timeNowFunc().Sub(f.started)
fmt.Fprintln(f.out, "")
if totalSc == 0 {
if total == 0 {
fmt.Fprintln(f.out, "No scenarios")
} else {
fmt.Fprintln(f.out, fmt.Sprintf("%d scenarios (%s)", totalSc, strings.Join(scenarios, ", ")))
fmt.Fprintln(f.out, fmt.Sprintf("%d scenarios (%s)", total, strings.Join(scenarios, ", ")))
}
if totalSt == 0 {
if nsteps == 0 {
fmt.Fprintln(f.out, "No steps")
} else {
fmt.Fprintln(f.out, fmt.Sprintf("%d steps (%s)", totalSt, strings.Join(steps, ", ")))
fmt.Fprintln(f.out, fmt.Sprintf("%d steps (%s)", nsteps, strings.Join(steps, ", ")))
}
elapsedString := elapsed.String()
@ -425,6 +484,21 @@ func (f *basefmt) Copy(cf ConcurrentFormatter) {
for _, v := range source.features {
f.features = append(f.features, v)
}
for _, v := range source.failed {
f.failed = append(f.failed, v)
}
for _, v := range source.passed {
f.passed = append(f.passed, v)
}
for _, v := range source.skipped {
f.skipped = append(f.skipped, v)
}
for _, v := range source.undefined {
f.undefined = append(f.undefined, v)
}
for _, v := range source.pending {
f.pending = append(f.pending, v)
}
}
}
@ -455,13 +529,12 @@ func (s *undefinedSnippet) Args() (ret string) {
args = append(args, reflect.String.String())
}
}
if s.argument != nil {
if s.argument.GetDocString() != nil {
args = append(args, "*messages.PickleStepArgument_PickleDocString")
}
if s.argument.GetDataTable() != nil {
args = append(args, "*messages.PickleStepArgument_PickleTable")
switch s.argument.(type) {
case *gherkin.DocString:
args = append(args, "*gherkin.DocString")
case *gherkin.DataTable:
args = append(args, "*gherkin.DataTable")
}
}
@ -477,30 +550,15 @@ func (s *undefinedSnippet) Args() (ret string) {
return strings.TrimSpace(strings.TrimRight(ret, ", ") + " " + last)
}
func (f *basefmt) findStepResults(status stepResultStatus) (res []*stepResult) {
for _, feat := range f.features {
for _, pr := range feat.pickleResults {
for _, sr := range pr.stepResults {
if sr.status == status {
res = append(res, sr)
}
}
}
}
return
}
func (f *basefmt) snippets() string {
undefinedStepResults := f.findStepResults(undefined)
if len(undefinedStepResults) == 0 {
if len(f.undefined) == 0 {
return ""
}
var index int
var snips []*undefinedSnippet
// build snippets
for _, u := range undefinedStepResults {
for _, u := range f.undefined {
steps := []string{u.step.Text}
arg := u.step.Argument
if u.def != nil {
@ -529,7 +587,7 @@ func (f *basefmt) snippets() string {
name = strings.Join(words, "")
if len(name) == 0 {
index++
name = fmt.Sprintf("StepDefinitioninition%d", index)
name = fmt.Sprintf("stepDefinition%d", index)
}
var found bool
@ -553,6 +611,25 @@ func (f *basefmt) snippets() string {
return strings.Replace(buf.String(), " \n", "\n", -1)
}
func isLastStep(pickle *messages.Pickle, step *messages.Pickle_PickleStep) bool {
return pickle.Steps[len(pickle.Steps)-1].Id == step.Id
func (f *basefmt) isLastStep(s *gherkin.Step) bool {
ft := f.features[len(f.features)-1]
for _, def := range ft.ScenarioDefinitions {
if outline, ok := def.(*gherkin.ScenarioOutline); ok {
for n, step := range outline.Steps {
if step.Location.Line == s.Location.Line {
return n == len(outline.Steps)-1
}
}
}
if scenario, ok := def.(*gherkin.Scenario); ok {
for n, step := range scenario.Steps {
if step.Location.Line == s.Location.Line {
return n == len(scenario.Steps)-1
}
}
}
}
return false
}

Просмотреть файл

@ -15,10 +15,11 @@ import (
"encoding/json"
"fmt"
"io"
"strconv"
"strings"
"time"
"github.com/cucumber/messages-go/v9"
"github.com/cucumber/godog/gherkin"
)
func init() {
@ -106,7 +107,7 @@ type cukefmt struct {
// it restricts this formatter to run only in synchronous single
// threaded execution. Unless running a copy of formatter for each feature
path string
status stepResultStatus // last step status, before skipped
stat stepType // last step status, before skipped
ID string // current test id.
results []cukeFeatureJSON // structure that represent cuke results
curStep *cukeStep // track the current step
@ -121,86 +122,115 @@ type cukefmt struct {
// of the example name inorder to build id fields.
}
func (f *cukefmt) Pickle(pickle *messages.Pickle) {
f.basefmt.Pickle(pickle)
func (f *cukefmt) Node(n interface{}) {
f.basefmt.Node(n)
scenario := f.findScenario(pickle.AstNodeIds[0])
switch t := n.(type) {
// When the example definition is seen we just need track the id and
// append the name associated with the example as part of the id.
case *gherkin.Examples:
f.curExampleName = makeID(t.Name)
f.curRow = 2 // there can be more than one example set per outline so reset row count.
// cucumber counts the header row as an example when creating the id.
// store any example level tags in a temp location.
f.curExampleTags = make([]cukeTag, len(t.Tags))
for idx, element := range t.Tags {
f.curExampleTags[idx].Line = element.Location.Line
f.curExampleTags[idx].Name = element.Name
}
// The outline node creates a placeholder and the actual element is added as each TableRow is processed.
case *gherkin.ScenarioOutline:
f.curOutline = cukeElement{}
f.curOutline.Name = t.Name
f.curOutline.Line = t.Location.Line
f.curOutline.Description = t.Description
f.curOutline.Keyword = t.Keyword
f.curOutline.ID = f.curFeature.ID + ";" + makeID(t.Name)
f.curOutline.Type = "scenario"
f.curOutline.Tags = make([]cukeTag, len(t.Tags)+len(f.curFeature.Tags))
// apply feature level tags
if len(f.curOutline.Tags) > 0 {
copy(f.curOutline.Tags, f.curFeature.Tags)
// apply outline level tags.
for idx, element := range t.Tags {
f.curOutline.Tags[idx+len(f.curFeature.Tags)].Line = element.Location.Line
f.curOutline.Tags[idx+len(f.curFeature.Tags)].Name = element.Name
}
}
// This scenario adds the element to the output immediately.
case *gherkin.Scenario:
f.curFeature.Elements = append(f.curFeature.Elements, cukeElement{})
f.curElement = &f.curFeature.Elements[len(f.curFeature.Elements)-1]
f.curElement.Name = pickle.Name
f.curElement.Line = int(scenario.Location.Line)
f.curElement.Description = scenario.Description
f.curElement.Keyword = scenario.Keyword
f.curElement.ID = f.curFeature.ID + ";" + makeID(pickle.Name)
f.curElement.Name = t.Name
f.curElement.Line = t.Location.Line
f.curElement.Description = t.Description
f.curElement.Keyword = t.Keyword
f.curElement.ID = f.curFeature.ID + ";" + makeID(t.Name)
f.curElement.Type = "scenario"
f.curElement.Tags = make([]cukeTag, len(scenario.Tags)+len(f.curFeature.Tags))
f.curElement.Tags = make([]cukeTag, len(t.Tags)+len(f.curFeature.Tags))
if len(f.curElement.Tags) > 0 {
// apply feature level tags
copy(f.curElement.Tags, f.curFeature.Tags)
// apply scenario level tags.
for idx, element := range scenario.Tags {
f.curElement.Tags[idx+len(f.curFeature.Tags)].Line = int(element.Location.Line)
for idx, element := range t.Tags {
f.curElement.Tags[idx+len(f.curFeature.Tags)].Line = element.Location.Line
f.curElement.Tags[idx+len(f.curFeature.Tags)].Name = element.Name
}
}
if len(pickle.AstNodeIds) == 1 {
return
}
// This is an outline scenario and the element is added to the output as
// the TableRows are encountered.
case *gherkin.TableRow:
tmpElem := f.curOutline
tmpElem.Line = t.Location.Line
tmpElem.ID = tmpElem.ID + ";" + f.curExampleName + ";" + strconv.Itoa(f.curRow)
f.curRow++
f.curFeature.Elements = append(f.curFeature.Elements, tmpElem)
f.curElement = &f.curFeature.Elements[len(f.curFeature.Elements)-1]
example, _ := f.findExample(pickle.AstNodeIds[1])
// apply example level tags.
for _, tag := range example.Tags {
tag := cukeTag{Line: int(tag.Location.Line), Name: tag.Name}
f.curElement.Tags = append(f.curElement.Tags, tag)
}
// copy in example level tags.
f.curElement.Tags = append(f.curElement.Tags, f.curExampleTags...)
examples := scenario.GetExamples()
if len(examples) > 0 {
rowID := pickle.AstNodeIds[1]
for _, example := range examples {
for idx, row := range example.TableBody {
if rowID == row.Id {
f.curElement.ID += fmt.Sprintf(";%s;%d", makeID(example.Name), idx+2)
f.curElement.Line = int(row.Location.Line)
}
}
}
}
}
func (f *cukefmt) Feature(gd *messages.GherkinDocument, p string, c []byte) {
f.basefmt.Feature(gd, p, c)
func (f *cukefmt) Feature(ft *gherkin.Feature, p string, c []byte) {
f.basefmt.Feature(ft, p, c)
f.path = p
f.ID = makeID(gd.Feature.Name)
f.ID = makeID(ft.Name)
f.results = append(f.results, cukeFeatureJSON{})
f.curFeature = &f.results[len(f.results)-1]
f.curFeature.URI = p
f.curFeature.Name = gd.Feature.Name
f.curFeature.Keyword = gd.Feature.Keyword
f.curFeature.Line = int(gd.Feature.Location.Line)
f.curFeature.Description = gd.Feature.Description
f.curFeature.Name = ft.Name
f.curFeature.Keyword = ft.Keyword
f.curFeature.Line = ft.Location.Line
f.curFeature.Description = ft.Description
f.curFeature.ID = f.ID
f.curFeature.Tags = make([]cukeTag, len(gd.Feature.Tags))
f.curFeature.Tags = make([]cukeTag, len(ft.Tags))
for idx, element := range gd.Feature.Tags {
f.curFeature.Tags[idx].Line = int(element.Location.Line)
for idx, element := range ft.Tags {
f.curFeature.Tags[idx].Line = element.Location.Line
f.curFeature.Tags[idx].Name = element.Name
}
f.curFeature.Comments = make([]cukeComment, len(gd.Comments))
for idx, comment := range gd.Comments {
f.curFeature.Comments = make([]cukeComment, len(ft.Comments))
for idx, comment := range ft.Comments {
f.curFeature.Comments[idx].Value = strings.TrimSpace(comment.Text)
f.curFeature.Comments[idx].Line = int(comment.Location.Line)
f.curFeature.Comments[idx].Line = comment.Location.Line
}
}
@ -214,43 +244,49 @@ func (f *cukefmt) Summary() {
}
func (f *cukefmt) step(res *stepResult) {
// determine if test case has finished
switch t := f.owner.(type) {
case *gherkin.TableRow:
d := int(timeNowFunc().Sub(f.startTime).Nanoseconds())
f.curStep.Result.Duration = &d
f.curStep.Result.Status = res.status.String()
f.curStep.Line = t.Location.Line
f.curStep.Result.Status = res.typ.String()
if res.err != nil {
f.curStep.Result.Error = res.err.Error()
}
case *gherkin.Scenario:
d := int(timeNowFunc().Sub(f.startTime).Nanoseconds())
f.curStep.Result.Duration = &d
f.curStep.Result.Status = res.typ.String()
if res.err != nil {
f.curStep.Result.Error = res.err.Error()
}
}
}
func (f *cukefmt) Defined(step *gherkin.Step, def *StepDef) {
func (f *cukefmt) Defined(pickle *messages.Pickle, pickleStep *messages.Pickle_PickleStep, def *StepDefinition) {
f.startTime = timeNowFunc() // start timing the step
f.curElement.Steps = append(f.curElement.Steps, cukeStep{})
f.curStep = &f.curElement.Steps[len(f.curElement.Steps)-1]
step := f.findStep(pickleStep.AstNodeIds[0])
line := step.Location.Line
if len(pickle.AstNodeIds) == 2 {
_, row := f.findExample(pickle.AstNodeIds[1])
line = row.Location.Line
}
f.curStep.Name = pickleStep.Text
f.curStep.Line = int(line)
f.curStep.Name = step.Text
f.curStep.Line = step.Location.Line
f.curStep.Keyword = step.Keyword
arg := pickleStep.Argument
if arg.GetDocString() != nil && step.GetDocString() != nil {
if _, ok := step.Argument.(*gherkin.DocString); ok {
f.curStep.Docstring = &cukeDocstring{}
f.curStep.Docstring.ContentType = strings.TrimSpace(arg.GetDocString().MediaType)
f.curStep.Docstring.Line = int(step.GetDocString().Location.Line)
f.curStep.Docstring.Value = arg.GetDocString().Content
f.curStep.Docstring.ContentType = strings.TrimSpace(step.Argument.(*gherkin.DocString).ContentType)
f.curStep.Docstring.Line = step.Argument.(*gherkin.DocString).Location.Line
f.curStep.Docstring.Value = step.Argument.(*gherkin.DocString).Content
}
if arg.GetDataTable() != nil {
f.curStep.DataTable = make([]*cukeDataTableRow, len(arg.GetDataTable().Rows))
for i, row := range arg.GetDataTable().Rows {
if _, ok := step.Argument.(*gherkin.DataTable); ok {
dataTable := step.Argument.(*gherkin.DataTable)
f.curStep.DataTable = make([]*cukeDataTableRow, len(dataTable.Rows))
for i, row := range dataTable.Rows {
cells := make([]string, len(row.Cells))
for j, cell := range row.Cells {
cells[j] = cell.Value
@ -264,47 +300,42 @@ func (f *cukefmt) Defined(pickle *messages.Pickle, pickleStep *messages.Pickle_P
}
}
func (f *cukefmt) Passed(pickle *messages.Pickle, step *messages.Pickle_PickleStep, match *StepDefinition) {
f.basefmt.Passed(pickle, step, match)
f.status = passed
f.step(f.lastStepResult())
func (f *cukefmt) Passed(step *gherkin.Step, match *StepDef) {
f.basefmt.Passed(step, match)
f.stat = passed
f.step(f.passed[len(f.passed)-1])
}
func (f *cukefmt) Skipped(pickle *messages.Pickle, step *messages.Pickle_PickleStep, match *StepDefinition) {
f.basefmt.Skipped(pickle, step, match)
f.step(f.lastStepResult())
func (f *cukefmt) Skipped(step *gherkin.Step, match *StepDef) {
f.basefmt.Skipped(step, match)
f.step(f.skipped[len(f.skipped)-1])
// no duration reported for skipped.
f.curStep.Result.Duration = nil
}
func (f *cukefmt) Undefined(pickle *messages.Pickle, step *messages.Pickle_PickleStep, match *StepDefinition) {
f.basefmt.Undefined(pickle, step, match)
f.status = undefined
f.step(f.lastStepResult())
func (f *cukefmt) Undefined(step *gherkin.Step, match *StepDef) {
f.basefmt.Undefined(step, match)
f.stat = undefined
f.step(f.undefined[len(f.undefined)-1])
// the location for undefined is the feature file location not the step file.
f.curStep.Match.Location = fmt.Sprintf("%s:%d", f.path, f.findStep(step.AstNodeIds[0]).Location.Line)
f.curStep.Match.Location = fmt.Sprintf("%s:%d", f.path, step.Location.Line)
f.curStep.Result.Duration = nil
}
func (f *cukefmt) Failed(pickle *messages.Pickle, step *messages.Pickle_PickleStep, match *StepDefinition, err error) {
f.basefmt.Failed(pickle, step, match, err)
f.status = failed
f.step(f.lastStepResult())
func (f *cukefmt) Failed(step *gherkin.Step, match *StepDef, err error) {
f.basefmt.Failed(step, match, err)
f.stat = failed
f.step(f.failed[len(f.failed)-1])
}
func (f *cukefmt) Pending(pickle *messages.Pickle, step *messages.Pickle_PickleStep, match *StepDefinition) {
f.basefmt.Pending(pickle, step, match)
f.status = pending
f.step(f.lastStepResult())
func (f *cukefmt) Pending(step *gherkin.Step, match *StepDef) {
f.stat = pending
f.basefmt.Pending(step, match)
f.step(f.pending[len(f.pending)-1])
// the location for pending is the feature file location not the step file.
f.curStep.Match.Location = fmt.Sprintf("%s:%d", f.path, f.findStep(step.AstNodeIds[0]).Location.Line)
f.curStep.Match.Location = fmt.Sprintf("%s:%d", f.path, step.Location.Line)
f.curStep.Result.Duration = nil
}

Просмотреть файл

@ -5,7 +5,7 @@ import (
"fmt"
"io"
"github.com/cucumber/messages-go/v9"
"github.com/cucumber/godog/gherkin"
)
const nanoSec = 1000000
@ -41,7 +41,7 @@ type events struct {
// it restricts this formatter to run only in synchronous single
// threaded execution. Unless running a copy of formatter for each feature
path string
status stepResultStatus // last step status, before skipped
stat stepType // last step status, before skipped
outlineSteps int // number of current outline scenario steps
}
@ -53,8 +53,25 @@ func (f *events) event(ev interface{}) {
fmt.Fprintln(f.out, string(data))
}
func (f *events) Pickle(pickle *messages.Pickle) {
f.basefmt.Pickle(pickle)
func (f *events) Node(n interface{}) {
f.basefmt.Node(n)
var id string
var undefined bool
switch t := n.(type) {
case *gherkin.Scenario:
id = fmt.Sprintf("%s:%d", f.path, t.Location.Line)
undefined = len(t.Steps) == 0
case *gherkin.TableRow:
id = fmt.Sprintf("%s:%d", f.path, t.Location.Line)
undefined = f.outlineSteps == 0
case *gherkin.ScenarioOutline:
f.outlineSteps = len(t.Steps)
}
if len(id) == 0 {
return
}
f.event(&struct {
Event string `json:"event"`
@ -62,11 +79,11 @@ func (f *events) Pickle(pickle *messages.Pickle) {
Timestamp int64 `json:"timestamp"`
}{
"TestCaseStarted",
f.scenarioLocation(pickle.AstNodeIds),
id,
timeNowFunc().UnixNano() / nanoSec,
})
if len(pickle.Steps) == 0 {
if undefined {
// @TODO: is status undefined or passed? when there are no steps
// for this scenario
f.event(&struct {
@ -76,14 +93,14 @@ func (f *events) Pickle(pickle *messages.Pickle) {
Status string `json:"status"`
}{
"TestCaseFinished",
f.scenarioLocation(pickle.AstNodeIds),
id,
timeNowFunc().UnixNano() / nanoSec,
"undefined",
})
}
}
func (f *events) Feature(ft *messages.GherkinDocument, p string, c []byte) {
func (f *events) Feature(ft *gherkin.Feature, p string, c []byte) {
f.basefmt.Feature(ft, p, c)
f.path = p
f.event(&struct {
@ -92,7 +109,7 @@ func (f *events) Feature(ft *messages.GherkinDocument, p string, c []byte) {
Source string `json:"source"`
}{
"TestSource",
fmt.Sprintf("%s:%d", p, ft.Feature.Location.Line),
fmt.Sprintf("%s:%d", p, ft.Location.Line),
string(c),
})
}
@ -100,10 +117,10 @@ func (f *events) Feature(ft *messages.GherkinDocument, p string, c []byte) {
func (f *events) Summary() {
// @TODO: determine status
status := passed
if len(f.findStepResults(failed)) > 0 {
if len(f.failed) > 0 {
status = failed
} else if len(f.findStepResults(passed)) == 0 {
if len(f.findStepResults(undefined)) > len(f.findStepResults(pending)) {
} else if len(f.passed) == 0 {
if len(f.undefined) > len(f.pending) {
status = undefined
} else {
status = pending
@ -131,8 +148,6 @@ func (f *events) Summary() {
}
func (f *events) step(res *stepResult) {
step := f.findStep(res.step.AstNodeIds[0])
var errMsg string
if res.err != nil {
errMsg = res.err.Error()
@ -145,13 +160,25 @@ func (f *events) step(res *stepResult) {
Summary string `json:"summary,omitempty"`
}{
"TestStepFinished",
fmt.Sprintf("%s:%d", f.path, step.Location.Line),
fmt.Sprintf("%s:%d", f.path, res.step.Location.Line),
timeNowFunc().UnixNano() / nanoSec,
res.status.String(),
res.typ.String(),
errMsg,
})
if isLastStep(res.owner, res.step) {
// determine if test case has finished
var finished bool
var line int
switch t := f.owner.(type) {
case *gherkin.TableRow:
line = t.Location.Line
finished = f.isLastStep(res.step)
case *gherkin.Scenario:
line = t.Location.Line
finished = f.isLastStep(res.step)
}
if finished {
f.event(&struct {
Event string `json:"event"`
Location string `json:"location"`
@ -159,18 +186,16 @@ func (f *events) step(res *stepResult) {
Status string `json:"status"`
}{
"TestCaseFinished",
f.scenarioLocation(res.owner.AstNodeIds),
fmt.Sprintf("%s:%d", f.path, line),
timeNowFunc().UnixNano() / nanoSec,
f.status.String(),
f.stat.String(),
})
}
}
func (f *events) Defined(pickle *messages.Pickle, pickleStep *messages.Pickle_PickleStep, def *StepDefinition) {
step := f.findStep(pickleStep.AstNodeIds[0])
func (f *events) Defined(step *gherkin.Step, def *StepDef) {
if def != nil {
m := def.Expr.FindStringSubmatchIndex(pickleStep.Text)[2:]
m := def.Expr.FindStringSubmatchIndex(step.Text)[2:]
var args [][2]int
for i := 0; i < len(m)/2; i++ {
pair := m[i : i*2+2]
@ -208,47 +233,31 @@ func (f *events) Defined(pickle *messages.Pickle, pickleStep *messages.Pickle_Pi
})
}
func (f *events) Passed(pickle *messages.Pickle, step *messages.Pickle_PickleStep, match *StepDefinition) {
f.basefmt.Passed(pickle, step, match)
f.status = passed
f.step(f.lastStepResult())
func (f *events) Passed(step *gherkin.Step, match *StepDef) {
f.basefmt.Passed(step, match)
f.stat = passed
f.step(f.passed[len(f.passed)-1])
}
func (f *events) Skipped(pickle *messages.Pickle, step *messages.Pickle_PickleStep, match *StepDefinition) {
f.basefmt.Skipped(pickle, step, match)
f.step(f.lastStepResult())
func (f *events) Skipped(step *gherkin.Step, match *StepDef) {
f.basefmt.Skipped(step, match)
f.step(f.skipped[len(f.skipped)-1])
}
func (f *events) Undefined(pickle *messages.Pickle, step *messages.Pickle_PickleStep, match *StepDefinition) {
f.basefmt.Undefined(pickle, step, match)
f.status = undefined
f.step(f.lastStepResult())
func (f *events) Undefined(step *gherkin.Step, match *StepDef) {
f.basefmt.Undefined(step, match)
f.stat = undefined
f.step(f.undefined[len(f.undefined)-1])
}
func (f *events) Failed(pickle *messages.Pickle, step *messages.Pickle_PickleStep, match *StepDefinition, err error) {
f.basefmt.Failed(pickle, step, match, err)
f.status = failed
f.step(f.lastStepResult())
func (f *events) Failed(step *gherkin.Step, match *StepDef, err error) {
f.basefmt.Failed(step, match, err)
f.stat = failed
f.step(f.failed[len(f.failed)-1])
}
func (f *events) Pending(pickle *messages.Pickle, step *messages.Pickle_PickleStep, match *StepDefinition) {
f.basefmt.Pending(pickle, step, match)
f.status = pending
f.step(f.lastStepResult())
}
func (f *events) scenarioLocation(astNodeIds []string) string {
scenario := f.findScenario(astNodeIds[0])
line := scenario.Location.Line
if len(astNodeIds) == 2 {
_, row := f.findExample(astNodeIds[1])
line = row.Location.Line
}
return fmt.Sprintf("%s:%d", f.path, line)
func (f *events) Pending(step *gherkin.Step, match *StepDef) {
f.stat = pending
f.basefmt.Pending(step, match)
f.step(f.pending[len(f.pending)-1])
}

Просмотреть файл

@ -64,55 +64,45 @@ func buildJUNITPackageSuite(suiteName string, startedAt time.Time, features []*f
for idx, feat := range features {
ts := junitTestSuite{
Name: feat.GherkinDocument.Feature.Name,
Name: feat.Name,
Time: junitTimeDuration(feat.startedAt(), feat.finishedAt()),
TestCases: make([]*junitTestCase, len(feat.pickleResults)),
TestCases: make([]*junitTestCase, len(feat.Scenarios)),
}
var testcaseNames = make(map[string]int)
for _, pickleResult := range feat.pickleResults {
testcaseNames[pickleResult.Name] = testcaseNames[pickleResult.Name] + 1
}
var outlineNo = make(map[string]int)
for idx, pickleResult := range feat.pickleResults {
tc := junitTestCase{}
tc.Time = junitTimeDuration(pickleResult.startedAt(), pickleResult.finishedAt())
tc.Name = pickleResult.Name
if testcaseNames[tc.Name] > 1 {
outlineNo[tc.Name] = outlineNo[tc.Name] + 1
tc.Name += fmt.Sprintf(" #%d", outlineNo[tc.Name])
for idx, scenario := range feat.Scenarios {
tc := junitTestCase{
Name: scenario.Name,
Time: junitTimeDuration(scenario.startedAt(), scenario.finishedAt()),
}
ts.Tests++
suite.Tests++
for _, stepResult := range pickleResult.stepResults {
switch stepResult.status {
for _, step := range scenario.Steps {
switch step.typ {
case passed:
tc.Status = passed.String()
case failed:
tc.Status = failed.String()
tc.Failure = &junitFailure{
Message: fmt.Sprintf("Step %s: %s", stepResult.step.Text, stepResult.err),
Message: fmt.Sprintf("%s %s: %s", step.step.Type, step.step.Text, step.err),
}
case skipped:
tc.Error = append(tc.Error, &junitError{
Type: "skipped",
Message: fmt.Sprintf("Step %s", stepResult.step.Text),
Message: fmt.Sprintf("%s %s", step.step.Type, step.step.Text),
})
case undefined:
tc.Status = undefined.String()
tc.Error = append(tc.Error, &junitError{
Type: "undefined",
Message: fmt.Sprintf("Step %s", stepResult.step.Text),
Message: fmt.Sprintf("%s %s", step.step.Type, step.step.Text),
})
case pending:
tc.Status = pending.String()
tc.Error = append(tc.Error, &junitError{
Type: "pending",
Message: fmt.Sprintf("Step %s: TODO: write pending definition", stepResult.step.Text),
Message: fmt.Sprintf("%s %s: TODO: write pending definition", step.step.Type, step.step.Text),
})
}
}

Просмотреть файл

@ -8,12 +8,8 @@ import (
"strings"
"testing"
"github.com/cucumber/gherkin-go/v9"
"github.com/cucumber/messages-go/v9"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/cucumber/godog/colors"
"github.com/cucumber/godog/gherkin"
)
var sampleGherkinFeature = `
@ -53,21 +49,18 @@ Feature: junit formatter
`
func TestJUnitFormatterOutput(t *testing.T) {
const path = "any.feature"
gd, err := gherkin.ParseGherkinDocument(strings.NewReader(sampleGherkinFeature), (&messages.Incrementing{}).NewId)
require.NoError(t, err)
pickles := gherkin.Pickles(*gd, path, (&messages.Incrementing{}).NewId)
feat, err := gherkin.ParseFeature(strings.NewReader(sampleGherkinFeature))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
var buf bytes.Buffer
w := colors.Uncolored(&buf)
s := &Suite{
fmt: junitFunc("junit", w),
features: []*feature{{
GherkinDocument: gd,
pickles: pickles,
Path: path,
features: []*feature{&feature{
Path: "any.feature",
Feature: feat,
Content: []byte(sampleGherkinFeature),
}},
}
@ -158,18 +151,19 @@ func TestJUnitFormatterOutput(t *testing.T) {
},
}},
}
s.run()
s.fmt.Summary()
var exp bytes.Buffer
_, err = io.WriteString(&exp, xml.Header)
require.NoError(t, err)
if _, err = io.WriteString(&exp, xml.Header); err != nil {
t.Fatalf("unexpected error: %v", err)
}
enc := xml.NewEncoder(&exp)
enc.Indent("", " ")
err = enc.Encode(expected)
require.NoError(t, err)
assert.Equal(t, exp.String(), buf.String())
if err = enc.Encode(expected); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if buf.String() != exp.String() {
t.Fatalf("expected output does not match: %s", buf.String())
}
}

Просмотреть файл

@ -3,13 +3,13 @@ package godog
import (
"fmt"
"io"
"math"
"regexp"
"strings"
"unicode/utf8"
"github.com/cucumber/messages-go/v9"
"github.com/cucumber/godog/colors"
"github.com/cucumber/godog/gherkin"
)
func init() {
@ -25,59 +25,79 @@ var outlinePlaceholderRegexp = regexp.MustCompile("<[^>]+>")
// a built in default pretty formatter
type pretty struct {
*basefmt
// currently processed
feature *gherkin.Feature
scenario *gherkin.Scenario
outline *gherkin.ScenarioOutline
// state
bgSteps int
totalBgSteps int
steps int
commentPos int
// whether scenario or scenario outline keyword was printed
scenarioKeyword bool
// outline
outlineSteps []*stepResult
outlineNumExample int
outlineNumExamples int
}
func (f *pretty) Feature(gd *messages.GherkinDocument, p string, c []byte) {
f.basefmt.Feature(gd, p, c)
f.printFeature(gd.Feature)
func (f *pretty) Feature(ft *gherkin.Feature, p string, c []byte) {
if len(f.features) != 0 {
// not a first feature, add a newline
fmt.Fprintln(f.out, "")
}
// Pickle takes a gherkin node for formatting
func (f *pretty) Pickle(pickle *messages.Pickle) {
f.basefmt.Pickle(pickle)
if len(pickle.Steps) == 0 {
f.printUndefinedPickle(pickle)
return
}
}
func (f *pretty) Passed(pickle *messages.Pickle, step *messages.Pickle_PickleStep, match *StepDefinition) {
f.basefmt.Passed(pickle, step, match)
f.printStep(f.lastStepResult())
}
func (f *pretty) Skipped(pickle *messages.Pickle, step *messages.Pickle_PickleStep, match *StepDefinition) {
f.basefmt.Skipped(pickle, step, match)
f.printStep(f.lastStepResult())
}
func (f *pretty) Undefined(pickle *messages.Pickle, step *messages.Pickle_PickleStep, match *StepDefinition) {
f.basefmt.Undefined(pickle, step, match)
f.printStep(f.lastStepResult())
}
func (f *pretty) Failed(pickle *messages.Pickle, step *messages.Pickle_PickleStep, match *StepDefinition, err error) {
f.basefmt.Failed(pickle, step, match, err)
f.printStep(f.lastStepResult())
}
func (f *pretty) Pending(pickle *messages.Pickle, step *messages.Pickle_PickleStep, match *StepDefinition) {
f.basefmt.Pending(pickle, step, match)
f.printStep(f.lastStepResult())
}
func (f *pretty) printFeature(feature *messages.GherkinDocument_Feature) {
if len(f.features) > 1 {
fmt.Fprintln(f.out, "") // not a first feature, add a newline
}
fmt.Fprintln(f.out, keywordAndName(feature.Keyword, feature.Name))
if strings.TrimSpace(feature.Description) != "" {
for _, line := range strings.Split(feature.Description, "\n") {
f.features = append(f.features, &feature{Path: p, Feature: ft})
fmt.Fprintln(f.out, keywordAndName(ft.Keyword, ft.Name))
if strings.TrimSpace(ft.Description) != "" {
for _, line := range strings.Split(ft.Description, "\n") {
fmt.Fprintln(f.out, s(f.indent)+strings.TrimSpace(line))
}
}
f.feature = ft
f.scenario = nil
f.outline = nil
f.bgSteps = 0
f.totalBgSteps = 0
if ft.Background != nil {
f.bgSteps = len(ft.Background.Steps)
f.totalBgSteps = len(ft.Background.Steps)
}
}
// Node takes a gherkin node for formatting
func (f *pretty) Node(node interface{}) {
f.basefmt.Node(node)
switch t := node.(type) {
case *gherkin.Examples:
f.outlineNumExamples = len(t.TableBody)
f.outlineNumExample++
case *gherkin.Scenario:
f.scenario = t
f.outline = nil
f.steps = len(t.Steps) + f.totalBgSteps
f.scenarioKeyword = false
if isEmptyScenario(t) {
f.printUndefinedScenario(t)
}
case *gherkin.ScenarioOutline:
f.outline = t
f.scenario = nil
f.outlineNumExample = -1
f.scenarioKeyword = false
if isEmptyScenario(t) {
f.printUndefinedScenario(t)
}
case *gherkin.TableRow:
f.steps = len(f.outline.Steps) + f.totalBgSteps
f.outlineSteps = []*stepResult{}
}
}
func keywordAndName(keyword, name string) string {
@ -88,302 +108,342 @@ func keywordAndName(keyword, name string) string {
return title
}
func (f *pretty) scenarioLengths(scenarioAstID string) (scenarioHeaderLength int, maxLength int) {
astScenario := f.findScenario(scenarioAstID)
astBackground := f.findBackground(scenarioAstID)
func (f *pretty) printUndefinedScenario(sc interface{}) {
if f.bgSteps > 0 {
bg := f.feature.Background
f.commentPos = f.longestStep(bg.Steps, f.length(bg))
fmt.Fprintln(f.out, "\n"+s(f.indent)+keywordAndName(bg.Keyword, bg.Name))
scenarioHeaderLength = f.lengthPickle(astScenario.Keyword, astScenario.Name)
maxLength = f.longestStep(astScenario.Steps, scenarioHeaderLength)
if astBackground != nil {
maxLength = f.longestStep(astBackground.Steps, maxLength)
for _, step := range bg.Steps {
f.bgSteps--
f.printStep(step, nil, colors.Cyan)
}
}
return scenarioHeaderLength, maxLength
}
func (f *pretty) printScenarioHeader(astScenario *messages.GherkinDocument_Feature_Scenario, spaceFilling int) {
text := s(f.indent) + keywordAndName(astScenario.Keyword, astScenario.Name)
text += s(spaceFilling) + f.line(astScenario.Location)
switch t := sc.(type) {
case *gherkin.Scenario:
f.commentPos = f.longestStep(t.Steps, f.length(sc))
text := s(f.indent) + keywordAndName(t.Keyword, t.Name)
text += s(f.commentPos-f.length(t)+1) + f.line(t.Location)
fmt.Fprintln(f.out, "\n"+text)
case *gherkin.ScenarioOutline:
f.commentPos = f.longestStep(t.Steps, f.length(sc))
text := s(f.indent) + keywordAndName(t.Keyword, t.Name)
text += s(f.commentPos-f.length(t)+1) + f.line(t.Location)
fmt.Fprintln(f.out, "\n"+text)
for _, example := range t.Examples {
max := longest(example, cyan)
f.printExampleHeader(example, max)
for _, row := range example.TableBody {
f.printExampleRow(row, max, cyan)
}
func (f *pretty) printUndefinedPickle(pickle *messages.Pickle) {
astScenario := f.findScenario(pickle.AstNodeIds[0])
astBackground := f.findBackground(pickle.AstNodeIds[0])
scenarioHeaderLength, maxLength := f.scenarioLengths(pickle.AstNodeIds[0])
if astBackground != nil {
fmt.Fprintln(f.out, "\n"+s(f.indent)+keywordAndName(astBackground.Keyword, astBackground.Name))
for _, step := range astBackground.Steps {
text := s(f.indent*2) + cyan(strings.TrimSpace(step.Keyword)) + " " + cyan(step.Text)
fmt.Fprintln(f.out, text)
}
}
// do not print scenario headers and examples multiple times
if len(astScenario.Examples) > 0 {
exampleTable, exampleRow := f.findExample(pickle.AstNodeIds[1])
firstExampleRow := exampleTable.TableBody[0].Id == exampleRow.Id
firstExamplesTable := astScenario.Examples[0].Location.Line == exampleTable.Location.Line
if !(firstExamplesTable && firstExampleRow) {
return
}
}
f.printScenarioHeader(astScenario, maxLength-scenarioHeaderLength)
for _, examples := range astScenario.Examples {
max := longestExampleRow(examples, cyan, cyan)
fmt.Fprintln(f.out, "")
fmt.Fprintln(f.out, s(f.indent*2)+keywordAndName(examples.Keyword, examples.Name))
f.printTableHeader(examples.TableHeader, max)
for _, row := range examples.TableBody {
f.printTableRow(row, max, cyan)
}
}
}
// Summary sumarize the feature formatter output
func (f *pretty) Summary() {
failedStepResults := f.findStepResults(failed)
if len(failedStepResults) > 0 {
if len(f.failed) > 0 {
fmt.Fprintln(f.out, "\n--- "+red("Failed steps:")+"\n")
for _, fail := range failedStepResults {
astScenario := f.findScenario(fail.owner.AstNodeIds[0])
scenarioDesc := fmt.Sprintf("%s: %s", astScenario.Keyword, fail.owner.Name)
astStep := f.findStep(fail.step.AstNodeIds[0])
stepDesc := strings.TrimSpace(astStep.Keyword) + " " + fail.step.Text
fmt.Fprintln(f.out, s(f.indent)+red(scenarioDesc)+f.line(astScenario.Location))
fmt.Fprintln(f.out, s(f.indent*2)+red(stepDesc)+f.line(astStep.Location))
fmt.Fprintln(f.out, s(f.indent*3)+red("Error: ")+redb(fmt.Sprintf("%+v", fail.err))+"\n")
for _, fail := range f.failed {
fmt.Fprintln(f.out, s(2)+red(fail.scenarioDesc())+blackb(" # "+fail.scenarioLine()))
fmt.Fprintln(f.out, s(4)+red(strings.TrimSpace(fail.step.Keyword)+" "+fail.step.Text)+blackb(" # "+fail.line()))
fmt.Fprintln(f.out, s(6)+red("Error: ")+redb(fmt.Sprintf("%+v", fail.err))+"\n")
}
}
f.basefmt.Summary()
}
func (f *pretty) printOutlineExample(pickle *messages.Pickle, backgroundSteps int) {
var errorMsg string
var clr = green
func (f *pretty) printOutlineExample(outline *gherkin.ScenarioOutline) {
var msg string
var clr colors.ColorFunc
astScenario := f.findScenario(pickle.AstNodeIds[0])
scenarioHeaderLength, maxLength := f.scenarioLengths(pickle.AstNodeIds[0])
exampleTable, exampleRow := f.findExample(pickle.AstNodeIds[1])
printExampleHeader := exampleTable.TableBody[0].Id == exampleRow.Id
firstExamplesTable := astScenario.Examples[0].Location.Line == exampleTable.Location.Line
firstExecutedScenarioStep := len(f.lastFeature().lastPickleResult().stepResults) == backgroundSteps+1
if firstExamplesTable && printExampleHeader && firstExecutedScenarioStep {
f.printScenarioHeader(astScenario, maxLength-scenarioHeaderLength)
}
if len(exampleTable.TableBody) == 0 {
ex := outline.Examples[f.outlineNumExample]
example, hasExamples := examples(ex)
if !hasExamples {
// do not print empty examples
return
}
lastStep := len(f.lastFeature().lastPickleResult().stepResults) == len(pickle.Steps)
if !lastStep {
// do not print examples unless all steps has finished
return
}
firstExample := f.outlineNumExamples == len(example.TableBody)
printSteps := firstExample && f.outlineNumExample == 0
for _, result := range f.lastFeature().lastPickleResult().stepResults {
for i, res := range f.outlineSteps {
// determine example row status
switch {
case result.status == failed:
errorMsg = result.err.Error()
clr = result.status.clr()
case result.status == undefined || result.status == pending:
clr = result.status.clr()
case result.status == skipped && clr == nil:
case res.typ == failed:
msg = res.err.Error()
clr = res.typ.clr()
case res.typ == undefined || res.typ == pending:
clr = res.typ.clr()
case res.typ == skipped && clr == nil:
clr = cyan
}
if firstExamplesTable && printExampleHeader {
if printSteps && i >= f.totalBgSteps {
// in first example, we need to print steps
var text string
astStep := f.findStep(result.step.AstNodeIds[0])
if result.def != nil {
if m := outlinePlaceholderRegexp.FindAllStringIndex(astStep.Text, -1); len(m) > 0 {
ostep := outline.Steps[i-f.totalBgSteps]
if res.def != nil {
if m := outlinePlaceholderRegexp.FindAllStringIndex(ostep.Text, -1); len(m) > 0 {
var pos int
for i := 0; i < len(m); i++ {
pair := m[i]
text += cyan(astStep.Text[pos:pair[0]])
text += cyanb(astStep.Text[pair[0]:pair[1]])
text += cyan(ostep.Text[pos:pair[0]])
text += cyanb(ostep.Text[pair[0]:pair[1]])
pos = pair[1]
}
text += cyan(astStep.Text[pos:len(astStep.Text)])
text += cyan(ostep.Text[pos:len(ostep.Text)])
} else {
text = cyan(astStep.Text)
text = cyan(ostep.Text)
}
_, maxLength := f.scenarioLengths(result.owner.AstNodeIds[0])
stepLength := f.lengthPickleStep(astStep.Keyword, astStep.Text)
text += s(maxLength - stepLength)
text += " " + blackb("# "+result.def.definitionID())
text += s(f.commentPos-f.length(ostep)+1) + blackb(fmt.Sprintf("# %s", res.def.definitionID()))
} else {
text = cyan(astStep.Text)
text = cyan(ostep.Text)
}
// print the step outline
fmt.Fprintln(f.out, s(f.indent*2)+cyan(strings.TrimSpace(astStep.Keyword))+" "+text)
fmt.Fprintln(f.out, s(f.indent*2)+cyan(strings.TrimSpace(ostep.Keyword))+" "+text)
if table := result.step.Argument.GetDataTable(); table != nil {
f.printTable(table, cyan)
// print step argument
// @TODO: need to make example header cells bold
switch t := ostep.Argument.(type) {
case *gherkin.DataTable:
f.printTable(t, cyan)
case *gherkin.DocString:
var ct string
if len(t.ContentType) > 0 {
ct = " " + cyan(t.ContentType)
}
if docString := astStep.GetDocString(); docString != nil {
f.printDocString(docString)
fmt.Fprintln(f.out, s(f.indent*3)+cyan(t.Delimitter)+ct)
for _, ln := range strings.Split(t.Content, "\n") {
fmt.Fprintln(f.out, s(f.indent*3)+cyan(ln))
}
fmt.Fprintln(f.out, s(f.indent*3)+cyan(t.Delimitter))
}
}
}
max := longestExampleRow(exampleTable, clr, cyan)
if clr == nil {
clr = green
}
max := longest(example, clr, cyan)
// an example table header
if printExampleHeader {
fmt.Fprintln(f.out, "")
fmt.Fprintln(f.out, s(f.indent*2)+keywordAndName(exampleTable.Keyword, exampleTable.Name))
f.printTableHeader(exampleTable.TableHeader, max)
if firstExample {
f.printExampleHeader(example, max)
}
f.printTableRow(exampleRow, max, clr)
// an example table row
row := example.TableBody[len(example.TableBody)-f.outlineNumExamples]
f.printExampleRow(row, max, clr)
if errorMsg != "" {
fmt.Fprintln(f.out, s(f.indent*4)+redb(errorMsg))
// if there is an error
if msg != "" {
fmt.Fprintln(f.out, s(f.indent*4)+redb(msg))
}
}
func (f *pretty) printTableRow(row *messages.GherkinDocument_Feature_TableRow, max []int, clr colors.ColorFunc) {
func (f *pretty) printExampleRow(row *gherkin.TableRow, max []int, clr colors.ColorFunc) {
cells := make([]string, len(row.Cells))
for i, cell := range row.Cells {
val := clr(cell.Value)
ln := utf8.RuneCountInString(val)
cells[i] = val + s(max[i]-ln)
}
fmt.Fprintln(f.out, s(f.indent*3)+"| "+strings.Join(cells, " | ")+" |")
}
func (f *pretty) printTableHeader(row *messages.GherkinDocument_Feature_TableRow, max []int) {
f.printTableRow(row, max, cyan)
func (f *pretty) printExampleHeader(example *gherkin.Examples, max []int) {
cells := make([]string, len(example.TableHeader.Cells))
// an example table header
fmt.Fprintln(f.out, "")
fmt.Fprintln(f.out, s(f.indent*2)+keywordAndName(example.Keyword, example.Name))
for i, cell := range example.TableHeader.Cells {
val := cyan(cell.Value)
ln := utf8.RuneCountInString(val)
cells[i] = val + s(max[i]-ln)
}
fmt.Fprintln(f.out, s(f.indent*3)+"| "+strings.Join(cells, " | ")+" |")
}
func (f *pretty) printStep(result *stepResult) {
astBackground := f.findBackground(result.owner.AstNodeIds[0])
astScenario := f.findScenario(result.owner.AstNodeIds[0])
astStep := f.findStep(result.step.AstNodeIds[0])
var backgroundSteps int
if astBackground != nil {
backgroundSteps = len(astBackground.Steps)
func (f *pretty) printStep(step *gherkin.Step, def *StepDef, c colors.ColorFunc) {
text := s(f.indent*2) + c(strings.TrimSpace(step.Keyword)) + " "
switch {
case def != nil:
if m := def.Expr.FindStringSubmatchIndex(step.Text)[2:]; len(m) > 0 {
var pos, i int
for pos, i = 0, 0; i < len(m); i++ {
if m[i] == -1 {
continue // no index for this match
}
if math.Mod(float64(i), 2) == 0 {
text += c(step.Text[pos:m[i]])
} else {
text += colors.Bold(c)(step.Text[pos:m[i]])
}
pos = m[i]
}
text += c(step.Text[pos:len(step.Text)])
} else {
text += c(step.Text)
}
text += s(f.commentPos-f.length(step)+1) + blackb(fmt.Sprintf("# %s", def.definitionID()))
default:
text += c(step.Text)
}
astBackgroundStep := backgroundSteps > 0 && backgroundSteps >= len(f.lastFeature().lastPickleResult().stepResults)
if astBackgroundStep {
if len(f.lastFeature().pickleResults) > 1 {
return
}
firstExecutedBackgroundStep := astBackground != nil && len(f.lastFeature().lastPickleResult().stepResults) == 1
if firstExecutedBackgroundStep {
fmt.Fprintln(f.out, "\n"+s(f.indent)+keywordAndName(astBackground.Keyword, astBackground.Name))
}
}
if !astBackgroundStep && len(astScenario.Examples) > 0 {
f.printOutlineExample(result.owner, backgroundSteps)
return
}
scenarioHeaderLength, maxLength := f.scenarioLengths(result.owner.AstNodeIds[0])
stepLength := f.lengthPickleStep(astStep.Keyword, astStep.Text)
firstExecutedScenarioStep := len(f.lastFeature().lastPickleResult().stepResults) == backgroundSteps+1
if !astBackgroundStep && firstExecutedScenarioStep {
f.printScenarioHeader(astScenario, maxLength-scenarioHeaderLength)
}
text := s(f.indent*2) + result.status.clr()(strings.TrimSpace(astStep.Keyword)) + " " + result.status.clr()(astStep.Text)
if result.def != nil {
text += s(maxLength - stepLength + 1)
text += blackb("# " + result.def.definitionID())
}
fmt.Fprintln(f.out, text)
if table := result.step.Argument.GetDataTable(); table != nil {
f.printTable(table, cyan)
switch t := step.Argument.(type) {
case *gherkin.DataTable:
f.printTable(t, c)
case *gherkin.DocString:
var ct string
if len(t.ContentType) > 0 {
ct = " " + c(t.ContentType)
}
fmt.Fprintln(f.out, s(f.indent*3)+c(t.Delimitter)+ct)
for _, ln := range strings.Split(t.Content, "\n") {
fmt.Fprintln(f.out, s(f.indent*3)+c(ln))
}
fmt.Fprintln(f.out, s(f.indent*3)+c(t.Delimitter))
}
}
if docString := astStep.GetDocString(); docString != nil {
f.printDocString(docString)
func (f *pretty) printStepKind(res *stepResult) {
f.steps--
if f.outline != nil {
f.outlineSteps = append(f.outlineSteps, res)
}
var bgStep bool
bg := f.feature.Background
// if has not printed background yet
switch {
// first background step
case f.bgSteps > 0 && f.bgSteps == len(bg.Steps):
f.commentPos = f.longestStep(bg.Steps, f.length(bg))
fmt.Fprintln(f.out, "\n"+s(f.indent)+keywordAndName(bg.Keyword, bg.Name))
f.bgSteps--
bgStep = true
// subsequent background steps
case f.bgSteps > 0:
f.bgSteps--
bgStep = true
// first step of scenario, print header and calculate comment position
case f.scenario != nil:
// print scenario keyword and value if first example
if !f.scenarioKeyword {
f.commentPos = f.longestStep(f.scenario.Steps, f.length(f.scenario))
if bg != nil {
if bgLen := f.longestStep(bg.Steps, f.length(bg)); bgLen > f.commentPos {
f.commentPos = bgLen
}
}
text := s(f.indent) + keywordAndName(f.scenario.Keyword, f.scenario.Name)
text += s(f.commentPos-f.length(f.scenario)+1) + f.line(f.scenario.Location)
fmt.Fprintln(f.out, "\n"+text)
f.scenarioKeyword = true
}
// first step of outline scenario, print header and calculate comment position
case f.outline != nil:
// print scenario keyword and value if first example
if !f.scenarioKeyword {
f.commentPos = f.longestStep(f.outline.Steps, f.length(f.outline))
if bg != nil {
if bgLen := f.longestStep(bg.Steps, f.length(bg)); bgLen > f.commentPos {
f.commentPos = bgLen
}
}
text := s(f.indent) + keywordAndName(f.outline.Keyword, f.outline.Name)
text += s(f.commentPos-f.length(f.outline)+1) + f.line(f.outline.Location)
fmt.Fprintln(f.out, "\n"+text)
f.scenarioKeyword = true
}
if len(f.outlineSteps) == len(f.outline.Steps)+f.totalBgSteps {
// an outline example steps has went through
f.printOutlineExample(f.outline)
f.outlineNumExamples--
}
return
}
if result.err != nil {
fmt.Fprintln(f.out, s(f.indent*2)+redb(fmt.Sprintf("%+v", result.err)))
if !f.isBackgroundStep(res.step) || bgStep {
f.printStep(res.step, res.def, res.typ.clr())
}
if result.status == pending {
if res.err != nil {
fmt.Fprintln(f.out, s(f.indent*2)+redb(fmt.Sprintf("%+v", res.err)))
}
if res.typ == pending {
fmt.Fprintln(f.out, s(f.indent*3)+yellow("TODO: write pending definition"))
}
}
func (f *pretty) printDocString(docString *messages.GherkinDocument_Feature_Step_DocString) {
var ct string
if len(docString.MediaType) > 0 {
ct = " " + cyan(docString.MediaType)
func (f *pretty) isBackgroundStep(step *gherkin.Step) bool {
if f.feature.Background == nil {
return false
}
fmt.Fprintln(f.out, s(f.indent*3)+cyan(docString.Delimiter)+ct)
for _, ln := range strings.Split(docString.Content, "\n") {
fmt.Fprintln(f.out, s(f.indent*3)+cyan(ln))
for _, bstep := range f.feature.Background.Steps {
if bstep.Location.Line == step.Location.Line {
return true
}
fmt.Fprintln(f.out, s(f.indent*3)+cyan(docString.Delimiter))
}
return false
}
// print table with aligned table cells
// @TODO: need to make example header cells bold
func (f *pretty) printTable(t *messages.PickleStepArgument_PickleTable, c colors.ColorFunc) {
maxColLengths := maxColLengths(t, c)
func (f *pretty) printTable(t *gherkin.DataTable, c colors.ColorFunc) {
var l = longest(t, c)
var cols = make([]string, len(t.Rows[0].Cells))
for _, row := range t.Rows {
for i, cell := range row.Cells {
val := c(cell.Value)
colLength := utf8.RuneCountInString(val)
cols[i] = val + s(maxColLengths[i]-colLength)
ln := utf8.RuneCountInString(val)
cols[i] = val + s(l[i]-ln)
}
fmt.Fprintln(f.out, s(f.indent*3)+"| "+strings.Join(cols, " | ")+" |")
}
}
func (f *pretty) Passed(step *gherkin.Step, match *StepDef) {
f.basefmt.Passed(step, match)
f.printStepKind(f.passed[len(f.passed)-1])
}
func (f *pretty) Skipped(step *gherkin.Step, match *StepDef) {
f.basefmt.Skipped(step, match)
f.printStepKind(f.skipped[len(f.skipped)-1])
}
func (f *pretty) Undefined(step *gherkin.Step, match *StepDef) {
f.basefmt.Undefined(step, match)
f.printStepKind(f.undefined[len(f.undefined)-1])
}
func (f *pretty) Failed(step *gherkin.Step, match *StepDef, err error) {
f.basefmt.Failed(step, match, err)
f.printStepKind(f.failed[len(f.failed)-1])
}
func (f *pretty) Pending(step *gherkin.Step, match *StepDef) {
f.basefmt.Pending(step, match)
f.printStepKind(f.pending[len(f.pending)-1])
}
// longest gives a list of longest columns of all rows in Table
func maxColLengths(t *messages.PickleStepArgument_PickleTable, clrs ...colors.ColorFunc) []int {
if t == nil {
return []int{}
func longest(tbl interface{}, clrs ...colors.ColorFunc) []int {
var rows []*gherkin.TableRow
switch t := tbl.(type) {
case *gherkin.Examples:
rows = append(rows, t.TableHeader)
rows = append(rows, t.TableBody...)
case *gherkin.DataTable:
rows = append(rows, t.Rows...)
}
longest := make([]int, len(t.Rows[0].Cells))
for _, row := range t.Rows {
longest := make([]int, len(rows[0].Cells))
for _, row := range rows {
for i, cell := range row.Cells {
for _, c := range clrs {
ln := utf8.RuneCountInString(c(cell.Value))
@ -398,71 +458,35 @@ func maxColLengths(t *messages.PickleStepArgument_PickleTable, clrs ...colors.Co
}
}
}
return longest
}
func longestExampleRow(t *messages.GherkinDocument_Feature_Scenario_Examples, clrs ...colors.ColorFunc) []int {
if t == nil {
return []int{}
}
longest := make([]int, len(t.TableHeader.Cells))
for i, cell := range t.TableHeader.Cells {
for _, c := range clrs {
ln := utf8.RuneCountInString(c(cell.Value))
if longest[i] < ln {
longest[i] = ln
}
}
ln := utf8.RuneCountInString(cell.Value)
if longest[i] < ln {
longest[i] = ln
}
}
for _, row := range t.TableBody {
for i, cell := range row.Cells {
for _, c := range clrs {
ln := utf8.RuneCountInString(c(cell.Value))
if longest[i] < ln {
longest[i] = ln
}
}
ln := utf8.RuneCountInString(cell.Value)
if longest[i] < ln {
longest[i] = ln
}
}
}
return longest
}
func (f *pretty) longestStep(steps []*messages.GherkinDocument_Feature_Step, pickleLength int) int {
max := pickleLength
func (f *pretty) longestStep(steps []*gherkin.Step, base int) int {
ret := base
for _, step := range steps {
length := f.lengthPickleStep(step.Keyword, step.Text)
if length > max {
max = length
length := f.length(step)
if length > ret {
ret = length
}
}
return max
return ret
}
// a line number representation in feature file
func (f *pretty) line(loc *messages.Location) string {
return " " + blackb(fmt.Sprintf("# %s:%d", f.lastFeature().Path, loc.Line))
func (f *pretty) line(loc *gherkin.Location) string {
return blackb(fmt.Sprintf("# %s:%d", f.features[len(f.features)-1].Path, loc.Line))
}
func (f *pretty) lengthPickleStep(keyword, text string) int {
return f.indent*2 + utf8.RuneCountInString(strings.TrimSpace(keyword)+" "+text)
func (f *pretty) length(node interface{}) int {
switch t := node.(type) {
case *gherkin.Background:
return f.indent + utf8.RuneCountInString(strings.TrimSpace(t.Keyword)+": "+t.Name)
case *gherkin.Step:
return f.indent*2 + utf8.RuneCountInString(strings.TrimSpace(t.Keyword)+" "+t.Text)
case *gherkin.Scenario:
return f.indent + utf8.RuneCountInString(strings.TrimSpace(t.Keyword)+": "+t.Name)
case *gherkin.ScenarioOutline:
return f.indent + utf8.RuneCountInString(strings.TrimSpace(t.Keyword)+": "+t.Name)
}
func (f *pretty) lengthPickle(keyword, name string) int {
return f.indent + utf8.RuneCountInString(strings.TrimSpace(keyword)+": "+name)
panic(fmt.Sprintf("unexpected node %T to determine length", node))
}

Просмотреть файл

@ -6,7 +6,7 @@ import (
"math"
"strings"
"github.com/cucumber/messages-go/v9"
"github.com/cucumber/godog/gherkin"
)
func init() {
@ -37,39 +37,21 @@ func (f *progress) Summary() {
fmt.Fprintf(f.out, " %d\n", *f.steps)
}
}
var failedStepsOutput []string
for _, sr := range f.findStepResults(failed) {
if sr.status == failed {
sc := f.findScenario(sr.owner.AstNodeIds[0])
scenarioDesc := fmt.Sprintf("%s: %s", sc.Keyword, sr.owner.Name)
scenarioLine := fmt.Sprintf("%s:%d", sr.owner.Uri, sc.Location.Line)
step := f.findStep(sr.step.AstNodeIds[0])
stepDesc := strings.TrimSpace(step.Keyword) + " " + sr.step.Text
stepLine := fmt.Sprintf("%s:%d", sr.owner.Uri, step.Location.Line)
failedStepsOutput = append(
failedStepsOutput,
s(2)+red(scenarioDesc)+blackb(" # "+scenarioLine),
s(4)+red(stepDesc)+blackb(" # "+stepLine),
s(6)+red("Error: ")+redb(fmt.Sprintf("%+v", sr.err)),
"",
)
}
}
if len(failedStepsOutput) > 0 {
fmt.Fprintln(f.out, "\n\n--- "+red("Failed steps:")+"\n")
fmt.Fprint(f.out, strings.Join(failedStepsOutput, "\n"))
}
fmt.Fprintln(f.out, "")
if len(f.failed) > 0 {
fmt.Fprintln(f.out, "\n--- "+red("Failed steps:")+"\n")
for _, fail := range f.failed {
fmt.Fprintln(f.out, s(2)+red(fail.scenarioDesc())+blackb(" # "+fail.scenarioLine()))
fmt.Fprintln(f.out, s(4)+red(strings.TrimSpace(fail.step.Keyword)+" "+fail.step.Text)+blackb(" # "+fail.line()))
fmt.Fprintln(f.out, s(6)+red("Error: ")+redb(fmt.Sprintf("%+v", fail.err))+"\n")
}
}
f.basefmt.Summary()
}
func (f *progress) step(res *stepResult) {
switch res.status {
switch res.typ {
case passed:
fmt.Fprint(f.out, green("."))
case skipped:
@ -89,49 +71,44 @@ func (f *progress) step(res *stepResult) {
}
}
func (f *progress) Passed(pickle *messages.Pickle, step *messages.Pickle_PickleStep, match *StepDefinition) {
f.basefmt.Passed(pickle, step, match)
func (f *progress) Passed(step *gherkin.Step, match *StepDef) {
f.basefmt.Passed(step, match)
f.lock.Lock()
defer f.lock.Unlock()
f.step(f.lastStepResult())
f.step(f.passed[len(f.passed)-1])
}
func (f *progress) Skipped(pickle *messages.Pickle, step *messages.Pickle_PickleStep, match *StepDefinition) {
f.basefmt.Skipped(pickle, step, match)
func (f *progress) Skipped(step *gherkin.Step, match *StepDef) {
f.basefmt.Skipped(step, match)
f.lock.Lock()
defer f.lock.Unlock()
f.step(f.lastStepResult())
f.step(f.skipped[len(f.skipped)-1])
}
func (f *progress) Undefined(pickle *messages.Pickle, step *messages.Pickle_PickleStep, match *StepDefinition) {
f.basefmt.Undefined(pickle, step, match)
func (f *progress) Undefined(step *gherkin.Step, match *StepDef) {
f.basefmt.Undefined(step, match)
f.lock.Lock()
defer f.lock.Unlock()
f.step(f.lastStepResult())
f.step(f.undefined[len(f.undefined)-1])
}
func (f *progress) Failed(pickle *messages.Pickle, step *messages.Pickle_PickleStep, match *StepDefinition, err error) {
f.basefmt.Failed(pickle, step, match, err)
func (f *progress) Failed(step *gherkin.Step, match *StepDef, err error) {
f.basefmt.Failed(step, match, err)
f.lock.Lock()
defer f.lock.Unlock()
f.step(f.lastStepResult())
f.step(f.failed[len(f.failed)-1])
}
func (f *progress) Pending(pickle *messages.Pickle, step *messages.Pickle_PickleStep, match *StepDefinition) {
f.basefmt.Pending(pickle, step, match)
func (f *progress) Pending(step *gherkin.Step, match *StepDef) {
f.basefmt.Pending(step, match)
f.lock.Lock()
defer f.lock.Unlock()
f.step(f.lastStepResult())
f.step(f.pending[len(f.pending)-1])
}
func (f *progress) Sync(cf ConcurrentFormatter) {

Просмотреть файл

@ -3,41 +3,27 @@ package godog
import (
"bytes"
"fmt"
"io/ioutil"
"strings"
"testing"
"github.com/cucumber/gherkin-go/v9"
"github.com/cucumber/messages-go/v9"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/cucumber/godog/colors"
"github.com/cucumber/godog/gherkin"
)
var basicGherkinFeature = `
Feature: basic
Scenario: passing scenario
When one
Then two
`
func TestProgressFormatterOutput(t *testing.T) {
const path = "any.feature"
gd, err := gherkin.ParseGherkinDocument(strings.NewReader(sampleGherkinFeature), (&messages.Incrementing{}).NewId)
require.NoError(t, err)
pickles := gherkin.Pickles(*gd, path, (&messages.Incrementing{}).NewId)
feat, err := gherkin.ParseFeature(strings.NewReader(sampleGherkinFeature))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
var buf bytes.Buffer
w := colors.Uncolored(&buf)
r := runner{
fmt: progressFunc("progress", w),
features: []*feature{{
GherkinDocument: gd,
pickles: pickles,
Path: path,
features: []*feature{&feature{
Path: "any.feature",
Feature: feat,
Content: []byte(sampleGherkinFeature),
}},
initializer: func(s *Suite) {
@ -81,52 +67,61 @@ func FeatureContext(s *godog.Suite) {
s.Step(` + "`^next undefined$`" + `, nextUndefined)
}`
require.True(t, r.run())
expected = trimAllLines(expected)
r.run()
actual := trimAllLines(buf.String())
assert.Equal(t, expected, actual)
shouldMatchOutput(expected, actual, t)
}
var basicGherkinFeature = `
Feature: basic
Scenario: passing scenario
When one
Then two
`
func TestProgressFormatterWhenStepPanics(t *testing.T) {
const path = "any.feature"
gd, err := gherkin.ParseGherkinDocument(strings.NewReader(basicGherkinFeature), (&messages.Incrementing{}).NewId)
require.NoError(t, err)
pickles := gherkin.Pickles(*gd, path, (&messages.Incrementing{}).NewId)
feat, err := gherkin.ParseFeature(strings.NewReader(basicGherkinFeature))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
var buf bytes.Buffer
w := colors.Uncolored(&buf)
r := runner{
fmt: progressFunc("progress", w),
features: []*feature{{GherkinDocument: gd, pickles: pickles}},
features: []*feature{&feature{Feature: feat}},
initializer: func(s *Suite) {
s.Step(`^one$`, func() error { return nil })
s.Step(`^two$`, func() error { panic("omg") })
},
}
require.True(t, r.run())
if !r.run() {
t.Fatal("the suite should have failed")
}
actual := buf.String()
assert.Contains(t, actual, "godog/fmt_progress_test.go:107")
out := buf.String()
if idx := strings.Index(out, "godog/fmt_progress_test.go:100"); idx == -1 {
t.Fatalf("expected to find panic stacktrace, actual:\n%s", out)
}
}
func TestProgressFormatterWithPassingMultisteps(t *testing.T) {
const path = "any.feature"
gd, err := gherkin.ParseGherkinDocument(strings.NewReader(basicGherkinFeature), (&messages.Incrementing{}).NewId)
require.NoError(t, err)
pickles := gherkin.Pickles(*gd, path, (&messages.Incrementing{}).NewId)
feat, err := gherkin.ParseFeature(strings.NewReader(basicGherkinFeature))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
var buf bytes.Buffer
w := colors.Uncolored(&buf)
r := runner{
fmt: progressFunc("progress", w),
features: []*feature{{GherkinDocument: gd, pickles: pickles}},
features: []*feature{&feature{Feature: feat}},
initializer: func(s *Suite) {
s.Step(`^sub1$`, func() error { return nil })
s.Step(`^sub-sub$`, func() error { return nil })
@ -136,22 +131,22 @@ func TestProgressFormatterWithPassingMultisteps(t *testing.T) {
},
}
assert.False(t, r.run())
if r.run() {
t.Fatal("the suite should have passed")
}
}
func TestProgressFormatterWithFailingMultisteps(t *testing.T) {
const path = "some.feature"
gd, err := gherkin.ParseGherkinDocument(strings.NewReader(basicGherkinFeature), (&messages.Incrementing{}).NewId)
require.NoError(t, err)
pickles := gherkin.Pickles(*gd, path, (&messages.Incrementing{}).NewId)
feat, err := gherkin.ParseFeature(strings.NewReader(basicGherkinFeature))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
var buf bytes.Buffer
w := colors.Uncolored(&buf)
r := runner{
fmt: progressFunc("progress", w),
features: []*feature{{GherkinDocument: gd, pickles: pickles, Path: path}},
features: []*feature{&feature{Feature: feat, Path: "some.feature"}},
initializer: func(s *Suite) {
s.Step(`^sub1$`, func() error { return nil })
s.Step(`^sub-sub$`, func() error { return fmt.Errorf("errored") })
@ -161,7 +156,9 @@ func TestProgressFormatterWithFailingMultisteps(t *testing.T) {
},
}
require.True(t, r.run())
if !r.run() {
t.Fatal("the suite should have failed")
}
expected := `
.F 2
@ -181,21 +178,48 @@ Error: sub2: sub-sub: errored
expected = trimAllLines(expected)
actual := trimAllLines(buf.String())
assert.Equal(t, expected, actual)
shouldMatchOutput(expected, actual, t)
}
func shouldMatchOutput(expected, actual string, t *testing.T) {
act := []byte(actual)
exp := []byte(expected)
if len(act) != len(exp) {
t.Fatalf("content lengths do not match, expected: %d, actual %d, actual output:\n%s", len(exp), len(act), actual)
}
for i := 0; i < len(exp); i++ {
if act[i] == exp[i] {
continue
}
cpe := make([]byte, len(exp))
copy(cpe, exp)
e := append(exp[:i], '^')
e = append(e, cpe[i:]...)
cpa := make([]byte, len(act))
copy(cpa, act)
a := append(act[:i], '^')
a = append(a, cpa[i:]...)
t.Fatalf("expected output does not match:\n%s\n\n%s", string(a), string(e))
}
}
func TestProgressFormatterWithPanicInMultistep(t *testing.T) {
const path = "any.feature"
feat, err := gherkin.ParseFeature(strings.NewReader(basicGherkinFeature))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
gd, err := gherkin.ParseGherkinDocument(strings.NewReader(basicGherkinFeature), (&messages.Incrementing{}).NewId)
require.NoError(t, err)
pickles := gherkin.Pickles(*gd, path, (&messages.Incrementing{}).NewId)
var buf bytes.Buffer
w := colors.Uncolored(&buf)
r := runner{
fmt: progressFunc("progress", w),
features: []*feature{{GherkinDocument: gd, pickles: pickles}},
features: []*feature{&feature{Feature: feat}},
initializer: func(s *Suite) {
s.Step(`^sub1$`, func() error { return nil })
s.Step(`^sub-sub$`, func() error { return nil })
@ -205,22 +229,22 @@ func TestProgressFormatterWithPanicInMultistep(t *testing.T) {
},
}
assert.True(t, r.run())
if !r.run() {
t.Fatal("the suite should have failed")
}
}
func TestProgressFormatterMultistepTemplates(t *testing.T) {
const path = "any.feature"
gd, err := gherkin.ParseGherkinDocument(strings.NewReader(basicGherkinFeature), (&messages.Incrementing{}).NewId)
require.NoError(t, err)
pickles := gherkin.Pickles(*gd, path, (&messages.Incrementing{}).NewId)
feat, err := gherkin.ParseFeature(strings.NewReader(basicGherkinFeature))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
var buf bytes.Buffer
w := colors.Uncolored(&buf)
r := runner{
fmt: progressFunc("progress", w),
features: []*feature{{GherkinDocument: gd, pickles: pickles}},
features: []*feature{&feature{Feature: feat}},
initializer: func(s *Suite) {
s.Step(`^sub-sub$`, func() error { return nil })
s.Step(`^substep$`, func() Steps { return Steps{"sub-sub", `unavailable "John" cost 5`, "one", "three"} })
@ -229,7 +253,9 @@ func TestProgressFormatterMultistepTemplates(t *testing.T) {
},
}
require.False(t, r.run())
if r.run() {
t.Fatal("the suite should have passed")
}
expected := `
.U 2
@ -261,13 +287,14 @@ func FeatureContext(s *godog.Suite) {
`
expected = trimAllLines(expected)
actual := trimAllLines(buf.String())
assert.Equal(t, expected, actual)
actual := trimAllLines(buf.String())
if actual != expected {
t.Fatalf("expected output does not match: %s", actual)
}
}
func TestProgressFormatterWhenMultiStepHasArgument(t *testing.T) {
const path = "any.feature"
var featureSource = `
Feature: basic
@ -279,28 +306,26 @@ Feature: basic
text
"""
`
feat, err := gherkin.ParseFeature(strings.NewReader(featureSource))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
gd, err := gherkin.ParseGherkinDocument(strings.NewReader(featureSource), (&messages.Incrementing{}).NewId)
require.NoError(t, err)
pickles := gherkin.Pickles(*gd, path, (&messages.Incrementing{}).NewId)
var buf bytes.Buffer
w := colors.Uncolored(&buf)
r := runner{
fmt: progressFunc("progress", w),
features: []*feature{{GherkinDocument: gd, pickles: pickles}},
fmt: progressFunc("progress", ioutil.Discard),
features: []*feature{&feature{Feature: feat}},
initializer: func(s *Suite) {
s.Step(`^one$`, func() error { return nil })
s.Step(`^two:$`, func(doc *messages.PickleStepArgument_PickleDocString) Steps { return Steps{"one"} })
s.Step(`^two:$`, func(doc *gherkin.DocString) Steps { return Steps{"one"} })
},
}
assert.False(t, r.run())
if r.run() {
t.Fatal("the suite should have passed")
}
}
func TestProgressFormatterWhenMultiStepHasStepWithArgument(t *testing.T) {
const path = "any.feature"
var featureSource = `
Feature: basic
@ -309,10 +334,10 @@ Feature: basic
When one
Then two`
gd, err := gherkin.ParseGherkinDocument(strings.NewReader(featureSource), (&messages.Incrementing{}).NewId)
require.NoError(t, err)
pickles := gherkin.Pickles(*gd, path, (&messages.Incrementing{}).NewId)
feat, err := gherkin.ParseFeature(strings.NewReader(featureSource))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
var subStep = `three:
"""
@ -323,15 +348,17 @@ Feature: basic
w := colors.Uncolored(&buf)
r := runner{
fmt: progressFunc("progress", w),
features: []*feature{{GherkinDocument: gd, pickles: pickles}},
features: []*feature{&feature{Feature: feat}},
initializer: func(s *Suite) {
s.Step(`^one$`, func() error { return nil })
s.Step(`^two$`, func() Steps { return Steps{subStep} })
s.Step(`^three:$`, func(doc *messages.PickleStepArgument_PickleDocString) error { return nil })
s.Step(`^three:$`, func(doc *gherkin.DocString) error { return nil })
},
}
require.True(t, r.run())
if !r.run() {
t.Fatal("the suite should have failed")
}
expected := `
.F 2
@ -339,8 +366,8 @@ Feature: basic
--- Failed steps:
Scenario: passing scenario # any.feature:4
Then two # any.feature:6
Scenario: passing scenario # :4
Then two # :6
Error: nested steps cannot be multiline and have table or content body argument
@ -351,6 +378,7 @@ Feature: basic
expected = trimAllLines(expected)
actual := trimAllLines(buf.String())
assert.Equal(t, expected, actual)
if actual != expected {
t.Fatalf("expected output does not match: %s", actual)
}
}

Просмотреть файл

@ -1,5 +1,5 @@
{"event":"TestRunStarted","version":"0.1.0","timestamp":-6795364578871,"suite":"events"}
{"event":"TestSource","location":"formatter-tests/features/scenario_outline.feature:2","source":"@outline @tag\nFeature: outline\n\n @scenario\n Scenario Outline: outline\n Given passing step\n When passing step\n Then odd \u003codd\u003e and even \u003ceven\u003e number\n\n @tagged\n Examples: tagged\n | odd | even |\n | 1 | 2 |\n | 2 | 0 |\n | 3 | 11 |\n\n @tag2\n Examples:\n | odd | even |\n | 1 | 14 |\n | 3 | 9 |\n"}
{"event":"TestSource","location":"formatter-tests/features/scenario_outline.feature:2","source":"@outline @tag\nFeature: outline\n\n @scenario\n Scenario Outline: outline\n Given passing step\n When passing step\n Then odd \u003codd\u003e and even \u003ceven\u003e number\n\n @tagged\n Examples: tagged\n | odd | even |\n | 1 | 2 |\n | 2 | 0 |\n | 3 | 11 |\n\n @tag2\n Examples:\n | odd | even |\n | 1 | 14 |\n | 3 | 9 |\n\n"}
{"event":"TestCaseStarted","location":"formatter-tests/features/scenario_outline.feature:13","timestamp":-6795364578871}
{"event":"StepDefinitionFound","location":"formatter-tests/features/scenario_outline.feature:6","definition_id":"formatters_print_test.go:63 -\u003e passingStepDef","arguments":[]}
{"event":"TestStepStarted","location":"formatter-tests/features/scenario_outline.feature:6","timestamp":-6795364578871}

Просмотреть файл

@ -19,3 +19,4 @@ Feature: outline
| odd | even |
| 1 | 14 |
| 3 | 9 |

Просмотреть файл

@ -10,6 +10,7 @@
<cyan>Then</cyan> <cyan>passing step</cyan> <bold-black># formatters_print_test.go:63 -> passingStepDef</bold-black>
<bold-white>Scenario:</bold-white> two <bold-black># formatter-tests/features/two_scenarios_with_background_fail.feature:11</bold-black>
<bold-red>step failed</bold-red>
<cyan>Then</cyan> <cyan>passing step</cyan> <bold-black># formatters_print_test.go:63 -> passingStepDef</bold-black>
--- <red>Failed steps:</red>

36
gherkin.go Обычный файл
Просмотреть файл

@ -0,0 +1,36 @@
package godog
import "github.com/cucumber/godog/gherkin"
// examples is a helper func to cast gherkin.Examples
// or gherkin.BaseExamples if its empty
// @TODO: this should go away with gherkin update
func examples(ex interface{}) (*gherkin.Examples, bool) {
t, ok := ex.(*gherkin.Examples)
return t, ok
}
// means there are no scenarios or they do not have steps
func isEmptyFeature(ft *gherkin.Feature) bool {
for _, def := range ft.ScenarioDefinitions {
if !isEmptyScenario(def) {
return false
}
}
return true
}
// means scenario dooes not have steps
func isEmptyScenario(def interface{}) bool {
switch t := def.(type) {
case *gherkin.Scenario:
if len(t.Steps) > 0 {
return false
}
case *gherkin.ScenarioOutline:
if len(t.Steps) > 0 {
return false
}
}
return true
}

21
gherkin/LICENSE Обычный файл
Просмотреть файл

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2014-2016 Cucumber Ltd, Gaspar Nagy
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

3
gherkin/README.md Обычный файл
Просмотреть файл

@ -0,0 +1,3 @@
[![Build Status](https://secure.travis-ci.org/cucumber/gherkin-go.svg)](http://travis-ci.org/cucumber/gherkin-go)
Gherkin parser/compiler for Go. Please see [Gherkin](https://github.com/cucumber/gherkin) for details.

95
gherkin/ast.go Обычный файл
Просмотреть файл

@ -0,0 +1,95 @@
package gherkin
type Location struct {
Line int `json:"line"`
Column int `json:"column"`
}
type Node struct {
Location *Location `json:"location,omitempty"`
Type string `json:"type"`
}
type Feature struct {
Node
Tags []*Tag `json:"tags"`
Language string `json:"language,omitempty"`
Keyword string `json:"keyword"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
Background *Background `json:"background,omitempty"`
ScenarioDefinitions []interface{} `json:"scenarioDefinitions"`
Comments []*Comment `json:"comments"`
}
type Comment struct {
Node
Text string `json:"text"`
}
type Tag struct {
Node
Name string `json:"name"`
}
type Background struct {
ScenarioDefinition
}
type Scenario struct {
ScenarioDefinition
Tags []*Tag `json:"tags"`
}
type ScenarioOutline struct {
ScenarioDefinition
Tags []*Tag `json:"tags"`
Examples []*Examples `json:"examples,omitempty"`
}
type Examples struct {
Node
Tags []*Tag `json:"tags"`
Keyword string `json:"keyword"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
TableHeader *TableRow `json:"tableHeader"`
TableBody []*TableRow `json:"tableBody"`
}
type TableRow struct {
Node
Cells []*TableCell `json:"cells"`
}
type TableCell struct {
Node
Value string `json:"value"`
}
type ScenarioDefinition struct {
Node
Keyword string `json:"keyword"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
Steps []*Step `json:"steps"`
}
type Step struct {
Node
Keyword string `json:"keyword"`
Text string `json:"text"`
Argument interface{} `json:"argument,omitempty"`
}
type DocString struct {
Node
ContentType string `json:"contentType,omitempty"`
Content string `json:"content"`
Delimitter string `json:"-"`
}
type DataTable struct {
Node
Rows []*TableRow `json:"rows"`
}

378
gherkin/astbuilder.go Обычный файл
Просмотреть файл

@ -0,0 +1,378 @@
package gherkin
import (
"strings"
)
type AstBuilder interface {
Builder
GetFeature() *Feature
}
type astBuilder struct {
stack []*astNode
comments []*Comment
}
func (t *astBuilder) Reset() {
t.comments = []*Comment{}
t.stack = []*astNode{}
t.push(newAstNode(RuleType_None))
}
func (t *astBuilder) GetFeature() *Feature {
res := t.currentNode().getSingle(RuleType_Feature)
if val, ok := res.(*Feature); ok {
return val
}
return nil
}
type astNode struct {
ruleType RuleType
subNodes map[RuleType][]interface{}
}
func (a *astNode) add(rt RuleType, obj interface{}) {
a.subNodes[rt] = append(a.subNodes[rt], obj)
}
func (a *astNode) getSingle(rt RuleType) interface{} {
if val, ok := a.subNodes[rt]; ok {
for i := range val {
return val[i]
}
}
return nil
}
func (a *astNode) getItems(rt RuleType) []interface{} {
var res []interface{}
if val, ok := a.subNodes[rt]; ok {
for i := range val {
res = append(res, val[i])
}
}
return res
}
func (a *astNode) getToken(tt TokenType) *Token {
if val, ok := a.getSingle(tt.RuleType()).(*Token); ok {
return val
}
return nil
}
func (a *astNode) getTokens(tt TokenType) []*Token {
var items = a.getItems(tt.RuleType())
var tokens []*Token
for i := range items {
if val, ok := items[i].(*Token); ok {
tokens = append(tokens, val)
}
}
return tokens
}
func (t *astBuilder) currentNode() *astNode {
if len(t.stack) > 0 {
return t.stack[len(t.stack)-1]
}
return nil
}
func newAstNode(rt RuleType) *astNode {
return &astNode{
ruleType: rt,
subNodes: make(map[RuleType][]interface{}),
}
}
func NewAstBuilder() AstBuilder {
builder := new(astBuilder)
builder.comments = []*Comment{}
builder.push(newAstNode(RuleType_None))
return builder
}
func (t *astBuilder) push(n *astNode) {
t.stack = append(t.stack, n)
}
func (t *astBuilder) pop() *astNode {
x := t.stack[len(t.stack)-1]
t.stack = t.stack[:len(t.stack)-1]
return x
}
func (t *astBuilder) Build(tok *Token) (bool, error) {
if tok.Type == TokenType_Comment {
comment := new(Comment)
comment.Type = "Comment"
comment.Location = astLocation(tok)
comment.Text = tok.Text
t.comments = append(t.comments, comment)
} else {
t.currentNode().add(tok.Type.RuleType(), tok)
}
return true, nil
}
func (t *astBuilder) StartRule(r RuleType) (bool, error) {
t.push(newAstNode(r))
return true, nil
}
func (t *astBuilder) EndRule(r RuleType) (bool, error) {
node := t.pop()
transformedNode, err := t.transformNode(node)
t.currentNode().add(node.ruleType, transformedNode)
return true, err
}
func (t *astBuilder) transformNode(node *astNode) (interface{}, error) {
switch node.ruleType {
case RuleType_Step:
stepLine := node.getToken(TokenType_StepLine)
step := new(Step)
step.Type = "Step"
step.Location = astLocation(stepLine)
step.Keyword = stepLine.Keyword
step.Text = stepLine.Text
step.Argument = node.getSingle(RuleType_DataTable)
if step.Argument == nil {
step.Argument = node.getSingle(RuleType_DocString)
}
return step, nil
case RuleType_DocString:
separatorToken := node.getToken(TokenType_DocStringSeparator)
contentType := separatorToken.Text
lineTokens := node.getTokens(TokenType_Other)
var text string
for i := range lineTokens {
if i > 0 {
text += "\n"
}
text += lineTokens[i].Text
}
ds := new(DocString)
ds.Type = "DocString"
ds.Location = astLocation(separatorToken)
ds.ContentType = contentType
ds.Content = text
ds.Delimitter = DOCSTRING_SEPARATOR // TODO: remember separator
return ds, nil
case RuleType_DataTable:
rows, err := astTableRows(node)
dt := new(DataTable)
dt.Type = "DataTable"
dt.Location = rows[0].Location
dt.Rows = rows
return dt, err
case RuleType_Background:
backgroundLine := node.getToken(TokenType_BackgroundLine)
description, _ := node.getSingle(RuleType_Description).(string)
bg := new(Background)
bg.Type = "Background"
bg.Location = astLocation(backgroundLine)
bg.Keyword = backgroundLine.Keyword
bg.Name = backgroundLine.Text
bg.Description = description
bg.Steps = astSteps(node)
return bg, nil
case RuleType_Scenario_Definition:
tags := astTags(node)
scenarioNode, _ := node.getSingle(RuleType_Scenario).(*astNode)
if scenarioNode != nil {
scenarioLine := scenarioNode.getToken(TokenType_ScenarioLine)
description, _ := scenarioNode.getSingle(RuleType_Description).(string)
sc := new(Scenario)
sc.Type = "Scenario"
sc.Tags = tags
sc.Location = astLocation(scenarioLine)
sc.Keyword = scenarioLine.Keyword
sc.Name = scenarioLine.Text
sc.Description = description
sc.Steps = astSteps(scenarioNode)
return sc, nil
} else {
scenarioOutlineNode, ok := node.getSingle(RuleType_ScenarioOutline).(*astNode)
if !ok {
panic("Internal grammar error")
}
scenarioOutlineLine := scenarioOutlineNode.getToken(TokenType_ScenarioOutlineLine)
description, _ := scenarioOutlineNode.getSingle(RuleType_Description).(string)
sc := new(ScenarioOutline)
sc.Type = "ScenarioOutline"
sc.Tags = tags
sc.Location = astLocation(scenarioOutlineLine)
sc.Keyword = scenarioOutlineLine.Keyword
sc.Name = scenarioOutlineLine.Text
sc.Description = description
sc.Steps = astSteps(scenarioOutlineNode)
sc.Examples = astExamples(scenarioOutlineNode)
return sc, nil
}
case RuleType_Examples_Definition:
tags := astTags(node)
examplesNode, _ := node.getSingle(RuleType_Examples).(*astNode)
examplesLine := examplesNode.getToken(TokenType_ExamplesLine)
description, _ := examplesNode.getSingle(RuleType_Description).(string)
allRows, err := astTableRows(examplesNode)
ex := new(Examples)
ex.Type = "Examples"
ex.Tags = tags
ex.Location = astLocation(examplesLine)
ex.Keyword = examplesLine.Keyword
ex.Name = examplesLine.Text
ex.Description = description
ex.TableHeader = allRows[0]
ex.TableBody = allRows[1:]
return ex, err
case RuleType_Description:
lineTokens := node.getTokens(TokenType_Other)
// Trim trailing empty lines
end := len(lineTokens)
for end > 0 && strings.TrimSpace(lineTokens[end-1].Text) == "" {
end--
}
var desc []string
for i := range lineTokens[0:end] {
desc = append(desc, lineTokens[i].Text)
}
return strings.Join(desc, "\n"), nil
case RuleType_Feature:
header, ok := node.getSingle(RuleType_Feature_Header).(*astNode)
if !ok {
return nil, nil
}
tags := astTags(header)
featureLine := header.getToken(TokenType_FeatureLine)
if featureLine == nil {
return nil, nil
}
background, _ := node.getSingle(RuleType_Background).(*Background)
scenarioDefinitions := node.getItems(RuleType_Scenario_Definition)
if scenarioDefinitions == nil {
scenarioDefinitions = []interface{}{}
}
description, _ := header.getSingle(RuleType_Description).(string)
feat := new(Feature)
feat.Type = "Feature"
feat.Tags = tags
feat.Location = astLocation(featureLine)
feat.Language = featureLine.GherkinDialect
feat.Keyword = featureLine.Keyword
feat.Name = featureLine.Text
feat.Description = description
feat.Background = background
feat.ScenarioDefinitions = scenarioDefinitions
feat.Comments = t.comments
return feat, nil
}
return node, nil
}
func astLocation(t *Token) *Location {
return &Location{
Line: t.Location.Line,
Column: t.Location.Column,
}
}
func astTableRows(t *astNode) (rows []*TableRow, err error) {
rows = []*TableRow{}
tokens := t.getTokens(TokenType_TableRow)
for i := range tokens {
row := new(TableRow)
row.Type = "TableRow"
row.Location = astLocation(tokens[i])
row.Cells = astTableCells(tokens[i])
rows = append(rows, row)
}
err = ensureCellCount(rows)
return
}
func ensureCellCount(rows []*TableRow) error {
if len(rows) <= 1 {
return nil
}
cellCount := len(rows[0].Cells)
for i := range rows {
if cellCount != len(rows[i].Cells) {
return &parseError{"inconsistent cell count within the table", &Location{
Line: rows[i].Location.Line,
Column: rows[i].Location.Column,
}}
}
}
return nil
}
func astTableCells(t *Token) (cells []*TableCell) {
cells = []*TableCell{}
for i := range t.Items {
item := t.Items[i]
cell := new(TableCell)
cell.Type = "TableCell"
cell.Location = &Location{
Line: t.Location.Line,
Column: item.Column,
}
cell.Value = item.Text
cells = append(cells, cell)
}
return
}
func astSteps(t *astNode) (steps []*Step) {
steps = []*Step{}
tokens := t.getItems(RuleType_Step)
for i := range tokens {
step, _ := tokens[i].(*Step)
steps = append(steps, step)
}
return
}
func astExamples(t *astNode) (examples []*Examples) {
examples = []*Examples{}
tokens := t.getItems(RuleType_Examples_Definition)
for i := range tokens {
example, _ := tokens[i].(*Examples)
examples = append(examples, example)
}
return
}
func astTags(node *astNode) (tags []*Tag) {
tags = []*Tag{}
tagsNode, ok := node.getSingle(RuleType_Tags).(*astNode)
if !ok {
return
}
tokens := tagsNode.getTokens(TokenType_TagLine)
for i := range tokens {
token := tokens[i]
for k := range token.Items {
item := token.Items[k]
tag := new(Tag)
tag.Type = "Tag"
tag.Location = &Location{
Line: token.Location.Line,
Column: item.Column,
}
tag.Name = item.Text
tags = append(tags, tag)
}
}
return
}

47
gherkin/dialect.go Обычный файл
Просмотреть файл

@ -0,0 +1,47 @@
package gherkin
type GherkinDialect struct {
Language string
Name string
Native string
Keywords map[string][]string
}
func (g *GherkinDialect) FeatureKeywords() []string {
return g.Keywords["feature"]
}
func (g *GherkinDialect) ScenarioKeywords() []string {
return g.Keywords["scenario"]
}
func (g *GherkinDialect) StepKeywords() []string {
result := g.Keywords["given"]
result = append(result, g.Keywords["when"]...)
result = append(result, g.Keywords["then"]...)
result = append(result, g.Keywords["and"]...)
result = append(result, g.Keywords["but"]...)
return result
}
func (g *GherkinDialect) BackgroundKeywords() []string {
return g.Keywords["background"]
}
func (g *GherkinDialect) ScenarioOutlineKeywords() []string {
return g.Keywords["scenarioOutline"]
}
func (g *GherkinDialect) ExamplesKeywords() []string {
return g.Keywords["examples"]
}
type GherkinDialectProvider interface {
GetDialect(language string) *GherkinDialect
}
type gherkinDialectMap map[string]*GherkinDialect
func (g gherkinDialectMap) GetDialect(language string) *GherkinDialect {
return g[language]
}

2988
gherkin/dialects_builtin.go Обычный файл

Различия файлов не показаны, т.к. их слишком много Показать различия

137
gherkin/gherkin.go Обычный файл
Просмотреть файл

@ -0,0 +1,137 @@
package gherkin
import (
"bufio"
"fmt"
"io"
"strings"
)
type Parser interface {
StopAtFirstError(b bool)
Parse(s Scanner, m Matcher) (err error)
}
/*
The scanner reads a gherkin doc (typically read from a .feature file) and creates a token for
each line. The tokens are passed to the parser, which outputs an AST (Abstract Syntax Tree).
If the scanner sees a # language header, it will reconfigure itself dynamically to look for
Gherkin keywords for the associated language. The keywords are defined in gherkin-languages.json.
*/
type Scanner interface {
Scan() (line *Line, atEof bool, err error)
}
type Builder interface {
Build(*Token) (bool, error)
StartRule(RuleType) (bool, error)
EndRule(RuleType) (bool, error)
Reset()
}
type Token struct {
Type TokenType
Keyword string
Text string
Items []*LineSpan
GherkinDialect string
Indent string
Location *Location
}
func (t *Token) IsEOF() bool {
return t.Type == TokenType_EOF
}
func (t *Token) String() string {
return fmt.Sprintf("%s: %s/%s", t.Type.Name(), t.Keyword, t.Text)
}
type LineSpan struct {
Column int
Text string
}
func (l *LineSpan) String() string {
return fmt.Sprintf("%d:%s", l.Column, l.Text)
}
type parser struct {
builder Builder
stopAtFirstError bool
}
func NewParser(b Builder) Parser {
return &parser{
builder: b,
}
}
func (p *parser) StopAtFirstError(b bool) {
p.stopAtFirstError = b
}
func NewScanner(r io.Reader) Scanner {
return &scanner{
s: bufio.NewScanner(r),
line: 0,
}
}
type scanner struct {
s *bufio.Scanner
line int
}
func (t *scanner) Scan() (line *Line, atEof bool, err error) {
scanning := t.s.Scan()
if !scanning {
err = t.s.Err()
if err == nil {
atEof = true
}
}
if err == nil {
t.line += 1
str := t.s.Text()
line = &Line{str, t.line, strings.TrimLeft(str, " \t"), atEof}
}
return
}
type Line struct {
LineText string
LineNumber int
TrimmedLineText string
AtEof bool
}
func (g *Line) Indent() int {
return len(g.LineText) - len(g.TrimmedLineText)
}
func (g *Line) IsEmpty() bool {
return len(g.TrimmedLineText) == 0
}
func (g *Line) IsEof() bool {
return g.AtEof
}
func (g *Line) StartsWith(prefix string) bool {
return strings.HasPrefix(g.TrimmedLineText, prefix)
}
func ParseFeature(in io.Reader) (feature *Feature, err error) {
builder := NewAstBuilder()
parser := NewParser(builder)
parser.StopAtFirstError(false)
matcher := NewMatcher(GherkinDialectsBuildin())
scanner := NewScanner(in)
err = parser.Parse(scanner, matcher)
return builder.GetFeature(), err
}

270
gherkin/matcher.go Обычный файл
Просмотреть файл

@ -0,0 +1,270 @@
package gherkin
import (
"regexp"
"strings"
"unicode/utf8"
)
const (
DEFAULT_DIALECT = "en"
COMMENT_PREFIX = "#"
TAG_PREFIX = "@"
TITLE_KEYWORD_SEPARATOR = ":"
TABLE_CELL_SEPARATOR = '|'
ESCAPE_CHAR = '\\'
ESCAPED_NEWLINE = 'n'
DOCSTRING_SEPARATOR = "\"\"\""
DOCSTRING_ALTERNATIVE_SEPARATOR = "```"
)
type matcher struct {
gdp GherkinDialectProvider
default_lang string
lang string
dialect *GherkinDialect
activeDocStringSeparator string
indentToRemove int
languagePattern *regexp.Regexp
}
func NewMatcher(gdp GherkinDialectProvider) Matcher {
return &matcher{
gdp: gdp,
default_lang: DEFAULT_DIALECT,
lang: DEFAULT_DIALECT,
dialect: gdp.GetDialect(DEFAULT_DIALECT),
languagePattern: regexp.MustCompile("^\\s*#\\s*language\\s*:\\s*([a-zA-Z\\-_]+)\\s*$"),
}
}
func NewLanguageMatcher(gdp GherkinDialectProvider, language string) Matcher {
return &matcher{
gdp: gdp,
default_lang: language,
lang: language,
dialect: gdp.GetDialect(language),
languagePattern: regexp.MustCompile("^\\s*#\\s*language\\s*:\\s*([a-zA-Z\\-_]+)\\s*$"),
}
}
func (m *matcher) Reset() {
m.indentToRemove = 0
m.activeDocStringSeparator = ""
if m.lang != "en" {
m.dialect = m.gdp.GetDialect(m.default_lang)
m.lang = "en"
}
}
func (m *matcher) newTokenAtLocation(line, index int) (token *Token) {
column := index + 1
token = new(Token)
token.GherkinDialect = m.lang
token.Location = &Location{line, column}
return
}
func (m *matcher) MatchEOF(line *Line) (ok bool, token *Token, err error) {
if line.IsEof() {
token, ok = m.newTokenAtLocation(line.LineNumber, line.Indent()), true
token.Type = TokenType_EOF
}
return
}
func (m *matcher) MatchEmpty(line *Line) (ok bool, token *Token, err error) {
if line.IsEmpty() {
token, ok = m.newTokenAtLocation(line.LineNumber, line.Indent()), true
token.Type = TokenType_Empty
}
return
}
func (m *matcher) MatchComment(line *Line) (ok bool, token *Token, err error) {
if line.StartsWith(COMMENT_PREFIX) {
token, ok = m.newTokenAtLocation(line.LineNumber, 0), true
token.Type = TokenType_Comment
token.Text = line.LineText
}
return
}
func (m *matcher) MatchTagLine(line *Line) (ok bool, token *Token, err error) {
if line.StartsWith(TAG_PREFIX) {
var tags []*LineSpan
var column = line.Indent()
splits := strings.Split(line.TrimmedLineText, TAG_PREFIX)
for i := range splits {
txt := strings.Trim(splits[i], " ")
if txt != "" {
tags = append(tags, &LineSpan{column, TAG_PREFIX + txt})
}
column = column + len(splits[i]) + 1
}
token, ok = m.newTokenAtLocation(line.LineNumber, line.Indent()), true
token.Type = TokenType_TagLine
token.Items = tags
}
return
}
func (m *matcher) matchTitleLine(line *Line, tokenType TokenType, keywords []string) (ok bool, token *Token, err error) {
for i := range keywords {
keyword := keywords[i]
if line.StartsWith(keyword + TITLE_KEYWORD_SEPARATOR) {
token, ok = m.newTokenAtLocation(line.LineNumber, line.Indent()), true
token.Type = tokenType
token.Keyword = keyword
token.Text = strings.Trim(line.TrimmedLineText[len(keyword)+1:], " ")
return
}
}
return
}
func (m *matcher) MatchFeatureLine(line *Line) (ok bool, token *Token, err error) {
return m.matchTitleLine(line, TokenType_FeatureLine, m.dialect.FeatureKeywords())
}
func (m *matcher) MatchBackgroundLine(line *Line) (ok bool, token *Token, err error) {
return m.matchTitleLine(line, TokenType_BackgroundLine, m.dialect.BackgroundKeywords())
}
func (m *matcher) MatchScenarioLine(line *Line) (ok bool, token *Token, err error) {
return m.matchTitleLine(line, TokenType_ScenarioLine, m.dialect.ScenarioKeywords())
}
func (m *matcher) MatchScenarioOutlineLine(line *Line) (ok bool, token *Token, err error) {
return m.matchTitleLine(line, TokenType_ScenarioOutlineLine, m.dialect.ScenarioOutlineKeywords())
}
func (m *matcher) MatchExamplesLine(line *Line) (ok bool, token *Token, err error) {
return m.matchTitleLine(line, TokenType_ExamplesLine, m.dialect.ExamplesKeywords())
}
func (m *matcher) MatchStepLine(line *Line) (ok bool, token *Token, err error) {
keywords := m.dialect.StepKeywords()
for i := range keywords {
keyword := keywords[i]
if line.StartsWith(keyword) {
token, ok = m.newTokenAtLocation(line.LineNumber, line.Indent()), true
token.Type = TokenType_StepLine
token.Keyword = keyword
token.Text = strings.Trim(line.TrimmedLineText[len(keyword):], " ")
return
}
}
return
}
func (m *matcher) MatchDocStringSeparator(line *Line) (ok bool, token *Token, err error) {
if m.activeDocStringSeparator != "" {
if line.StartsWith(m.activeDocStringSeparator) {
// close
token, ok = m.newTokenAtLocation(line.LineNumber, line.Indent()), true
token.Type = TokenType_DocStringSeparator
m.indentToRemove = 0
m.activeDocStringSeparator = ""
}
return
}
if line.StartsWith(DOCSTRING_SEPARATOR) {
m.activeDocStringSeparator = DOCSTRING_SEPARATOR
} else if line.StartsWith(DOCSTRING_ALTERNATIVE_SEPARATOR) {
m.activeDocStringSeparator = DOCSTRING_ALTERNATIVE_SEPARATOR
}
if m.activeDocStringSeparator != "" {
// open
contentType := line.TrimmedLineText[len(m.activeDocStringSeparator):]
m.indentToRemove = line.Indent()
token, ok = m.newTokenAtLocation(line.LineNumber, line.Indent()), true
token.Type = TokenType_DocStringSeparator
token.Text = contentType
}
return
}
func (m *matcher) MatchTableRow(line *Line) (ok bool, token *Token, err error) {
var firstChar, firstPos = utf8.DecodeRuneInString(line.TrimmedLineText)
if firstChar == TABLE_CELL_SEPARATOR {
var cells []*LineSpan
var cell []rune
var startCol = line.Indent() + 2 // column where the current cell started
// start after the first separator, it's not included in the cell
for i, w, col := firstPos, 0, startCol; i < len(line.TrimmedLineText); i += w {
var char rune
char, w = utf8.DecodeRuneInString(line.TrimmedLineText[i:])
if char == TABLE_CELL_SEPARATOR {
// append current cell
txt := string(cell)
txtTrimmed := strings.TrimLeft(txt, " ")
ind := len(txt) - len(txtTrimmed)
cells = append(cells, &LineSpan{startCol + ind, strings.TrimRight(txtTrimmed, " ")})
// start building next
cell = make([]rune, 0)
startCol = col + 1
} else if char == ESCAPE_CHAR {
// skip this character but count the column
i += w
col++
char, w = utf8.DecodeRuneInString(line.TrimmedLineText[i:])
if char == ESCAPED_NEWLINE {
cell = append(cell, '\n')
} else {
if char != TABLE_CELL_SEPARATOR && char != ESCAPE_CHAR {
cell = append(cell, ESCAPE_CHAR)
}
cell = append(cell, char)
}
} else {
cell = append(cell, char)
}
col++
}
token, ok = m.newTokenAtLocation(line.LineNumber, line.Indent()), true
token.Type = TokenType_TableRow
token.Items = cells
}
return
}
func (m *matcher) MatchLanguage(line *Line) (ok bool, token *Token, err error) {
matches := m.languagePattern.FindStringSubmatch(line.TrimmedLineText)
if len(matches) > 0 {
lang := matches[1]
token, ok = m.newTokenAtLocation(line.LineNumber, line.Indent()), true
token.Type = TokenType_Language
token.Text = lang
dialect := m.gdp.GetDialect(lang)
if dialect == nil {
err = &parseError{"Language not supported: " + lang, token.Location}
} else {
m.lang = lang
m.dialect = dialect
}
}
return
}
func (m *matcher) MatchOther(line *Line) (ok bool, token *Token, err error) {
token, ok = m.newTokenAtLocation(line.LineNumber, 0), true
token.Type = TokenType_Other
element := line.LineText
txt := strings.TrimLeft(element, " ")
if len(element)-len(txt) > m.indentToRemove {
token.Text = m.unescapeDocString(element[m.indentToRemove:])
} else {
token.Text = m.unescapeDocString(txt)
}
return
}
func (m *matcher) unescapeDocString(text string) string {
if m.activeDocStringSeparator != "" {
return strings.Replace(text, "\\\"\\\"\\\"", "\"\"\"", -1)
} else {
return text
}
}

2270
gherkin/parser.go Обычный файл

Различия файлов не показаны, т.к. их слишком много Показать различия

4
go.mod
Просмотреть файл

@ -3,7 +3,7 @@ module github.com/cucumber/godog
go 1.13
require (
github.com/cucumber/gherkin-go/v9 v9.2.0
github.com/cucumber/messages-go/v9 v9.0.3
github.com/DATA-DOG/go-txdb v0.1.3
github.com/go-sql-driver/mysql v1.5.0
github.com/stretchr/testify v1.4.0
)

32
go.sum
Просмотреть файл

@ -1,33 +1,15 @@
github.com/aslakhellesoy/gox v1.0.100/go.mod h1:AJl542QsKKG96COVsv0N74HHzVQgDIQPceVUh1aeU2M=
github.com/cucumber/gherkin-go/v9 v9.2.0 h1:vxpzP4JtfNSDGH4s0u4TIxv+RaX533MCD+XNakz5kLY=
github.com/cucumber/gherkin-go/v9 v9.2.0/go.mod h1:W/+Z5yOowYWXRMlC6lJvM9LFDAFfsicZ1sstjPKfWWQ=
github.com/cucumber/messages-go/v9 v9.0.3 h1:xXYjyj2aUOdkakEJAQIvP+1Bn2gOQNN+pY5pCRZQZzI=
github.com/cucumber/messages-go/v9 v9.0.3/go.mod h1:TICon2O2emBWMY1eeQvog6b+zK5c+puAFO6avjzC/JA=
github.com/DATA-DOG/go-txdb v0.1.3 h1:R4v6OuOcy2O147e2zHxU0B4NDtF+INb5R9q/CV7AEMg=
github.com/DATA-DOG/go-txdb v0.1.3/go.mod h1:DhAhxMXZpUJVGnT+p9IbzJoRKvlArO2pkHjnGX7o0n0=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE=
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls=
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
github.com/hashicorp/go-version v1.0.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo=
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

Просмотреть файл

@ -10,12 +10,11 @@ import (
"strings"
"testing"
"github.com/cucumber/gherkin-go/v9"
"github.com/cucumber/messages-go/v9"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/cucumber/godog/colors"
"github.com/cucumber/godog/gherkin"
)
func okStep() error {
@ -60,16 +59,12 @@ func TestPrintsNoStepDefinitionsIfNoneFound(t *testing.T) {
}
func TestFailsOrPassesBasedOnStrictModeWhenHasPendingSteps(t *testing.T) {
const path = "any.feature"
gd, err := gherkin.ParseGherkinDocument(strings.NewReader(basicGherkinFeature), (&messages.Incrementing{}).NewId)
feat, err := gherkin.ParseFeature(strings.NewReader(basicGherkinFeature))
require.NoError(t, err)
pickles := gherkin.Pickles(*gd, path, (&messages.Incrementing{}).NewId)
r := runner{
fmt: progressFunc("progress", ioutil.Discard),
features: []*feature{{GherkinDocument: gd, pickles: pickles}},
features: []*feature{&feature{Feature: feat}},
initializer: func(s *Suite) {
s.Step(`^one$`, func() error { return nil })
s.Step(`^two$`, func() error { return ErrPending })
@ -83,16 +78,12 @@ func TestFailsOrPassesBasedOnStrictModeWhenHasPendingSteps(t *testing.T) {
}
func TestFailsOrPassesBasedOnStrictModeWhenHasUndefinedSteps(t *testing.T) {
const path = "any.feature"
gd, err := gherkin.ParseGherkinDocument(strings.NewReader(basicGherkinFeature), (&messages.Incrementing{}).NewId)
feat, err := gherkin.ParseFeature(strings.NewReader(basicGherkinFeature))
require.NoError(t, err)
pickles := gherkin.Pickles(*gd, path, (&messages.Incrementing{}).NewId)
r := runner{
fmt: progressFunc("progress", ioutil.Discard),
features: []*feature{{GherkinDocument: gd, pickles: pickles}},
features: []*feature{&feature{Feature: feat}},
initializer: func(s *Suite) {
s.Step(`^one$`, func() error { return nil })
// two - is undefined
@ -106,16 +97,12 @@ func TestFailsOrPassesBasedOnStrictModeWhenHasUndefinedSteps(t *testing.T) {
}
func TestShouldFailOnError(t *testing.T) {
const path = "any.feature"
gd, err := gherkin.ParseGherkinDocument(strings.NewReader(basicGherkinFeature), (&messages.Incrementing{}).NewId)
feat, err := gherkin.ParseFeature(strings.NewReader(basicGherkinFeature))
require.NoError(t, err)
pickles := gherkin.Pickles(*gd, path, (&messages.Incrementing{}).NewId)
r := runner{
fmt: progressFunc("progress", ioutil.Discard),
features: []*feature{{GherkinDocument: gd, pickles: pickles}},
features: []*feature{&feature{Feature: feat}},
initializer: func(s *Suite) {
s.Step(`^one$`, func() error { return nil })
s.Step(`^two$`, func() error { return fmt.Errorf("error") })
@ -256,10 +243,11 @@ type succeedRunTestCase struct {
filename string // expected output file
}
func TestConcurrencyRun(t *testing.T) {
func TestSucceedRun(t *testing.T) {
testCases := []succeedRunTestCase{
{format: "progress", concurrency: 4, filename: "fixtures/progress_output.txt"},
{format: "junit", concurrency: 4, filename: "fixtures/junit_output.xml"},
{format: "cucumber", concurrency: 2, filename: "fixtures/cucumber_output.json"},
}
for _, tc := range testCases {
@ -277,7 +265,7 @@ func TestConcurrencyRun(t *testing.T) {
}
}
func testSucceedRun(t *testing.T, format string, concurrency int, expected string) {
func testSucceedRun(t *testing.T, format string, concurrency int, expectedOutput string) {
output := new(bytes.Buffer)
opt := Options{
@ -294,12 +282,11 @@ func testSucceedRun(t *testing.T, format string, concurrency int, expected strin
b, err := ioutil.ReadAll(output)
require.NoError(t, err)
suiteCtxReg := regexp.MustCompile(`suite_context.go:\d+`)
expected = suiteCtxReg.ReplaceAllString(expected, `suite_context.go:0`)
actual := strings.TrimSpace(string(b))
suiteCtxReg := regexp.MustCompile(`suite_context.go:\d+`)
expectedOutput = suiteCtxReg.ReplaceAllString(expectedOutput, `suite_context.go:0`)
actual = suiteCtxReg.ReplaceAllString(actual, `suite_context.go:0`)
assert.Equalf(t, expected, actual, "[%s]", actual)
assert.Equalf(t, expectedOutput, actual, "[%s]", actual)
}

Просмотреть файл

@ -10,7 +10,7 @@ import (
"strconv"
"strings"
"github.com/cucumber/messages-go/v9"
"github.com/cucumber/godog/gherkin"
)
var matchFuncDefRef = regexp.MustCompile(`\(([^\)]+)\)`)
@ -31,7 +31,7 @@ var matchFuncDefRef = regexp.MustCompile(`\(([^\)]+)\)`)
// will result in main step failure.
type Steps []string
// StepDefinition is a registered step definition
// StepDef is a registered step definition
// contains a StepHandler and regexp which
// is used to match a step. Args which
// were matched by last executed step
@ -39,7 +39,7 @@ type Steps []string
// This structure is passed to the formatter
// when step is matched and is either failed
// or successful
type StepDefinition struct {
type StepDef struct {
args []interface{}
hv reflect.Value
Expr *regexp.Regexp
@ -50,7 +50,7 @@ type StepDefinition struct {
undefined []string
}
func (sd *StepDefinition) definitionID() string {
func (sd *StepDef) definitionID() string {
ptr := sd.hv.Pointer()
f := runtime.FuncForPC(ptr)
file, line := f.FileLine(ptr)
@ -80,7 +80,7 @@ func (sd *StepDefinition) definitionID() string {
// run a step with the matched arguments using
// reflect
func (sd *StepDefinition) run() interface{} {
func (sd *StepDef) run() interface{} {
typ := sd.hv.Type()
if len(sd.args) < typ.NumIn() {
return fmt.Errorf("func expects %d arguments, which is more than %d matched from step", typ.NumIn(), len(sd.args))
@ -168,32 +168,20 @@ func (sd *StepDefinition) run() interface{} {
case reflect.Ptr:
arg := sd.args[i]
switch param.Elem().String() {
case "messages.PickleStepArgument_PickleDocString":
if v, ok := arg.(*messages.PickleStepArgument); ok {
values = append(values, reflect.ValueOf(v.GetDocString()))
break
case "gherkin.DocString":
v, ok := arg.(*gherkin.DocString)
if !ok {
return fmt.Errorf(`cannot convert argument %d: "%v" of type "%T" to *gherkin.DocString`, i, arg, arg)
}
if v, ok := arg.(*messages.PickleStepArgument_PickleDocString); ok {
values = append(values, reflect.ValueOf(v))
break
case "gherkin.DataTable":
v, ok := arg.(*gherkin.DataTable)
if !ok {
return fmt.Errorf(`cannot convert argument %d: "%v" of type "%T" to *gherkin.DocString`, i, arg, arg)
}
return fmt.Errorf(`cannot convert argument %d: "%v" of type "%T" to *messages.PickleStepArgument_PickleDocString`, i, arg, arg)
case "messages.PickleStepArgument_PickleTable":
if v, ok := arg.(*messages.PickleStepArgument); ok {
values = append(values, reflect.ValueOf(v.GetDataTable()))
break
}
if v, ok := arg.(*messages.PickleStepArgument_PickleTable); ok {
values = append(values, reflect.ValueOf(v))
break
}
return fmt.Errorf(`cannot convert argument %d: "%v" of type "%T" to *messages.PickleStepArgument_PickleTable`, i, arg, arg)
default:
return fmt.Errorf("the argument %d type %T is not supported %s", i, arg, param.Elem().String())
return fmt.Errorf("the argument %d type %T is not supported", i, arg)
}
case reflect.Slice:
switch param {
@ -210,11 +198,10 @@ func (sd *StepDefinition) run() interface{} {
return fmt.Errorf("the argument %d type %s is not supported", i, param.Kind())
}
}
return sd.hv.Call(values)[0].Interface()
}
func (sd *StepDefinition) shouldBeString(idx int) (string, error) {
func (sd *StepDef) shouldBeString(idx int) (string, error) {
arg := sd.args[idx]
s, ok := arg.(string)
if !ok {

Просмотреть файл

@ -5,13 +5,13 @@ import (
"strings"
"testing"
"github.com/cucumber/messages-go/v9"
"github.com/cucumber/godog/gherkin"
)
func TestShouldSupportIntTypes(t *testing.T) {
fn := func(a int64, b int32, c int16, d int8) error { return nil }
def := &StepDefinition{
def := &StepDef{
Handler: fn,
hv: reflect.ValueOf(fn),
}
@ -30,7 +30,7 @@ func TestShouldSupportIntTypes(t *testing.T) {
func TestShouldSupportFloatTypes(t *testing.T) {
fn := func(a float64, b float32) error { return nil }
def := &StepDefinition{
def := &StepDef{
Handler: fn,
hv: reflect.ValueOf(fn),
}
@ -48,12 +48,12 @@ func TestShouldSupportFloatTypes(t *testing.T) {
func TestShouldNotSupportOtherPointerTypesThanGherkin(t *testing.T) {
fn1 := func(a *int) error { return nil }
fn2 := func(a *messages.PickleStepArgument_PickleDocString) error { return nil }
fn3 := func(a *messages.PickleStepArgument_PickleTable) error { return nil }
fn2 := func(a *gherkin.DocString) error { return nil }
fn3 := func(a *gherkin.DataTable) error { return nil }
def1 := &StepDefinition{Handler: fn1, hv: reflect.ValueOf(fn1), args: []interface{}{(*int)(nil)}}
def2 := &StepDefinition{Handler: fn2, hv: reflect.ValueOf(fn2), args: []interface{}{&messages.PickleStepArgument_PickleDocString{}}}
def3 := &StepDefinition{Handler: fn3, hv: reflect.ValueOf(fn3), args: []interface{}{(*messages.PickleStepArgument_PickleTable)(nil)}}
def1 := &StepDef{Handler: fn1, hv: reflect.ValueOf(fn1), args: []interface{}{(*int)(nil)}}
def2 := &StepDef{Handler: fn2, hv: reflect.ValueOf(fn2), args: []interface{}{(*gherkin.DocString)(nil)}}
def3 := &StepDef{Handler: fn3, hv: reflect.ValueOf(fn3), args: []interface{}{(*gherkin.DataTable)(nil)}}
if err := def1.run(); err == nil {
t.Fatalf("expected conversion error, but got none")
@ -70,8 +70,8 @@ func TestShouldSupportOnlyByteSlice(t *testing.T) {
fn1 := func(a []byte) error { return nil }
fn2 := func(a []string) error { return nil }
def1 := &StepDefinition{Handler: fn1, hv: reflect.ValueOf(fn1), args: []interface{}{"str"}}
def2 := &StepDefinition{Handler: fn2, hv: reflect.ValueOf(fn2), args: []interface{}{[]string{}}}
def1 := &StepDef{Handler: fn1, hv: reflect.ValueOf(fn1), args: []interface{}{"str"}}
def2 := &StepDef{Handler: fn2, hv: reflect.ValueOf(fn2), args: []interface{}{[]string{}}}
if err := def1.run(); err != nil {
t.Fatalf("unexpected error: %v", err)
@ -83,7 +83,7 @@ func TestShouldSupportOnlyByteSlice(t *testing.T) {
func TestUnexpectedArguments(t *testing.T) {
fn := func(a, b int) error { return nil }
def := &StepDefinition{Handler: fn, hv: reflect.ValueOf(fn)}
def := &StepDef{Handler: fn, hv: reflect.ValueOf(fn)}
def.args = []interface{}{"1"}
if err := def.run(); err == nil {
@ -97,7 +97,7 @@ func TestUnexpectedArguments(t *testing.T) {
// @TODO maybe we should support duration
// fn2 := func(err time.Duration) error { return nil }
// def = &StepDefinition{Handler: fn2, hv: reflect.ValueOf(fn2)}
// def = &StepDef{Handler: fn2, hv: reflect.ValueOf(fn2)}
// def.args = []interface{}{"1"}
// if err := def.run(); err == nil {

481
suite.go
Просмотреть файл

@ -4,6 +4,7 @@ import (
"bytes"
"fmt"
"io"
"math/rand"
"os"
"path/filepath"
"reflect"
@ -14,17 +15,16 @@ import (
"time"
"unicode/utf8"
"github.com/cucumber/gherkin-go/v9"
"github.com/cucumber/messages-go/v9"
"github.com/cucumber/godog/gherkin"
)
var errorInterface = reflect.TypeOf((*error)(nil)).Elem()
var typeOfBytes = reflect.TypeOf([]byte(nil))
type feature struct {
*messages.GherkinDocument
pickles []*messages.Pickle
pickleResults []*pickleResult
*gherkin.Feature
Scenarios []*scenario
time time.Time
Content []byte `json:"-"`
@ -32,118 +32,47 @@ type feature struct {
order int
}
func (f feature) findScenario(astScenarioID string) *messages.GherkinDocument_Feature_Scenario {
for _, child := range f.GherkinDocument.Feature.Children {
if sc := child.GetScenario(); sc != nil && sc.Id == astScenarioID {
return sc
}
}
return nil
}
func (f feature) findBackground(astScenarioID string) *messages.GherkinDocument_Feature_Background {
var bg *messages.GherkinDocument_Feature_Background
for _, child := range f.GherkinDocument.Feature.Children {
if tmp := child.GetBackground(); tmp != nil {
bg = tmp
}
if sc := child.GetScenario(); sc != nil && sc.Id == astScenarioID {
return bg
}
}
return nil
}
func (f feature) findExample(exampleAstID string) (*messages.GherkinDocument_Feature_Scenario_Examples, *messages.GherkinDocument_Feature_TableRow) {
for _, child := range f.GherkinDocument.Feature.Children {
if sc := child.GetScenario(); sc != nil {
for _, example := range sc.Examples {
for _, row := range example.TableBody {
if row.Id == exampleAstID {
return example, row
}
}
}
}
}
return nil, nil
}
func (f feature) findStep(astStepID string) *messages.GherkinDocument_Feature_Step {
for _, child := range f.GherkinDocument.Feature.Children {
if sc := child.GetScenario(); sc != nil {
for _, step := range sc.GetSteps() {
if step.Id == astStepID {
return step
}
}
}
if bg := child.GetBackground(); bg != nil {
for _, step := range bg.GetSteps() {
if step.Id == astStepID {
return step
}
}
}
}
return nil
}
func (f feature) startedAt() time.Time {
return f.time
}
func (f feature) finishedAt() time.Time {
if len(f.pickleResults) == 0 {
if len(f.Scenarios) == 0 {
return f.startedAt()
}
return f.pickleResults[len(f.pickleResults)-1].finishedAt()
return f.Scenarios[len(f.Scenarios)-1].finishedAt()
}
func (f feature) appendStepResult(s *stepResult) {
pickles := f.pickleResults[len(f.pickleResults)-1]
pickles.stepResults = append(pickles.stepResults, s)
}
func (f feature) lastPickleResult() *pickleResult {
return f.pickleResults[len(f.pickleResults)-1]
}
func (f feature) lastStepResult() *stepResult {
last := f.lastPickleResult()
return last.stepResults[len(last.stepResults)-1]
scenario := f.Scenarios[len(f.Scenarios)-1]
scenario.Steps = append(scenario.Steps, s)
}
type sortByName []*feature
func (s sortByName) Len() int { return len(s) }
func (s sortByName) Less(i, j int) bool { return s[i].Feature.Name < s[j].Feature.Name }
func (s sortByName) Less(i, j int) bool { return s[i].Name < s[j].Name }
func (s sortByName) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
type pickleResult struct {
type scenario struct {
Name string
OutlineName string
ExampleNo int
time time.Time
stepResults []*stepResult
Steps []*stepResult
}
func (s pickleResult) startedAt() time.Time {
func (s scenario) startedAt() time.Time {
return s.time
}
func (s pickleResult) finishedAt() time.Time {
if len(s.stepResults) == 0 {
func (s scenario) finishedAt() time.Time {
if len(s.Steps) == 0 {
return s.startedAt()
}
return s.stepResults[len(s.stepResults)-1].time
return s.Steps[len(s.Steps)-1].time
}
// ErrUndefined is returned in case if step definition was not found
@ -165,7 +94,7 @@ var ErrPending = fmt.Errorf("step implementation is pending")
// executions are catching panic error since it may
// be a context specific error.
type Suite struct {
steps []*StepDefinition
steps []*StepDef
features []*feature
fmt Formatter
@ -176,16 +105,16 @@ type Suite struct {
// suite event handlers
beforeSuiteHandlers []func()
beforeFeatureHandlers []func(*messages.GherkinDocument)
beforeScenarioHandlers []func(*messages.Pickle)
beforeStepHandlers []func(*messages.Pickle_PickleStep)
afterStepHandlers []func(*messages.Pickle_PickleStep, error)
afterScenarioHandlers []func(*messages.Pickle, error)
afterFeatureHandlers []func(*messages.GherkinDocument)
beforeFeatureHandlers []func(*gherkin.Feature)
beforeScenarioHandlers []func(interface{})
beforeStepHandlers []func(*gherkin.Step)
afterStepHandlers []func(*gherkin.Step, error)
afterScenarioHandlers []func(interface{}, error)
afterFeatureHandlers []func(*gherkin.Feature)
afterSuiteHandlers []func()
}
// Step allows to register a *StepDefinition in Godog
// Step allows to register a *StepDef in Godog
// feature suite, the definition will be applied
// to all steps matching the given Regexp expr.
//
@ -197,7 +126,7 @@ type Suite struct {
// the same step, then only the first matched handler
// will be applied.
//
// If none of the *StepDefinition is matched, then
// If none of the *StepDef is matched, then
// ErrUndefined error will be returned when
// running steps.
func (s *Suite) Step(expr interface{}, stepFunc interface{}) {
@ -224,7 +153,7 @@ func (s *Suite) Step(expr interface{}, stepFunc interface{}) {
panic(fmt.Sprintf("expected handler to return only one value, but it has: %d", typ.NumOut()))
}
def := &StepDefinition{
def := &StepDef{
Handler: stepFunc,
Expr: regex,
hv: v,
@ -271,28 +200,31 @@ func (s *Suite) BeforeSuite(fn func()) {
// scenario to restart it.
//
// Use it wisely and avoid sharing state between scenarios.
func (s *Suite) BeforeFeature(fn func(*messages.GherkinDocument)) {
func (s *Suite) BeforeFeature(fn func(*gherkin.Feature)) {
s.beforeFeatureHandlers = append(s.beforeFeatureHandlers, fn)
}
// BeforeScenario registers a function or method
// to be run before every pickle.
// to be run before every scenario or scenario outline.
//
// The interface argument may be *gherkin.Scenario
// or *gherkin.ScenarioOutline
//
// It is a good practice to restore the default state
// before every scenario so it would be isolated from
// any kind of state.
func (s *Suite) BeforeScenario(fn func(*messages.Pickle)) {
func (s *Suite) BeforeScenario(fn func(interface{})) {
s.beforeScenarioHandlers = append(s.beforeScenarioHandlers, fn)
}
// BeforeStep registers a function or method
// to be run before every step.
func (s *Suite) BeforeStep(fn func(*messages.Pickle_PickleStep)) {
// to be run before every scenario
func (s *Suite) BeforeStep(fn func(*gherkin.Step)) {
s.beforeStepHandlers = append(s.beforeStepHandlers, fn)
}
// AfterStep registers an function or method
// to be run after every step.
// to be run after every scenario
//
// It may be convenient to return a different kind of error
// in order to print more state details which may help
@ -300,19 +232,22 @@ func (s *Suite) BeforeStep(fn func(*messages.Pickle_PickleStep)) {
//
// In some cases, for example when running a headless
// browser, to take a screenshot after failure.
func (s *Suite) AfterStep(fn func(*messages.Pickle_PickleStep, error)) {
func (s *Suite) AfterStep(fn func(*gherkin.Step, error)) {
s.afterStepHandlers = append(s.afterStepHandlers, fn)
}
// AfterScenario registers an function or method
// to be run after every pickle.
func (s *Suite) AfterScenario(fn func(*messages.Pickle, error)) {
// to be run after every scenario or scenario outline
//
// The interface argument may be *gherkin.Scenario
// or *gherkin.ScenarioOutline
func (s *Suite) AfterScenario(fn func(interface{}, error)) {
s.afterScenarioHandlers = append(s.afterScenarioHandlers, fn)
}
// AfterFeature registers a function or method
// to be run once after feature executed all scenarios.
func (s *Suite) AfterFeature(fn func(*messages.GherkinDocument)) {
func (s *Suite) AfterFeature(fn func(*gherkin.Feature)) {
s.afterFeatureHandlers = append(s.afterFeatureHandlers, fn)
}
@ -341,7 +276,7 @@ func (s *Suite) run() {
}
}
func (s *Suite) matchStep(step *messages.Pickle_PickleStep) *StepDefinition {
func (s *Suite) matchStep(step *gherkin.Step) *StepDef {
def := s.matchStepText(step.Text)
if def != nil && step.Argument != nil {
def.args = append(def.args, step.Argument)
@ -349,14 +284,14 @@ func (s *Suite) matchStep(step *messages.Pickle_PickleStep) *StepDefinition {
return def
}
func (s *Suite) runStep(pickle *messages.Pickle, step *messages.Pickle_PickleStep, prevStepErr error) (err error) {
func (s *Suite) runStep(step *gherkin.Step, prevStepErr error) (err error) {
// run before step handlers
for _, f := range s.beforeStepHandlers {
f(step)
}
match := s.matchStep(step)
s.fmt.Defined(pickle, step, match)
s.fmt.Defined(step, match)
// user multistep definitions may panic
defer func() {
@ -377,11 +312,11 @@ func (s *Suite) runStep(pickle *messages.Pickle, step *messages.Pickle_PickleSte
switch err {
case nil:
s.fmt.Passed(pickle, step, match)
s.fmt.Passed(step, match)
case ErrPending:
s.fmt.Pending(pickle, step, match)
s.fmt.Pending(step, match)
default:
s.fmt.Failed(pickle, step, match, err)
s.fmt.Failed(step, match, err)
}
// run after step handlers
@ -394,7 +329,7 @@ func (s *Suite) runStep(pickle *messages.Pickle, step *messages.Pickle_PickleSte
return err
} else if len(undef) > 0 {
if match != nil {
match = &StepDefinition{
match = &StepDef{
args: match.args,
hv: match.hv,
Expr: match.Expr,
@ -403,12 +338,12 @@ func (s *Suite) runStep(pickle *messages.Pickle, step *messages.Pickle_PickleSte
undefined: undef,
}
}
s.fmt.Undefined(pickle, step, match)
s.fmt.Undefined(step, match)
return ErrUndefined
}
if prevStepErr != nil {
s.fmt.Skipped(pickle, step, match)
s.fmt.Skipped(step, match)
return nil
}
@ -473,7 +408,7 @@ func (s *Suite) maybeSubSteps(result interface{}) error {
return nil
}
func (s *Suite) matchStepText(text string) *StepDefinition {
func (s *Suite) matchStepText(text string) *StepDef {
for _, h := range s.steps {
if m := h.Expr.FindStringSubmatch(text); len(m) > 0 {
var args []interface{}
@ -483,7 +418,7 @@ func (s *Suite) matchStepText(text string) *StepDefinition {
// since we need to assign arguments
// better to copy the step definition
return &StepDefinition{
return &StepDef{
args: args,
hv: h.hv,
Expr: h.Expr,
@ -495,9 +430,9 @@ func (s *Suite) matchStepText(text string) *StepDefinition {
return nil
}
func (s *Suite) runSteps(pickle *messages.Pickle, steps []*messages.Pickle_PickleStep) (err error) {
func (s *Suite) runSteps(steps []*gherkin.Step) (err error) {
for _, step := range steps {
stepErr := s.runStep(pickle, step, err)
stepErr := s.runStep(step, err)
switch stepErr {
case ErrUndefined:
// do not overwrite failed error
@ -514,6 +449,110 @@ func (s *Suite) runSteps(pickle *messages.Pickle, steps []*messages.Pickle_Pickl
return
}
func (s *Suite) runOutline(outline *gherkin.ScenarioOutline, b *gherkin.Background) (failErr error) {
s.fmt.Node(outline)
for _, ex := range outline.Examples {
example, hasExamples := examples(ex)
if !hasExamples {
// @TODO: may need to print empty example node, but
// for backward compatibility, cannot cast to *gherkin.ExamplesBase
// at the moment
continue
}
s.fmt.Node(example)
placeholders := example.TableHeader.Cells
groups := example.TableBody
for _, group := range groups {
if !isEmptyScenario(outline) {
for _, f := range s.beforeScenarioHandlers {
f(outline)
}
}
var steps []*gherkin.Step
for _, outlineStep := range outline.Steps {
text := outlineStep.Text
for i, placeholder := range placeholders {
text = strings.Replace(text, "<"+placeholder.Value+">", group.Cells[i].Value, -1)
}
// translate argument
arg := outlineStep.Argument
switch t := outlineStep.Argument.(type) {
case *gherkin.DataTable:
tbl := &gherkin.DataTable{
Node: t.Node,
Rows: make([]*gherkin.TableRow, len(t.Rows)),
}
for i, row := range t.Rows {
cells := make([]*gherkin.TableCell, len(row.Cells))
for j, cell := range row.Cells {
trans := cell.Value
for i, placeholder := range placeholders {
trans = strings.Replace(trans, "<"+placeholder.Value+">", group.Cells[i].Value, -1)
}
cells[j] = &gherkin.TableCell{
Node: cell.Node,
Value: trans,
}
}
tbl.Rows[i] = &gherkin.TableRow{
Node: row.Node,
Cells: cells,
}
}
arg = tbl
case *gherkin.DocString:
trans := t.Content
for i, placeholder := range placeholders {
trans = strings.Replace(trans, "<"+placeholder.Value+">", group.Cells[i].Value, -1)
}
arg = &gherkin.DocString{
Node: t.Node,
Content: trans,
ContentType: t.ContentType,
Delimitter: t.Delimitter,
}
}
// clone a step
step := &gherkin.Step{
Node: outlineStep.Node,
Text: text,
Keyword: outlineStep.Keyword,
Argument: arg,
}
steps = append(steps, step)
}
// run example table row
s.fmt.Node(group)
if b != nil {
steps = append(b.Steps, steps...)
}
err := s.runSteps(steps)
if !isEmptyScenario(outline) {
for _, f := range s.afterScenarioHandlers {
f(outline, err)
}
}
if s.shouldFail(err) {
failErr = err
if s.stopOnFailure {
return
}
}
}
}
return
}
func (s *Suite) shouldFail(err error) bool {
if err == nil {
return false
@ -527,24 +566,46 @@ func (s *Suite) shouldFail(err error) bool {
}
func (s *Suite) runFeature(f *feature) {
if !isEmptyFeature(f.pickles) {
if !isEmptyFeature(f.Feature) {
for _, fn := range s.beforeFeatureHandlers {
fn(f.GherkinDocument)
fn(f.Feature)
}
}
s.fmt.Feature(f.GherkinDocument, f.Path, f.Content)
s.fmt.Feature(f.Feature, f.Path, f.Content)
// make a local copy of the feature scenario defenitions,
// then shuffle it if we are randomizing scenarios
scenarios := make([]interface{}, len(f.ScenarioDefinitions))
if s.randomSeed != 0 {
r := rand.New(rand.NewSource(s.randomSeed))
perm := r.Perm(len(f.ScenarioDefinitions))
for i, v := range perm {
scenarios[v] = f.ScenarioDefinitions[i]
}
} else {
copy(scenarios, f.ScenarioDefinitions)
}
defer func() {
if !isEmptyFeature(f.pickles) {
if !isEmptyFeature(f.Feature) {
for _, fn := range s.afterFeatureHandlers {
fn(f.GherkinDocument)
fn(f.Feature)
}
}
}()
for _, pickle := range f.pickles {
err := s.runPickle(pickle)
for _, scenario := range scenarios {
var err error
if f.Background != nil {
s.fmt.Node(f.Background)
}
switch t := scenario.(type) {
case *gherkin.ScenarioOutline:
err = s.runOutline(t, f.Background)
case *gherkin.Scenario:
err = s.runScenario(t, f.Background)
}
if s.shouldFail(err) {
s.failed = true
if s.stopOnFailure {
@ -554,35 +615,31 @@ func (s *Suite) runFeature(f *feature) {
}
}
func isEmptyFeature(pickles []*messages.Pickle) bool {
for _, pickle := range pickles {
if len(pickle.Steps) > 0 {
return false
}
}
return true
}
func (s *Suite) runPickle(pickle *messages.Pickle) (err error) {
if len(pickle.Steps) == 0 {
s.fmt.Pickle(pickle)
func (s *Suite) runScenario(scenario *gherkin.Scenario, b *gherkin.Background) (err error) {
if isEmptyScenario(scenario) {
s.fmt.Node(scenario)
return ErrUndefined
}
// run before scenario handlers
for _, f := range s.beforeScenarioHandlers {
f(pickle)
f(scenario)
}
s.fmt.Pickle(pickle)
s.fmt.Node(scenario)
// background
steps := scenario.Steps
if b != nil {
steps = append(b.Steps, steps...)
}
// scenario
err = s.runSteps(pickle, pickle.Steps)
err = s.runSteps(steps)
// run after scenario handlers
for _, f := range s.afterScenarioHandlers {
f(pickle, err)
f(scenario, err)
}
return
@ -621,7 +678,7 @@ func extractFeaturePathLine(p string) (string, int) {
return retPath, line
}
func parseFeatureFile(path string, newIDFunc func() string) (*feature, error) {
func parseFeatureFile(path string) (*feature, error) {
reader, err := os.Open(path)
if err != nil {
return nil, err
@ -629,22 +686,19 @@ func parseFeatureFile(path string, newIDFunc func() string) (*feature, error) {
defer reader.Close()
var buf bytes.Buffer
gherkinDocument, err := gherkin.ParseGherkinDocument(io.TeeReader(reader, &buf), newIDFunc)
ft, err := gherkin.ParseFeature(io.TeeReader(reader, &buf))
if err != nil {
return nil, fmt.Errorf("%s - %v", path, err)
}
pickles := gherkin.Pickles(*gherkinDocument, path, newIDFunc)
return &feature{
GherkinDocument: gherkinDocument,
pickles: pickles,
Content: buf.Bytes(),
Path: path,
Feature: ft,
Content: buf.Bytes(),
}, nil
}
func parseFeatureDir(dir string, newIDFunc func() string) ([]*feature, error) {
func parseFeatureDir(dir string) ([]*feature, error) {
var features []*feature
return features, filepath.Walk(dir, func(p string, f os.FileInfo, err error) error {
if err != nil {
@ -659,7 +713,7 @@ func parseFeatureDir(dir string, newIDFunc func() string) ([]*feature, error) {
return nil
}
feat, err := parseFeatureFile(p, newIDFunc)
feat, err := parseFeatureFile(p)
if err != nil {
return err
}
@ -670,36 +724,39 @@ func parseFeatureDir(dir string, newIDFunc func() string) ([]*feature, error) {
func parsePath(path string) ([]*feature, error) {
var features []*feature
path, line := extractFeaturePathLine(path)
// check if line number is specified
var line int
path, line = extractFeaturePathLine(path)
fi, err := os.Stat(path)
if err != nil {
return features, err
}
newIDFunc := (&messages.Incrementing{}).NewId
if fi.IsDir() {
return parseFeatureDir(path, newIDFunc)
return parseFeatureDir(path)
}
ft, err := parseFeatureFile(path, newIDFunc)
ft, err := parseFeatureFile(path)
if err != nil {
return features, err
}
// filter scenario by line number
var pickles []*messages.Pickle
for _, pickle := range ft.pickles {
sc := ft.findScenario(pickle.AstNodeIds[0])
if line == -1 || uint32(line) == sc.Location.Line {
pickles = append(pickles, pickle)
var scenarios []interface{}
for _, def := range ft.ScenarioDefinitions {
var ln int
switch t := def.(type) {
case *gherkin.Scenario:
ln = t.Location.Line
case *gherkin.ScenarioOutline:
ln = t.Location.Line
}
if line == -1 || ln == line {
scenarios = append(scenarios, def)
}
}
ft.pickles = pickles
ft.ScenarioDefinitions = scenarios
return append(features, ft), nil
}
@ -727,7 +784,6 @@ func parseFeatures(filter string, paths []string) ([]*feature, error) {
byPath[ft.Path] = ft
}
}
return filterFeatures(filter, byPath), nil
}
@ -739,7 +795,7 @@ func (s sortByOrderGiven) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
func filterFeatures(tags string, collected map[string]*feature) (features []*feature) {
for _, ft := range collected {
applyTagFilter(tags, ft)
applyTagFilter(tags, ft.Feature)
features = append(features, ft)
}
@ -748,23 +804,81 @@ func filterFeatures(tags string, collected map[string]*feature) (features []*fea
return features
}
func applyTagFilter(tags string, ft *feature) {
func applyTagFilter(tags string, ft *gherkin.Feature) {
if len(tags) == 0 {
return
}
var pickles []*messages.Pickle
for _, pickle := range ft.pickles {
if matchesTags(tags, pickle.Tags) {
pickles = append(pickles, pickle)
var scenarios []interface{}
for _, scenario := range ft.ScenarioDefinitions {
switch t := scenario.(type) {
case *gherkin.ScenarioOutline:
var allExamples []*gherkin.Examples
for _, examples := range t.Examples {
if matchesTags(tags, allTags(ft, t, examples)) {
allExamples = append(allExamples, examples)
}
}
t.Examples = allExamples
if len(t.Examples) > 0 {
scenarios = append(scenarios, scenario)
}
case *gherkin.Scenario:
if matchesTags(tags, allTags(ft, t)) {
scenarios = append(scenarios, scenario)
}
}
}
ft.ScenarioDefinitions = scenarios
}
ft.pickles = pickles
func allTags(nodes ...interface{}) []string {
var tags, tmp []string
for _, node := range nodes {
var gr []*gherkin.Tag
switch t := node.(type) {
case *gherkin.Feature:
gr = t.Tags
case *gherkin.ScenarioOutline:
gr = t.Tags
case *gherkin.Scenario:
gr = t.Tags
case *gherkin.Examples:
gr = t.Tags
}
for _, gtag := range gr {
tag := strings.TrimSpace(gtag.Name)
if tag[0] == '@' {
tag = tag[1:]
}
copy(tmp, tags)
var found bool
for _, tg := range tmp {
if tg == tag {
found = true
break
}
}
if !found {
tags = append(tags, tag)
}
}
}
return tags
}
func hasTag(tags []string, tag string) bool {
for _, t := range tags {
if t == tag {
return true
}
}
return false
}
// based on http://behat.readthedocs.org/en/v2.5/guides/6.cli.html#gherkin-filters
func matchesTags(filter string, tags []*messages.Pickle_PickleTag) (ok bool) {
func matchesTags(filter string, tags []string) (ok bool) {
ok = true
for _, andTags := range strings.Split(filter, "&&") {
var okComma bool
@ -781,14 +895,3 @@ func matchesTags(filter string, tags []*messages.Pickle_PickleTag) (ok bool) {
}
return
}
func hasTag(tags []*messages.Pickle_PickleTag, tag string) bool {
for _, t := range tags {
tName := strings.Replace(t.Name, "@", "", -1)
if tName == tag {
return true
}
}
return false
}

Просмотреть файл

@ -12,10 +12,8 @@ import (
"strconv"
"strings"
"github.com/cucumber/gherkin-go/v9"
"github.com/cucumber/messages-go/v9"
"github.com/cucumber/godog/colors"
"github.com/cucumber/godog/gherkin"
)
// SuiteContext provides steps for godog suite execution and
@ -114,7 +112,7 @@ type suiteContext struct {
out bytes.Buffer
}
func (s *suiteContext) ResetBeforeEachScenario(*messages.Pickle) {
func (s *suiteContext) ResetBeforeEachScenario(interface{}) {
// reset whole suite with the state
s.out.Reset()
s.paths = []string{}
@ -130,7 +128,7 @@ func (s *suiteContext) iRunFeatureSuiteWithTags(tags string) error {
return err
}
for _, feat := range s.testedSuite.features {
applyTagFilter(tags, feat)
applyTagFilter(tags, feat.Feature)
}
s.testedSuite.fmt = testFormatterFunc("godog", &s.out)
s.testedSuite.run()
@ -153,7 +151,7 @@ func (s *suiteContext) iRunFeatureSuiteWithFormatter(name string) error {
return nil
}
func (s *suiteContext) thereShouldBeEventsFired(doc *messages.PickleStepArgument_PickleDocString) error {
func (s *suiteContext) thereShouldBeEventsFired(doc *gherkin.DocString) error {
actual := strings.Split(strings.TrimSpace(s.out.String()), "\n")
expect := strings.Split(strings.TrimSpace(doc.Content), "\n")
if len(expect) != len(actual) {
@ -186,7 +184,7 @@ func (s *suiteContext) cleanupSnippet(snip string) string {
return strings.Join(lines, "\n")
}
func (s *suiteContext) theUndefinedStepSnippetsShouldBe(body *messages.PickleStepArgument_PickleDocString) error {
func (s *suiteContext) theUndefinedStepSnippetsShouldBe(body *gherkin.DocString) error {
f, ok := s.testedSuite.fmt.(*testFormatter)
if !ok {
return fmt.Errorf("this step requires testFormatter, but there is: %T", s.testedSuite.fmt)
@ -199,7 +197,7 @@ func (s *suiteContext) theUndefinedStepSnippetsShouldBe(body *messages.PickleSte
return nil
}
func (s *suiteContext) followingStepsShouldHave(status string, steps *messages.PickleStepArgument_PickleDocString) error {
func (s *suiteContext) followingStepsShouldHave(status string, steps *gherkin.DocString) error {
var expected = strings.Split(steps.Content, "\n")
var actual, unmatched, matched []string
@ -209,23 +207,23 @@ func (s *suiteContext) followingStepsShouldHave(status string, steps *messages.P
}
switch status {
case "passed":
for _, st := range f.findStepResults(passed) {
for _, st := range f.passed {
actual = append(actual, st.step.Text)
}
case "failed":
for _, st := range f.findStepResults(failed) {
for _, st := range f.failed {
actual = append(actual, st.step.Text)
}
case "skipped":
for _, st := range f.findStepResults(skipped) {
for _, st := range f.skipped {
actual = append(actual, st.step.Text)
}
case "undefined":
for _, st := range f.findStepResults(undefined) {
for _, st := range f.undefined {
actual = append(actual, st.step.Text)
}
case "pending":
for _, st := range f.findStepResults(pending) {
for _, st := range f.pending {
actual = append(actual, st.step.Text)
}
default:
@ -270,23 +268,19 @@ func (s *suiteContext) allStepsShouldHave(status string) error {
return fmt.Errorf("this step requires testFormatter, but there is: %T", s.testedSuite.fmt)
}
total := len(f.findStepResults(passed)) +
len(f.findStepResults(failed)) +
len(f.findStepResults(skipped)) +
len(f.findStepResults(undefined)) +
len(f.findStepResults(pending))
total := len(f.passed) + len(f.failed) + len(f.skipped) + len(f.undefined) + len(f.pending)
var actual int
switch status {
case "passed":
actual = len(f.findStepResults(passed))
actual = len(f.passed)
case "failed":
actual = len(f.findStepResults(failed))
actual = len(f.failed)
case "skipped":
actual = len(f.findStepResults(skipped))
actual = len(f.skipped)
case "undefined":
actual = len(f.findStepResults(undefined))
actual = len(f.undefined)
case "pending":
actual = len(f.findStepResults(pending))
actual = len(f.pending)
default:
return fmt.Errorf("unexpected step status wanted: %s", status)
}
@ -304,22 +298,22 @@ func (s *suiteContext) iAmListeningToSuiteEvents() error {
s.testedSuite.AfterSuite(func() {
s.events = append(s.events, &firedEvent{"AfterSuite", []interface{}{}})
})
s.testedSuite.BeforeFeature(func(ft *messages.GherkinDocument) {
s.testedSuite.BeforeFeature(func(ft *gherkin.Feature) {
s.events = append(s.events, &firedEvent{"BeforeFeature", []interface{}{ft}})
})
s.testedSuite.AfterFeature(func(ft *messages.GherkinDocument) {
s.testedSuite.AfterFeature(func(ft *gherkin.Feature) {
s.events = append(s.events, &firedEvent{"AfterFeature", []interface{}{ft}})
})
s.testedSuite.BeforeScenario(func(pickle *messages.Pickle) {
s.events = append(s.events, &firedEvent{"BeforeScenario", []interface{}{pickle}})
s.testedSuite.BeforeScenario(func(scenario interface{}) {
s.events = append(s.events, &firedEvent{"BeforeScenario", []interface{}{scenario}})
})
s.testedSuite.AfterScenario(func(pickle *messages.Pickle, err error) {
s.events = append(s.events, &firedEvent{"AfterScenario", []interface{}{pickle, err}})
s.testedSuite.AfterScenario(func(scenario interface{}, err error) {
s.events = append(s.events, &firedEvent{"AfterScenario", []interface{}{scenario, err}})
})
s.testedSuite.BeforeStep(func(step *messages.Pickle_PickleStep) {
s.testedSuite.BeforeStep(func(step *gherkin.Step) {
s.events = append(s.events, &firedEvent{"BeforeStep", []interface{}{step}})
})
s.testedSuite.AfterStep(func(step *messages.Pickle_PickleStep, err error) {
s.testedSuite.AfterStep(func(step *gherkin.Step, err error) {
s.events = append(s.events, &firedEvent{"AfterStep", []interface{}{step, err}})
})
return nil
@ -330,10 +324,9 @@ func (s *suiteContext) aFailingStep() error {
}
// parse a given feature file body as a feature
func (s *suiteContext) aFeatureFile(path string, body *messages.PickleStepArgument_PickleDocString) error {
gd, err := gherkin.ParseGherkinDocument(strings.NewReader(body.Content), (&messages.Incrementing{}).NewId)
pickles := gherkin.Pickles(*gd, path, (&messages.Incrementing{}).NewId)
s.testedSuite.features = append(s.testedSuite.features, &feature{GherkinDocument: gd, pickles: pickles, Path: path})
func (s *suiteContext) aFeatureFile(name string, body *gherkin.DocString) error {
ft, err := gherkin.ParseFeature(strings.NewReader(body.Content))
s.testedSuite.features = append(s.testedSuite.features, &feature{Feature: ft, Path: name})
return err
}
@ -361,7 +354,7 @@ func (s *suiteContext) theSuiteShouldHave(state string) error {
return nil
}
func (s *suiteContext) iShouldHaveNumFeatureFiles(num int, files *messages.PickleStepArgument_PickleDocString) error {
func (s *suiteContext) iShouldHaveNumFeatureFiles(num int, files *gherkin.DocString) error {
if len(s.testedSuite.features) != num {
return fmt.Errorf("expected %d features to be parsed, but have %d", num, len(s.testedSuite.features))
}
@ -406,7 +399,7 @@ func (s *suiteContext) iRunFeatureSuite() error {
func (s *suiteContext) numScenariosRegistered(expected int) (err error) {
var num int
for _, ft := range s.testedSuite.features {
num += len(ft.pickles)
num += len(ft.ScenarioDefinitions)
}
if num != expected {
err = fmt.Errorf("expected %d scenarios to be registered, but got %d", expected, num)
@ -436,7 +429,9 @@ func (s *suiteContext) thereWasEventTriggeredBeforeScenario(expected string) err
var name string
switch t := event.args[0].(type) {
case *messages.Pickle:
case *gherkin.Scenario:
name = t.Name
case *gherkin.ScenarioOutline:
name = t.Name
}
if name == expected {
@ -453,7 +448,7 @@ func (s *suiteContext) thereWasEventTriggeredBeforeScenario(expected string) err
return fmt.Errorf(`expected "%s" scenario, but got these fired %s`, expected, `"`+strings.Join(found, `", "`)+`"`)
}
func (s *suiteContext) theseEventsHadToBeFiredForNumberOfTimes(tbl *messages.PickleStepArgument_PickleTable) error {
func (s *suiteContext) theseEventsHadToBeFiredForNumberOfTimes(tbl *gherkin.DataTable) error {
if len(tbl.Rows[0].Cells) != 2 {
return fmt.Errorf("expected two columns for event table row, got: %d", len(tbl.Rows[0].Cells))
}
@ -470,7 +465,7 @@ func (s *suiteContext) theseEventsHadToBeFiredForNumberOfTimes(tbl *messages.Pic
return nil
}
func (s *suiteContext) theRenderJSONWillBe(docstring *messages.PickleStepArgument_PickleDocString) error {
func (s *suiteContext) theRenderJSONWillBe(docstring *gherkin.DocString) error {
suiteCtxReg := regexp.MustCompile(`suite_context.go:\d+`)
expectedString := docstring.Content
@ -494,7 +489,7 @@ func (s *suiteContext) theRenderJSONWillBe(docstring *messages.PickleStepArgumen
return nil
}
func (s *suiteContext) theRenderOutputWillBe(docstring *messages.PickleStepArgument_PickleDocString) error {
func (s *suiteContext) theRenderOutputWillBe(docstring *gherkin.DocString) error {
suiteCtxReg := regexp.MustCompile(`suite_context.go:\d+`)
suiteCtxFuncReg := regexp.MustCompile(`github.com/cucumber/godog.SuiteContext.func(\d+)`)
@ -513,7 +508,7 @@ func (s *suiteContext) theRenderOutputWillBe(docstring *messages.PickleStepArgum
return nil
}
func (s *suiteContext) theRenderXMLWillBe(docstring *messages.PickleStepArgument_PickleDocString) error {
func (s *suiteContext) theRenderXMLWillBe(docstring *gherkin.DocString) error {
expectedString := docstring.Content
actualString := s.out.String()
@ -528,23 +523,28 @@ func (s *suiteContext) theRenderXMLWillBe(docstring *messages.PickleStepArgument
}
if !reflect.DeepEqual(expected, actual) {
return fmt.Errorf("expected json does not match actual: %s", actualString)
return fmt.Errorf("expected xml does not match actual: %s", actualString)
}
return nil
}
type testFormatter struct {
*basefmt
pickles []*messages.Pickle
scenarios []interface{}
}
func testFormatterFunc(suite string, out io.Writer) Formatter {
return &testFormatter{basefmt: newBaseFmt(suite, out)}
}
func (f *testFormatter) Pickle(p *messages.Pickle) {
f.basefmt.Pickle(p)
f.pickles = append(f.pickles, p)
func (f *testFormatter) Node(node interface{}) {
f.basefmt.Node(node)
switch t := node.(type) {
case *gherkin.Scenario:
f.scenarios = append(f.scenarios, t)
case *gherkin.ScenarioOutline:
f.scenarios = append(f.scenarios, t)
}
}
func (f *testFormatter) Summary() {}

Просмотреть файл

@ -2,32 +2,28 @@ package godog
import (
"testing"
"github.com/cucumber/messages-go/v9"
)
func assertNotMatchesTagFilter(tags []*messages.Pickle_PickleTag, filter string, t *testing.T) {
func assertNotMatchesTagFilter(tags []string, filter string, t *testing.T) {
if matchesTags(filter, tags) {
t.Errorf(`expected tags: %v not to match tag filter "%s", but it did`, tags, filter)
}
}
func assertMatchesTagFilter(tags []*messages.Pickle_PickleTag, filter string, t *testing.T) {
func assertMatchesTagFilter(tags []string, filter string, t *testing.T) {
if !matchesTags(filter, tags) {
t.Errorf(`expected tags: %v to match tag filter "%s", but it did not`, tags, filter)
}
}
func TestTagFilter(t *testing.T) {
assertMatchesTagFilter([]*tag{{Name: "wip"}}, "@wip", t)
assertMatchesTagFilter([]*tag{}, "~@wip", t)
assertMatchesTagFilter([]*tag{{Name: "one"}, {Name: "two"}}, "@two,@three", t)
assertMatchesTagFilter([]*tag{{Name: "one"}, {Name: "two"}}, "@one&&@two", t)
assertMatchesTagFilter([]*tag{{Name: "one"}, {Name: "two"}}, "one && two", t)
assertMatchesTagFilter([]string{"wip"}, "@wip", t)
assertMatchesTagFilter([]string{}, "~@wip", t)
assertMatchesTagFilter([]string{"one", "two"}, "@two,@three", t)
assertMatchesTagFilter([]string{"one", "two"}, "@one&&@two", t)
assertMatchesTagFilter([]string{"one", "two"}, "one && two", t)
assertNotMatchesTagFilter([]*tag{}, "@wip", t)
assertNotMatchesTagFilter([]*tag{{Name: "one"}, {Name: "two"}}, "@one&&~@two", t)
assertNotMatchesTagFilter([]*tag{{Name: "one"}, {Name: "two"}}, "@one && ~@two", t)
assertNotMatchesTagFilter([]string{}, "@wip", t)
assertNotMatchesTagFilter([]string{"one", "two"}, "@one&&~@two", t)
assertNotMatchesTagFilter([]string{"one", "two"}, "@one && ~@two", t)
}
type tag = messages.Pickle_PickleTag