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