diff --git a/fmt_cucumber.go b/fmt_cucumber.go index b839e51..d98480c 100644 --- a/fmt_cucumber.go +++ b/fmt_cucumber.go @@ -64,7 +64,7 @@ type cukeTag struct { type cukeResult struct { Status string `json:"status"` Error string `json:"error_message,omitempty"` - Duration int `json:"duration"` + Duration *int `json:"duration,omitempty"` } type cukeMatch struct { @@ -75,7 +75,7 @@ type cukeStep struct { Keyword string `json:"keyword"` Name string `json:"name"` Line int `json:"line"` - Docstring cukeDocstring `json:"doc_string,omitempty"` + Docstring *cukeDocstring `json:"doc_string,omitempty"` Match cukeMatch `json:"match"` Result cukeResult `json:"result"` } @@ -106,25 +106,25 @@ type cukeFeatureJSON struct { type cukefmt struct { basefmt - // currently running feature path, to be part of id. - // this is sadly not passed by gherkin nodes. - // it restricts this formatter to run only in synchronous single - // threaded execution. Unless running a copy of formatter for each feature - path string - stat stepType // last step status, before skipped - outlineSteps int // number of current outline scenario steps - ID string // current test id. - results []cukeFeatureJSON // structure that represent cuke results - curStep *cukeStep // track the current step - curElement *cukeElement // track the current element - curFeature *cukeFeatureJSON // track the current feature - curOutline cukeElement // Each example show up as an outline element but the outline is parsed only once - // so I need to keep track of the current outline - curRow int // current row of the example table as it is being processed. - curExampleTags []cukeTag // temporary storage for tags associate with the current example table. - startTime time.Time // used to time duration of the step execution - curExampleName string // Due to the fact that examples are parsed once and then iterated over for each result then we need to keep track - // of the example name inorder to build id fields. + // currently running feature path, to be part of id. + // this is sadly not passed by gherkin nodes. + // it restricts this formatter to run only in synchronous single + // threaded execution. Unless running a copy of formatter for each feature + path string + stat stepType // last step status, before skipped + outlineSteps int // number of current outline scenario steps + ID string // current test id. + results []cukeFeatureJSON // structure that represent cuke results + curStep *cukeStep // track the current step + curElement *cukeElement // track the current element + curFeature *cukeFeatureJSON // track the current feature + curOutline cukeElement // Each example show up as an outline element but the outline is parsed only once + // so I need to keep track of the current outline + curRow int // current row of the example table as it is being processed. + curExampleTags []cukeTag // temporary storage for tags associate with the current example table. + startTime time.Time // used to time duration of the step execution + curExampleName string // Due to the fact that examples are parsed once and then iterated over for each result then we need to keep track + // of the example name inorder to build id fields. } func (f *cukefmt) Node(n interface{}) { @@ -157,7 +157,7 @@ func (f *cukefmt) Node(n interface{}) { f.curOutline.Keyword = t.Keyword f.curOutline.ID = f.curFeature.ID + ";" + makeID(t.Name) f.curOutline.Type = "scenario" - f.curOutline.Tags = make([]cukeTag, len(t.Tags)+len(f.curFeature.Tags)) + f.curOutline.Tags = make([]cukeTag, len(t.Tags) + len(f.curFeature.Tags)) // apply feature level tags if len(f.curOutline.Tags) > 0 { @@ -165,15 +165,15 @@ func (f *cukefmt) Node(n interface{}) { // apply outline level tags. for idx, element := range t.Tags { - f.curOutline.Tags[idx+len(f.curFeature.Tags)].Line = element.Location.Line - f.curOutline.Tags[idx+len(f.curFeature.Tags)].Name = element.Name + f.curOutline.Tags[idx + len(f.curFeature.Tags)].Line = element.Location.Line + f.curOutline.Tags[idx + len(f.curFeature.Tags)].Name = element.Name } } // This scenario adds the element to the output immediately. case *gherkin.Scenario: f.curFeature.Elements = append(f.curFeature.Elements, cukeElement{}) - f.curElement = &f.curFeature.Elements[len(f.curFeature.Elements)-1] + f.curElement = &f.curFeature.Elements[len(f.curFeature.Elements) - 1] f.curElement.Name = t.Name f.curElement.Line = t.Location.Line @@ -181,7 +181,7 @@ func (f *cukefmt) Node(n interface{}) { f.curElement.Keyword = t.Keyword f.curElement.ID = f.curFeature.ID + ";" + makeID(t.Name) f.curElement.Type = "scenario" - f.curElement.Tags = make([]cukeTag, len(t.Tags)+len(f.curFeature.Tags)) + f.curElement.Tags = make([]cukeTag, len(t.Tags) + len(f.curFeature.Tags)) if len(f.curElement.Tags) > 0 { // apply feature level tags @@ -189,8 +189,8 @@ func (f *cukefmt) Node(n interface{}) { // apply scenario level tags. for idx, element := range t.Tags { - f.curElement.Tags[idx+len(f.curFeature.Tags)].Line = element.Location.Line - f.curElement.Tags[idx+len(f.curFeature.Tags)].Name = element.Name + f.curElement.Tags[idx + len(f.curFeature.Tags)].Line = element.Location.Line + f.curElement.Tags[idx + len(f.curFeature.Tags)].Name = element.Name } } @@ -202,7 +202,7 @@ func (f *cukefmt) Node(n interface{}) { tmpElem.ID = tmpElem.ID + ";" + f.curExampleName + ";" + strconv.Itoa(f.curRow) f.curRow++ f.curFeature.Elements = append(f.curFeature.Elements, tmpElem) - f.curElement = &f.curFeature.Elements[len(f.curFeature.Elements)-1] + f.curElement = &f.curFeature.Elements[len(f.curFeature.Elements) - 1] // copy in example level tags. f.curElement.Tags = append(f.curElement.Tags, f.curExampleTags...) @@ -218,7 +218,7 @@ func (f *cukefmt) Feature(ft *gherkin.Feature, p string, c []byte) { f.ID = makeID(ft.Name) f.results = append(f.results, cukeFeatureJSON{}) - f.curFeature = &f.results[len(f.results)-1] + f.curFeature = &f.results[len(f.results) - 1] f.curFeature.URI = p f.curFeature.Name = ft.Name f.curFeature.Keyword = ft.Keyword @@ -250,17 +250,20 @@ func (f *cukefmt) Summary() { func (f *cukefmt) step(res *stepResult) { + // determine if test case has finished switch t := f.owner.(type) { case *gherkin.TableRow: - f.curStep.Result.Duration = int(time.Since(f.startTime).Nanoseconds()) + d := int(time.Since(f.startTime).Nanoseconds()) + f.curStep.Result.Duration = &d f.curStep.Line = t.Location.Line f.curStep.Result.Status = res.typ.String() if res.err != nil { f.curStep.Result.Error = res.err.Error() } case *gherkin.Scenario: - f.curStep.Result.Duration = int(time.Since(f.startTime).Nanoseconds()) + d := int(time.Since(f.startTime).Nanoseconds()) + f.curStep.Result.Duration = &d f.curStep.Result.Status = res.typ.String() if res.err != nil { f.curStep.Result.Error = res.err.Error() @@ -272,13 +275,14 @@ func (f *cukefmt) Defined(step *gherkin.Step, def *StepDef) { f.startTime = time.Now() // start timing the step f.curElement.Steps = append(f.curElement.Steps, cukeStep{}) - f.curStep = &f.curElement.Steps[len(f.curElement.Steps)-1] + f.curStep = &f.curElement.Steps[len(f.curElement.Steps) - 1] f.curStep.Name = step.Text f.curStep.Line = step.Location.Line f.curStep.Keyword = step.Keyword if _, ok := step.Argument.(*gherkin.DocString); ok { + f.curStep.Docstring = &cukeDocstring{} f.curStep.Docstring.ContentType = strings.TrimSpace(step.Argument.(*gherkin.DocString).ContentType) f.curStep.Docstring.Line = step.Argument.(*gherkin.DocString).Location.Line f.curStep.Docstring.Value = step.Argument.(*gherkin.DocString).Content @@ -292,28 +296,39 @@ func (f *cukefmt) Defined(step *gherkin.Step, def *StepDef) { func (f *cukefmt) Passed(step *gherkin.Step, match *StepDef) { f.basefmt.Passed(step, match) f.stat = passed - f.step(f.passed[len(f.passed)-1]) + f.step(f.passed[len(f.passed) - 1]) } func (f *cukefmt) Skipped(step *gherkin.Step) { f.basefmt.Skipped(step) - f.step(f.skipped[len(f.skipped)-1]) + f.step(f.skipped[len(f.skipped) - 1]) + + // no duration reported for skipped. + f.curStep.Result.Duration = nil } func (f *cukefmt) Undefined(step *gherkin.Step) { f.basefmt.Undefined(step) f.stat = undefined - f.step(f.undefined[len(f.undefined)-1]) + f.step(f.undefined[len(f.undefined) - 1]) + + // the location for undefined is the feature file location not the step file. + f.curStep.Match.Location = fmt.Sprintf("%s:%d",f.path,step.Location.Line) + f.curStep.Result.Duration = nil } func (f *cukefmt) Failed(step *gherkin.Step, match *StepDef, err error) { f.basefmt.Failed(step, match, err) f.stat = failed - f.step(f.failed[len(f.failed)-1]) + f.step(f.failed[len(f.failed) - 1]) } func (f *cukefmt) Pending(step *gherkin.Step, match *StepDef) { f.stat = pending f.basefmt.Pending(step, match) - f.step(f.pending[len(f.pending)-1]) + f.step(f.pending[len(f.pending) - 1]) + + // the location for pending is the feature file location not the step file. + f.curStep.Match.Location = fmt.Sprintf("%s:%d",f.path,step.Location.Line) + f.curStep.Result.Duration = nil } diff --git a/suite_test.go b/suite_test.go index 8028fc4..00f05ab 100644 --- a/suite_test.go +++ b/suite_test.go @@ -401,9 +401,27 @@ func (s *suiteContext) theRenderJSONWillBe(docstring *gherkin.DocString) error { expectedArr := expected.([]interface{}) actualArr := actual.([]interface{}) + // Created to use in error reporting. + expectedCompact := &bytes.Buffer{} + actualCompact := &bytes.Buffer{} + json.Compact(expectedCompact,[]byte(docstring.Content)) + json.Compact(actualCompact,s.out.Bytes()) + for idx, entry := range expectedArr { + + // Make sure all of the expected are in the actual + if err := s.mapCompareStructure(entry.(map[string]interface{}), actualArr[idx].(map[string]interface{})); err != nil { + return fmt.Errorf("err:%v actual result is missing fields: expected:%s actual:%s",err,expectedCompact, actualCompact) + } + + // Make sure all of actual are in expected + if err := s.mapCompareStructure(actualArr[idx].(map[string]interface{}),entry.(map[string]interface{})); err != nil { + return fmt.Errorf("err:%v actual result contains too many fields: expected:%s actual:%s",err,expectedCompact, actualCompact) + } + + // Make sure the values are correct if err := s.mapCompare(entry.(map[string]interface{}), actualArr[idx].(map[string]interface{})); err != nil { - return err + return fmt.Errorf("err:%v values don't match expected:%s actual:%s",err,expectedCompact, actualCompact) } } return nil @@ -433,18 +451,38 @@ func (s *suiteContext) mapCompare(expected map[string]interface{}, actual map[st return err } } - // We need special rules to check location so that we are not bound to version of the code. + // We need special rules to check location so that we are not bound to version of the code. } else if k == "location" { - if !strings.Contains(actual[k].(string), "suite_test.go:") { - return fmt.Errorf("location has unexpected filename [%s] should contains suite_test.go", - actual[k]) + + // location is tricky. the cucumber value is either a the step def location for passed,failed, and skipped. + // it is the feature file location for undefined and skipped. + // I dont have the result context readily available so the expected input will have + // the context i need contained within its value. + // FEATURE_PATH myfile.feature:20 or + // STEP_ID + t := strings.Split(v.(string)," ") + if t[0] == "FEATURE_PATH" { + if actual[k].(string) != t[1]{ + return fmt.Errorf("location has unexpected value [%s] should be [%s]", + actual[k], t[1]) + } + + } else if t[0] == "STEP_ID" { + if !strings.Contains(actual[k].(string), "suite_test.go:") { + return fmt.Errorf("location has unexpected filename [%s] should contain suite_test.go", + actual[k]) + } + + } else { + return fmt.Errorf("Bad location value [%v]",v) } - // We need special rules to validate duration too. + + // We need special rules to validate duration too. } else if k == "duration" { if actual[k].(float64) <= 0 { return fmt.Errorf("duration is <= zero: actual:[%v]", actual[k]) } - // default numbers in json are coming as float64 + // default numbers in json are coming as float64 } else if reflect.TypeOf(v).Kind() == reflect.Float64 { if v.(float64) != actual[k].(float64) { if v.(float64) != actual[k].(float64) { @@ -465,3 +503,32 @@ func (s *suiteContext) mapCompare(expected map[string]interface{}, actual map[st return nil } +/* + Due to specialize matching logic to ignore exact matches on the "location" and "duration" fields. It was + necessary to create this compare function to validate the values of the map. +*/ +func (s *suiteContext) mapCompareStructure(expected map[string]interface{}, actual map[string]interface{}) error { + + // Process all keys in the map and handle them based on the type of the field. + for k, v := range expected { + + if actual[k] == nil { + return fmt.Errorf("Structure Mismatch: no matching field:[%s] expected value:[%v]",k, v) + } + // Process other maps via recursion + if reflect.TypeOf(v).Kind() == reflect.Map { + if err := s.mapCompareStructure(v.(map[string]interface{}), actual[k].(map[string]interface{})); err != nil { + return err + } + // This is an array of maps show as a slice + } else if reflect.TypeOf(v).Kind() == reflect.Slice { + for i, e := range v.([]interface{}) { + if err := s.mapCompareStructure(e.(map[string]interface{}), actual[k].([]interface{})[i].(map[string]interface{})); err != nil { + return err + } + } + } + } + + return nil +} \ No newline at end of file