github.com/lonnblad/godog@v0.7.14-0.20200306004719-1b0cb3259847/fmt_pretty.go (about)

     1  package godog
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"regexp"
     7  	"strings"
     8  	"unicode/utf8"
     9  
    10  	"github.com/cucumber/messages-go/v9"
    11  
    12  	"github.com/lonnblad/godog/colors"
    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{basefmt: newBaseFmt(suite, out)}
    21  }
    22  
    23  var outlinePlaceholderRegexp = regexp.MustCompile("<[^>]+>")
    24  
    25  // a built in default pretty formatter
    26  type pretty struct {
    27  	*basefmt
    28  }
    29  
    30  func (f *pretty) Feature(gd *messages.GherkinDocument, p string, c []byte) {
    31  	f.basefmt.Feature(gd, p, c)
    32  	f.printFeature(gd.Feature)
    33  }
    34  
    35  // Pickle takes a gherkin node for formatting
    36  func (f *pretty) Pickle(pickle *messages.Pickle) {
    37  	f.basefmt.Pickle(pickle)
    38  
    39  	if len(pickle.Steps) == 0 {
    40  		f.printUndefinedPickle(pickle)
    41  		return
    42  	}
    43  }
    44  
    45  func (f *pretty) Passed(pickle *messages.Pickle, step *messages.Pickle_PickleStep, match *StepDefinition) {
    46  	f.basefmt.Passed(pickle, step, match)
    47  	f.printStep(f.lastStepResult())
    48  }
    49  
    50  func (f *pretty) Skipped(pickle *messages.Pickle, step *messages.Pickle_PickleStep, match *StepDefinition) {
    51  	f.basefmt.Skipped(pickle, step, match)
    52  	f.printStep(f.lastStepResult())
    53  }
    54  
    55  func (f *pretty) Undefined(pickle *messages.Pickle, step *messages.Pickle_PickleStep, match *StepDefinition) {
    56  	f.basefmt.Undefined(pickle, step, match)
    57  	f.printStep(f.lastStepResult())
    58  }
    59  
    60  func (f *pretty) Failed(pickle *messages.Pickle, step *messages.Pickle_PickleStep, match *StepDefinition, err error) {
    61  	f.basefmt.Failed(pickle, step, match, err)
    62  	f.printStep(f.lastStepResult())
    63  }
    64  
    65  func (f *pretty) Pending(pickle *messages.Pickle, step *messages.Pickle_PickleStep, match *StepDefinition) {
    66  	f.basefmt.Pending(pickle, step, match)
    67  	f.printStep(f.lastStepResult())
    68  }
    69  
    70  func (f *pretty) printFeature(feature *messages.GherkinDocument_Feature) {
    71  	if len(f.features) > 1 {
    72  		fmt.Fprintln(f.out, "") // not a first feature, add a newline
    73  	}
    74  
    75  	fmt.Fprintln(f.out, keywordAndName(feature.Keyword, feature.Name))
    76  	if strings.TrimSpace(feature.Description) != "" {
    77  		for _, line := range strings.Split(feature.Description, "\n") {
    78  			fmt.Fprintln(f.out, s(f.indent)+strings.TrimSpace(line))
    79  		}
    80  	}
    81  }
    82  
    83  func keywordAndName(keyword, name string) string {
    84  	title := whiteb(keyword + ":")
    85  	if len(name) > 0 {
    86  		title += " " + name
    87  	}
    88  	return title
    89  }
    90  
    91  func (f *pretty) scenarioLengths(scenarioAstID string) (scenarioHeaderLength int, maxLength int) {
    92  	astScenario := f.findScenario(scenarioAstID)
    93  	astBackground := f.findBackground(scenarioAstID)
    94  
    95  	scenarioHeaderLength = f.lengthPickle(astScenario.Keyword, astScenario.Name)
    96  	maxLength = f.longestStep(astScenario.Steps, scenarioHeaderLength)
    97  
    98  	if astBackground != nil {
    99  		maxLength = f.longestStep(astBackground.Steps, maxLength)
   100  	}
   101  
   102  	return scenarioHeaderLength, maxLength
   103  }
   104  
   105  func (f *pretty) printScenarioHeader(astScenario *messages.GherkinDocument_Feature_Scenario, spaceFilling int) {
   106  	text := s(f.indent) + keywordAndName(astScenario.Keyword, astScenario.Name)
   107  	text += s(spaceFilling) + f.line(astScenario.Location)
   108  	fmt.Fprintln(f.out, "\n"+text)
   109  }
   110  
   111  func (f *pretty) printUndefinedPickle(pickle *messages.Pickle) {
   112  	astScenario := f.findScenario(pickle.AstNodeIds[0])
   113  	astBackground := f.findBackground(pickle.AstNodeIds[0])
   114  
   115  	scenarioHeaderLength, maxLength := f.scenarioLengths(pickle.AstNodeIds[0])
   116  
   117  	if astBackground != nil {
   118  		fmt.Fprintln(f.out, "\n"+s(f.indent)+keywordAndName(astBackground.Keyword, astBackground.Name))
   119  		for _, step := range astBackground.Steps {
   120  			text := s(f.indent*2) + cyan(strings.TrimSpace(step.Keyword)) + " " + cyan(step.Text)
   121  			fmt.Fprintln(f.out, text)
   122  		}
   123  	}
   124  
   125  	//  do not print scenario headers and examples multiple times
   126  	if len(astScenario.Examples) > 0 {
   127  		exampleTable, exampleRow := f.findExample(pickle.AstNodeIds[1])
   128  		firstExampleRow := exampleTable.TableBody[0].Id == exampleRow.Id
   129  		firstExamplesTable := astScenario.Examples[0].Location.Line == exampleTable.Location.Line
   130  
   131  		if !(firstExamplesTable && firstExampleRow) {
   132  			return
   133  		}
   134  	}
   135  
   136  	f.printScenarioHeader(astScenario, maxLength-scenarioHeaderLength)
   137  
   138  	for _, examples := range astScenario.Examples {
   139  		max := longestExampleRow(examples, cyan, cyan)
   140  
   141  		fmt.Fprintln(f.out, "")
   142  		fmt.Fprintln(f.out, s(f.indent*2)+keywordAndName(examples.Keyword, examples.Name))
   143  
   144  		f.printTableHeader(examples.TableHeader, max)
   145  
   146  		for _, row := range examples.TableBody {
   147  			f.printTableRow(row, max, cyan)
   148  		}
   149  	}
   150  }
   151  
   152  // Summary sumarize the feature formatter output
   153  func (f *pretty) Summary() {
   154  	failedStepResults := f.findStepResults(failed)
   155  	if len(failedStepResults) > 0 {
   156  		fmt.Fprintln(f.out, "\n--- "+red("Failed steps:")+"\n")
   157  		for _, fail := range failedStepResults {
   158  			astScenario := f.findScenario(fail.owner.AstNodeIds[0])
   159  			scenarioDesc := fmt.Sprintf("%s: %s", astScenario.Keyword, fail.owner.Name)
   160  
   161  			astStep := f.findStep(fail.step.AstNodeIds[0])
   162  			stepDesc := strings.TrimSpace(astStep.Keyword) + " " + fail.step.Text
   163  
   164  			fmt.Fprintln(f.out, s(f.indent)+red(scenarioDesc)+f.line(astScenario.Location))
   165  			fmt.Fprintln(f.out, s(f.indent*2)+red(stepDesc)+f.line(astStep.Location))
   166  			fmt.Fprintln(f.out, s(f.indent*3)+red("Error: ")+redb(fmt.Sprintf("%+v", fail.err))+"\n")
   167  		}
   168  	}
   169  
   170  	f.basefmt.Summary()
   171  }
   172  
   173  func (f *pretty) printOutlineExample(pickle *messages.Pickle, backgroundSteps int) {
   174  	var errorMsg string
   175  	var clr = green
   176  
   177  	astScenario := f.findScenario(pickle.AstNodeIds[0])
   178  	scenarioHeaderLength, maxLength := f.scenarioLengths(pickle.AstNodeIds[0])
   179  
   180  	exampleTable, exampleRow := f.findExample(pickle.AstNodeIds[1])
   181  	printExampleHeader := exampleTable.TableBody[0].Id == exampleRow.Id
   182  	firstExamplesTable := astScenario.Examples[0].Location.Line == exampleTable.Location.Line
   183  
   184  	firstExecutedScenarioStep := len(f.lastFeature().lastPickleResult().stepResults) == backgroundSteps+1
   185  	if firstExamplesTable && printExampleHeader && firstExecutedScenarioStep {
   186  		f.printScenarioHeader(astScenario, maxLength-scenarioHeaderLength)
   187  	}
   188  
   189  	if len(exampleTable.TableBody) == 0 {
   190  		// do not print empty examples
   191  		return
   192  	}
   193  
   194  	lastStep := len(f.lastFeature().lastPickleResult().stepResults) == len(pickle.Steps)
   195  	if !lastStep {
   196  		// do not print examples unless all steps has finished
   197  		return
   198  	}
   199  
   200  	for _, result := range f.lastFeature().lastPickleResult().stepResults {
   201  		// determine example row status
   202  		switch {
   203  		case result.status == failed:
   204  			errorMsg = result.err.Error()
   205  			clr = result.status.clr()
   206  		case result.status == undefined || result.status == pending:
   207  			clr = result.status.clr()
   208  		case result.status == skipped && clr == nil:
   209  			clr = cyan
   210  		}
   211  
   212  		if firstExamplesTable && printExampleHeader {
   213  			// in first example, we need to print steps
   214  			var text string
   215  
   216  			astStep := f.findStep(result.step.AstNodeIds[0])
   217  
   218  			if result.def != nil {
   219  				if m := outlinePlaceholderRegexp.FindAllStringIndex(astStep.Text, -1); len(m) > 0 {
   220  					var pos int
   221  					for i := 0; i < len(m); i++ {
   222  						pair := m[i]
   223  						text += cyan(astStep.Text[pos:pair[0]])
   224  						text += cyanb(astStep.Text[pair[0]:pair[1]])
   225  						pos = pair[1]
   226  					}
   227  					text += cyan(astStep.Text[pos:len(astStep.Text)])
   228  				} else {
   229  					text = cyan(astStep.Text)
   230  				}
   231  
   232  				_, maxLength := f.scenarioLengths(result.owner.AstNodeIds[0])
   233  				stepLength := f.lengthPickleStep(astStep.Keyword, astStep.Text)
   234  
   235  				text += s(maxLength - stepLength)
   236  				text += " " + blackb("# "+result.def.definitionID())
   237  			} else {
   238  				text = cyan(astStep.Text)
   239  			}
   240  			// print the step outline
   241  			fmt.Fprintln(f.out, s(f.indent*2)+cyan(strings.TrimSpace(astStep.Keyword))+" "+text)
   242  
   243  			if table := result.step.Argument.GetDataTable(); table != nil {
   244  				f.printTable(table, cyan)
   245  			}
   246  
   247  			if docString := astStep.GetDocString(); docString != nil {
   248  				f.printDocString(docString)
   249  			}
   250  		}
   251  	}
   252  
   253  	max := longestExampleRow(exampleTable, clr, cyan)
   254  
   255  	// an example table header
   256  	if printExampleHeader {
   257  		fmt.Fprintln(f.out, "")
   258  		fmt.Fprintln(f.out, s(f.indent*2)+keywordAndName(exampleTable.Keyword, exampleTable.Name))
   259  
   260  		f.printTableHeader(exampleTable.TableHeader, max)
   261  	}
   262  
   263  	f.printTableRow(exampleRow, max, clr)
   264  
   265  	if errorMsg != "" {
   266  		fmt.Fprintln(f.out, s(f.indent*4)+redb(errorMsg))
   267  	}
   268  }
   269  
   270  func (f *pretty) printTableRow(row *messages.GherkinDocument_Feature_TableRow, max []int, clr colors.ColorFunc) {
   271  	cells := make([]string, len(row.Cells))
   272  
   273  	for i, cell := range row.Cells {
   274  		val := clr(cell.Value)
   275  		ln := utf8.RuneCountInString(val)
   276  		cells[i] = val + s(max[i]-ln)
   277  	}
   278  
   279  	fmt.Fprintln(f.out, s(f.indent*3)+"| "+strings.Join(cells, " | ")+" |")
   280  }
   281  
   282  func (f *pretty) printTableHeader(row *messages.GherkinDocument_Feature_TableRow, max []int) {
   283  	f.printTableRow(row, max, cyan)
   284  }
   285  
   286  func (f *pretty) printStep(result *stepResult) {
   287  	astBackground := f.findBackground(result.owner.AstNodeIds[0])
   288  	astScenario := f.findScenario(result.owner.AstNodeIds[0])
   289  	astStep := f.findStep(result.step.AstNodeIds[0])
   290  
   291  	var backgroundSteps int
   292  	if astBackground != nil {
   293  		backgroundSteps = len(astBackground.Steps)
   294  	}
   295  
   296  	astBackgroundStep := backgroundSteps > 0 && backgroundSteps >= len(f.lastFeature().lastPickleResult().stepResults)
   297  
   298  	if astBackgroundStep {
   299  		if len(f.lastFeature().pickleResults) > 1 {
   300  			return
   301  		}
   302  
   303  		firstExecutedBackgroundStep := astBackground != nil && len(f.lastFeature().lastPickleResult().stepResults) == 1
   304  		if firstExecutedBackgroundStep {
   305  			fmt.Fprintln(f.out, "\n"+s(f.indent)+keywordAndName(astBackground.Keyword, astBackground.Name))
   306  		}
   307  	}
   308  
   309  	if !astBackgroundStep && len(astScenario.Examples) > 0 {
   310  		f.printOutlineExample(result.owner, backgroundSteps)
   311  		return
   312  	}
   313  
   314  	scenarioHeaderLength, maxLength := f.scenarioLengths(result.owner.AstNodeIds[0])
   315  	stepLength := f.lengthPickleStep(astStep.Keyword, astStep.Text)
   316  
   317  	firstExecutedScenarioStep := len(f.lastFeature().lastPickleResult().stepResults) == backgroundSteps+1
   318  	if !astBackgroundStep && firstExecutedScenarioStep {
   319  		f.printScenarioHeader(astScenario, maxLength-scenarioHeaderLength)
   320  	}
   321  
   322  	text := s(f.indent*2) + result.status.clr()(strings.TrimSpace(astStep.Keyword)) + " " + result.status.clr()(astStep.Text)
   323  	if result.def != nil {
   324  		text += s(maxLength - stepLength + 1)
   325  		text += blackb("# " + result.def.definitionID())
   326  	}
   327  	fmt.Fprintln(f.out, text)
   328  
   329  	if table := result.step.Argument.GetDataTable(); table != nil {
   330  		f.printTable(table, cyan)
   331  	}
   332  
   333  	if docString := astStep.GetDocString(); docString != nil {
   334  		f.printDocString(docString)
   335  	}
   336  
   337  	if result.err != nil {
   338  		fmt.Fprintln(f.out, s(f.indent*2)+redb(fmt.Sprintf("%+v", result.err)))
   339  	}
   340  
   341  	if result.status == pending {
   342  		fmt.Fprintln(f.out, s(f.indent*3)+yellow("TODO: write pending definition"))
   343  	}
   344  }
   345  
   346  func (f *pretty) printDocString(docString *messages.GherkinDocument_Feature_Step_DocString) {
   347  	var ct string
   348  
   349  	if len(docString.MediaType) > 0 {
   350  		ct = " " + cyan(docString.MediaType)
   351  	}
   352  
   353  	fmt.Fprintln(f.out, s(f.indent*3)+cyan(docString.Delimiter)+ct)
   354  
   355  	for _, ln := range strings.Split(docString.Content, "\n") {
   356  		fmt.Fprintln(f.out, s(f.indent*3)+cyan(ln))
   357  	}
   358  
   359  	fmt.Fprintln(f.out, s(f.indent*3)+cyan(docString.Delimiter))
   360  }
   361  
   362  // print table with aligned table cells
   363  // @TODO: need to make example header cells bold
   364  func (f *pretty) printTable(t *messages.PickleStepArgument_PickleTable, c colors.ColorFunc) {
   365  	maxColLengths := maxColLengths(t, c)
   366  	var cols = make([]string, len(t.Rows[0].Cells))
   367  
   368  	for _, row := range t.Rows {
   369  		for i, cell := range row.Cells {
   370  			val := c(cell.Value)
   371  			colLength := utf8.RuneCountInString(val)
   372  			cols[i] = val + s(maxColLengths[i]-colLength)
   373  		}
   374  
   375  		fmt.Fprintln(f.out, s(f.indent*3)+"| "+strings.Join(cols, " | ")+" |")
   376  	}
   377  }
   378  
   379  // longest gives a list of longest columns of all rows in Table
   380  func maxColLengths(t *messages.PickleStepArgument_PickleTable, clrs ...colors.ColorFunc) []int {
   381  	if t == nil {
   382  		return []int{}
   383  	}
   384  
   385  	longest := make([]int, len(t.Rows[0].Cells))
   386  	for _, row := range t.Rows {
   387  		for i, cell := range row.Cells {
   388  			for _, c := range clrs {
   389  				ln := utf8.RuneCountInString(c(cell.Value))
   390  				if longest[i] < ln {
   391  					longest[i] = ln
   392  				}
   393  			}
   394  
   395  			ln := utf8.RuneCountInString(cell.Value)
   396  			if longest[i] < ln {
   397  				longest[i] = ln
   398  			}
   399  		}
   400  	}
   401  
   402  	return longest
   403  }
   404  
   405  func longestExampleRow(t *messages.GherkinDocument_Feature_Scenario_Examples, clrs ...colors.ColorFunc) []int {
   406  	if t == nil {
   407  		return []int{}
   408  	}
   409  
   410  	longest := make([]int, len(t.TableHeader.Cells))
   411  	for i, cell := range t.TableHeader.Cells {
   412  		for _, c := range clrs {
   413  			ln := utf8.RuneCountInString(c(cell.Value))
   414  			if longest[i] < ln {
   415  				longest[i] = ln
   416  			}
   417  		}
   418  
   419  		ln := utf8.RuneCountInString(cell.Value)
   420  		if longest[i] < ln {
   421  			longest[i] = ln
   422  		}
   423  	}
   424  
   425  	for _, row := range t.TableBody {
   426  		for i, cell := range row.Cells {
   427  			for _, c := range clrs {
   428  				ln := utf8.RuneCountInString(c(cell.Value))
   429  				if longest[i] < ln {
   430  					longest[i] = ln
   431  				}
   432  			}
   433  
   434  			ln := utf8.RuneCountInString(cell.Value)
   435  			if longest[i] < ln {
   436  				longest[i] = ln
   437  			}
   438  		}
   439  	}
   440  
   441  	return longest
   442  }
   443  
   444  func (f *pretty) longestStep(steps []*messages.GherkinDocument_Feature_Step, pickleLength int) int {
   445  	max := pickleLength
   446  
   447  	for _, step := range steps {
   448  		length := f.lengthPickleStep(step.Keyword, step.Text)
   449  		if length > max {
   450  			max = length
   451  		}
   452  	}
   453  
   454  	return max
   455  }
   456  
   457  // a line number representation in feature file
   458  func (f *pretty) line(loc *messages.Location) string {
   459  	return " " + blackb(fmt.Sprintf("# %s:%d", f.lastFeature().Path, loc.Line))
   460  }
   461  
   462  func (f *pretty) lengthPickleStep(keyword, text string) int {
   463  	return f.indent*2 + utf8.RuneCountInString(strings.TrimSpace(keyword)+" "+text)
   464  }
   465  
   466  func (f *pretty) lengthPickle(keyword, name string) int {
   467  	return f.indent + utf8.RuneCountInString(strings.TrimSpace(keyword)+": "+name)
   468  }