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