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

     1  package godog
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"io"
     7  	"os"
     8  	"reflect"
     9  	"regexp"
    10  	"strconv"
    11  	"strings"
    12  	"sync"
    13  	"text/template"
    14  	"time"
    15  	"unicode"
    16  
    17  	"github.com/lonnblad/godog/colors"
    18  
    19  	"github.com/cucumber/messages-go/v9"
    20  )
    21  
    22  // some snippet formatting regexps
    23  var snippetExprCleanup = regexp.MustCompile("([\\/\\[\\]\\(\\)\\\\^\\$\\.\\|\\?\\*\\+\\'])")
    24  var snippetExprQuoted = regexp.MustCompile("(\\W|^)\"(?:[^\"]*)\"(\\W|$)")
    25  var snippetMethodName = regexp.MustCompile("[^a-zA-Z\\_\\ ]")
    26  var snippetNumbers = regexp.MustCompile("(\\d+)")
    27  
    28  var snippetHelperFuncs = template.FuncMap{
    29  	"backticked": func(s string) string {
    30  		return "`" + s + "`"
    31  	},
    32  }
    33  
    34  var undefinedSnippetsTpl = template.Must(template.New("snippets").Funcs(snippetHelperFuncs).Parse(`
    35  {{ range . }}func {{ .Method }}({{ .Args }}) error {
    36  	return godog.ErrPending
    37  }
    38  
    39  {{end}}func FeatureContext(s *godog.Suite) { {{ range . }}
    40  	s.Step({{ backticked .Expr }}, {{ .Method }}){{end}}
    41  }
    42  `))
    43  
    44  type undefinedSnippet struct {
    45  	Method   string
    46  	Expr     string
    47  	argument *messages.PickleStepArgument
    48  }
    49  
    50  type registeredFormatter struct {
    51  	name        string
    52  	fmt         FormatterFunc
    53  	description string
    54  }
    55  
    56  var formatters []*registeredFormatter
    57  
    58  // FindFmt searches available formatters registered
    59  // and returns FormaterFunc matched by given
    60  // format name or nil otherwise
    61  func FindFmt(name string) FormatterFunc {
    62  	for _, el := range formatters {
    63  		if el.name == name {
    64  			return el.fmt
    65  		}
    66  	}
    67  	return nil
    68  }
    69  
    70  // Format registers a feature suite output
    71  // formatter by given name, description and
    72  // FormatterFunc constructor function, to initialize
    73  // formatter with the output recorder.
    74  func Format(name, description string, f FormatterFunc) {
    75  	formatters = append(formatters, &registeredFormatter{
    76  		name:        name,
    77  		fmt:         f,
    78  		description: description,
    79  	})
    80  }
    81  
    82  // AvailableFormatters gives a map of all
    83  // formatters registered with their name as key
    84  // and description as value
    85  func AvailableFormatters() map[string]string {
    86  	fmts := make(map[string]string, len(formatters))
    87  	for _, f := range formatters {
    88  		fmts[f.name] = f.description
    89  	}
    90  	return fmts
    91  }
    92  
    93  // Formatter is an interface for feature runner
    94  // output summary presentation.
    95  //
    96  // New formatters may be created to represent
    97  // suite results in different ways. These new
    98  // formatters needs to be registered with a
    99  // godog.Format function call
   100  type Formatter interface {
   101  	Feature(*messages.GherkinDocument, string, []byte)
   102  	Pickle(*messages.Pickle)
   103  	Defined(*messages.Pickle, *messages.Pickle_PickleStep, *StepDefinition)
   104  	Failed(*messages.Pickle, *messages.Pickle_PickleStep, *StepDefinition, error)
   105  	Passed(*messages.Pickle, *messages.Pickle_PickleStep, *StepDefinition)
   106  	Skipped(*messages.Pickle, *messages.Pickle_PickleStep, *StepDefinition)
   107  	Undefined(*messages.Pickle, *messages.Pickle_PickleStep, *StepDefinition)
   108  	Pending(*messages.Pickle, *messages.Pickle_PickleStep, *StepDefinition)
   109  	Summary()
   110  }
   111  
   112  // ConcurrentFormatter is an interface for a Concurrent
   113  // version of the Formatter interface.
   114  type ConcurrentFormatter interface {
   115  	Formatter
   116  	Copy(ConcurrentFormatter)
   117  	Sync(ConcurrentFormatter)
   118  }
   119  
   120  // FormatterFunc builds a formatter with given
   121  // suite name and io.Writer to record output
   122  type FormatterFunc func(string, io.Writer) Formatter
   123  
   124  type stepResultStatus int
   125  
   126  const (
   127  	passed stepResultStatus = iota
   128  	failed
   129  	skipped
   130  	undefined
   131  	pending
   132  )
   133  
   134  func (st stepResultStatus) clr() colors.ColorFunc {
   135  	switch st {
   136  	case passed:
   137  		return green
   138  	case failed:
   139  		return red
   140  	case skipped:
   141  		return cyan
   142  	default:
   143  		return yellow
   144  	}
   145  }
   146  
   147  func (st stepResultStatus) String() string {
   148  	switch st {
   149  	case passed:
   150  		return "passed"
   151  	case failed:
   152  		return "failed"
   153  	case skipped:
   154  		return "skipped"
   155  	case undefined:
   156  		return "undefined"
   157  	case pending:
   158  		return "pending"
   159  	default:
   160  		return "unknown"
   161  	}
   162  }
   163  
   164  type stepResult struct {
   165  	status stepResultStatus
   166  	time   time.Time
   167  	err    error
   168  
   169  	owner *messages.Pickle
   170  	step  *messages.Pickle_PickleStep
   171  	def   *StepDefinition
   172  }
   173  
   174  func newStepResult(pickle *messages.Pickle, step *messages.Pickle_PickleStep, match *StepDefinition) *stepResult {
   175  	return &stepResult{time: timeNowFunc(), owner: pickle, step: step, def: match}
   176  }
   177  
   178  func newBaseFmt(suite string, out io.Writer) *basefmt {
   179  	return &basefmt{
   180  		suiteName: suite,
   181  		started:   timeNowFunc(),
   182  		indent:    2,
   183  		out:       out,
   184  		lock:      new(sync.Mutex),
   185  	}
   186  }
   187  
   188  type basefmt struct {
   189  	suiteName string
   190  
   191  	out    io.Writer
   192  	owner  interface{}
   193  	indent int
   194  
   195  	started  time.Time
   196  	features []*feature
   197  
   198  	lock *sync.Mutex
   199  }
   200  
   201  func (f *basefmt) lastFeature() *feature {
   202  	return f.features[len(f.features)-1]
   203  }
   204  
   205  func (f *basefmt) lastStepResult() *stepResult {
   206  	return f.lastFeature().lastStepResult()
   207  }
   208  
   209  func (f *basefmt) findScenario(scenarioAstID string) *messages.GherkinDocument_Feature_Scenario {
   210  	for _, ft := range f.features {
   211  		if sc := ft.findScenario(scenarioAstID); sc != nil {
   212  			return sc
   213  		}
   214  	}
   215  
   216  	panic("Couldn't find scenario for AST ID: " + scenarioAstID)
   217  }
   218  
   219  func (f *basefmt) findBackground(scenarioAstID string) *messages.GherkinDocument_Feature_Background {
   220  	for _, ft := range f.features {
   221  		if bg := ft.findBackground(scenarioAstID); bg != nil {
   222  			return bg
   223  		}
   224  	}
   225  
   226  	return nil
   227  }
   228  
   229  func (f *basefmt) findExample(exampleAstID string) (*messages.GherkinDocument_Feature_Scenario_Examples, *messages.GherkinDocument_Feature_TableRow) {
   230  	for _, ft := range f.features {
   231  		if es, rs := ft.findExample(exampleAstID); es != nil && rs != nil {
   232  			return es, rs
   233  		}
   234  	}
   235  
   236  	return nil, nil
   237  }
   238  
   239  func (f *basefmt) findStep(stepAstID string) *messages.GherkinDocument_Feature_Step {
   240  	for _, ft := range f.features {
   241  		if st := ft.findStep(stepAstID); st != nil {
   242  			return st
   243  		}
   244  	}
   245  
   246  	panic("Couldn't find step for AST ID: " + stepAstID)
   247  }
   248  
   249  func (f *basefmt) Pickle(p *messages.Pickle) {
   250  	f.lock.Lock()
   251  	defer f.lock.Unlock()
   252  
   253  	feature := f.features[len(f.features)-1]
   254  	feature.pickleResults = append(feature.pickleResults, &pickleResult{Name: p.Name, time: timeNowFunc()})
   255  }
   256  
   257  func (f *basefmt) Defined(*messages.Pickle, *messages.Pickle_PickleStep, *StepDefinition) {}
   258  
   259  func (f *basefmt) Feature(ft *messages.GherkinDocument, p string, c []byte) {
   260  	f.lock.Lock()
   261  	defer f.lock.Unlock()
   262  
   263  	f.features = append(f.features, &feature{Path: p, GherkinDocument: ft, time: timeNowFunc()})
   264  }
   265  
   266  func (f *basefmt) Passed(pickle *messages.Pickle, step *messages.Pickle_PickleStep, match *StepDefinition) {
   267  	f.lock.Lock()
   268  	defer f.lock.Unlock()
   269  
   270  	s := newStepResult(pickle, step, match)
   271  	s.status = passed
   272  	f.lastFeature().appendStepResult(s)
   273  }
   274  
   275  func (f *basefmt) Skipped(pickle *messages.Pickle, step *messages.Pickle_PickleStep, match *StepDefinition) {
   276  	f.lock.Lock()
   277  	defer f.lock.Unlock()
   278  
   279  	s := newStepResult(pickle, step, match)
   280  	s.status = skipped
   281  	f.lastFeature().appendStepResult(s)
   282  }
   283  
   284  func (f *basefmt) Undefined(pickle *messages.Pickle, step *messages.Pickle_PickleStep, match *StepDefinition) {
   285  	f.lock.Lock()
   286  	defer f.lock.Unlock()
   287  
   288  	s := newStepResult(pickle, step, match)
   289  	s.status = undefined
   290  	f.lastFeature().appendStepResult(s)
   291  }
   292  
   293  func (f *basefmt) Failed(pickle *messages.Pickle, step *messages.Pickle_PickleStep, match *StepDefinition, err error) {
   294  	f.lock.Lock()
   295  	defer f.lock.Unlock()
   296  
   297  	s := newStepResult(pickle, step, match)
   298  	s.status = failed
   299  	s.err = err
   300  	f.lastFeature().appendStepResult(s)
   301  }
   302  
   303  func (f *basefmt) Pending(pickle *messages.Pickle, step *messages.Pickle_PickleStep, match *StepDefinition) {
   304  	f.lock.Lock()
   305  	defer f.lock.Unlock()
   306  
   307  	s := newStepResult(pickle, step, match)
   308  	s.status = pending
   309  	f.lastFeature().appendStepResult(s)
   310  }
   311  
   312  func (f *basefmt) Summary() {
   313  	var totalSc, passedSc, undefinedSc int
   314  	var totalSt, passedSt, failedSt, skippedSt, pendingSt, undefinedSt int
   315  
   316  	for _, feat := range f.features {
   317  		for _, pr := range feat.pickleResults {
   318  			var prStatus stepResultStatus
   319  			totalSc++
   320  
   321  			if len(pr.stepResults) == 0 {
   322  				prStatus = undefined
   323  			}
   324  
   325  			for _, sr := range pr.stepResults {
   326  				totalSt++
   327  
   328  				switch sr.status {
   329  				case passed:
   330  					prStatus = passed
   331  					passedSt++
   332  				case failed:
   333  					prStatus = failed
   334  					failedSt++
   335  				case skipped:
   336  					skippedSt++
   337  				case undefined:
   338  					prStatus = undefined
   339  					undefinedSt++
   340  				case pending:
   341  					prStatus = pending
   342  					pendingSt++
   343  				}
   344  			}
   345  
   346  			if prStatus == passed {
   347  				passedSc++
   348  			} else if prStatus == undefined {
   349  				undefinedSc++
   350  			}
   351  		}
   352  	}
   353  
   354  	var steps, parts, scenarios []string
   355  	if passedSt > 0 {
   356  		steps = append(steps, green(fmt.Sprintf("%d passed", passedSt)))
   357  	}
   358  	if failedSt > 0 {
   359  		parts = append(parts, red(fmt.Sprintf("%d failed", failedSt)))
   360  		steps = append(steps, red(fmt.Sprintf("%d failed", failedSt)))
   361  	}
   362  	if pendingSt > 0 {
   363  		parts = append(parts, yellow(fmt.Sprintf("%d pending", pendingSt)))
   364  		steps = append(steps, yellow(fmt.Sprintf("%d pending", pendingSt)))
   365  	}
   366  	if undefinedSt > 0 {
   367  		parts = append(parts, yellow(fmt.Sprintf("%d undefined", undefinedSc)))
   368  		steps = append(steps, yellow(fmt.Sprintf("%d undefined", undefinedSt)))
   369  	} else if undefinedSc > 0 {
   370  		// there may be some scenarios without steps
   371  		parts = append(parts, yellow(fmt.Sprintf("%d undefined", undefinedSc)))
   372  	}
   373  	if skippedSt > 0 {
   374  		steps = append(steps, cyan(fmt.Sprintf("%d skipped", skippedSt)))
   375  	}
   376  	if passedSc > 0 {
   377  		scenarios = append(scenarios, green(fmt.Sprintf("%d passed", passedSc)))
   378  	}
   379  	scenarios = append(scenarios, parts...)
   380  	elapsed := timeNowFunc().Sub(f.started)
   381  
   382  	fmt.Fprintln(f.out, "")
   383  
   384  	if totalSc == 0 {
   385  		fmt.Fprintln(f.out, "No scenarios")
   386  	} else {
   387  		fmt.Fprintln(f.out, fmt.Sprintf("%d scenarios (%s)", totalSc, strings.Join(scenarios, ", ")))
   388  	}
   389  
   390  	if totalSt == 0 {
   391  		fmt.Fprintln(f.out, "No steps")
   392  	} else {
   393  		fmt.Fprintln(f.out, fmt.Sprintf("%d steps (%s)", totalSt, strings.Join(steps, ", ")))
   394  	}
   395  
   396  	elapsedString := elapsed.String()
   397  	if elapsed.Nanoseconds() == 0 {
   398  		// go 1.5 and 1.6 prints 0 instead of 0s, if duration is zero.
   399  		elapsedString = "0s"
   400  	}
   401  	fmt.Fprintln(f.out, elapsedString)
   402  
   403  	// prints used randomization seed
   404  	seed, err := strconv.ParseInt(os.Getenv("GODOG_SEED"), 10, 64)
   405  	if err == nil && seed != 0 {
   406  		fmt.Fprintln(f.out, "")
   407  		fmt.Fprintln(f.out, "Randomized with seed:", colors.Yellow(seed))
   408  	}
   409  
   410  	if text := f.snippets(); text != "" {
   411  		fmt.Fprintln(f.out, "")
   412  		fmt.Fprintln(f.out, yellow("You can implement step definitions for undefined steps with these snippets:"))
   413  		fmt.Fprintln(f.out, yellow(text))
   414  	}
   415  }
   416  
   417  func (f *basefmt) Sync(cf ConcurrentFormatter) {
   418  	if source, ok := cf.(*basefmt); ok {
   419  		f.lock = source.lock
   420  	}
   421  }
   422  
   423  func (f *basefmt) Copy(cf ConcurrentFormatter) {
   424  	if source, ok := cf.(*basefmt); ok {
   425  		for _, v := range source.features {
   426  			f.features = append(f.features, v)
   427  		}
   428  	}
   429  }
   430  
   431  func (s *undefinedSnippet) Args() (ret string) {
   432  	var (
   433  		args      []string
   434  		pos       int
   435  		breakLoop bool
   436  	)
   437  	for !breakLoop {
   438  		part := s.Expr[pos:]
   439  		ipos := strings.Index(part, "(\\d+)")
   440  		spos := strings.Index(part, "\"([^\"]*)\"")
   441  		switch {
   442  		case spos == -1 && ipos == -1:
   443  			breakLoop = true
   444  		case spos == -1:
   445  			pos += ipos + len("(\\d+)")
   446  			args = append(args, reflect.Int.String())
   447  		case ipos == -1:
   448  			pos += spos + len("\"([^\"]*)\"")
   449  			args = append(args, reflect.String.String())
   450  		case ipos < spos:
   451  			pos += ipos + len("(\\d+)")
   452  			args = append(args, reflect.Int.String())
   453  		case spos < ipos:
   454  			pos += spos + len("\"([^\"]*)\"")
   455  			args = append(args, reflect.String.String())
   456  		}
   457  	}
   458  
   459  	if s.argument != nil {
   460  		if s.argument.GetDocString() != nil {
   461  			args = append(args, "*messages.PickleStepArgument_PickleDocString")
   462  		}
   463  		if s.argument.GetDataTable() != nil {
   464  			args = append(args, "*messages.PickleStepArgument_PickleTable")
   465  		}
   466  	}
   467  
   468  	var last string
   469  	for i, arg := range args {
   470  		if last == "" || last == arg {
   471  			ret += fmt.Sprintf("arg%d, ", i+1)
   472  		} else {
   473  			ret = strings.TrimRight(ret, ", ") + fmt.Sprintf(" %s, arg%d, ", last, i+1)
   474  		}
   475  		last = arg
   476  	}
   477  	return strings.TrimSpace(strings.TrimRight(ret, ", ") + " " + last)
   478  }
   479  
   480  func (f *basefmt) findStepResults(status stepResultStatus) (res []*stepResult) {
   481  	for _, feat := range f.features {
   482  		for _, pr := range feat.pickleResults {
   483  			for _, sr := range pr.stepResults {
   484  				if sr.status == status {
   485  					res = append(res, sr)
   486  				}
   487  			}
   488  		}
   489  	}
   490  
   491  	return
   492  }
   493  
   494  func (f *basefmt) snippets() string {
   495  	undefinedStepResults := f.findStepResults(undefined)
   496  	if len(undefinedStepResults) == 0 {
   497  		return ""
   498  	}
   499  
   500  	var index int
   501  	var snips []*undefinedSnippet
   502  	// build snippets
   503  	for _, u := range undefinedStepResults {
   504  		steps := []string{u.step.Text}
   505  		arg := u.step.Argument
   506  		if u.def != nil {
   507  			steps = u.def.undefined
   508  			arg = nil
   509  		}
   510  		for _, step := range steps {
   511  			expr := snippetExprCleanup.ReplaceAllString(step, "\\$1")
   512  			expr = snippetNumbers.ReplaceAllString(expr, "(\\d+)")
   513  			expr = snippetExprQuoted.ReplaceAllString(expr, "$1\"([^\"]*)\"$2")
   514  			expr = "^" + strings.TrimSpace(expr) + "$"
   515  
   516  			name := snippetNumbers.ReplaceAllString(step, " ")
   517  			name = snippetExprQuoted.ReplaceAllString(name, " ")
   518  			name = strings.TrimSpace(snippetMethodName.ReplaceAllString(name, ""))
   519  			var words []string
   520  			for i, w := range strings.Split(name, " ") {
   521  				switch {
   522  				case i != 0:
   523  					w = strings.Title(w)
   524  				case len(w) > 0:
   525  					w = string(unicode.ToLower(rune(w[0]))) + w[1:]
   526  				}
   527  				words = append(words, w)
   528  			}
   529  			name = strings.Join(words, "")
   530  			if len(name) == 0 {
   531  				index++
   532  				name = fmt.Sprintf("StepDefinitioninition%d", index)
   533  			}
   534  
   535  			var found bool
   536  			for _, snip := range snips {
   537  				if snip.Expr == expr {
   538  					found = true
   539  					break
   540  				}
   541  			}
   542  			if !found {
   543  				snips = append(snips, &undefinedSnippet{Method: name, Expr: expr, argument: arg})
   544  			}
   545  		}
   546  	}
   547  
   548  	var buf bytes.Buffer
   549  	if err := undefinedSnippetsTpl.Execute(&buf, snips); err != nil {
   550  		panic(err)
   551  	}
   552  	// there may be trailing spaces
   553  	return strings.Replace(buf.String(), " \n", "\n", -1)
   554  }
   555  
   556  func isLastStep(pickle *messages.Pickle, step *messages.Pickle_PickleStep) bool {
   557  	return pickle.Steps[len(pickle.Steps)-1].Id == step.Id
   558  }