mirror of
https://github.com/weaveworks/scope.git
synced 2026-03-04 18:51:17 +00:00
420 lines
8.7 KiB
Go
420 lines
8.7 KiB
Go
package gucumber
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"runtime"
|
|
"strings"
|
|
|
|
"github.com/lsegal/gucumber/gherkin"
|
|
"github.com/shiena/ansicolor"
|
|
)
|
|
|
|
const (
|
|
clrWhite = "0"
|
|
clrRed = "31"
|
|
clrGreen = "32"
|
|
clrYellow = "33"
|
|
clrCyan = "36"
|
|
|
|
txtUnmatchInt = `(\d+)`
|
|
txtUnmatchFloat = `(-?\d+(?:\.\d+)?)`
|
|
txtUnmatchStr = `"(.+?)"`
|
|
)
|
|
|
|
var (
|
|
reUnmatchInt = regexp.MustCompile(txtUnmatchInt)
|
|
reUnmatchFloat = regexp.MustCompile(txtUnmatchFloat)
|
|
reUnmatchStr = regexp.MustCompile(`(<|").+?("|>)`)
|
|
reOutlineVal = regexp.MustCompile(`<(.+?)>`)
|
|
)
|
|
|
|
type Runner struct {
|
|
*Context
|
|
Features []*gherkin.Feature
|
|
Results []RunnerResult
|
|
Unmatched []*gherkin.Step
|
|
FailCount int
|
|
SkipCount int
|
|
}
|
|
|
|
type RunnerResult struct {
|
|
*TestingT
|
|
*gherkin.Feature
|
|
*gherkin.Scenario
|
|
}
|
|
|
|
func (c *Context) RunDir(dir string) (*Runner, error) {
|
|
g, _ := filepath.Glob(filepath.Join(dir, "*.feature"))
|
|
g2, _ := filepath.Glob(filepath.Join(dir, "**", "*.feature"))
|
|
g = append(g, g2...)
|
|
|
|
runner, err := c.RunFiles(g)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
if len(runner.Unmatched) > 0 {
|
|
fmt.Println("Some steps were missing, you can add them by using the following step definition stubs: ")
|
|
fmt.Println("")
|
|
fmt.Print(runner.MissingMatcherStubs())
|
|
}
|
|
|
|
os.Exit(runner.FailCount)
|
|
return runner, err
|
|
}
|
|
|
|
func (c *Context) RunFiles(featureFiles []string) (*Runner, error) {
|
|
r := Runner{
|
|
Context: c,
|
|
Features: []*gherkin.Feature{},
|
|
Results: []RunnerResult{},
|
|
Unmatched: []*gherkin.Step{},
|
|
}
|
|
|
|
for _, file := range featureFiles {
|
|
fd, err := os.Open(file)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer fd.Close()
|
|
|
|
b, err := ioutil.ReadAll(fd)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
fs, err := gherkin.ParseFilename(string(b), file)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, f := range fs {
|
|
r.Features = append(r.Features, &f)
|
|
}
|
|
}
|
|
|
|
r.run()
|
|
return &r, nil
|
|
}
|
|
|
|
func (c *Runner) MissingMatcherStubs() string {
|
|
var buf bytes.Buffer
|
|
matches := map[string]bool{}
|
|
|
|
buf.WriteString(`import . "github.com/lsegal/gucumber"` + "\n\n")
|
|
buf.WriteString("func init() {\n")
|
|
|
|
for _, m := range c.Unmatched {
|
|
numInts, numFloats, numStrs := 1, 1, 1
|
|
str, args := m.Text, []string{}
|
|
str = reUnmatchInt.ReplaceAllStringFunc(str, func(s string) string {
|
|
args = append(args, fmt.Sprintf("i%d int", numInts))
|
|
numInts++
|
|
return txtUnmatchInt
|
|
})
|
|
str = reUnmatchFloat.ReplaceAllStringFunc(str, func(s string) string {
|
|
args = append(args, fmt.Sprintf("s%d float64", numFloats))
|
|
numFloats++
|
|
return txtUnmatchFloat
|
|
})
|
|
str = reUnmatchStr.ReplaceAllStringFunc(str, func(s string) string {
|
|
args = append(args, fmt.Sprintf("s%d string", numStrs))
|
|
numStrs++
|
|
return txtUnmatchStr
|
|
})
|
|
|
|
if len(m.Argument) > 0 {
|
|
if m.Argument.IsTabular() {
|
|
args = append(args, "table [][]string")
|
|
} else {
|
|
args = append(args, "data string")
|
|
}
|
|
}
|
|
|
|
// Don't duplicate matchers. This is mostly for scenario outlines.
|
|
if matches[str] {
|
|
continue
|
|
}
|
|
matches[str] = true
|
|
|
|
fmt.Fprintf(&buf, "\t%s(`^%s$`, func(%s) {\n\t\tT.Skip() // pending\n\t})\n\n",
|
|
m.Type, str, strings.Join(args, ", "))
|
|
}
|
|
|
|
buf.WriteString("}\n")
|
|
return buf.String()
|
|
}
|
|
|
|
func (c *Runner) run() {
|
|
if c.BeforeAllFilter != nil {
|
|
c.BeforeAllFilter()
|
|
}
|
|
for _, f := range c.Features {
|
|
c.runFeature(f)
|
|
}
|
|
if c.AfterAllFilter != nil {
|
|
c.AfterAllFilter()
|
|
}
|
|
|
|
c.line("0;1", "Finished (%d passed, %d failed, %d skipped).\n",
|
|
len(c.Results)-c.FailCount-c.SkipCount, c.FailCount, c.SkipCount)
|
|
}
|
|
|
|
func (c *Runner) runFeature(f *gherkin.Feature) {
|
|
if !f.FilterMatched(c.Filters...) {
|
|
result := false
|
|
|
|
// if any scenarios match, we will run those
|
|
for _, s := range f.Scenarios {
|
|
if s.FilterMatched(f, c.Filters...) {
|
|
result = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !result {
|
|
return
|
|
}
|
|
}
|
|
|
|
for k, fn := range c.BeforeFilters {
|
|
if f.FilterMatched(strings.Split(k, "|")...) {
|
|
fn()
|
|
}
|
|
}
|
|
|
|
if len(f.Tags) > 0 {
|
|
c.line(clrCyan, strings.Join([]string(f.Tags), " "))
|
|
}
|
|
c.line("0;1", "Feature: %s", f.Title)
|
|
|
|
if f.Background.Steps != nil {
|
|
c.runScenario("Background", f, &f.Background, false)
|
|
}
|
|
|
|
for _, s := range f.Scenarios {
|
|
c.runScenario("Scenario", f, &s, false)
|
|
}
|
|
|
|
for k, fn := range c.AfterFilters {
|
|
if f.FilterMatched(strings.Split(k, "|")...) {
|
|
fn()
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *Runner) runScenario(title string, f *gherkin.Feature, s *gherkin.Scenario, isExample bool) {
|
|
if !s.FilterMatched(f, c.Filters...) {
|
|
return
|
|
}
|
|
|
|
for k, fn := range c.BeforeFilters {
|
|
if s.FilterMatched(f, strings.Split(k, "|")...) {
|
|
fn()
|
|
}
|
|
}
|
|
|
|
if s.Examples != "" { // run scenario outline data
|
|
exrows := strings.Split(string(s.Examples), "\n")
|
|
|
|
c.line(clrCyan, " "+strings.Join([]string(s.Tags), " "))
|
|
c.fileLine("0;1", " %s Outline: %s", s.Filename, s.Line, s.LongestLine()+1,
|
|
title, s.Title)
|
|
|
|
for _, step := range s.Steps {
|
|
c.fileLine("0;0", " %s %s", step.Filename, step.Line,
|
|
s.LongestLine()+1, step.Type, step.Text)
|
|
}
|
|
|
|
c.line(clrWhite, "")
|
|
c.line("0;1", " Examples:")
|
|
c.line(clrCyan, " %s", exrows[0])
|
|
|
|
tab := s.Examples.ToTable()
|
|
tabmap := tab.ToMap()
|
|
for i, rows := 1, len(tab); i < rows; i++ {
|
|
other := gherkin.Scenario{
|
|
Filename: s.Filename,
|
|
Line: s.Line,
|
|
Title: s.Title,
|
|
Examples: gherkin.StringData(""),
|
|
Steps: []gherkin.Step{},
|
|
}
|
|
|
|
for _, step := range s.Steps {
|
|
step.Text = reOutlineVal.ReplaceAllStringFunc(step.Text, func(t string) string {
|
|
return tabmap[t[1:len(t)-1]][i-1]
|
|
})
|
|
other.Steps = append(other.Steps, step)
|
|
}
|
|
|
|
fc := c.FailCount
|
|
clr := clrGreen
|
|
c.runScenario(title, f, &other, true)
|
|
|
|
if fc != c.FailCount {
|
|
clr = clrRed
|
|
}
|
|
|
|
c.line(clr, " %s", exrows[i])
|
|
}
|
|
c.line(clrWhite, "")
|
|
|
|
for k, fn := range c.AfterFilters {
|
|
if s.FilterMatched(f, strings.Split(k, "|")...) {
|
|
fn()
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
t := &TestingT{}
|
|
skipping := false
|
|
clr := clrGreen
|
|
|
|
if !isExample {
|
|
if len(s.Tags) > 0 {
|
|
c.line(clrCyan, " "+strings.Join([]string(s.Tags), " "))
|
|
}
|
|
c.fileLine("0;1", " %s: %s", s.Filename, s.Line, s.LongestLine(),
|
|
title, s.Title)
|
|
}
|
|
|
|
for _, step := range s.Steps {
|
|
errCount := len(t.errors)
|
|
found := false
|
|
if !skipping && !t.Failed() {
|
|
done := make(chan bool)
|
|
go func() {
|
|
defer func() {
|
|
c.Results = append(c.Results, RunnerResult{t, f, s})
|
|
|
|
if t.Skipped() {
|
|
c.SkipCount++
|
|
skipping = true
|
|
clr = clrYellow
|
|
} else if t.Failed() {
|
|
c.FailCount++
|
|
clr = clrRed
|
|
}
|
|
|
|
done <- true
|
|
}()
|
|
|
|
f, err := c.Execute(t, step.Text, string(step.Argument))
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
found = f
|
|
|
|
if !f {
|
|
t.Skip("no match function for step")
|
|
}
|
|
}()
|
|
<-done
|
|
}
|
|
|
|
if skipping && !found {
|
|
cstep := step
|
|
c.Unmatched = append(c.Unmatched, &cstep)
|
|
}
|
|
|
|
if !isExample {
|
|
c.fileLine(clr, " %s %s", step.Filename, step.Line,
|
|
s.LongestLine(), step.Type, step.Text)
|
|
if len(step.Argument) > 0 {
|
|
if !step.Argument.IsTabular() {
|
|
c.line(clrWhite, ` """`)
|
|
}
|
|
for _, l := range strings.Split(string(step.Argument), "\n") {
|
|
c.line(clrWhite, " %s", l)
|
|
}
|
|
if !step.Argument.IsTabular() {
|
|
c.line(clrWhite, ` """`)
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(t.errors) > errCount {
|
|
c.line(clrRed, "\n"+t.errors[len(t.errors)-1].message)
|
|
}
|
|
}
|
|
if !isExample {
|
|
c.line(clrWhite, "")
|
|
}
|
|
|
|
for k, fn := range c.AfterFilters {
|
|
if s.FilterMatched(f, strings.Split(k, "|")...) {
|
|
fn()
|
|
}
|
|
}
|
|
}
|
|
|
|
var writer = ansicolor.NewAnsiColorWriter(os.Stdout)
|
|
|
|
func (c *Runner) line(clr, text string, args ...interface{}) {
|
|
fmt.Fprintf(writer, "\033[%sm%s\033[0;0m\n", clr, fmt.Sprintf(text, args...))
|
|
}
|
|
|
|
func (c *Runner) fileLine(clr, text, filename string, line int, max int, args ...interface{}) {
|
|
space, str := "", fmt.Sprintf(text, args...)
|
|
if l := max + 5 - len(str); l > 0 {
|
|
space = strings.Repeat(" ", l)
|
|
}
|
|
comment := fmt.Sprintf("%s \033[39;0m# %s:%d", space, filename, line)
|
|
c.line(clr, "%s%s", str, comment)
|
|
}
|
|
|
|
type Tester interface {
|
|
Errorf(format string, args ...interface{})
|
|
Skip(args ...interface{})
|
|
}
|
|
|
|
type TestingT struct {
|
|
skipped bool
|
|
errors []TestError
|
|
}
|
|
|
|
type TestError struct {
|
|
message string
|
|
stack []byte
|
|
}
|
|
|
|
func (t *TestingT) Errorf(format string, args ...interface{}) {
|
|
var buf bytes.Buffer
|
|
|
|
str := fmt.Sprintf(format, args...)
|
|
sbuf := make([]byte, 8192)
|
|
for {
|
|
size := runtime.Stack(sbuf, false)
|
|
if size < len(sbuf) {
|
|
break
|
|
}
|
|
buf.Write(sbuf[0:size])
|
|
}
|
|
|
|
t.errors = append(t.errors, TestError{message: str, stack: buf.Bytes()})
|
|
}
|
|
|
|
func (t *TestingT) Skip(args ...interface{}) {
|
|
t.skipped = true
|
|
}
|
|
|
|
func (t *TestingT) Skipped() bool {
|
|
return t.skipped
|
|
}
|
|
|
|
func (t *TestingT) Failed() bool {
|
|
return len(t.errors) > 0
|
|
}
|
|
|
|
func (t *TestingT) Error(err error) {
|
|
t.errors = append(t.errors, TestError{message: err.Error()})
|
|
}
|