github.com/getgauge/gauge@v1.6.9/reporter/jsonConsole.go (about)

     1  /*----------------------------------------------------------------
     2   *  Copyright (c) ThoughtWorks, Inc.
     3   *  Licensed under the Apache License, Version 2.0
     4   *  See LICENSE in the project root for license information.
     5   *----------------------------------------------------------------*/
     6  
     7  package reporter
     8  
     9  import (
    10  	"encoding/json"
    11  	"fmt"
    12  	"io"
    13  	"strconv"
    14  	"strings"
    15  	"sync"
    16  
    17  	gm "github.com/getgauge/gauge-proto/go/gauge_messages"
    18  	"github.com/getgauge/gauge/execution/result"
    19  	"github.com/getgauge/gauge/formatter"
    20  	"github.com/getgauge/gauge/gauge"
    21  	"github.com/getgauge/gauge/logger"
    22  	"github.com/getgauge/gauge/util"
    23  )
    24  
    25  type eventType string
    26  type status string
    27  
    28  const (
    29  	suiteStart    eventType = "suiteStart"
    30  	specStart     eventType = "specStart"
    31  	scenarioStart eventType = "scenarioStart"
    32  	scenarioEnd   eventType = "scenarioEnd"
    33  	specEnd       eventType = "specEnd"
    34  	suiteEnd      eventType = "suiteEnd"
    35  	pass          status    = "pass"
    36  	fail          status    = "fail"
    37  	skip          status    = "skip"
    38  )
    39  
    40  type jsonConsole struct {
    41  	*sync.Mutex
    42  	writer     io.Writer
    43  	isParallel bool
    44  	stream     int
    45  	stepCache  map[*gm.ScenarioInfo][]*stepInfo
    46  }
    47  
    48  type stepInfo struct {
    49  	step      *gauge.Step
    50  	protoStep *gm.ProtoStep
    51  }
    52  
    53  type executionEvent struct {
    54  	EventType eventType        `json:"type"`
    55  	ID        string           `json:"id,omitempty"`
    56  	ParentID  string           `json:"parentId,omitempty"`
    57  	Name      string           `json:"name,omitempty"`
    58  	Filename  string           `json:"filename,omitempty"`
    59  	Line      int              `json:"line,omitempty"`
    60  	Stream    int              `json:"stream,omitempty"`
    61  	Res       *executionResult `json:"result,omitempty"`
    62  }
    63  
    64  type executionResult struct {
    65  	Status            status           `json:"status,omitempty"`
    66  	Time              int64            `json:"time"`
    67  	Stdout            string           `json:"out,omitempty"`
    68  	Errors            []executionError `json:"errors,omitempty"`
    69  	BeforeHookFailure *executionError  `json:"beforeHookFailure,omitempty"`
    70  	AfterHookFailure  *executionError  `json:"afterHookFailure,omitempty"`
    71  	Table             *tableInfo       `json:"table,omitempty"`
    72  }
    73  
    74  type tableInfo struct {
    75  	Text string `json:"text"`
    76  	Row  int    `json:"rowIndex"`
    77  }
    78  
    79  type executionError struct {
    80  	Text       string `json:"text"`
    81  	Filename   string `json:"filename"`
    82  	Message    string `json:"message"`
    83  	LineNo     string `json:"lineNo"`
    84  	StackTrace string `json:"stackTrace"`
    85  }
    86  
    87  func newJSONConsole(out io.Writer, isParallel bool, stream int) *jsonConsole {
    88  	return &jsonConsole{Mutex: &sync.Mutex{}, writer: out, isParallel: isParallel, stream: stream, stepCache: make(map[*gm.ScenarioInfo][]*stepInfo)}
    89  }
    90  
    91  func (c *jsonConsole) SuiteStart() {
    92  	c.Lock()
    93  	defer c.Unlock()
    94  	c.write(executionEvent{EventType: suiteStart, Stream: c.stream})
    95  }
    96  
    97  func (c *jsonConsole) SuiteEnd(res result.Result) {
    98  	c.Lock()
    99  	defer c.Unlock()
   100  	sRes := res.(*result.SuiteResult)
   101  	c.write(executionEvent{
   102  		EventType: suiteEnd,
   103  		Stream:    c.stream,
   104  		Res: &executionResult{
   105  			Status:            getStatus(sRes.IsFailed, false),
   106  			BeforeHookFailure: getHookFailure(res.GetPreHook(), "Before Suite"),
   107  			AfterHookFailure:  getHookFailure(res.GetPostHook(), "After Suite"),
   108  		},
   109  	})
   110  }
   111  
   112  func (c *jsonConsole) SpecStart(spec *gauge.Specification, res result.Result) {
   113  	c.Lock()
   114  	defer c.Unlock()
   115  	addRow := c.isParallel && spec.DataTable.IsInitialized()
   116  	c.write(executionEvent{
   117  		EventType: specStart,
   118  		ID:        getIDWithRow(spec.FileName, spec.Scenarios, addRow),
   119  		Name:      spec.Heading.Value,
   120  		Filename:  spec.FileName,
   121  		Line:      spec.Heading.LineNo,
   122  		Stream:    c.stream,
   123  	})
   124  }
   125  
   126  func (c *jsonConsole) SpecEnd(spec *gauge.Specification, res result.Result) {
   127  	c.Lock()
   128  	defer c.Unlock()
   129  	protoSpec := res.Item().(*gm.ProtoSpec)
   130  	sRes := res.(*result.SpecResult)
   131  	addRow := c.isParallel && spec.DataTable.IsInitialized()
   132  	e := executionEvent{
   133  		EventType: specEnd,
   134  		ID:        getIDWithRow(spec.FileName, spec.Scenarios, addRow),
   135  		Name:      protoSpec.GetSpecHeading(),
   136  		Filename:  spec.FileName,
   137  		Line:      spec.Heading.LineNo,
   138  		Stream:    c.stream,
   139  		Res: &executionResult{
   140  			Status:            getStatus(sRes.GetFailed(), sRes.Skipped),
   141  			BeforeHookFailure: getHookFailure(res.GetPreHook(), "Before Specification"),
   142  			AfterHookFailure:  getHookFailure(res.GetPostHook(), "After Specification"),
   143  		},
   144  	}
   145  	c.write(e)
   146  }
   147  
   148  func (c *jsonConsole) ScenarioStart(scenario *gauge.Scenario, i *gm.ExecutionInfo, res result.Result) {
   149  	c.Lock()
   150  	defer c.Unlock()
   151  	addRow := c.isParallel && scenario.SpecDataTableRow.IsInitialized()
   152  	parentID := getIDWithRow(i.CurrentSpec.FileName, []*gauge.Scenario{scenario}, addRow)
   153  	e := executionEvent{
   154  		EventType: scenarioStart,
   155  		ID:        parentID + ":" + strconv.Itoa(scenario.Span.Start),
   156  		ParentID:  parentID,
   157  		Filename:  i.CurrentSpec.FileName,
   158  		Line:      scenario.Heading.LineNo,
   159  		Name:      scenario.Heading.Value,
   160  		Stream:    c.stream,
   161  		Res:       &executionResult{Table: getTable(scenario)},
   162  	}
   163  	c.write(e)
   164  }
   165  
   166  func (c *jsonConsole) ScenarioEnd(scenario *gauge.Scenario, res result.Result, i *gm.ExecutionInfo) {
   167  	c.Lock()
   168  	defer c.Unlock()
   169  	addRow := c.isParallel && scenario.SpecDataTableRow.IsInitialized()
   170  	parentID := getIDWithRow(i.CurrentSpec.FileName, []*gauge.Scenario{scenario}, addRow)
   171  	e := executionEvent{
   172  		EventType: scenarioEnd,
   173  		ID:        parentID + ":" + strconv.Itoa(scenario.Span.Start),
   174  		ParentID:  parentID,
   175  		Filename:  i.CurrentSpec.FileName,
   176  		Line:      scenario.Heading.LineNo,
   177  		Name:      scenario.Heading.Value,
   178  		Stream:    c.stream,
   179  		Res: &executionResult{
   180  			Status:            getScenarioStatus(res.(*result.ScenarioResult)),
   181  			Time:              res.ExecTime(),
   182  			Errors:            getErrors(c.stepCache, getAllStepsFromScenario(res.(*result.ScenarioResult).ProtoScenario), i.CurrentSpec.FileName, i),
   183  			BeforeHookFailure: getHookFailure(res.GetPreHook(), "Before Scenario"),
   184  			AfterHookFailure:  getHookFailure(res.GetPostHook(), "After Scenario"),
   185  			Table:             getTable(scenario),
   186  		},
   187  	}
   188  	c.write(e)
   189  }
   190  
   191  func getAllStepsFromScenario(scenario *gm.ProtoScenario) []*gm.ProtoItem {
   192  	return append(scenario.GetContexts(), append(scenario.GetScenarioItems(), scenario.GetTearDownSteps()...)...)
   193  }
   194  
   195  func (c *jsonConsole) StepStart(stepText string) {
   196  }
   197  
   198  func (c *jsonConsole) StepEnd(step gauge.Step, res result.Result, execInfo *gm.ExecutionInfo) {
   199  	si := &stepInfo{step: &step, protoStep: res.(*result.StepResult).Item().(*gm.ProtoStep)}
   200  	c.stepCache[execInfo.CurrentScenario] = append(c.stepCache[execInfo.CurrentScenario], si)
   201  }
   202  
   203  func (c *jsonConsole) ConceptStart(conceptHeading string) {
   204  }
   205  
   206  func (c *jsonConsole) ConceptEnd(res result.Result) {
   207  }
   208  
   209  func (c *jsonConsole) DataTable(table string) {
   210  }
   211  
   212  func (c *jsonConsole) Errorf(err string, args ...interface{}) {
   213  	c.Lock()
   214  	defer c.Unlock()
   215  }
   216  
   217  func (c *jsonConsole) Write(b []byte) (int, error) {
   218  	c.Lock()
   219  	defer c.Unlock()
   220  	s := strings.Split(string(b), "\n")
   221  	for _, m := range s {
   222  		outMessage := &logger.OutMessage{MessageType: "out", Message: strings.Trim(m, "\n ")}
   223  		t, err := outMessage.ToJSON()
   224  		if err != nil {
   225  			return 0, err
   226  		}
   227  		fmt.Fprintf(c.writer, "%s\n", string(t))
   228  	}
   229  	return len(b), nil
   230  }
   231  
   232  func (c *jsonConsole) write(e executionEvent) {
   233  	b, _ := json.Marshal(e)
   234  	fmt.Fprint(c.writer, string(b)+newline)
   235  }
   236  
   237  func getIDWithRow(name string, scenarios []*gauge.Scenario, isDataTable bool) string {
   238  	if !isDataTable || len(scenarios) < 1 {
   239  		return name
   240  	}
   241  	return name + ":" + strconv.Itoa(scenarios[0].SpecDataTableRowIndex)
   242  }
   243  
   244  func getScenarioStatus(result *result.ScenarioResult) status {
   245  	return getStatus(result.ProtoScenario.GetExecutionStatus() == gm.ExecutionStatus_FAILED,
   246  		result.ProtoScenario.GetExecutionStatus() == gm.ExecutionStatus_SKIPPED)
   247  }
   248  
   249  func getStatus(failed, skipped bool) status {
   250  	if failed {
   251  		return fail
   252  	}
   253  	if skipped {
   254  		return skip
   255  	}
   256  	return pass
   257  }
   258  
   259  func getErrors(stepCache map[*gm.ScenarioInfo][]*stepInfo, items []*gm.ProtoItem, id string, execInfo *gm.ExecutionInfo) (errors []executionError) {
   260  	for _, item := range items {
   261  		executionResult := item.GetStep().GetStepExecutionResult()
   262  		res := executionResult.GetExecutionResult()
   263  		switch item.GetItemType() {
   264  		case gm.ProtoItem_Step:
   265  			filename := util.RelPathToProjectRoot(id)
   266  			if err := executionResult.GetPreHookFailure(); err != nil {
   267  				errors = append(errors, *getHookFailure([]*gm.ProtoHookFailure{err}, "BeforeStep hook for step: "+item.Step.ActualText))
   268  			} else {
   269  				if executionResult.GetSkipped() {
   270  					errors = append(errors, executionError{
   271  						Text:     item.Step.ActualText,
   272  						Filename: getFileName(filename, stepCache, item.GetStep(), execInfo),
   273  						Message:  executionResult.SkippedReason,
   274  					})
   275  				} else if res.GetFailed() {
   276  					errors = append(errors, executionError{
   277  						Text:       item.Step.ActualText,
   278  						Filename:   getFileName(filename, stepCache, item.GetStep(), execInfo),
   279  						LineNo:     getLineNo(stepCache, item.GetStep(), execInfo),
   280  						StackTrace: res.StackTrace,
   281  						Message:    res.ErrorMessage,
   282  					})
   283  				}
   284  			}
   285  			if err := executionResult.GetPostHookFailure(); err != nil {
   286  				errors = append(errors, *getHookFailure([]*gm.ProtoHookFailure{err}, "AfterStep hook for step: "+item.Step.ActualText))
   287  			}
   288  		case gm.ProtoItem_Concept:
   289  			errors = append(errors, getErrors(stepCache, item.GetConcept().GetSteps(), id, execInfo)...)
   290  		}
   291  	}
   292  	return
   293  }
   294  
   295  func getFileName(file string, stepCache map[*gm.ScenarioInfo][]*stepInfo, step *gm.ProtoStep, info *gm.ExecutionInfo) string {
   296  	for _, si := range stepCache[info.CurrentScenario] {
   297  		if si.protoStep == step {
   298  			return si.step.FileName
   299  		}
   300  	}
   301  	return file
   302  }
   303  
   304  func getLineNo(stepCache map[*gm.ScenarioInfo][]*stepInfo, step *gm.ProtoStep, info *gm.ExecutionInfo) string {
   305  	for _, si := range stepCache[info.CurrentScenario] {
   306  		if si.protoStep == step {
   307  			return strconv.Itoa(si.step.LineNo)
   308  		}
   309  	}
   310  	return ""
   311  }
   312  
   313  func getTable(scenario *gauge.Scenario) *tableInfo {
   314  	if scenario.SpecDataTableRow.IsInitialized() {
   315  		return &tableInfo{
   316  			Text: formatter.FormatTable(&scenario.SpecDataTableRow),
   317  			Row:  scenario.SpecDataTableRowIndex,
   318  		}
   319  	}
   320  	return nil
   321  }
   322  
   323  func getHookFailure(hookFailure []*gm.ProtoHookFailure, text string) *executionError {
   324  	if len(hookFailure) > 0 {
   325  		return &executionError{
   326  			Text:       text,
   327  			Message:    hookFailure[0].ErrorMessage,
   328  			StackTrace: hookFailure[0].StackTrace,
   329  		}
   330  	}
   331  	return nil
   332  }