junit formatter implementation, adapt pretty formatter

closes #14 background was not printed with outline scenarios in feature
Этот коммит содержится в:
gedi 2016-03-04 10:22:13 +02:00
родитель b192f0bd23
коммит 8a07e4cae3
7 изменённых файлов: 203 добавлений и 264 удалений

Просмотреть файл

@ -1,6 +1,6 @@
The three clause BSD license (http://en.wikipedia.org/wiki/BSD_licenses) 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. All rights reserved.
Redistribution and use in source and binary forms, with or without Redistribution and use in source and binary forms, with or without

Просмотреть файл

@ -144,6 +144,12 @@ See implementation examples:
### Changes ### 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** **2015-07-03**
- changed **godog.Suite** from interface to struct. Context registration should be updated accordingly. The reason - 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 for change: since it exports the same methods and there is no need to mock a function in tests, there is no

Просмотреть файл

@ -3,6 +3,7 @@ package godog
import ( import (
"encoding/xml" "encoding/xml"
"fmt" "fmt"
"io"
"os" "os"
"time" "time"
@ -10,198 +11,175 @@ import (
) )
func init() { func init() {
Format("junit", "Prints out in junit compatible xml format", &junitFormatter{ Format("junit", "Prints junit compatible xml to stdout", &junitFormatter{
suites: make([]*junitTestSuites, 0), suite: &junitPackageSuite{
Name: "main", // @TODO: it should extract package name
TestSuites: make([]*junitTestSuite, 0),
},
started: time.Now(),
}) })
} }
type junitFormatter struct { 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) { func (j *junitFormatter) Feature(feature *gherkin.Feature, path string) {
testSuites := &junitTestSuites{ testSuite := &junitTestSuite{
Name: feature.Name, TestCases: make([]*TestCase, 0),
TestSuites: make([]*junitTestSuite, 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{}) { func (j *junitFormatter) Node(node interface{}) {
testSuite := &junitTestSuite{ suite := j.current()
TestCases: make([]*TestCase, 0), tcase := &TestCase{}
Timestamp: time.Now(),
}
switch t := node.(type) { switch t := node.(type) {
case *gherkin.ScenarioOutline: case *gherkin.ScenarioOutline:
testSuite.Name = t.Name j.outline = t
return
case *gherkin.Scenario: case *gherkin.Scenario:
testSuite.Name = t.Name tcase.Name = t.Name
case *gherkin.Background: suite.Tests++
testSuite.Name = "Background" 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
} }
if len(suite.TestCases) > 0 {
currentSuites := j.currentSuites() suite.current().Time = time.Since(j.caseStarted).String()
currentSuites.TestSuites = append(currentSuites.TestSuites, testSuite) }
j.caseStarted = time.Now()
suite.TestCases = append(suite.TestCases, tcase)
} }
func (j *junitFormatter) Failed(step *gherkin.Step, match *StepDef, err error) { func (j *junitFormatter) Failed(step *gherkin.Step, match *StepDef, err error) {
testCase := &TestCase{ suite := j.current()
Name: step.Text, suite.Failures++
} j.suite.Failures++
testCase.Failure = &junitFailure{ tcase := suite.current()
Contents: err.Error(), 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) { func (j *junitFormatter) Passed(step *gherkin.Step, match *StepDef) {
testCase := &TestCase{ suite := j.current()
Name: step.Text,
}
currentSuites := j.currentSuites() tcase := suite.current()
currentSuites.Tests++ tcase.Status = "passed"
currentSuite := currentSuites.currentSuite()
currentSuite.Tests++
currentSuite.TestCases = append(currentSuite.TestCases, testCase)
} }
func (j *junitFormatter) Skipped(step *gherkin.Step) { 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) { func (j *junitFormatter) Undefined(step *gherkin.Step) {
testCase := &TestCase{ suite := j.current()
Name: step.Text, suite.Errors++
} j.suite.Errors++
currentSuites := j.currentSuites() tcase := suite.current()
currentSuites.Disabled++ tcase.Status = "undefined"
currentSuite := currentSuites.currentSuite() tcase.Error = append(tcase.Error, &junitError{
currentSuite.Disabled++ Type: "undefined",
currentSuite.TestCases = append(currentSuite.TestCases, testCase) Message: fmt.Sprintf("%s %s", step.Type, step.Text),
})
} }
func (j *junitFormatter) Pending(step *gherkin.Step, match *StepDef) { func (j *junitFormatter) Pending(step *gherkin.Step, match *StepDef) {
testCase := &TestCase{ suite := j.current()
Name: step.Text, suite.Errors++
} j.suite.Errors++
testCase.Skipped = &junitSkipped{ tcase := suite.current()
Contents: step.Text, tcase.Status = "pending"
} tcase.Error = append(tcase.Error, &junitError{
Type: "pending",
currentSuites := j.currentSuites() Message: fmt.Sprintf("%s %s: TODO: write pending definition", step.Type, step.Text),
currentSuite := currentSuites.currentSuite() })
currentSuite.Skipped++
currentSuite.TestCases = append(currentSuite.TestCases, testCase)
} }
func (j *junitFormatter) Summary() { func (j *junitFormatter) Summary() {
j.suite.Time = time.Since(j.started).String()
io.WriteString(os.Stdout, xml.Header)
enc := xml.NewEncoder(os.Stdout) enc := xml.NewEncoder(os.Stdout)
enc.Indent(" ", " ") enc.Indent("", s(2))
if err := enc.Encode(j.suites); err != nil { if err := enc.Encode(j.suite); err != nil {
fmt.Printf("error: %v\n", err) fmt.Println("failed to write junit xml:", err)
} }
} }
type junitFailure struct { type junitFailure struct {
Message string `xml:"message,attr"` Message string `xml:"message,attr"`
Type string `xml:"type,attr"` Type string `xml:"type,attr,omitempty"`
Contents string `xml:",chardata"`
} }
type junitError struct { type junitError struct {
Message string `xml:"message,attr"` XMLName xml.Name `xml:"error,omitempty"`
Type string `xml:"type,attr"` Message string `xml:"message,attr"`
Contents string `xml:",chardata"` Type string `xml:"type,attr"`
}
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"`
} }
type TestCase struct { type TestCase struct {
XMLName xml.Name `xml:"testcase"` XMLName xml.Name `xml:"testcase"`
Name string `xml:"name,attr"` Name string `xml:"name,attr"`
Classname string `xml:"classname,attr"` Status string `xml:"status,attr"`
Assertions string `xml:"assertions,attr"` Time string `xml:"time,attr"`
Status string `xml:"status,attr"` Failure *junitFailure `xml:"failure,omitempty"`
Time string `xml:"time,attr"` Error []*junitError
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"`
} }
type junitTestSuite struct { type junitTestSuite struct {
XMLName xml.Name `xml:"testsuite"` XMLName xml.Name `xml:"testsuite"`
Name string `xml:"name,attr"` Name string `xml:"name,attr"`
Tests int `xml:"tests,attr"` Tests int `xml:"tests,attr"`
Failures int `xml:"failures,attr"` Skipped int `xml:"skipped,attr"`
Errors int `xml:"errors,attr"` Failures int `xml:"failures,attr"`
Disabled int `xml:"disabled,attr"` Errors int `xml:"errors,attr"`
Skipped int `xml:"skipped,attr"` Time string `xml:"time,attr"`
Time string `xml:"time,attr"` TestCases []*TestCase
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
} }
func (ts *junitTestSuite) currentCase() *TestCase { func (ts *junitTestSuite) current() *TestCase {
return ts.TestCases[len(ts.TestCases)-1] return ts.TestCases[len(ts.TestCases)-1]
} }
type junitTestSuites struct { type junitPackageSuite struct {
XMLName xml.Name `xml:"testsuites"` XMLName xml.Name `xml:"testsuites"`
Name string `xml:"name,attr"` Name string `xml:"name,attr"`
Tests int `xml:"tests,attr"` Tests int `xml:"tests,attr"`
Skipped int `xml:"skipped,attr"`
Failures int `xml:"failures,attr"` Failures int `xml:"failures,attr"`
Errors int `xml:"errors,attr"` Errors int `xml:"errors,attr"`
Disabled int `xml:"disabled,attr"`
Time string `xml:"time,attr"` Time string `xml:"time,attr"`
TestSuites []*junitTestSuite TestSuites []*junitTestSuite
} }
func (ts *junitTestSuites) currentSuite() *junitTestSuite { func (j *junitFormatter) current() *junitTestSuite {
return ts.TestSuites[len(ts.TestSuites)-1] return j.suite.TestSuites[len(j.suite.TestSuites)-1]
}
func (j *junitFormatter) currentSuites() *junitTestSuites {
return j.suites[len(j.suites)-1]
} }

Просмотреть файл

@ -24,8 +24,16 @@ var outlinePlaceholderRegexp = regexp.MustCompile("<[^>]+>")
// a built in default pretty formatter // a built in default pretty formatter
type pretty struct { type pretty struct {
basefmt basefmt
commentPos int
backgroundSteps int // currently processed
feature *gherkin.Feature
scenario *gherkin.Scenario
outline *gherkin.ScenarioOutline
// state
bgSteps int
steps int
commentPos int
// outline // outline
outlineSteps []*stepResult outlineSteps []*stepResult
@ -33,25 +41,6 @@ type pretty struct {
outlineNumExamples int 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) { func (f *pretty) Feature(ft *gherkin.Feature, p string) {
if len(f.features) != 0 { if len(f.features) != 0 {
// not a first feature, add a newline // 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)) fmt.Println(s(f.indent) + strings.TrimSpace(line))
} }
} }
f.feature = ft
f.scenario = nil
f.outline = nil
f.bgSteps = 0
if ft.Background != nil { if ft.Background != nil {
f.commentPos = f.longestStep(ft.Background.Steps, f.length(ft.Background)) f.bgSteps = len(ft.Background.Steps)
f.backgroundSteps = len(ft.Background.Steps)
fmt.Println("\n" + s(f.indent) + bcl(ft.Background.Keyword+": "+ft.Background.Name, white))
} }
} }
@ -80,15 +72,13 @@ func (f *pretty) Node(node interface{}) {
f.outlineNumExamples = len(t.TableBody) f.outlineNumExamples = len(t.TableBody)
f.outlineNumExample++ f.outlineNumExample++
case *gherkin.Scenario: case *gherkin.Scenario:
f.commentPos = f.longestStep(t.Steps, f.length(t)) f.scenario = t
text := s(f.indent) + bcl(t.Keyword+": ", white) + t.Name f.outline = nil
text += s(f.commentPos-f.length(t)+1) + f.line(t.Location) f.steps = len(t.Steps)
fmt.Println("\n" + text)
case *gherkin.ScenarioOutline: case *gherkin.ScenarioOutline:
f.commentPos = f.longestStep(t.Steps, f.length(t)) f.outline = t
text := s(f.indent) + bcl(t.Keyword+": ", white) + t.Name f.scenario = nil
text += s(f.commentPos-f.length(t)+1) + f.line(t.Location) f.steps = len(t.Steps)
fmt.Println("\n" + text)
f.outlineNumExample = -1 f.outlineNumExample = -1
} }
} }
@ -240,21 +230,54 @@ func (f *pretty) printStep(step *gherkin.Step, def *StepDef, c color) {
} }
func (f *pretty) printStepKind(res *stepResult) { func (f *pretty) printStepKind(res *stepResult) {
// do not print background more than once _, isBgStep := res.owner.(*gherkin.Background)
if _, ok := res.owner.(*gherkin.Background); ok {
switch {
case f.backgroundSteps == 0:
return
case f.backgroundSteps > 0:
f.backgroundSteps--
}
}
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) 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 // 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.outlineSteps = []*stepResult{}
f.outlineNumExamples-- f.outlineNumExamples--
} }
@ -339,3 +362,22 @@ func (f *pretty) longestStep(steps []*gherkin.Step, base int) int {
} }
return ret 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))
}

Просмотреть файл

@ -1,91 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="failure">
<xs:complexType mixed="true">
<xs:attribute name="type" type="xs:string" use="optional"/>
<xs:attribute name="message" type="xs:string" use="optional"/>
</xs:complexType>
</xs:element>
<xs:element name="error">
<xs:complexType mixed="true">
<xs:attribute name="type" type="xs:string" use="optional"/>
<xs:attribute name="message" type="xs:string" use="optional"/>
</xs:complexType>
</xs:element>
<xs:element name="properties">
<xs:complexType>
<xs:sequence>
<xs:element ref="property" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="property">
<xs:complexType>
<xs:attribute name="name" type="xs:string" use="required"/>
<xs:attribute name="value" type="xs:string" use="required"/>
</xs:complexType>
</xs:element>
<xs:element name="skipped" type="xs:string"/>
<xs:element name="system-err" type="xs:string"/>
<xs:element name="system-out" type="xs:string"/>
<xs:element name="testcase">
<xs:complexType>
<xs:sequence>
<xs:element ref="skipped" minOccurs="0" maxOccurs="1"/>
<xs:element ref="error" minOccurs="0" maxOccurs="unbounded"/>
<xs:element ref="failure" minOccurs="0" maxOccurs="unbounded"/>
<xs:element ref="system-out" minOccurs="0" maxOccurs="unbounded"/>
<xs:element ref="system-err" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
<xs:attribute name="name" type="xs:string" use="required"/>
<xs:attribute name="assertions" type="xs:string" use="optional"/>
<xs:attribute name="time" type="xs:string" use="optional"/>
<xs:attribute name="classname" type="xs:string" use="optional"/>
<xs:attribute name="status" type="xs:string" use="optional"/>
</xs:complexType>
</xs:element>
<xs:element name="testsuite">
<xs:complexType>
<xs:sequence>
<xs:element ref="properties" minOccurs="0" maxOccurs="1"/>
<xs:element ref="testcase" minOccurs="0" maxOccurs="unbounded"/>
<xs:element ref="system-out" minOccurs="0" maxOccurs="1"/>
<xs:element ref="system-err" minOccurs="0" maxOccurs="1"/>
</xs:sequence>
<xs:attribute name="name" type="xs:string" use="required"/>
<xs:attribute name="tests" type="xs:string" use="required"/>
<xs:attribute name="failures" type="xs:string" use="optional"/>
<xs:attribute name="errors" type="xs:string" use="optional"/>
<xs:attribute name="time" type="xs:string" use="optional"/>
<xs:attribute name="disabled" type="xs:string" use="optional"/>
<xs:attribute name="skipped" type="xs:string" use="optional"/>
<xs:attribute name="timestamp" type="xs:string" use="optional"/>
<xs:attribute name="hostname" type="xs:string" use="optional"/>
<xs:attribute name="id" type="xs:string" use="optional"/>
<xs:attribute name="package" type="xs:string" use="optional"/>
</xs:complexType>
</xs:element>
<xs:element name="testsuites">
<xs:complexType>
<xs:sequence>
<xs:element ref="testsuite" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
<xs:attribute name="name" type="xs:string" use="optional"/>
<xs:attribute name="time" type="xs:string" use="optional"/>
<xs:attribute name="tests" type="xs:string" use="optional"/>
<xs:attribute name="failures" type="xs:string" use="optional"/>
<xs:attribute name="disabled" type="xs:string" use="optional"/>
<xs:attribute name="errors" type="xs:string" use="optional"/>
</xs:complexType>
</xs:element>
</xs:schema>

Просмотреть файл

@ -1 +0,0 @@
package junit

Просмотреть файл

@ -290,6 +290,10 @@ func (s *Suite) runOutline(outline *gherkin.ScenarioOutline, b *gherkin.Backgrou
} }
steps = append(steps, step) steps = append(steps, step)
} }
// run example table row
s.fmt.Node(group)
// run background // run background
var err error var err error
if b != nil { if b != nil {
@ -340,13 +344,14 @@ func (s *Suite) runScenario(scenario *gherkin.Scenario, b *gherkin.Background) (
f(scenario) f(scenario)
} }
s.fmt.Node(scenario)
// background // background
if b != nil { if b != nil {
err = s.runSteps(b.Steps, err) err = s.runSteps(b.Steps, err)
} }
// scenario // scenario
s.fmt.Node(scenario)
err = s.runSteps(scenario.Steps, err) err = s.runSteps(scenario.Steps, err)
// run after scenario handlers // run after scenario handlers