godog/fmt_pretty.go

407 строки
11 КиБ
Go

package godog
import (
"fmt"
"io"
"math"
"regexp"
"strings"
"unicode/utf8"
"github.com/DATA-DOG/godog/colors"
"github.com/DATA-DOG/godog/gherkin"
)
func init() {
Format("pretty", "Prints every feature with runtime statuses.", prettyFunc)
}
func prettyFunc(suite string, out io.Writer) Formatter {
return &pretty{
basefmt: basefmt{
started: timeNowFunc(),
indent: 2,
out: 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
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, whiteb(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
if ft.Background != nil {
f.bgSteps = 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.bgSteps
f.scenarioKeyword = false
case *gherkin.ScenarioOutline:
f.outline = t
f.scenario = nil
f.outlineNumExample = -1
f.scenarioKeyword = false
case *gherkin.TableRow:
f.steps = len(f.outline.Steps) + f.bgSteps
f.outlineSteps = []*stepResult{}
}
}
// Summary sumarize the feature formatter output
func (f *pretty) Summary() {
// failed steps on background are not scenarios
var failedScenarios []*stepResult
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.Fprintln(f.out, "\n--- "+red("Failed scenarios:")+"\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.Fprintln(f.out, " "+red(fail))
}
}
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 {
// in first example, we need to print steps
var text string
ostep := outline.Steps[i]
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) + black(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)
}
}
cells := make([]string, len(example.TableHeader.Cells))
max := longest(example)
// an example table header
if firstExample {
fmt.Fprintln(f.out, "")
fmt.Fprintln(f.out, s(f.indent*2)+whiteb(example.Keyword+": ")+example.Name)
for i, cell := range example.TableHeader.Cells {
cells[i] = cyan(cell.Value) + s(max[i]-len(cell.Value))
}
fmt.Fprintln(f.out, s(f.indent*3)+"| "+strings.Join(cells, " | ")+" |")
}
if clr == nil {
clr = green
}
// an example table row
row := example.TableBody[len(example.TableBody)-f.outlineNumExamples]
for i, cell := range row.Cells {
cells[i] = clr(cell.Value) + s(max[i]-len(cell.Value))
}
fmt.Fprintln(f.out, s(f.indent*3)+"| "+strings.Join(cells, " | ")+" |")
// if there is an error
if msg != "" {
fmt.Fprintln(f.out, s(f.indent*4)+redb(msg))
}
}
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) + black(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) {
// if has not printed background yet
switch {
// first background step
case f.bgSteps > 0 && f.bgSteps == len(f.feature.Background.Steps):
f.commentPos = f.longestStep(f.feature.Background.Steps, f.length(f.feature.Background))
fmt.Fprintln(f.out, "\n"+s(f.indent)+whiteb(f.feature.Background.Keyword+": "+f.feature.Background.Name))
f.bgSteps--
// subsequent background steps
case f.bgSteps > 0:
f.bgSteps--
// 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 f.feature.Background != nil {
if bgLen := f.longestStep(f.feature.Background.Steps, f.length(f.feature.Background)); bgLen > f.commentPos {
f.commentPos = bgLen
}
}
text := s(f.indent) + whiteb(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
}
f.steps--
// first step of outline scenario, print header and calculate comment position
case f.outline != nil:
f.outlineSteps = append(f.outlineSteps, res)
f.steps--
// print scenario keyword and value if first example
if !f.scenarioKeyword {
f.commentPos = f.longestStep(f.outline.Steps, f.length(f.outline))
if f.feature.Background != nil {
if bgLen := f.longestStep(f.feature.Background.Steps, f.length(f.feature.Background)); bgLen > f.commentPos {
f.commentPos = bgLen
}
}
text := s(f.indent) + whiteb(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.bgSteps {
// an outline example steps has went through
f.printOutlineExample(f.outline)
f.outlineNumExamples--
}
return
}
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"))
}
}
// print table with aligned table cells
func (f *pretty) printTable(t *gherkin.DataTable, c colors.ColorFunc) {
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.Fprintln(f.out, s(f.indent*3)+c("| "+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{}) []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
}
// a line number representation in feature file
func (f *pretty) line(loc *gherkin.Location) string {
return black(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))
}