refactor suite runner to support concurrent model

* ef86715 add a concurrency flag
* 8674a58 run suite concurrently, closes #3
Этот коммит содержится в:
gedi 2015-07-03 14:49:31 +03:00
родитель 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'

Просмотреть файл

@ -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

Просмотреть файл

@ -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)
}

Просмотреть файл

@ -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
Просмотреть файл

@ -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 Обычный файл
Просмотреть файл

@ -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)
}
}

Просмотреть файл

@ -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)
}
}