From a201aed43219f37984aa64ddfc94c9924a05c93e Mon Sep 17 00:00:00 2001 From: "Stettler, Robert" Date: Fri, 17 Feb 2017 10:54:08 -0500 Subject: [PATCH 01/10] Save some work for cucumber format. --- fmt_cucumber.go | 223 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 223 insertions(+) create mode 100644 fmt_cucumber.go diff --git a/fmt_cucumber.go b/fmt_cucumber.go new file mode 100644 index 0000000..47d077a --- /dev/null +++ b/fmt_cucumber.go @@ -0,0 +1,223 @@ +package godog + +import ( + "fmt" + "io" + "time" + "strings" + "github.com/DATA-DOG/godog/gherkin" + "encoding/json" +) + +const cukeurl = "https://www.relishapp.com/cucumber/cucumber/docs/formatters/json-output-formatter" + +func init() { + Format("cucumber", fmt.Sprintf("Produces cucumber JSON stream, based on spec @: %s.", cukeurl), cucumberFunc) +} + +func cucumberFunc(suite string, out io.Writer) Formatter { + formatter := &cukefmt{ + basefmt: basefmt{ + started: time.Now(), + indent: 2, + out: out, + }, + } + + return formatter +} + +// Replace spaces with - +func makeId(name string) string { + return strings.Replace(strings.ToLower(name)," ","-",-1) +} + +type cukeTag struct { + Name string `json:"name"` + Line int `json:"line"` +} + +type cukeResult struct { + Status string `json:"status"` + Duration int `json:"duration"` + Error string `json:"error_message,omitempty"` +} + +type cukeMatch struct { + Location string `json:"location"` +} + +type cukeStep struct { + Keyword string `json:"keyword"` + Name string `json:"name"` + Line int `json:"line"` + Match cukeMatch `json:"match"` + Result cukeResult `json:"result"` +} + + +type cukeElement struct { + Keyword string `json:"keyword"` + Id string `json:"id"` + Name string `json:"name"` + Line int `json:"line"` + Description string `json:"description"` + Tags []cukeTag `json:"tags"` + Type string `json:type` + Steps []cukeStep `json:steps` + +} + +type cukeFeatureJson struct { + Uri string `json:"uri"` + Id string `json:"id"` + Keyword string `json:"keyword"` + Name string `json:"name"` + Line int `json:"line"` + Description string `json:"description"` + Tags []cukeTag `json:"tags"` + Elements []cukeElement `json:"elements"` + +} + +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 + +} + + +func (f *cukefmt) Node(n interface{}) { + f.basefmt.Node(n) + + switch t := n.(type) { + + case *gherkin.ScenarioOutline: + case *gherkin.Scenario: + f.curFeature.Elements = append(f.curFeature.Elements,cukeElement{}) + f.curElement = &f.curFeature.Elements[len(f.curFeature.Elements)-1] + + f.curElement.Name = t.Name + f.curElement.Line = t.Location.Line + f.curElement.Description = t.Description + f.curElement.Keyword = t.Keyword + f.curElement.Id = f.curFeature.Id+";"+makeId(t.Name) + f.curElement.Type = t.Type + f.curElement.Tags = make([]cukeTag,len(t.Tags)) + for idx,element := range t.Tags { + f.curElement.Tags[idx].Line = element.Location.Line + f.curElement.Tags[idx].Name = element.Name + } + + case *gherkin.TableRow: + fmt.Fprintf(f.out,"Entering Node TableRow: %s:%d\n",f.path, t.Location.Line) + } + +} + +func (f *cukefmt) Feature(ft *gherkin.Feature, p string, c []byte) { + + + f.basefmt.Feature(ft, p, c) + f.path = p + f.id = makeId(ft.Name) + f.results = append(f.results,cukeFeatureJson{}) + + f.curFeature = &f.results[len(f.results)-1] + f.curFeature.Uri = p + f.curFeature.Name = ft.Name + f.curFeature.Keyword = ft.Keyword + f.curFeature.Line = ft.Location.Line + f.curFeature.Description = ft.Description + f.curFeature.Id = f.id + f.curFeature.Tags = make([]cukeTag, len(ft.Tags)) + + for idx,element := range ft.Tags { + f.curFeature.Tags[idx].Line = element.Location.Line + f.curFeature.Tags[idx].Name = element.Name + } + +} + +func (f *cukefmt) Summary() { + dat, err := json.MarshalIndent(f.results, "", "\t") + if err != nil { + panic(err) + } + fmt.Fprintf(f.out,"%s\n",string(dat)) +} + +func (f *cukefmt) step(res *stepResult) { + + + // determine if test case has finished + var finished bool + var line int + switch t := f.owner.(type) { + case *gherkin.TableRow: + line = t.Location.Line + finished = f.isLastStep(res.step) + fmt.Fprintf(f.out,"step: TableRow: line:%v finished:%v\n",line, finished) + case *gherkin.Scenario: + f.curStep.Result.Status = res.typ.String() + if res.err != nil { + f.curStep.Result.Error = res.err.Error() + } + } +} + +func (f *cukefmt) Defined(step *gherkin.Step, def *StepDef) { + fmt.Fprintf(f.out,"Defined: step:%v stepDef:%v\n",step,def) + + f.curElement.Steps = append(f.curElement.Steps,cukeStep{}) + 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 def != nil { + f.curStep.Match.Location = strings.Split(def.definitionID()," ")[0] + } +} + +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]) +} + +func (f *cukefmt) Skipped(step *gherkin.Step) { + f.basefmt.Skipped(step) + f.step(f.skipped[len(f.skipped)-1]) +} + +func (f *cukefmt) Undefined(step *gherkin.Step) { + f.basefmt.Undefined(step) + f.stat = undefined + f.step(f.undefined[len(f.undefined)-1]) +} + +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]) +} + +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]) +} From 9820f49ceb004a2beca1ef7d4ff2442055102b78 Mon Sep 17 00:00:00 2001 From: "Stettler, Robert" Date: Fri, 17 Feb 2017 12:36:07 -0500 Subject: [PATCH 02/10] Added examples logic. --- fmt_cucumber.go | 181 ++++++++++++++++++++++++++++++++---------------- 1 file changed, 121 insertions(+), 60 deletions(-) diff --git a/fmt_cucumber.go b/fmt_cucumber.go index 47d077a..7069a0a 100644 --- a/fmt_cucumber.go +++ b/fmt_cucumber.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/DATA-DOG/godog/gherkin" "encoding/json" + "strconv" ) const cukeurl = "https://www.relishapp.com/cucumber/cucumber/docs/formatters/json-output-formatter" @@ -29,7 +30,7 @@ func cucumberFunc(suite string, out io.Writer) Formatter { // Replace spaces with - func makeId(name string) string { - return strings.Replace(strings.ToLower(name)," ","-",-1) + return strings.Replace(strings.ToLower(name), " ", "-", -1) } type cukeTag struct { @@ -38,104 +39,159 @@ type cukeTag struct { } type cukeResult struct { - Status string `json:"status"` + Status string `json:"status"` Duration int `json:"duration"` - Error string `json:"error_message,omitempty"` + Error string `json:"error_message,omitempty"` } type cukeMatch struct { Location string `json:"location"` } -type cukeStep struct { - Keyword string `json:"keyword"` - Name string `json:"name"` - Line int `json:"line"` - Match cukeMatch `json:"match"` - Result cukeResult `json:"result"` +type cukeExample struct { + Keyword string `json:"keyword"` + Id string `json:"id"` + Name string `json:"name"` + Line int `json:"line"` + Description string `json:"description"` + Rows []cukeRow `json:"rows"` } +type cukeRow struct { + Cells []string `json:"cells"` + Id string `json:"id"` + Line int `json:"line"` +} + +type cukeStep struct { + Keyword string `json:"keyword"` + Name string `json:"name"` + Line int `json:"line"` + Match cukeMatch `json:"match"` + Result cukeResult `json:"result"` +} type cukeElement struct { - Keyword string `json:"keyword"` - Id string `json:"id"` - Name string `json:"name"` - Line int `json:"line"` + Keyword string `json:"keyword"` + Id string `json:"id"` + Name string `json:"name"` + Line int `json:"line"` Description string `json:"description"` - Tags []cukeTag `json:"tags"` - Type string `json:type` - Steps []cukeStep `json:steps` - + Tags []cukeTag `json:"tags"` + Type string `json:type` + Steps []cukeStep `json:steps` + Examples []cukeExample `json:examples,omitempty` } type cukeFeatureJson struct { - Uri string `json:"uri"` - Id string `json:"id"` - Keyword string `json:"keyword"` - Name string `json:"name"` - Line int `json:"line"` + Uri string `json:"uri"` + Id string `json:"id"` + Keyword string `json:"keyword"` + Name string `json:"name"` + Line int `json:"line"` Description string `json:"description"` - Tags []cukeTag `json:"tags"` - Elements []cukeElement `json:"elements"` - + Tags []cukeTag `json:"tags"` + Elements []cukeElement `json:"elements"` } 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 + // 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. + 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 - + curStep *cukeStep // track the current step + curElement *cukeElement // track the current element + curFeature *cukeFeatureJson // track the current feature } - func (f *cukefmt) Node(n interface{}) { f.basefmt.Node(n) switch t := n.(type) { + case *gherkin.Examples: + ex := cukeExample{} + ex.Description = t.Description + ex.Id = f.curElement.Id + ";" + makeId(t.Name) + ex.Line = t.Location.Line + ex.Name = t.Name + ex.Keyword = t.Keyword + ex.Rows = make([]cukeRow,len(t.TableBody)+1) + + // first row is the header + ex.Rows[0].Line = t.TableHeader.Location.Line + ex.Rows[0].Cells = make([]string,len(t.TableHeader.Cells)) + ex.Rows[0].Id = ex.Id + ";1" + for idx, val := range t.TableHeader.Cells { + ex.Rows[0].Cells[idx] = val.Value + } + + // The other example are in the body + for i, row := range t.TableBody { + ex.Rows[1+i].Line = row.Location.Line + ex.Rows[1+i].Cells = make([]string,len(row.Cells)) + ex.Rows[1+i].Id = ex.Id + ";"+strconv.Itoa(i+2) + for idx, val := range row.Cells { + ex.Rows[1+i].Cells[idx] = val.Value + } + } + + f.curElement.Examples = append(f.curElement.Examples,ex) + case *gherkin.ScenarioOutline: - case *gherkin.Scenario: - f.curFeature.Elements = append(f.curFeature.Elements,cukeElement{}) - f.curElement = &f.curFeature.Elements[len(f.curFeature.Elements)-1] + f.curFeature.Elements = append(f.curFeature.Elements, cukeElement{}) + f.curElement = &f.curFeature.Elements[len(f.curFeature.Elements) - 1] f.curElement.Name = t.Name f.curElement.Line = t.Location.Line f.curElement.Description = t.Description f.curElement.Keyword = t.Keyword - f.curElement.Id = f.curFeature.Id+";"+makeId(t.Name) + f.curElement.Id = f.curFeature.Id + ";" + makeId(t.Name) f.curElement.Type = t.Type - f.curElement.Tags = make([]cukeTag,len(t.Tags)) - for idx,element := range t.Tags { + f.curElement.Tags = make([]cukeTag, len(t.Tags)) + for idx, element := range t.Tags { + f.curElement.Tags[idx].Line = element.Location.Line + f.curElement.Tags[idx].Name = element.Name + } + + case *gherkin.Scenario: + f.curFeature.Elements = append(f.curFeature.Elements, cukeElement{}) + f.curElement = &f.curFeature.Elements[len(f.curFeature.Elements) - 1] + + f.curElement.Name = t.Name + f.curElement.Line = t.Location.Line + f.curElement.Description = t.Description + f.curElement.Keyword = t.Keyword + f.curElement.Id = f.curFeature.Id + ";" + makeId(t.Name) + f.curElement.Type = t.Type + f.curElement.Tags = make([]cukeTag, len(t.Tags)) + for idx, element := range t.Tags { f.curElement.Tags[idx].Line = element.Location.Line f.curElement.Tags[idx].Name = element.Name } case *gherkin.TableRow: - fmt.Fprintf(f.out,"Entering Node TableRow: %s:%d\n",f.path, t.Location.Line) + + fmt.Fprintf(f.out, "Entering Node TableRow: %s:%d\n", f.path, t.Location.Line) } } func (f *cukefmt) Feature(ft *gherkin.Feature, p string, c []byte) { - f.basefmt.Feature(ft, p, c) f.path = p f.id = makeId(ft.Name) - f.results = append(f.results,cukeFeatureJson{}) + 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 @@ -144,7 +200,7 @@ func (f *cukefmt) Feature(ft *gherkin.Feature, p string, c []byte) { f.curFeature.Id = f.id f.curFeature.Tags = make([]cukeTag, len(ft.Tags)) - for idx,element := range ft.Tags { + for idx, element := range ft.Tags { f.curFeature.Tags[idx].Line = element.Location.Line f.curFeature.Tags[idx].Name = element.Name } @@ -156,12 +212,11 @@ func (f *cukefmt) Summary() { if err != nil { panic(err) } - fmt.Fprintf(f.out,"%s\n",string(dat)) + fmt.Fprintf(f.out, "%s\n", string(dat)) } func (f *cukefmt) step(res *stepResult) { - // determine if test case has finished var finished bool var line int @@ -169,7 +224,7 @@ func (f *cukefmt) step(res *stepResult) { case *gherkin.TableRow: line = t.Location.Line finished = f.isLastStep(res.step) - fmt.Fprintf(f.out,"step: TableRow: line:%v finished:%v\n",line, finished) + fmt.Fprintf(f.out, "step: TableRow: line:%v finished:%v\n", line, finished) case *gherkin.Scenario: f.curStep.Result.Status = res.typ.String() if res.err != nil { @@ -179,45 +234,51 @@ func (f *cukefmt) step(res *stepResult) { } func (f *cukefmt) Defined(step *gherkin.Step, def *StepDef) { - fmt.Fprintf(f.out,"Defined: step:%v stepDef:%v\n",step,def) + fmt.Fprintf(f.out, "Defined: step:%v stepDef:%v\n", step, def) - f.curElement.Steps = append(f.curElement.Steps,cukeStep{}) - f.curStep = &f.curElement.Steps[len(f.curElement.Steps)-1] + f.curElement.Steps = append(f.curElement.Steps, cukeStep{}) + f.curStep = &f.curElement.Steps[len(f.curElement.Steps) - 1] + + if def != nil { + if def.args != nil { + fmt.Fprintf(f.out, "Argument: %v\n", def.args) + } + } f.curStep.Name = step.Text f.curStep.Line = step.Location.Line f.curStep.Keyword = step.Keyword if def != nil { - f.curStep.Match.Location = strings.Split(def.definitionID()," ")[0] + f.curStep.Match.Location = strings.Split(def.definitionID(), " ")[0] } } 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]) } 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]) } 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]) } From afc629be69c09ab50682282c1e577ac142a56be6 Mon Sep 17 00:00:00 2001 From: "Stettler, Robert" Date: Tue, 21 Feb 2017 10:45:05 -0500 Subject: [PATCH 03/10] Added examples logic. --- fmt_cucumber.go | 82 ++++++++++++++++--------------------------------- 1 file changed, 26 insertions(+), 56 deletions(-) diff --git a/fmt_cucumber.go b/fmt_cucumber.go index 7069a0a..b3ed9eb 100644 --- a/fmt_cucumber.go +++ b/fmt_cucumber.go @@ -7,7 +7,6 @@ import ( "strings" "github.com/DATA-DOG/godog/gherkin" "encoding/json" - "strconv" ) const cukeurl = "https://www.relishapp.com/cucumber/cucumber/docs/formatters/json-output-formatter" @@ -48,20 +47,6 @@ type cukeMatch struct { Location string `json:"location"` } -type cukeExample struct { - Keyword string `json:"keyword"` - Id string `json:"id"` - Name string `json:"name"` - Line int `json:"line"` - Description string `json:"description"` - Rows []cukeRow `json:"rows"` -} - -type cukeRow struct { - Cells []string `json:"cells"` - Id string `json:"id"` - Line int `json:"line"` -} type cukeStep struct { Keyword string `json:"keyword"` @@ -78,9 +63,8 @@ type cukeElement struct { Line int `json:"line"` Description string `json:"description"` Tags []cukeTag `json:"tags"` - Type string `json:type` - Steps []cukeStep `json:steps` - Examples []cukeExample `json:examples,omitempty` + Type string `json:"type"` + Steps []cukeStep `json:"steps"` } type cukeFeatureJson struct { @@ -109,6 +93,8 @@ type cukefmt struct { 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 } func (f *cukefmt) Node(n interface{}) { @@ -117,44 +103,26 @@ func (f *cukefmt) Node(n interface{}) { switch t := n.(type) { case *gherkin.Examples: - ex := cukeExample{} - ex.Description = t.Description - ex.Id = f.curElement.Id + ";" + makeId(t.Name) - ex.Line = t.Location.Line - ex.Name = t.Name - ex.Keyword = t.Keyword - ex.Rows = make([]cukeRow,len(t.TableBody)+1) + fmt.Fprintln("Examples") + saveElement := f.curElement - // first row is the header - ex.Rows[0].Line = t.TableHeader.Location.Line - ex.Rows[0].Cells = make([]string,len(t.TableHeader.Cells)) - ex.Rows[0].Id = ex.Id + ";1" - for idx, val := range t.TableHeader.Cells { - ex.Rows[0].Cells[idx] = val.Value - } + f.curElement.Id = f.curElement.Id + ";" + makeId(t.Name) + f.curFeature.Elements = append(f.curFeature.Elements, f.curElement) - // The other example are in the body - for i, row := range t.TableBody { - ex.Rows[1+i].Line = row.Location.Line - ex.Rows[1+i].Cells = make([]string,len(row.Cells)) - ex.Rows[1+i].Id = ex.Id + ";"+strconv.Itoa(i+2) - for idx, val := range row.Cells { - ex.Rows[1+i].Cells[idx] = val.Value - } - } - - f.curElement.Examples = append(f.curElement.Examples,ex) + f.curElement = saveElement case *gherkin.ScenarioOutline: - f.curFeature.Elements = append(f.curFeature.Elements, cukeElement{}) - f.curElement = &f.curFeature.Elements[len(f.curFeature.Elements) - 1] + fmt.Fprintln("Outline") + //f.curFeature.Elements = append(f.curFeature.Elements, cukeElement{}) + //f.curElement = &f.curFeature.Elements[len(f.curFeature.Elements) - 1] + f.curElement=cukeElement{} f.curElement.Name = t.Name f.curElement.Line = t.Location.Line f.curElement.Description = t.Description f.curElement.Keyword = t.Keyword f.curElement.Id = f.curFeature.Id + ";" + makeId(t.Name) - f.curElement.Type = t.Type + f.curElement.Type = "scenario" f.curElement.Tags = make([]cukeTag, len(t.Tags)) for idx, element := range t.Tags { f.curElement.Tags[idx].Line = element.Location.Line @@ -162,24 +130,26 @@ func (f *cukefmt) Node(n interface{}) { } case *gherkin.Scenario: - f.curFeature.Elements = append(f.curFeature.Elements, cukeElement{}) - f.curElement = &f.curFeature.Elements[len(f.curFeature.Elements) - 1] + fmt.Fprintln("Scenario") + f.curElement = cukeElement{} f.curElement.Name = t.Name f.curElement.Line = t.Location.Line f.curElement.Description = t.Description f.curElement.Keyword = t.Keyword f.curElement.Id = f.curFeature.Id + ";" + makeId(t.Name) - f.curElement.Type = t.Type + f.curElement.Type = "scenario" f.curElement.Tags = make([]cukeTag, len(t.Tags)) for idx, element := range t.Tags { f.curElement.Tags[idx].Line = element.Location.Line f.curElement.Tags[idx].Name = element.Name } + f.curFeature.Elements = append(f.curFeature.Elements, f.curElement) case *gherkin.TableRow: + lastElement := &f.curFeature.Elements[len(f.curFeature.Elements) - 1] + lastElement.Id = lastElement.Id + ";cnt" - fmt.Fprintf(f.out, "Entering Node TableRow: %s:%d\n", f.path, t.Location.Line) } } @@ -218,13 +188,13 @@ func (f *cukefmt) Summary() { func (f *cukefmt) step(res *stepResult) { // determine if test case has finished - var finished bool - var line int - switch t := f.owner.(type) { + switch t:= f.owner.(type) { case *gherkin.TableRow: - line = t.Location.Line - finished = f.isLastStep(res.step) - fmt.Fprintf(f.out, "step: TableRow: line:%v finished:%v\n", line, finished) + 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.Status = res.typ.String() if res.err != nil { From 7c9f5bfb023d023538230b20ef671f2aff367be8 Mon Sep 17 00:00:00 2001 From: "Stettler, Robert" Date: Wed, 22 Feb 2017 09:39:21 -0500 Subject: [PATCH 04/10] Added comments to feature node. Fixed duration Fix defect is example id --- fmt_cucumber.go | 158 ++++++++++++++++++++++++++++++------------------ 1 file changed, 98 insertions(+), 60 deletions(-) diff --git a/fmt_cucumber.go b/fmt_cucumber.go index b3ed9eb..ff3d131 100644 --- a/fmt_cucumber.go +++ b/fmt_cucumber.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/DATA-DOG/godog/gherkin" "encoding/json" + "strconv" ) const cukeurl = "https://www.relishapp.com/cucumber/cucumber/docs/formatters/json-output-formatter" @@ -32,6 +33,10 @@ func makeId(name string) string { return strings.Replace(strings.ToLower(name), " ", "-", -1) } +type cukeComment struct { + Value string `json:"value"` + Line int `json:"line"` +} type cukeTag struct { Name string `json:"name"` Line int `json:"line"` @@ -39,15 +44,14 @@ type cukeTag struct { type cukeResult struct { Status string `json:"status"` - Duration int `json:"duration"` Error string `json:"error_message,omitempty"` + Duration int `json:"duration"` } type cukeMatch struct { Location string `json:"location"` } - type cukeStep struct { Keyword string `json:"keyword"` Name string `json:"name"` @@ -57,13 +61,13 @@ type cukeStep struct { } type cukeElement struct { - Keyword string `json:"keyword"` Id string `json:"id"` + Keyword string `json:"keyword"` Name string `json:"name"` - Line int `json:"line"` Description string `json:"description"` - Tags []cukeTag `json:"tags"` + Line int `json:"line"` Type string `json:"type"` + Tags []cukeTag `json:"tags,omitempty"` Steps []cukeStep `json:"steps"` } @@ -72,29 +76,34 @@ type cukeFeatureJson struct { Id string `json:"id"` Keyword string `json:"keyword"` Name string `json:"name"` - Line int `json:"line"` Description string `json:"description"` - Tags []cukeTag `json:"tags"` + Line int `json:"line"` + Comments []cukeComment `json:"comments,omitempty"` + Tags []cukeTag `json:"tags,omitempty"` Elements []cukeElement `json:"elements"` } 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 + // 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 + curExampleName string } func (f *cukefmt) Node(n interface{}) { @@ -102,53 +111,80 @@ func (f *cukefmt) Node(n interface{}) { switch t := n.(type) { + // When the example definition is seen we just need track the id and + // append the name associated with the example as part of the id. case *gherkin.Examples: - fmt.Fprintln("Examples") - saveElement := f.curElement + f.curExampleName = makeId(t.Name) + f.curRow = 2 // there can be more than one example set per outline so reset row count. + // cucumber counts the header row as an example when creating the id. - f.curElement.Id = f.curElement.Id + ";" + makeId(t.Name) - f.curFeature.Elements = append(f.curFeature.Elements, f.curElement) - - f.curElement = saveElement + // store any example level tags in a temp location. + f.curExampleTags = make([]cukeTag, len(t.Tags)) + for idx, element := range t.Tags { + f.curExampleTags[idx].Line = element.Location.Line + f.curExampleTags[idx].Name = element.Name + } + // The outline node creates a placeholder and the actual element is added as each TableRow is processed. case *gherkin.ScenarioOutline: - fmt.Fprintln("Outline") - //f.curFeature.Elements = append(f.curFeature.Elements, cukeElement{}) - //f.curElement = &f.curFeature.Elements[len(f.curFeature.Elements) - 1] - f.curElement=cukeElement{} - f.curElement.Name = t.Name - f.curElement.Line = t.Location.Line - f.curElement.Description = t.Description - 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)) - for idx, element := range t.Tags { - f.curElement.Tags[idx].Line = element.Location.Line - f.curElement.Tags[idx].Name = element.Name + f.curOutline = cukeElement{} + f.curOutline.Name = t.Name + f.curOutline.Line = t.Location.Line + f.curOutline.Description = t.Description + 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)) + + // apply feature level tags + if (len(f.curOutline.Tags) > 0) { + copy(f.curOutline.Tags, f.curFeature.Tags) + + // 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 + } } + // This scenario adds the element to the output immediately. case *gherkin.Scenario: - fmt.Fprintln("Scenario") + f.curFeature.Elements = append(f.curFeature.Elements, cukeElement{}) + f.curElement = &f.curFeature.Elements[len(f.curFeature.Elements) - 1] - f.curElement = cukeElement{} f.curElement.Name = t.Name f.curElement.Line = t.Location.Line f.curElement.Description = t.Description 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)) - for idx, element := range t.Tags { - f.curElement.Tags[idx].Line = element.Location.Line - f.curElement.Tags[idx].Name = element.Name - } - f.curFeature.Elements = append(f.curFeature.Elements, f.curElement) + f.curElement.Tags = make([]cukeTag, len(t.Tags) + len(f.curFeature.Tags)) + if (len(f.curElement.Tags) > 0) { + // apply feature level tags + copy(f.curElement.Tags, f.curFeature.Tags) + + // 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 + } + } + + + // This is an outline scenario and the element is added to the output as + // the TableRows are encountered. case *gherkin.TableRow: - lastElement := &f.curFeature.Elements[len(f.curFeature.Elements) - 1] - lastElement.Id = lastElement.Id + ";cnt" + tmpElem := f.curOutline + tmpElem.Line = t.Location.Line + 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] + + // copy in example level tags. + f.curElement.Tags = append(f.curElement.Tags, f.curExampleTags...) } @@ -175,10 +211,16 @@ func (f *cukefmt) Feature(ft *gherkin.Feature, p string, c []byte) { f.curFeature.Tags[idx].Name = element.Name } + f.curFeature.Comments = make([]cukeComment, len(ft.Comments)) + for idx, comment := range ft.Comments { + f.curFeature.Comments[idx].Value = comment.Text + f.curFeature.Comments[idx].Line = comment.Location.Line + } + } func (f *cukefmt) Summary() { - dat, err := json.MarshalIndent(f.results, "", "\t") + dat, err := json.MarshalIndent(f.results, "", " ") if err != nil { panic(err) } @@ -188,14 +230,16 @@ func (f *cukefmt) Summary() { func (f *cukefmt) step(res *stepResult) { // determine if test case has finished - switch t:= f.owner.(type) { + switch t := f.owner.(type) { case *gherkin.TableRow: + f.curStep.Result.Duration = int(time.Since(f.startTime).Nanoseconds()) 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()) f.curStep.Result.Status = res.typ.String() if res.err != nil { f.curStep.Result.Error = res.err.Error() @@ -204,17 +248,11 @@ func (f *cukefmt) step(res *stepResult) { } func (f *cukefmt) Defined(step *gherkin.Step, def *StepDef) { - fmt.Fprintf(f.out, "Defined: step:%v stepDef:%v\n", step, def) + 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] - if def != nil { - if def.args != nil { - fmt.Fprintf(f.out, "Argument: %v\n", def.args) - } - } - f.curStep.Name = step.Text f.curStep.Line = step.Location.Line f.curStep.Keyword = step.Keyword From 061d3a3b74c64e8cff64338e639b94f6e8f5f554 Mon Sep 17 00:00:00 2001 From: "Stettler, Robert" Date: Thu, 23 Feb 2017 11:49:04 -0500 Subject: [PATCH 05/10] Added support for Docstirngs Updated lang.feature with the additional cucumber.feature Updated load.feature with the additional cucumber.feature Updated events.feature to account for ripple effect of updating lang and load Updated run.go to indicate that cucumber formatter does not support concurrent. Updated suite_test.go to handle one new testing step. --- features/formatter/events.feature | 4 +- features/lang.feature | 3 +- features/load.feature | 3 +- fmt_cucumber.go | 151 ++++++++++++++++++------------ run.go | 3 + suite_test.go | 110 +++++++++++++++++++++- 6 files changed, 205 insertions(+), 69 deletions(-) diff --git a/features/formatter/events.feature b/features/formatter/events.feature index d9c2b2a..cd99b33 100644 --- a/features/formatter/events.feature +++ b/features/formatter/events.feature @@ -14,7 +14,7 @@ Feature: event stream formatter """ Scenario: should process simple scenario - Given a feature path "features/load.feature:21" + Given a feature path "features/load.feature:22" When I run feature suite with formatter "events" Then the following events should be fired: """ @@ -35,7 +35,7 @@ Feature: event stream formatter """ Scenario: should process outline scenario - Given a feature path "features/load.feature:29" + Given a feature path "features/load.feature:30" When I run feature suite with formatter "events" Then the following events should be fired: """ diff --git a/features/lang.feature b/features/lang.feature index 0cf35a0..829a939 100644 --- a/features/lang.feature +++ b/features/lang.feature @@ -8,10 +8,11 @@ Savybė: užkrauti savybes Scenarijus: savybių užkrovimas iš aplanko Duota savybių aplankas "features" Kai aš išskaitau savybes - Tada aš turėčiau turėti 8 savybių failus: + Tada aš turėčiau turėti 9 savybių failus: """ features/background.feature features/events.feature + features/formatter/cucumber.feature features/formatter/events.feature features/lang.feature features/load.feature diff --git a/features/load.feature b/features/load.feature index f15cee1..22ed683 100644 --- a/features/load.feature +++ b/features/load.feature @@ -6,10 +6,11 @@ Feature: load features Scenario: load features within path Given a feature path "features" When I parse features - Then I should have 8 feature files: + Then I should have 9 feature files: """ features/background.feature features/events.feature + features/formatter/cucumber.feature features/formatter/events.feature features/lang.feature features/load.feature diff --git a/fmt_cucumber.go b/fmt_cucumber.go index ff3d131..0824d34 100644 --- a/fmt_cucumber.go +++ b/fmt_cucumber.go @@ -1,12 +1,22 @@ package godog +/* + The specification for the formatting originated from https://www.relishapp.com/cucumber/cucumber/docs/formatters/json-output-formatter. + I found that documentation was misleading or out dated. To validate formatting I create a ruby cucumber test harness and ran the + same feature files through godog and the ruby cucumber. + + The docstrings in the cucumber.feature represent the cucumber output for those same feature definitions. + + I did note that comments in ruby could be at just about any level in particular Feature, Scenario and Step. In godog I + could only find comments under the Feature data structure. + */ import ( "fmt" "io" "time" "strings" - "github.com/DATA-DOG/godog/gherkin" "encoding/json" + "github.com/DATA-DOG/godog/gherkin" "strconv" ) @@ -28,24 +38,32 @@ func cucumberFunc(suite string, out io.Writer) Formatter { return formatter } -// Replace spaces with - +// Replace spaces with - This function is used to create the "id" fields of the cucumber output. func makeId(name string) string { return strings.Replace(strings.ToLower(name), " ", "-", -1) } +// The sequence of type structs are used to marshall the json object. type cukeComment struct { Value string `json:"value"` - Line int `json:"line"` + Line int `json:"line"` } + +type cukeDocstring struct { + Value string `json:"value"` + ContentType string `json:"content_type"` + Line int `json:"line"` +} + type cukeTag struct { Name string `json:"name"` - Line int `json:"line"` + Line int `json:"line"` } type cukeResult struct { Status string `json:"status"` Error string `json:"error_message,omitempty"` - Duration int `json:"duration"` + Duration int `json:"duration"` } type cukeMatch struct { @@ -53,57 +71,59 @@ type cukeMatch struct { } type cukeStep struct { - Keyword string `json:"keyword"` - Name string `json:"name"` - Line int `json:"line"` - Match cukeMatch `json:"match"` + Keyword string `json:"keyword"` + Name string `json:"name"` + Line int `json:"line"` + Docstring cukeDocstring `json:"doc_string,omitempty"` + Match cukeMatch `json:"match"` Result cukeResult `json:"result"` } type cukeElement struct { - Id string `json:"id"` - Keyword string `json:"keyword"` - Name string `json:"name"` - Description string `json:"description"` - Line int `json:"line"` - Type string `json:"type"` - Tags []cukeTag `json:"tags,omitempty"` - Steps []cukeStep `json:"steps"` + Id string `json:"id"` + Keyword string `json:"keyword"` + Name string `json:"name"` + Description string `json:"description"` + Line int `json:"line"` + Type string `json:"type"` + Tags []cukeTag `json:"tags,omitempty"` + Steps []cukeStep `json:"steps,omitempty"` } type cukeFeatureJson struct { - Uri string `json:"uri"` - Id string `json:"id"` - Keyword string `json:"keyword"` - Name string `json:"name"` - Description string `json:"description"` - Line int `json:"line"` - Comments []cukeComment `json:"comments,omitempty"` - Tags []cukeTag `json:"tags,omitempty"` - Elements []cukeElement `json:"elements"` + Uri string `json:"uri"` + Id string `json:"id"` + Keyword string `json:"keyword"` + Name string `json:"name"` + Description string `json:"description"` + Line int `json:"line"` + Comments []cukeComment `json:"comments,omitempty"` + Tags []cukeTag `json:"tags,omitempty"` + Elements []cukeElement `json:"elements,omitempty"` } 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 - curExampleName string + // 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{}) { @@ -114,6 +134,7 @@ func (f *cukefmt) Node(n interface{}) { // When the example definition is seen we just need track the id and // append the name associated with the example as part of the id. case *gherkin.Examples: + f.curExampleName = makeId(t.Name) f.curRow = 2 // there can be more than one example set per outline so reset row count. // cucumber counts the header row as an example when creating the id. @@ -135,23 +156,23 @@ 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) { + if len(f.curOutline.Tags) > 0 { copy(f.curOutline.Tags, f.curFeature.Tags) // 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 @@ -159,16 +180,16 @@ 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) { + if len(f.curElement.Tags) > 0 { // apply feature level tags copy(f.curElement.Tags, f.curFeature.Tags) // 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 } } @@ -181,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...) @@ -197,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 @@ -213,7 +234,7 @@ func (f *cukefmt) Feature(ft *gherkin.Feature, p string, c []byte) { f.curFeature.Comments = make([]cukeComment, len(ft.Comments)) for idx, comment := range ft.Comments { - f.curFeature.Comments[idx].Value = comment.Text + f.curFeature.Comments[idx].Value = strings.TrimSpace(comment.Text) f.curFeature.Comments[idx].Line = comment.Location.Line } @@ -251,12 +272,18 @@ 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.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 + } + if def != nil { f.curStep.Match.Location = strings.Split(def.definitionID(), " ")[0] } @@ -265,28 +292,28 @@ 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]) } 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]) } 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]) } diff --git a/run.go b/run.go index eea6b6a..cb73447 100644 --- a/run.go +++ b/run.go @@ -157,6 +157,9 @@ func supportsConcurrency(format string) bool { return false case "pretty": return false + case "cucumber": + return false } + return true // all custom formatters are treated as supporting concurrency } diff --git a/suite_test.go b/suite_test.go index 638a0c6..36e1d7f 100644 --- a/suite_test.go +++ b/suite_test.go @@ -4,13 +4,13 @@ import ( "bytes" "encoding/json" "fmt" + "github.com/DATA-DOG/godog/gherkin" "os" "path/filepath" + "reflect" "strconv" "strings" "testing" - - "github.com/DATA-DOG/godog/gherkin" ) func TestMain(m *testing.M) { @@ -66,6 +66,10 @@ func SuiteContext(s *Suite) { s.Step(`^passing step$`, func() error { return nil }) + + // Introduced to test formatter/cucumber.feature + s.Step(`^the rendered json will be as follows:$`, c.theRenderJsonWillBe) + } type firedEvent struct { @@ -185,7 +189,7 @@ func (s *suiteContext) followingStepsShouldHave(status string, steps *gherkin.Do } if len(expected) > len(actual) { - return fmt.Errorf("number of expected %s steps: %d is less than actual %s steps: %d", status, len(expected), status, len(actual)) + return fmt.Errorf("number of expeted %s steps: %d is less than actual %s steps: %d", status, len(expected), status, len(actual)) } for _, a := range actual { @@ -376,3 +380,103 @@ func (s *suiteContext) theseEventsHadToBeFiredForNumberOfTimes(tbl *gherkin.Data } return nil } + +func (s *suiteContext) theRenderJsonWillBe2(docstring *gherkin.DocString) error { + + var expected bytes.Buffer + var actual bytes.Buffer + + if err := json.Compact(&expected, []byte(docstring.Content)); err != nil { + return err + } + + if err := json.Compact(&actual, s.out.Bytes()); err != nil { + return err + } + + if string(expected.Bytes()) != string(actual.Bytes()) { + return fmt.Errorf("format mismatch expected:[%v] actual:[%v]", string(expected.Bytes()), string(actual.Bytes())) + } + + return nil +} + +func (s *suiteContext) theRenderJsonWillBe(docstring *gherkin.DocString) error { + + var expected interface{} + if err := json.Unmarshal([]byte(docstring.Content), &expected); err != nil { + return err + } + + var actual interface{} + if err := json.Unmarshal(s.out.Bytes(), &actual); err != nil { + return err + } + + expectedArr := expected.([]interface{}) + actualArr := actual.([]interface{}) + + for idx, entry := range expectedArr { + if err := s.mapCompare(entry.(map[string]interface{}), actualArr[idx].(map[string]interface{})); err != nil { + return err + } + } + 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) mapCompare(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("No matching field in actual:[%s] expected value:[%v]",k,v) + } + // Process other maps via recursion + if reflect.TypeOf(v).Kind() == reflect.Map { + if err := s.mapCompare(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.mapCompare(e.(map[string]interface{}), actual[k].([]interface{})[i].(map[string]interface{})); err != nil { + return err + } + } + // 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]) + } + // 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 + } else if reflect.TypeOf(v).Kind() == reflect.Float64 { + if v.(float64) != actual[k].(float64) { + if v.(float64) != actual[k].(float64) { + return fmt.Errorf("Field:[%s] not matching expected:[%v] actual:[%v]", + k, v, actual[k]) + } + } + + } else if reflect.TypeOf(v).Kind() == reflect.String { + if v.(string) != actual[k].(string) { + return fmt.Errorf("Field:[%s] not matching expected:[%v] actual:[%v]", + k, v, actual[k]) + } + } else { + return fmt.Errorf("Unexepcted type encountered in json at key:[%s] Type:[%v]", k, reflect.TypeOf(v).Kind()) + } + } + + return nil +} From 00836409e43bec3a5e9244da701460b5a6244120 Mon Sep 17 00:00:00 2001 From: "Stettler, Robert" Date: Fri, 24 Feb 2017 06:48:46 -0500 Subject: [PATCH 06/10] Made updates based on golint report. --- fmt_cucumber.go | 62 ++++++++++++++++++++++++------------------------- suite_test.go | 26 +++------------------ 2 files changed, 34 insertions(+), 54 deletions(-) diff --git a/fmt_cucumber.go b/fmt_cucumber.go index 0824d34..b839e51 100644 --- a/fmt_cucumber.go +++ b/fmt_cucumber.go @@ -1,4 +1,5 @@ package godog + /* The specification for the formatting originated from https://www.relishapp.com/cucumber/cucumber/docs/formatters/json-output-formatter. I found that documentation was misleading or out dated. To validate formatting I create a ruby cucumber test harness and ran the @@ -8,16 +9,16 @@ package godog I did note that comments in ruby could be at just about any level in particular Feature, Scenario and Step. In godog I could only find comments under the Feature data structure. - */ +*/ import ( - "fmt" - "io" - "time" - "strings" "encoding/json" + "fmt" "github.com/DATA-DOG/godog/gherkin" + "io" "strconv" + "strings" + "time" ) const cukeurl = "https://www.relishapp.com/cucumber/cucumber/docs/formatters/json-output-formatter" @@ -39,7 +40,7 @@ func cucumberFunc(suite string, out io.Writer) Formatter { } // Replace spaces with - This function is used to create the "id" fields of the cucumber output. -func makeId(name string) string { +func makeID(name string) string { return strings.Replace(strings.ToLower(name), " ", "-", -1) } @@ -50,9 +51,9 @@ type cukeComment struct { } type cukeDocstring struct { - Value string `json:"value"` + Value string `json:"value"` ContentType string `json:"content_type"` - Line int `json:"line"` + Line int `json:"line"` } type cukeTag struct { @@ -71,16 +72,16 @@ type cukeMatch struct { } type cukeStep struct { - Keyword string `json:"keyword"` - Name string `json:"name"` - Line int `json:"line"` + Keyword string `json:"keyword"` + Name string `json:"name"` + Line int `json:"line"` Docstring cukeDocstring `json:"doc_string,omitempty"` - Match cukeMatch `json:"match"` - Result cukeResult `json:"result"` + Match cukeMatch `json:"match"` + Result cukeResult `json:"result"` } type cukeElement struct { - Id string `json:"id"` + ID string `json:"id"` Keyword string `json:"keyword"` Name string `json:"name"` Description string `json:"description"` @@ -90,9 +91,9 @@ type cukeElement struct { Steps []cukeStep `json:"steps,omitempty"` } -type cukeFeatureJson struct { - Uri string `json:"uri"` - Id string `json:"id"` +type cukeFeatureJSON struct { + URI string `json:"uri"` + ID string `json:"id"` Keyword string `json:"keyword"` Name string `json:"name"` Description string `json:"description"` @@ -112,18 +113,18 @@ type cukefmt struct { 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 + 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 + 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. + 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{}) { @@ -135,7 +136,7 @@ func (f *cukefmt) Node(n interface{}) { // append the name associated with the example as part of the id. case *gherkin.Examples: - f.curExampleName = makeId(t.Name) + f.curExampleName = makeID(t.Name) f.curRow = 2 // there can be more than one example set per outline so reset row count. // cucumber counts the header row as an example when creating the id. @@ -154,7 +155,7 @@ func (f *cukefmt) Node(n interface{}) { f.curOutline.Line = t.Location.Line f.curOutline.Description = t.Description f.curOutline.Keyword = t.Keyword - f.curOutline.Id = f.curFeature.Id + ";" + makeId(t.Name) + 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)) @@ -178,7 +179,7 @@ func (f *cukefmt) Node(n interface{}) { f.curElement.Line = t.Location.Line f.curElement.Description = t.Description f.curElement.Keyword = t.Keyword - f.curElement.Id = f.curFeature.Id + ";" + makeId(t.Name) + 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)) @@ -193,13 +194,12 @@ func (f *cukefmt) Node(n interface{}) { } } - // This is an outline scenario and the element is added to the output as // the TableRows are encountered. case *gherkin.TableRow: tmpElem := f.curOutline tmpElem.Line = t.Location.Line - tmpElem.Id = tmpElem.Id + ";" + f.curExampleName + ";" + strconv.Itoa(f.curRow) + 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] @@ -215,16 +215,16 @@ func (f *cukefmt) Feature(ft *gherkin.Feature, p string, c []byte) { f.basefmt.Feature(ft, p, c) f.path = p - f.id = makeId(ft.Name) - f.results = append(f.results, cukeFeatureJson{}) + f.ID = makeID(ft.Name) + f.results = append(f.results, cukeFeatureJSON{}) f.curFeature = &f.results[len(f.results)-1] - f.curFeature.Uri = p + f.curFeature.URI = p f.curFeature.Name = ft.Name f.curFeature.Keyword = ft.Keyword f.curFeature.Line = ft.Location.Line f.curFeature.Description = ft.Description - f.curFeature.Id = f.id + f.curFeature.ID = f.ID f.curFeature.Tags = make([]cukeTag, len(ft.Tags)) for idx, element := range ft.Tags { diff --git a/suite_test.go b/suite_test.go index 36e1d7f..0c2a9eb 100644 --- a/suite_test.go +++ b/suite_test.go @@ -68,7 +68,7 @@ func SuiteContext(s *Suite) { }) // Introduced to test formatter/cucumber.feature - s.Step(`^the rendered json will be as follows:$`, c.theRenderJsonWillBe) + s.Step(`^the rendered json will be as follows:$`, c.theRenderJSONWillBe) } @@ -381,27 +381,7 @@ func (s *suiteContext) theseEventsHadToBeFiredForNumberOfTimes(tbl *gherkin.Data return nil } -func (s *suiteContext) theRenderJsonWillBe2(docstring *gherkin.DocString) error { - - var expected bytes.Buffer - var actual bytes.Buffer - - if err := json.Compact(&expected, []byte(docstring.Content)); err != nil { - return err - } - - if err := json.Compact(&actual, s.out.Bytes()); err != nil { - return err - } - - if string(expected.Bytes()) != string(actual.Bytes()) { - return fmt.Errorf("format mismatch expected:[%v] actual:[%v]", string(expected.Bytes()), string(actual.Bytes())) - } - - return nil -} - -func (s *suiteContext) theRenderJsonWillBe(docstring *gherkin.DocString) error { +func (s *suiteContext) theRenderJSONWillBe(docstring *gherkin.DocString) error { var expected interface{} if err := json.Unmarshal([]byte(docstring.Content), &expected); err != nil { @@ -434,7 +414,7 @@ func (s *suiteContext) mapCompare(expected map[string]interface{}, actual map[st for k, v := range expected { if actual[k] == nil { - return fmt.Errorf("No matching field in actual:[%s] expected value:[%v]",k,v) + return fmt.Errorf("No matching field in actual:[%s] expected value:[%v]", k, v) } // Process other maps via recursion if reflect.TypeOf(v).Kind() == reflect.Map { From 877f6a2c49c968e8b2f701752e50511541ad3cc8 Mon Sep 17 00:00:00 2001 From: "Stettler, Robert" Date: Fri, 24 Feb 2017 10:22:14 -0500 Subject: [PATCH 07/10] Added an additional test case to improve coverage. --- suite_test.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/suite_test.go b/suite_test.go index 0c2a9eb..8028fc4 100644 --- a/suite_test.go +++ b/suite_test.go @@ -67,6 +67,11 @@ func SuiteContext(s *Suite) { return nil }) + // duplicate step to 'a failing step' I added to help test cucumber.feature + // I needed to have an Scenario Outline where the status was passing or failing + // I needed the same step def language. + s.Step(`^failing step$`, c.aFailingStep) + // Introduced to test formatter/cucumber.feature s.Step(`^the rendered json will be as follows:$`, c.theRenderJSONWillBe) From d0e613d6c86d97daa5023faa5a0d1599a9174695 Mon Sep 17 00:00:00 2001 From: "Stettler, Robert" Date: Mon, 27 Feb 2017 07:24:54 -0500 Subject: [PATCH 08/10] Discovered defects resulting from not comparing the expected and actual structures. Times when actual contained more fields than expected. --- fmt_cucumber.go | 91 ++++++++++++++++++++++++++++--------------------- suite_test.go | 81 +++++++++++++++++++++++++++++++++++++++---- 2 files changed, 127 insertions(+), 45 deletions(-) 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 From 7a4d079c2aadd27d0ae107da01d45032ba949219 Mon Sep 17 00:00:00 2001 From: "Stettler, Robert" Date: Mon, 27 Feb 2017 08:15:51 -0500 Subject: [PATCH 09/10] Reformatting. --- fmt_cucumber.go | 5 ++--- suite_test.go | 28 ++++++++++++++-------------- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/fmt_cucumber.go b/fmt_cucumber.go index d98480c..fbf15c4 100644 --- a/fmt_cucumber.go +++ b/fmt_cucumber.go @@ -250,7 +250,6 @@ func (f *cukefmt) Summary() { func (f *cukefmt) step(res *stepResult) { - // determine if test case has finished switch t := f.owner.(type) { case *gherkin.TableRow: @@ -313,7 +312,7 @@ func (f *cukefmt) Undefined(step *gherkin.Step) { 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.Match.Location = fmt.Sprintf("%s:%d", f.path, step.Location.Line) f.curStep.Result.Duration = nil } @@ -329,6 +328,6 @@ func (f *cukefmt) Pending(step *gherkin.Step, match *StepDef) { 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.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 00f05ab..a52e457 100644 --- a/suite_test.go +++ b/suite_test.go @@ -366,7 +366,7 @@ func (s *suiteContext) thereWasEventTriggeredBeforeScenario(expected string) err return fmt.Errorf("before scenario event was never triggered or listened") } - return fmt.Errorf(`expected "%s" scenario, but got these fired %s`, expected, `"`+strings.Join(found, `", "`)+`"`) + return fmt.Errorf(`expected "%s" scenario, but got these fired %s`, expected, `"` + strings.Join(found, `", "`) + `"`) } func (s *suiteContext) theseEventsHadToBeFiredForNumberOfTimes(tbl *gherkin.DataTable) error { @@ -404,24 +404,24 @@ func (s *suiteContext) theRenderJSONWillBe(docstring *gherkin.DocString) error { // Created to use in error reporting. expectedCompact := &bytes.Buffer{} actualCompact := &bytes.Buffer{} - json.Compact(expectedCompact,[]byte(docstring.Content)) - json.Compact(actualCompact,s.out.Bytes()) + 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) + 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) + 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 fmt.Errorf("err:%v values don't match expected:%s actual:%s",err,expectedCompact, actualCompact) + return fmt.Errorf("err:%v values don't match expected:%s actual:%s", err, expectedCompact, actualCompact) } } return nil @@ -451,7 +451,7 @@ 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" { // location is tricky. the cucumber value is either a the step def location for passed,failed, and skipped. @@ -460,9 +460,9 @@ func (s *suiteContext) mapCompare(expected map[string]interface{}, actual map[st // the context i need contained within its value. // FEATURE_PATH myfile.feature:20 or // STEP_ID - t := strings.Split(v.(string)," ") + t := strings.Split(v.(string), " ") if t[0] == "FEATURE_PATH" { - if actual[k].(string) != t[1]{ + if actual[k].(string) != t[1] { return fmt.Errorf("location has unexpected value [%s] should be [%s]", actual[k], t[1]) } @@ -474,15 +474,15 @@ func (s *suiteContext) mapCompare(expected map[string]interface{}, actual map[st } } else { - return fmt.Errorf("Bad location value [%v]",v) + 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) { @@ -513,7 +513,7 @@ func (s *suiteContext) mapCompareStructure(expected map[string]interface{}, actu for k, v := range expected { if actual[k] == nil { - return fmt.Errorf("Structure Mismatch: no matching field:[%s] expected value:[%v]",k, v) + 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 { From 819ce03090008ab1dec7dcc3e0997b6dc4f0a304 Mon Sep 17 00:00:00 2001 From: "Stettler, Robert" Date: Thu, 2 Mar 2017 10:50:30 -0500 Subject: [PATCH 10/10] Reformatting. --- features/formatter/cucumber.feature | 551 ++++++++++++++++++++++++++++ fmt_cucumber.go | 80 ++-- suite_test.go | 5 +- 3 files changed, 594 insertions(+), 42 deletions(-) create mode 100644 features/formatter/cucumber.feature diff --git a/features/formatter/cucumber.feature b/features/formatter/cucumber.feature new file mode 100644 index 0000000..78a499e --- /dev/null +++ b/features/formatter/cucumber.feature @@ -0,0 +1,551 @@ +Feature: cucumber json formatter + In order to support tools that import cucumber json output + I need to be able to support cucumber json formatted output + + Scenario: Support of Feature Plus Scenario Node + Given a feature "features/simple.feature" file: + """ + Feature: simple feature + simple feature description + Scenario: simple scenario + simple scenario description + """ + When I run feature suite with formatter "cucumber" + Then the rendered json will be as follows: + """ application/json + [ + { + "uri": "features/simple.feature", + "id": "simple-feature", + "keyword": "Feature", + "name": "simple feature", + "description": " simple feature description", + "line": 1, + "elements": [ + { + "id": "simple-feature;simple-scenario", + "keyword": "Scenario", + "name": "simple scenario", + "description": " simple scenario description", + "line": 3, + "type": "scenario" + } + ] + } + ] + """ + + Scenario: Support of Feature Plus Scenario Node With Tags + Given a feature "features/simple.feature" file: + """ + @TAG1 + Feature: simple feature + simple feature description + @TAG2 @TAG3 + Scenario: simple scenario + simple scenario description + """ + When I run feature suite with formatter "cucumber" + Then the rendered json will be as follows: + """ application/json + [ + { + "uri": "features/simple.feature", + "id": "simple-feature", + "keyword": "Feature", + "name": "simple feature", + "description": " simple feature description", + "line": 2, + "tags": [ + { + "name": "@TAG1", + "line": 1 + } + ], + "elements": [ + { + "id": "simple-feature;simple-scenario", + "keyword": "Scenario", + "name": "simple scenario", + "description": " simple scenario description", + "line": 5, + "type": "scenario", + "tags": [ + { + "name": "@TAG1", + "line": 1 + }, + { + "name": "@TAG2", + "line": 4 + }, + { + "name": "@TAG3", + "line": 4 + } + ] + } + ] + } + ] + """ + Scenario: Support of Feature Plus Scenario Outline + Given a feature "features/simple.feature" file: + """ + Feature: simple feature + simple feature description + + Scenario Outline: simple scenario + simple scenario description + + Examples: simple examples + | status | + | pass | + | fail | + """ + When I run feature suite with formatter "cucumber" + Then the rendered json will be as follows: + """ + [ + { + "uri": "features/simple.feature", + "id": "simple-feature", + "keyword": "Feature", + "name": "simple feature", + "description": " simple feature description", + "line": 1, + "elements": [ + { + "id": "simple-feature;simple-scenario;simple-examples;2", + "keyword": "Scenario Outline", + "name": "simple scenario", + "description": " simple scenario description", + "line": 9, + "type": "scenario" + }, + { + "id": "simple-feature;simple-scenario;simple-examples;3", + "keyword": "Scenario Outline", + "name": "simple scenario", + "description": " simple scenario description", + "line": 10, + "type": "scenario" + } + ] + } + ] + """ + + Scenario: Support of Feature Plus Scenario Outline With Tags + Given a feature "features/simple.feature" file: + """ + @TAG1 + Feature: simple feature + simple feature description + + @TAG2 + Scenario Outline: simple scenario + simple scenario description + + @TAG3 + Examples: simple examples + | status | + | pass | + | fail | + """ + When I run feature suite with formatter "cucumber" + Then the rendered json will be as follows: + """ + [ + { + "uri": "features/simple.feature", + "id": "simple-feature", + "keyword": "Feature", + "name": "simple feature", + "description": " simple feature description", + "line": 2, + "tags": [ + { + "name": "@TAG1", + "line": 1 + } + ], + "elements": [ + { + "id": "simple-feature;simple-scenario;simple-examples;2", + "keyword": "Scenario Outline", + "name": "simple scenario", + "description": " simple scenario description", + "line": 12, + "type": "scenario", + "tags": [ + { + "name": "@TAG1", + "line": 1 + }, + { + "name": "@TAG2", + "line": 5 + }, + { + "name": "@TAG3", + "line": 9 + } + ] + }, + { + "id": "simple-feature;simple-scenario;simple-examples;3", + "keyword": "Scenario Outline", + "name": "simple scenario", + "description": " simple scenario description", + "line": 13, + "type": "scenario", + "tags": [ + { + "name": "@TAG1", + "line": 1 + }, + { + "name": "@TAG2", + "line": 5 + }, + { + "name": "@TAG3", + "line": 9 + } + ] + } + ] + } + ] + """ + Scenario: Support of Feature Plus Scenario With Steps + Given a feature "features/simple.feature" file: + """ + Feature: simple feature + simple feature description + + Scenario: simple scenario + simple scenario description + + Given passing step + Then a failing step + + """ + When I run feature suite with formatter "cucumber" + Then the rendered json will be as follows: + """ + [ + { + "uri": "features/simple.feature", + "id": "simple-feature", + "keyword": "Feature", + "name": "simple feature", + "description": " simple feature description", + "line": 1, + "elements": [ + { + "id": "simple-feature;simple-scenario", + "keyword": "Scenario", + "name": "simple scenario", + "description": " simple scenario description", + "line": 4, + "type": "scenario", + "steps": [ + { + "keyword": "Given ", + "name": "passing step", + "line": 7, + "match": { + "location": "STEP_ID" + }, + "result": { + "status": "passed", + "duration": -1 + } + }, + { + "keyword": "Then ", + "name": "a failing step", + "line": 8, + "match": { + "location": "STEP_ID" + }, + "result": { + "status": "failed", + "error_message": "intentional failure", + "duration": -1 + } + } + ] + } + ] + } + ] + """ + Scenario: Support of Feature Plus Scenario Outline With Steps + Given a feature "features/simple.feature" file: + """ + Feature: simple feature + simple feature description + + Scenario Outline: simple scenario + simple scenario description + + Given step + + Examples: simple examples + | status | + | passing | + | failing | + + """ + When I run feature suite with formatter "cucumber" + Then the rendered json will be as follows: + """ + [ + { + "uri": "features/simple.feature", + "id": "simple-feature", + "keyword": "Feature", + "name": "simple feature", + "description": " simple feature description", + "line": 1, + "elements": [ + { + "id": "simple-feature;simple-scenario;simple-examples;2", + "keyword": "Scenario Outline", + "name": "simple scenario", + "description": " simple scenario description", + "line": 11, + "type": "scenario", + "steps": [ + { + "keyword": "Given ", + "name": "passing step", + "line": 11, + "match": { + "location": "STEP_ID" + }, + "result": { + "status": "passed", + "duration": -1 + } + } + ] + }, + { + "id": "simple-feature;simple-scenario;simple-examples;3", + "keyword": "Scenario Outline", + "name": "simple scenario", + "description": " simple scenario description", + "line": 12, + "type": "scenario", + "steps": [ + { + "keyword": "Given ", + "name": "failing step", + "line": 12, + "match": { + "location": "STEP_ID" + }, + "result": { + "status": "failed", + "error_message": "intentional failure", + "duration": -1 + } + } + ] + } + ] + } + ] + """ + + # Currently godog only supports comments on Feature and not + # scenario and steps. + Scenario: Support of Comments + Given a feature "features/simple.feature" file: + """ + #Feature comment + Feature: simple feature + simple description + + Scenario: simple scenario + simple feature description + """ + When I run feature suite with formatter "cucumber" + Then the rendered json will be as follows: + """ + [ + { + "uri": "features/simple.feature", + "id": "simple-feature", + "keyword": "Feature", + "name": "simple feature", + "description": " simple description", + "line": 2, + "comments": [ + { + "value": "#Feature comment", + "line": 1 + } + ], + "elements": [ + { + "id": "simple-feature;simple-scenario", + "keyword": "Scenario", + "name": "simple scenario", + "description": " simple feature description", + "line": 5, + "type": "scenario" + } + ] + } + ] + """ + Scenario: Support of Docstrings + Given a feature "features/simple.feature" file: + """ + Feature: simple feature + simple description + + Scenario: simple scenario + simple feature description + + Given passing step + \"\"\" content type + step doc string + \"\"\" + """ + When I run feature suite with formatter "cucumber" + Then the rendered json will be as follows: + """ + [ + { + "uri": "features/simple.feature", + "id": "simple-feature", + "keyword": "Feature", + "name": "simple feature", + "description": " simple description", + "line": 1, + "elements": [ + { + "id": "simple-feature;simple-scenario", + "keyword": "Scenario", + "name": "simple scenario", + "description": " simple feature description", + "line": 4, + "type": "scenario", + "steps": [ + { + "keyword": "Given ", + "name": "passing step", + "line": 7, + "doc_string": { + "value": "step doc string", + "content_type": "content type", + "line": 8 + }, + "match": { + "location": "STEP_ID" + }, + "result": { + "status": "passed", + "duration": -1 + } + } + ] + } + ] + } + ] + """ + Scenario: Support of Undefined, Pending and Skipped status + Given a feature "features/simple.feature" file: + """ + Feature: simple feature + simple feature description + + Scenario: simple scenario + simple scenario description + + Given passing step + And pending step + And undefined + And passing step + + """ + When I run feature suite with formatter "cucumber" + Then the rendered json will be as follows: + """ + [ + { + "uri": "features/simple.feature", + "id": "simple-feature", + "keyword": "Feature", + "name": "simple feature", + "description": " simple feature description", + "line": 1, + "elements": [ + { + "id": "simple-feature;simple-scenario", + "keyword": "Scenario", + "name": "simple scenario", + "description": " simple scenario description", + "line": 4, + "type": "scenario", + "steps": [ + { + "keyword": "Given ", + "name": "passing step", + "line": 7, + "match": { + "location": "STEP_ID" + }, + "result": { + "status": "passed", + "duration": 397087 + } + }, + { + "keyword": "And ", + "name": "pending step", + "line": 8, + "match": { + "location": "FEATURE_PATH features/simple.feature:8" + }, + "result": { + "status": "pending" + } + }, + { + "keyword": "And ", + "name": "undefined", + "line": 9, + "match": { + "location": "FEATURE_PATH features/simple.feature:9" + }, + "result": { + "status": "undefined" + } + }, + { + "keyword": "And ", + "name": "passing step", + "line": 10, + "match": { + "location": "STEP_ID" + }, + "result": { + "status": "skipped" + } + } + ] + } + ] + } + ] + """ + + diff --git a/fmt_cucumber.go b/fmt_cucumber.go index fbf15c4..52a00cc 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,omitempty"` + Duration *int `json:"duration,omitempty"` } type cukeMatch struct { @@ -72,12 +72,12 @@ type cukeMatch struct { } type cukeStep struct { - Keyword string `json:"keyword"` - Name string `json:"name"` - Line int `json:"line"` + Keyword string `json:"keyword"` + Name string `json:"name"` + Line int `json:"line"` Docstring *cukeDocstring `json:"doc_string,omitempty"` - Match cukeMatch `json:"match"` - Result cukeResult `json:"result"` + Match cukeMatch `json:"match"` + Result cukeResult `json:"result"` } type cukeElement struct { @@ -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 @@ -274,7 +274,7 @@ 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 @@ -295,12 +295,12 @@ 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 @@ -309,7 +309,7 @@ func (f *cukefmt) Skipped(step *gherkin.Step) { 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) @@ -319,13 +319,13 @@ func (f *cukefmt) Undefined(step *gherkin.Step) { 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) diff --git a/suite_test.go b/suite_test.go index a52e457..e8c3f9e 100644 --- a/suite_test.go +++ b/suite_test.go @@ -366,7 +366,7 @@ func (s *suiteContext) thereWasEventTriggeredBeforeScenario(expected string) err return fmt.Errorf("before scenario event was never triggered or listened") } - return fmt.Errorf(`expected "%s" scenario, but got these fired %s`, expected, `"` + strings.Join(found, `", "`) + `"`) + return fmt.Errorf(`expected "%s" scenario, but got these fired %s`, expected, `"`+strings.Join(found, `", "`)+`"`) } func (s *suiteContext) theseEventsHadToBeFiredForNumberOfTimes(tbl *gherkin.DataTable) error { @@ -503,6 +503,7 @@ 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. @@ -531,4 +532,4 @@ func (s *suiteContext) mapCompareStructure(expected map[string]interface{}, actu } return nil -} \ No newline at end of file +}