Merge branch 'master' into release/v0.9.0-rc1

Этот коммит содержится в:
Fredrik Lönnblad 2020-03-09 13:44:19 -03:00
родитель 7caae4e11a fcb462c54a
коммит b2f20ce589
45 изменённых файлов: 13362 добавлений и 1368 удалений

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

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

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

@ -156,36 +156,12 @@ console output snippets in order to test our feature requirements:
package main package main
import ( import (
"flag"
"fmt" "fmt"
"os"
"testing"
"github.com/cucumber/godog" "github.com/cucumber/godog"
"github.com/cucumber/godog/colors"
messages "github.com/cucumber/messages-go/v9" messages "github.com/cucumber/messages-go/v9"
) )
var opt = godog.Options{Output: colors.Colored(os.Stdout)}
func init() {
godog.BindFlags("godog.", flag.CommandLine, &opt)
}
func TestMain(m *testing.M) {
flag.Parse()
opt.Paths = flag.Args()
status := godog.RunWithOptions("godogs", func(s *godog.Suite) {
FeatureContext(s)
}, opt)
if st := m.Run(); st > status {
status = st
}
os.Exit(status)
}
func thereAreGodogs(available int) error { func thereAreGodogs(available int) error {
Godogs = available Godogs = available
return nil return nil
@ -354,6 +330,17 @@ func TestMain(m *testing.M) {
Now when running `go test -v` it will use **pretty** format. Now when running `go test -v` it will use **pretty** format.
### Tags
If you want to filter scenarios by tags, you can use the
`-t=<expression>` or `--tags=<expression>` where `<expression>`
is one of the following:
- `@wip` - run all scenarios with wip tag
- `~@wip` - exclude all scenarios with wip tag
- `@wip && ~@new` - run wip scenarios, but exclude new
- `@wip,@undone` - run wip or undone scenarios
### Configure common options for godog CLI ### Configure common options for godog CLI
There are no global options or configuration files. Alias your common or There are no global options or configuration files. Alias your common or

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

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

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

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

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

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

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

@ -9,7 +9,6 @@ import (
"github.com/cucumber/godog" "github.com/cucumber/godog"
"github.com/cucumber/godog/colors" "github.com/cucumber/godog/colors"
messages "github.com/cucumber/messages-go/v9"
) )
var opt = godog.Options{Output: colors.Colored(os.Stdout)} 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(`^I eat (\d+)$`, iEat)
s.Step(`^there should be (\d+) remaining$`, thereShouldBeRemaining) 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 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) { func TestGodogBuildWithVendoredGodogWithoutModule(t *testing.T) {
gopath := filepath.Join(os.TempDir(), "_gp") gopath := filepath.Join(os.TempDir(), "_gp")
dir := filepath.Join(gopath, "src", "godogs") dir := filepath.Join(gopath, "src", "godogs")

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

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

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

@ -409,7 +409,7 @@ func (cw *ansiColorWriter) Write(p []byte) (int, error) {
} }
if cw.mode != discardNonColorEscSeq || cw.state == outsideCsiCode { 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 r += nw
} }

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

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

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

@ -48,7 +48,7 @@ Feature: undefined step snippets
When I run feature suite When I run feature suite
Then the undefined step snippets should be: 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 return godog.ErrPending
} }

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

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

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

@ -15,8 +15,7 @@ import (
"unicode" "unicode"
"github.com/cucumber/godog/colors" "github.com/cucumber/godog/colors"
"github.com/cucumber/godog/gherkin"
"github.com/cucumber/messages-go/v9"
) )
// some snippet formatting regexps // some snippet formatting regexps
@ -44,7 +43,7 @@ var undefinedSnippetsTpl = template.Must(template.New("snippets").Funcs(snippetH
type undefinedSnippet struct { type undefinedSnippet struct {
Method string Method string
Expr string Expr string
argument *messages.PickleStepArgument argument interface{} // gherkin step argument
} }
type registeredFormatter struct { type registeredFormatter struct {
@ -98,14 +97,14 @@ func AvailableFormatters() map[string]string {
// formatters needs to be registered with a // formatters needs to be registered with a
// godog.Format function call // godog.Format function call
type Formatter interface { type Formatter interface {
Feature(*messages.GherkinDocument, string, []byte) Feature(*gherkin.Feature, string, []byte)
Pickle(*messages.Pickle) Node(interface{})
Defined(*messages.Pickle, *messages.Pickle_PickleStep, *StepDefinition) Defined(*gherkin.Step, *StepDef)
Failed(*messages.Pickle, *messages.Pickle_PickleStep, *StepDefinition, error) Failed(*gherkin.Step, *StepDef, error)
Passed(*messages.Pickle, *messages.Pickle_PickleStep, *StepDefinition) Passed(*gherkin.Step, *StepDef)
Skipped(*messages.Pickle, *messages.Pickle_PickleStep, *StepDefinition) Skipped(*gherkin.Step, *StepDef)
Undefined(*messages.Pickle, *messages.Pickle_PickleStep, *StepDefinition) Undefined(*gherkin.Step, *StepDef)
Pending(*messages.Pickle, *messages.Pickle_PickleStep, *StepDefinition) Pending(*gherkin.Step, *StepDef)
Summary() Summary()
} }
@ -121,17 +120,17 @@ type ConcurrentFormatter interface {
// suite name and io.Writer to record output // suite name and io.Writer to record output
type FormatterFunc func(string, io.Writer) Formatter type FormatterFunc func(string, io.Writer) Formatter
type stepResultStatus int type stepType int
const ( const (
passed stepResultStatus = iota passed stepType = iota
failed failed
skipped skipped
undefined undefined
pending pending
) )
func (st stepResultStatus) clr() colors.ColorFunc { func (st stepType) clr() colors.ColorFunc {
switch st { switch st {
case passed: case passed:
return green return green
@ -144,7 +143,7 @@ func (st stepResultStatus) clr() colors.ColorFunc {
} }
} }
func (st stepResultStatus) String() string { func (st stepType) String() string {
switch st { switch st {
case passed: case passed:
return "passed" return "passed"
@ -162,17 +161,65 @@ func (st stepResultStatus) String() string {
} }
type stepResult struct { type stepResult struct {
status stepResultStatus typ stepType
time time.Time feature *feature
err error owner interface{}
step *gherkin.Step
owner *messages.Pickle time time.Time
step *messages.Pickle_PickleStep def *StepDef
def *StepDefinition err error
} }
func newStepResult(pickle *messages.Pickle, step *messages.Pickle_PickleStep, match *StepDefinition) *stepResult { func (f stepResult) line() string {
return &stepResult{time: timeNowFunc(), owner: pickle, step: step, def: match} 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 { func newBaseFmt(suite string, out io.Writer) *basefmt {
@ -192,205 +239,217 @@ type basefmt struct {
owner interface{} owner interface{}
indent int indent int
started time.Time started time.Time
features []*feature features []*feature
failed []*stepResult
passed []*stepResult
skipped []*stepResult
undefined []*stepResult
pending []*stepResult
lock *sync.Mutex lock *sync.Mutex
} }
func (f *basefmt) lastFeature() *feature { func (f *basefmt) Node(n interface{}) {
return f.features[len(f.features)-1] f.lock.Lock()
} defer f.lock.Unlock()
func (f *basefmt) lastStepResult() *stepResult { switch t := n.(type) {
return f.lastFeature().lastStepResult() 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
func (f *basefmt) findScenario(scenarioAstID string) *messages.GherkinDocument_Feature_Scenario { feature := f.features[len(f.features)-1]
for _, ft := range f.features { lastExample := feature.Scenarios[len(feature.Scenarios)-1]
if sc := ft.findScenario(scenarioAstID); sc != nil {
return sc 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)
} }
} }
panic("Couldn't find scenario for AST ID: " + scenarioAstID)
} }
func (f *basefmt) findBackground(scenarioAstID string) *messages.GherkinDocument_Feature_Background { func (f *basefmt) Defined(*gherkin.Step, *StepDef) {
for _, ft := range f.features { f.lock.Lock()
if bg := ft.findBackground(scenarioAstID); bg != nil { defer f.lock.Unlock()
return bg }
}
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, Feature: ft, time: timeNowFunc()})
}
func (f *basefmt) Passed(step *gherkin.Step, match *StepDef) {
f.lock.Lock()
defer f.lock.Unlock()
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)
return nil f.features[len(f.features)-1].appendStepResult(s)
} }
func (f *basefmt) findExample(exampleAstID string) (*messages.GherkinDocument_Feature_Scenario_Examples, *messages.GherkinDocument_Feature_TableRow) { func (f *basefmt) Skipped(step *gherkin.Step, match *StepDef) {
for _, ft := range f.features { f.lock.Lock()
if es, rs := ft.findExample(exampleAstID); es != nil && rs != nil { defer f.lock.Unlock()
return es, rs
} 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)
return nil, nil f.features[len(f.features)-1].appendStepResult(s)
} }
func (f *basefmt) findStep(stepAstID string) *messages.GherkinDocument_Feature_Step { func (f *basefmt) Undefined(step *gherkin.Step, match *StepDef) {
for _, ft := range f.features { f.lock.Lock()
if st := ft.findStep(stepAstID); st != nil { defer f.lock.Unlock()
return st
} 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)
panic("Couldn't find step for AST ID: " + stepAstID) f.features[len(f.features)-1].appendStepResult(s)
} }
func (f *basefmt) Pickle(p *messages.Pickle) { func (f *basefmt) Failed(step *gherkin.Step, match *StepDef, err error) {
f.lock.Lock() f.lock.Lock()
defer f.lock.Unlock() defer f.lock.Unlock()
feature := f.features[len(f.features)-1] s := &stepResult{
feature.pickleResults = append(feature.pickleResults, &pickleResult{Name: p.Name, time: timeNowFunc()}) 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) Defined(*messages.Pickle, *messages.Pickle_PickleStep, *StepDefinition) {} func (f *basefmt) Pending(step *gherkin.Step, match *StepDef) {
func (f *basefmt) Feature(ft *messages.GherkinDocument, p string, c []byte) {
f.lock.Lock() f.lock.Lock()
defer f.lock.Unlock() defer f.lock.Unlock()
f.features = append(f.features, &feature{Path: p, GherkinDocument: ft, time: timeNowFunc()}) 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)
func (f *basefmt) Passed(pickle *messages.Pickle, step *messages.Pickle_PickleStep, match *StepDefinition) { f.features[len(f.features)-1].appendStepResult(s)
f.lock.Lock()
defer f.lock.Unlock()
s := newStepResult(pickle, step, match)
s.status = passed
f.lastFeature().appendStepResult(s)
}
func (f *basefmt) Skipped(pickle *messages.Pickle, step *messages.Pickle_PickleStep, match *StepDefinition) {
f.lock.Lock()
defer f.lock.Unlock()
s := newStepResult(pickle, step, match)
s.status = skipped
f.lastFeature().appendStepResult(s)
}
func (f *basefmt) Undefined(pickle *messages.Pickle, step *messages.Pickle_PickleStep, match *StepDefinition) {
f.lock.Lock()
defer f.lock.Unlock()
s := newStepResult(pickle, step, match)
s.status = undefined
f.lastFeature().appendStepResult(s)
}
func (f *basefmt) Failed(pickle *messages.Pickle, step *messages.Pickle_PickleStep, match *StepDefinition, err error) {
f.lock.Lock()
defer f.lock.Unlock()
s := newStepResult(pickle, step, match)
s.status = failed
s.err = err
f.lastFeature().appendStepResult(s)
}
func (f *basefmt) Pending(pickle *messages.Pickle, step *messages.Pickle_PickleStep, match *StepDefinition) {
f.lock.Lock()
defer f.lock.Unlock()
s := newStepResult(pickle, step, match)
s.status = pending
f.lastFeature().appendStepResult(s)
} }
func (f *basefmt) Summary() { func (f *basefmt) Summary() {
var totalSc, passedSc, undefinedSc int var total, passed, undefined int
var totalSt, passedSt, failedSt, skippedSt, pendingSt, undefinedSt int for _, ft := range f.features {
for _, def := range ft.ScenarioDefinitions {
for _, feat := range f.features { switch t := def.(type) {
for _, pr := range feat.pickleResults { case *gherkin.Scenario:
var prStatus stepResultStatus total++
totalSc++ if len(t.Steps) == 0 {
undefined++
if len(pr.stepResults) == 0 { }
prStatus = undefined case *gherkin.ScenarioOutline:
} for _, ex := range t.Examples {
total += len(ex.TableBody)
for _, sr := range pr.stepResults { if len(t.Steps) == 0 {
totalSt++ undefined += len(ex.TableBody)
}
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++
} }
} }
}
if prStatus == passed { }
passedSc++ passed = total - undefined
} else if prStatus == undefined { var owner interface{}
undefinedSc++ for _, undef := range f.undefined {
} if owner != undef.owner {
undefined++
owner = undef.owner
} }
} }
var steps, parts, scenarios []string var steps, parts, scenarios []string
if passedSt > 0 { nsteps := len(f.passed) + len(f.failed) + len(f.skipped) + len(f.undefined) + len(f.pending)
steps = append(steps, green(fmt.Sprintf("%d passed", passedSt))) if len(f.passed) > 0 {
steps = append(steps, green(fmt.Sprintf("%d passed", len(f.passed))))
} }
if failedSt > 0 { if len(f.failed) > 0 {
parts = append(parts, red(fmt.Sprintf("%d failed", failedSt))) passed -= len(f.failed)
steps = append(steps, red(fmt.Sprintf("%d failed", failedSt))) parts = append(parts, red(fmt.Sprintf("%d failed", len(f.failed))))
steps = append(steps, parts[len(parts)-1])
} }
if pendingSt > 0 { if len(f.pending) > 0 {
parts = append(parts, yellow(fmt.Sprintf("%d pending", pendingSt))) passed -= len(f.pending)
steps = append(steps, yellow(fmt.Sprintf("%d pending", pendingSt))) 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 { if len(f.undefined) > 0 {
parts = append(parts, yellow(fmt.Sprintf("%d undefined", undefinedSc))) passed -= undefined
steps = append(steps, yellow(fmt.Sprintf("%d undefined", undefinedSt))) parts = append(parts, yellow(fmt.Sprintf("%d undefined", undefined)))
} else if undefinedSc > 0 { steps = append(steps, yellow(fmt.Sprintf("%d undefined", len(f.undefined))))
} else if undefined > 0 {
// there may be some scenarios without steps // 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 { if len(f.skipped) > 0 {
steps = append(steps, cyan(fmt.Sprintf("%d skipped", skippedSt))) steps = append(steps, cyan(fmt.Sprintf("%d skipped", len(f.skipped))))
} }
if passedSc > 0 { if passed > 0 {
scenarios = append(scenarios, green(fmt.Sprintf("%d passed", passedSc))) scenarios = append(scenarios, green(fmt.Sprintf("%d passed", passed)))
} }
scenarios = append(scenarios, parts...) scenarios = append(scenarios, parts...)
elapsed := timeNowFunc().Sub(f.started) elapsed := timeNowFunc().Sub(f.started)
fmt.Fprintln(f.out, "") fmt.Fprintln(f.out, "")
if total == 0 {
if totalSc == 0 {
fmt.Fprintln(f.out, "No scenarios") fmt.Fprintln(f.out, "No scenarios")
} else { } 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") fmt.Fprintln(f.out, "No steps")
} else { } 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() elapsedString := elapsed.String()
@ -425,6 +484,21 @@ func (f *basefmt) Copy(cf ConcurrentFormatter) {
for _, v := range source.features { for _, v := range source.features {
f.features = append(f.features, v) 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()) args = append(args, reflect.String.String())
} }
} }
if s.argument != nil { if s.argument != nil {
if s.argument.GetDocString() != nil { switch s.argument.(type) {
args = append(args, "*messages.PickleStepArgument_PickleDocString") case *gherkin.DocString:
} args = append(args, "*gherkin.DocString")
if s.argument.GetDataTable() != nil { case *gherkin.DataTable:
args = append(args, "*messages.PickleStepArgument_PickleTable") args = append(args, "*gherkin.DataTable")
} }
} }
@ -477,30 +550,15 @@ func (s *undefinedSnippet) Args() (ret string) {
return strings.TrimSpace(strings.TrimRight(ret, ", ") + " " + last) 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 { func (f *basefmt) snippets() string {
undefinedStepResults := f.findStepResults(undefined) if len(f.undefined) == 0 {
if len(undefinedStepResults) == 0 {
return "" return ""
} }
var index int var index int
var snips []*undefinedSnippet var snips []*undefinedSnippet
// build snippets // build snippets
for _, u := range undefinedStepResults { for _, u := range f.undefined {
steps := []string{u.step.Text} steps := []string{u.step.Text}
arg := u.step.Argument arg := u.step.Argument
if u.def != nil { if u.def != nil {
@ -529,7 +587,7 @@ func (f *basefmt) snippets() string {
name = strings.Join(words, "") name = strings.Join(words, "")
if len(name) == 0 { if len(name) == 0 {
index++ index++
name = fmt.Sprintf("StepDefinitioninition%d", index) name = fmt.Sprintf("stepDefinition%d", index)
} }
var found bool var found bool
@ -553,6 +611,25 @@ func (f *basefmt) snippets() string {
return strings.Replace(buf.String(), " \n", "\n", -1) return strings.Replace(buf.String(), " \n", "\n", -1)
} }
func isLastStep(pickle *messages.Pickle, step *messages.Pickle_PickleStep) bool { func (f *basefmt) isLastStep(s *gherkin.Step) bool {
return pickle.Steps[len(pickle.Steps)-1].Id == step.Id 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" "encoding/json"
"fmt" "fmt"
"io" "io"
"strconv"
"strings" "strings"
"time" "time"
"github.com/cucumber/messages-go/v9" "github.com/cucumber/godog/gherkin"
) )
func init() { func init() {
@ -106,7 +107,7 @@ type cukefmt struct {
// it restricts this formatter to run only in synchronous single // it restricts this formatter to run only in synchronous single
// threaded execution. Unless running a copy of formatter for each feature // threaded execution. Unless running a copy of formatter for each feature
path string path string
status stepResultStatus // last step status, before skipped stat stepType // last step status, before skipped
ID string // current test id. ID string // current test id.
results []cukeFeatureJSON // structure that represent cuke results results []cukeFeatureJSON // structure that represent cuke results
curStep *cukeStep // track the current step curStep *cukeStep // track the current step
@ -121,86 +122,115 @@ type cukefmt struct {
// of the example name inorder to build id fields. // of the example name inorder to build id fields.
} }
func (f *cukefmt) Pickle(pickle *messages.Pickle) { func (f *cukefmt) Node(n interface{}) {
f.basefmt.Pickle(pickle) f.basefmt.Node(n)
scenario := f.findScenario(pickle.AstNodeIds[0]) switch t := n.(type) {
f.curFeature.Elements = append(f.curFeature.Elements, cukeElement{}) // When the example definition is seen we just need track the id and
f.curElement = &f.curFeature.Elements[len(f.curFeature.Elements)-1] // append the name associated with the example as part of the id.
case *gherkin.Examples:
f.curElement.Name = pickle.Name f.curExampleName = makeID(t.Name)
f.curElement.Line = int(scenario.Location.Line) f.curRow = 2 // there can be more than one example set per outline so reset row count.
f.curElement.Description = scenario.Description // cucumber counts the header row as an example when creating the id.
f.curElement.Keyword = scenario.Keyword
f.curElement.ID = f.curFeature.ID + ";" + makeID(pickle.Name)
f.curElement.Type = "scenario"
f.curElement.Tags = make([]cukeTag, len(scenario.Tags)+len(f.curFeature.Tags)) // store any example level tags in a temp location.
f.curExampleTags = make([]cukeTag, len(t.Tags))
if len(f.curElement.Tags) > 0 { for idx, element := range t.Tags {
// apply feature level tags f.curExampleTags[idx].Line = element.Location.Line
copy(f.curElement.Tags, f.curFeature.Tags) f.curExampleTags[idx].Name = element.Name
// apply scenario level tags.
for idx, element := range scenario.Tags {
f.curElement.Tags[idx+len(f.curFeature.Tags)].Line = int(element.Location.Line)
f.curElement.Tags[idx+len(f.curFeature.Tags)].Name = element.Name
} }
}
if len(pickle.AstNodeIds) == 1 { // The outline node creates a placeholder and the actual element is added as each TableRow is processed.
return case *gherkin.ScenarioOutline:
}
example, _ := f.findExample(pickle.AstNodeIds[1]) f.curOutline = cukeElement{}
// apply example level tags. f.curOutline.Name = t.Name
for _, tag := range example.Tags { f.curOutline.Line = t.Location.Line
tag := cukeTag{Line: int(tag.Location.Line), Name: tag.Name} f.curOutline.Description = t.Description
f.curElement.Tags = append(f.curElement.Tags, tag) 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))
examples := scenario.GetExamples() // apply feature level tags
if len(examples) > 0 { if len(f.curOutline.Tags) > 0 {
rowID := pickle.AstNodeIds[1] copy(f.curOutline.Tags, f.curFeature.Tags)
for _, example := range examples { // apply outline level tags.
for idx, row := range example.TableBody { for idx, element := range t.Tags {
if rowID == row.Id { f.curOutline.Tags[idx+len(f.curFeature.Tags)].Line = element.Location.Line
f.curElement.ID += fmt.Sprintf(";%s;%d", makeID(example.Name), idx+2) f.curOutline.Tags[idx+len(f.curFeature.Tags)].Name = element.Name
f.curElement.Line = int(row.Location.Line)
}
} }
} }
// 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 = 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(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 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
}
}
// 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]
// copy in example level tags.
f.curElement.Tags = append(f.curElement.Tags, f.curExampleTags...)
} }
} }
func (f *cukefmt) Feature(gd *messages.GherkinDocument, p string, c []byte) { func (f *cukefmt) Feature(ft *gherkin.Feature, p string, c []byte) {
f.basefmt.Feature(gd, p, c)
f.basefmt.Feature(ft, p, c)
f.path = p f.path = p
f.ID = makeID(gd.Feature.Name) f.ID = makeID(ft.Name)
f.results = append(f.results, cukeFeatureJSON{}) f.results = append(f.results, cukeFeatureJSON{})
f.curFeature = &f.results[len(f.results)-1] f.curFeature = &f.results[len(f.results)-1]
f.curFeature.URI = p f.curFeature.URI = p
f.curFeature.Name = gd.Feature.Name f.curFeature.Name = ft.Name
f.curFeature.Keyword = gd.Feature.Keyword f.curFeature.Keyword = ft.Keyword
f.curFeature.Line = int(gd.Feature.Location.Line) f.curFeature.Line = ft.Location.Line
f.curFeature.Description = gd.Feature.Description f.curFeature.Description = ft.Description
f.curFeature.ID = f.ID 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 { for idx, element := range ft.Tags {
f.curFeature.Tags[idx].Line = int(element.Location.Line) f.curFeature.Tags[idx].Line = element.Location.Line
f.curFeature.Tags[idx].Name = element.Name f.curFeature.Tags[idx].Name = element.Name
} }
f.curFeature.Comments = make([]cukeComment, len(gd.Comments)) f.curFeature.Comments = make([]cukeComment, len(ft.Comments))
for idx, comment := range gd.Comments { for idx, comment := range ft.Comments {
f.curFeature.Comments[idx].Value = strings.TrimSpace(comment.Text) 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) { func (f *cukefmt) step(res *stepResult) {
d := int(timeNowFunc().Sub(f.startTime).Nanoseconds())
f.curStep.Result.Duration = &d // determine if test case has finished
f.curStep.Result.Status = res.status.String() switch t := f.owner.(type) {
if res.err != nil { case *gherkin.TableRow:
f.curStep.Result.Error = res.err.Error() d := int(timeNowFunc().Sub(f.startTime).Nanoseconds())
f.curStep.Result.Duration = &d
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(pickle *messages.Pickle, pickleStep *messages.Pickle_PickleStep, def *StepDefinition) { func (f *cukefmt) Defined(step *gherkin.Step, def *StepDef) {
f.startTime = timeNowFunc() // start timing the step f.startTime = timeNowFunc() // start timing the step
f.curElement.Steps = append(f.curElement.Steps, cukeStep{}) f.curElement.Steps = append(f.curElement.Steps, cukeStep{})
f.curStep = &f.curElement.Steps[len(f.curElement.Steps)-1] f.curStep = &f.curElement.Steps[len(f.curElement.Steps)-1]
step := f.findStep(pickleStep.AstNodeIds[0]) f.curStep.Name = step.Text
f.curStep.Line = step.Location.Line
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.Keyword = step.Keyword f.curStep.Keyword = step.Keyword
arg := pickleStep.Argument if _, ok := step.Argument.(*gherkin.DocString); ok {
if arg.GetDocString() != nil && step.GetDocString() != nil {
f.curStep.Docstring = &cukeDocstring{} f.curStep.Docstring = &cukeDocstring{}
f.curStep.Docstring.ContentType = strings.TrimSpace(arg.GetDocString().MediaType) f.curStep.Docstring.ContentType = strings.TrimSpace(step.Argument.(*gherkin.DocString).ContentType)
f.curStep.Docstring.Line = int(step.GetDocString().Location.Line) f.curStep.Docstring.Line = step.Argument.(*gherkin.DocString).Location.Line
f.curStep.Docstring.Value = arg.GetDocString().Content f.curStep.Docstring.Value = step.Argument.(*gherkin.DocString).Content
} }
if arg.GetDataTable() != nil { if _, ok := step.Argument.(*gherkin.DataTable); ok {
f.curStep.DataTable = make([]*cukeDataTableRow, len(arg.GetDataTable().Rows)) dataTable := step.Argument.(*gherkin.DataTable)
for i, row := range arg.GetDataTable().Rows {
f.curStep.DataTable = make([]*cukeDataTableRow, len(dataTable.Rows))
for i, row := range dataTable.Rows {
cells := make([]string, len(row.Cells)) cells := make([]string, len(row.Cells))
for j, cell := range row.Cells { for j, cell := range row.Cells {
cells[j] = cell.Value 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) { func (f *cukefmt) Passed(step *gherkin.Step, match *StepDef) {
f.basefmt.Passed(pickle, step, match) f.basefmt.Passed(step, match)
f.stat = passed
f.status = passed f.step(f.passed[len(f.passed)-1])
f.step(f.lastStepResult())
} }
func (f *cukefmt) Skipped(pickle *messages.Pickle, step *messages.Pickle_PickleStep, match *StepDefinition) { func (f *cukefmt) Skipped(step *gherkin.Step, match *StepDef) {
f.basefmt.Skipped(pickle, step, match) f.basefmt.Skipped(step, match)
f.step(f.skipped[len(f.skipped)-1])
f.step(f.lastStepResult())
// no duration reported for skipped. // no duration reported for skipped.
f.curStep.Result.Duration = nil f.curStep.Result.Duration = nil
} }
func (f *cukefmt) Undefined(pickle *messages.Pickle, step *messages.Pickle_PickleStep, match *StepDefinition) { func (f *cukefmt) Undefined(step *gherkin.Step, match *StepDef) {
f.basefmt.Undefined(pickle, step, match) f.basefmt.Undefined(step, match)
f.stat = undefined
f.status = undefined f.step(f.undefined[len(f.undefined)-1])
f.step(f.lastStepResult())
// the location for undefined is the feature file location not the step file. // 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 f.curStep.Result.Duration = nil
} }
func (f *cukefmt) Failed(pickle *messages.Pickle, step *messages.Pickle_PickleStep, match *StepDefinition, err error) { func (f *cukefmt) Failed(step *gherkin.Step, match *StepDef, err error) {
f.basefmt.Failed(pickle, step, match, err) f.basefmt.Failed(step, match, err)
f.stat = failed
f.status = failed f.step(f.failed[len(f.failed)-1])
f.step(f.lastStepResult())
} }
func (f *cukefmt) Pending(pickle *messages.Pickle, step *messages.Pickle_PickleStep, match *StepDefinition) { func (f *cukefmt) Pending(step *gherkin.Step, match *StepDef) {
f.basefmt.Pending(pickle, step, match) f.stat = pending
f.basefmt.Pending(step, match)
f.status = pending f.step(f.pending[len(f.pending)-1])
f.step(f.lastStepResult())
// the location for pending is the feature file location not the step file. // 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 f.curStep.Result.Duration = nil
} }

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

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

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

@ -64,55 +64,45 @@ func buildJUNITPackageSuite(suiteName string, startedAt time.Time, features []*f
for idx, feat := range features { for idx, feat := range features {
ts := junitTestSuite{ ts := junitTestSuite{
Name: feat.GherkinDocument.Feature.Name, Name: feat.Name,
Time: junitTimeDuration(feat.startedAt(), feat.finishedAt()), 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 idx, scenario := range feat.Scenarios {
for _, pickleResult := range feat.pickleResults { tc := junitTestCase{
testcaseNames[pickleResult.Name] = testcaseNames[pickleResult.Name] + 1 Name: scenario.Name,
} Time: junitTimeDuration(scenario.startedAt(), scenario.finishedAt()),
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])
} }
ts.Tests++ ts.Tests++
suite.Tests++ suite.Tests++
for _, stepResult := range pickleResult.stepResults { for _, step := range scenario.Steps {
switch stepResult.status { switch step.typ {
case passed: case passed:
tc.Status = passed.String() tc.Status = passed.String()
case failed: case failed:
tc.Status = failed.String() tc.Status = failed.String()
tc.Failure = &junitFailure{ 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: case skipped:
tc.Error = append(tc.Error, &junitError{ tc.Error = append(tc.Error, &junitError{
Type: "skipped", Type: "skipped",
Message: fmt.Sprintf("Step %s", stepResult.step.Text), Message: fmt.Sprintf("%s %s", step.step.Type, step.step.Text),
}) })
case undefined: case undefined:
tc.Status = undefined.String() tc.Status = undefined.String()
tc.Error = append(tc.Error, &junitError{ tc.Error = append(tc.Error, &junitError{
Type: "undefined", Type: "undefined",
Message: fmt.Sprintf("Step %s", stepResult.step.Text), Message: fmt.Sprintf("%s %s", step.step.Type, step.step.Text),
}) })
case pending: case pending:
tc.Status = pending.String() tc.Status = pending.String()
tc.Error = append(tc.Error, &junitError{ tc.Error = append(tc.Error, &junitError{
Type: "pending", 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" "strings"
"testing" "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/colors"
"github.com/cucumber/godog/gherkin"
) )
var sampleGherkinFeature = ` var sampleGherkinFeature = `
@ -53,22 +49,19 @@ Feature: junit formatter
` `
func TestJUnitFormatterOutput(t *testing.T) { func TestJUnitFormatterOutput(t *testing.T) {
const path = "any.feature" feat, err := gherkin.ParseFeature(strings.NewReader(sampleGherkinFeature))
if err != nil {
gd, err := gherkin.ParseGherkinDocument(strings.NewReader(sampleGherkinFeature), (&messages.Incrementing{}).NewId) t.Fatalf("unexpected error: %v", err)
require.NoError(t, err) }
pickles := gherkin.Pickles(*gd, path, (&messages.Incrementing{}).NewId)
var buf bytes.Buffer var buf bytes.Buffer
w := colors.Uncolored(&buf) w := colors.Uncolored(&buf)
s := &Suite{ s := &Suite{
fmt: junitFunc("junit", w), fmt: junitFunc("junit", w),
features: []*feature{{ features: []*feature{&feature{
GherkinDocument: gd, Path: "any.feature",
pickles: pickles, Feature: feat,
Path: path, Content: []byte(sampleGherkinFeature),
Content: []byte(sampleGherkinFeature),
}}, }},
} }
@ -158,18 +151,19 @@ func TestJUnitFormatterOutput(t *testing.T) {
}, },
}}, }},
} }
s.run() s.run()
s.fmt.Summary() s.fmt.Summary()
var exp bytes.Buffer var exp bytes.Buffer
_, err = io.WriteString(&exp, xml.Header) if _, err = io.WriteString(&exp, xml.Header); err != nil {
require.NoError(t, err) t.Fatalf("unexpected error: %v", err)
}
enc := xml.NewEncoder(&exp) enc := xml.NewEncoder(&exp)
enc.Indent("", " ") enc.Indent("", " ")
err = enc.Encode(expected) if err = enc.Encode(expected); err != nil {
require.NoError(t, err) t.Fatalf("unexpected error: %v", err)
}
assert.Equal(t, exp.String(), buf.String()) if buf.String() != exp.String() {
t.Fatalf("expected output does not match: %s", buf.String())
}
} }

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

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

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

@ -6,7 +6,7 @@ import (
"math" "math"
"strings" "strings"
"github.com/cucumber/messages-go/v9" "github.com/cucumber/godog/gherkin"
) )
func init() { func init() {
@ -37,39 +37,21 @@ func (f *progress) Summary() {
fmt.Fprintf(f.out, " %d\n", *f.steps) 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, "") 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() f.basefmt.Summary()
} }
func (f *progress) step(res *stepResult) { func (f *progress) step(res *stepResult) {
switch res.status { switch res.typ {
case passed: case passed:
fmt.Fprint(f.out, green(".")) fmt.Fprint(f.out, green("."))
case skipped: 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) { func (f *progress) Passed(step *gherkin.Step, match *StepDef) {
f.basefmt.Passed(pickle, step, match) f.basefmt.Passed(step, match)
f.lock.Lock() f.lock.Lock()
defer f.lock.Unlock() defer f.lock.Unlock()
f.step(f.passed[len(f.passed)-1])
f.step(f.lastStepResult())
} }
func (f *progress) Skipped(pickle *messages.Pickle, step *messages.Pickle_PickleStep, match *StepDefinition) { func (f *progress) Skipped(step *gherkin.Step, match *StepDef) {
f.basefmt.Skipped(pickle, step, match) f.basefmt.Skipped(step, match)
f.lock.Lock() f.lock.Lock()
defer f.lock.Unlock() defer f.lock.Unlock()
f.step(f.skipped[len(f.skipped)-1])
f.step(f.lastStepResult())
} }
func (f *progress) Undefined(pickle *messages.Pickle, step *messages.Pickle_PickleStep, match *StepDefinition) { func (f *progress) Undefined(step *gherkin.Step, match *StepDef) {
f.basefmt.Undefined(pickle, step, match) f.basefmt.Undefined(step, match)
f.lock.Lock() f.lock.Lock()
defer f.lock.Unlock() defer f.lock.Unlock()
f.step(f.undefined[len(f.undefined)-1])
f.step(f.lastStepResult())
} }
func (f *progress) Failed(pickle *messages.Pickle, step *messages.Pickle_PickleStep, match *StepDefinition, err error) { func (f *progress) Failed(step *gherkin.Step, match *StepDef, err error) {
f.basefmt.Failed(pickle, step, match, err) f.basefmt.Failed(step, match, err)
f.lock.Lock() f.lock.Lock()
defer f.lock.Unlock() defer f.lock.Unlock()
f.step(f.failed[len(f.failed)-1])
f.step(f.lastStepResult())
} }
func (f *progress) Pending(pickle *messages.Pickle, step *messages.Pickle_PickleStep, match *StepDefinition) { func (f *progress) Pending(step *gherkin.Step, match *StepDef) {
f.basefmt.Pending(pickle, step, match) f.basefmt.Pending(step, match)
f.lock.Lock() f.lock.Lock()
defer f.lock.Unlock() defer f.lock.Unlock()
f.step(f.pending[len(f.pending)-1])
f.step(f.lastStepResult())
} }
func (f *progress) Sync(cf ConcurrentFormatter) { func (f *progress) Sync(cf ConcurrentFormatter) {

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

@ -3,42 +3,28 @@ package godog
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"io/ioutil"
"strings" "strings"
"testing" "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/colors"
"github.com/cucumber/godog/gherkin"
) )
var basicGherkinFeature = `
Feature: basic
Scenario: passing scenario
When one
Then two
`
func TestProgressFormatterOutput(t *testing.T) { func TestProgressFormatterOutput(t *testing.T) {
const path = "any.feature" feat, err := gherkin.ParseFeature(strings.NewReader(sampleGherkinFeature))
if err != nil {
gd, err := gherkin.ParseGherkinDocument(strings.NewReader(sampleGherkinFeature), (&messages.Incrementing{}).NewId) t.Fatalf("unexpected error: %v", err)
require.NoError(t, err) }
pickles := gherkin.Pickles(*gd, path, (&messages.Incrementing{}).NewId)
var buf bytes.Buffer var buf bytes.Buffer
w := colors.Uncolored(&buf) w := colors.Uncolored(&buf)
r := runner{ r := runner{
fmt: progressFunc("progress", w), fmt: progressFunc("progress", w),
features: []*feature{{ features: []*feature{&feature{
GherkinDocument: gd, Path: "any.feature",
pickles: pickles, Feature: feat,
Path: path, Content: []byte(sampleGherkinFeature),
Content: []byte(sampleGherkinFeature),
}}, }},
initializer: func(s *Suite) { initializer: func(s *Suite) {
s.Step(`^passing$`, func() error { return nil }) s.Step(`^passing$`, func() error { return nil })
@ -81,52 +67,61 @@ func FeatureContext(s *godog.Suite) {
s.Step(` + "`^next undefined$`" + `, nextUndefined) s.Step(` + "`^next undefined$`" + `, nextUndefined)
}` }`
require.True(t, r.run())
expected = trimAllLines(expected) expected = trimAllLines(expected)
r.run()
actual := trimAllLines(buf.String()) 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) { func TestProgressFormatterWhenStepPanics(t *testing.T) {
const path = "any.feature" feat, err := gherkin.ParseFeature(strings.NewReader(basicGherkinFeature))
if err != nil {
gd, err := gherkin.ParseGherkinDocument(strings.NewReader(basicGherkinFeature), (&messages.Incrementing{}).NewId) t.Fatalf("unexpected error: %v", err)
require.NoError(t, err) }
pickles := gherkin.Pickles(*gd, path, (&messages.Incrementing{}).NewId)
var buf bytes.Buffer var buf bytes.Buffer
w := colors.Uncolored(&buf) w := colors.Uncolored(&buf)
r := runner{ r := runner{
fmt: progressFunc("progress", w), fmt: progressFunc("progress", w),
features: []*feature{{GherkinDocument: gd, pickles: pickles}}, features: []*feature{&feature{Feature: feat}},
initializer: func(s *Suite) { initializer: func(s *Suite) {
s.Step(`^one$`, func() error { return nil }) s.Step(`^one$`, func() error { return nil })
s.Step(`^two$`, func() error { panic("omg") }) 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() out := buf.String()
assert.Contains(t, actual, "godog/fmt_progress_test.go:107") 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) { func TestProgressFormatterWithPassingMultisteps(t *testing.T) {
const path = "any.feature" feat, err := gherkin.ParseFeature(strings.NewReader(basicGherkinFeature))
if err != nil {
gd, err := gherkin.ParseGherkinDocument(strings.NewReader(basicGherkinFeature), (&messages.Incrementing{}).NewId) t.Fatalf("unexpected error: %v", err)
require.NoError(t, err) }
pickles := gherkin.Pickles(*gd, path, (&messages.Incrementing{}).NewId)
var buf bytes.Buffer var buf bytes.Buffer
w := colors.Uncolored(&buf) w := colors.Uncolored(&buf)
r := runner{ r := runner{
fmt: progressFunc("progress", w), fmt: progressFunc("progress", w),
features: []*feature{{GherkinDocument: gd, pickles: pickles}}, features: []*feature{&feature{Feature: feat}},
initializer: func(s *Suite) { initializer: func(s *Suite) {
s.Step(`^sub1$`, func() error { return nil }) s.Step(`^sub1$`, func() error { return nil })
s.Step(`^sub-sub$`, 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) { func TestProgressFormatterWithFailingMultisteps(t *testing.T) {
const path = "some.feature" feat, err := gherkin.ParseFeature(strings.NewReader(basicGherkinFeature))
if err != nil {
gd, err := gherkin.ParseGherkinDocument(strings.NewReader(basicGherkinFeature), (&messages.Incrementing{}).NewId) t.Fatalf("unexpected error: %v", err)
require.NoError(t, err) }
pickles := gherkin.Pickles(*gd, path, (&messages.Incrementing{}).NewId)
var buf bytes.Buffer var buf bytes.Buffer
w := colors.Uncolored(&buf) w := colors.Uncolored(&buf)
r := runner{ r := runner{
fmt: progressFunc("progress", w), fmt: progressFunc("progress", w),
features: []*feature{{GherkinDocument: gd, pickles: pickles, Path: path}}, features: []*feature{&feature{Feature: feat, Path: "some.feature"}},
initializer: func(s *Suite) { initializer: func(s *Suite) {
s.Step(`^sub1$`, func() error { return nil }) s.Step(`^sub1$`, func() error { return nil })
s.Step(`^sub-sub$`, func() error { return fmt.Errorf("errored") }) 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 := ` expected := `
.F 2 .F 2
@ -181,21 +178,48 @@ Error: sub2: sub-sub: errored
expected = trimAllLines(expected) expected = trimAllLines(expected)
actual := trimAllLines(buf.String()) 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) { 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 var buf bytes.Buffer
w := colors.Uncolored(&buf) w := colors.Uncolored(&buf)
r := runner{ r := runner{
fmt: progressFunc("progress", w), fmt: progressFunc("progress", w),
features: []*feature{{GherkinDocument: gd, pickles: pickles}}, features: []*feature{&feature{Feature: feat}},
initializer: func(s *Suite) { initializer: func(s *Suite) {
s.Step(`^sub1$`, func() error { return nil }) s.Step(`^sub1$`, func() error { return nil })
s.Step(`^sub-sub$`, 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) { func TestProgressFormatterMultistepTemplates(t *testing.T) {
const path = "any.feature" feat, err := gherkin.ParseFeature(strings.NewReader(basicGherkinFeature))
if err != nil {
gd, err := gherkin.ParseGherkinDocument(strings.NewReader(basicGherkinFeature), (&messages.Incrementing{}).NewId) t.Fatalf("unexpected error: %v", err)
require.NoError(t, err) }
pickles := gherkin.Pickles(*gd, path, (&messages.Incrementing{}).NewId)
var buf bytes.Buffer var buf bytes.Buffer
w := colors.Uncolored(&buf) w := colors.Uncolored(&buf)
r := runner{ r := runner{
fmt: progressFunc("progress", w), fmt: progressFunc("progress", w),
features: []*feature{{GherkinDocument: gd, pickles: pickles}}, features: []*feature{&feature{Feature: feat}},
initializer: func(s *Suite) { initializer: func(s *Suite) {
s.Step(`^sub-sub$`, func() error { return nil }) s.Step(`^sub-sub$`, func() error { return nil })
s.Step(`^substep$`, func() Steps { return Steps{"sub-sub", `unavailable "John" cost 5`, "one", "three"} }) 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 := ` expected := `
.U 2 .U 2
@ -261,13 +287,14 @@ func FeatureContext(s *godog.Suite) {
` `
expected = trimAllLines(expected) 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) { func TestProgressFormatterWhenMultiStepHasArgument(t *testing.T) {
const path = "any.feature"
var featureSource = ` var featureSource = `
Feature: basic Feature: basic
@ -279,28 +306,26 @@ Feature: basic
text 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{ r := runner{
fmt: progressFunc("progress", w), fmt: progressFunc("progress", ioutil.Discard),
features: []*feature{{GherkinDocument: gd, pickles: pickles}}, features: []*feature{&feature{Feature: feat}},
initializer: func(s *Suite) { initializer: func(s *Suite) {
s.Step(`^one$`, func() error { return nil }) 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) { func TestProgressFormatterWhenMultiStepHasStepWithArgument(t *testing.T) {
const path = "any.feature"
var featureSource = ` var featureSource = `
Feature: basic Feature: basic
@ -309,10 +334,10 @@ Feature: basic
When one When one
Then two` Then two`
gd, err := gherkin.ParseGherkinDocument(strings.NewReader(featureSource), (&messages.Incrementing{}).NewId) feat, err := gherkin.ParseFeature(strings.NewReader(featureSource))
require.NoError(t, err) if err != nil {
t.Fatalf("unexpected error: %v", err)
pickles := gherkin.Pickles(*gd, path, (&messages.Incrementing{}).NewId) }
var subStep = `three: var subStep = `three:
""" """
@ -323,15 +348,17 @@ Feature: basic
w := colors.Uncolored(&buf) w := colors.Uncolored(&buf)
r := runner{ r := runner{
fmt: progressFunc("progress", w), fmt: progressFunc("progress", w),
features: []*feature{{GherkinDocument: gd, pickles: pickles}}, features: []*feature{&feature{Feature: feat}},
initializer: func(s *Suite) { initializer: func(s *Suite) {
s.Step(`^one$`, func() error { return nil }) s.Step(`^one$`, func() error { return nil })
s.Step(`^two$`, func() Steps { return Steps{subStep} }) 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 := ` expected := `
.F 2 .F 2
@ -339,8 +366,8 @@ Feature: basic
--- Failed steps: --- Failed steps:
Scenario: passing scenario # any.feature:4 Scenario: passing scenario # :4
Then two # any.feature:6 Then two # :6
Error: nested steps cannot be multiline and have table or content body argument Error: nested steps cannot be multiline and have table or content body argument
@ -351,6 +378,7 @@ Feature: basic
expected = trimAllLines(expected) expected = trimAllLines(expected)
actual := trimAllLines(buf.String()) actual := trimAllLines(buf.String())
if actual != expected {
assert.Equal(t, expected, actual) t.Fatalf("expected output does not match: %s", actual)
}
} }

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

@ -1,5 +1,5 @@
{"event":"TestRunStarted","version":"0.1.0","timestamp":-6795364578871,"suite":"events"} {"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":"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":"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} {"event":"TestStepStarted","location":"formatter-tests/features/scenario_outline.feature:6","timestamp":-6795364578871}

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

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

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

@ -21,16 +21,16 @@
--- <red>Failed steps:</red> --- <red>Failed steps:</red>
<red>Scenario Outline: outline</red> <bold-black># formatter-tests/features/scenario_outline.feature:5</bold-black> <red>Scenario Outline: outline</red><bold-black> # formatter-tests/features/scenario_outline.feature:5</bold-black>
<red>Then odd 2 and even 0 number</red> <bold-black># formatter-tests/features/scenario_outline.feature:8</bold-black> <red>Then odd 2 and even 0 number</red><bold-black> # formatter-tests/features/scenario_outline.feature:8</bold-black>
<red>Error: </red><bold-red>2 is not odd</bold-red> <red>Error: </red><bold-red>2 is not odd</bold-red>
<red>Scenario Outline: outline</red> <bold-black># formatter-tests/features/scenario_outline.feature:5</bold-black> <red>Scenario Outline: outline</red><bold-black> # formatter-tests/features/scenario_outline.feature:5</bold-black>
<red>Then odd 3 and even 11 number</red> <bold-black># formatter-tests/features/scenario_outline.feature:8</bold-black> <red>Then odd 3 and even 11 number</red><bold-black> # formatter-tests/features/scenario_outline.feature:8</bold-black>
<red>Error: </red><bold-red>11 is not even</bold-red> <red>Error: </red><bold-red>11 is not even</bold-red>
<red>Scenario Outline: outline</red> <bold-black># formatter-tests/features/scenario_outline.feature:5</bold-black> <red>Scenario Outline: outline</red><bold-black> # formatter-tests/features/scenario_outline.feature:5</bold-black>
<red>Then odd 3 and even 9 number</red> <bold-black># formatter-tests/features/scenario_outline.feature:8</bold-black> <red>Then odd 3 and even 9 number</red><bold-black> # formatter-tests/features/scenario_outline.feature:8</bold-black>
<red>Error: </red><bold-red>9 is not even</bold-red> <red>Error: </red><bold-red>9 is not even</bold-red>

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

@ -17,8 +17,8 @@
--- <red>Failed steps:</red> --- <red>Failed steps:</red>
<red>Scenario: failing</red> <bold-black># formatter-tests/features/some_scenarions_including_failing.feature:3</bold-black> <red>Scenario: failing</red><bold-black> # formatter-tests/features/some_scenarions_including_failing.feature:3</bold-black>
<red>When failing step</red> <bold-black># formatter-tests/features/some_scenarions_including_failing.feature:5</bold-black> <red>When failing step</red><bold-black> # formatter-tests/features/some_scenarions_including_failing.feature:5</bold-black>
<red>Error: </red><bold-red>step failed</bold-red> <red>Error: </red><bold-red>step failed</bold-red>

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

@ -10,16 +10,17 @@
<cyan>Then</cyan> <cyan>passing step</cyan> <bold-black># formatters_print_test.go:63 -> passingStepDef</bold-black> <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-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> <cyan>Then</cyan> <cyan>passing step</cyan> <bold-black># formatters_print_test.go:63 -> passingStepDef</bold-black>
--- <red>Failed steps:</red> --- <red>Failed steps:</red>
<red>Scenario: one</red> <bold-black># formatter-tests/features/two_scenarios_with_background_fail.feature:7</bold-black> <red>Scenario: one</red><bold-black> # formatter-tests/features/two_scenarios_with_background_fail.feature:7</bold-black>
<red>And failing step</red> <bold-black># formatter-tests/features/two_scenarios_with_background_fail.feature:5</bold-black> <red>And failing step</red><bold-black> # formatter-tests/features/two_scenarios_with_background_fail.feature:5</bold-black>
<red>Error: </red><bold-red>step failed</bold-red> <red>Error: </red><bold-red>step failed</bold-red>
<red>Scenario: two</red> <bold-black># formatter-tests/features/two_scenarios_with_background_fail.feature:11</bold-black> <red>Scenario: two</red><bold-black> # formatter-tests/features/two_scenarios_with_background_fail.feature:11</bold-black>
<red>And failing step</red> <bold-black># formatter-tests/features/two_scenarios_with_background_fail.feature:5</bold-black> <red>And failing step</red><bold-black> # formatter-tests/features/two_scenarios_with_background_fail.feature:5</bold-black>
<red>Error: </red><bold-red>step failed</bold-red> <red>Error: </red><bold-red>step failed</bold-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 go 1.13
require ( require (
github.com/cucumber/gherkin-go/v9 v9.2.0 github.com/DATA-DOG/go-txdb v0.1.3
github.com/cucumber/messages-go/v9 v9.0.3 github.com/go-sql-driver/mysql v1.5.0
github.com/stretchr/testify v1.4.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/DATA-DOG/go-txdb v0.1.3 h1:R4v6OuOcy2O147e2zHxU0B4NDtF+INb5R9q/CV7AEMg=
github.com/cucumber/gherkin-go/v9 v9.2.0 h1:vxpzP4JtfNSDGH4s0u4TIxv+RaX533MCD+XNakz5kLY= github.com/DATA-DOG/go-txdb v0.1.3/go.mod h1:DhAhxMXZpUJVGnT+p9IbzJoRKvlArO2pkHjnGX7o0n0=
github.com/cucumber/gherkin-go/v9 v9.2.0/go.mod h1:W/+Z5yOowYWXRMlC6lJvM9LFDAFfsicZ1sstjPKfWWQ= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
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/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 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/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/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 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 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 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/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 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" "strings"
"testing" "testing"
"github.com/cucumber/gherkin-go/v9"
"github.com/cucumber/messages-go/v9"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/cucumber/godog/colors" "github.com/cucumber/godog/colors"
"github.com/cucumber/godog/gherkin"
) )
func okStep() error { func okStep() error {
@ -60,16 +59,12 @@ func TestPrintsNoStepDefinitionsIfNoneFound(t *testing.T) {
} }
func TestFailsOrPassesBasedOnStrictModeWhenHasPendingSteps(t *testing.T) { func TestFailsOrPassesBasedOnStrictModeWhenHasPendingSteps(t *testing.T) {
const path = "any.feature" feat, err := gherkin.ParseFeature(strings.NewReader(basicGherkinFeature))
gd, err := gherkin.ParseGherkinDocument(strings.NewReader(basicGherkinFeature), (&messages.Incrementing{}).NewId)
require.NoError(t, err) require.NoError(t, err)
pickles := gherkin.Pickles(*gd, path, (&messages.Incrementing{}).NewId)
r := runner{ r := runner{
fmt: progressFunc("progress", ioutil.Discard), fmt: progressFunc("progress", ioutil.Discard),
features: []*feature{{GherkinDocument: gd, pickles: pickles}}, features: []*feature{&feature{Feature: feat}},
initializer: func(s *Suite) { initializer: func(s *Suite) {
s.Step(`^one$`, func() error { return nil }) s.Step(`^one$`, func() error { return nil })
s.Step(`^two$`, func() error { return ErrPending }) s.Step(`^two$`, func() error { return ErrPending })
@ -83,16 +78,12 @@ func TestFailsOrPassesBasedOnStrictModeWhenHasPendingSteps(t *testing.T) {
} }
func TestFailsOrPassesBasedOnStrictModeWhenHasUndefinedSteps(t *testing.T) { func TestFailsOrPassesBasedOnStrictModeWhenHasUndefinedSteps(t *testing.T) {
const path = "any.feature" feat, err := gherkin.ParseFeature(strings.NewReader(basicGherkinFeature))
gd, err := gherkin.ParseGherkinDocument(strings.NewReader(basicGherkinFeature), (&messages.Incrementing{}).NewId)
require.NoError(t, err) require.NoError(t, err)
pickles := gherkin.Pickles(*gd, path, (&messages.Incrementing{}).NewId)
r := runner{ r := runner{
fmt: progressFunc("progress", ioutil.Discard), fmt: progressFunc("progress", ioutil.Discard),
features: []*feature{{GherkinDocument: gd, pickles: pickles}}, features: []*feature{&feature{Feature: feat}},
initializer: func(s *Suite) { initializer: func(s *Suite) {
s.Step(`^one$`, func() error { return nil }) s.Step(`^one$`, func() error { return nil })
// two - is undefined // two - is undefined
@ -106,16 +97,12 @@ func TestFailsOrPassesBasedOnStrictModeWhenHasUndefinedSteps(t *testing.T) {
} }
func TestShouldFailOnError(t *testing.T) { func TestShouldFailOnError(t *testing.T) {
const path = "any.feature" feat, err := gherkin.ParseFeature(strings.NewReader(basicGherkinFeature))
gd, err := gherkin.ParseGherkinDocument(strings.NewReader(basicGherkinFeature), (&messages.Incrementing{}).NewId)
require.NoError(t, err) require.NoError(t, err)
pickles := gherkin.Pickles(*gd, path, (&messages.Incrementing{}).NewId)
r := runner{ r := runner{
fmt: progressFunc("progress", ioutil.Discard), fmt: progressFunc("progress", ioutil.Discard),
features: []*feature{{GherkinDocument: gd, pickles: pickles}}, features: []*feature{&feature{Feature: feat}},
initializer: func(s *Suite) { initializer: func(s *Suite) {
s.Step(`^one$`, func() error { return nil }) s.Step(`^one$`, func() error { return nil })
s.Step(`^two$`, func() error { return fmt.Errorf("error") }) s.Step(`^two$`, func() error { return fmt.Errorf("error") })
@ -256,10 +243,11 @@ type succeedRunTestCase struct {
filename string // expected output file filename string // expected output file
} }
func TestConcurrencyRun(t *testing.T) { func TestSucceedRun(t *testing.T) {
testCases := []succeedRunTestCase{ testCases := []succeedRunTestCase{
{format: "progress", concurrency: 4, filename: "fixtures/progress_output.txt"}, {format: "progress", concurrency: 4, filename: "fixtures/progress_output.txt"},
{format: "junit", concurrency: 4, filename: "fixtures/junit_output.xml"}, {format: "junit", concurrency: 4, filename: "fixtures/junit_output.xml"},
{format: "cucumber", concurrency: 2, filename: "fixtures/cucumber_output.json"},
} }
for _, tc := range testCases { 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) output := new(bytes.Buffer)
opt := Options{ opt := Options{
@ -294,12 +282,11 @@ func testSucceedRun(t *testing.T, format string, concurrency int, expected strin
b, err := ioutil.ReadAll(output) b, err := ioutil.ReadAll(output)
require.NoError(t, err) require.NoError(t, err)
suiteCtxReg := regexp.MustCompile(`suite_context.go:\d+`)
expected = suiteCtxReg.ReplaceAllString(expected, `suite_context.go:0`)
actual := strings.TrimSpace(string(b)) 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`) 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" "strconv"
"strings" "strings"
"github.com/cucumber/messages-go/v9" "github.com/cucumber/godog/gherkin"
) )
var matchFuncDefRef = regexp.MustCompile(`\(([^\)]+)\)`) var matchFuncDefRef = regexp.MustCompile(`\(([^\)]+)\)`)
@ -31,7 +31,7 @@ var matchFuncDefRef = regexp.MustCompile(`\(([^\)]+)\)`)
// will result in main step failure. // will result in main step failure.
type Steps []string type Steps []string
// StepDefinition is a registered step definition // StepDef is a registered step definition
// contains a StepHandler and regexp which // contains a StepHandler and regexp which
// is used to match a step. Args which // is used to match a step. Args which
// were matched by last executed step // were matched by last executed step
@ -39,7 +39,7 @@ type Steps []string
// This structure is passed to the formatter // This structure is passed to the formatter
// when step is matched and is either failed // when step is matched and is either failed
// or successful // or successful
type StepDefinition struct { type StepDef struct {
args []interface{} args []interface{}
hv reflect.Value hv reflect.Value
Expr *regexp.Regexp Expr *regexp.Regexp
@ -50,7 +50,7 @@ type StepDefinition struct {
undefined []string undefined []string
} }
func (sd *StepDefinition) definitionID() string { func (sd *StepDef) definitionID() string {
ptr := sd.hv.Pointer() ptr := sd.hv.Pointer()
f := runtime.FuncForPC(ptr) f := runtime.FuncForPC(ptr)
file, line := f.FileLine(ptr) file, line := f.FileLine(ptr)
@ -80,7 +80,7 @@ func (sd *StepDefinition) definitionID() string {
// run a step with the matched arguments using // run a step with the matched arguments using
// reflect // reflect
func (sd *StepDefinition) run() interface{} { func (sd *StepDef) run() interface{} {
typ := sd.hv.Type() typ := sd.hv.Type()
if len(sd.args) < typ.NumIn() { 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)) 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: case reflect.Ptr:
arg := sd.args[i] arg := sd.args[i]
switch param.Elem().String() { switch param.Elem().String() {
case "messages.PickleStepArgument_PickleDocString": case "gherkin.DocString":
if v, ok := arg.(*messages.PickleStepArgument); ok { v, ok := arg.(*gherkin.DocString)
values = append(values, reflect.ValueOf(v.GetDocString())) if !ok {
break return fmt.Errorf(`cannot convert argument %d: "%v" of type "%T" to *gherkin.DocString`, i, arg, arg)
} }
values = append(values, reflect.ValueOf(v))
if v, ok := arg.(*messages.PickleStepArgument_PickleDocString); ok { case "gherkin.DataTable":
values = append(values, reflect.ValueOf(v)) v, ok := arg.(*gherkin.DataTable)
break if !ok {
return fmt.Errorf(`cannot convert argument %d: "%v" of type "%T" to *gherkin.DocString`, i, arg, arg)
} }
values = append(values, reflect.ValueOf(v))
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: 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: case reflect.Slice:
switch param { 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 fmt.Errorf("the argument %d type %s is not supported", i, param.Kind())
} }
} }
return sd.hv.Call(values)[0].Interface() 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] arg := sd.args[idx]
s, ok := arg.(string) s, ok := arg.(string)
if !ok { if !ok {

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

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

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

@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"io" "io"
"math/rand"
"os" "os"
"path/filepath" "path/filepath"
"reflect" "reflect"
@ -14,17 +15,16 @@ import (
"time" "time"
"unicode/utf8" "unicode/utf8"
"github.com/cucumber/gherkin-go/v9" "github.com/cucumber/godog/gherkin"
"github.com/cucumber/messages-go/v9"
) )
var errorInterface = reflect.TypeOf((*error)(nil)).Elem() var errorInterface = reflect.TypeOf((*error)(nil)).Elem()
var typeOfBytes = reflect.TypeOf([]byte(nil)) var typeOfBytes = reflect.TypeOf([]byte(nil))
type feature struct { type feature struct {
*messages.GherkinDocument *gherkin.Feature
pickles []*messages.Pickle
pickleResults []*pickleResult Scenarios []*scenario
time time.Time time time.Time
Content []byte `json:"-"` Content []byte `json:"-"`
@ -32,118 +32,47 @@ type feature struct {
order int 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 { func (f feature) startedAt() time.Time {
return f.time return f.time
} }
func (f feature) finishedAt() time.Time { func (f feature) finishedAt() time.Time {
if len(f.pickleResults) == 0 { if len(f.Scenarios) == 0 {
return f.startedAt() 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) { func (f feature) appendStepResult(s *stepResult) {
pickles := f.pickleResults[len(f.pickleResults)-1] scenario := f.Scenarios[len(f.Scenarios)-1]
pickles.stepResults = append(pickles.stepResults, s) scenario.Steps = append(scenario.Steps, 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]
} }
type sortByName []*feature type sortByName []*feature
func (s sortByName) Len() int { return len(s) } 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] } func (s sortByName) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
type pickleResult struct { type scenario struct {
Name string Name string
OutlineName string
ExampleNo int
time time.Time time time.Time
stepResults []*stepResult Steps []*stepResult
} }
func (s pickleResult) startedAt() time.Time { func (s scenario) startedAt() time.Time {
return s.time return s.time
} }
func (s pickleResult) finishedAt() time.Time { func (s scenario) finishedAt() time.Time {
if len(s.stepResults) == 0 { if len(s.Steps) == 0 {
return s.startedAt() 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 // 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 // executions are catching panic error since it may
// be a context specific error. // be a context specific error.
type Suite struct { type Suite struct {
steps []*StepDefinition steps []*StepDef
features []*feature features []*feature
fmt Formatter fmt Formatter
@ -176,16 +105,16 @@ type Suite struct {
// suite event handlers // suite event handlers
beforeSuiteHandlers []func() beforeSuiteHandlers []func()
beforeFeatureHandlers []func(*messages.GherkinDocument) beforeFeatureHandlers []func(*gherkin.Feature)
beforeScenarioHandlers []func(*messages.Pickle) beforeScenarioHandlers []func(interface{})
beforeStepHandlers []func(*messages.Pickle_PickleStep) beforeStepHandlers []func(*gherkin.Step)
afterStepHandlers []func(*messages.Pickle_PickleStep, error) afterStepHandlers []func(*gherkin.Step, error)
afterScenarioHandlers []func(*messages.Pickle, error) afterScenarioHandlers []func(interface{}, error)
afterFeatureHandlers []func(*messages.GherkinDocument) afterFeatureHandlers []func(*gherkin.Feature)
afterSuiteHandlers []func() 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 // feature suite, the definition will be applied
// to all steps matching the given Regexp expr. // to all steps matching the given Regexp expr.
// //
@ -197,7 +126,7 @@ type Suite struct {
// the same step, then only the first matched handler // the same step, then only the first matched handler
// will be applied. // 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 // ErrUndefined error will be returned when
// running steps. // running steps.
func (s *Suite) Step(expr interface{}, stepFunc interface{}) { 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())) panic(fmt.Sprintf("expected handler to return only one value, but it has: %d", typ.NumOut()))
} }
def := &StepDefinition{ def := &StepDef{
Handler: stepFunc, Handler: stepFunc,
Expr: regex, Expr: regex,
hv: v, hv: v,
@ -271,28 +200,31 @@ func (s *Suite) BeforeSuite(fn func()) {
// scenario to restart it. // scenario to restart it.
// //
// Use it wisely and avoid sharing state between scenarios. // 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) s.beforeFeatureHandlers = append(s.beforeFeatureHandlers, fn)
} }
// BeforeScenario registers a function or method // 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 // It is a good practice to restore the default state
// before every scenario so it would be isolated from // before every scenario so it would be isolated from
// any kind of state. // 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) s.beforeScenarioHandlers = append(s.beforeScenarioHandlers, fn)
} }
// BeforeStep registers a function or method // BeforeStep registers a function or method
// to be run before every step. // to be run before every scenario
func (s *Suite) BeforeStep(fn func(*messages.Pickle_PickleStep)) { func (s *Suite) BeforeStep(fn func(*gherkin.Step)) {
s.beforeStepHandlers = append(s.beforeStepHandlers, fn) s.beforeStepHandlers = append(s.beforeStepHandlers, fn)
} }
// AfterStep registers an function or method // 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 // It may be convenient to return a different kind of error
// in order to print more state details which may help // 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 // In some cases, for example when running a headless
// browser, to take a screenshot after failure. // 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) s.afterStepHandlers = append(s.afterStepHandlers, fn)
} }
// AfterScenario registers an function or method // AfterScenario registers an function or method
// to be run after every pickle. // to be run after every scenario or scenario outline
func (s *Suite) AfterScenario(fn func(*messages.Pickle, error)) { //
// The interface argument may be *gherkin.Scenario
// or *gherkin.ScenarioOutline
func (s *Suite) AfterScenario(fn func(interface{}, error)) {
s.afterScenarioHandlers = append(s.afterScenarioHandlers, fn) s.afterScenarioHandlers = append(s.afterScenarioHandlers, fn)
} }
// AfterFeature registers a function or method // AfterFeature registers a function or method
// to be run once after feature executed all scenarios. // 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) 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) def := s.matchStepText(step.Text)
if def != nil && step.Argument != nil { if def != nil && step.Argument != nil {
def.args = append(def.args, step.Argument) def.args = append(def.args, step.Argument)
@ -349,14 +284,14 @@ func (s *Suite) matchStep(step *messages.Pickle_PickleStep) *StepDefinition {
return def 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 // run before step handlers
for _, f := range s.beforeStepHandlers { for _, f := range s.beforeStepHandlers {
f(step) f(step)
} }
match := s.matchStep(step) match := s.matchStep(step)
s.fmt.Defined(pickle, step, match) s.fmt.Defined(step, match)
// user multistep definitions may panic // user multistep definitions may panic
defer func() { defer func() {
@ -377,11 +312,11 @@ func (s *Suite) runStep(pickle *messages.Pickle, step *messages.Pickle_PickleSte
switch err { switch err {
case nil: case nil:
s.fmt.Passed(pickle, step, match) s.fmt.Passed(step, match)
case ErrPending: case ErrPending:
s.fmt.Pending(pickle, step, match) s.fmt.Pending(step, match)
default: default:
s.fmt.Failed(pickle, step, match, err) s.fmt.Failed(step, match, err)
} }
// run after step handlers // run after step handlers
@ -394,7 +329,7 @@ func (s *Suite) runStep(pickle *messages.Pickle, step *messages.Pickle_PickleSte
return err return err
} else if len(undef) > 0 { } else if len(undef) > 0 {
if match != nil { if match != nil {
match = &StepDefinition{ match = &StepDef{
args: match.args, args: match.args,
hv: match.hv, hv: match.hv,
Expr: match.Expr, Expr: match.Expr,
@ -403,12 +338,12 @@ func (s *Suite) runStep(pickle *messages.Pickle, step *messages.Pickle_PickleSte
undefined: undef, undefined: undef,
} }
} }
s.fmt.Undefined(pickle, step, match) s.fmt.Undefined(step, match)
return ErrUndefined return ErrUndefined
} }
if prevStepErr != nil { if prevStepErr != nil {
s.fmt.Skipped(pickle, step, match) s.fmt.Skipped(step, match)
return nil return nil
} }
@ -473,7 +408,7 @@ func (s *Suite) maybeSubSteps(result interface{}) error {
return nil return nil
} }
func (s *Suite) matchStepText(text string) *StepDefinition { func (s *Suite) matchStepText(text string) *StepDef {
for _, h := range s.steps { for _, h := range s.steps {
if m := h.Expr.FindStringSubmatch(text); len(m) > 0 { if m := h.Expr.FindStringSubmatch(text); len(m) > 0 {
var args []interface{} var args []interface{}
@ -483,7 +418,7 @@ func (s *Suite) matchStepText(text string) *StepDefinition {
// since we need to assign arguments // since we need to assign arguments
// better to copy the step definition // better to copy the step definition
return &StepDefinition{ return &StepDef{
args: args, args: args,
hv: h.hv, hv: h.hv,
Expr: h.Expr, Expr: h.Expr,
@ -495,9 +430,9 @@ func (s *Suite) matchStepText(text string) *StepDefinition {
return nil 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 { for _, step := range steps {
stepErr := s.runStep(pickle, step, err) stepErr := s.runStep(step, err)
switch stepErr { switch stepErr {
case ErrUndefined: case ErrUndefined:
// do not overwrite failed error // do not overwrite failed error
@ -514,6 +449,110 @@ func (s *Suite) runSteps(pickle *messages.Pickle, steps []*messages.Pickle_Pickl
return 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 { func (s *Suite) shouldFail(err error) bool {
if err == nil { if err == nil {
return false return false
@ -527,24 +566,46 @@ func (s *Suite) shouldFail(err error) bool {
} }
func (s *Suite) runFeature(f *feature) { func (s *Suite) runFeature(f *feature) {
if !isEmptyFeature(f.pickles) { if !isEmptyFeature(f.Feature) {
for _, fn := range s.beforeFeatureHandlers { 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() { defer func() {
if !isEmptyFeature(f.pickles) { if !isEmptyFeature(f.Feature) {
for _, fn := range s.afterFeatureHandlers { for _, fn := range s.afterFeatureHandlers {
fn(f.GherkinDocument) fn(f.Feature)
} }
} }
}() }()
for _, pickle := range f.pickles { for _, scenario := range scenarios {
err := s.runPickle(pickle) 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) { if s.shouldFail(err) {
s.failed = true s.failed = true
if s.stopOnFailure { if s.stopOnFailure {
@ -554,35 +615,31 @@ func (s *Suite) runFeature(f *feature) {
} }
} }
func isEmptyFeature(pickles []*messages.Pickle) bool { func (s *Suite) runScenario(scenario *gherkin.Scenario, b *gherkin.Background) (err error) {
for _, pickle := range pickles { if isEmptyScenario(scenario) {
if len(pickle.Steps) > 0 { s.fmt.Node(scenario)
return false
}
}
return true
}
func (s *Suite) runPickle(pickle *messages.Pickle) (err error) {
if len(pickle.Steps) == 0 {
s.fmt.Pickle(pickle)
return ErrUndefined return ErrUndefined
} }
// run before scenario handlers // run before scenario handlers
for _, f := range s.beforeScenarioHandlers { 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 // scenario
err = s.runSteps(pickle, pickle.Steps) err = s.runSteps(steps)
// run after scenario handlers // run after scenario handlers
for _, f := range s.afterScenarioHandlers { for _, f := range s.afterScenarioHandlers {
f(pickle, err) f(scenario, err)
} }
return return
@ -621,7 +678,7 @@ func extractFeaturePathLine(p string) (string, int) {
return retPath, line return retPath, line
} }
func parseFeatureFile(path string, newIDFunc func() string) (*feature, error) { func parseFeatureFile(path string) (*feature, error) {
reader, err := os.Open(path) reader, err := os.Open(path)
if err != nil { if err != nil {
return nil, err return nil, err
@ -629,22 +686,19 @@ func parseFeatureFile(path string, newIDFunc func() string) (*feature, error) {
defer reader.Close() defer reader.Close()
var buf bytes.Buffer var buf bytes.Buffer
gherkinDocument, err := gherkin.ParseGherkinDocument(io.TeeReader(reader, &buf), newIDFunc) ft, err := gherkin.ParseFeature(io.TeeReader(reader, &buf))
if err != nil { if err != nil {
return nil, fmt.Errorf("%s - %v", path, err) return nil, fmt.Errorf("%s - %v", path, err)
} }
pickles := gherkin.Pickles(*gherkinDocument, path, newIDFunc)
return &feature{ return &feature{
GherkinDocument: gherkinDocument, Path: path,
pickles: pickles, Feature: ft,
Content: buf.Bytes(), Content: buf.Bytes(),
Path: path,
}, nil }, nil
} }
func parseFeatureDir(dir string, newIDFunc func() string) ([]*feature, error) { func parseFeatureDir(dir string) ([]*feature, error) {
var features []*feature var features []*feature
return features, filepath.Walk(dir, func(p string, f os.FileInfo, err error) error { return features, filepath.Walk(dir, func(p string, f os.FileInfo, err error) error {
if err != nil { if err != nil {
@ -659,7 +713,7 @@ func parseFeatureDir(dir string, newIDFunc func() string) ([]*feature, error) {
return nil return nil
} }
feat, err := parseFeatureFile(p, newIDFunc) feat, err := parseFeatureFile(p)
if err != nil { if err != nil {
return err return err
} }
@ -670,36 +724,39 @@ func parseFeatureDir(dir string, newIDFunc func() string) ([]*feature, error) {
func parsePath(path string) ([]*feature, error) { func parsePath(path string) ([]*feature, error) {
var features []*feature var features []*feature
// check if line number is specified
path, line := extractFeaturePathLine(path) var line int
path, line = extractFeaturePathLine(path)
fi, err := os.Stat(path) fi, err := os.Stat(path)
if err != nil { if err != nil {
return features, err return features, err
} }
newIDFunc := (&messages.Incrementing{}).NewId
if fi.IsDir() { if fi.IsDir() {
return parseFeatureDir(path, newIDFunc) return parseFeatureDir(path)
} }
ft, err := parseFeatureFile(path, newIDFunc) ft, err := parseFeatureFile(path)
if err != nil { if err != nil {
return features, err return features, err
} }
// filter scenario by line number // filter scenario by line number
var pickles []*messages.Pickle var scenarios []interface{}
for _, pickle := range ft.pickles { for _, def := range ft.ScenarioDefinitions {
sc := ft.findScenario(pickle.AstNodeIds[0]) var ln int
switch t := def.(type) {
if line == -1 || uint32(line) == sc.Location.Line { case *gherkin.Scenario:
pickles = append(pickles, pickle) 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 return append(features, ft), nil
} }
@ -727,7 +784,6 @@ func parseFeatures(filter string, paths []string) ([]*feature, error) {
byPath[ft.Path] = ft byPath[ft.Path] = ft
} }
} }
return filterFeatures(filter, byPath), nil 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) { func filterFeatures(tags string, collected map[string]*feature) (features []*feature) {
for _, ft := range collected { for _, ft := range collected {
applyTagFilter(tags, ft) applyTagFilter(tags, ft.Feature)
features = append(features, ft) features = append(features, ft)
} }
@ -748,23 +804,81 @@ func filterFeatures(tags string, collected map[string]*feature) (features []*fea
return features return features
} }
func applyTagFilter(tags string, ft *feature) { func applyTagFilter(tags string, ft *gherkin.Feature) {
if len(tags) == 0 { if len(tags) == 0 {
return return
} }
var pickles []*messages.Pickle var scenarios []interface{}
for _, pickle := range ft.pickles { for _, scenario := range ft.ScenarioDefinitions {
if matchesTags(tags, pickle.Tags) { switch t := scenario.(type) {
pickles = append(pickles, pickle) 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 // 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 ok = true
for _, andTags := range strings.Split(filter, "&&") { for _, andTags := range strings.Split(filter, "&&") {
var okComma bool var okComma bool
@ -781,14 +895,3 @@ func matchesTags(filter string, tags []*messages.Pickle_PickleTag) (ok bool) {
} }
return 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" "strconv"
"strings" "strings"
"github.com/cucumber/gherkin-go/v9"
"github.com/cucumber/messages-go/v9"
"github.com/cucumber/godog/colors" "github.com/cucumber/godog/colors"
"github.com/cucumber/godog/gherkin"
) )
// SuiteContext provides steps for godog suite execution and // SuiteContext provides steps for godog suite execution and
@ -114,7 +112,7 @@ type suiteContext struct {
out bytes.Buffer out bytes.Buffer
} }
func (s *suiteContext) ResetBeforeEachScenario(*messages.Pickle) { func (s *suiteContext) ResetBeforeEachScenario(interface{}) {
// reset whole suite with the state // reset whole suite with the state
s.out.Reset() s.out.Reset()
s.paths = []string{} s.paths = []string{}
@ -130,7 +128,7 @@ func (s *suiteContext) iRunFeatureSuiteWithTags(tags string) error {
return err return err
} }
for _, feat := range s.testedSuite.features { for _, feat := range s.testedSuite.features {
applyTagFilter(tags, feat) applyTagFilter(tags, feat.Feature)
} }
s.testedSuite.fmt = testFormatterFunc("godog", &s.out) s.testedSuite.fmt = testFormatterFunc("godog", &s.out)
s.testedSuite.run() s.testedSuite.run()
@ -153,7 +151,7 @@ func (s *suiteContext) iRunFeatureSuiteWithFormatter(name string) error {
return nil 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") actual := strings.Split(strings.TrimSpace(s.out.String()), "\n")
expect := strings.Split(strings.TrimSpace(doc.Content), "\n") expect := strings.Split(strings.TrimSpace(doc.Content), "\n")
if len(expect) != len(actual) { if len(expect) != len(actual) {
@ -186,7 +184,7 @@ func (s *suiteContext) cleanupSnippet(snip string) string {
return strings.Join(lines, "\n") 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) f, ok := s.testedSuite.fmt.(*testFormatter)
if !ok { if !ok {
return fmt.Errorf("this step requires testFormatter, but there is: %T", s.testedSuite.fmt) 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 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 expected = strings.Split(steps.Content, "\n")
var actual, unmatched, matched []string var actual, unmatched, matched []string
@ -209,23 +207,23 @@ func (s *suiteContext) followingStepsShouldHave(status string, steps *messages.P
} }
switch status { switch status {
case "passed": case "passed":
for _, st := range f.findStepResults(passed) { for _, st := range f.passed {
actual = append(actual, st.step.Text) actual = append(actual, st.step.Text)
} }
case "failed": case "failed":
for _, st := range f.findStepResults(failed) { for _, st := range f.failed {
actual = append(actual, st.step.Text) actual = append(actual, st.step.Text)
} }
case "skipped": case "skipped":
for _, st := range f.findStepResults(skipped) { for _, st := range f.skipped {
actual = append(actual, st.step.Text) actual = append(actual, st.step.Text)
} }
case "undefined": case "undefined":
for _, st := range f.findStepResults(undefined) { for _, st := range f.undefined {
actual = append(actual, st.step.Text) actual = append(actual, st.step.Text)
} }
case "pending": case "pending":
for _, st := range f.findStepResults(pending) { for _, st := range f.pending {
actual = append(actual, st.step.Text) actual = append(actual, st.step.Text)
} }
default: 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) return fmt.Errorf("this step requires testFormatter, but there is: %T", s.testedSuite.fmt)
} }
total := len(f.findStepResults(passed)) + total := len(f.passed) + len(f.failed) + len(f.skipped) + len(f.undefined) + len(f.pending)
len(f.findStepResults(failed)) +
len(f.findStepResults(skipped)) +
len(f.findStepResults(undefined)) +
len(f.findStepResults(pending))
var actual int var actual int
switch status { switch status {
case "passed": case "passed":
actual = len(f.findStepResults(passed)) actual = len(f.passed)
case "failed": case "failed":
actual = len(f.findStepResults(failed)) actual = len(f.failed)
case "skipped": case "skipped":
actual = len(f.findStepResults(skipped)) actual = len(f.skipped)
case "undefined": case "undefined":
actual = len(f.findStepResults(undefined)) actual = len(f.undefined)
case "pending": case "pending":
actual = len(f.findStepResults(pending)) actual = len(f.pending)
default: default:
return fmt.Errorf("unexpected step status wanted: %s", status) return fmt.Errorf("unexpected step status wanted: %s", status)
} }
@ -304,22 +298,22 @@ func (s *suiteContext) iAmListeningToSuiteEvents() error {
s.testedSuite.AfterSuite(func() { s.testedSuite.AfterSuite(func() {
s.events = append(s.events, &firedEvent{"AfterSuite", []interface{}{}}) 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.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.events = append(s.events, &firedEvent{"AfterFeature", []interface{}{ft}})
}) })
s.testedSuite.BeforeScenario(func(pickle *messages.Pickle) { s.testedSuite.BeforeScenario(func(scenario interface{}) {
s.events = append(s.events, &firedEvent{"BeforeScenario", []interface{}{pickle}}) s.events = append(s.events, &firedEvent{"BeforeScenario", []interface{}{scenario}})
}) })
s.testedSuite.AfterScenario(func(pickle *messages.Pickle, err error) { s.testedSuite.AfterScenario(func(scenario interface{}, err error) {
s.events = append(s.events, &firedEvent{"AfterScenario", []interface{}{pickle, err}}) 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.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}}) s.events = append(s.events, &firedEvent{"AfterStep", []interface{}{step, err}})
}) })
return nil return nil
@ -330,10 +324,9 @@ func (s *suiteContext) aFailingStep() error {
} }
// parse a given feature file body as a feature // parse a given feature file body as a feature
func (s *suiteContext) aFeatureFile(path string, body *messages.PickleStepArgument_PickleDocString) error { func (s *suiteContext) aFeatureFile(name string, body *gherkin.DocString) error {
gd, err := gherkin.ParseGherkinDocument(strings.NewReader(body.Content), (&messages.Incrementing{}).NewId) ft, err := gherkin.ParseFeature(strings.NewReader(body.Content))
pickles := gherkin.Pickles(*gd, path, (&messages.Incrementing{}).NewId) s.testedSuite.features = append(s.testedSuite.features, &feature{Feature: ft, Path: name})
s.testedSuite.features = append(s.testedSuite.features, &feature{GherkinDocument: gd, pickles: pickles, Path: path})
return err return err
} }
@ -361,7 +354,7 @@ func (s *suiteContext) theSuiteShouldHave(state string) error {
return nil 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 { if len(s.testedSuite.features) != num {
return fmt.Errorf("expected %d features to be parsed, but have %d", num, len(s.testedSuite.features)) 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) { func (s *suiteContext) numScenariosRegistered(expected int) (err error) {
var num int var num int
for _, ft := range s.testedSuite.features { for _, ft := range s.testedSuite.features {
num += len(ft.pickles) num += len(ft.ScenarioDefinitions)
} }
if num != expected { if num != expected {
err = fmt.Errorf("expected %d scenarios to be registered, but got %d", expected, num) 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 var name string
switch t := event.args[0].(type) { switch t := event.args[0].(type) {
case *messages.Pickle: case *gherkin.Scenario:
name = t.Name
case *gherkin.ScenarioOutline:
name = t.Name name = t.Name
} }
if name == expected { 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, `", "`)+`"`) 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 { if len(tbl.Rows[0].Cells) != 2 {
return fmt.Errorf("expected two columns for event table row, got: %d", len(tbl.Rows[0].Cells)) 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 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+`) suiteCtxReg := regexp.MustCompile(`suite_context.go:\d+`)
expectedString := docstring.Content expectedString := docstring.Content
@ -494,7 +489,7 @@ func (s *suiteContext) theRenderJSONWillBe(docstring *messages.PickleStepArgumen
return nil 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+`) suiteCtxReg := regexp.MustCompile(`suite_context.go:\d+`)
suiteCtxFuncReg := regexp.MustCompile(`github.com/cucumber/godog.SuiteContext.func(\d+)`) suiteCtxFuncReg := regexp.MustCompile(`github.com/cucumber/godog.SuiteContext.func(\d+)`)
@ -513,7 +508,7 @@ func (s *suiteContext) theRenderOutputWillBe(docstring *messages.PickleStepArgum
return nil return nil
} }
func (s *suiteContext) theRenderXMLWillBe(docstring *messages.PickleStepArgument_PickleDocString) error { func (s *suiteContext) theRenderXMLWillBe(docstring *gherkin.DocString) error {
expectedString := docstring.Content expectedString := docstring.Content
actualString := s.out.String() actualString := s.out.String()
@ -528,23 +523,28 @@ func (s *suiteContext) theRenderXMLWillBe(docstring *messages.PickleStepArgument
} }
if !reflect.DeepEqual(expected, actual) { 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 return nil
} }
type testFormatter struct { type testFormatter struct {
*basefmt *basefmt
pickles []*messages.Pickle scenarios []interface{}
} }
func testFormatterFunc(suite string, out io.Writer) Formatter { func testFormatterFunc(suite string, out io.Writer) Formatter {
return &testFormatter{basefmt: newBaseFmt(suite, out)} return &testFormatter{basefmt: newBaseFmt(suite, out)}
} }
func (f *testFormatter) Pickle(p *messages.Pickle) { func (f *testFormatter) Node(node interface{}) {
f.basefmt.Pickle(p) f.basefmt.Node(node)
f.pickles = append(f.pickles, p) 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() {} func (f *testFormatter) Summary() {}

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

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