package godog import ( "bytes" "fmt" "io" "os" "reflect" "regexp" "strconv" "strings" "sync" "text/template" "time" "unicode" "github.com/cucumber/godog/colors" "github.com/cucumber/godog/gherkin" ) // some snippet formatting regexps var snippetExprCleanup = regexp.MustCompile("([\\/\\[\\]\\(\\)\\\\^\\$\\.\\|\\?\\*\\+\\'])") var snippetExprQuoted = regexp.MustCompile("(\\W|^)\"(?:[^\"]*)\"(\\W|$)") var snippetMethodName = regexp.MustCompile("[^a-zA-Z\\_\\ ]") var snippetNumbers = regexp.MustCompile("(\\d+)") var snippetHelperFuncs = template.FuncMap{ "backticked": func(s string) string { return "`" + s + "`" }, } var undefinedSnippetsTpl = template.Must(template.New("snippets").Funcs(snippetHelperFuncs).Parse(` {{ range . }}func {{ .Method }}({{ .Args }}) error { return godog.ErrPending } {{end}}func FeatureContext(s *godog.Suite) { {{ range . }} s.Step({{ backticked .Expr }}, {{ .Method }}){{end}} } `)) type undefinedSnippet struct { Method string Expr string argument interface{} // gherkin step argument } type registeredFormatter struct { name string fmt FormatterFunc description string } var formatters []*registeredFormatter // FindFmt searches available formatters registered // and returns FormaterFunc matched by given // format name or nil otherwise func FindFmt(name string) FormatterFunc { for _, el := range formatters { if el.name == name { return el.fmt } } return nil } // Format registers a feature suite output // formatter by given name, description and // FormatterFunc constructor function, to initialize // formatter with the output recorder. func Format(name, description string, f FormatterFunc) { formatters = append(formatters, ®isteredFormatter{ name: name, fmt: f, description: description, }) } // AvailableFormatters gives a map of all // formatters registered with their name as key // and description as value func AvailableFormatters() map[string]string { fmts := make(map[string]string, len(formatters)) for _, f := range formatters { fmts[f.name] = f.description } return fmts } // Formatter is an interface for feature runner // output summary presentation. // // New formatters may be created to represent // suite results in different ways. These new // formatters needs to be registered with a // godog.Format function call type Formatter interface { Feature(*gherkin.Feature, string, []byte) Node(interface{}) Defined(*gherkin.Step, *StepDef) Failed(*gherkin.Step, *StepDef, error) Passed(*gherkin.Step, *StepDef) Skipped(*gherkin.Step, *StepDef) Undefined(*gherkin.Step, *StepDef) Pending(*gherkin.Step, *StepDef) Summary() } // ConcurrentFormatter is an interface for a Concurrent // version of the Formatter interface. type ConcurrentFormatter interface { Formatter Copy(ConcurrentFormatter) Sync(ConcurrentFormatter) } // FormatterFunc builds a formatter with given // suite name and io.Writer to record output type FormatterFunc func(string, io.Writer) Formatter type stepType int const ( passed stepType = iota failed skipped undefined pending ) func (st stepType) clr() colors.ColorFunc { switch st { case passed: return green case failed: return red case skipped: return cyan default: return yellow } } func (st stepType) String() string { switch st { case passed: return "passed" case failed: return "failed" case skipped: return "skipped" case undefined: return "undefined" case pending: return "pending" default: return "unknown" } } type stepResult struct { typ stepType feature *feature owner interface{} step *gherkin.Step time time.Time def *StepDef err error } func (f stepResult) line() string { return fmt.Sprintf("%s:%d", f.feature.Path, f.step.Location.Line) } func (f stepResult) scenarioDesc() string { if sc, ok := f.owner.(*gherkin.Scenario); ok { return fmt.Sprintf("%s: %s", sc.Keyword, sc.Name) } if row, ok := f.owner.(*gherkin.TableRow); ok { for _, def := range f.feature.Feature.ScenarioDefinitions { out, ok := def.(*gherkin.ScenarioOutline) if !ok { continue } for _, ex := range out.Examples { for _, rw := range ex.TableBody { if rw.Location.Line == row.Location.Line { return fmt.Sprintf("%s: %s", out.Keyword, out.Name) } } } } } return f.line() // was not expecting different owner } func (f stepResult) scenarioLine() string { if sc, ok := f.owner.(*gherkin.Scenario); ok { return fmt.Sprintf("%s:%d", f.feature.Path, sc.Location.Line) } if row, ok := f.owner.(*gherkin.TableRow); ok { for _, def := range f.feature.Feature.ScenarioDefinitions { out, ok := def.(*gherkin.ScenarioOutline) if !ok { continue } for _, ex := range out.Examples { for _, rw := range ex.TableBody { if rw.Location.Line == row.Location.Line { return fmt.Sprintf("%s:%d", f.feature.Path, out.Location.Line) } } } } } return f.line() // was not expecting different owner } func newBaseFmt(suite string, out io.Writer) *basefmt { return &basefmt{ suiteName: suite, started: timeNowFunc(), indent: 2, out: out, lock: new(sync.Mutex), } } type basefmt struct { suiteName string out io.Writer owner interface{} indent int started time.Time features []*feature failed []*stepResult passed []*stepResult skipped []*stepResult undefined []*stepResult pending []*stepResult lock *sync.Mutex } func (f *basefmt) Node(n interface{}) { f.lock.Lock() defer f.lock.Unlock() switch t := n.(type) { case *gherkin.Scenario: f.owner = t feature := f.features[len(f.features)-1] feature.Scenarios = append(feature.Scenarios, &scenario{Name: t.Name, time: timeNowFunc()}) case *gherkin.ScenarioOutline: feature := f.features[len(f.features)-1] feature.Scenarios = append(feature.Scenarios, &scenario{OutlineName: t.Name}) case *gherkin.TableRow: f.owner = t feature := f.features[len(f.features)-1] lastExample := feature.Scenarios[len(feature.Scenarios)-1] newExample := scenario{OutlineName: lastExample.OutlineName, ExampleNo: lastExample.ExampleNo + 1, time: timeNowFunc()} newExample.Name = fmt.Sprintf("%s #%d", newExample.OutlineName, newExample.ExampleNo) const firstExample = 1 if newExample.ExampleNo == firstExample { feature.Scenarios[len(feature.Scenarios)-1] = &newExample } else { feature.Scenarios = append(feature.Scenarios, &newExample) } } } func (f *basefmt) Defined(*gherkin.Step, *StepDef) { f.lock.Lock() defer f.lock.Unlock() } func (f *basefmt) Feature(ft *gherkin.Feature, p string, c []byte) { f.lock.Lock() defer f.lock.Unlock() f.features = append(f.features, &feature{Path: p, Feature: ft, time: timeNowFunc()}) } func (f *basefmt) Passed(step *gherkin.Step, match *StepDef) { f.lock.Lock() defer f.lock.Unlock() s := &stepResult{ owner: f.owner, feature: f.features[len(f.features)-1], step: step, def: match, typ: passed, time: timeNowFunc(), } f.passed = append(f.passed, s) f.features[len(f.features)-1].appendStepResult(s) } func (f *basefmt) Skipped(step *gherkin.Step, match *StepDef) { f.lock.Lock() defer f.lock.Unlock() s := &stepResult{ owner: f.owner, feature: f.features[len(f.features)-1], step: step, def: match, typ: skipped, time: timeNowFunc(), } f.skipped = append(f.skipped, s) f.features[len(f.features)-1].appendStepResult(s) } func (f *basefmt) Undefined(step *gherkin.Step, match *StepDef) { f.lock.Lock() defer f.lock.Unlock() s := &stepResult{ owner: f.owner, feature: f.features[len(f.features)-1], step: step, def: match, typ: undefined, time: timeNowFunc(), } f.undefined = append(f.undefined, s) f.features[len(f.features)-1].appendStepResult(s) } func (f *basefmt) Failed(step *gherkin.Step, match *StepDef, err error) { f.lock.Lock() defer f.lock.Unlock() s := &stepResult{ owner: f.owner, feature: f.features[len(f.features)-1], step: step, def: match, err: err, typ: failed, time: timeNowFunc(), } f.failed = append(f.failed, s) f.features[len(f.features)-1].appendStepResult(s) } func (f *basefmt) Pending(step *gherkin.Step, match *StepDef) { f.lock.Lock() defer f.lock.Unlock() s := &stepResult{ owner: f.owner, feature: f.features[len(f.features)-1], step: step, def: match, typ: pending, time: timeNowFunc(), } f.pending = append(f.pending, s) f.features[len(f.features)-1].appendStepResult(s) } func (f *basefmt) Summary() { var total, passed, undefined int for _, ft := range f.features { for _, def := range ft.ScenarioDefinitions { switch t := def.(type) { case *gherkin.Scenario: total++ if len(t.Steps) == 0 { undefined++ } case *gherkin.ScenarioOutline: for _, ex := range t.Examples { total += len(ex.TableBody) if len(t.Steps) == 0 { undefined += len(ex.TableBody) } } } } } passed = total - undefined var owner interface{} for _, undef := range f.undefined { if owner != undef.owner { undefined++ owner = undef.owner } } var steps, parts, scenarios []string nsteps := len(f.passed) + len(f.failed) + len(f.skipped) + len(f.undefined) + len(f.pending) if len(f.passed) > 0 { steps = append(steps, green(fmt.Sprintf("%d passed", len(f.passed)))) } if len(f.failed) > 0 { passed -= len(f.failed) parts = append(parts, red(fmt.Sprintf("%d failed", len(f.failed)))) steps = append(steps, parts[len(parts)-1]) } if len(f.pending) > 0 { passed -= len(f.pending) parts = append(parts, yellow(fmt.Sprintf("%d pending", len(f.pending)))) steps = append(steps, yellow(fmt.Sprintf("%d pending", len(f.pending)))) } if len(f.undefined) > 0 { passed -= undefined parts = append(parts, yellow(fmt.Sprintf("%d undefined", undefined))) steps = append(steps, yellow(fmt.Sprintf("%d undefined", len(f.undefined)))) } else if undefined > 0 { // there may be some scenarios without steps parts = append(parts, yellow(fmt.Sprintf("%d undefined", undefined))) } if len(f.skipped) > 0 { steps = append(steps, cyan(fmt.Sprintf("%d skipped", len(f.skipped)))) } if passed > 0 { scenarios = append(scenarios, green(fmt.Sprintf("%d passed", passed))) } scenarios = append(scenarios, parts...) elapsed := timeNowFunc().Sub(f.started) fmt.Fprintln(f.out, "") if total == 0 { fmt.Fprintln(f.out, "No scenarios") } else { fmt.Fprintln(f.out, fmt.Sprintf("%d scenarios (%s)", total, strings.Join(scenarios, ", "))) } if nsteps == 0 { fmt.Fprintln(f.out, "No steps") } else { fmt.Fprintln(f.out, fmt.Sprintf("%d steps (%s)", nsteps, strings.Join(steps, ", "))) } elapsedString := elapsed.String() if elapsed.Nanoseconds() == 0 { // go 1.5 and 1.6 prints 0 instead of 0s, if duration is zero. elapsedString = "0s" } fmt.Fprintln(f.out, elapsedString) // prints used randomization seed seed, err := strconv.ParseInt(os.Getenv("GODOG_SEED"), 10, 64) if err == nil && seed != 0 { fmt.Fprintln(f.out, "") fmt.Fprintln(f.out, "Randomized with seed:", colors.Yellow(seed)) } if text := f.snippets(); text != "" { fmt.Fprintln(f.out, "") fmt.Fprintln(f.out, yellow("You can implement step definitions for undefined steps with these snippets:")) fmt.Fprintln(f.out, yellow(text)) } } func (f *basefmt) Sync(cf ConcurrentFormatter) { if source, ok := cf.(*basefmt); ok { f.lock = source.lock } } func (f *basefmt) Copy(cf ConcurrentFormatter) { if source, ok := cf.(*basefmt); ok { for _, v := range source.features { f.features = append(f.features, v) } for _, v := range source.failed { f.failed = append(f.failed, v) } for _, v := range source.passed { f.passed = append(f.passed, v) } for _, v := range source.skipped { f.skipped = append(f.skipped, v) } for _, v := range source.undefined { f.undefined = append(f.undefined, v) } for _, v := range source.pending { f.pending = append(f.pending, v) } } } func (s *undefinedSnippet) Args() (ret string) { var ( args []string pos int breakLoop bool ) for !breakLoop { part := s.Expr[pos:] ipos := strings.Index(part, "(\\d+)") spos := strings.Index(part, "\"([^\"]*)\"") switch { case spos == -1 && ipos == -1: breakLoop = true case spos == -1: pos += ipos + len("(\\d+)") args = append(args, reflect.Int.String()) case ipos == -1: pos += spos + len("\"([^\"]*)\"") args = append(args, reflect.String.String()) case ipos < spos: pos += ipos + len("(\\d+)") args = append(args, reflect.Int.String()) case spos < ipos: pos += spos + len("\"([^\"]*)\"") args = append(args, reflect.String.String()) } } if s.argument != nil { switch s.argument.(type) { case *gherkin.DocString: args = append(args, "*gherkin.DocString") case *gherkin.DataTable: args = append(args, "*gherkin.DataTable") } } var last string for i, arg := range args { if last == "" || last == arg { ret += fmt.Sprintf("arg%d, ", i+1) } else { ret = strings.TrimRight(ret, ", ") + fmt.Sprintf(" %s, arg%d, ", last, i+1) } last = arg } return strings.TrimSpace(strings.TrimRight(ret, ", ") + " " + last) } func (f *basefmt) snippets() string { if len(f.undefined) == 0 { return "" } var index int var snips []*undefinedSnippet // build snippets for _, u := range f.undefined { steps := []string{u.step.Text} arg := u.step.Argument if u.def != nil { steps = u.def.undefined arg = nil } for _, step := range steps { expr := snippetExprCleanup.ReplaceAllString(step, "\\$1") expr = snippetNumbers.ReplaceAllString(expr, "(\\d+)") expr = snippetExprQuoted.ReplaceAllString(expr, "$1\"([^\"]*)\"$2") expr = "^" + strings.TrimSpace(expr) + "$" name := snippetNumbers.ReplaceAllString(step, " ") name = snippetExprQuoted.ReplaceAllString(name, " ") name = strings.TrimSpace(snippetMethodName.ReplaceAllString(name, "")) var words []string for i, w := range strings.Split(name, " ") { switch { case i != 0: w = strings.Title(w) case len(w) > 0: w = string(unicode.ToLower(rune(w[0]))) + w[1:] } words = append(words, w) } name = strings.Join(words, "") if len(name) == 0 { index++ name = fmt.Sprintf("stepDefinition%d", index) } var found bool for _, snip := range snips { if snip.Expr == expr { found = true break } } if !found { snips = append(snips, &undefinedSnippet{Method: name, Expr: expr, argument: arg}) } } } var buf bytes.Buffer if err := undefinedSnippetsTpl.Execute(&buf, snips); err != nil { panic(err) } // there may be trailing spaces return strings.Replace(buf.String(), " \n", "\n", -1) } func (f *basefmt) isLastStep(s *gherkin.Step) bool { ft := f.features[len(f.features)-1] for _, def := range ft.ScenarioDefinitions { if outline, ok := def.(*gherkin.ScenarioOutline); ok { for n, step := range outline.Steps { if step.Location.Line == s.Location.Line { return n == len(outline.Steps)-1 } } } if scenario, ok := def.(*gherkin.Scenario); ok { for n, step := range scenario.Steps { if step.Location.Line == s.Location.Line { return n == len(scenario.Steps)-1 } } } } return false }