
Context: While trying to create an helper library to manage http rest api testing, I made a system witch allow to pick value from responses, header, cookie, ... and inject then as variables. Issue: Doing this, when the inject variable make the line longer than the longest declared step, godog will failed to render test result under pretty formatting cause it will try to write a comment on a negative index Fix: Fix s methods so it will not goes to fatal when recieving negative number.
496 строки
13 КиБ
Go
496 строки
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)) + 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)) + 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+1)
|
|
}
|
|
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+1)
|
|
}
|
|
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+1)
|
|
}
|
|
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))
|
|
}
|