github.com/data-DOG/godog@v0.7.9/fmt_pretty.go (about)

     1  package godog
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"math"
     7  	"regexp"
     8  	"strings"
     9  	"unicode/utf8"
    10  
    11  	"github.com/DATA-DOG/godog/colors"
    12  	"github.com/DATA-DOG/godog/gherkin"
    13  )
    14  
    15  func init() {
    16  	Format("pretty", "Prints every feature with runtime statuses.", prettyFunc)
    17  }
    18  
    19  func prettyFunc(suite string, out io.Writer) Formatter {
    20  	return &pretty{
    21  		basefmt: basefmt{
    22  			started: timeNowFunc(),
    23  			indent:  2,
    24  			out:     out,
    25  		},
    26  	}
    27  }
    28  
    29  var outlinePlaceholderRegexp = regexp.MustCompile("<[^>]+>")
    30  
    31  // a built in default pretty formatter
    32  type pretty struct {
    33  	basefmt
    34  
    35  	// currently processed
    36  	feature  *gherkin.Feature
    37  	scenario *gherkin.Scenario
    38  	outline  *gherkin.ScenarioOutline
    39  
    40  	// state
    41  	bgSteps      int
    42  	totalBgSteps int
    43  	steps        int
    44  	commentPos   int
    45  
    46  	// whether scenario or scenario outline keyword was printed
    47  	scenarioKeyword bool
    48  
    49  	// outline
    50  	outlineSteps       []*stepResult
    51  	outlineNumExample  int
    52  	outlineNumExamples int
    53  }
    54  
    55  func (f *pretty) Feature(ft *gherkin.Feature, p string, c []byte) {
    56  	if len(f.features) != 0 {
    57  		// not a first feature, add a newline
    58  		fmt.Fprintln(f.out, "")
    59  	}
    60  	f.features = append(f.features, &feature{Path: p, Feature: ft})
    61  	fmt.Fprintln(f.out, keywordAndName(ft.Keyword, ft.Name))
    62  	if strings.TrimSpace(ft.Description) != "" {
    63  		for _, line := range strings.Split(ft.Description, "\n") {
    64  			fmt.Fprintln(f.out, s(f.indent)+strings.TrimSpace(line))
    65  		}
    66  	}
    67  
    68  	f.feature = ft
    69  	f.scenario = nil
    70  	f.outline = nil
    71  	f.bgSteps = 0
    72  	f.totalBgSteps = 0
    73  	if ft.Background != nil {
    74  		f.bgSteps = len(ft.Background.Steps)
    75  		f.totalBgSteps = len(ft.Background.Steps)
    76  	}
    77  }
    78  
    79  // Node takes a gherkin node for formatting
    80  func (f *pretty) Node(node interface{}) {
    81  	f.basefmt.Node(node)
    82  
    83  	switch t := node.(type) {
    84  	case *gherkin.Examples:
    85  		f.outlineNumExamples = len(t.TableBody)
    86  		f.outlineNumExample++
    87  	case *gherkin.Scenario:
    88  		f.scenario = t
    89  		f.outline = nil
    90  		f.steps = len(t.Steps) + f.totalBgSteps
    91  		f.scenarioKeyword = false
    92  		if isEmptyScenario(t) {
    93  			f.printUndefinedScenario(t)
    94  		}
    95  	case *gherkin.ScenarioOutline:
    96  		f.outline = t
    97  		f.scenario = nil
    98  		f.outlineNumExample = -1
    99  		f.scenarioKeyword = false
   100  		if isEmptyScenario(t) {
   101  			f.printUndefinedScenario(t)
   102  		}
   103  	case *gherkin.TableRow:
   104  		f.steps = len(f.outline.Steps) + f.totalBgSteps
   105  		f.outlineSteps = []*stepResult{}
   106  	}
   107  }
   108  
   109  func keywordAndName(keyword, name string) string {
   110  	title := whiteb(keyword + ":")
   111  	if len(name) > 0 {
   112  		title += " " + name
   113  	}
   114  	return title
   115  }
   116  
   117  func (f *pretty) printUndefinedScenario(sc interface{}) {
   118  	if f.bgSteps > 0 {
   119  		bg := f.feature.Background
   120  		f.commentPos = f.longestStep(bg.Steps, f.length(bg))
   121  		fmt.Fprintln(f.out, "\n"+s(f.indent)+keywordAndName(bg.Keyword, bg.Name))
   122  
   123  		for _, step := range bg.Steps {
   124  			f.bgSteps--
   125  			f.printStep(step, nil, colors.Cyan)
   126  		}
   127  	}
   128  
   129  	switch t := sc.(type) {
   130  	case *gherkin.Scenario:
   131  		f.commentPos = f.longestStep(t.Steps, f.length(sc))
   132  		text := s(f.indent) + keywordAndName(t.Keyword, t.Name)
   133  		text += s(f.commentPos-f.length(t)+1) + f.line(t.Location)
   134  		fmt.Fprintln(f.out, "\n"+text)
   135  	case *gherkin.ScenarioOutline:
   136  		f.commentPos = f.longestStep(t.Steps, f.length(sc))
   137  		text := s(f.indent) + keywordAndName(t.Keyword, t.Name)
   138  		text += s(f.commentPos-f.length(t)+1) + f.line(t.Location)
   139  		fmt.Fprintln(f.out, "\n"+text)
   140  
   141  		for _, example := range t.Examples {
   142  			max := longest(example, cyan)
   143  			f.printExampleHeader(example, max)
   144  			for _, row := range example.TableBody {
   145  				f.printExampleRow(row, max, cyan)
   146  			}
   147  		}
   148  	}
   149  }
   150  
   151  // Summary sumarize the feature formatter output
   152  func (f *pretty) Summary() {
   153  	if len(f.failed) > 0 {
   154  		fmt.Fprintln(f.out, "\n--- "+red("Failed steps:")+"\n")
   155  		for _, fail := range f.failed {
   156  			fmt.Fprintln(f.out, s(2)+red(fail.scenarioDesc())+black(" # "+fail.scenarioLine()))
   157  			fmt.Fprintln(f.out, s(4)+red(strings.TrimSpace(fail.step.Keyword)+" "+fail.step.Text)+black(" # "+fail.line()))
   158  			fmt.Fprintln(f.out, s(6)+red("Error: ")+redb(fmt.Sprintf("%+v", fail.err))+"\n")
   159  		}
   160  	}
   161  	f.basefmt.Summary()
   162  }
   163  
   164  func (f *pretty) printOutlineExample(outline *gherkin.ScenarioOutline) {
   165  	var msg string
   166  	var clr colors.ColorFunc
   167  
   168  	ex := outline.Examples[f.outlineNumExample]
   169  	example, hasExamples := examples(ex)
   170  	if !hasExamples {
   171  		// do not print empty examples
   172  		return
   173  	}
   174  
   175  	firstExample := f.outlineNumExamples == len(example.TableBody)
   176  	printSteps := firstExample && f.outlineNumExample == 0
   177  
   178  	for i, res := range f.outlineSteps {
   179  		// determine example row status
   180  		switch {
   181  		case res.typ == failed:
   182  			msg = res.err.Error()
   183  			clr = res.typ.clr()
   184  		case res.typ == undefined || res.typ == pending:
   185  			clr = res.typ.clr()
   186  		case res.typ == skipped && clr == nil:
   187  			clr = cyan
   188  		}
   189  		if printSteps && i >= f.totalBgSteps {
   190  			// in first example, we need to print steps
   191  			var text string
   192  			ostep := outline.Steps[i-f.totalBgSteps]
   193  			if res.def != nil {
   194  				if m := outlinePlaceholderRegexp.FindAllStringIndex(ostep.Text, -1); len(m) > 0 {
   195  					var pos int
   196  					for i := 0; i < len(m); i++ {
   197  						pair := m[i]
   198  						text += cyan(ostep.Text[pos:pair[0]])
   199  						text += cyanb(ostep.Text[pair[0]:pair[1]])
   200  						pos = pair[1]
   201  					}
   202  					text += cyan(ostep.Text[pos:len(ostep.Text)])
   203  				} else {
   204  					text = cyan(ostep.Text)
   205  				}
   206  				text += s(f.commentPos-f.length(ostep)+1) + black(fmt.Sprintf("# %s", res.def.definitionID()))
   207  			} else {
   208  				text = cyan(ostep.Text)
   209  			}
   210  			// print the step outline
   211  			fmt.Fprintln(f.out, s(f.indent*2)+cyan(strings.TrimSpace(ostep.Keyword))+" "+text)
   212  
   213  			// print step argument
   214  			// @TODO: need to make example header cells bold
   215  			switch t := ostep.Argument.(type) {
   216  			case *gherkin.DataTable:
   217  				f.printTable(t, cyan)
   218  			case *gherkin.DocString:
   219  				var ct string
   220  				if len(t.ContentType) > 0 {
   221  					ct = " " + cyan(t.ContentType)
   222  				}
   223  				fmt.Fprintln(f.out, s(f.indent*3)+cyan(t.Delimitter)+ct)
   224  				for _, ln := range strings.Split(t.Content, "\n") {
   225  					fmt.Fprintln(f.out, s(f.indent*3)+cyan(ln))
   226  				}
   227  				fmt.Fprintln(f.out, s(f.indent*3)+cyan(t.Delimitter))
   228  			}
   229  		}
   230  	}
   231  
   232  	if clr == nil {
   233  		clr = green
   234  	}
   235  
   236  	max := longest(example, clr, cyan)
   237  	// an example table header
   238  	if firstExample {
   239  		f.printExampleHeader(example, max)
   240  	}
   241  
   242  	// an example table row
   243  	row := example.TableBody[len(example.TableBody)-f.outlineNumExamples]
   244  	f.printExampleRow(row, max, clr)
   245  
   246  	// if there is an error
   247  	if msg != "" {
   248  		fmt.Fprintln(f.out, s(f.indent*4)+redb(msg))
   249  	}
   250  }
   251  
   252  func (f *pretty) printExampleRow(row *gherkin.TableRow, max []int, clr colors.ColorFunc) {
   253  	cells := make([]string, len(row.Cells))
   254  	for i, cell := range row.Cells {
   255  		val := clr(cell.Value)
   256  		ln := utf8.RuneCountInString(val)
   257  		cells[i] = val + s(max[i]-ln)
   258  	}
   259  	fmt.Fprintln(f.out, s(f.indent*3)+"| "+strings.Join(cells, " | ")+" |")
   260  }
   261  
   262  func (f *pretty) printExampleHeader(example *gherkin.Examples, max []int) {
   263  	cells := make([]string, len(example.TableHeader.Cells))
   264  	// an example table header
   265  	fmt.Fprintln(f.out, "")
   266  	fmt.Fprintln(f.out, s(f.indent*2)+keywordAndName(example.Keyword, example.Name))
   267  
   268  	for i, cell := range example.TableHeader.Cells {
   269  		val := cyan(cell.Value)
   270  		ln := utf8.RuneCountInString(val)
   271  		cells[i] = val + s(max[i]-ln)
   272  	}
   273  	fmt.Fprintln(f.out, s(f.indent*3)+"| "+strings.Join(cells, " | ")+" |")
   274  }
   275  
   276  func (f *pretty) printStep(step *gherkin.Step, def *StepDef, c colors.ColorFunc) {
   277  	text := s(f.indent*2) + c(strings.TrimSpace(step.Keyword)) + " "
   278  	switch {
   279  	case def != nil:
   280  		if m := def.Expr.FindStringSubmatchIndex(step.Text)[2:]; len(m) > 0 {
   281  			var pos, i int
   282  			for pos, i = 0, 0; i < len(m); i++ {
   283  				if m[i] == -1 {
   284  					continue // no index for this match
   285  				}
   286  				if math.Mod(float64(i), 2) == 0 {
   287  					text += c(step.Text[pos:m[i]])
   288  				} else {
   289  					text += colors.Bold(c)(step.Text[pos:m[i]])
   290  				}
   291  				pos = m[i]
   292  			}
   293  			text += c(step.Text[pos:len(step.Text)])
   294  		} else {
   295  			text += c(step.Text)
   296  		}
   297  		text += s(f.commentPos-f.length(step)+1) + black(fmt.Sprintf("# %s", def.definitionID()))
   298  	default:
   299  		text += c(step.Text)
   300  	}
   301  
   302  	fmt.Fprintln(f.out, text)
   303  	switch t := step.Argument.(type) {
   304  	case *gherkin.DataTable:
   305  		f.printTable(t, c)
   306  	case *gherkin.DocString:
   307  		var ct string
   308  		if len(t.ContentType) > 0 {
   309  			ct = " " + c(t.ContentType)
   310  		}
   311  		fmt.Fprintln(f.out, s(f.indent*3)+c(t.Delimitter)+ct)
   312  		for _, ln := range strings.Split(t.Content, "\n") {
   313  			fmt.Fprintln(f.out, s(f.indent*3)+c(ln))
   314  		}
   315  		fmt.Fprintln(f.out, s(f.indent*3)+c(t.Delimitter))
   316  	}
   317  }
   318  
   319  func (f *pretty) printStepKind(res *stepResult) {
   320  	f.steps--
   321  	if f.outline != nil {
   322  		f.outlineSteps = append(f.outlineSteps, res)
   323  	}
   324  	var bgStep bool
   325  	bg := f.feature.Background
   326  
   327  	// if has not printed background yet
   328  	switch {
   329  	// first background step
   330  	case f.bgSteps > 0 && f.bgSteps == len(bg.Steps):
   331  		f.commentPos = f.longestStep(bg.Steps, f.length(bg))
   332  		fmt.Fprintln(f.out, "\n"+s(f.indent)+keywordAndName(bg.Keyword, bg.Name))
   333  		f.bgSteps--
   334  		bgStep = true
   335  	// subsequent background steps
   336  	case f.bgSteps > 0:
   337  		f.bgSteps--
   338  		bgStep = true
   339  	// first step of scenario, print header and calculate comment position
   340  	case f.scenario != nil:
   341  		// print scenario keyword and value if first example
   342  		if !f.scenarioKeyword {
   343  			f.commentPos = f.longestStep(f.scenario.Steps, f.length(f.scenario))
   344  			if bg != nil {
   345  				if bgLen := f.longestStep(bg.Steps, f.length(bg)); bgLen > f.commentPos {
   346  					f.commentPos = bgLen
   347  				}
   348  			}
   349  			text := s(f.indent) + keywordAndName(f.scenario.Keyword, f.scenario.Name)
   350  			text += s(f.commentPos-f.length(f.scenario)+1) + f.line(f.scenario.Location)
   351  			fmt.Fprintln(f.out, "\n"+text)
   352  			f.scenarioKeyword = true
   353  		}
   354  	// first step of outline scenario, print header and calculate comment position
   355  	case f.outline != nil:
   356  		// print scenario keyword and value if first example
   357  		if !f.scenarioKeyword {
   358  			f.commentPos = f.longestStep(f.outline.Steps, f.length(f.outline))
   359  			if bg != nil {
   360  				if bgLen := f.longestStep(bg.Steps, f.length(bg)); bgLen > f.commentPos {
   361  					f.commentPos = bgLen
   362  				}
   363  			}
   364  			text := s(f.indent) + keywordAndName(f.outline.Keyword, f.outline.Name)
   365  			text += s(f.commentPos-f.length(f.outline)+1) + f.line(f.outline.Location)
   366  			fmt.Fprintln(f.out, "\n"+text)
   367  			f.scenarioKeyword = true
   368  		}
   369  		if len(f.outlineSteps) == len(f.outline.Steps)+f.totalBgSteps {
   370  			// an outline example steps has went through
   371  			f.printOutlineExample(f.outline)
   372  			f.outlineNumExamples--
   373  		}
   374  		return
   375  	}
   376  
   377  	if !f.isBackgroundStep(res.step) || bgStep {
   378  		f.printStep(res.step, res.def, res.typ.clr())
   379  	}
   380  	if res.err != nil {
   381  		fmt.Fprintln(f.out, s(f.indent*2)+redb(fmt.Sprintf("%+v", res.err)))
   382  	}
   383  	if res.typ == pending {
   384  		fmt.Fprintln(f.out, s(f.indent*3)+yellow("TODO: write pending definition"))
   385  	}
   386  }
   387  
   388  func (f *pretty) isBackgroundStep(step *gherkin.Step) bool {
   389  	if f.feature.Background == nil {
   390  		return false
   391  	}
   392  
   393  	for _, bstep := range f.feature.Background.Steps {
   394  		if bstep.Location.Line == step.Location.Line {
   395  			return true
   396  		}
   397  	}
   398  	return false
   399  }
   400  
   401  // print table with aligned table cells
   402  func (f *pretty) printTable(t *gherkin.DataTable, c colors.ColorFunc) {
   403  	var l = longest(t, c)
   404  	var cols = make([]string, len(t.Rows[0].Cells))
   405  	for _, row := range t.Rows {
   406  		for i, cell := range row.Cells {
   407  			val := c(cell.Value)
   408  			ln := utf8.RuneCountInString(val)
   409  			cols[i] = val + s(l[i]-ln)
   410  		}
   411  		fmt.Fprintln(f.out, s(f.indent*3)+"| "+strings.Join(cols, " | ")+" |")
   412  	}
   413  }
   414  
   415  func (f *pretty) Passed(step *gherkin.Step, match *StepDef) {
   416  	f.basefmt.Passed(step, match)
   417  	f.printStepKind(f.passed[len(f.passed)-1])
   418  }
   419  
   420  func (f *pretty) Skipped(step *gherkin.Step, match *StepDef) {
   421  	f.basefmt.Skipped(step, match)
   422  	f.printStepKind(f.skipped[len(f.skipped)-1])
   423  }
   424  
   425  func (f *pretty) Undefined(step *gherkin.Step, match *StepDef) {
   426  	f.basefmt.Undefined(step, match)
   427  	f.printStepKind(f.undefined[len(f.undefined)-1])
   428  }
   429  
   430  func (f *pretty) Failed(step *gherkin.Step, match *StepDef, err error) {
   431  	f.basefmt.Failed(step, match, err)
   432  	f.printStepKind(f.failed[len(f.failed)-1])
   433  }
   434  
   435  func (f *pretty) Pending(step *gherkin.Step, match *StepDef) {
   436  	f.basefmt.Pending(step, match)
   437  	f.printStepKind(f.pending[len(f.pending)-1])
   438  }
   439  
   440  // longest gives a list of longest columns of all rows in Table
   441  func longest(tbl interface{}, clrs ...colors.ColorFunc) []int {
   442  	var rows []*gherkin.TableRow
   443  	switch t := tbl.(type) {
   444  	case *gherkin.Examples:
   445  		rows = append(rows, t.TableHeader)
   446  		rows = append(rows, t.TableBody...)
   447  	case *gherkin.DataTable:
   448  		rows = append(rows, t.Rows...)
   449  	}
   450  
   451  	longest := make([]int, len(rows[0].Cells))
   452  	for _, row := range rows {
   453  		for i, cell := range row.Cells {
   454  			for _, c := range clrs {
   455  				ln := utf8.RuneCountInString(c(cell.Value))
   456  				if longest[i] < ln {
   457  					longest[i] = ln
   458  				}
   459  			}
   460  
   461  			ln := utf8.RuneCountInString(cell.Value)
   462  			if longest[i] < ln {
   463  				longest[i] = ln
   464  			}
   465  		}
   466  	}
   467  	return longest
   468  }
   469  
   470  func (f *pretty) longestStep(steps []*gherkin.Step, base int) int {
   471  	ret := base
   472  	for _, step := range steps {
   473  		length := f.length(step)
   474  		if length > ret {
   475  			ret = length
   476  		}
   477  	}
   478  	return ret
   479  }
   480  
   481  // a line number representation in feature file
   482  func (f *pretty) line(loc *gherkin.Location) string {
   483  	return black(fmt.Sprintf("# %s:%d", f.features[len(f.features)-1].Path, loc.Line))
   484  }
   485  
   486  func (f *pretty) length(node interface{}) int {
   487  	switch t := node.(type) {
   488  	case *gherkin.Background:
   489  		return f.indent + utf8.RuneCountInString(strings.TrimSpace(t.Keyword)+": "+t.Name)
   490  	case *gherkin.Step:
   491  		return f.indent*2 + utf8.RuneCountInString(strings.TrimSpace(t.Keyword)+" "+t.Text)
   492  	case *gherkin.Scenario:
   493  		return f.indent + utf8.RuneCountInString(strings.TrimSpace(t.Keyword)+": "+t.Name)
   494  	case *gherkin.ScenarioOutline:
   495  		return f.indent + utf8.RuneCountInString(strings.TrimSpace(t.Keyword)+": "+t.Name)
   496  	}
   497  	panic(fmt.Sprintf("unexpected node %T to determine length", node))
   498  }