349 строки
11 КиБ
Go
349 строки
11 КиБ
Go
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/cucumber/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
|
|
}
|