Этот коммит содержится в:
Stettler, Robert 2017-03-02 10:50:30 -05:00
родитель 7a4d079c2a
коммит 819ce03090
3 изменённых файлов: 594 добавлений и 42 удалений

551
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 <status> 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"
}
}
]
}
]
}
]
"""

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

@ -64,7 +64,7 @@ type cukeTag struct {
type cukeResult struct { type cukeResult struct {
Status string `json:"status"` Status string `json:"status"`
Error string `json:"error_message,omitempty"` Error string `json:"error_message,omitempty"`
Duration *int `json:"duration,omitempty"` Duration *int `json:"duration,omitempty"`
} }
type cukeMatch struct { type cukeMatch struct {
@ -72,12 +72,12 @@ type cukeMatch struct {
} }
type cukeStep struct { type cukeStep struct {
Keyword string `json:"keyword"` Keyword string `json:"keyword"`
Name string `json:"name"` Name string `json:"name"`
Line int `json:"line"` Line int `json:"line"`
Docstring *cukeDocstring `json:"doc_string,omitempty"` Docstring *cukeDocstring `json:"doc_string,omitempty"`
Match cukeMatch `json:"match"` Match cukeMatch `json:"match"`
Result cukeResult `json:"result"` Result cukeResult `json:"result"`
} }
type cukeElement struct { type cukeElement struct {
@ -106,25 +106,25 @@ type cukeFeatureJSON struct {
type cukefmt struct { type cukefmt struct {
basefmt basefmt
// currently running feature path, to be part of id. // currently running feature path, to be part of id.
// this is sadly not passed by gherkin nodes. // this is sadly not passed by gherkin nodes.
// it restricts this formatter to run only in synchronous single // it restricts this formatter to run only in synchronous single
// threaded execution. Unless running a copy of formatter for each feature // threaded execution. Unless running a copy of formatter for each feature
path string path string
stat stepType // last step status, before skipped stat stepType // last step status, before skipped
outlineSteps int // number of current outline scenario steps outlineSteps int // number of current outline scenario steps
ID string // current test id. ID string // current test id.
results []cukeFeatureJSON // structure that represent cuke results results []cukeFeatureJSON // structure that represent cuke results
curStep *cukeStep // track the current step curStep *cukeStep // track the current step
curElement *cukeElement // track the current element 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 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 // so I need to keep track of the current outline
curRow int // current row of the example table as it is being processed. 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. curExampleTags []cukeTag // temporary storage for tags associate with the current example table.
startTime time.Time // used to time duration of the step execution 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 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. // of the example name inorder to build id fields.
} }
func (f *cukefmt) Node(n interface{}) { func (f *cukefmt) Node(n interface{}) {
@ -157,7 +157,7 @@ func (f *cukefmt) Node(n interface{}) {
f.curOutline.Keyword = t.Keyword 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.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 // apply feature level tags
if len(f.curOutline.Tags) > 0 { if len(f.curOutline.Tags) > 0 {
@ -165,15 +165,15 @@ func (f *cukefmt) Node(n interface{}) {
// apply outline level tags. // apply outline level tags.
for idx, element := range t.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)].Line = element.Location.Line
f.curOutline.Tags[idx + len(f.curFeature.Tags)].Name = element.Name f.curOutline.Tags[idx+len(f.curFeature.Tags)].Name = element.Name
} }
} }
// This scenario adds the element to the output immediately. // This scenario adds the element to the output immediately.
case *gherkin.Scenario: case *gherkin.Scenario:
f.curFeature.Elements = append(f.curFeature.Elements, cukeElement{}) 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.Name = t.Name
f.curElement.Line = t.Location.Line f.curElement.Line = t.Location.Line
@ -181,7 +181,7 @@ func (f *cukefmt) Node(n interface{}) {
f.curElement.Keyword = t.Keyword 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.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 // apply feature level tags
@ -189,8 +189,8 @@ func (f *cukefmt) Node(n interface{}) {
// apply scenario level tags. // apply scenario level tags.
for idx, element := range t.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)].Line = element.Location.Line
f.curElement.Tags[idx + len(f.curFeature.Tags)].Name = element.Name 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) tmpElem.ID = tmpElem.ID + ";" + f.curExampleName + ";" + strconv.Itoa(f.curRow)
f.curRow++ f.curRow++
f.curFeature.Elements = append(f.curFeature.Elements, tmpElem) 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. // copy in example level tags.
f.curElement.Tags = append(f.curElement.Tags, f.curExampleTags...) 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.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.URI = p
f.curFeature.Name = ft.Name f.curFeature.Name = ft.Name
f.curFeature.Keyword = ft.Keyword 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.startTime = time.Now() // start timing the step
f.curElement.Steps = append(f.curElement.Steps, cukeStep{}) 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.Name = step.Text
f.curStep.Line = step.Location.Line 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) { func (f *cukefmt) Passed(step *gherkin.Step, match *StepDef) {
f.basefmt.Passed(step, match) f.basefmt.Passed(step, match)
f.stat = passed 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) { func (f *cukefmt) Skipped(step *gherkin.Step) {
f.basefmt.Skipped(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. // no duration reported for skipped.
f.curStep.Result.Duration = nil f.curStep.Result.Duration = nil
@ -309,7 +309,7 @@ func (f *cukefmt) Skipped(step *gherkin.Step) {
func (f *cukefmt) Undefined(step *gherkin.Step) { func (f *cukefmt) Undefined(step *gherkin.Step) {
f.basefmt.Undefined(step) f.basefmt.Undefined(step)
f.stat = undefined 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. // 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)
@ -319,13 +319,13 @@ func (f *cukefmt) Undefined(step *gherkin.Step) {
func (f *cukefmt) Failed(step *gherkin.Step, match *StepDef, err error) { func (f *cukefmt) Failed(step *gherkin.Step, match *StepDef, err error) {
f.basefmt.Failed(step, match, err) f.basefmt.Failed(step, match, err)
f.stat = failed 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) { func (f *cukefmt) Pending(step *gherkin.Step, match *StepDef) {
f.stat = pending f.stat = pending
f.basefmt.Pending(step, match) 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. // 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)

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

@ -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("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 { 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 return nil
} }
/* /*
Due to specialize matching logic to ignore exact matches on the "location" and "duration" fields. It was 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. 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 return nil
} }