Merge pull request #67 from ckstettler/cuke-fmt
Added support for the cucumber json format.
Этот коммит содержится в:
коммит
f726a8ed55
7 изменённых файлов: 1053 добавлений и 7 удалений
551
features/formatter/cucumber.feature
Обычный файл
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ Feature: event stream formatter
|
||||||
"""
|
"""
|
||||||
|
|
||||||
Scenario: should process simple scenario
|
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"
|
When I run feature suite with formatter "events"
|
||||||
Then the following events should be fired:
|
Then the following events should be fired:
|
||||||
"""
|
"""
|
||||||
|
@ -35,7 +35,7 @@ Feature: event stream formatter
|
||||||
"""
|
"""
|
||||||
|
|
||||||
Scenario: should process outline scenario
|
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"
|
When I run feature suite with formatter "events"
|
||||||
Then the following events should be fired:
|
Then the following events should be fired:
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -8,10 +8,11 @@ Savybė: užkrauti savybes
|
||||||
Scenarijus: savybių užkrovimas iš aplanko
|
Scenarijus: savybių užkrovimas iš aplanko
|
||||||
Duota savybių aplankas "features"
|
Duota savybių aplankas "features"
|
||||||
Kai aš išskaitau savybes
|
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/background.feature
|
||||||
features/events.feature
|
features/events.feature
|
||||||
|
features/formatter/cucumber.feature
|
||||||
features/formatter/events.feature
|
features/formatter/events.feature
|
||||||
features/lang.feature
|
features/lang.feature
|
||||||
features/load.feature
|
features/load.feature
|
||||||
|
|
|
@ -6,10 +6,11 @@ Feature: load features
|
||||||
Scenario: load features within path
|
Scenario: load features within path
|
||||||
Given a feature path "features"
|
Given a feature path "features"
|
||||||
When I parse features
|
When I parse features
|
||||||
Then I should have 8 feature files:
|
Then I should have 9 feature files:
|
||||||
"""
|
"""
|
||||||
features/background.feature
|
features/background.feature
|
||||||
features/events.feature
|
features/events.feature
|
||||||
|
features/formatter/cucumber.feature
|
||||||
features/formatter/events.feature
|
features/formatter/events.feature
|
||||||
features/lang.feature
|
features/lang.feature
|
||||||
features/load.feature
|
features/load.feature
|
||||||
|
|
333
fmt_cucumber.go
Обычный файл
333
fmt_cucumber.go
Обычный файл
|
@ -0,0 +1,333 @@
|
||||||
|
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 (
|
||||||
|
"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"
|
||||||
|
|
||||||
|
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 - 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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type cukeResult struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
Error string `json:"error_message,omitempty"`
|
||||||
|
Duration *int `json:"duration,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type cukeMatch struct {
|
||||||
|
Location string `json:"location"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type cukeStep struct {
|
||||||
|
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,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,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 // 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{}) {
|
||||||
|
f.basefmt.Node(n)
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
// 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:
|
||||||
|
|
||||||
|
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:
|
||||||
|
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 = "scenario"
|
||||||
|
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:
|
||||||
|
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...)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
f.curFeature.Comments = make([]cukeComment, len(ft.Comments))
|
||||||
|
for idx, comment := range ft.Comments {
|
||||||
|
f.curFeature.Comments[idx].Value = strings.TrimSpace(comment.Text)
|
||||||
|
f.curFeature.Comments[idx].Line = comment.Location.Line
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *cukefmt) Summary() {
|
||||||
|
dat, err := json.MarshalIndent(f.results, "", " ")
|
||||||
|
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
|
||||||
|
switch t := f.owner.(type) {
|
||||||
|
case *gherkin.TableRow:
|
||||||
|
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:
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.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
|
||||||
|
}
|
||||||
|
|
||||||
|
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])
|
||||||
|
|
||||||
|
// 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])
|
||||||
|
|
||||||
|
// 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])
|
||||||
|
}
|
||||||
|
|
||||||
|
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])
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
3
run.go
3
run.go
|
@ -157,6 +157,9 @@ func supportsConcurrency(format string) bool {
|
||||||
return false
|
return false
|
||||||
case "pretty":
|
case "pretty":
|
||||||
return false
|
return false
|
||||||
|
case "cucumber":
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return true // all custom formatters are treated as supporting concurrency
|
return true // all custom formatters are treated as supporting concurrency
|
||||||
}
|
}
|
||||||
|
|
163
suite_test.go
163
suite_test.go
|
@ -4,13 +4,13 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/DATA-DOG/godog/gherkin"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/DATA-DOG/godog/gherkin"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
func TestMain(m *testing.M) {
|
||||||
|
@ -66,6 +66,15 @@ func SuiteContext(s *Suite) {
|
||||||
s.Step(`^passing step$`, func() error {
|
s.Step(`^passing step$`, func() error {
|
||||||
return nil
|
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)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type firedEvent struct {
|
type firedEvent struct {
|
||||||
|
@ -185,7 +194,7 @@ func (s *suiteContext) followingStepsShouldHave(status string, steps *gherkin.Do
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(expected) > len(actual) {
|
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 {
|
for _, a := range actual {
|
||||||
|
@ -376,3 +385,151 @@ func (s *suiteContext) theseEventsHadToBeFiredForNumberOfTimes(tbl *gherkin.Data
|
||||||
}
|
}
|
||||||
return nil
|
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{})
|
||||||
|
|
||||||
|
// 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 fmt.Errorf("err:%v values don't match expected:%s actual:%s", err, expectedCompact, actualCompact)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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" {
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
} 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
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
Загрузка…
Создание таблицы
Сослаться в новой задаче