refactor suite runner to support concurrent model
* ef86715 add a concurrency flag * 8674a58 run suite concurrently, closes #3
Этот коммит содержится в:
родитель
ca36316b7a
коммит
970eddc16a
14 изменённых файлов: 204 добавлений и 102 удалений
|
@ -21,7 +21,7 @@ script:
|
|||
- go test -race
|
||||
|
||||
# run features
|
||||
- go run cmd/godog/main.go -f progress
|
||||
- go run cmd/godog/main.go --format=progress --concurrency=4
|
||||
|
||||
# code correctness
|
||||
- sh -c 'RES="$(go fmt ./...)"; if [ ! -z "$RES" ]; then echo $RES; exit 1; fi'
|
||||
|
|
2
Makefile
2
Makefile
|
@ -6,7 +6,7 @@ test:
|
|||
@golint ./...
|
||||
go vet ./...
|
||||
go test
|
||||
go run cmd/godog/main.go -f progress
|
||||
go run cmd/godog/main.go -f progress -c 4
|
||||
|
||||
deps:
|
||||
@echo "updating all dependencies"
|
||||
|
|
|
@ -71,6 +71,7 @@ We only need a number of **godogs** for now. Lets define steps.
|
|||
/* file: examples/godogs/godog.go */
|
||||
package main
|
||||
|
||||
// Godogs to eat
|
||||
var Godogs int
|
||||
|
||||
func main() { /* usual main func */ }
|
||||
|
@ -146,6 +147,9 @@ See implementation examples:
|
|||
- changed **godog.Suite** from interface to struct. Context registration should be updated accordingly. The reason
|
||||
for change: since it exports the same methods and there is no need to mock a function in tests, there is no
|
||||
obvious reason to keep an interface.
|
||||
- in order to support running suite concurrently, needed to refactor an entry point of application. The **Run** method
|
||||
now is a func of godog package which initializes and run the suite (or more suites). Method **New** is removed. This
|
||||
change made godog a little cleaner.
|
||||
|
||||
### FAQ
|
||||
|
||||
|
|
11
builder.go
11
builder.go
|
@ -49,11 +49,12 @@ func newBuilder(buildPath string) (*builder, error) {
|
|||
){{ end }}
|
||||
|
||||
func main() {
|
||||
suite := {{ if not .Internal }}godog.{{ end }}New()
|
||||
{{range .Contexts}}
|
||||
{{ . }}(suite)
|
||||
{{end}}
|
||||
suite.Run()
|
||||
|
||||
{{ if not .Internal }}godog.{{ end }}Run(func (suite *{{ if not .Internal }}godog.{{ end }}Suite) {
|
||||
{{range .Contexts}}
|
||||
{{ . }}(suite)
|
||||
{{end}}
|
||||
})
|
||||
}`)),
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"time"
|
||||
|
||||
"github.com/DATA-DOG/godog"
|
||||
"github.com/shiena/ansicolor"
|
||||
|
@ -12,7 +14,7 @@ func buildAndRun() error {
|
|||
// will support Ansi colors for windows
|
||||
stdout := ansicolor.NewAnsiColorWriter(os.Stdout)
|
||||
|
||||
builtFile := "/tmp/bgodog.go"
|
||||
builtFile := fmt.Sprintf("%s/%dgodog.go", os.TempDir(), time.Now().UnixNano())
|
||||
// @TODO: then there is a suite error or panic, it may
|
||||
// be interesting to see the built file. But we
|
||||
// even cannot determine the status of exit error
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package main
|
||||
|
||||
// Godogs to eat
|
||||
var Godogs int
|
||||
|
||||
func main() { /* usual main func */ }
|
||||
|
|
|
@ -28,7 +28,7 @@ Feature: undefined step snippets
|
|||
return godog.ErrPending
|
||||
}
|
||||
|
||||
func featureContext(s godog.Suite) {
|
||||
func featureContext(s *godog.Suite) {
|
||||
s.Step(`^I send "([^"]*)" request to "([^"]*)"$`, iSendrequestTo)
|
||||
s.Step(`^the response code should be (\d+)$`, theResponseCodeShouldBe)
|
||||
}
|
||||
|
@ -56,7 +56,7 @@ Feature: undefined step snippets
|
|||
return godog.ErrPending
|
||||
}
|
||||
|
||||
func featureContext(s godog.Suite) {
|
||||
func featureContext(s *godog.Suite) {
|
||||
s.Step(`^I send "([^"]*)" request to "([^"]*)" with:$`, iSendrequestTowith)
|
||||
s.Step(`^the response code should be (\d+) and header "([^"]*)" should be "([^"]*)"$`, theResponseCodeShouldBeAndHeadershouldBe)
|
||||
}
|
||||
|
|
29
flags.go
29
flags.go
|
@ -5,18 +5,18 @@ import (
|
|||
"fmt"
|
||||
)
|
||||
|
||||
// Flags builds a *flag.FlagSet with all flags
|
||||
// required for the godog suite
|
||||
func flags(s *Suite) *flag.FlagSet {
|
||||
func flags(format, tags *string, defs, sof, vers *bool, cl *int) *flag.FlagSet {
|
||||
set := flag.NewFlagSet("godog", flag.ExitOnError)
|
||||
set.StringVar(&s.format, "format", "pretty", "")
|
||||
set.StringVar(&s.format, "f", "pretty", "")
|
||||
set.StringVar(&s.tags, "tags", "", "")
|
||||
set.StringVar(&s.tags, "t", "", "")
|
||||
set.BoolVar(&s.definitions, "definitions", false, "")
|
||||
set.BoolVar(&s.definitions, "d", false, "")
|
||||
set.BoolVar(&s.stopOnFailure, "stop-on-failure", false, "")
|
||||
set.BoolVar(&s.version, "version", false, "")
|
||||
set.StringVar(format, "format", "pretty", "")
|
||||
set.StringVar(format, "f", "pretty", "")
|
||||
set.StringVar(tags, "tags", "", "")
|
||||
set.StringVar(tags, "t", "", "")
|
||||
set.IntVar(cl, "concurrency", 1, "")
|
||||
set.IntVar(cl, "c", 1, "")
|
||||
set.BoolVar(defs, "definitions", false, "")
|
||||
set.BoolVar(defs, "d", false, "")
|
||||
set.BoolVar(sof, "stop-on-failure", false, "")
|
||||
set.BoolVar(vers, "version", false, "")
|
||||
set.Usage = usage
|
||||
return set
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ func usage() {
|
|||
if len(name) > 0 {
|
||||
name += ":"
|
||||
}
|
||||
return s(2) + cl(name, green) + s(30-len(name)) + desc
|
||||
return s(2) + cl(name, green) + s(22-len(name)) + desc
|
||||
}
|
||||
|
||||
// --- GENERAL ---
|
||||
|
@ -48,6 +48,11 @@ func usage() {
|
|||
fmt.Println(cl("Options:", yellow))
|
||||
// --> step definitions
|
||||
fmt.Println(opt("-d, --definitions", "Print all available step definitions."))
|
||||
// --> concurrency
|
||||
fmt.Println(opt("-c, --concurrency=1", "Run the test suite with concurrency level:"))
|
||||
fmt.Println(opt("", s(4)+"- "+cl(`= 1`, yellow)+": supports all types of formats."))
|
||||
fmt.Println(opt("", s(4)+"- "+cl(`>= 2`, yellow)+": only supports "+cl("progress", yellow)+". Note, that"))
|
||||
fmt.Println(opt("", s(4)+"your context needs to support parallel execution."))
|
||||
// --> format
|
||||
fmt.Println(opt("-f, --format=pretty", "How to format tests output. Available formats:"))
|
||||
for _, f := range formatters {
|
||||
|
|
16
fmt.go
16
fmt.go
|
@ -49,6 +49,22 @@ type registeredFormatter struct {
|
|||
|
||||
var formatters []*registeredFormatter
|
||||
|
||||
func findFmt(format string) (f Formatter, err error) {
|
||||
var names []string
|
||||
for _, el := range formatters {
|
||||
if el.name == format {
|
||||
f = el.fmt
|
||||
break
|
||||
}
|
||||
names = append(names, el.name)
|
||||
}
|
||||
|
||||
if f == nil {
|
||||
err = fmt.Errorf(`unregistered formatter name: "%s", use one of: %s`, format, strings.Join(names, ", "))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// RegisterFormatter registers a feature suite output
|
||||
// Formatter as the name and descriptiongiven.
|
||||
// Formatter is used to represent suite output
|
||||
|
|
|
@ -3,6 +3,7 @@ package godog
|
|||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/cucumber/gherkin-go"
|
||||
|
@ -20,10 +21,23 @@ func init() {
|
|||
|
||||
type progress struct {
|
||||
basefmt
|
||||
sync.Mutex
|
||||
stepsPerRow int
|
||||
steps int
|
||||
}
|
||||
|
||||
func (f *progress) Node(n interface{}) {
|
||||
f.Lock()
|
||||
defer f.Unlock()
|
||||
f.basefmt.Node(n)
|
||||
}
|
||||
|
||||
func (f *progress) Feature(ft *gherkin.Feature, p string) {
|
||||
f.Lock()
|
||||
defer f.Unlock()
|
||||
f.basefmt.Feature(ft, p)
|
||||
}
|
||||
|
||||
func (f *progress) Summary() {
|
||||
left := math.Mod(float64(f.steps), float64(f.stepsPerRow))
|
||||
if left != 0 {
|
||||
|
@ -65,26 +79,36 @@ func (f *progress) step(res *stepResult) {
|
|||
}
|
||||
|
||||
func (f *progress) Passed(step *gherkin.Step, match *StepDef) {
|
||||
f.Lock()
|
||||
defer f.Unlock()
|
||||
f.basefmt.Passed(step, match)
|
||||
f.step(f.passed[len(f.passed)-1])
|
||||
}
|
||||
|
||||
func (f *progress) Skipped(step *gherkin.Step) {
|
||||
f.Lock()
|
||||
defer f.Unlock()
|
||||
f.basefmt.Skipped(step)
|
||||
f.step(f.skipped[len(f.skipped)-1])
|
||||
}
|
||||
|
||||
func (f *progress) Undefined(step *gherkin.Step) {
|
||||
f.Lock()
|
||||
defer f.Unlock()
|
||||
f.basefmt.Undefined(step)
|
||||
f.step(f.undefined[len(f.undefined)-1])
|
||||
}
|
||||
|
||||
func (f *progress) Failed(step *gherkin.Step, match *StepDef, err error) {
|
||||
f.Lock()
|
||||
defer f.Unlock()
|
||||
f.basefmt.Failed(step, match, err)
|
||||
f.step(f.failed[len(f.failed)-1])
|
||||
}
|
||||
|
||||
func (f *progress) Pending(step *gherkin.Step, match *StepDef) {
|
||||
f.Lock()
|
||||
defer f.Unlock()
|
||||
f.basefmt.Pending(step, match)
|
||||
f.step(f.pending[len(f.pending)-1])
|
||||
}
|
||||
|
|
104
run.go
Обычный файл
104
run.go
Обычный файл
|
@ -0,0 +1,104 @@
|
|||
package godog
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type initializer func(*Suite)
|
||||
|
||||
type runner struct {
|
||||
sync.WaitGroup
|
||||
|
||||
semaphore chan int
|
||||
features []*feature
|
||||
fmt Formatter // needs to support concurrency
|
||||
initializer initializer
|
||||
}
|
||||
|
||||
func (r *runner) run() (failed bool) {
|
||||
r.Add(len(r.features))
|
||||
for _, ft := range r.features {
|
||||
go func(fail *bool, feat *feature) {
|
||||
r.semaphore <- 1
|
||||
suite := &Suite{
|
||||
fmt: r.fmt,
|
||||
features: []*feature{feat},
|
||||
}
|
||||
r.initializer(suite)
|
||||
suite.run()
|
||||
if suite.failed {
|
||||
*fail = true
|
||||
}
|
||||
<-r.semaphore
|
||||
r.Done()
|
||||
}(&failed, ft)
|
||||
}
|
||||
r.Wait()
|
||||
|
||||
r.fmt.Summary()
|
||||
return
|
||||
}
|
||||
|
||||
// Run creates and runs the feature suite.
|
||||
// uses contextInitializer to register contexts
|
||||
//
|
||||
// the concurrency option allows runner to
|
||||
// initialize a number of suites to be run
|
||||
// separately. Only progress formatter
|
||||
// is supported when concurrency level is
|
||||
// higher than 1
|
||||
//
|
||||
// contextInitializer must be able to register
|
||||
// the step definitions and event handlers.
|
||||
func Run(contextInitializer func(suite *Suite)) {
|
||||
var vers, defs, sof bool
|
||||
var tags, format string
|
||||
var concurrency int
|
||||
flagSet := flags(&format, &tags, &defs, &sof, &vers, &concurrency)
|
||||
err := flagSet.Parse(os.Args[1:])
|
||||
fatal(err)
|
||||
|
||||
switch {
|
||||
case vers:
|
||||
fmt.Println(cl("Godog", green) + " version is " + cl(Version, yellow))
|
||||
return
|
||||
case defs:
|
||||
s := &Suite{}
|
||||
contextInitializer(s)
|
||||
s.printStepDefinitions()
|
||||
return
|
||||
}
|
||||
|
||||
paths := flagSet.Args()
|
||||
if len(paths) == 0 {
|
||||
inf, err := os.Stat("features")
|
||||
if err == nil && inf.IsDir() {
|
||||
paths = []string{"features"}
|
||||
}
|
||||
}
|
||||
|
||||
if concurrency > 1 && format != "progress" {
|
||||
fatal(fmt.Errorf("when concurrency level is higher than 1, only progress format is supported"))
|
||||
}
|
||||
if concurrency > 1 && sof {
|
||||
fatal(fmt.Errorf("when concurrency level is higher than 1, cannot stop on first failure for now"))
|
||||
}
|
||||
formatter, err := findFmt(format)
|
||||
fatal(err)
|
||||
|
||||
features, err := parseFeatures(tags, paths)
|
||||
fatal(err)
|
||||
|
||||
r := runner{
|
||||
fmt: formatter,
|
||||
initializer: contextInitializer,
|
||||
semaphore: make(chan int, concurrency),
|
||||
features: features,
|
||||
}
|
||||
|
||||
if failed := r.run(); failed {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
88
suite.go
88
suite.go
|
@ -44,15 +44,8 @@ type Suite struct {
|
|||
features []*feature
|
||||
fmt Formatter
|
||||
|
||||
failed bool
|
||||
|
||||
// options
|
||||
paths []string
|
||||
format string
|
||||
tags string
|
||||
definitions bool
|
||||
failed bool
|
||||
stopOnFailure bool
|
||||
version bool
|
||||
|
||||
// suite event handlers
|
||||
beforeSuiteHandlers []func()
|
||||
|
@ -63,12 +56,6 @@ type Suite struct {
|
|||
afterSuiteHandlers []func()
|
||||
}
|
||||
|
||||
// New initializes a Suite. The instance is passed around
|
||||
// to all context initialization functions from *_test.go files.
|
||||
func New() *Suite {
|
||||
return &Suite{}
|
||||
}
|
||||
|
||||
// Step allows to register a *StepDef in Godog
|
||||
// feature suite, the definition will be applied
|
||||
// to all steps matching the given Regexp expr.
|
||||
|
@ -172,52 +159,6 @@ func (s *Suite) AfterSuite(f func()) {
|
|||
s.afterSuiteHandlers = append(s.afterSuiteHandlers, f)
|
||||
}
|
||||
|
||||
// Run starts the Godog feature suite
|
||||
func (s *Suite) Run() {
|
||||
flagSet := flags(s)
|
||||
fatal(flagSet.Parse(os.Args[1:]))
|
||||
|
||||
s.paths = flagSet.Args()
|
||||
// check the default path
|
||||
if len(s.paths) == 0 {
|
||||
inf, err := os.Stat("features")
|
||||
if err == nil && inf.IsDir() {
|
||||
s.paths = []string{"features"}
|
||||
}
|
||||
}
|
||||
// validate formatter
|
||||
var names []string
|
||||
for _, f := range formatters {
|
||||
if f.name == s.format {
|
||||
s.fmt = f.fmt
|
||||
break
|
||||
}
|
||||
names = append(names, f.name)
|
||||
}
|
||||
|
||||
if s.fmt == nil {
|
||||
fatal(fmt.Errorf(`unregistered formatter name: "%s", use one of: %s`, s.format, strings.Join(names, ", ")))
|
||||
}
|
||||
|
||||
// check if we need to just show something first
|
||||
switch {
|
||||
case s.version:
|
||||
fmt.Println(cl("Godog", green) + " version is " + cl(Version, yellow))
|
||||
return
|
||||
case s.definitions:
|
||||
s.printStepDefinitions()
|
||||
return
|
||||
}
|
||||
|
||||
fatal(s.parseFeatures())
|
||||
// run a feature suite
|
||||
s.run()
|
||||
|
||||
if s.failed {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Suite) run() {
|
||||
// run before suite handlers
|
||||
for _, f := range s.beforeSuiteHandlers {
|
||||
|
@ -235,7 +176,6 @@ func (s *Suite) run() {
|
|||
for _, f := range s.afterSuiteHandlers {
|
||||
f()
|
||||
}
|
||||
s.fmt.Summary()
|
||||
}
|
||||
|
||||
func (s *Suite) matchStep(step *gherkin.Step) *StepDef {
|
||||
|
@ -434,8 +374,8 @@ func (s *Suite) printStepDefinitions() {
|
|||
}
|
||||
}
|
||||
|
||||
func (s *Suite) parseFeatures() (err error) {
|
||||
for _, pat := range s.paths {
|
||||
func parseFeatures(filter string, paths []string) (features []*feature, err error) {
|
||||
for _, pat := range paths {
|
||||
// check if line number is specified
|
||||
parts := strings.Split(pat, ":")
|
||||
path := parts[0]
|
||||
|
@ -443,7 +383,7 @@ func (s *Suite) parseFeatures() (err error) {
|
|||
if len(parts) > 1 {
|
||||
line, err = strconv.Atoi(parts[1])
|
||||
if err != nil {
|
||||
return fmt.Errorf("line number should follow after colon path delimiter")
|
||||
return features, fmt.Errorf("line number should follow after colon path delimiter")
|
||||
}
|
||||
}
|
||||
// parse features
|
||||
|
@ -458,7 +398,7 @@ func (s *Suite) parseFeatures() (err error) {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.features = append(s.features, &feature{Path: p, Feature: ft})
|
||||
features = append(features, &feature{Path: p, Feature: ft})
|
||||
// filter scenario by line number
|
||||
if line != -1 {
|
||||
var scenarios []interface{}
|
||||
|
@ -477,31 +417,31 @@ func (s *Suite) parseFeatures() (err error) {
|
|||
}
|
||||
ft.ScenarioDefinitions = scenarios
|
||||
}
|
||||
s.applyTagFilter(ft)
|
||||
applyTagFilter(filter, ft)
|
||||
}
|
||||
return err
|
||||
})
|
||||
// check error
|
||||
switch {
|
||||
case os.IsNotExist(err):
|
||||
return fmt.Errorf(`feature path "%s" is not available`, path)
|
||||
return features, fmt.Errorf(`feature path "%s" is not available`, path)
|
||||
case os.IsPermission(err):
|
||||
return fmt.Errorf(`feature path "%s" is not accessible`, path)
|
||||
return features, fmt.Errorf(`feature path "%s" is not accessible`, path)
|
||||
case err != nil:
|
||||
return err
|
||||
return features, err
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *Suite) applyTagFilter(ft *gherkin.Feature) {
|
||||
if len(s.tags) == 0 {
|
||||
func applyTagFilter(tags string, ft *gherkin.Feature) {
|
||||
if len(tags) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
var scenarios []interface{}
|
||||
for _, scenario := range ft.ScenarioDefinitions {
|
||||
if s.matchesTags(allTags(ft, scenario)) {
|
||||
if matchesTags(tags, allTags(ft, scenario)) {
|
||||
scenarios = append(scenarios, scenario)
|
||||
}
|
||||
}
|
||||
|
@ -554,9 +494,9 @@ func hasTag(tags []string, tag string) bool {
|
|||
}
|
||||
|
||||
// based on http://behat.readthedocs.org/en/v2.5/guides/6.cli.html#gherkin-filters
|
||||
func (s *Suite) matchesTags(tags []string) (ok bool) {
|
||||
func matchesTags(filter string, tags []string) (ok bool) {
|
||||
ok = true
|
||||
for _, andTags := range strings.Split(s.tags, "&&") {
|
||||
for _, andTags := range strings.Split(filter, "&&") {
|
||||
var okComma bool
|
||||
for _, tag := range strings.Split(andTags, ",") {
|
||||
tag = strings.Replace(strings.TrimSpace(tag), "@", "", -1)
|
||||
|
|
|
@ -50,6 +50,7 @@ type firedEvent struct {
|
|||
}
|
||||
|
||||
type suiteContext struct {
|
||||
paths []string
|
||||
testedSuite *Suite
|
||||
events []*firedEvent
|
||||
fmt *testFormatter
|
||||
|
@ -58,6 +59,7 @@ type suiteContext struct {
|
|||
func (s *suiteContext) ResetBeforeEachScenario(interface{}) {
|
||||
// reset whole suite with the state
|
||||
s.fmt = &testFormatter{}
|
||||
s.paths = []string{}
|
||||
s.testedSuite = &Suite{fmt: s.fmt}
|
||||
// our tested suite will have the same context registered
|
||||
SuiteContext(s.testedSuite)
|
||||
|
@ -177,12 +179,17 @@ func (s *suiteContext) aFeatureFile(name string, body *gherkin.DocString) error
|
|||
}
|
||||
|
||||
func (s *suiteContext) featurePath(path string) error {
|
||||
s.testedSuite.paths = append(s.testedSuite.paths, path)
|
||||
s.paths = append(s.paths, path)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *suiteContext) parseFeatures() error {
|
||||
return s.testedSuite.parseFeatures()
|
||||
fts, err := parseFeatures("", s.paths)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.testedSuite.features = append(s.testedSuite.features, fts...)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *suiteContext) theSuiteShouldHave(state string) error {
|
||||
|
|
|
@ -5,15 +5,13 @@ import (
|
|||
)
|
||||
|
||||
func assertNotMatchesTagFilter(tags []string, filter string, t *testing.T) {
|
||||
s := &Suite{tags: filter}
|
||||
if s.matchesTags(tags) {
|
||||
if matchesTags(filter, tags) {
|
||||
t.Errorf(`expected tags: %v not to match tag filter "%s", but it did`, tags, filter)
|
||||
}
|
||||
}
|
||||
|
||||
func assertMatchesTagFilter(tags []string, filter string, t *testing.T) {
|
||||
s := &Suite{tags: filter}
|
||||
if !s.matchesTags(tags) {
|
||||
if !matchesTags(filter, tags) {
|
||||
t.Errorf(`expected tags: %v to match tag filter "%s", but it did not`, tags, filter)
|
||||
}
|
||||
}
|
||||
|
|
Загрузка…
Создание таблицы
Сослаться в новой задаче