github.com/agilebits/godog@v0.7.9/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  // FormatterFunc builds a formatter with given
   111  // suite name and io.Writer to record output
   112  type FormatterFunc func(string, io.Writer) Formatter
   113  
   114  type stepType int
   115  
   116  const (
   117  	passed stepType = iota
   118  	failed
   119  	skipped
   120  	undefined
   121  	pending
   122  )
   123  
   124  func (st stepType) clr() colors.ColorFunc {
   125  	switch st {
   126  	case passed:
   127  		return green
   128  	case failed:
   129  		return red
   130  	case skipped:
   131  		return cyan
   132  	default:
   133  		return yellow
   134  	}
   135  }
   136  
   137  func (st stepType) String() string {
   138  	switch st {
   139  	case passed:
   140  		return "passed"
   141  	case failed:
   142  		return "failed"
   143  	case skipped:
   144  		return "skipped"
   145  	case undefined:
   146  		return "undefined"
   147  	case pending:
   148  		return "pending"
   149  	default:
   150  		return "unknown"
   151  	}
   152  }
   153  
   154  type stepResult struct {
   155  	typ     stepType
   156  	feature *feature
   157  	owner   interface{}
   158  	step    *gherkin.Step
   159  	def     *StepDef
   160  	err     error
   161  }
   162  
   163  func (f stepResult) line() string {
   164  	return fmt.Sprintf("%s:%d", f.feature.Path, f.step.Location.Line)
   165  }
   166  
   167  func (f stepResult) scenarioDesc() string {
   168  	if sc, ok := f.owner.(*gherkin.Scenario); ok {
   169  		return fmt.Sprintf("%s: %s", sc.Keyword, sc.Name)
   170  	}
   171  
   172  	if row, ok := f.owner.(*gherkin.TableRow); ok {
   173  		for _, def := range f.feature.Feature.ScenarioDefinitions {
   174  			out, ok := def.(*gherkin.ScenarioOutline)
   175  			if !ok {
   176  				continue
   177  			}
   178  
   179  			for _, ex := range out.Examples {
   180  				for _, rw := range ex.TableBody {
   181  					if rw.Location.Line == row.Location.Line {
   182  						return fmt.Sprintf("%s: %s", out.Keyword, out.Name)
   183  					}
   184  				}
   185  			}
   186  		}
   187  	}
   188  	return f.line() // was not expecting different owner
   189  }
   190  
   191  func (f stepResult) scenarioLine() string {
   192  	if sc, ok := f.owner.(*gherkin.Scenario); ok {
   193  		return fmt.Sprintf("%s:%d", f.feature.Path, sc.Location.Line)
   194  	}
   195  
   196  	if row, ok := f.owner.(*gherkin.TableRow); ok {
   197  		for _, def := range f.feature.Feature.ScenarioDefinitions {
   198  			out, ok := def.(*gherkin.ScenarioOutline)
   199  			if !ok {
   200  				continue
   201  			}
   202  
   203  			for _, ex := range out.Examples {
   204  				for _, rw := range ex.TableBody {
   205  					if rw.Location.Line == row.Location.Line {
   206  						return fmt.Sprintf("%s:%d", f.feature.Path, out.Location.Line)
   207  					}
   208  				}
   209  			}
   210  		}
   211  	}
   212  	return f.line() // was not expecting different owner
   213  }
   214  
   215  type basefmt struct {
   216  	out    io.Writer
   217  	owner  interface{}
   218  	indent int
   219  
   220  	started   time.Time
   221  	features  []*feature
   222  	failed    []*stepResult
   223  	passed    []*stepResult
   224  	skipped   []*stepResult
   225  	undefined []*stepResult
   226  	pending   []*stepResult
   227  }
   228  
   229  func (f *basefmt) Node(n interface{}) {
   230  	switch t := n.(type) {
   231  	case *gherkin.TableRow:
   232  		f.owner = t
   233  	case *gherkin.Scenario:
   234  		f.owner = t
   235  	}
   236  }
   237  
   238  func (f *basefmt) Defined(*gherkin.Step, *StepDef) {
   239  
   240  }
   241  
   242  func (f *basefmt) Feature(ft *gherkin.Feature, p string, c []byte) {
   243  	f.features = append(f.features, &feature{Path: p, Feature: ft})
   244  }
   245  
   246  func (f *basefmt) Passed(step *gherkin.Step, match *StepDef) {
   247  	s := &stepResult{
   248  		owner:   f.owner,
   249  		feature: f.features[len(f.features)-1],
   250  		step:    step,
   251  		def:     match,
   252  		typ:     passed,
   253  	}
   254  	f.passed = append(f.passed, s)
   255  }
   256  
   257  func (f *basefmt) Skipped(step *gherkin.Step, match *StepDef) {
   258  	s := &stepResult{
   259  		owner:   f.owner,
   260  		feature: f.features[len(f.features)-1],
   261  		step:    step,
   262  		def:     match,
   263  		typ:     skipped,
   264  	}
   265  	f.skipped = append(f.skipped, s)
   266  }
   267  
   268  func (f *basefmt) Undefined(step *gherkin.Step, match *StepDef) {
   269  	s := &stepResult{
   270  		owner:   f.owner,
   271  		feature: f.features[len(f.features)-1],
   272  		step:    step,
   273  		def:     match,
   274  		typ:     undefined,
   275  	}
   276  	f.undefined = append(f.undefined, s)
   277  }
   278  
   279  func (f *basefmt) Failed(step *gherkin.Step, match *StepDef, err error) {
   280  	s := &stepResult{
   281  		owner:   f.owner,
   282  		feature: f.features[len(f.features)-1],
   283  		step:    step,
   284  		def:     match,
   285  		err:     err,
   286  		typ:     failed,
   287  	}
   288  	f.failed = append(f.failed, s)
   289  }
   290  
   291  func (f *basefmt) Pending(step *gherkin.Step, match *StepDef) {
   292  	s := &stepResult{
   293  		owner:   f.owner,
   294  		feature: f.features[len(f.features)-1],
   295  		step:    step,
   296  		def:     match,
   297  		typ:     pending,
   298  	}
   299  	f.pending = append(f.pending, s)
   300  }
   301  
   302  func (f *basefmt) Summary() {
   303  	var total, passed, undefined int
   304  	for _, ft := range f.features {
   305  		for _, def := range ft.ScenarioDefinitions {
   306  			switch t := def.(type) {
   307  			case *gherkin.Scenario:
   308  				total++
   309  				if len(t.Steps) == 0 {
   310  					undefined++
   311  				}
   312  			case *gherkin.ScenarioOutline:
   313  				for _, ex := range t.Examples {
   314  					total += len(ex.TableBody)
   315  					if len(t.Steps) == 0 {
   316  						undefined += len(ex.TableBody)
   317  					}
   318  				}
   319  			}
   320  		}
   321  	}
   322  	passed = total - undefined
   323  	var owner interface{}
   324  	for _, undef := range f.undefined {
   325  		if owner != undef.owner {
   326  			undefined++
   327  			owner = undef.owner
   328  		}
   329  	}
   330  
   331  	var steps, parts, scenarios []string
   332  	nsteps := len(f.passed) + len(f.failed) + len(f.skipped) + len(f.undefined) + len(f.pending)
   333  	if len(f.passed) > 0 {
   334  		steps = append(steps, green(fmt.Sprintf("%d passed", len(f.passed))))
   335  	}
   336  	if len(f.failed) > 0 {
   337  		passed -= len(f.failed)
   338  		parts = append(parts, red(fmt.Sprintf("%d failed", len(f.failed))))
   339  		steps = append(steps, parts[len(parts)-1])
   340  	}
   341  	if len(f.pending) > 0 {
   342  		passed -= len(f.pending)
   343  		parts = append(parts, yellow(fmt.Sprintf("%d pending", len(f.pending))))
   344  		steps = append(steps, yellow(fmt.Sprintf("%d pending", len(f.pending))))
   345  	}
   346  	if len(f.undefined) > 0 {
   347  		passed -= undefined
   348  		parts = append(parts, yellow(fmt.Sprintf("%d undefined", undefined)))
   349  		steps = append(steps, yellow(fmt.Sprintf("%d undefined", len(f.undefined))))
   350  	} else if undefined > 0 {
   351  		// there may be some scenarios without steps
   352  		parts = append(parts, yellow(fmt.Sprintf("%d undefined", undefined)))
   353  	}
   354  	if len(f.skipped) > 0 {
   355  		steps = append(steps, cyan(fmt.Sprintf("%d skipped", len(f.skipped))))
   356  	}
   357  	if passed > 0 {
   358  		scenarios = append(scenarios, green(fmt.Sprintf("%d passed", passed)))
   359  	}
   360  	scenarios = append(scenarios, parts...)
   361  	elapsed := timeNowFunc().Sub(f.started)
   362  
   363  	fmt.Fprintln(f.out, "")
   364  	if total == 0 {
   365  		fmt.Fprintln(f.out, "No scenarios")
   366  	} else {
   367  		fmt.Fprintln(f.out, fmt.Sprintf("%d scenarios (%s)", total, strings.Join(scenarios, ", ")))
   368  	}
   369  
   370  	if nsteps == 0 {
   371  		fmt.Fprintln(f.out, "No steps")
   372  	} else {
   373  		fmt.Fprintln(f.out, fmt.Sprintf("%d steps (%s)", nsteps, strings.Join(steps, ", ")))
   374  	}
   375  
   376  	elapsedString := elapsed.String()
   377  	if elapsed.Nanoseconds() == 0 {
   378  		// go 1.5 and 1.6 prints 0 instead of 0s, if duration is zero.
   379  		elapsedString = "0s"
   380  	}
   381  	fmt.Fprintln(f.out, elapsedString)
   382  
   383  	// prints used randomization seed
   384  	seed, err := strconv.ParseInt(os.Getenv("GODOG_SEED"), 10, 64)
   385  	if err == nil && seed != 0 {
   386  		fmt.Fprintln(f.out, "")
   387  		fmt.Fprintln(f.out, "Randomized with seed:", colors.Yellow(seed))
   388  	}
   389  
   390  	if text := f.snippets(); text != "" {
   391  		fmt.Fprintln(f.out, "")
   392  		fmt.Fprintln(f.out, yellow("You can implement step definitions for undefined steps with these snippets:"))
   393  		fmt.Fprintln(f.out, yellow(text))
   394  	}
   395  }
   396  
   397  func (s *undefinedSnippet) Args() (ret string) {
   398  	var (
   399  		args      []string
   400  		pos       int
   401  		breakLoop bool
   402  	)
   403  	for !breakLoop {
   404  		part := s.Expr[pos:]
   405  		ipos := strings.Index(part, "(\\d+)")
   406  		spos := strings.Index(part, "\"([^\"]*)\"")
   407  		switch {
   408  		case spos == -1 && ipos == -1:
   409  			breakLoop = true
   410  		case spos == -1:
   411  			pos += ipos + len("(\\d+)")
   412  			args = append(args, reflect.Int.String())
   413  		case ipos == -1:
   414  			pos += spos + len("\"([^\"]*)\"")
   415  			args = append(args, reflect.String.String())
   416  		case ipos < spos:
   417  			pos += ipos + len("(\\d+)")
   418  			args = append(args, reflect.Int.String())
   419  		case spos < ipos:
   420  			pos += spos + len("\"([^\"]*)\"")
   421  			args = append(args, reflect.String.String())
   422  		}
   423  	}
   424  	if s.argument != nil {
   425  		switch s.argument.(type) {
   426  		case *gherkin.DocString:
   427  			args = append(args, "*gherkin.DocString")
   428  		case *gherkin.DataTable:
   429  			args = append(args, "*gherkin.DataTable")
   430  		}
   431  	}
   432  
   433  	var last string
   434  	for i, arg := range args {
   435  		if last == "" || last == arg {
   436  			ret += fmt.Sprintf("arg%d, ", i+1)
   437  		} else {
   438  			ret = strings.TrimRight(ret, ", ") + fmt.Sprintf(" %s, arg%d, ", last, i+1)
   439  		}
   440  		last = arg
   441  	}
   442  	return strings.TrimSpace(strings.TrimRight(ret, ", ") + " " + last)
   443  }
   444  
   445  func (f *basefmt) snippets() string {
   446  	if len(f.undefined) == 0 {
   447  		return ""
   448  	}
   449  
   450  	var index int
   451  	var snips []*undefinedSnippet
   452  	// build snippets
   453  	for _, u := range f.undefined {
   454  		steps := []string{u.step.Text}
   455  		arg := u.step.Argument
   456  		if u.def != nil {
   457  			steps = u.def.undefined
   458  			arg = nil
   459  		}
   460  		for _, step := range steps {
   461  			expr := snippetExprCleanup.ReplaceAllString(step, "\\$1")
   462  			expr = snippetNumbers.ReplaceAllString(expr, "(\\d+)")
   463  			expr = snippetExprQuoted.ReplaceAllString(expr, "$1\"([^\"]*)\"$2")
   464  			expr = "^" + strings.TrimSpace(expr) + "$"
   465  
   466  			name := snippetNumbers.ReplaceAllString(step, " ")
   467  			name = snippetExprQuoted.ReplaceAllString(name, " ")
   468  			name = strings.TrimSpace(snippetMethodName.ReplaceAllString(name, ""))
   469  			var words []string
   470  			for i, w := range strings.Split(name, " ") {
   471  				switch {
   472  				case i != 0:
   473  					w = strings.Title(w)
   474  				case len(w) > 0:
   475  					w = string(unicode.ToLower(rune(w[0]))) + w[1:]
   476  				}
   477  				words = append(words, w)
   478  			}
   479  			name = strings.Join(words, "")
   480  			if len(name) == 0 {
   481  				index++
   482  				name = fmt.Sprintf("stepDefinition%d", index)
   483  			}
   484  
   485  			var found bool
   486  			for _, snip := range snips {
   487  				if snip.Expr == expr {
   488  					found = true
   489  					break
   490  				}
   491  			}
   492  			if !found {
   493  				snips = append(snips, &undefinedSnippet{Method: name, Expr: expr, argument: arg})
   494  			}
   495  		}
   496  	}
   497  
   498  	var buf bytes.Buffer
   499  	if err := undefinedSnippetsTpl.Execute(&buf, snips); err != nil {
   500  		panic(err)
   501  	}
   502  	// there may be trailing spaces
   503  	return strings.Replace(buf.String(), " \n", "\n", -1)
   504  }
   505  
   506  func (f *basefmt) isLastStep(s *gherkin.Step) bool {
   507  	ft := f.features[len(f.features)-1]
   508  
   509  	for _, def := range ft.ScenarioDefinitions {
   510  		if outline, ok := def.(*gherkin.ScenarioOutline); ok {
   511  			for n, step := range outline.Steps {
   512  				if step.Location.Line == s.Location.Line {
   513  					return n == len(outline.Steps)-1
   514  				}
   515  			}
   516  		}
   517  
   518  		if scenario, ok := def.(*gherkin.Scenario); ok {
   519  			for n, step := range scenario.Steps {
   520  				if step.Location.Line == s.Location.Line {
   521  					return n == len(scenario.Steps)-1
   522  				}
   523  			}
   524  		}
   525  	}
   526  	return false
   527  }