Skip to content

Commit c5a88f6

Browse files
tigh-lattevearutop
andauthored
fix(formatter): On concurrent execution, execute formatter at end of Scenario (#645)
* fix(formatter): add onflush logger only print output at end of scenario when running concurrently * add to changelog * fix tests * fix scenario outline output for the Pretty formatter * fix casing for linter * add coverage for new storage function * relate suite back to where it was originally * better type assertion on flush log * var name for asserted formatter that doesn't clash with stdlib's fmt * add coverage to summary * only defer flush func when running concurrently * much more concise way of deferring the flush --------- Co-authored-by: Viacheslav Poturaev <[email protected]>
1 parent 9b699ff commit c5a88f6

File tree

9 files changed

+240
-9
lines changed

9 files changed

+240
-9
lines changed

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ This document is formatted according to the principles of [Keep A CHANGELOG](htt
88

99
## Unreleased
1010

11+
- fix(formatter): On concurrent execution, execute formatter at end of Scenario - ([645](https://github.com/cucumber/godog/pull/645) - [tigh-latte](https://github.com/tigh-latte))
12+
1113
## [v0.15.0]
1214

1315
### Added
@@ -39,7 +41,7 @@ This document is formatted according to the principles of [Keep A CHANGELOG](htt
3941

4042
### Changed
4143
- Update test.yml ([583](https://github.com/cucumber/godog/pull/583) - [vearutop](https://github.com/vearutop))
42-
44+
4345
## [v0.13.0]
4446
### Added
4547
- Support for reading feature files from an `fs.FS` ([550](https://github.com/cucumber/godog/pull/550) - [tigh-latte](https://github.com/tigh-latte))

formatters/fmt.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,12 @@ type Formatter interface {
7474
Summary()
7575
}
7676

77+
// FlushFormatter is a `Formatter` but can be flushed.
78+
type FlushFormatter interface {
79+
Formatter
80+
Flush()
81+
}
82+
7783
// FormatterFunc builds a formatter with given
7884
// suite name and io.Writer to record output
7985
type FormatterFunc func(string, io.Writer) Formatter

internal/formatters/fmt_flushwrap.go

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
package formatters
2+
3+
import (
4+
"sync"
5+
6+
"github.com/cucumber/godog/formatters"
7+
messages "github.com/cucumber/messages/go/v21"
8+
)
9+
10+
// WrapOnFlush wrap a `formatters.Formatter` in a `formatters.FlushFormatter`, which only
11+
// executes when `Flush` is called
12+
func WrapOnFlush(fmt formatters.Formatter) formatters.FlushFormatter {
13+
return &onFlushFormatter{
14+
fmt: fmt,
15+
fns: make([]func(), 0),
16+
mu: &sync.Mutex{},
17+
}
18+
}
19+
20+
type onFlushFormatter struct {
21+
fmt formatters.Formatter
22+
fns []func()
23+
mu *sync.Mutex
24+
}
25+
26+
func (o *onFlushFormatter) Pickle(pickle *messages.Pickle) {
27+
o.fns = append(o.fns, func() {
28+
o.fmt.Pickle(pickle)
29+
})
30+
}
31+
32+
func (o *onFlushFormatter) Passed(pickle *messages.Pickle, step *messages.PickleStep, definition *formatters.StepDefinition) {
33+
o.fns = append(o.fns, func() {
34+
o.fmt.Passed(pickle, step, definition)
35+
})
36+
}
37+
38+
// Ambiguous implements formatters.Formatter.
39+
func (o *onFlushFormatter) Ambiguous(pickle *messages.Pickle, step *messages.PickleStep, definition *formatters.StepDefinition, err error) {
40+
o.fns = append(o.fns, func() {
41+
o.fmt.Ambiguous(pickle, step, definition, err)
42+
})
43+
}
44+
45+
// Defined implements formatters.Formatter.
46+
func (o *onFlushFormatter) Defined(pickle *messages.Pickle, step *messages.PickleStep, definition *formatters.StepDefinition) {
47+
o.fns = append(o.fns, func() {
48+
o.fmt.Defined(pickle, step, definition)
49+
})
50+
}
51+
52+
// Failed implements formatters.Formatter.
53+
func (o *onFlushFormatter) Failed(pickle *messages.Pickle, step *messages.PickleStep, definition *formatters.StepDefinition, err error) {
54+
o.fns = append(o.fns, func() {
55+
o.fmt.Failed(pickle, step, definition, err)
56+
})
57+
}
58+
59+
// Feature implements formatters.Formatter.
60+
func (o *onFlushFormatter) Feature(pickle *messages.GherkinDocument, p string, c []byte) {
61+
o.fns = append(o.fns, func() {
62+
o.fmt.Feature(pickle, p, c)
63+
})
64+
}
65+
66+
// Pending implements formatters.Formatter.
67+
func (o *onFlushFormatter) Pending(pickle *messages.Pickle, step *messages.PickleStep, definition *formatters.StepDefinition) {
68+
o.fns = append(o.fns, func() {
69+
o.fmt.Pending(pickle, step, definition)
70+
})
71+
}
72+
73+
// Skipped implements formatters.Formatter.
74+
func (o *onFlushFormatter) Skipped(pickle *messages.Pickle, step *messages.PickleStep, definition *formatters.StepDefinition) {
75+
o.fns = append(o.fns, func() {
76+
o.fmt.Skipped(pickle, step, definition)
77+
})
78+
}
79+
80+
// Summary implements formatters.Formatter.
81+
func (o *onFlushFormatter) Summary() {
82+
o.fns = append(o.fns, func() {
83+
o.fmt.Summary()
84+
})
85+
}
86+
87+
// TestRunStarted implements formatters.Formatter.
88+
func (o *onFlushFormatter) TestRunStarted() {
89+
o.fns = append(o.fns, func() {
90+
o.fmt.TestRunStarted()
91+
})
92+
}
93+
94+
// Undefined implements formatters.Formatter.
95+
func (o *onFlushFormatter) Undefined(pickle *messages.Pickle, step *messages.PickleStep, definition *formatters.StepDefinition) {
96+
o.fns = append(o.fns, func() {
97+
o.fmt.Undefined(pickle, step, definition)
98+
})
99+
}
100+
101+
// Flush the logs.
102+
func (o *onFlushFormatter) Flush() {
103+
o.mu.Lock()
104+
defer o.mu.Unlock()
105+
for _, fn := range o.fns {
106+
fn()
107+
}
108+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package formatters
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
)
8+
9+
var flushMock = DummyFormatter{}
10+
11+
func TestFlushWrapOnFormatter(t *testing.T) {
12+
flushMock.tt = t
13+
14+
fmt := WrapOnFlush(&flushMock)
15+
16+
fmt.Feature(document, str, byt)
17+
fmt.TestRunStarted()
18+
fmt.Pickle(pickle)
19+
fmt.Defined(pickle, step, definition)
20+
fmt.Passed(pickle, step, definition)
21+
fmt.Skipped(pickle, step, definition)
22+
fmt.Undefined(pickle, step, definition)
23+
fmt.Failed(pickle, step, definition, err)
24+
fmt.Pending(pickle, step, definition)
25+
fmt.Ambiguous(pickle, step, definition, err)
26+
fmt.Summary()
27+
28+
assert.Equal(t, 0, flushMock.CountFeature)
29+
assert.Equal(t, 0, flushMock.CountTestRunStarted)
30+
assert.Equal(t, 0, flushMock.CountPickle)
31+
assert.Equal(t, 0, flushMock.CountDefined)
32+
assert.Equal(t, 0, flushMock.CountPassed)
33+
assert.Equal(t, 0, flushMock.CountSkipped)
34+
assert.Equal(t, 0, flushMock.CountUndefined)
35+
assert.Equal(t, 0, flushMock.CountFailed)
36+
assert.Equal(t, 0, flushMock.CountPending)
37+
assert.Equal(t, 0, flushMock.CountAmbiguous)
38+
assert.Equal(t, 0, flushMock.CountSummary)
39+
40+
fmt.Flush()
41+
42+
assert.Equal(t, 1, flushMock.CountFeature)
43+
assert.Equal(t, 1, flushMock.CountTestRunStarted)
44+
assert.Equal(t, 1, flushMock.CountPickle)
45+
assert.Equal(t, 1, flushMock.CountDefined)
46+
assert.Equal(t, 1, flushMock.CountPassed)
47+
assert.Equal(t, 1, flushMock.CountSkipped)
48+
assert.Equal(t, 1, flushMock.CountUndefined)
49+
assert.Equal(t, 1, flushMock.CountFailed)
50+
assert.Equal(t, 1, flushMock.CountPending)
51+
assert.Equal(t, 1, flushMock.CountAmbiguous)
52+
assert.Equal(t, 1, flushMock.CountSummary)
53+
}

internal/formatters/fmt_multi_test.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ var (
2424

2525
// TestRepeater tests the delegation of the repeater functions.
2626
func TestRepeater(t *testing.T) {
27-
2827
mock.tt = t
2928
f := make(repeater, 0)
3029
f = append(f, &mock)
@@ -52,7 +51,6 @@ func TestRepeater(t *testing.T) {
5251
assert.Equal(t, 2, mock.CountFailed)
5352
assert.Equal(t, 2, mock.CountPending)
5453
assert.Equal(t, 2, mock.CountAmbiguous)
55-
5654
}
5755

5856
type BaseFormatter struct {
@@ -73,6 +71,7 @@ type DummyFormatter struct {
7371
CountFailed int
7472
CountPending int
7573
CountAmbiguous int
74+
CountSummary int
7675
}
7776

7877
// SetStorage assigns gherkin data storage.
@@ -158,3 +157,8 @@ func (f *DummyFormatter) Ambiguous(p *messages.Pickle, s *messages.PickleStep, d
158157
assert.Equal(f.tt, d, definition)
159158
f.CountAmbiguous++
160159
}
160+
161+
// Pickle receives scenario.
162+
func (f *DummyFormatter) Summary() {
163+
f.CountSummary++
164+
}

internal/formatters/fmt_pretty.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ func (f *Pretty) Summary() {
243243
f.Base.Summary()
244244
}
245245

246-
func (f *Pretty) printOutlineExample(pickle *messages.Pickle, backgroundSteps int) {
246+
func (f *Pretty) printOutlineExample(pickle *messages.Pickle, step *messages.PickleStep, backgroundSteps int) {
247247
var errorMsg string
248248
var clr = green
249249

@@ -255,7 +255,7 @@ func (f *Pretty) printOutlineExample(pickle *messages.Pickle, backgroundSteps in
255255
printExampleHeader := exampleTable.TableBody[0].Id == exampleRow.Id
256256
firstExamplesTable := astScenario.Examples[0].Location.Line == exampleTable.Location.Line
257257

258-
pickleStepResults := f.Storage.MustGetPickleStepResultsByPickleID(pickle.Id)
258+
pickleStepResults := f.Storage.MustGetPickleStepResultsByPickleIDUntilStep(pickle.Id, step.Id)
259259

260260
firstExecutedScenarioStep := len(pickleStepResults) == backgroundSteps+1
261261
if firstExamplesTable && printExampleHeader && firstExecutedScenarioStep {
@@ -419,7 +419,7 @@ func (f *Pretty) printStep(pickle *messages.Pickle, pickleStep *messages.PickleS
419419
}
420420

421421
if !astBackgroundStep && len(astScenario.Examples) > 0 {
422-
f.printOutlineExample(pickle, backgroundSteps)
422+
f.printOutlineExample(pickle, pickleStep, backgroundSteps)
423423
return
424424
}
425425

internal/storage/storage.go

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ func (s *Storage) MustGetPickleStepResult(id string) models.PickleStepResult {
223223
return v.(models.PickleStepResult)
224224
}
225225

226-
// MustGetPickleStepResultsByPickleID will retrieve pickle strep results by pickle id and panic on error.
226+
// MustGetPickleStepResultsByPickleID will retrieve pickle step results by pickle id and panic on error.
227227
func (s *Storage) MustGetPickleStepResultsByPickleID(pickleID string) (psrs []models.PickleStepResult) {
228228
it := s.mustGet(tablePickleStepResult, tablePickleStepResultIndexPickleID, pickleID)
229229
for v := it.Next(); v != nil; v = it.Next() {
@@ -233,6 +233,21 @@ func (s *Storage) MustGetPickleStepResultsByPickleID(pickleID string) (psrs []mo
233233
return psrs
234234
}
235235

236+
// MustGetPickleStepResultsByPickleIDUntilStep will retrieve pickle step results by pickle id
237+
// from 0..stepID for that pickle.
238+
func (s *Storage) MustGetPickleStepResultsByPickleIDUntilStep(pickleID string, untilStepID string) (psrs []models.PickleStepResult) {
239+
it := s.mustGet(tablePickleStepResult, tablePickleStepResultIndexPickleID, pickleID)
240+
for v := it.Next(); v != nil; v = it.Next() {
241+
psr := v.(models.PickleStepResult)
242+
psrs = append(psrs, psr)
243+
if psr.PickleStepID == untilStepID {
244+
break
245+
}
246+
}
247+
248+
return psrs
249+
}
250+
236251
// MustGetPickleStepResultsByStatus will retrieve pickle strep results by status and panic on error.
237252
func (s *Storage) MustGetPickleStepResultsByStatus(status models.StepResultStatus) (psrs []models.PickleStepResult) {
238253
it := s.mustGet(tablePickleStepResult, tablePickleStepResultIndexStatus, status)

internal/storage/storage_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,40 @@ func Test_MustGetPickleStepResultsByPickleID(t *testing.T) {
128128
assert.Equal(t, expected, actual)
129129
}
130130

131+
func Test_MustGetPickleStepResultsByPickleIDUntilStep(t *testing.T) {
132+
s := storage.NewStorage()
133+
134+
const pickleID = "p1"
135+
const stepID = "s2"
136+
137+
store := []models.PickleStepResult{
138+
{
139+
Status: models.Passed,
140+
PickleID: pickleID,
141+
PickleStepID: "s1",
142+
},
143+
{
144+
Status: models.Passed,
145+
PickleID: pickleID,
146+
PickleStepID: "s2",
147+
},
148+
{
149+
Status: models.Passed,
150+
PickleID: pickleID,
151+
PickleStepID: "s3",
152+
},
153+
}
154+
155+
for _, psr := range store {
156+
s.MustInsertPickleStepResult(psr)
157+
}
158+
159+
expected := store[:2]
160+
161+
actual := s.MustGetPickleStepResultsByPickleIDUntilStep(pickleID, stepID)
162+
assert.Equal(t, expected, actual)
163+
}
164+
131165
func Test_MustGetPickleStepResultsByStatus(t *testing.T) {
132166
s := storage.NewStorage()
133167

run.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,10 @@ const (
3333
exitOptionError
3434
)
3535

36-
type testSuiteInitializer func(*TestSuiteContext)
37-
type scenarioInitializer func(*ScenarioContext)
36+
type (
37+
testSuiteInitializer func(*TestSuiteContext)
38+
scenarioInitializer func(*ScenarioContext)
39+
)
3840

3941
type runner struct {
4042
randomSeed int64
@@ -115,6 +117,13 @@ func (r *runner) concurrent(rate int) (failed bool) {
115117

116118
// Copy base suite.
117119
suite := *testSuiteContext.suite
120+
if rate > 1 {
121+
// if running concurrently, only print at end of scenario to keep
122+
// scenario logs segregated
123+
ffmt := ifmt.WrapOnFlush(testSuiteContext.suite.fmt)
124+
suite.fmt = ffmt
125+
defer ffmt.Flush()
126+
}
118127

119128
if r.scenarioInitializer != nil {
120129
sc := ScenarioContext{suite: &suite}

0 commit comments

Comments
 (0)