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  }