github.com/maps90/godog@v0.7.5-0.20170923143419-0093943021d4/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  }
    81  
    82  type cukeElement struct {
    83  	ID          string     `json:"id"`
    84  	Keyword     string     `json:"keyword"`
    85  	Name        string     `json:"name"`
    86  	Description string     `json:"description"`
    87  	Line        int        `json:"line"`
    88  	Type        string     `json:"type"`
    89  	Tags        []cukeTag  `json:"tags,omitempty"`
    90  	Steps       []cukeStep `json:"steps,omitempty"`
    91  }
    92  
    93  type cukeFeatureJSON struct {
    94  	URI         string        `json:"uri"`
    95  	ID          string        `json:"id"`
    96  	Keyword     string        `json:"keyword"`
    97  	Name        string        `json:"name"`
    98  	Description string        `json:"description"`
    99  	Line        int           `json:"line"`
   100  	Comments    []cukeComment `json:"comments,omitempty"`
   101  	Tags        []cukeTag     `json:"tags,omitempty"`
   102  	Elements    []cukeElement `json:"elements,omitempty"`
   103  }
   104  
   105  type cukefmt struct {
   106  	basefmt
   107  
   108  	// currently running feature path, to be part of id.
   109  	// this is sadly not passed by gherkin nodes.
   110  	// it restricts this formatter to run only in synchronous single
   111  	// threaded execution. Unless running a copy of formatter for each feature
   112  	path         string
   113  	stat         stepType          // last step status, before skipped
   114  	outlineSteps int               // number of current outline scenario steps
   115  	ID           string            // current test id.
   116  	results      []cukeFeatureJSON // structure that represent cuke results
   117  	curStep      *cukeStep         // track the current step
   118  	curElement   *cukeElement      // track the current element
   119  	curFeature   *cukeFeatureJSON  // track the current feature
   120  	curOutline   cukeElement       // Each example show up as an outline element but the outline is parsed only once
   121  	// so I need to keep track of the current outline
   122  	curRow         int       // current row of the example table as it is being processed.
   123  	curExampleTags []cukeTag // temporary storage for tags associate with the current example table.
   124  	startTime      time.Time // used to time duration of the step execution
   125  	curExampleName string    // Due to the fact that examples are parsed once and then iterated over for each result then we need to keep track
   126  	// of the example name inorder to build id fields.
   127  }
   128  
   129  func (f *cukefmt) Node(n interface{}) {
   130  	f.basefmt.Node(n)
   131  
   132  	switch t := n.(type) {
   133  
   134  	// When the example definition is seen we just need track the id and
   135  	// append the name associated with the example as part of the id.
   136  	case *gherkin.Examples:
   137  
   138  		f.curExampleName = makeID(t.Name)
   139  		f.curRow = 2 // there can be more than one example set per outline so reset row count.
   140  		// cucumber counts the header row as an example when creating the id.
   141  
   142  		// store any example level tags in a  temp location.
   143  		f.curExampleTags = make([]cukeTag, len(t.Tags))
   144  		for idx, element := range t.Tags {
   145  			f.curExampleTags[idx].Line = element.Location.Line
   146  			f.curExampleTags[idx].Name = element.Name
   147  		}
   148  
   149  	// The outline node creates a placeholder and the actual element is added as each TableRow is processed.
   150  	case *gherkin.ScenarioOutline:
   151  
   152  		f.curOutline = cukeElement{}
   153  		f.curOutline.Name = t.Name
   154  		f.curOutline.Line = t.Location.Line
   155  		f.curOutline.Description = t.Description
   156  		f.curOutline.Keyword = t.Keyword
   157  		f.curOutline.ID = f.curFeature.ID + ";" + makeID(t.Name)
   158  		f.curOutline.Type = "scenario"
   159  		f.curOutline.Tags = make([]cukeTag, len(t.Tags)+len(f.curFeature.Tags))
   160  
   161  		// apply feature level tags
   162  		if len(f.curOutline.Tags) > 0 {
   163  			copy(f.curOutline.Tags, f.curFeature.Tags)
   164  
   165  			// apply outline level tags.
   166  			for idx, element := range t.Tags {
   167  				f.curOutline.Tags[idx+len(f.curFeature.Tags)].Line = element.Location.Line
   168  				f.curOutline.Tags[idx+len(f.curFeature.Tags)].Name = element.Name
   169  			}
   170  		}
   171  
   172  	// This scenario adds the element to the output immediately.
   173  	case *gherkin.Scenario:
   174  		f.curFeature.Elements = append(f.curFeature.Elements, cukeElement{})
   175  		f.curElement = &f.curFeature.Elements[len(f.curFeature.Elements)-1]
   176  
   177  		f.curElement.Name = t.Name
   178  		f.curElement.Line = t.Location.Line
   179  		f.curElement.Description = t.Description
   180  		f.curElement.Keyword = t.Keyword
   181  		f.curElement.ID = f.curFeature.ID + ";" + makeID(t.Name)
   182  		f.curElement.Type = "scenario"
   183  		f.curElement.Tags = make([]cukeTag, len(t.Tags)+len(f.curFeature.Tags))
   184  
   185  		if len(f.curElement.Tags) > 0 {
   186  			// apply feature level tags
   187  			copy(f.curElement.Tags, f.curFeature.Tags)
   188  
   189  			// apply scenario level tags.
   190  			for idx, element := range t.Tags {
   191  				f.curElement.Tags[idx+len(f.curFeature.Tags)].Line = element.Location.Line
   192  				f.curElement.Tags[idx+len(f.curFeature.Tags)].Name = element.Name
   193  			}
   194  		}
   195  
   196  	// This is an outline scenario and the element is added to the output as
   197  	// the TableRows are encountered.
   198  	case *gherkin.TableRow:
   199  		tmpElem := f.curOutline
   200  		tmpElem.Line = t.Location.Line
   201  		tmpElem.ID = tmpElem.ID + ";" + f.curExampleName + ";" + strconv.Itoa(f.curRow)
   202  		f.curRow++
   203  		f.curFeature.Elements = append(f.curFeature.Elements, tmpElem)
   204  		f.curElement = &f.curFeature.Elements[len(f.curFeature.Elements)-1]
   205  
   206  		// copy in example level tags.
   207  		f.curElement.Tags = append(f.curElement.Tags, f.curExampleTags...)
   208  
   209  	}
   210  
   211  }
   212  
   213  func (f *cukefmt) Feature(ft *gherkin.Feature, p string, c []byte) {
   214  
   215  	f.basefmt.Feature(ft, p, c)
   216  	f.path = p
   217  	f.ID = makeID(ft.Name)
   218  	f.results = append(f.results, cukeFeatureJSON{})
   219  
   220  	f.curFeature = &f.results[len(f.results)-1]
   221  	f.curFeature.URI = p
   222  	f.curFeature.Name = ft.Name
   223  	f.curFeature.Keyword = ft.Keyword
   224  	f.curFeature.Line = ft.Location.Line
   225  	f.curFeature.Description = ft.Description
   226  	f.curFeature.ID = f.ID
   227  	f.curFeature.Tags = make([]cukeTag, len(ft.Tags))
   228  
   229  	for idx, element := range ft.Tags {
   230  		f.curFeature.Tags[idx].Line = element.Location.Line
   231  		f.curFeature.Tags[idx].Name = element.Name
   232  	}
   233  
   234  	f.curFeature.Comments = make([]cukeComment, len(ft.Comments))
   235  	for idx, comment := range ft.Comments {
   236  		f.curFeature.Comments[idx].Value = strings.TrimSpace(comment.Text)
   237  		f.curFeature.Comments[idx].Line = comment.Location.Line
   238  	}
   239  
   240  }
   241  
   242  func (f *cukefmt) Summary() {
   243  	dat, err := json.MarshalIndent(f.results, "", "    ")
   244  	if err != nil {
   245  		panic(err)
   246  	}
   247  	fmt.Fprintf(f.out, "%s\n", string(dat))
   248  }
   249  
   250  func (f *cukefmt) step(res *stepResult) {
   251  
   252  	// determine if test case has finished
   253  	switch t := f.owner.(type) {
   254  	case *gherkin.TableRow:
   255  		d := int(timeNowFunc().Sub(f.startTime).Nanoseconds())
   256  		f.curStep.Result.Duration = &d
   257  		f.curStep.Line = t.Location.Line
   258  		f.curStep.Result.Status = res.typ.String()
   259  		if res.err != nil {
   260  			f.curStep.Result.Error = res.err.Error()
   261  		}
   262  	case *gherkin.Scenario:
   263  		d := int(timeNowFunc().Sub(f.startTime).Nanoseconds())
   264  		f.curStep.Result.Duration = &d
   265  		f.curStep.Result.Status = res.typ.String()
   266  		if res.err != nil {
   267  			f.curStep.Result.Error = res.err.Error()
   268  		}
   269  	}
   270  }
   271  
   272  func (f *cukefmt) Defined(step *gherkin.Step, def *StepDef) {
   273  
   274  	f.startTime = timeNowFunc() // start timing the step
   275  	f.curElement.Steps = append(f.curElement.Steps, cukeStep{})
   276  	f.curStep = &f.curElement.Steps[len(f.curElement.Steps)-1]
   277  
   278  	f.curStep.Name = step.Text
   279  	f.curStep.Line = step.Location.Line
   280  	f.curStep.Keyword = step.Keyword
   281  
   282  	if _, ok := step.Argument.(*gherkin.DocString); ok {
   283  		f.curStep.Docstring = &cukeDocstring{}
   284  		f.curStep.Docstring.ContentType = strings.TrimSpace(step.Argument.(*gherkin.DocString).ContentType)
   285  		f.curStep.Docstring.Line = step.Argument.(*gherkin.DocString).Location.Line
   286  		f.curStep.Docstring.Value = step.Argument.(*gherkin.DocString).Content
   287  	}
   288  
   289  	if def != nil {
   290  		f.curStep.Match.Location = strings.Split(def.definitionID(), " ")[0]
   291  	}
   292  }
   293  
   294  func (f *cukefmt) Passed(step *gherkin.Step, match *StepDef) {
   295  	f.basefmt.Passed(step, match)
   296  	f.stat = passed
   297  	f.step(f.passed[len(f.passed)-1])
   298  }
   299  
   300  func (f *cukefmt) Skipped(step *gherkin.Step, match *StepDef) {
   301  	f.basefmt.Skipped(step, match)
   302  	f.step(f.skipped[len(f.skipped)-1])
   303  
   304  	// no duration reported for skipped.
   305  	f.curStep.Result.Duration = nil
   306  }
   307  
   308  func (f *cukefmt) Undefined(step *gherkin.Step, match *StepDef) {
   309  	f.basefmt.Undefined(step, match)
   310  	f.stat = undefined
   311  	f.step(f.undefined[len(f.undefined)-1])
   312  
   313  	// the location for undefined is the feature file location not the step file.
   314  	f.curStep.Match.Location = fmt.Sprintf("%s:%d", f.path, step.Location.Line)
   315  	f.curStep.Result.Duration = nil
   316  }
   317  
   318  func (f *cukefmt) Failed(step *gherkin.Step, match *StepDef, err error) {
   319  	f.basefmt.Failed(step, match, err)
   320  	f.stat = failed
   321  	f.step(f.failed[len(f.failed)-1])
   322  }
   323  
   324  func (f *cukefmt) Pending(step *gherkin.Step, match *StepDef) {
   325  	f.stat = pending
   326  	f.basefmt.Pending(step, match)
   327  	f.step(f.pending[len(f.pending)-1])
   328  
   329  	// the location for pending is the feature file location not the step file.
   330  	f.curStep.Match.Location = fmt.Sprintf("%s:%d", f.path, step.Location.Line)
   331  	f.curStep.Result.Duration = nil
   332  }