
* bdde4c4 fix test suite and migration changes * a3b6e01 refactor pretty formatter * 2c0c7ba fix outline scenario handling * f6b411d add a different language test feature add a different language test feature
436 строки
12 КиБ
Go
436 строки
12 КиБ
Go
package godog
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"reflect"
|
|
"regexp"
|
|
"runtime"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/cucumber/gherkin-go"
|
|
)
|
|
|
|
func init() {
|
|
RegisterFormatter("pretty", "Prints every feature with runtime statuses.", &pretty{
|
|
started: time.Now(),
|
|
indent: 2,
|
|
})
|
|
}
|
|
|
|
var outlinePlaceholderRegexp = regexp.MustCompile("<[^>]+>")
|
|
|
|
// a built in default pretty formatter
|
|
type pretty struct {
|
|
scope interface{}
|
|
indent int
|
|
commentPos int
|
|
backgroundSteps int
|
|
|
|
// outline
|
|
outline *gherkin.ScenarioOutline
|
|
outlineSteps []interface{}
|
|
outlineNumExample int
|
|
outlineNumExamples int
|
|
|
|
// summary
|
|
started time.Time
|
|
features []*feature
|
|
failed []*failed
|
|
passed []*passed
|
|
skipped []*skipped
|
|
undefined []*undefined
|
|
}
|
|
|
|
// a line number representation in feature file
|
|
func (f *pretty) line(loc *gherkin.Location) string {
|
|
return cl(fmt.Sprintf("# %s:%d", f.features[len(f.features)-1].Path, loc.Line), black)
|
|
}
|
|
|
|
func (f *pretty) length(node interface{}) int {
|
|
switch t := node.(type) {
|
|
case *gherkin.Background:
|
|
return f.indent + len(strings.TrimSpace(t.Keyword)+": "+t.Name)
|
|
case *gherkin.Step:
|
|
return f.indent*2 + len(strings.TrimSpace(t.Keyword)+" "+t.Text)
|
|
case *gherkin.Scenario:
|
|
return f.indent + len(strings.TrimSpace(t.Keyword)+": "+t.Name)
|
|
case *gherkin.ScenarioOutline:
|
|
return f.indent + len(strings.TrimSpace(t.Keyword)+": "+t.Name)
|
|
}
|
|
panic(fmt.Sprintf("unexpected node %T to determine length", node))
|
|
}
|
|
|
|
func (f *pretty) Feature(ft *gherkin.Feature, p string) {
|
|
if len(f.features) != 0 {
|
|
// not a first feature, add a newline
|
|
fmt.Println("")
|
|
}
|
|
f.features = append(f.features, &feature{Path: p, Feature: ft})
|
|
fmt.Println(bcl(ft.Keyword+": ", white) + ft.Name)
|
|
if strings.TrimSpace(ft.Description) != "" {
|
|
for _, line := range strings.Split(ft.Description, "\n") {
|
|
fmt.Println(s(f.indent) + strings.TrimSpace(line))
|
|
}
|
|
}
|
|
if ft.Background != nil {
|
|
f.commentPos = f.longestStep(ft.Background.Steps, f.length(ft.Background))
|
|
f.backgroundSteps = len(ft.Background.Steps)
|
|
fmt.Println("\n" + s(f.indent) + bcl(ft.Background.Keyword+": "+ft.Background.Name, white))
|
|
}
|
|
}
|
|
|
|
// Node takes a gherkin node for formatting
|
|
func (f *pretty) Node(node interface{}) {
|
|
switch t := node.(type) {
|
|
case *gherkin.Examples:
|
|
f.outlineNumExamples = len(t.TableBody)
|
|
f.outlineNumExample++
|
|
case *gherkin.Background:
|
|
f.scope = t
|
|
case *gherkin.Scenario:
|
|
f.scope = t
|
|
f.commentPos = f.longestStep(t.Steps, f.length(t))
|
|
text := s(f.indent) + bcl(t.Keyword+": ", white) + t.Name
|
|
text += s(f.commentPos-f.length(t)+1) + f.line(t.Location)
|
|
fmt.Println("\n" + text)
|
|
case *gherkin.ScenarioOutline:
|
|
f.scope = t
|
|
f.outline = t
|
|
f.commentPos = f.longestStep(t.Steps, f.length(t))
|
|
text := s(f.indent) + bcl(t.Keyword+": ", white) + t.Name
|
|
text += s(f.commentPos-f.length(t)+1) + f.line(t.Location)
|
|
fmt.Println("\n" + text)
|
|
f.outlineNumExample = -1
|
|
}
|
|
}
|
|
|
|
// Summary sumarize the feature formatter output
|
|
func (f *pretty) Summary() {
|
|
// failed steps on background are not scenarios
|
|
var failedScenarios []*failed
|
|
for _, fail := range f.failed {
|
|
switch fail.owner.(type) {
|
|
case *gherkin.Scenario:
|
|
failedScenarios = append(failedScenarios, fail)
|
|
case *gherkin.ScenarioOutline:
|
|
failedScenarios = append(failedScenarios, fail)
|
|
}
|
|
}
|
|
if len(failedScenarios) > 0 {
|
|
fmt.Println("\n--- " + cl("Failed scenarios:", red) + "\n")
|
|
var unique []string
|
|
for _, fail := range failedScenarios {
|
|
var found bool
|
|
for _, in := range unique {
|
|
if in == fail.line() {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
unique = append(unique, fail.line())
|
|
}
|
|
}
|
|
|
|
for _, fail := range unique {
|
|
fmt.Println(" " + cl(fail, red))
|
|
}
|
|
}
|
|
var total, passed int
|
|
for _, ft := range f.features {
|
|
for _, def := range ft.ScenarioDefinitions {
|
|
switch t := def.(type) {
|
|
case *gherkin.Scenario:
|
|
total++
|
|
case *gherkin.ScenarioOutline:
|
|
for _, ex := range t.Examples {
|
|
total += len(ex.TableBody)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
passed = total
|
|
|
|
var steps, parts, scenarios []string
|
|
nsteps := len(f.passed) + len(f.failed) + len(f.skipped) + len(f.undefined)
|
|
if len(f.passed) > 0 {
|
|
steps = append(steps, cl(fmt.Sprintf("%d passed", len(f.passed)), green))
|
|
}
|
|
if len(f.failed) > 0 {
|
|
passed -= len(f.failed)
|
|
parts = append(parts, cl(fmt.Sprintf("%d failed", len(f.failed)), red))
|
|
steps = append(steps, parts[len(parts)-1])
|
|
}
|
|
if len(f.skipped) > 0 {
|
|
steps = append(steps, cl(fmt.Sprintf("%d skipped", len(f.skipped)), cyan))
|
|
}
|
|
if len(f.undefined) > 0 {
|
|
passed -= len(f.undefined)
|
|
parts = append(parts, cl(fmt.Sprintf("%d undefined", len(f.undefined)), yellow))
|
|
steps = append(steps, parts[len(parts)-1])
|
|
}
|
|
if passed > 0 {
|
|
scenarios = append(scenarios, cl(fmt.Sprintf("%d passed", passed), green))
|
|
}
|
|
scenarios = append(scenarios, parts...)
|
|
elapsed := time.Since(f.started)
|
|
|
|
fmt.Println("")
|
|
if total == 0 {
|
|
fmt.Println("No scenarios")
|
|
} else {
|
|
fmt.Println(fmt.Sprintf("%d scenarios (%s)", total, strings.Join(scenarios, ", ")))
|
|
}
|
|
|
|
if nsteps == 0 {
|
|
fmt.Println("No steps")
|
|
} else {
|
|
fmt.Println(fmt.Sprintf("%d steps (%s)", nsteps, strings.Join(steps, ", ")))
|
|
}
|
|
fmt.Println(elapsed)
|
|
}
|
|
|
|
func (f *pretty) printOutlineExample(outline *gherkin.ScenarioOutline) {
|
|
var failed error
|
|
clr := green
|
|
|
|
example := outline.Examples[f.outlineNumExample]
|
|
firstExample := f.outlineNumExamples == len(example.TableBody)
|
|
printSteps := firstExample && f.outlineNumExample == 0
|
|
|
|
// var replace make(map[])
|
|
for i, act := range f.outlineSteps {
|
|
_, _, def, c, err := f.stepDetails(act)
|
|
// determine example row status
|
|
switch {
|
|
case err != nil:
|
|
failed = err
|
|
clr = red
|
|
case c == yellow:
|
|
clr = yellow
|
|
case c == cyan && clr == green:
|
|
clr = cyan
|
|
}
|
|
if printSteps {
|
|
// in first example, we need to print steps
|
|
var text string
|
|
ostep := outline.Steps[i]
|
|
if def != nil {
|
|
if m := outlinePlaceholderRegexp.FindAllStringIndex(ostep.Text, -1); len(m) > 0 {
|
|
var pos int
|
|
for i := 0; i < len(m); i++ {
|
|
pair := m[i]
|
|
text += cl(ostep.Text[pos:pair[0]], cyan)
|
|
text += bcl(ostep.Text[pair[0]:pair[1]], cyan)
|
|
pos = pair[1]
|
|
}
|
|
text += cl(ostep.Text[pos:len(ostep.Text)], cyan)
|
|
} else {
|
|
text = cl(ostep.Text, cyan)
|
|
}
|
|
// use reflect to get step handler function name
|
|
name := runtime.FuncForPC(reflect.ValueOf(def.Handler).Pointer()).Name()
|
|
text += s(f.commentPos-f.length(ostep)+1) + cl(fmt.Sprintf("# %s", name), black)
|
|
} else {
|
|
text = cl(ostep.Text, cyan)
|
|
}
|
|
// print the step outline
|
|
fmt.Println(s(f.indent*2) + cl(strings.TrimSpace(ostep.Keyword), cyan) + " " + text)
|
|
}
|
|
}
|
|
|
|
cells := make([]string, len(example.TableHeader.Cells))
|
|
max := longest(example)
|
|
// an example table header
|
|
if firstExample {
|
|
fmt.Println("")
|
|
fmt.Println(s(f.indent*2) + bcl(example.Keyword+": ", white) + example.Name)
|
|
|
|
for i, cell := range example.TableHeader.Cells {
|
|
cells[i] = cl(cell.Value, cyan) + s(max[i]-len(cell.Value))
|
|
}
|
|
fmt.Println(s(f.indent*3) + "| " + strings.Join(cells, " | ") + " |")
|
|
}
|
|
|
|
// an example table row
|
|
row := example.TableBody[len(example.TableBody)-f.outlineNumExamples]
|
|
for i, cell := range row.Cells {
|
|
cells[i] = cl(cell.Value, clr) + s(max[i]-len(cell.Value))
|
|
}
|
|
fmt.Println(s(f.indent*3) + "| " + strings.Join(cells, " | ") + " |")
|
|
|
|
// if there is an error
|
|
if failed != nil {
|
|
fmt.Println(s(f.indent*3) + bcl(failed, red))
|
|
}
|
|
}
|
|
|
|
func (f *pretty) printStep(step *gherkin.Step, def *StepDef, c color) {
|
|
text := s(f.indent*2) + cl(strings.TrimSpace(step.Keyword), c) + " "
|
|
switch {
|
|
case def != nil:
|
|
if m := (def.Expr.FindStringSubmatchIndex(step.Text))[2:]; len(m) > 0 {
|
|
var pos, i int
|
|
for pos, i = 0, 0; i < len(m); i++ {
|
|
if math.Mod(float64(i), 2) == 0 {
|
|
text += cl(step.Text[pos:m[i]], c)
|
|
} else {
|
|
text += bcl(step.Text[pos:m[i]], c)
|
|
}
|
|
pos = m[i]
|
|
}
|
|
text += cl(step.Text[pos:len(step.Text)], c)
|
|
} else {
|
|
text += cl(step.Text, c)
|
|
}
|
|
// use reflect to get step handler function name
|
|
name := runtime.FuncForPC(reflect.ValueOf(def.Handler).Pointer()).Name()
|
|
text += s(f.commentPos-f.length(step)+1) + cl(fmt.Sprintf("# %s", name), black)
|
|
default:
|
|
text += cl(step.Text, c)
|
|
}
|
|
|
|
fmt.Println(text)
|
|
switch t := step.Argument.(type) {
|
|
case *gherkin.DataTable:
|
|
f.printTable(t, c)
|
|
case *gherkin.DocString:
|
|
fmt.Println(s(f.indent*3) + cl(t.Delimitter, c)) // @TODO: content type
|
|
for _, ln := range strings.Split(t.Content, "\n") {
|
|
fmt.Println(s(f.indent*3) + cl(ln, c))
|
|
}
|
|
fmt.Println(s(f.indent*3) + cl(t.Delimitter, c))
|
|
}
|
|
}
|
|
|
|
func (f *pretty) stepDetails(stepAction interface{}) (owner interface{}, step *gherkin.Step, def *StepDef, c color, err error) {
|
|
switch typ := stepAction.(type) {
|
|
case *passed:
|
|
step = typ.step
|
|
def = typ.def
|
|
owner = typ.owner
|
|
c = green
|
|
case *failed:
|
|
step = typ.step
|
|
def = typ.def
|
|
owner = typ.owner
|
|
err = typ.err
|
|
c = red
|
|
case *skipped:
|
|
step = typ.step
|
|
owner = typ.owner
|
|
c = cyan
|
|
case *undefined:
|
|
step = typ.step
|
|
owner = typ.owner
|
|
c = yellow
|
|
default:
|
|
fatal(fmt.Errorf("unexpected step type received: %T", typ))
|
|
}
|
|
return
|
|
}
|
|
|
|
func (f *pretty) printStepKind(stepAction interface{}) {
|
|
owner, step, def, c, err := f.stepDetails(stepAction)
|
|
|
|
// do not print background more than once
|
|
if _, ok := owner.(*gherkin.Background); ok {
|
|
switch {
|
|
case f.backgroundSteps == 0:
|
|
return
|
|
case f.backgroundSteps > 0:
|
|
f.backgroundSteps--
|
|
}
|
|
}
|
|
|
|
if outline, ok := owner.(*gherkin.ScenarioOutline); ok {
|
|
f.outlineSteps = append(f.outlineSteps, stepAction)
|
|
if len(f.outlineSteps) == len(outline.Steps) {
|
|
// an outline example steps has went through
|
|
f.printOutlineExample(outline)
|
|
f.outlineSteps = []interface{}{}
|
|
f.outlineNumExamples--
|
|
}
|
|
return
|
|
}
|
|
|
|
f.printStep(step, def, c)
|
|
if err != nil {
|
|
fmt.Println(s(f.indent*2) + bcl(err, red))
|
|
}
|
|
}
|
|
|
|
// print table with aligned table cells
|
|
func (f *pretty) printTable(t *gherkin.DataTable, c color) {
|
|
var l = longest(t)
|
|
var cols = make([]string, len(t.Rows[0].Cells))
|
|
for _, row := range t.Rows {
|
|
for i, cell := range row.Cells {
|
|
cols[i] = cell.Value + s(l[i]-len(cell.Value))
|
|
}
|
|
fmt.Println(s(f.indent*3) + cl("| "+strings.Join(cols, " | ")+" |", c))
|
|
}
|
|
}
|
|
|
|
// Passed is called to represent a passed step
|
|
func (f *pretty) Passed(step *gherkin.Step, match *StepDef) {
|
|
s := &passed{owner: f.scope, feature: f.features[len(f.features)-1], step: step, def: match}
|
|
f.printStepKind(s)
|
|
f.passed = append(f.passed, s)
|
|
}
|
|
|
|
// Skipped is called to represent a passed step
|
|
func (f *pretty) Skipped(step *gherkin.Step) {
|
|
s := &skipped{owner: f.scope, feature: f.features[len(f.features)-1], step: step}
|
|
f.printStepKind(s)
|
|
f.skipped = append(f.skipped, s)
|
|
}
|
|
|
|
// Undefined is called to represent a pending step
|
|
func (f *pretty) Undefined(step *gherkin.Step) {
|
|
s := &undefined{owner: f.scope, feature: f.features[len(f.features)-1], step: step}
|
|
f.printStepKind(s)
|
|
f.undefined = append(f.undefined, s)
|
|
}
|
|
|
|
// Failed is called to represent a failed step
|
|
func (f *pretty) Failed(step *gherkin.Step, match *StepDef, err error) {
|
|
s := &failed{owner: f.scope, feature: f.features[len(f.features)-1], step: step, def: match, err: err}
|
|
f.printStepKind(s)
|
|
f.failed = append(f.failed, s)
|
|
}
|
|
|
|
// longest gives a list of longest columns of all rows in Table
|
|
func longest(tbl interface{}) []int {
|
|
var rows []*gherkin.TableRow
|
|
switch t := tbl.(type) {
|
|
case *gherkin.Examples:
|
|
rows = append(rows, t.TableHeader)
|
|
rows = append(rows, t.TableBody...)
|
|
case *gherkin.DataTable:
|
|
rows = append(rows, t.Rows...)
|
|
}
|
|
|
|
longest := make([]int, len(rows[0].Cells))
|
|
for _, row := range rows {
|
|
for i, cell := range row.Cells {
|
|
if longest[i] < len(cell.Value) {
|
|
longest[i] = len(cell.Value)
|
|
}
|
|
}
|
|
}
|
|
return longest
|
|
}
|
|
|
|
func (f *pretty) longestStep(steps []*gherkin.Step, base int) int {
|
|
ret := base
|
|
for _, step := range steps {
|
|
length := f.length(step)
|
|
if length > ret {
|
|
ret = length
|
|
}
|
|
}
|
|
return ret
|
|
}
|