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