github.com/maps90/godog@v0.7.5-0.20170923143419-0093943021d4/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, whiteb(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  	case *gherkin.ScenarioOutline:
    93  		f.outline = t
    94  		f.scenario = nil
    95  		f.outlineNumExample = -1
    96  		f.scenarioKeyword = false
    97  	case *gherkin.TableRow:
    98  		f.steps = len(f.outline.Steps) + f.totalBgSteps
    99  		f.outlineSteps = []*stepResult{}
   100  	}
   101  }
   102  
   103  // Summary sumarize the feature formatter output
   104  func (f *pretty) Summary() {
   105  	// failed steps on background are not scenarios
   106  	var failedScenarios []*stepResult
   107  	for _, fail := range f.failed {
   108  		switch fail.owner.(type) {
   109  		case *gherkin.Scenario:
   110  			failedScenarios = append(failedScenarios, fail)
   111  		case *gherkin.ScenarioOutline:
   112  			failedScenarios = append(failedScenarios, fail)
   113  		}
   114  	}
   115  	if len(failedScenarios) > 0 {
   116  		fmt.Fprintln(f.out, "\n--- "+red("Failed scenarios:")+"\n")
   117  		var unique []string
   118  		for _, fail := range failedScenarios {
   119  			var found bool
   120  			for _, in := range unique {
   121  				if in == fail.line() {
   122  					found = true
   123  					break
   124  				}
   125  			}
   126  			if !found {
   127  				unique = append(unique, fail.line())
   128  			}
   129  		}
   130  
   131  		for _, fail := range unique {
   132  			fmt.Fprintln(f.out, "    "+red(fail))
   133  		}
   134  	}
   135  	f.basefmt.Summary()
   136  }
   137  
   138  func (f *pretty) printOutlineExample(outline *gherkin.ScenarioOutline) {
   139  	var msg string
   140  	var clr colors.ColorFunc
   141  
   142  	ex := outline.Examples[f.outlineNumExample]
   143  	example, hasExamples := examples(ex)
   144  	if !hasExamples {
   145  		// do not print empty examples
   146  		return
   147  	}
   148  
   149  	firstExample := f.outlineNumExamples == len(example.TableBody)
   150  	printSteps := firstExample && f.outlineNumExample == 0
   151  
   152  	for i, res := range f.outlineSteps {
   153  		// determine example row status
   154  		switch {
   155  		case res.typ == failed:
   156  			msg = res.err.Error()
   157  			clr = res.typ.clr()
   158  		case res.typ == undefined || res.typ == pending:
   159  			clr = res.typ.clr()
   160  		case res.typ == skipped && clr == nil:
   161  			clr = cyan
   162  		}
   163  		if printSteps && i >= f.totalBgSteps {
   164  			// in first example, we need to print steps
   165  			var text string
   166  			ostep := outline.Steps[i-f.totalBgSteps]
   167  			if res.def != nil {
   168  				if m := outlinePlaceholderRegexp.FindAllStringIndex(ostep.Text, -1); len(m) > 0 {
   169  					var pos int
   170  					for i := 0; i < len(m); i++ {
   171  						pair := m[i]
   172  						text += cyan(ostep.Text[pos:pair[0]])
   173  						text += cyanb(ostep.Text[pair[0]:pair[1]])
   174  						pos = pair[1]
   175  					}
   176  					text += cyan(ostep.Text[pos:len(ostep.Text)])
   177  				} else {
   178  					text = cyan(ostep.Text)
   179  				}
   180  				text += s(f.commentPos-f.length(ostep)+1) + black(fmt.Sprintf("# %s", res.def.definitionID()))
   181  			} else {
   182  				text = cyan(ostep.Text)
   183  			}
   184  			// print the step outline
   185  			fmt.Fprintln(f.out, s(f.indent*2)+cyan(strings.TrimSpace(ostep.Keyword))+" "+text)
   186  
   187  			// print step argument
   188  			// @TODO: need to make example header cells bold
   189  			switch t := ostep.Argument.(type) {
   190  			case *gherkin.DataTable:
   191  				f.printTable(t, cyan)
   192  			case *gherkin.DocString:
   193  				var ct string
   194  				if len(t.ContentType) > 0 {
   195  					ct = " " + cyan(t.ContentType)
   196  				}
   197  				fmt.Fprintln(f.out, s(f.indent*3)+cyan(t.Delimitter)+ct)
   198  				for _, ln := range strings.Split(t.Content, "\n") {
   199  					fmt.Fprintln(f.out, s(f.indent*3)+cyan(ln))
   200  				}
   201  				fmt.Fprintln(f.out, s(f.indent*3)+cyan(t.Delimitter))
   202  			}
   203  		}
   204  	}
   205  
   206  	if clr == nil {
   207  		clr = green
   208  	}
   209  	cells := make([]string, len(example.TableHeader.Cells))
   210  	max := longest(example, clr, cyan)
   211  	// an example table header
   212  	if firstExample {
   213  		fmt.Fprintln(f.out, "")
   214  		fmt.Fprintln(f.out, s(f.indent*2)+whiteb(example.Keyword+": ")+example.Name)
   215  
   216  		for i, cell := range example.TableHeader.Cells {
   217  			val := cyan(cell.Value)
   218  			ln := utf8.RuneCountInString(val)
   219  			cells[i] = val + s(max[i]-ln)
   220  		}
   221  		fmt.Fprintln(f.out, s(f.indent*3)+"| "+strings.Join(cells, " | ")+" |")
   222  	}
   223  
   224  	// an example table row
   225  	row := example.TableBody[len(example.TableBody)-f.outlineNumExamples]
   226  	for i, cell := range row.Cells {
   227  		val := clr(cell.Value)
   228  		ln := utf8.RuneCountInString(val)
   229  		cells[i] = val + s(max[i]-ln)
   230  	}
   231  	fmt.Fprintln(f.out, s(f.indent*3)+"| "+strings.Join(cells, " | ")+" |")
   232  
   233  	// if there is an error
   234  	if msg != "" {
   235  		fmt.Fprintln(f.out, s(f.indent*4)+redb(msg))
   236  	}
   237  }
   238  
   239  func (f *pretty) printStep(step *gherkin.Step, def *StepDef, c colors.ColorFunc) {
   240  	text := s(f.indent*2) + c(strings.TrimSpace(step.Keyword)) + " "
   241  	switch {
   242  	case def != nil:
   243  		if m := def.Expr.FindStringSubmatchIndex(step.Text)[2:]; len(m) > 0 {
   244  			var pos, i int
   245  			for pos, i = 0, 0; i < len(m); i++ {
   246  				if m[i] == -1 {
   247  					continue // no index for this match
   248  				}
   249  				if math.Mod(float64(i), 2) == 0 {
   250  					text += c(step.Text[pos:m[i]])
   251  				} else {
   252  					text += colors.Bold(c)(step.Text[pos:m[i]])
   253  				}
   254  				pos = m[i]
   255  			}
   256  			text += c(step.Text[pos:len(step.Text)])
   257  		} else {
   258  			text += c(step.Text)
   259  		}
   260  		text += s(f.commentPos-f.length(step)+1) + black(fmt.Sprintf("# %s", def.definitionID()))
   261  	default:
   262  		text += c(step.Text)
   263  	}
   264  
   265  	fmt.Fprintln(f.out, text)
   266  	switch t := step.Argument.(type) {
   267  	case *gherkin.DataTable:
   268  		f.printTable(t, c)
   269  	case *gherkin.DocString:
   270  		var ct string
   271  		if len(t.ContentType) > 0 {
   272  			ct = " " + c(t.ContentType)
   273  		}
   274  		fmt.Fprintln(f.out, s(f.indent*3)+c(t.Delimitter)+ct)
   275  		for _, ln := range strings.Split(t.Content, "\n") {
   276  			fmt.Fprintln(f.out, s(f.indent*3)+c(ln))
   277  		}
   278  		fmt.Fprintln(f.out, s(f.indent*3)+c(t.Delimitter))
   279  	}
   280  }
   281  
   282  func (f *pretty) printStepKind(res *stepResult) {
   283  	f.steps--
   284  	if f.outline != nil {
   285  		f.outlineSteps = append(f.outlineSteps, res)
   286  	}
   287  
   288  	// if has not printed background yet
   289  	switch {
   290  	// first background step
   291  	case f.bgSteps > 0 && f.bgSteps == len(f.feature.Background.Steps):
   292  		f.commentPos = f.longestStep(f.feature.Background.Steps, f.length(f.feature.Background))
   293  		fmt.Fprintln(f.out, "\n"+s(f.indent)+whiteb(f.feature.Background.Keyword+": "+f.feature.Background.Name))
   294  		f.bgSteps--
   295  	// subsequent background steps
   296  	case f.bgSteps > 0:
   297  		f.bgSteps--
   298  	// first step of scenario, print header and calculate comment position
   299  	case f.scenario != nil:
   300  		// print scenario keyword and value if first example
   301  		if !f.scenarioKeyword {
   302  			f.commentPos = f.longestStep(f.scenario.Steps, f.length(f.scenario))
   303  			if f.feature.Background != nil {
   304  				if bgLen := f.longestStep(f.feature.Background.Steps, f.length(f.feature.Background)); bgLen > f.commentPos {
   305  					f.commentPos = bgLen
   306  				}
   307  			}
   308  			text := s(f.indent) + whiteb(f.scenario.Keyword+": ") + f.scenario.Name
   309  			text += s(f.commentPos-f.length(f.scenario)+1) + f.line(f.scenario.Location)
   310  			fmt.Fprintln(f.out, "\n"+text)
   311  			f.scenarioKeyword = true
   312  		}
   313  	// first step of outline scenario, print header and calculate comment position
   314  	case f.outline != nil:
   315  		// print scenario keyword and value if first example
   316  		if !f.scenarioKeyword {
   317  			f.commentPos = f.longestStep(f.outline.Steps, f.length(f.outline))
   318  			if f.feature.Background != nil {
   319  				if bgLen := f.longestStep(f.feature.Background.Steps, f.length(f.feature.Background)); bgLen > f.commentPos {
   320  					f.commentPos = bgLen
   321  				}
   322  			}
   323  			text := s(f.indent) + whiteb(f.outline.Keyword+": ") + f.outline.Name
   324  			text += s(f.commentPos-f.length(f.outline)+1) + f.line(f.outline.Location)
   325  			fmt.Fprintln(f.out, "\n"+text)
   326  			f.scenarioKeyword = true
   327  		}
   328  		if len(f.outlineSteps) == len(f.outline.Steps)+f.totalBgSteps {
   329  			// an outline example steps has went through
   330  			f.printOutlineExample(f.outline)
   331  			f.outlineNumExamples--
   332  		}
   333  		return
   334  	}
   335  
   336  	f.printStep(res.step, res.def, res.typ.clr())
   337  	if res.err != nil {
   338  		fmt.Fprintln(f.out, s(f.indent*2)+redb(fmt.Sprintf("%+v", res.err)))
   339  	}
   340  	if res.typ == pending {
   341  		fmt.Fprintln(f.out, s(f.indent*3)+yellow("TODO: write pending definition"))
   342  	}
   343  }
   344  
   345  // print table with aligned table cells
   346  func (f *pretty) printTable(t *gherkin.DataTable, c colors.ColorFunc) {
   347  	var l = longest(t, c)
   348  	var cols = make([]string, len(t.Rows[0].Cells))
   349  	for _, row := range t.Rows {
   350  		for i, cell := range row.Cells {
   351  			val := c(cell.Value)
   352  			ln := utf8.RuneCountInString(val)
   353  			cols[i] = val + s(l[i]-ln)
   354  		}
   355  		fmt.Fprintln(f.out, s(f.indent*3)+"| "+strings.Join(cols, " | ")+" |")
   356  	}
   357  }
   358  
   359  func (f *pretty) Passed(step *gherkin.Step, match *StepDef) {
   360  	f.basefmt.Passed(step, match)
   361  	f.printStepKind(f.passed[len(f.passed)-1])
   362  }
   363  
   364  func (f *pretty) Skipped(step *gherkin.Step, match *StepDef) {
   365  	f.basefmt.Skipped(step, match)
   366  	f.printStepKind(f.skipped[len(f.skipped)-1])
   367  }
   368  
   369  func (f *pretty) Undefined(step *gherkin.Step, match *StepDef) {
   370  	f.basefmt.Undefined(step, match)
   371  	f.printStepKind(f.undefined[len(f.undefined)-1])
   372  }
   373  
   374  func (f *pretty) Failed(step *gherkin.Step, match *StepDef, err error) {
   375  	f.basefmt.Failed(step, match, err)
   376  	f.printStepKind(f.failed[len(f.failed)-1])
   377  }
   378  
   379  func (f *pretty) Pending(step *gherkin.Step, match *StepDef) {
   380  	f.basefmt.Pending(step, match)
   381  	f.printStepKind(f.pending[len(f.pending)-1])
   382  }
   383  
   384  // longest gives a list of longest columns of all rows in Table
   385  func longest(tbl interface{}, clrs ...colors.ColorFunc) []int {
   386  	var rows []*gherkin.TableRow
   387  	switch t := tbl.(type) {
   388  	case *gherkin.Examples:
   389  		rows = append(rows, t.TableHeader)
   390  		rows = append(rows, t.TableBody...)
   391  	case *gherkin.DataTable:
   392  		rows = append(rows, t.Rows...)
   393  	}
   394  
   395  	longest := make([]int, len(rows[0].Cells))
   396  	for _, row := range rows {
   397  		for i, cell := range row.Cells {
   398  			for _, c := range clrs {
   399  				ln := utf8.RuneCountInString(c(cell.Value))
   400  				if longest[i] < ln {
   401  					longest[i] = ln
   402  				}
   403  			}
   404  
   405  			ln := utf8.RuneCountInString(cell.Value)
   406  			if longest[i] < ln {
   407  				longest[i] = ln
   408  			}
   409  		}
   410  	}
   411  	return longest
   412  }
   413  
   414  func (f *pretty) longestStep(steps []*gherkin.Step, base int) int {
   415  	ret := base
   416  	for _, step := range steps {
   417  		length := f.length(step)
   418  		if length > ret {
   419  			ret = length
   420  		}
   421  	}
   422  	return ret
   423  }
   424  
   425  // a line number representation in feature file
   426  func (f *pretty) line(loc *gherkin.Location) string {
   427  	return black(fmt.Sprintf("# %s:%d", f.features[len(f.features)-1].Path, loc.Line))
   428  }
   429  
   430  func (f *pretty) length(node interface{}) int {
   431  	switch t := node.(type) {
   432  	case *gherkin.Background:
   433  		return f.indent + utf8.RuneCountInString(strings.TrimSpace(t.Keyword)+": "+t.Name)
   434  	case *gherkin.Step:
   435  		return f.indent*2 + utf8.RuneCountInString(strings.TrimSpace(t.Keyword)+" "+t.Text)
   436  	case *gherkin.Scenario:
   437  		return f.indent + utf8.RuneCountInString(strings.TrimSpace(t.Keyword)+": "+t.Name)
   438  	case *gherkin.ScenarioOutline:
   439  		return f.indent + utf8.RuneCountInString(strings.TrimSpace(t.Keyword)+": "+t.Name)
   440  	}
   441  	panic(fmt.Sprintf("unexpected node %T to determine length", node))
   442  }