From 8a07e4cae3874951e3d33843a05af580bfb911bc Mon Sep 17 00:00:00 2001 From: gedi Date: Fri, 4 Mar 2016 10:22:13 +0200 Subject: [PATCH] junit formatter implementation, adapt pretty formatter closes #14 background was not printed with outline scenarios in feature --- LICENSE | 2 +- README.md | 6 ++ fmt_junit.go | 230 +++++++++++++++++++++------------------------- fmt_pretty.go | 130 +++++++++++++++++--------- junit/juint-4.xsd | 91 ------------------ junit/structs.go | 1 - suite.go | 7 +- 7 files changed, 203 insertions(+), 264 deletions(-) delete mode 100644 junit/juint-4.xsd delete mode 100644 junit/structs.go diff --git a/LICENSE b/LICENSE index f767265..3da1447 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The three clause BSD license (http://en.wikipedia.org/wiki/BSD_licenses) -Copyright (c) 2015, DataDog.lt team +Copyright (c) 2015-2016, DataDog.lt team All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/README.md b/README.md index ef302ad..37342f3 100644 --- a/README.md +++ b/README.md @@ -144,6 +144,12 @@ See implementation examples: ### Changes +**2016-03-04** +- added **junit** compatible output formatter, which prints **xml** + results to **os.Stdout** +- fixed #14 which skipped printing background steps when there was + scenario outline in feature. + **2015-07-03** - changed **godog.Suite** from interface to struct. Context registration should be updated accordingly. The reason for change: since it exports the same methods and there is no need to mock a function in tests, there is no diff --git a/fmt_junit.go b/fmt_junit.go index e20b9cd..3e76b21 100644 --- a/fmt_junit.go +++ b/fmt_junit.go @@ -3,6 +3,7 @@ package godog import ( "encoding/xml" "fmt" + "io" "os" "time" @@ -10,198 +11,175 @@ import ( ) func init() { - Format("junit", "Prints out in junit compatible xml format", &junitFormatter{ - suites: make([]*junitTestSuites, 0), + Format("junit", "Prints junit compatible xml to stdout", &junitFormatter{ + suite: &junitPackageSuite{ + Name: "main", // @TODO: it should extract package name + TestSuites: make([]*junitTestSuite, 0), + }, + started: time.Now(), }) } type junitFormatter struct { - suites []*junitTestSuites + suite *junitPackageSuite + + // timing + started time.Time + caseStarted time.Time + featStarted time.Time + + outline *gherkin.ScenarioOutline + outlineExample int } func (j *junitFormatter) Feature(feature *gherkin.Feature, path string) { - testSuites := &junitTestSuites{ - Name: feature.Name, - TestSuites: make([]*junitTestSuite, 0), + testSuite := &junitTestSuite{ + TestCases: make([]*TestCase, 0), + Name: feature.Name, } - j.suites = append(j.suites, testSuites) + if len(j.suite.TestSuites) > 0 { + j.current().Time = time.Since(j.featStarted).String() + } + j.featStarted = time.Now() + j.suite.TestSuites = append(j.suite.TestSuites, testSuite) } func (j *junitFormatter) Node(node interface{}) { - testSuite := &junitTestSuite{ - TestCases: make([]*TestCase, 0), - Timestamp: time.Now(), - } + suite := j.current() + tcase := &TestCase{} switch t := node.(type) { case *gherkin.ScenarioOutline: - testSuite.Name = t.Name + j.outline = t + return case *gherkin.Scenario: - testSuite.Name = t.Name - case *gherkin.Background: - testSuite.Name = "Background" + tcase.Name = t.Name + suite.Tests++ + j.suite.Tests++ + case *gherkin.Examples: + j.outlineExample = 0 + return + case *gherkin.TableRow: + j.outlineExample++ + tcase.Name = fmt.Sprintf("%s #%d", j.outline.Name, j.outlineExample) + suite.Tests++ + j.suite.Tests++ + default: + return } - - currentSuites := j.currentSuites() - currentSuites.TestSuites = append(currentSuites.TestSuites, testSuite) + if len(suite.TestCases) > 0 { + suite.current().Time = time.Since(j.caseStarted).String() + } + j.caseStarted = time.Now() + suite.TestCases = append(suite.TestCases, tcase) } func (j *junitFormatter) Failed(step *gherkin.Step, match *StepDef, err error) { - testCase := &TestCase{ - Name: step.Text, - } + suite := j.current() + suite.Failures++ + j.suite.Failures++ - testCase.Failure = &junitFailure{ - Contents: err.Error(), + tcase := suite.current() + tcase.Status = "failed" + tcase.Failure = &junitFailure{ + Message: fmt.Sprintf("%s %s: %s", step.Type, step.Text, err.Error()), } - - currentSuites := j.currentSuites() - currentSuites.Failures++ - currentSuite := currentSuites.currentSuite() - currentSuite.Failures++ - currentSuite.TestCases = append(currentSuite.TestCases, testCase) } func (j *junitFormatter) Passed(step *gherkin.Step, match *StepDef) { - testCase := &TestCase{ - Name: step.Text, - } + suite := j.current() - currentSuites := j.currentSuites() - currentSuites.Tests++ - currentSuite := currentSuites.currentSuite() - currentSuite.Tests++ - currentSuite.TestCases = append(currentSuite.TestCases, testCase) + tcase := suite.current() + tcase.Status = "passed" } func (j *junitFormatter) Skipped(step *gherkin.Step) { - testCase := &TestCase{ - Name: step.Text, - } - - currentSuites := j.currentSuites() - currentSuite := currentSuites.currentSuite() - currentSuite.Skipped++ - currentSuite.TestCases = append(currentSuite.TestCases, testCase) } func (j *junitFormatter) Undefined(step *gherkin.Step) { - testCase := &TestCase{ - Name: step.Text, - } + suite := j.current() + suite.Errors++ + j.suite.Errors++ - currentSuites := j.currentSuites() - currentSuites.Disabled++ - currentSuite := currentSuites.currentSuite() - currentSuite.Disabled++ - currentSuite.TestCases = append(currentSuite.TestCases, testCase) + tcase := suite.current() + tcase.Status = "undefined" + tcase.Error = append(tcase.Error, &junitError{ + Type: "undefined", + Message: fmt.Sprintf("%s %s", step.Type, step.Text), + }) } func (j *junitFormatter) Pending(step *gherkin.Step, match *StepDef) { - testCase := &TestCase{ - Name: step.Text, - } + suite := j.current() + suite.Errors++ + j.suite.Errors++ - testCase.Skipped = &junitSkipped{ - Contents: step.Text, - } - - currentSuites := j.currentSuites() - currentSuite := currentSuites.currentSuite() - currentSuite.Skipped++ - currentSuite.TestCases = append(currentSuite.TestCases, testCase) + tcase := suite.current() + tcase.Status = "pending" + tcase.Error = append(tcase.Error, &junitError{ + Type: "pending", + Message: fmt.Sprintf("%s %s: TODO: write pending definition", step.Type, step.Text), + }) } func (j *junitFormatter) Summary() { + j.suite.Time = time.Since(j.started).String() + io.WriteString(os.Stdout, xml.Header) + enc := xml.NewEncoder(os.Stdout) - enc.Indent(" ", " ") - if err := enc.Encode(j.suites); err != nil { - fmt.Printf("error: %v\n", err) + enc.Indent("", s(2)) + if err := enc.Encode(j.suite); err != nil { + fmt.Println("failed to write junit xml:", err) } } type junitFailure struct { - Message string `xml:"message,attr"` - Type string `xml:"type,attr"` - Contents string `xml:",chardata"` + Message string `xml:"message,attr"` + Type string `xml:"type,attr,omitempty"` } type junitError struct { - Message string `xml:"message,attr"` - Type string `xml:"type,attr"` - Contents string `xml:",chardata"` -} - -type junitProperty struct { - Name string `xml:"name,attr"` - Value string `xml:"value,attr"` -} - -type junitSkipped struct { - Contents string `xml:",chardata"` -} - -type SystemErr struct { - Contents string `xml:",chardata"` -} - -type SystemOut struct { - Contents string `xml:",chardata"` + XMLName xml.Name `xml:"error,omitempty"` + Message string `xml:"message,attr"` + Type string `xml:"type,attr"` } type TestCase struct { - XMLName xml.Name `xml:"testcase"` - Name string `xml:"name,attr"` - Classname string `xml:"classname,attr"` - Assertions string `xml:"assertions,attr"` - Status string `xml:"status,attr"` - Time string `xml:"time,attr"` - Skipped *junitSkipped `xml:"skipped,omitempty"` - Failure *junitFailure `xml:"failure,omitempty"` - Error *junitError `xml:"error,omitempty"` - SystemOut *SystemOut `xml:"system-out,omitempty"` - SystemErr *SystemErr `xml:"system-err,omitempty"` + XMLName xml.Name `xml:"testcase"` + Name string `xml:"name,attr"` + Status string `xml:"status,attr"` + Time string `xml:"time,attr"` + Failure *junitFailure `xml:"failure,omitempty"` + Error []*junitError } type junitTestSuite struct { - XMLName xml.Name `xml:"testsuite"` - Name string `xml:"name,attr"` - Tests int `xml:"tests,attr"` - Failures int `xml:"failures,attr"` - Errors int `xml:"errors,attr"` - Disabled int `xml:"disabled,attr"` - Skipped int `xml:"skipped,attr"` - Time string `xml:"time,attr"` - Hostname string `xml:"hostname,attr"` - ID string `xml:"id,attr"` - Package string `xml:"package,attr"` - Timestamp time.Time `xml:"timestamp,attr"` - SystemOut *SystemOut `xml:"system-out,omitempty"` - SystemErr *SystemErr `xml:"system-err,omitempty"` - Properties []*junitProperty `xml:"properties>property,omitempty"` - TestCases []*TestCase + XMLName xml.Name `xml:"testsuite"` + Name string `xml:"name,attr"` + Tests int `xml:"tests,attr"` + Skipped int `xml:"skipped,attr"` + Failures int `xml:"failures,attr"` + Errors int `xml:"errors,attr"` + Time string `xml:"time,attr"` + TestCases []*TestCase } -func (ts *junitTestSuite) currentCase() *TestCase { +func (ts *junitTestSuite) current() *TestCase { return ts.TestCases[len(ts.TestCases)-1] } -type junitTestSuites struct { +type junitPackageSuite struct { XMLName xml.Name `xml:"testsuites"` Name string `xml:"name,attr"` Tests int `xml:"tests,attr"` + Skipped int `xml:"skipped,attr"` Failures int `xml:"failures,attr"` Errors int `xml:"errors,attr"` - Disabled int `xml:"disabled,attr"` Time string `xml:"time,attr"` TestSuites []*junitTestSuite } -func (ts *junitTestSuites) currentSuite() *junitTestSuite { - return ts.TestSuites[len(ts.TestSuites)-1] -} - -func (j *junitFormatter) currentSuites() *junitTestSuites { - return j.suites[len(j.suites)-1] +func (j *junitFormatter) current() *junitTestSuite { + return j.suite.TestSuites[len(j.suite.TestSuites)-1] } diff --git a/fmt_pretty.go b/fmt_pretty.go index 840dc24..9354c3a 100644 --- a/fmt_pretty.go +++ b/fmt_pretty.go @@ -24,8 +24,16 @@ var outlinePlaceholderRegexp = regexp.MustCompile("<[^>]+>") // a built in default pretty formatter type pretty struct { basefmt - commentPos int - backgroundSteps int + + // currently processed + feature *gherkin.Feature + scenario *gherkin.Scenario + outline *gherkin.ScenarioOutline + + // state + bgSteps int + steps int + commentPos int // outline outlineSteps []*stepResult @@ -33,25 +41,6 @@ type pretty struct { outlineNumExamples int } -// a line number representation in feature file -func (f *pretty) line(loc *gherkin.Location) string { - return cl(fmt.Sprintf("# %s:%d", f.features[len(f.features)-1].Path, loc.Line), black) -} - -func (f *pretty) length(node interface{}) int { - switch t := node.(type) { - case *gherkin.Background: - return f.indent + len(strings.TrimSpace(t.Keyword)+": "+t.Name) - case *gherkin.Step: - return f.indent*2 + len(strings.TrimSpace(t.Keyword)+" "+t.Text) - case *gherkin.Scenario: - return f.indent + len(strings.TrimSpace(t.Keyword)+": "+t.Name) - case *gherkin.ScenarioOutline: - return f.indent + len(strings.TrimSpace(t.Keyword)+": "+t.Name) - } - panic(fmt.Sprintf("unexpected node %T to determine length", node)) -} - func (f *pretty) Feature(ft *gherkin.Feature, p string) { if len(f.features) != 0 { // not a first feature, add a newline @@ -64,10 +53,13 @@ func (f *pretty) Feature(ft *gherkin.Feature, p string) { fmt.Println(s(f.indent) + strings.TrimSpace(line)) } } + + f.feature = ft + f.scenario = nil + f.outline = nil + f.bgSteps = 0 if ft.Background != nil { - f.commentPos = f.longestStep(ft.Background.Steps, f.length(ft.Background)) - f.backgroundSteps = len(ft.Background.Steps) - fmt.Println("\n" + s(f.indent) + bcl(ft.Background.Keyword+": "+ft.Background.Name, white)) + f.bgSteps = len(ft.Background.Steps) } } @@ -80,15 +72,13 @@ func (f *pretty) Node(node interface{}) { f.outlineNumExamples = len(t.TableBody) f.outlineNumExample++ case *gherkin.Scenario: - f.commentPos = f.longestStep(t.Steps, f.length(t)) - text := s(f.indent) + bcl(t.Keyword+": ", white) + t.Name - text += s(f.commentPos-f.length(t)+1) + f.line(t.Location) - fmt.Println("\n" + text) + f.scenario = t + f.outline = nil + f.steps = len(t.Steps) case *gherkin.ScenarioOutline: - f.commentPos = f.longestStep(t.Steps, f.length(t)) - text := s(f.indent) + bcl(t.Keyword+": ", white) + t.Name - text += s(f.commentPos-f.length(t)+1) + f.line(t.Location) - fmt.Println("\n" + text) + f.outline = t + f.scenario = nil + f.steps = len(t.Steps) f.outlineNumExample = -1 } } @@ -240,21 +230,54 @@ func (f *pretty) printStep(step *gherkin.Step, def *StepDef, c color) { } func (f *pretty) printStepKind(res *stepResult) { - // do not print background more than once - if _, ok := res.owner.(*gherkin.Background); ok { - switch { - case f.backgroundSteps == 0: - return - case f.backgroundSteps > 0: - f.backgroundSteps-- - } - } + _, isBgStep := res.owner.(*gherkin.Background) - if outline, ok := res.owner.(*gherkin.ScenarioOutline); ok { + // if has not printed background yet + switch { + // first background step + case f.bgSteps > 0 && f.bgSteps == len(f.feature.Background.Steps): + f.commentPos = f.longestStep(f.feature.Background.Steps, f.length(f.feature.Background)) + fmt.Println("\n" + s(f.indent) + bcl(f.feature.Background.Keyword+": "+f.feature.Background.Name, white)) + f.bgSteps-- + // subsequent background steps + case f.bgSteps > 0: + f.bgSteps-- + // a background step for another scenario, but all bg steps are + // already printed. so just skip it + case isBgStep: + return + // first step of scenario, print header and calculate comment position + case f.scenario != nil && f.steps == len(f.scenario.Steps): + f.commentPos = f.longestStep(f.scenario.Steps, f.length(f.scenario)) + text := s(f.indent) + bcl(f.scenario.Keyword+": ", white) + f.scenario.Name + text += s(f.commentPos-f.length(f.scenario)+1) + f.line(f.scenario.Location) + fmt.Println("\n" + text) + f.steps-- + // all subsequent scenario steps + case f.scenario != nil: + f.steps-- + // first step of outline scenario, print header and calculate comment position + case f.outline != nil && f.steps == len(f.outline.Steps): + f.commentPos = f.longestStep(f.outline.Steps, f.length(f.outline)) + text := s(f.indent) + bcl(f.outline.Keyword+": ", white) + f.outline.Name + text += s(f.commentPos-f.length(f.outline)+1) + f.line(f.outline.Location) + fmt.Println("\n" + text) f.outlineSteps = append(f.outlineSteps, res) - if len(f.outlineSteps) == len(outline.Steps) { + f.steps-- + if len(f.outlineSteps) == len(f.outline.Steps) { // an outline example steps has went through - f.printOutlineExample(outline) + f.printOutlineExample(f.outline) + f.outlineSteps = []*stepResult{} + f.outlineNumExamples-- + } + return + // all subsequent outline steps + case f.outline != nil: + f.outlineSteps = append(f.outlineSteps, res) + f.steps-- + if len(f.outlineSteps) == len(f.outline.Steps) { + // an outline example steps has went through + f.printOutlineExample(f.outline) f.outlineSteps = []*stepResult{} f.outlineNumExamples-- } @@ -339,3 +362,22 @@ func (f *pretty) longestStep(steps []*gherkin.Step, base int) int { } return ret } + +// a line number representation in feature file +func (f *pretty) line(loc *gherkin.Location) string { + return cl(fmt.Sprintf("# %s:%d", f.features[len(f.features)-1].Path, loc.Line), black) +} + +func (f *pretty) length(node interface{}) int { + switch t := node.(type) { + case *gherkin.Background: + return f.indent + len(strings.TrimSpace(t.Keyword)+": "+t.Name) + case *gherkin.Step: + return f.indent*2 + len(strings.TrimSpace(t.Keyword)+" "+t.Text) + case *gherkin.Scenario: + return f.indent + len(strings.TrimSpace(t.Keyword)+": "+t.Name) + case *gherkin.ScenarioOutline: + return f.indent + len(strings.TrimSpace(t.Keyword)+": "+t.Name) + } + panic(fmt.Sprintf("unexpected node %T to determine length", node)) +} diff --git a/junit/juint-4.xsd b/junit/juint-4.xsd deleted file mode 100644 index 242d5b2..0000000 --- a/junit/juint-4.xsd +++ /dev/null @@ -1,91 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/junit/structs.go b/junit/structs.go deleted file mode 100644 index 770abab..0000000 --- a/junit/structs.go +++ /dev/null @@ -1 +0,0 @@ -package junit diff --git a/suite.go b/suite.go index 8fc8e6b..d2df014 100644 --- a/suite.go +++ b/suite.go @@ -290,6 +290,10 @@ func (s *Suite) runOutline(outline *gherkin.ScenarioOutline, b *gherkin.Backgrou } steps = append(steps, step) } + + // run example table row + s.fmt.Node(group) + // run background var err error if b != nil { @@ -340,13 +344,14 @@ func (s *Suite) runScenario(scenario *gherkin.Scenario, b *gherkin.Background) ( f(scenario) } + s.fmt.Node(scenario) + // background if b != nil { err = s.runSteps(b.Steps, err) } // scenario - s.fmt.Node(scenario) err = s.runSteps(scenario.Steps, err) // run after scenario handlers