godog/fmt_pretty.go

492 строки
13 КиБ
Go

package godog
import (
"fmt"
"io"
"math"
"regexp"
"strings"
"unicode/utf8"
"github.com/cucumber/godog/colors"
"github.com/cucumber/godog/gherkin"
)
func init() {
Format("pretty", "Prints every feature with runtime statuses.", prettyFunc)
}
func prettyFunc(suite string, out io.Writer) Formatter {
return &pretty{basefmt: newBaseFmt(suite, out)}
}
var outlinePlaceholderRegexp = regexp.MustCompile("<[^>]+>")
// a built in default pretty formatter
type pretty struct {
*basefmt
// currently processed
feature *gherkin.Feature
scenario *gherkin.Scenario
outline *gherkin.ScenarioOutline
// state
bgSteps int
totalBgSteps int
steps int
commentPos int
// whether scenario or scenario outline keyword was printed
scenarioKeyword bool
// outline
outlineSteps []*stepResult
outlineNumExample int
outlineNumExamples int
}
func (f *pretty) Feature(ft *gherkin.Feature, p string, c []byte) {
if len(f.features) != 0 {
// not a first feature, add a newline
fmt.Fprintln(f.out, "")
}
f.features = append(f.features, &feature{Path: p, Feature: ft})
fmt.Fprintln(f.out, keywordAndName(ft.Keyword, ft.Name))
if strings.TrimSpace(ft.Description) != "" {
for _, line := range strings.Split(ft.Description, "\n") {
fmt.Fprintln(f.out, s(f.indent)+strings.TrimSpace(line))
}
}
f.feature = ft
f.scenario = nil
f.outline = nil
f.bgSteps = 0
f.totalBgSteps = 0
if ft.Background != nil {
f.bgSteps = len(ft.Background.Steps)
f.totalBgSteps = len(ft.Background.Steps)
}
}
// Node takes a gherkin node for formatting
func (f *pretty) Node(node interface{}) {
f.basefmt.Node(node)
switch t := node.(type) {
case *gherkin.Examples:
f.outlineNumExamples = len(t.TableBody)
f.outlineNumExample++
case *gherkin.Scenario:
f.scenario = t
f.outline = nil
f.steps = len(t.Steps) + f.totalBgSteps
f.scenarioKeyword = false
if isEmptyScenario(t) {
f.printUndefinedScenario(t)
}
case *gherkin.ScenarioOutline:
f.outline = t
f.scenario = nil
f.outlineNumExample = -1
f.scenarioKeyword = false
if isEmptyScenario(t) {
f.printUndefinedScenario(t)
}
case *gherkin.TableRow:
f.steps = len(f.outline.Steps) + f.totalBgSteps
f.outlineSteps = []*stepResult{}
}
}
func keywordAndName(keyword, name string) string {
title := whiteb(keyword + ":")
if len(name) > 0 {
title += " " + name
}
return title
}
func (f *pretty) printUndefinedScenario(sc interface{}) {
if f.bgSteps > 0 {
bg := f.feature.Background
f.commentPos = f.longestStep(bg.Steps, f.length(bg))
fmt.Fprintln(f.out, "\n"+s(f.indent)+keywordAndName(bg.Keyword, bg.Name))
for _, step := range bg.Steps {
f.bgSteps--
f.printStep(step, nil, colors.Cyan)
}
}
switch t := sc.(type) {
case *gherkin.Scenario:
f.commentPos = f.longestStep(t.Steps, f.length(sc))
text := s(f.indent) + keywordAndName(t.Keyword, t.Name)
text += s(f.commentPos-f.length(t)+1) + f.line(t.Location)
fmt.Fprintln(f.out, "\n"+text)
case *gherkin.ScenarioOutline:
f.commentPos = f.longestStep(t.Steps, f.length(sc))
text := s(f.indent) + keywordAndName(t.Keyword, t.Name)
text += s(f.commentPos-f.length(t)+1) + f.line(t.Location)
fmt.Fprintln(f.out, "\n"+text)
for _, example := range t.Examples {
max := longest(example, cyan)
f.printExampleHeader(example, max)
for _, row := range example.TableBody {
f.printExampleRow(row, max, cyan)
}
}
}
}
// Summary sumarize the feature formatter output
func (f *pretty) Summary() {
if len(f.failed) > 0 {
fmt.Fprintln(f.out, "\n--- "+red("Failed steps:")+"\n")
for _, fail := range f.failed {
fmt.Fprintln(f.out, s(2)+red(fail.scenarioDesc())+blackb(" # "+fail.scenarioLine()))
fmt.Fprintln(f.out, s(4)+red(strings.TrimSpace(fail.step.Keyword)+" "+fail.step.Text)+blackb(" # "+fail.line()))
fmt.Fprintln(f.out, s(6)+red("Error: ")+redb(fmt.Sprintf("%+v", fail.err))+"\n")
}
}
f.basefmt.Summary()
}
func (f *pretty) printOutlineExample(outline *gherkin.ScenarioOutline) {
var msg string
var clr colors.ColorFunc
ex := outline.Examples[f.outlineNumExample]
example, hasExamples := examples(ex)
if !hasExamples {
// do not print empty examples
return
}
firstExample := f.outlineNumExamples == len(example.TableBody)
printSteps := firstExample && f.outlineNumExample == 0
for i, res := range f.outlineSteps {
// determine example row status
switch {
case res.typ == failed:
msg = res.err.Error()
clr = res.typ.clr()
case res.typ == undefined || res.typ == pending:
clr = res.typ.clr()
case res.typ == skipped && clr == nil:
clr = cyan
}
if printSteps && i >= f.totalBgSteps {
// in first example, we need to print steps
var text string
ostep := outline.Steps[i-f.totalBgSteps]
if res.def != nil {
if m := outlinePlaceholderRegexp.FindAllStringIndex(ostep.Text, -1); len(m) > 0 {
var pos int
for i := 0; i < len(m); i++ {
pair := m[i]
text += cyan(ostep.Text[pos:pair[0]])
text += cyanb(ostep.Text[pair[0]:pair[1]])
pos = pair[1]
}
text += cyan(ostep.Text[pos:len(ostep.Text)])
} else {
text = cyan(ostep.Text)
}
text += s(f.commentPos-f.length(ostep)+1) + blackb(fmt.Sprintf("# %s", res.def.definitionID()))
} else {
text = cyan(ostep.Text)
}
// print the step outline
fmt.Fprintln(f.out, s(f.indent*2)+cyan(strings.TrimSpace(ostep.Keyword))+" "+text)
// print step argument
// @TODO: need to make example header cells bold
switch t := ostep.Argument.(type) {
case *gherkin.DataTable:
f.printTable(t, cyan)
case *gherkin.DocString:
var ct string
if len(t.ContentType) > 0 {
ct = " " + cyan(t.ContentType)
}
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))
}
}
}
if clr == nil {
clr = green
}
max := longest(example, clr, cyan)
// an example table header
if firstExample {
f.printExampleHeader(example, max)
}
// an example table row
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) printExampleRow(row *gherkin.TableRow, max []int, clr colors.ColorFunc) {
cells := make([]string, len(row.Cells))
for i, cell := range row.Cells {
val := clr(cell.Value)
ln := utf8.RuneCountInString(val)
cells[i] = val + s(max[i]-ln)
}
fmt.Fprintln(f.out, s(f.indent*3)+"| "+strings.Join(cells, " | ")+" |")
}
func (f *pretty) printExampleHeader(example *gherkin.Examples, max []int) {
cells := make([]string, len(example.TableHeader.Cells))
// an example table header
fmt.Fprintln(f.out, "")
fmt.Fprintln(f.out, s(f.indent*2)+keywordAndName(example.Keyword, example.Name))
for i, cell := range example.TableHeader.Cells {
val := cyan(cell.Value)
ln := utf8.RuneCountInString(val)
cells[i] = val + s(max[i]-ln)
}
fmt.Fprintln(f.out, s(f.indent*3)+"| "+strings.Join(cells, " | ")+" |")
}
func (f *pretty) printStep(step *gherkin.Step, def *StepDef, c colors.ColorFunc) {
text := s(f.indent*2) + c(strings.TrimSpace(step.Keyword)) + " "
switch {
case def != nil:
if m := def.Expr.FindStringSubmatchIndex(step.Text)[2:]; len(m) > 0 {
var pos, i int
for pos, i = 0, 0; i < len(m); i++ {
if m[i] == -1 {
continue // no index for this match
}
if math.Mod(float64(i), 2) == 0 {
text += c(step.Text[pos:m[i]])
} else {
text += colors.Bold(c)(step.Text[pos:m[i]])
}
pos = m[i]
}
text += c(step.Text[pos:len(step.Text)])
} else {
text += c(step.Text)
}
text += s(f.commentPos-f.length(step)+1) + blackb(fmt.Sprintf("# %s", def.definitionID()))
default:
text += c(step.Text)
}
fmt.Fprintln(f.out, text)
switch t := step.Argument.(type) {
case *gherkin.DataTable:
f.printTable(t, c)
case *gherkin.DocString:
var ct string
if len(t.ContentType) > 0 {
ct = " " + c(t.ContentType)
}
fmt.Fprintln(f.out, s(f.indent*3)+c(t.Delimitter)+ct)
for _, ln := range strings.Split(t.Content, "\n") {
fmt.Fprintln(f.out, s(f.indent*3)+c(ln))
}
fmt.Fprintln(f.out, s(f.indent*3)+c(t.Delimitter))
}
}
func (f *pretty) printStepKind(res *stepResult) {
f.steps--
if f.outline != nil {
f.outlineSteps = append(f.outlineSteps, res)
}
var bgStep bool
bg := f.feature.Background
// if has not printed background yet
switch {
// first background step
case f.bgSteps > 0 && f.bgSteps == len(bg.Steps):
f.commentPos = f.longestStep(bg.Steps, f.length(bg))
fmt.Fprintln(f.out, "\n"+s(f.indent)+keywordAndName(bg.Keyword, bg.Name))
f.bgSteps--
bgStep = true
// subsequent background steps
case f.bgSteps > 0:
f.bgSteps--
bgStep = true
// first step of scenario, print header and calculate comment position
case f.scenario != nil:
// print scenario keyword and value if first example
if !f.scenarioKeyword {
f.commentPos = f.longestStep(f.scenario.Steps, f.length(f.scenario))
if bg != nil {
if bgLen := f.longestStep(bg.Steps, f.length(bg)); bgLen > f.commentPos {
f.commentPos = bgLen
}
}
text := s(f.indent) + keywordAndName(f.scenario.Keyword, f.scenario.Name)
text += s(f.commentPos-f.length(f.scenario)+1) + f.line(f.scenario.Location)
fmt.Fprintln(f.out, "\n"+text)
f.scenarioKeyword = true
}
// first step of outline scenario, print header and calculate comment position
case f.outline != nil:
// print scenario keyword and value if first example
if !f.scenarioKeyword {
f.commentPos = f.longestStep(f.outline.Steps, f.length(f.outline))
if bg != nil {
if bgLen := f.longestStep(bg.Steps, f.length(bg)); bgLen > f.commentPos {
f.commentPos = bgLen
}
}
text := s(f.indent) + keywordAndName(f.outline.Keyword, f.outline.Name)
text += s(f.commentPos-f.length(f.outline)+1) + f.line(f.outline.Location)
fmt.Fprintln(f.out, "\n"+text)
f.scenarioKeyword = true
}
if len(f.outlineSteps) == len(f.outline.Steps)+f.totalBgSteps {
// an outline example steps has went through
f.printOutlineExample(f.outline)
f.outlineNumExamples--
}
return
}
if !f.isBackgroundStep(res.step) || bgStep {
f.printStep(res.step, res.def, res.typ.clr())
}
if res.err != nil {
fmt.Fprintln(f.out, s(f.indent*2)+redb(fmt.Sprintf("%+v", res.err)))
}
if res.typ == pending {
fmt.Fprintln(f.out, s(f.indent*3)+yellow("TODO: write pending definition"))
}
}
func (f *pretty) isBackgroundStep(step *gherkin.Step) bool {
if f.feature.Background == nil {
return false
}
for _, bstep := range f.feature.Background.Steps {
if bstep.Location.Line == step.Location.Line {
return true
}
}
return false
}
// print table with aligned table cells
func (f *pretty) printTable(t *gherkin.DataTable, c colors.ColorFunc) {
var l = longest(t, c)
var cols = make([]string, len(t.Rows[0].Cells))
for _, row := range t.Rows {
for i, cell := range row.Cells {
val := c(cell.Value)
ln := utf8.RuneCountInString(val)
cols[i] = val + s(l[i]-ln)
}
fmt.Fprintln(f.out, s(f.indent*3)+"| "+strings.Join(cols, " | ")+" |")
}
}
func (f *pretty) Passed(step *gherkin.Step, match *StepDef) {
f.basefmt.Passed(step, match)
f.printStepKind(f.passed[len(f.passed)-1])
}
func (f *pretty) Skipped(step *gherkin.Step, match *StepDef) {
f.basefmt.Skipped(step, match)
f.printStepKind(f.skipped[len(f.skipped)-1])
}
func (f *pretty) Undefined(step *gherkin.Step, match *StepDef) {
f.basefmt.Undefined(step, match)
f.printStepKind(f.undefined[len(f.undefined)-1])
}
func (f *pretty) Failed(step *gherkin.Step, match *StepDef, err error) {
f.basefmt.Failed(step, match, err)
f.printStepKind(f.failed[len(f.failed)-1])
}
func (f *pretty) Pending(step *gherkin.Step, match *StepDef) {
f.basefmt.Pending(step, match)
f.printStepKind(f.pending[len(f.pending)-1])
}
// longest gives a list of longest columns of all rows in Table
func longest(tbl interface{}, clrs ...colors.ColorFunc) []int {
var rows []*gherkin.TableRow
switch t := tbl.(type) {
case *gherkin.Examples:
rows = append(rows, t.TableHeader)
rows = append(rows, t.TableBody...)
case *gherkin.DataTable:
rows = append(rows, t.Rows...)
}
longest := make([]int, len(rows[0].Cells))
for _, row := range rows {
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 []*gherkin.Step, base int) int {
ret := base
for _, step := range steps {
length := f.length(step)
if length > ret {
ret = length
}
}
return ret
}
// a line number representation in feature file
func (f *pretty) line(loc *gherkin.Location) string {
return blackb(fmt.Sprintf("# %s:%d", f.features[len(f.features)-1].Path, loc.Line))
}
func (f *pretty) length(node interface{}) int {
switch t := node.(type) {
case *gherkin.Background:
return f.indent + utf8.RuneCountInString(strings.TrimSpace(t.Keyword)+": "+t.Name)
case *gherkin.Step:
return f.indent*2 + utf8.RuneCountInString(strings.TrimSpace(t.Keyword)+" "+t.Text)
case *gherkin.Scenario:
return f.indent + utf8.RuneCountInString(strings.TrimSpace(t.Keyword)+": "+t.Name)
case *gherkin.ScenarioOutline:
return f.indent + utf8.RuneCountInString(strings.TrimSpace(t.Keyword)+": "+t.Name)
}
panic(fmt.Sprintf("unexpected node %T to determine length", node))
}