Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
3b60a9e
start implementing cucumber json
bubenkoff Apr 8, 2014
b9a9634
implementing cucumber json in progress
bubenkoff Apr 11, 2014
c9b509a
correct func name
May 11, 2014
c22464f
fix test
bubenkoff May 11, 2014
961b724
update gitignore for pycharm
May 11, 2014
fd16428
store item in the report
bubenkoff May 11, 2014
3d10266
Merge branch 'cucumber-json-formatter' of https://github.com/paylogic…
May 11, 2014
9f6ce33
add scenario
bubenkoff May 11, 2014
c7cd4ee
Merge branch 'cucumber-json-formatter' of https://github.com/albertja…
May 11, 2014
bc2c347
correct scenario saving
bubenkoff May 11, 2014
9dbb638
Merge branch 'cucumber-json-formatter' of github.com:albertjan/pytest…
bubenkoff May 11, 2014
5935411
scenario attrs
bubenkoff May 11, 2014
375729b
use tests var
May 11, 2014
fe56114
Just get the status
May 11, 2014
870c69f
map each step
May 11, 2014
70472f3
cast reprcrash to string skip if there are no steps
May 11, 2014
127cba2
equals_any added
bubenkoff May 11, 2014
a9ca6db
add feature line number
bubenkoff May 11, 2014
1f62168
add feature hash
May 11, 2014
052e74e
adds description
May 11, 2014
c2dd06f
rename tests to features
May 11, 2014
aa81fea
correct expectation
bubenkoff May 11, 2014
cb6db6f
Merge branch 'cucumber-json-formatter' of github.com:albertjan/pytest…
bubenkoff May 11, 2014
fe49943
change name of scenario
May 11, 2014
50d2bd7
Merge branch 'cucumber-json-formatter' of https://github.com/albertja…
May 11, 2014
504d722
makes the keyword nice
May 11, 2014
164ac9c
correct expectation
bubenkoff May 11, 2014
faa5ece
corrects the reference
May 11, 2014
9c19653
corrects the reference
May 11, 2014
29249b6
Merge branch 'cucumber-json-formatter' of https://github.com/albertja…
May 11, 2014
18ae4c9
cleans some stuff up
May 11, 2014
3d37863
store base dir and filename separately
bubenkoff May 11, 2014
11c1ef5
Merge branch 'cucumber-json-formatter' of github.com:albertjan/pytest…
bubenkoff May 11, 2014
be87dff
fix failing test
bubenkoff May 11, 2014
4ed618e
clean up and use rel path
May 11, 2014
d740515
clean up and use rel path
May 11, 2014
62898e5
PR
bubenkoff May 11, 2014
34a4ac6
Merge branch 'cucumber-json-formatter' of github.com:albertjan/pytest…
bubenkoff May 11, 2014
fce9aab
changes according to @dimazest
May 11, 2014
02989bb
Merge branch 'cucumber-json-formatter' of https://github.com/albertja…
May 11, 2014
1fe8341
pep fixes
bubenkoff May 11, 2014
f1bf4bc
pep fixes
bubenkoff May 11, 2014
3f5da0f
pep fixes
bubenkoff May 11, 2014
56d2782
pep fixes
bubenkoff May 11, 2014
79b40fb
pep fixes
bubenkoff May 11, 2014
2c6c35f
added docs
bubenkoff May 11, 2014
8f970bd
comma added
May 11, 2014
15f95df
merge with master
bubenkoff Jul 24, 2014
fa66c68
add makefile
bubenkoff Jul 24, 2014
44be5f4
merge with master
bubenkoff Jul 24, 2014
59fc750
upgrade py
bubenkoff Jul 24, 2014
5d51498
serialize scenario info
bubenkoff Jul 24, 2014
a427de3
add verbosity
bubenkoff Jul 24, 2014
fde61f7
Merge branch 'master' into cucumber-json-formatter
bubenkoff Jul 24, 2014
9f24385
use filename instead of name which can be empty #ep14boat
bubenkoff Jul 24, 2014
5aaf42e
better reporting
bubenkoff Jul 25, 2014
b08d385
correct reporting of exceptions
bubenkoff Jul 25, 2014
76be972
unicode fixes
bubenkoff Jul 25, 2014
ae5a2a1
Merge branch 'master' of github.com:olegpidsadnyi/pytest-bdd
bubenkoff Jul 25, 2014
7a968fb
Merge branch 'master' into cucumber-json-formatter
bubenkoff Jul 25, 2014
6e80100
multiple steps instead of one
bubenkoff Jul 26, 2014
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
*.rej
*.py[cod]
/.env
*.orig

# C extensions
*.so
Expand Down Expand Up @@ -40,6 +43,9 @@ nosetests.xml
# Sublime
/*.sublime-*

#PyCharm
/.idea

# virtualenv
/.Python
/lib
Expand Down
5 changes: 5 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
Changelog
=========

2.2.0
-----

- Implemented cucumber json formatter (bubenkoff, albertjan)


2.1.2
-----
Expand Down
11 changes: 11 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# create virtual environment
.env:
virtualenv .env

# install all needed for development
develop: .env
.env/bin/pip install -e . -r requirements-testing.txt

# clean the development envrironment
clean:
-rm -rf .env
14 changes: 14 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -594,6 +594,20 @@ Tools recommended to use for browser testing:
* `pytest-splinter <https://github.com/paylogic/pytest-splinter>`_ - pytest `splinter <http://splinter.cobrateam.info/>`_ integration for the real browser testing


Reporting
---------

It's important to have nice reporting out of your bdd tests. Cucumber introduced some kind of standard for
`json format <https://www.relishapp.com/cucumber/cucumber/docs/json-output-formatter>`_
which can be used for `this <https://wiki.jenkins-ci.org/display/JENKINS/Cucumber+Test+Result+Plugin>`_ jenkins
plugin

To have an output in json format:

::

py.test --cucumberjson=<path to json report>


Migration of your tests from versions 0.x.x-1.x.x
-------------------------------------------------
Expand Down
3 changes: 2 additions & 1 deletion pytest_bdd/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
__version__ = '2.1.2'
"""pytest-bdd public api."""
__version__ = '2.2.0'

try:
from pytest_bdd.steps import given, when, then # pragma: no cover
Expand Down
120 changes: 120 additions & 0 deletions pytest_bdd/cucumber_json.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
"""Cucumber json output formatter."""
import json
import os
import time

import py

from .feature import force_unicode


def pytest_addoption(parser):
group = parser.getgroup('pytest-bdd')
group.addoption(
'--cucumberjson', '--cucumber-json', action='store',
dest='cucumber_json_path', metavar='path', default=None,
help='create cucumber json style report file at given path.')


def pytest_configure(config):
cucumber_json_path = config.option.cucumber_json_path
# prevent opening json log on slave nodes (xdist)
if cucumber_json_path and not hasattr(config, 'slaveinput'):
config._bddcucumberjson = LogBDDCucumberJSON(cucumber_json_path)
config.pluginmanager.register(config._bddcucumberjson)


def pytest_unconfigure(config):
xml = getattr(config, '_bddcucumberjson', None)
if xml is not None:
del config._bddcucumberjson
config.pluginmanager.unregister(xml)


class LogBDDCucumberJSON(object):

"""Logging plugin for cucumber like json output."""

def __init__(self, logfile):
logfile = os.path.expanduser(os.path.expandvars(logfile))
self.logfile = os.path.normpath(os.path.abspath(logfile))
self.features = {}

def append(self, obj):
self.features[-1].append(obj)

def _get_result(self, step, report):
"""Get scenario test run result.

:param step: `Step` step we get result for
:param report: pytest `Report` object
:return: `dict` in form {'status': '<passed|failed|skipped>', ['error_message': '<error_message>']}
"""
if report.passed or not step['failed']: # ignore setup/teardown
return {'status': 'passed'}
elif report.failed and step['failed']:
return {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return {
    'status': 'failed',
    'error_message': str(report.longrepr.reprcrash), # add a comma
 } # move this bracket to the new line

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

'status': 'failed',
'error_message': force_unicode(report.longrepr),
}
elif report.skipped:
return {'status': 'skipped'}

def pytest_runtest_logreport(self, report):
try:
scenario = report.scenario
except AttributeError:
# skip reporting for non-bdd tests
return

if not scenario['steps'] or report.when != 'call':
# skip if there isn't a result or scenario has no steps
return

def stepmap(step):
return {
"keyword": step['keyword'],
"name": step['name'],
"line": step['line_number'],
"match": {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe it can be fitted to one line

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

"location": ""
},
"result": self._get_result(step, report),
}

if scenario['feature']['filename'] not in self.features:
self.features[scenario['feature']['filename']] = {
"keyword": "Feature",
"uri": scenario['feature']['rel_filename'],
"name": scenario['feature']['name'] or scenario['feature']['rel_filename'],
"id": scenario['feature']['rel_filename'].lower().replace(' ', '-'),
"line": scenario['feature']['line_number'],
"description": scenario['feature']['description'],
"tags": [],
"elements": [],
}

self.features[scenario['feature']['filename']]['elements'].append({
"keyword": "Scenario",
"id": report.item['name'],
"name": scenario['name'],
"line": scenario['line_number'],
"description": '',
"tags": [],
"type": "scenario",
"steps": [stepmap(step) for step in scenario['steps']],
})

def pytest_sessionstart(self):
self.suite_start_time = time.time()

def pytest_sessionfinish(self):
if py.std.sys.version_info[0] < 3:
logfile_open = py.std.codecs.open
else:
logfile_open = open
with logfile_open(self.logfile, 'w', encoding='utf-8') as logfile:
logfile.write(json.dumps(list(self.features.values())))

def pytest_terminal_summary(self, terminalreporter):
terminalreporter.write_sep('-', 'generated json file: %s' % (self.logfile))
77 changes: 51 additions & 26 deletions pytest_bdd/feature.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
one line.

"""
from os import path as op # pragma: no cover

import re # pragma: no cover
import sys # pragma: no cover
import textwrap
Expand All @@ -32,6 +34,7 @@


class FeatureError(Exception): # pragma: no cover

"""Feature parse error."""

message = u'{0}.\nLine number: {1}.\nLine: {2}.'
Expand Down Expand Up @@ -93,18 +96,17 @@ def strip_comments(line):
return line.strip()


def remove_prefix(line):
"""Remove the step prefix (Scenario, Given, When, Then or And).
def parse_line(line):
"""Parse step line to get the step prefix (Scenario, Given, When, Then or And) and the actual step name.

:param line: Line of the Feature file.

:return: Line without the prefix.

:return: `tuple` in form ('<prefix>', '<Line without the prefix>').
"""
for prefix, _ in STEP_PREFIXES:
if line.startswith(prefix):
return line[len(prefix):].strip()
return line
return prefix.strip(), line[len(prefix):].strip()
return '', line


def _open_file(filename, encoding):
Expand All @@ -114,11 +116,19 @@ def _open_file(filename, encoding):
return open(filename, 'r', encoding=encoding)


def force_unicode(string, encoding='utf-8'):
if sys.version_info < (3, 0) and isinstance(string, str):
return string.decode(encoding)
def force_unicode(obj, encoding='utf-8'):
"""Get the unicode string out of given object (python 2 and python 3).

:param obj: `object`, usually a string
:return: unicode string
"""
if sys.version_info < (3, 0):
if isinstance(obj, str):
return obj.decode(encoding)
else:
return unicode(obj)
else:
return string
return str(obj)


def force_encode(string, encoding='utf-8'):
Expand All @@ -129,17 +139,20 @@ def force_encode(string, encoding='utf-8'):


class Feature(object):

"""Feature."""

def __init__(self, filename, encoding='utf-8'):
def __init__(self, basedir, filename, encoding='utf-8'):
"""Parse the feature file.

:param filename: Relative path to the feature file.

"""
self.scenarios = {}

self.filename = filename
self.rel_filename = op.join(op.basename(basedir), filename)
self.filename = filename = op.abspath(op.join(basedir, filename))
self.line_number = 1
self.name = None
scenario = None
mode = None
prev_mode = None
Expand Down Expand Up @@ -181,16 +194,17 @@ def __init__(self, filename, encoding='utf-8'):

if mode == types.FEATURE:
if prev_mode != types.FEATURE:
self.name = remove_prefix(clean_line)
_, self.name = parse_line(clean_line)
self.line_number = line_number
else:
description.append(clean_line)

prev_mode = mode

# Remove Feature, Given, When, Then, And
clean_line = remove_prefix(clean_line)
keyword, clean_line = parse_line(clean_line)
if mode in [types.SCENARIO, types.SCENARIO_OUTLINE]:
self.scenarios[clean_line] = scenario = Scenario(self, clean_line)
self.scenarios[clean_line] = scenario = Scenario(self, clean_line, line_number)
elif mode == types.EXAMPLES:
mode = types.EXAMPLES_HEADERS
elif mode == types.EXAMPLES_VERTICAL:
Expand All @@ -205,14 +219,16 @@ def __init__(self, filename, encoding='utf-8'):
scenario.add_example_row(clean_line[0], clean_line[1:])
elif mode and mode != types.FEATURE:
step = scenario.add_step(
step_name=clean_line, step_type=mode, indent=line_indent, line_number=line_number)
step_name=clean_line, step_type=mode, indent=line_indent, line_number=line_number,
keyword=keyword)

self.description = u'\n'.join(description)

@classmethod
def get_feature(cls, filename, encoding='utf-8'):
def get_feature(cls, base_path, filename, encoding='utf-8'):
"""Get a feature by the filename.

:param base_path: Base feature directory.
:param filename: Filename of the feature file.

:return: `Feature` instance from the parsed feature cache.
Expand All @@ -222,37 +238,43 @@ def get_feature(cls, filename, encoding='utf-8'):
when multiple scenarios are referencing the same file.

"""
feature = features.get(filename)
full_name = op.abspath(op.join(base_path, filename))
feature = features.get(full_name)
if not feature:
feature = Feature(filename, encoding=encoding)
features[filename] = feature
feature = Feature(base_path, filename, encoding=encoding)
features[full_name] = feature
return feature


class Scenario(object):

"""Scenario."""

def __init__(self, feature, name, example_converters=None):
def __init__(self, feature, name, line_number, example_converters=None):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think line_number is not optional?

self.feature = feature
self.name = name
self.params = set()
self.steps = []
self.example_params = []
self.examples = []
self.vertical_examples = []
self.line_number = line_number
self.example_converters = example_converters

def add_step(self, step_name, step_type, indent, line_number):
def add_step(self, step_name, step_type, indent, line_number, keyword):
"""Add step to the scenario.

:param step_name: Step name.
:param step_type: Step type.

:param indent: `int` step text indent
:param line_number: `int` line number
:param keyword: `str` step keyword
"""
params = get_step_params(step_name)
self.params.update(params)
step = Step(
name=step_name, type=step_type, params=params, scenario=self, indent=indent, line_number=line_number)
name=step_name, type=step_type, params=params, scenario=self, indent=indent, line_number=line_number,
keyword=keyword)
self.steps.append(step)
return step

Expand Down Expand Up @@ -326,16 +348,19 @@ def validate(self):


class Step(object):

"""Step."""

def __init__(self, name, type, params, scenario, indent, line_number):
def __init__(self, name, type, params, scenario, indent, line_number, keyword):
self.name = name
self.keyword = keyword
self.lines = []
self.indent = indent
self.type = type
self.params = params
self.scenario = scenario
self.line_number = line_number
self.failed = False

def add_line(self, line):
"""Add line to the multiple step."""
Expand Down
Loading