github.com/maps90/godog@v0.7.5-0.20170923143419-0093943021d4/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  func findFmt(format string) FormatterFunc {
    57  	for _, el := range formatters {
    58  		if el.name == format {
    59  			return el.fmt
    60  		}
    61  	}
    62  	return nil
    63  }
    64  
    65  // Format registers a feature suite output
    66  // formatter by given name, description and
    67  // FormatterFunc constructor function, to initialize
    68  // formatter with the output recorder.
    69  func Format(name, description string, f FormatterFunc) {
    70  	formatters = append(formatters, &registeredFormatter{
    71  		name:        name,
    72  		fmt:         f,
    73  		description: description,
    74  	})
    75  }
    76  
    77  // AvailableFormatters gives a map of all
    78  // formatters registered with their name as key
    79  // and description as value
    80  func AvailableFormatters() map[string]string {
    81  	fmts := make(map[string]string, len(formatters))
    82  	for _, f := range formatters {
    83  		fmts[f.name] = f.description
    84  	}
    85  	return fmts
    86  }
    87  
    88  // Formatter is an interface for feature runner
    89  // output summary presentation.
    90  //
    91  // New formatters may be created to represent
    92  // suite results in different ways. These new
    93  // formatters needs to be registered with a
    94  // godog.Format function call
    95  type Formatter interface {
    96  	Feature(*gherkin.Feature, string, []byte)
    97  	Node(interface{})
    98  	Defined(*gherkin.Step, *StepDef)
    99  	Failed(*gherkin.Step, *StepDef, error)
   100  	Passed(*gherkin.Step, *StepDef)
   101  	Skipped(*gherkin.Step, *StepDef)
   102  	Undefined(*gherkin.Step, *StepDef)
   103  	Pending(*gherkin.Step, *StepDef)
   104  	Summary()
   105  }
   106  
   107  // FormatterFunc builds a formatter with given
   108  // suite name and io.Writer to record output
   109  type FormatterFunc func(string, io.Writer) Formatter
   110  
   111  type stepType int
   112  
   113  const (
   114  	passed stepType = iota
   115  	failed
   116  	skipped
   117  	undefined
   118  	pending
   119  )
   120  
   121  func (st stepType) clr() colors.ColorFunc {
   122  	switch st {
   123  	case passed:
   124  		return green
   125  	case failed:
   126  		return red
   127  	case skipped:
   128  		return cyan
   129  	default:
   130  		return yellow
   131  	}
   132  }
   133  
   134  func (st stepType) String() string {
   135  	switch st {
   136  	case passed:
   137  		return "passed"
   138  	case failed:
   139  		return "failed"
   140  	case skipped:
   141  		return "skipped"
   142  	case undefined:
   143  		return "undefined"
   144  	case pending:
   145  		return "pending"
   146  	default:
   147  		return "unknown"
   148  	}
   149  }
   150  
   151  type stepResult struct {
   152  	typ     stepType
   153  	feature *feature
   154  	owner   interface{}
   155  	step    *gherkin.Step
   156  	def     *StepDef
   157  	err     error
   158  }
   159  
   160  func (f stepResult) line() string {
   161  	return fmt.Sprintf("%s:%d", f.feature.Path, f.step.Location.Line)
   162  }
   163  
   164  type basefmt struct {
   165  	out    io.Writer
   166  	owner  interface{}
   167  	indent int
   168  
   169  	started   time.Time
   170  	features  []*feature
   171  	failed    []*stepResult
   172  	passed    []*stepResult
   173  	skipped   []*stepResult
   174  	undefined []*stepResult
   175  	pending   []*stepResult
   176  }
   177  
   178  func (f *basefmt) Node(n interface{}) {
   179  	switch t := n.(type) {
   180  	case *gherkin.TableRow:
   181  		f.owner = t
   182  	case *gherkin.Scenario:
   183  		f.owner = t
   184  	}
   185  }
   186  
   187  func (f *basefmt) Defined(*gherkin.Step, *StepDef) {
   188  
   189  }
   190  
   191  func (f *basefmt) Feature(ft *gherkin.Feature, p string, c []byte) {
   192  	f.features = append(f.features, &feature{Path: p, Feature: ft})
   193  }
   194  
   195  func (f *basefmt) Passed(step *gherkin.Step, match *StepDef) {
   196  	s := &stepResult{
   197  		owner:   f.owner,
   198  		feature: f.features[len(f.features)-1],
   199  		step:    step,
   200  		def:     match,
   201  		typ:     passed,
   202  	}
   203  	f.passed = append(f.passed, s)
   204  }
   205  
   206  func (f *basefmt) Skipped(step *gherkin.Step, match *StepDef) {
   207  	s := &stepResult{
   208  		owner:   f.owner,
   209  		feature: f.features[len(f.features)-1],
   210  		step:    step,
   211  		def:     match,
   212  		typ:     skipped,
   213  	}
   214  	f.skipped = append(f.skipped, s)
   215  }
   216  
   217  func (f *basefmt) Undefined(step *gherkin.Step, match *StepDef) {
   218  	s := &stepResult{
   219  		owner:   f.owner,
   220  		feature: f.features[len(f.features)-1],
   221  		step:    step,
   222  		def:     match,
   223  		typ:     undefined,
   224  	}
   225  	f.undefined = append(f.undefined, s)
   226  }
   227  
   228  func (f *basefmt) Failed(step *gherkin.Step, match *StepDef, err error) {
   229  	s := &stepResult{
   230  		owner:   f.owner,
   231  		feature: f.features[len(f.features)-1],
   232  		step:    step,
   233  		def:     match,
   234  		err:     err,
   235  		typ:     failed,
   236  	}
   237  	f.failed = append(f.failed, s)
   238  }
   239  
   240  func (f *basefmt) Pending(step *gherkin.Step, match *StepDef) {
   241  	s := &stepResult{
   242  		owner:   f.owner,
   243  		feature: f.features[len(f.features)-1],
   244  		step:    step,
   245  		def:     match,
   246  		typ:     pending,
   247  	}
   248  	f.pending = append(f.pending, s)
   249  }
   250  
   251  func (f *basefmt) Summary() {
   252  	var total, passed, undefined int
   253  	for _, ft := range f.features {
   254  		for _, def := range ft.ScenarioDefinitions {
   255  			switch t := def.(type) {
   256  			case *gherkin.Scenario:
   257  				total++
   258  			case *gherkin.ScenarioOutline:
   259  				for _, ex := range t.Examples {
   260  					if examples, hasExamples := examples(ex); hasExamples {
   261  						total += len(examples.TableBody)
   262  					}
   263  				}
   264  			}
   265  		}
   266  	}
   267  	passed = total
   268  	var owner interface{}
   269  	for _, undef := range f.undefined {
   270  		if owner != undef.owner {
   271  			undefined++
   272  			owner = undef.owner
   273  		}
   274  	}
   275  
   276  	var steps, parts, scenarios []string
   277  	nsteps := len(f.passed) + len(f.failed) + len(f.skipped) + len(f.undefined) + len(f.pending)
   278  	if len(f.passed) > 0 {
   279  		steps = append(steps, green(fmt.Sprintf("%d passed", len(f.passed))))
   280  	}
   281  	if len(f.failed) > 0 {
   282  		passed -= len(f.failed)
   283  		parts = append(parts, red(fmt.Sprintf("%d failed", len(f.failed))))
   284  		steps = append(steps, parts[len(parts)-1])
   285  	}
   286  	if len(f.pending) > 0 {
   287  		passed -= len(f.pending)
   288  		parts = append(parts, yellow(fmt.Sprintf("%d pending", len(f.pending))))
   289  		steps = append(steps, yellow(fmt.Sprintf("%d pending", len(f.pending))))
   290  	}
   291  	if len(f.undefined) > 0 {
   292  		passed -= undefined
   293  		parts = append(parts, yellow(fmt.Sprintf("%d undefined", undefined)))
   294  		steps = append(steps, yellow(fmt.Sprintf("%d undefined", len(f.undefined))))
   295  	}
   296  	if len(f.skipped) > 0 {
   297  		steps = append(steps, cyan(fmt.Sprintf("%d skipped", len(f.skipped))))
   298  	}
   299  	if passed > 0 {
   300  		scenarios = append(scenarios, green(fmt.Sprintf("%d passed", passed)))
   301  	}
   302  	scenarios = append(scenarios, parts...)
   303  	elapsed := timeNowFunc().Sub(f.started)
   304  
   305  	fmt.Fprintln(f.out, "")
   306  	if total == 0 {
   307  		fmt.Fprintln(f.out, "No scenarios")
   308  	} else {
   309  		fmt.Fprintln(f.out, fmt.Sprintf("%d scenarios (%s)", total, strings.Join(scenarios, ", ")))
   310  	}
   311  
   312  	if nsteps == 0 {
   313  		fmt.Fprintln(f.out, "No steps")
   314  	} else {
   315  		fmt.Fprintln(f.out, fmt.Sprintf("%d steps (%s)", nsteps, strings.Join(steps, ", ")))
   316  	}
   317  	fmt.Fprintln(f.out, elapsed)
   318  
   319  	// prints used randomization seed
   320  	seed, err := strconv.ParseInt(os.Getenv("GODOG_SEED"), 10, 64)
   321  	if err == nil && seed != 0 {
   322  		fmt.Fprintln(f.out, "")
   323  		fmt.Fprintln(f.out, "Randomized with seed:", colors.Yellow(seed))
   324  	}
   325  
   326  	if text := f.snippets(); text != "" {
   327  		fmt.Fprintln(f.out, yellow("\nYou can implement step definitions for undefined steps with these snippets:"))
   328  		fmt.Fprintln(f.out, yellow(text))
   329  	}
   330  }
   331  
   332  func (s *undefinedSnippet) Args() (ret string) {
   333  	var args []string
   334  	var pos, idx int
   335  	var breakLoop bool
   336  	for !breakLoop {
   337  		part := s.Expr[pos:]
   338  		ipos := strings.Index(part, "(\\d+)")
   339  		spos := strings.Index(part, "\"([^\"]*)\"")
   340  		switch {
   341  		case spos == -1 && ipos == -1:
   342  			breakLoop = true
   343  		case spos == -1:
   344  			idx++
   345  			pos += ipos + len("(\\d+)")
   346  			args = append(args, reflect.Int.String())
   347  		case ipos == -1:
   348  			idx++
   349  			pos += spos + len("\"([^\"]*)\"")
   350  			args = append(args, reflect.String.String())
   351  		case ipos < spos:
   352  			idx++
   353  			pos += ipos + len("(\\d+)")
   354  			args = append(args, reflect.Int.String())
   355  		case spos < ipos:
   356  			idx++
   357  			pos += spos + len("\"([^\"]*)\"")
   358  			args = append(args, reflect.String.String())
   359  		}
   360  	}
   361  	if s.argument != nil {
   362  		idx++
   363  		switch s.argument.(type) {
   364  		case *gherkin.DocString:
   365  			args = append(args, "*gherkin.DocString")
   366  		case *gherkin.DataTable:
   367  			args = append(args, "*gherkin.DataTable")
   368  		}
   369  	}
   370  
   371  	var last string
   372  	for i, arg := range args {
   373  		if last == "" || last == arg {
   374  			ret += fmt.Sprintf("arg%d, ", i+1)
   375  		} else {
   376  			ret = strings.TrimRight(ret, ", ") + fmt.Sprintf(" %s, arg%d, ", last, i+1)
   377  		}
   378  		last = arg
   379  	}
   380  	return strings.TrimSpace(strings.TrimRight(ret, ", ") + " " + last)
   381  }
   382  
   383  func (f *basefmt) snippets() string {
   384  	if len(f.undefined) == 0 {
   385  		return ""
   386  	}
   387  
   388  	var index int
   389  	var snips []*undefinedSnippet
   390  	// build snippets
   391  	for _, u := range f.undefined {
   392  		steps := []string{u.step.Text}
   393  		arg := u.step.Argument
   394  		if u.def != nil {
   395  			steps = u.def.undefined
   396  			arg = nil
   397  		}
   398  		for _, step := range steps {
   399  			expr := snippetExprCleanup.ReplaceAllString(step, "\\$1")
   400  			expr = snippetNumbers.ReplaceAllString(expr, "(\\d+)")
   401  			expr = snippetExprQuoted.ReplaceAllString(expr, "$1\"([^\"]*)\"$2")
   402  			expr = "^" + strings.TrimSpace(expr) + "$"
   403  
   404  			name := snippetNumbers.ReplaceAllString(step, " ")
   405  			name = snippetExprQuoted.ReplaceAllString(name, " ")
   406  			name = strings.TrimSpace(snippetMethodName.ReplaceAllString(name, ""))
   407  			var words []string
   408  			for i, w := range strings.Split(name, " ") {
   409  				switch {
   410  				case i != 0:
   411  					w = strings.Title(w)
   412  				case len(w) > 0:
   413  					w = string(unicode.ToLower(rune(w[0]))) + w[1:]
   414  				}
   415  				words = append(words, w)
   416  			}
   417  			name = strings.Join(words, "")
   418  			if len(name) == 0 {
   419  				index++
   420  				name = fmt.Sprintf("stepDefinition%d", index)
   421  			}
   422  
   423  			var found bool
   424  			for _, snip := range snips {
   425  				if snip.Expr == expr {
   426  					found = true
   427  					break
   428  				}
   429  			}
   430  			if !found {
   431  				snips = append(snips, &undefinedSnippet{Method: name, Expr: expr, argument: arg})
   432  			}
   433  		}
   434  	}
   435  
   436  	var buf bytes.Buffer
   437  	if err := undefinedSnippetsTpl.Execute(&buf, snips); err != nil {
   438  		panic(err)
   439  	}
   440  	// there may be trailing spaces
   441  	return strings.Replace(buf.String(), " \n", "\n", -1)
   442  }
   443  
   444  func (f *basefmt) isLastStep(s *gherkin.Step) bool {
   445  	ft := f.features[len(f.features)-1]
   446  
   447  	for _, def := range ft.ScenarioDefinitions {
   448  		if outline, ok := def.(*gherkin.ScenarioOutline); ok {
   449  			for n, step := range outline.Steps {
   450  				if step.Location.Line == s.Location.Line {
   451  					return n == len(outline.Steps)-1
   452  				}
   453  			}
   454  		}
   455  
   456  		if scenario, ok := def.(*gherkin.Scenario); ok {
   457  			for n, step := range scenario.Steps {
   458  				if step.Location.Line == s.Location.Line {
   459  					return n == len(scenario.Steps)-1
   460  				}
   461  			}
   462  		}
   463  	}
   464  	return false
   465  }