509 строки
12 КиБ
Go
509 строки
12 КиБ
Go
package godog
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
"unicode"
|
|
|
|
"github.com/cucumber/messages-go/v10"
|
|
|
|
"github.com/cucumber/godog/colors"
|
|
)
|
|
|
|
type registeredFormatter struct {
|
|
name string
|
|
description string
|
|
fmt FormatterFunc
|
|
}
|
|
|
|
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 {
|
|
TestRunStarted()
|
|
Feature(*messages.GherkinDocument, string, []byte)
|
|
Pickle(*messages.Pickle)
|
|
Defined(*messages.Pickle, *messages.Pickle_PickleStep, *StepDefinition)
|
|
Failed(*messages.Pickle, *messages.Pickle_PickleStep, *StepDefinition, error)
|
|
Passed(*messages.Pickle, *messages.Pickle_PickleStep, *StepDefinition)
|
|
Skipped(*messages.Pickle, *messages.Pickle_PickleStep, *StepDefinition)
|
|
Undefined(*messages.Pickle, *messages.Pickle_PickleStep, *StepDefinition)
|
|
Pending(*messages.Pickle, *messages.Pickle_PickleStep, *StepDefinition)
|
|
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 stepResultStatus int
|
|
|
|
const (
|
|
passed stepResultStatus = iota
|
|
failed
|
|
skipped
|
|
undefined
|
|
pending
|
|
)
|
|
|
|
func (st stepResultStatus) clr() colors.ColorFunc {
|
|
switch st {
|
|
case passed:
|
|
return green
|
|
case failed:
|
|
return red
|
|
case skipped:
|
|
return cyan
|
|
default:
|
|
return yellow
|
|
}
|
|
}
|
|
|
|
func (st stepResultStatus) 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 {
|
|
status stepResultStatus
|
|
time time.Time
|
|
err error
|
|
|
|
owner *messages.Pickle
|
|
step *messages.Pickle_PickleStep
|
|
def *StepDefinition
|
|
}
|
|
|
|
func newStepResult(pickle *messages.Pickle, step *messages.Pickle_PickleStep, match *StepDefinition) *stepResult {
|
|
return &stepResult{time: timeNowFunc(), owner: pickle, step: step, def: match}
|
|
}
|
|
|
|
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
|
|
|
|
firstFeature *bool
|
|
lock *sync.Mutex
|
|
}
|
|
|
|
func (f *basefmt) lastFeature() *feature {
|
|
return f.features[len(f.features)-1]
|
|
}
|
|
|
|
func (f *basefmt) lastStepResult() *stepResult {
|
|
return f.lastFeature().lastStepResult()
|
|
}
|
|
|
|
func (f *basefmt) findFeature(scenarioAstID string) *feature {
|
|
for _, ft := range f.features {
|
|
if sc := ft.findScenario(scenarioAstID); sc != nil {
|
|
return ft
|
|
}
|
|
}
|
|
|
|
panic("Couldn't find scenario for AST ID: " + scenarioAstID)
|
|
}
|
|
|
|
func (f *basefmt) findScenario(scenarioAstID string) *messages.GherkinDocument_Feature_Scenario {
|
|
for _, ft := range f.features {
|
|
if sc := ft.findScenario(scenarioAstID); sc != nil {
|
|
return sc
|
|
}
|
|
}
|
|
|
|
panic("Couldn't find scenario for AST ID: " + scenarioAstID)
|
|
}
|
|
|
|
func (f *basefmt) findBackground(scenarioAstID string) *messages.GherkinDocument_Feature_Background {
|
|
for _, ft := range f.features {
|
|
if bg := ft.findBackground(scenarioAstID); bg != nil {
|
|
return bg
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (f *basefmt) findExample(exampleAstID string) (*messages.GherkinDocument_Feature_Scenario_Examples, *messages.GherkinDocument_Feature_TableRow) {
|
|
for _, ft := range f.features {
|
|
if es, rs := ft.findExample(exampleAstID); es != nil && rs != nil {
|
|
return es, rs
|
|
}
|
|
}
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
func (f *basefmt) findStep(stepAstID string) *messages.GherkinDocument_Feature_Step {
|
|
for _, ft := range f.features {
|
|
if st := ft.findStep(stepAstID); st != nil {
|
|
return st
|
|
}
|
|
}
|
|
|
|
panic("Couldn't find step for AST ID: " + stepAstID)
|
|
}
|
|
|
|
func (f *basefmt) TestRunStarted() {
|
|
f.lock.Lock()
|
|
defer f.lock.Unlock()
|
|
|
|
firstFeature := true
|
|
f.firstFeature = &firstFeature
|
|
}
|
|
|
|
func (f *basefmt) Pickle(p *messages.Pickle) {
|
|
f.lock.Lock()
|
|
defer f.lock.Unlock()
|
|
|
|
feature := f.features[len(f.features)-1]
|
|
|
|
pr := pickleResult{name: p.Name, astNodeIDs: p.AstNodeIds, time: timeNowFunc()}
|
|
feature.pickleResults = append(feature.pickleResults, &pr)
|
|
}
|
|
|
|
func (f *basefmt) Defined(*messages.Pickle, *messages.Pickle_PickleStep, *StepDefinition) {}
|
|
|
|
func (f *basefmt) Feature(ft *messages.GherkinDocument, p string, c []byte) {
|
|
f.lock.Lock()
|
|
defer f.lock.Unlock()
|
|
|
|
*f.firstFeature = false
|
|
|
|
f.features = append(f.features, &feature{path: p, GherkinDocument: ft, time: timeNowFunc()})
|
|
}
|
|
|
|
func (f *basefmt) Passed(pickle *messages.Pickle, step *messages.Pickle_PickleStep, match *StepDefinition) {
|
|
f.lock.Lock()
|
|
defer f.lock.Unlock()
|
|
|
|
s := newStepResult(pickle, step, match)
|
|
s.status = passed
|
|
f.lastFeature().appendStepResult(s)
|
|
}
|
|
|
|
func (f *basefmt) Skipped(pickle *messages.Pickle, step *messages.Pickle_PickleStep, match *StepDefinition) {
|
|
f.lock.Lock()
|
|
defer f.lock.Unlock()
|
|
|
|
s := newStepResult(pickle, step, match)
|
|
s.status = skipped
|
|
f.lastFeature().appendStepResult(s)
|
|
}
|
|
|
|
func (f *basefmt) Undefined(pickle *messages.Pickle, step *messages.Pickle_PickleStep, match *StepDefinition) {
|
|
f.lock.Lock()
|
|
defer f.lock.Unlock()
|
|
|
|
s := newStepResult(pickle, step, match)
|
|
s.status = undefined
|
|
f.lastFeature().appendStepResult(s)
|
|
}
|
|
|
|
func (f *basefmt) Failed(pickle *messages.Pickle, step *messages.Pickle_PickleStep, match *StepDefinition, err error) {
|
|
f.lock.Lock()
|
|
defer f.lock.Unlock()
|
|
|
|
s := newStepResult(pickle, step, match)
|
|
s.status = failed
|
|
s.err = err
|
|
f.lastFeature().appendStepResult(s)
|
|
}
|
|
|
|
func (f *basefmt) Pending(pickle *messages.Pickle, step *messages.Pickle_PickleStep, match *StepDefinition) {
|
|
f.lock.Lock()
|
|
defer f.lock.Unlock()
|
|
|
|
s := newStepResult(pickle, step, match)
|
|
s.status = pending
|
|
f.lastFeature().appendStepResult(s)
|
|
}
|
|
|
|
func (f *basefmt) Summary() {
|
|
var totalSc, passedSc, undefinedSc int
|
|
var totalSt, passedSt, failedSt, skippedSt, pendingSt, undefinedSt int
|
|
|
|
for _, feat := range f.features {
|
|
for _, pr := range feat.pickleResults {
|
|
var prStatus stepResultStatus
|
|
totalSc++
|
|
|
|
if len(pr.stepResults) == 0 {
|
|
prStatus = undefined
|
|
}
|
|
|
|
for _, sr := range pr.stepResults {
|
|
totalSt++
|
|
|
|
switch sr.status {
|
|
case passed:
|
|
prStatus = passed
|
|
passedSt++
|
|
case failed:
|
|
prStatus = failed
|
|
failedSt++
|
|
case skipped:
|
|
skippedSt++
|
|
case undefined:
|
|
prStatus = undefined
|
|
undefinedSt++
|
|
case pending:
|
|
prStatus = pending
|
|
pendingSt++
|
|
}
|
|
}
|
|
|
|
if prStatus == passed {
|
|
passedSc++
|
|
} else if prStatus == undefined {
|
|
undefinedSc++
|
|
}
|
|
}
|
|
}
|
|
|
|
var steps, parts, scenarios []string
|
|
if passedSt > 0 {
|
|
steps = append(steps, green(fmt.Sprintf("%d passed", passedSt)))
|
|
}
|
|
if failedSt > 0 {
|
|
parts = append(parts, red(fmt.Sprintf("%d failed", failedSt)))
|
|
steps = append(steps, red(fmt.Sprintf("%d failed", failedSt)))
|
|
}
|
|
if pendingSt > 0 {
|
|
parts = append(parts, yellow(fmt.Sprintf("%d pending", pendingSt)))
|
|
steps = append(steps, yellow(fmt.Sprintf("%d pending", pendingSt)))
|
|
}
|
|
if undefinedSt > 0 {
|
|
parts = append(parts, yellow(fmt.Sprintf("%d undefined", undefinedSc)))
|
|
steps = append(steps, yellow(fmt.Sprintf("%d undefined", undefinedSt)))
|
|
} else if undefinedSc > 0 {
|
|
// there may be some scenarios without steps
|
|
parts = append(parts, yellow(fmt.Sprintf("%d undefined", undefinedSc)))
|
|
}
|
|
if skippedSt > 0 {
|
|
steps = append(steps, cyan(fmt.Sprintf("%d skipped", skippedSt)))
|
|
}
|
|
if passedSc > 0 {
|
|
scenarios = append(scenarios, green(fmt.Sprintf("%d passed", passedSc)))
|
|
}
|
|
scenarios = append(scenarios, parts...)
|
|
elapsed := timeNowFunc().Sub(f.started)
|
|
|
|
fmt.Fprintln(f.out, "")
|
|
|
|
if totalSc == 0 {
|
|
fmt.Fprintln(f.out, "No scenarios")
|
|
} else {
|
|
fmt.Fprintln(f.out, fmt.Sprintf("%d scenarios (%s)", totalSc, strings.Join(scenarios, ", ")))
|
|
}
|
|
|
|
if totalSt == 0 {
|
|
fmt.Fprintln(f.out, "No steps")
|
|
} else {
|
|
fmt.Fprintln(f.out, fmt.Sprintf("%d steps (%s)", totalSt, 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
|
|
f.firstFeature = source.firstFeature
|
|
}
|
|
}
|
|
|
|
func (f *basefmt) Copy(cf ConcurrentFormatter) {
|
|
if source, ok := cf.(*basefmt); ok {
|
|
for _, v := range source.features {
|
|
f.features = append(f.features, v)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (f *basefmt) findStepResults(status stepResultStatus) (res []*stepResult) {
|
|
for _, feat := range f.features {
|
|
for _, pr := range feat.pickleResults {
|
|
for _, sr := range pr.stepResults {
|
|
if sr.status == status {
|
|
res = append(res, sr)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func (f *basefmt) snippets() string {
|
|
undefinedStepResults := f.findStepResults(undefined)
|
|
if len(undefinedStepResults) == 0 {
|
|
return ""
|
|
}
|
|
|
|
var index int
|
|
var snips []undefinedSnippet
|
|
// build snippets
|
|
for _, u := range undefinedStepResults {
|
|
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("StepDefinitioninition%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})
|
|
}
|
|
}
|
|
}
|
|
|
|
sort.Sort(snippetSortByMethod(snips))
|
|
|
|
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 isLastStep(pickle *messages.Pickle, step *messages.Pickle_PickleStep) bool {
|
|
return pickle.Steps[len(pickle.Steps)-1].Id == step.Id
|
|
}
|