forked from cucumber/godog
-
Notifications
You must be signed in to change notification settings - Fork 0
/
fmt_cucumber.go
349 lines (288 loc) · 11 KB
/
fmt_cucumber.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
package godog
/*
The specification for the formatting originated from https://www.relishapp.com/cucumber/cucumber/docs/formatters/json-output-formatter.
I found that documentation was misleading or out dated. To validate formatting I create a ruby cucumber test harness and ran the
same feature files through godog and the ruby cucumber.
The docstrings in the cucumber.feature represent the cucumber output for those same feature definitions.
I did note that comments in ruby could be at just about any level in particular Feature, Scenario and Step. In godog I
could only find comments under the Feature data structure.
*/
import (
"encoding/json"
"fmt"
"io"
"strconv"
"strings"
"time"
"github.com/DATA-DOG/godog/gherkin"
)
func init() {
Format("cucumber", "Produces cucumber JSON format output.", cucumberFunc)
}
func cucumberFunc(suite string, out io.Writer) Formatter {
formatter := &cukefmt{
basefmt: basefmt{
started: timeNowFunc(),
indent: 2,
out: out,
},
}
return formatter
}
// Replace spaces with - This function is used to create the "id" fields of the cucumber output.
func makeID(name string) string {
return strings.Replace(strings.ToLower(name), " ", "-", -1)
}
// The sequence of type structs are used to marshall the json object.
type cukeComment struct {
Value string `json:"value"`
Line int `json:"line"`
}
type cukeDocstring struct {
Value string `json:"value"`
ContentType string `json:"content_type"`
Line int `json:"line"`
}
type cukeTag struct {
Name string `json:"name"`
Line int `json:"line"`
}
type cukeResult struct {
Status string `json:"status"`
Error string `json:"error_message,omitempty"`
Duration *int `json:"duration,omitempty"`
}
type cukeMatch struct {
Location string `json:"location"`
}
type cukeStep struct {
Keyword string `json:"keyword"`
Name string `json:"name"`
Line int `json:"line"`
Docstring *cukeDocstring `json:"doc_string,omitempty"`
Match cukeMatch `json:"match"`
Result cukeResult `json:"result"`
DataTable []*cukeDataTableRow `json:"rows,omitempty"`
}
type cukeDataTableRow struct {
Cells []string `json:"cells"`
}
type cukeElement struct {
ID string `json:"id"`
Keyword string `json:"keyword"`
Name string `json:"name"`
Description string `json:"description"`
Line int `json:"line"`
Type string `json:"type"`
Tags []cukeTag `json:"tags,omitempty"`
Steps []cukeStep `json:"steps,omitempty"`
}
type cukeFeatureJSON struct {
URI string `json:"uri"`
ID string `json:"id"`
Keyword string `json:"keyword"`
Name string `json:"name"`
Description string `json:"description"`
Line int `json:"line"`
Comments []cukeComment `json:"comments,omitempty"`
Tags []cukeTag `json:"tags,omitempty"`
Elements []cukeElement `json:"elements,omitempty"`
}
type cukefmt struct {
basefmt
// currently running feature path, to be part of id.
// this is sadly not passed by gherkin nodes.
// it restricts this formatter to run only in synchronous single
// threaded execution. Unless running a copy of formatter for each feature
path string
stat stepType // last step status, before skipped
ID string // current test id.
results []cukeFeatureJSON // structure that represent cuke results
curStep *cukeStep // track the current step
curElement *cukeElement // track the current element
curFeature *cukeFeatureJSON // track the current feature
curOutline cukeElement // Each example show up as an outline element but the outline is parsed only once
// so I need to keep track of the current outline
curRow int // current row of the example table as it is being processed.
curExampleTags []cukeTag // temporary storage for tags associate with the current example table.
startTime time.Time // used to time duration of the step execution
curExampleName string // Due to the fact that examples are parsed once and then iterated over for each result then we need to keep track
// of the example name inorder to build id fields.
}
func (f *cukefmt) Node(n interface{}) {
f.basefmt.Node(n)
switch t := n.(type) {
// When the example definition is seen we just need track the id and
// append the name associated with the example as part of the id.
case *gherkin.Examples:
f.curExampleName = makeID(t.Name)
f.curRow = 2 // there can be more than one example set per outline so reset row count.
// cucumber counts the header row as an example when creating the id.
// store any example level tags in a temp location.
f.curExampleTags = make([]cukeTag, len(t.Tags))
for idx, element := range t.Tags {
f.curExampleTags[idx].Line = element.Location.Line
f.curExampleTags[idx].Name = element.Name
}
// The outline node creates a placeholder and the actual element is added as each TableRow is processed.
case *gherkin.ScenarioOutline:
f.curOutline = cukeElement{}
f.curOutline.Name = t.Name
f.curOutline.Line = t.Location.Line
f.curOutline.Description = t.Description
f.curOutline.Keyword = t.Keyword
f.curOutline.ID = f.curFeature.ID + ";" + makeID(t.Name)
f.curOutline.Type = "scenario"
f.curOutline.Tags = make([]cukeTag, len(t.Tags)+len(f.curFeature.Tags))
// apply feature level tags
if len(f.curOutline.Tags) > 0 {
copy(f.curOutline.Tags, f.curFeature.Tags)
// apply outline level tags.
for idx, element := range t.Tags {
f.curOutline.Tags[idx+len(f.curFeature.Tags)].Line = element.Location.Line
f.curOutline.Tags[idx+len(f.curFeature.Tags)].Name = element.Name
}
}
// This scenario adds the element to the output immediately.
case *gherkin.Scenario:
f.curFeature.Elements = append(f.curFeature.Elements, cukeElement{})
f.curElement = &f.curFeature.Elements[len(f.curFeature.Elements)-1]
f.curElement.Name = t.Name
f.curElement.Line = t.Location.Line
f.curElement.Description = t.Description
f.curElement.Keyword = t.Keyword
f.curElement.ID = f.curFeature.ID + ";" + makeID(t.Name)
f.curElement.Type = "scenario"
f.curElement.Tags = make([]cukeTag, len(t.Tags)+len(f.curFeature.Tags))
if len(f.curElement.Tags) > 0 {
// apply feature level tags
copy(f.curElement.Tags, f.curFeature.Tags)
// apply scenario level tags.
for idx, element := range t.Tags {
f.curElement.Tags[idx+len(f.curFeature.Tags)].Line = element.Location.Line
f.curElement.Tags[idx+len(f.curFeature.Tags)].Name = element.Name
}
}
// This is an outline scenario and the element is added to the output as
// the TableRows are encountered.
case *gherkin.TableRow:
tmpElem := f.curOutline
tmpElem.Line = t.Location.Line
tmpElem.ID = tmpElem.ID + ";" + f.curExampleName + ";" + strconv.Itoa(f.curRow)
f.curRow++
f.curFeature.Elements = append(f.curFeature.Elements, tmpElem)
f.curElement = &f.curFeature.Elements[len(f.curFeature.Elements)-1]
// copy in example level tags.
f.curElement.Tags = append(f.curElement.Tags, f.curExampleTags...)
}
}
func (f *cukefmt) Feature(ft *gherkin.Feature, p string, c []byte) {
f.basefmt.Feature(ft, p, c)
f.path = p
f.ID = makeID(ft.Name)
f.results = append(f.results, cukeFeatureJSON{})
f.curFeature = &f.results[len(f.results)-1]
f.curFeature.URI = p
f.curFeature.Name = ft.Name
f.curFeature.Keyword = ft.Keyword
f.curFeature.Line = ft.Location.Line
f.curFeature.Description = ft.Description
f.curFeature.ID = f.ID
f.curFeature.Tags = make([]cukeTag, len(ft.Tags))
for idx, element := range ft.Tags {
f.curFeature.Tags[idx].Line = element.Location.Line
f.curFeature.Tags[idx].Name = element.Name
}
f.curFeature.Comments = make([]cukeComment, len(ft.Comments))
for idx, comment := range ft.Comments {
f.curFeature.Comments[idx].Value = strings.TrimSpace(comment.Text)
f.curFeature.Comments[idx].Line = comment.Location.Line
}
}
func (f *cukefmt) Summary() {
dat, err := json.MarshalIndent(f.results, "", " ")
if err != nil {
panic(err)
}
fmt.Fprintf(f.out, "%s\n", string(dat))
}
func (f *cukefmt) step(res *stepResult) {
// determine if test case has finished
switch t := f.owner.(type) {
case *gherkin.TableRow:
d := int(timeNowFunc().Sub(f.startTime).Nanoseconds())
f.curStep.Result.Duration = &d
f.curStep.Line = t.Location.Line
f.curStep.Result.Status = res.typ.String()
if res.err != nil {
f.curStep.Result.Error = res.err.Error()
}
case *gherkin.Scenario:
d := int(timeNowFunc().Sub(f.startTime).Nanoseconds())
f.curStep.Result.Duration = &d
f.curStep.Result.Status = res.typ.String()
if res.err != nil {
f.curStep.Result.Error = res.err.Error()
}
}
}
func (f *cukefmt) Defined(step *gherkin.Step, def *StepDef) {
f.startTime = timeNowFunc() // start timing the step
f.curElement.Steps = append(f.curElement.Steps, cukeStep{})
f.curStep = &f.curElement.Steps[len(f.curElement.Steps)-1]
f.curStep.Name = step.Text
f.curStep.Line = step.Location.Line
f.curStep.Keyword = step.Keyword
if _, ok := step.Argument.(*gherkin.DocString); ok {
f.curStep.Docstring = &cukeDocstring{}
f.curStep.Docstring.ContentType = strings.TrimSpace(step.Argument.(*gherkin.DocString).ContentType)
f.curStep.Docstring.Line = step.Argument.(*gherkin.DocString).Location.Line
f.curStep.Docstring.Value = step.Argument.(*gherkin.DocString).Content
}
if _, ok := step.Argument.(*gherkin.DataTable); ok {
dataTable := step.Argument.(*gherkin.DataTable)
f.curStep.DataTable = make([]*cukeDataTableRow, len(dataTable.Rows))
for i, row := range dataTable.Rows {
cells := make([]string, len(row.Cells))
for j, cell := range row.Cells {
cells[j] = cell.Value
}
f.curStep.DataTable[i] = &cukeDataTableRow{Cells: cells}
}
}
if def != nil {
f.curStep.Match.Location = strings.Split(def.definitionID(), " ")[0]
}
}
func (f *cukefmt) Passed(step *gherkin.Step, match *StepDef) {
f.basefmt.Passed(step, match)
f.stat = passed
f.step(f.passed[len(f.passed)-1])
}
func (f *cukefmt) Skipped(step *gherkin.Step, match *StepDef) {
f.basefmt.Skipped(step, match)
f.step(f.skipped[len(f.skipped)-1])
// no duration reported for skipped.
f.curStep.Result.Duration = nil
}
func (f *cukefmt) Undefined(step *gherkin.Step, match *StepDef) {
f.basefmt.Undefined(step, match)
f.stat = undefined
f.step(f.undefined[len(f.undefined)-1])
// the location for undefined is the feature file location not the step file.
f.curStep.Match.Location = fmt.Sprintf("%s:%d", f.path, step.Location.Line)
f.curStep.Result.Duration = nil
}
func (f *cukefmt) Failed(step *gherkin.Step, match *StepDef, err error) {
f.basefmt.Failed(step, match, err)
f.stat = failed
f.step(f.failed[len(f.failed)-1])
}
func (f *cukefmt) Pending(step *gherkin.Step, match *StepDef) {
f.stat = pending
f.basefmt.Pending(step, match)
f.step(f.pending[len(f.pending)-1])
// the location for pending is the feature file location not the step file.
f.curStep.Match.Location = fmt.Sprintf("%s:%d", f.path, step.Location.Line)
f.curStep.Result.Duration = nil
}