github.com/lonnblad/godog@v0.7.14-0.20200306004719-1b0cb3259847/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 "strings" 19 "time" 20 21 "github.com/cucumber/messages-go/v9" 22 ) 23 24 func init() { 25 Format("cucumber", "Produces cucumber JSON format output.", cucumberFunc) 26 } 27 28 func cucumberFunc(suite string, out io.Writer) Formatter { 29 return &cukefmt{basefmt: newBaseFmt(suite, out)} 30 } 31 32 // Replace spaces with - This function is used to create the "id" fields of the cucumber output. 33 func makeID(name string) string { 34 return strings.Replace(strings.ToLower(name), " ", "-", -1) 35 } 36 37 // The sequence of type structs are used to marshall the json object. 38 type cukeComment struct { 39 Value string `json:"value"` 40 Line int `json:"line"` 41 } 42 43 type cukeDocstring struct { 44 Value string `json:"value"` 45 ContentType string `json:"content_type"` 46 Line int `json:"line"` 47 } 48 49 type cukeTag struct { 50 Name string `json:"name"` 51 Line int `json:"line"` 52 } 53 54 type cukeResult struct { 55 Status string `json:"status"` 56 Error string `json:"error_message,omitempty"` 57 Duration *int `json:"duration,omitempty"` 58 } 59 60 type cukeMatch struct { 61 Location string `json:"location"` 62 } 63 64 type cukeStep struct { 65 Keyword string `json:"keyword"` 66 Name string `json:"name"` 67 Line int `json:"line"` 68 Docstring *cukeDocstring `json:"doc_string,omitempty"` 69 Match cukeMatch `json:"match"` 70 Result cukeResult `json:"result"` 71 DataTable []*cukeDataTableRow `json:"rows,omitempty"` 72 } 73 74 type cukeDataTableRow struct { 75 Cells []string `json:"cells"` 76 } 77 78 type cukeElement struct { 79 ID string `json:"id"` 80 Keyword string `json:"keyword"` 81 Name string `json:"name"` 82 Description string `json:"description"` 83 Line int `json:"line"` 84 Type string `json:"type"` 85 Tags []cukeTag `json:"tags,omitempty"` 86 Steps []cukeStep `json:"steps,omitempty"` 87 } 88 89 type cukeFeatureJSON struct { 90 URI string `json:"uri"` 91 ID string `json:"id"` 92 Keyword string `json:"keyword"` 93 Name string `json:"name"` 94 Description string `json:"description"` 95 Line int `json:"line"` 96 Comments []cukeComment `json:"comments,omitempty"` 97 Tags []cukeTag `json:"tags,omitempty"` 98 Elements []cukeElement `json:"elements,omitempty"` 99 } 100 101 type cukefmt struct { 102 *basefmt 103 104 // currently running feature path, to be part of id. 105 // this is sadly not passed by gherkin nodes. 106 // it restricts this formatter to run only in synchronous single 107 // threaded execution. Unless running a copy of formatter for each feature 108 path string 109 status stepResultStatus // last step status, before skipped 110 ID string // current test id. 111 results []cukeFeatureJSON // structure that represent cuke results 112 curStep *cukeStep // track the current step 113 curElement *cukeElement // track the current element 114 curFeature *cukeFeatureJSON // track the current feature 115 curOutline cukeElement // Each example show up as an outline element but the outline is parsed only once 116 // so I need to keep track of the current outline 117 curRow int // current row of the example table as it is being processed. 118 curExampleTags []cukeTag // temporary storage for tags associate with the current example table. 119 startTime time.Time // used to time duration of the step execution 120 curExampleName string // Due to the fact that examples are parsed once and then iterated over for each result then we need to keep track 121 // of the example name inorder to build id fields. 122 } 123 124 func (f *cukefmt) Pickle(pickle *messages.Pickle) { 125 f.basefmt.Pickle(pickle) 126 127 scenario := f.findScenario(pickle.AstNodeIds[0]) 128 129 f.curFeature.Elements = append(f.curFeature.Elements, cukeElement{}) 130 f.curElement = &f.curFeature.Elements[len(f.curFeature.Elements)-1] 131 132 f.curElement.Name = pickle.Name 133 f.curElement.Line = int(scenario.Location.Line) 134 f.curElement.Description = scenario.Description 135 f.curElement.Keyword = scenario.Keyword 136 f.curElement.ID = f.curFeature.ID + ";" + makeID(pickle.Name) 137 f.curElement.Type = "scenario" 138 139 f.curElement.Tags = make([]cukeTag, len(scenario.Tags)+len(f.curFeature.Tags)) 140 141 if len(f.curElement.Tags) > 0 { 142 // apply feature level tags 143 copy(f.curElement.Tags, f.curFeature.Tags) 144 145 // apply scenario level tags. 146 for idx, element := range scenario.Tags { 147 f.curElement.Tags[idx+len(f.curFeature.Tags)].Line = int(element.Location.Line) 148 f.curElement.Tags[idx+len(f.curFeature.Tags)].Name = element.Name 149 } 150 } 151 152 if len(pickle.AstNodeIds) == 1 { 153 return 154 } 155 156 example, _ := f.findExample(pickle.AstNodeIds[1]) 157 // apply example level tags. 158 for _, tag := range example.Tags { 159 tag := cukeTag{Line: int(tag.Location.Line), Name: tag.Name} 160 f.curElement.Tags = append(f.curElement.Tags, tag) 161 } 162 163 examples := scenario.GetExamples() 164 if len(examples) > 0 { 165 rowID := pickle.AstNodeIds[1] 166 167 for _, example := range examples { 168 for idx, row := range example.TableBody { 169 if rowID == row.Id { 170 f.curElement.ID += fmt.Sprintf(";%s;%d", makeID(example.Name), idx+2) 171 f.curElement.Line = int(row.Location.Line) 172 } 173 } 174 } 175 } 176 177 } 178 179 func (f *cukefmt) Feature(gd *messages.GherkinDocument, p string, c []byte) { 180 f.basefmt.Feature(gd, p, c) 181 182 f.path = p 183 f.ID = makeID(gd.Feature.Name) 184 f.results = append(f.results, cukeFeatureJSON{}) 185 186 f.curFeature = &f.results[len(f.results)-1] 187 f.curFeature.URI = p 188 f.curFeature.Name = gd.Feature.Name 189 f.curFeature.Keyword = gd.Feature.Keyword 190 f.curFeature.Line = int(gd.Feature.Location.Line) 191 f.curFeature.Description = gd.Feature.Description 192 f.curFeature.ID = f.ID 193 f.curFeature.Tags = make([]cukeTag, len(gd.Feature.Tags)) 194 195 for idx, element := range gd.Feature.Tags { 196 f.curFeature.Tags[idx].Line = int(element.Location.Line) 197 f.curFeature.Tags[idx].Name = element.Name 198 } 199 200 f.curFeature.Comments = make([]cukeComment, len(gd.Comments)) 201 for idx, comment := range gd.Comments { 202 f.curFeature.Comments[idx].Value = strings.TrimSpace(comment.Text) 203 f.curFeature.Comments[idx].Line = int(comment.Location.Line) 204 } 205 206 } 207 208 func (f *cukefmt) Summary() { 209 dat, err := json.MarshalIndent(f.results, "", " ") 210 if err != nil { 211 panic(err) 212 } 213 fmt.Fprintf(f.out, "%s\n", string(dat)) 214 } 215 216 func (f *cukefmt) step(res *stepResult) { 217 d := int(timeNowFunc().Sub(f.startTime).Nanoseconds()) 218 f.curStep.Result.Duration = &d 219 f.curStep.Result.Status = res.status.String() 220 if res.err != nil { 221 f.curStep.Result.Error = res.err.Error() 222 } 223 } 224 225 func (f *cukefmt) Defined(pickle *messages.Pickle, pickleStep *messages.Pickle_PickleStep, def *StepDefinition) { 226 f.startTime = timeNowFunc() // start timing the step 227 f.curElement.Steps = append(f.curElement.Steps, cukeStep{}) 228 f.curStep = &f.curElement.Steps[len(f.curElement.Steps)-1] 229 230 step := f.findStep(pickleStep.AstNodeIds[0]) 231 232 line := step.Location.Line 233 if len(pickle.AstNodeIds) == 2 { 234 _, row := f.findExample(pickle.AstNodeIds[1]) 235 line = row.Location.Line 236 } 237 238 f.curStep.Name = pickleStep.Text 239 f.curStep.Line = int(line) 240 f.curStep.Keyword = step.Keyword 241 242 arg := pickleStep.Argument 243 244 if arg.GetDocString() != nil && step.GetDocString() != nil { 245 f.curStep.Docstring = &cukeDocstring{} 246 f.curStep.Docstring.ContentType = strings.TrimSpace(arg.GetDocString().MediaType) 247 f.curStep.Docstring.Line = int(step.GetDocString().Location.Line) 248 f.curStep.Docstring.Value = arg.GetDocString().Content 249 } 250 251 if arg.GetDataTable() != nil { 252 f.curStep.DataTable = make([]*cukeDataTableRow, len(arg.GetDataTable().Rows)) 253 for i, row := range arg.GetDataTable().Rows { 254 cells := make([]string, len(row.Cells)) 255 for j, cell := range row.Cells { 256 cells[j] = cell.Value 257 } 258 f.curStep.DataTable[i] = &cukeDataTableRow{Cells: cells} 259 } 260 } 261 262 if def != nil { 263 f.curStep.Match.Location = strings.Split(def.definitionID(), " ")[0] 264 } 265 } 266 267 func (f *cukefmt) Passed(pickle *messages.Pickle, step *messages.Pickle_PickleStep, match *StepDefinition) { 268 f.basefmt.Passed(pickle, step, match) 269 270 f.status = passed 271 f.step(f.lastStepResult()) 272 } 273 274 func (f *cukefmt) Skipped(pickle *messages.Pickle, step *messages.Pickle_PickleStep, match *StepDefinition) { 275 f.basefmt.Skipped(pickle, step, match) 276 277 f.step(f.lastStepResult()) 278 279 // no duration reported for skipped. 280 f.curStep.Result.Duration = nil 281 } 282 283 func (f *cukefmt) Undefined(pickle *messages.Pickle, step *messages.Pickle_PickleStep, match *StepDefinition) { 284 f.basefmt.Undefined(pickle, step, match) 285 286 f.status = undefined 287 f.step(f.lastStepResult()) 288 289 // the location for undefined is the feature file location not the step file. 290 f.curStep.Match.Location = fmt.Sprintf("%s:%d", f.path, f.findStep(step.AstNodeIds[0]).Location.Line) 291 f.curStep.Result.Duration = nil 292 } 293 294 func (f *cukefmt) Failed(pickle *messages.Pickle, step *messages.Pickle_PickleStep, match *StepDefinition, err error) { 295 f.basefmt.Failed(pickle, step, match, err) 296 297 f.status = failed 298 f.step(f.lastStepResult()) 299 } 300 301 func (f *cukefmt) Pending(pickle *messages.Pickle, step *messages.Pickle_PickleStep, match *StepDefinition) { 302 f.basefmt.Pending(pickle, step, match) 303 304 f.status = pending 305 f.step(f.lastStepResult()) 306 307 // the location for pending is the feature file location not the step file. 308 f.curStep.Match.Location = fmt.Sprintf("%s:%d", f.path, f.findStep(step.AstNodeIds[0]).Location.Line) 309 f.curStep.Result.Duration = nil 310 }