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  }