From 1a762a8938a459fd91d96cc6491a1495d9e6980c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20L=C3=B6nnblad?= Date: Sun, 19 Jan 2020 11:10:53 -0300 Subject: [PATCH] Refactored basfmt and junitFormatter to support concurrency --- fmt.go | 48 ++++++++- fmt_junit.go | 282 ++++++++++++++++++++++++++---------------------- fmt_progress.go | 30 ++++++ go.mod | 6 ++ run.go | 53 +++------ suite.go | 32 ++++++ 6 files changed, 282 insertions(+), 169 deletions(-) diff --git a/fmt.go b/fmt.go index 6cf23f4..ea1b9ea 100644 --- a/fmt.go +++ b/fmt.go @@ -107,6 +107,14 @@ type Formatter interface { 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 @@ -156,6 +164,7 @@ type stepResult struct { feature *feature owner interface{} step *gherkin.Step + time time.Time def *StepDef err error } @@ -213,6 +222,8 @@ func (f stepResult) scenarioLine() string { } type basefmt struct { + suiteName string + out io.Writer owner interface{} indent int @@ -228,10 +239,28 @@ type basefmt struct { func (f *basefmt) Node(n interface{}) { switch t := n.(type) { - case *gherkin.TableRow: - f.owner = t case *gherkin.Scenario: f.owner = t + feature := f.features[len(f.features)-1] + feature.Scenarios = append(feature.Scenarios, &scenario{Name: t.Name}) + 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} + 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) + } } } @@ -250,8 +279,11 @@ func (f *basefmt) Passed(step *gherkin.Step, match *StepDef) { 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) { @@ -261,8 +293,11 @@ func (f *basefmt) Skipped(step *gherkin.Step, match *StepDef) { 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) { @@ -272,8 +307,11 @@ func (f *basefmt) Undefined(step *gherkin.Step, match *StepDef) { 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) { @@ -284,8 +322,11 @@ func (f *basefmt) Failed(step *gherkin.Step, match *StepDef, err error) { 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) { @@ -295,8 +336,11 @@ func (f *basefmt) Pending(step *gherkin.Step, match *StepDef) { 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() { diff --git a/fmt_junit.go b/fmt_junit.go index 0a0f27b..4f2af8c 100644 --- a/fmt_junit.go +++ b/fmt_junit.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "os" + "sync" "time" "github.com/DATA-DOG/godog/gherkin" @@ -16,149 +17,176 @@ func init() { func junitFunc(suite string, out io.Writer) Formatter { return &junitFormatter{ - suite: &junitPackageSuite{ - Name: suite, - TestSuites: make([]*junitTestSuite, 0), + basefmt: basefmt{ + suiteName: suite, + started: timeNowFunc(), + indent: 2, + out: out, }, - out: out, - started: timeNowFunc(), + lock: new(sync.Mutex), } } type junitFormatter struct { - suite *junitPackageSuite - out io.Writer - - // timing - started time.Time - caseStarted time.Time - featStarted time.Time - - outline *gherkin.ScenarioOutline - outlineExample int + basefmt + lock *sync.Mutex } -func (j *junitFormatter) Feature(feature *gherkin.Feature, path string, c []byte) { - testSuite := &junitTestSuite{ - TestCases: make([]*junitTestCase, 0), - Name: feature.Name, - } - - if len(j.suite.TestSuites) > 0 { - j.current().Time = timeNowFunc().Sub(j.featStarted).String() - } - j.featStarted = timeNowFunc() - j.suite.TestSuites = append(j.suite.TestSuites, testSuite) +func (f *junitFormatter) Node(n interface{}) { + f.lock.Lock() + defer f.lock.Unlock() + f.basefmt.Node(n) } -func (j *junitFormatter) Defined(*gherkin.Step, *StepDef) { - +func (f *junitFormatter) Feature(ft *gherkin.Feature, p string, c []byte) { + f.lock.Lock() + defer f.lock.Unlock() + f.basefmt.Feature(ft, p, c) } -func (j *junitFormatter) Node(node interface{}) { - suite := j.current() - tcase := &junitTestCase{} +func (f *junitFormatter) Summary() { + suite := buildJUNITPackageSuite(f.suiteName, f.started, f.features) - switch t := node.(type) { - case *gherkin.ScenarioOutline: - j.outline = t - j.outlineExample = 0 - return - case *gherkin.Scenario: - tcase.Name = t.Name - suite.Tests++ - j.suite.Tests++ - case *gherkin.TableRow: - j.outlineExample++ - tcase.Name = fmt.Sprintf("%s #%d", j.outline.Name, j.outlineExample) - suite.Tests++ - j.suite.Tests++ - default: - return - } - j.caseStarted = timeNowFunc() - suite.TestCases = append(suite.TestCases, tcase) -} - -func (j *junitFormatter) Failed(step *gherkin.Step, match *StepDef, err error) { - suite := j.current() - suite.Failures++ - j.suite.Failures++ - - tcase := suite.current() - tcase.Time = timeNowFunc().Sub(j.caseStarted).String() - tcase.Status = "failed" - tcase.Failure = &junitFailure{ - Message: fmt.Sprintf("%s %s: %s", step.Type, step.Text, err.Error()), - } -} - -func (j *junitFormatter) Passed(step *gherkin.Step, match *StepDef) { - suite := j.current() - - tcase := suite.current() - tcase.Time = timeNowFunc().Sub(j.caseStarted).String() - tcase.Status = "passed" -} - -func (j *junitFormatter) Skipped(step *gherkin.Step, match *StepDef) { - suite := j.current() - - tcase := suite.current() - tcase.Time = timeNowFunc().Sub(j.caseStarted).String() - tcase.Error = append(tcase.Error, &junitError{ - Type: "skipped", - Message: fmt.Sprintf("%s %s", step.Type, step.Text), - }) -} - -func (j *junitFormatter) Undefined(step *gherkin.Step, match *StepDef) { - suite := j.current() - tcase := suite.current() - if tcase.Status != "undefined" { - // do not count two undefined steps as another error - suite.Errors++ - j.suite.Errors++ - } - - tcase.Time = timeNowFunc().Sub(j.caseStarted).String() - tcase.Status = "undefined" - tcase.Error = append(tcase.Error, &junitError{ - Type: "undefined", - Message: fmt.Sprintf("%s %s", step.Type, step.Text), - }) -} - -func (j *junitFormatter) Pending(step *gherkin.Step, match *StepDef) { - suite := j.current() - suite.Errors++ - j.suite.Errors++ - - tcase := suite.current() - tcase.Time = timeNowFunc().Sub(j.caseStarted).String() - tcase.Status = "pending" - tcase.Error = append(tcase.Error, &junitError{ - Type: "pending", - Message: fmt.Sprintf("%s %s: TODO: write pending definition", step.Type, step.Text), - }) -} - -func (j *junitFormatter) Summary() { - if j.current() != nil { - j.current().Time = timeNowFunc().Sub(j.featStarted).String() - } - j.suite.Time = timeNowFunc().Sub(j.started).String() - _, err := io.WriteString(j.out, xml.Header) + _, err := io.WriteString(f.out, xml.Header) if err != nil { fmt.Fprintln(os.Stderr, "failed to write junit string:", err) } - enc := xml.NewEncoder(j.out) + + enc := xml.NewEncoder(f.out) enc.Indent("", s(2)) - if err = enc.Encode(j.suite); err != nil { + if err = enc.Encode(suite); err != nil { fmt.Fprintln(os.Stderr, "failed to write junit xml:", err) } } +func (f *junitFormatter) Passed(step *gherkin.Step, match *StepDef) { + f.lock.Lock() + defer f.lock.Unlock() + f.basefmt.Passed(step, match) +} + +func (f *junitFormatter) Skipped(step *gherkin.Step, match *StepDef) { + f.lock.Lock() + defer f.lock.Unlock() + f.basefmt.Skipped(step, match) +} + +func (f *junitFormatter) Undefined(step *gherkin.Step, match *StepDef) { + f.lock.Lock() + defer f.lock.Unlock() + f.basefmt.Undefined(step, match) +} + +func (f *junitFormatter) Failed(step *gherkin.Step, match *StepDef, err error) { + f.lock.Lock() + defer f.lock.Unlock() + f.basefmt.Failed(step, match, err) +} + +func (f *junitFormatter) Pending(step *gherkin.Step, match *StepDef) { + f.lock.Lock() + defer f.lock.Unlock() + f.basefmt.Pending(step, match) +} + +func (f *junitFormatter) Sync(cf ConcurrentFormatter) { + if source, ok := cf.(*junitFormatter); ok { + f.lock = source.lock + } +} + +func (f *junitFormatter) Copy(cf ConcurrentFormatter) { + if source, ok := cf.(*junitFormatter); 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 buildJUNITPackageSuite(suiteName string, startedAt time.Time, features []*feature) junitPackageSuite { + suite := junitPackageSuite{ + Name: suiteName, + TestSuites: make([]*junitTestSuite, len(features)), + Time: startedAt.Sub(timeNowFunc()).String(), + } + + for idx, feat := range features { + ts := junitTestSuite{ + Name: feat.Name, + Time: feat.startedAt().Sub(feat.finishedAt()).String(), + TestCases: make([]*junitTestCase, len(feat.Scenarios)), + } + + for idx, scenario := range feat.Scenarios { + tc := junitTestCase{} + tc.Name = scenario.Name + tc.Time = scenario.startedAt().Sub(scenario.finishedAt()).String() + + ts.Tests++ + suite.Tests++ + + for _, step := range scenario.Steps { + switch step.typ { + case passed: + tc.Status = passed.String() + case failed: + tc.Status = failed.String() + tc.Failure = &junitFailure{ + Message: fmt.Sprintf("%s %s: %s", step.step.Type, step.step.Text, step.err), + } + case skipped: + tc.Error = append(tc.Error, &junitError{ + Type: "skipped", + Message: fmt.Sprintf("%s %s", step.step.Type, step.step.Text), + }) + case undefined: + tc.Status = undefined.String() + tc.Error = append(tc.Error, &junitError{ + Type: "undefined", + Message: fmt.Sprintf("%s %s", step.step.Type, step.step.Text), + }) + case pending: + tc.Status = pending.String() + tc.Error = append(tc.Error, &junitError{ + Type: "pending", + Message: fmt.Sprintf("%s %s: TODO: write pending definition", step.step.Type, step.step.Text), + }) + } + } + + switch tc.Status { + case failed.String(): + ts.Failures++ + suite.Failures++ + case undefined.String(), pending.String(): + ts.Errors++ + suite.Errors++ + } + + ts.TestCases[idx] = &tc + } + + suite.TestSuites[idx] = &ts + } + + return suite +} + type junitFailure struct { Message string `xml:"message,attr"` Type string `xml:"type,attr,omitempty"` @@ -190,10 +218,6 @@ type junitTestSuite struct { TestCases []*junitTestCase } -func (ts *junitTestSuite) current() *junitTestCase { - return ts.TestCases[len(ts.TestCases)-1] -} - type junitPackageSuite struct { XMLName xml.Name `xml:"testsuites"` Name string `xml:"name,attr"` @@ -204,7 +228,3 @@ type junitPackageSuite struct { Time string `xml:"time,attr"` TestSuites []*junitTestSuite } - -func (j *junitFormatter) current() *junitTestSuite { - return j.suite.TestSuites[len(j.suite.TestSuites)-1] -} diff --git a/fmt_progress.go b/fmt_progress.go index 52e338f..bca5d75 100644 --- a/fmt_progress.go +++ b/fmt_progress.go @@ -122,3 +122,33 @@ func (f *progress) Pending(step *gherkin.Step, match *StepDef) { f.basefmt.Pending(step, match) f.step(f.pending[len(f.pending)-1]) } + +func (f *progress) Sync(cf ConcurrentFormatter) { + if source, ok := cf.(*progress); ok { + f.lock = source.lock + f.steps = source.steps + } +} + +func (f *progress) Copy(cf ConcurrentFormatter) { + if source, ok := cf.(*progress); 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) + } + } +} diff --git a/go.mod b/go.mod index ed7a335..9f6d438 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,9 @@ module github.com/DATA-DOG/godog go 1.12 + +require ( + github.com/DATA-DOG/go-txdb v0.1.3 + github.com/go-sql-driver/mysql v1.5.0 + github.com/lib/pq v1.3.0 // indirect +) diff --git a/run.go b/run.go index 3ad0640..1c5682a 100644 --- a/run.go +++ b/run.go @@ -33,8 +33,8 @@ func (r *runner) concurrent(rate int, formatterFn func() Formatter) (failed bool var useFmtCopy bool var copyLock sync.Mutex - // special mode for progress-formatter - if _, ok := r.fmt.(*progress); ok { + // special mode for concurrent-formatter + if _, ok := r.fmt.(ConcurrentFormatter); ok { useFmtCopy = true } @@ -62,12 +62,11 @@ func (r *runner) concurrent(rate int, formatterFn func() Formatter) (failed bool fmtCopy = formatterFn() suite.fmt = fmtCopy - // sync lock and steps for progress printing - if sf, ok := suite.fmt.(*progress); ok { - if rf, ok := r.fmt.(*progress); ok { - sf.lock = rf.lock - sf.steps = rf.steps - } + concurrentDestFmt, dOk := fmtCopy.(ConcurrentFormatter) + concurrentSourceFmt, sOk := r.fmt.(ConcurrentFormatter) + + if dOk && sOk { + concurrentDestFmt.Sync(concurrentSourceFmt) } } else { suite.fmt = r.fmt @@ -80,34 +79,18 @@ func (r *runner) concurrent(rate int, formatterFn func() Formatter) (failed bool } if useFmtCopy { copyLock.Lock() - dest, dOk := r.fmt.(*progress) - source, sOk := fmtCopy.(*progress) + + concurrentDestFmt, dOk := r.fmt.(ConcurrentFormatter) + concurrentSourceFmt, sOk := fmtCopy.(ConcurrentFormatter) if dOk && sOk { - for _, v := range source.features { - dest.features = append(dest.features, v) - } - - for _, v := range source.failed { - dest.failed = append(dest.failed, v) - } - for _, v := range source.passed { - dest.passed = append(dest.passed, v) - } - for _, v := range source.skipped { - dest.skipped = append(dest.skipped, v) - } - for _, v := range source.undefined { - dest.undefined = append(dest.undefined, v) - } - for _, v := range source.pending { - dest.pending = append(dest.pending, v) - } + concurrentDestFmt.Copy(concurrentSourceFmt) } else if !dOk { panic("cant cast dest formatter to progress-typed") } else if !sOk { panic("cant cast source formatter to progress-typed") } + copyLock.Unlock() } }(&failed, &ft) @@ -289,13 +272,11 @@ func Run(suite string, contextInitializer func(suite *Suite)) int { func supportsConcurrency(format string) bool { switch format { - case "events": - case "junit": - case "pretty": - case "cucumber": + case "progress", "junit": + return true + case "events", "pretty", "cucumber": + return false default: - return true // supports concurrency + return false } - - return false // does not support concurrency } diff --git a/suite.go b/suite.go index 75679eb..4a22d19 100644 --- a/suite.go +++ b/suite.go @@ -12,6 +12,7 @@ import ( "sort" "strconv" "strings" + "time" "unicode/utf8" "github.com/DATA-DOG/godog/gherkin" @@ -22,11 +23,42 @@ var typeOfBytes = reflect.TypeOf([]byte(nil)) type feature struct { *gherkin.Feature + + Scenarios []*scenario + Content []byte `json:"-"` Path string `json:"path"` order int } +func (f feature) startedAt() time.Time { + return f.Scenarios[0].startedAt() +} + +func (f feature) finishedAt() time.Time { + return f.Scenarios[len(f.Scenarios)-1].finishedAt() +} + +func (f feature) appendStepResult(s *stepResult) { + scenario := f.Scenarios[len(f.Scenarios)-1] + scenario.Steps = append(scenario.Steps, s) +} + +type scenario struct { + Name string + OutlineName string + ExampleNo int + Steps []*stepResult +} + +func (s scenario) startedAt() time.Time { + return s.Steps[0].time +} + +func (s scenario) finishedAt() time.Time { + return s.Steps[len(s.Steps)-1].time +} + // ErrUndefined is returned in case if step definition was not found var ErrUndefined = fmt.Errorf("step is undefined")