From 3b60a9e0e5f43fc079a18bc314afe0f43b1957c3 Mon Sep 17 00:00:00 2001 From: Anatoly Bubenkov Date: Wed, 9 Apr 2014 00:40:30 +0200 Subject: [PATCH 01/46] start implementing cucumber json --- pytest_bdd/cucumber_json.py | 177 ++++++++++++++++++++++++++++ setup.py | 1 + tests/feature/test_cucumber_json.py | 120 +++++++++++++++++++ 3 files changed, 298 insertions(+) create mode 100644 pytest_bdd/cucumber_json.py create mode 100644 tests/feature/test_cucumber_json.py diff --git a/pytest_bdd/cucumber_json.py b/pytest_bdd/cucumber_json.py new file mode 100644 index 000000000..6872e4534 --- /dev/null +++ b/pytest_bdd/cucumber_json.py @@ -0,0 +1,177 @@ +'''Cucumber json output formatter.''' +import os + +import py + + +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: + del config._bddcucumberjson + config.pluginmanager.unregister(xml) + + +def mangle_testnames(names): + names = [x.replace('.py', '') for x in names if x != '()'] + names[0] = names[0].replace('/', '.') + return names + + +class LogBDDCucumberJSON(object): + """Log 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.tests = [] + self.passed = self.skipped = 0 + self.failed = self.errors = 0 + + def _opentestcase(self, report): + names = mangle_testnames(report.nodeid.split('::')) + classnames = names[:-1] + if self.prefix: + classnames.insert(0, self.prefix) + self.tests.append(Junit.testcase( + classname='.'.join(classnames), + name=bin_xml_escape(names[-1]), + time=getattr(report, 'duration', 0) + )) + + def _write_captured_output(self, report): + for capname in ('out', 'err'): + allcontent = '' + for name, content in report.get_sections('Captured std%s' % capname): + allcontent += content + if allcontent: + tag = getattr(Junit, 'system-'+capname) + self.append(tag(bin_xml_escape(allcontent))) + + def append(self, obj): + self.tests[-1].append(obj) + + def append_pass(self, report): + self.passed += 1 + self._write_captured_output(report) + + def append_failure(self, report): + #msg = str(report.longrepr.reprtraceback.extraline) + if hasattr(report, 'wasxfail'): + self.append( + Junit.skipped(message='xfail-marked test passes unexpectedly')) + self.skipped += 1 + else: + fail = Junit.failure(message='test failure') + fail.append(bin_xml_escape(report.longrepr)) + self.append(fail) + self.failed += 1 + self._write_captured_output(report) + + def append_collect_failure(self, report): + #msg = str(report.longrepr.reprtraceback.extraline) + self.append(Junit.failure(bin_xml_escape(report.longrepr), + message='collection failure')) + self.errors += 1 + + def append_collect_skipped(self, report): + #msg = str(report.longrepr.reprtraceback.extraline) + self.append(Junit.skipped(bin_xml_escape(report.longrepr), + message='collection skipped')) + self.skipped += 1 + + def append_error(self, report): + self.append(Junit.error(bin_xml_escape(report.longrepr), + message='test setup failure')) + self.errors += 1 + + def append_skipped(self, report): + if hasattr(report, 'wasxfail'): + self.append(Junit.skipped(bin_xml_escape(report.wasxfail), + message='expected test failure')) + else: + filename, lineno, skipreason = report.longrepr + if skipreason.startswith('Skipped: '): + skipreason = bin_xml_escape(skipreason[9:]) + self.append( + Junit.skipped( + '%s:%s: %s' % report.longrepr, + type='pytest.skip', + message=skipreason)) + self.skipped += 1 + self._write_captured_output(report) + + def pytest_runtest_logreport(self, report): + if report.passed: + if report.when == 'call': # ignore setup/teardown + self._opentestcase(report) + self.append_pass(report) + elif report.failed: + self._opentestcase(report) + if report.when != 'call': + self.append_error(report) + else: + self.append_failure(report) + elif report.skipped: + self._opentestcase(report) + self.append_skipped(report) + + def pytest_collectreport(self, report): + if not report.passed: + self._opentestcase(report) + if report.failed: + self.append_collect_failure(report) + else: + self.append_collect_skipped(report) + + def pytest_internalerror(self, excrepr): + self.errors += 1 + data = bin_xml_escape(excrepr) + self.tests.append( + Junit.testcase( + Junit.error(data, message='internal error'), + classname='pytest', + name='internal')) + + def pytest_sessionstart(self): + self.suite_start_time = time.time() + + def pytest_sessionfinish(self): + if py.std.sys.version_info[0] < 3: + logfile = py.std.codecs.open(self.logfile, 'w', encoding='utf-8') + else: + logfile = open(self.logfile, 'w', encoding='utf-8') + + suite_stop_time = time.time() + suite_time_delta = suite_stop_time - self.suite_start_time + numtests = self.passed + self.failed + + logfile.write('') + logfile.write(Junit.testsuite( + self.tests, + name='pytest', + errors=self.errors, + failures=self.failed, + skips=self.skipped, + tests=numtests, + time='%.3f' % suite_time_delta, + ).unicode(indent=0)) + logfile.close() + + def pytest_terminal_summary(self, terminalreporter): + terminalreporter.write_sep('-', 'generated json file: %s' % (self.logfile)) diff --git a/setup.py b/setup.py index 7e1c23a16..97314af66 100755 --- a/setup.py +++ b/setup.py @@ -59,6 +59,7 @@ def run_tests(self): entry_points={ 'pytest11': [ 'pytest-bdd = pytest_bdd.plugin', + 'pytest-bdd-cucumber-json = pytest_bdd.cucumber_json', ], 'console_scripts': [ 'pytestbdd_migrate_tests = pytest_bdd.scripts:migrate_tests [migrate]' diff --git a/tests/feature/test_cucumber_json.py b/tests/feature/test_cucumber_json.py new file mode 100644 index 000000000..120439965 --- /dev/null +++ b/tests/feature/test_cucumber_json.py @@ -0,0 +1,120 @@ +"""Test cucumber json output.""" +import json +import textwrap + + +def runandparse(testdir, *args): + """Run tests in testdir and parse json output.""" + resultpath = testdir.tmpdir.join("cucumber.json") + result = testdir.runpytest('--cucumberjson={0}'.format(resultpath), *args) + jsonobject = json.loads(result) + return result, jsonobject + + +def test_step_trace(testdir): + """Test step trace.""" + testdir.makefile('.feature', test=textwrap.dedent(""" + Feature: One passing scenario, one failing scenario + + Scenario: Passing + Given a passing step + + Scenario: Failing + Given a failing step + """)) + testdir.makepyfile(textwrap.dedent(""" + import pytest + from pytest_bdd import given, when, scenario + + @given('a passing step') + def a_passing_step(): + return 'pass' + + @given('a failing step') + def a_passing_step(): + raise Exception('Error') + + @scenario('test.feature', 'Passing') + def test_passing(): + pass + + @scenario('test.feature', 'Failing') + def test_failing(): + pass + """)) + result, jsonobject = runandparse(testdir) + assert result.ret + assert jsonobject == [ + { + "description": "", + "elements": [ + { + "description": "", + "id": "one-passing-scenario,-one-failing-scenario;passing", + "keyword": "Scenario", + "line": 5, + "name": "Passing", + "steps": [ + { + "keyword": "Given ", + "line": 6, + "match": { + "location": "features/step_definitions/steps.rb:1" + }, + "name": "a passing step", + "result": { + "status": "passed" + } + } + ], + "tags": [ + { + "line": 4, + "name": "@b" + } + ], + "type": "scenario" + }, + { + "description": "", + "id": "one-passing-scenario,-one-failing-scenario;failing", + "keyword": "Scenario", + "line": 9, + "name": "Failing", + "steps": [ + { + "keyword": "Given ", + "line": 10, + "match": { + "location": "features/step_definitions/steps.rb:5" + }, + "name": "a failing step", + "result": { + "error_message": " (RuntimeError)\n./features/step_definitions/steps.rb:6:in /a " + "failing step/'\nfeatures/one_passing_one_failing.feature:10:in Given a failing step'", + "status": "failed" + } + } + ], + "tags": [ + { + "line": 8, + "name": "@c" + } + ], + "type": "scenario" + } + ], + "id": "one-passing-scenario,-one-failing-scenario", + "keyword": "Feature", + "line": 2, + "name": "One passing scenario, one failing scenario", + "tags": [ + { + "line": 1, + "name": "@a" + } + ], + "uri": "features/one_passing_one_failing.feature" + } + ] From b9a9634482c77a2d44d655804720aab9a65bfaf8 Mon Sep 17 00:00:00 2001 From: Anatoly Bubenkov Date: Fri, 11 Apr 2014 22:13:04 +0200 Subject: [PATCH 02/46] implementing cucumber json in progress --- pytest_bdd/cucumber_json.py | 157 +++++++++++++----------------------- 1 file changed, 55 insertions(+), 102 deletions(-) diff --git a/pytest_bdd/cucumber_json.py b/pytest_bdd/cucumber_json.py index 6872e4534..d38fd303a 100644 --- a/pytest_bdd/cucumber_json.py +++ b/pytest_bdd/cucumber_json.py @@ -1,5 +1,8 @@ '''Cucumber json output formatter.''' import os +import time + +import json import py @@ -40,96 +43,60 @@ def __init__(self, logfile): logfile = os.path.expanduser(os.path.expandvars(logfile)) self.logfile = os.path.normpath(os.path.abspath(logfile)) self.tests = [] - self.passed = self.skipped = 0 - self.failed = self.errors = 0 - - def _opentestcase(self, report): - names = mangle_testnames(report.nodeid.split('::')) - classnames = names[:-1] - if self.prefix: - classnames.insert(0, self.prefix) - self.tests.append(Junit.testcase( - classname='.'.join(classnames), - name=bin_xml_escape(names[-1]), - time=getattr(report, 'duration', 0) - )) + self.features = [] - def _write_captured_output(self, report): - for capname in ('out', 'err'): - allcontent = '' - for name, content in report.get_sections('Captured std%s' % capname): - allcontent += content - if allcontent: - tag = getattr(Junit, 'system-'+capname) - self.append(tag(bin_xml_escape(allcontent))) + # def _write_captured_output(self, report): + # for capname in ('out', 'err'): + # allcontent = '' + # for name, content in report.get_sections('Captured std%s' % capname): + # allcontent += content + # if allcontent: + # tag = getattr(Junit, 'system-'+capname) + # self.append(tag(bin_xml_escape(allcontent))) def append(self, obj): self.tests[-1].append(obj) - def append_pass(self, report): - self.passed += 1 - self._write_captured_output(report) - - def append_failure(self, report): - #msg = str(report.longrepr.reprtraceback.extraline) - if hasattr(report, 'wasxfail'): - self.append( - Junit.skipped(message='xfail-marked test passes unexpectedly')) - self.skipped += 1 - else: - fail = Junit.failure(message='test failure') - fail.append(bin_xml_escape(report.longrepr)) - self.append(fail) - self.failed += 1 - self._write_captured_output(report) - - def append_collect_failure(self, report): - #msg = str(report.longrepr.reprtraceback.extraline) - self.append(Junit.failure(bin_xml_escape(report.longrepr), - message='collection failure')) - self.errors += 1 - - def append_collect_skipped(self, report): - #msg = str(report.longrepr.reprtraceback.extraline) - self.append(Junit.skipped(bin_xml_escape(report.longrepr), - message='collection skipped')) - self.skipped += 1 - - def append_error(self, report): - self.append(Junit.error(bin_xml_escape(report.longrepr), - message='test setup failure')) - self.errors += 1 - - def append_skipped(self, report): - if hasattr(report, 'wasxfail'): - self.append(Junit.skipped(bin_xml_escape(report.wasxfail), - message='expected test failure')) - else: - filename, lineno, skipreason = report.longrepr - if skipreason.startswith('Skipped: '): - skipreason = bin_xml_escape(skipreason[9:]) - self.append( - Junit.skipped( - '%s:%s: %s' % report.longrepr, - type='pytest.skip', - message=skipreason)) - self.skipped += 1 - self._write_captured_output(report) - - def pytest_runtest_logreport(self, report): + def _get_result(report): + """Get scenario test run result.""" if report.passed: if report.when == 'call': # ignore setup/teardown - self._opentestcase(report) - self.append_pass(report) + return {'status': 'passed'} elif report.failed: - self._opentestcase(report) - if report.when != 'call': - self.append_error(report) - else: - self.append_failure(report) + return { + 'status': 'failed', + 'error_message': report.longrepr} elif report.skipped: - self._opentestcase(report) - self.append_skipped(report) + return {'status': 'skipped'} + + def pytest_runtest_logreport(self, report): + names = mangle_testnames(report.nodeid.split('::')) + classnames = names[:-1] + test_id = '.'.join(classnames) + scenario = report.node.scenario + self.tests.append( + { + "keyword": "Scenario", + "id": test_id, + "name": scenario.name, + "line": scenario.line_number, + "description": scenario.description, + "tags": [], + "type": "scenario", + "time": getattr(report, 'duration', 0), + "steps": [ + { + "keyword": "Given ", + "name": "a failing step", + "line": 10, + "match": { + "location": "features/step_definitions/steps.rb:5" + }, + "result": self._get_result(report) + } + ] + } + ) def pytest_collectreport(self, report): if not report.passed: @@ -140,13 +107,12 @@ def pytest_collectreport(self, report): self.append_collect_skipped(report) def pytest_internalerror(self, excrepr): - self.errors += 1 - data = bin_xml_escape(excrepr) - self.tests.append( - Junit.testcase( - Junit.error(data, message='internal error'), - classname='pytest', - name='internal')) + self.tests.append(dict( + name='internal', + description='pytest', + steps=[], + error_message=dict(error=excrepr, message='internal error'), + )) def pytest_sessionstart(self): self.suite_start_time = time.time() @@ -157,20 +123,7 @@ def pytest_sessionfinish(self): else: logfile = open(self.logfile, 'w', encoding='utf-8') - suite_stop_time = time.time() - suite_time_delta = suite_stop_time - self.suite_start_time - numtests = self.passed + self.failed - - logfile.write('') - logfile.write(Junit.testsuite( - self.tests, - name='pytest', - errors=self.errors, - failures=self.failed, - skips=self.skipped, - tests=numtests, - time='%.3f' % suite_time_delta, - ).unicode(indent=0)) + logfile.write(json.dumps(self.features)) logfile.close() def pytest_terminal_summary(self, terminalreporter): From c9b509a296f4247c6834b54348284ebbfdfc0f74 Mon Sep 17 00:00:00 2001 From: albertjan Date: Sun, 11 May 2014 11:21:09 +0200 Subject: [PATCH 03/46] correct func name --- tests/feature/test_cucumber_json.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/feature/test_cucumber_json.py b/tests/feature/test_cucumber_json.py index 120439965..25548072f 100644 --- a/tests/feature/test_cucumber_json.py +++ b/tests/feature/test_cucumber_json.py @@ -31,7 +31,7 @@ def a_passing_step(): return 'pass' @given('a failing step') - def a_passing_step(): + def a_failing_step(): raise Exception('Error') @scenario('test.feature', 'Passing') From c22464f01aa7448b21549a6c276a8e517412b251 Mon Sep 17 00:00:00 2001 From: Anatoly Bubenkov Date: Sun, 11 May 2014 11:26:18 +0200 Subject: [PATCH 04/46] fix test --- .gitignore | 1 + pytest_bdd/cucumber_json.py | 3 +-- tests/feature/test_cucumber_json.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 054d99c51..9cf52df73 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ *.py[cod] +/.env # C extensions *.so diff --git a/pytest_bdd/cucumber_json.py b/pytest_bdd/cucumber_json.py index d38fd303a..a770be407 100644 --- a/pytest_bdd/cucumber_json.py +++ b/pytest_bdd/cucumber_json.py @@ -1,4 +1,4 @@ -'''Cucumber json output formatter.''' +"""Cucumber json output formatter.""" import os import time @@ -100,7 +100,6 @@ def pytest_runtest_logreport(self, report): def pytest_collectreport(self, report): if not report.passed: - self._opentestcase(report) if report.failed: self.append_collect_failure(report) else: diff --git a/tests/feature/test_cucumber_json.py b/tests/feature/test_cucumber_json.py index 120439965..0c672a586 100644 --- a/tests/feature/test_cucumber_json.py +++ b/tests/feature/test_cucumber_json.py @@ -7,7 +7,7 @@ def runandparse(testdir, *args): """Run tests in testdir and parse json output.""" resultpath = testdir.tmpdir.join("cucumber.json") result = testdir.runpytest('--cucumberjson={0}'.format(resultpath), *args) - jsonobject = json.loads(result) + jsonobject = json.load(resultpath.open()) return result, jsonobject From 961b724a0708f9beb41a5323b921c24e3e163e9e Mon Sep 17 00:00:00 2001 From: albertjan Date: Sun, 11 May 2014 11:36:22 +0200 Subject: [PATCH 05/46] update gitignore for pycharm --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 054d99c51..57b548430 100644 --- a/.gitignore +++ b/.gitignore @@ -40,6 +40,9 @@ nosetests.xml # Sublime /*.sublime-* +#PyCharm +/.idea + # virtualenv /.Python /lib From fd16428ab764610124935dde7f932b9c243b6ce0 Mon Sep 17 00:00:00 2001 From: Anatoly Bubenkov Date: Sun, 11 May 2014 13:18:43 +0200 Subject: [PATCH 06/46] store item in the report --- pytest_bdd/cucumber_json.py | 21 +++++---------------- pytest_bdd/plugin.py | 9 +++++++++ tests/feature/test_cucumber_json.py | 2 +- 3 files changed, 15 insertions(+), 17 deletions(-) diff --git a/pytest_bdd/cucumber_json.py b/pytest_bdd/cucumber_json.py index a770be407..bf9db7f6b 100644 --- a/pytest_bdd/cucumber_json.py +++ b/pytest_bdd/cucumber_json.py @@ -73,7 +73,11 @@ def pytest_runtest_logreport(self, report): names = mangle_testnames(report.nodeid.split('::')) classnames = names[:-1] test_id = '.'.join(classnames) - scenario = report.node.scenario + try: + scenario = report.item.scenario + except AttributeError: + # skip reporting for non-bdd tests + return self.tests.append( { "keyword": "Scenario", @@ -98,21 +102,6 @@ def pytest_runtest_logreport(self, report): } ) - def pytest_collectreport(self, report): - if not report.passed: - if report.failed: - self.append_collect_failure(report) - else: - self.append_collect_skipped(report) - - def pytest_internalerror(self, excrepr): - self.tests.append(dict( - name='internal', - description='pytest', - steps=[], - error_message=dict(error=excrepr, message='internal error'), - )) - def pytest_sessionstart(self): self.suite_start_time = time.time() diff --git a/pytest_bdd/plugin.py b/pytest_bdd/plugin.py index 9ac251e66..d75a8c9b7 100644 --- a/pytest_bdd/plugin.py +++ b/pytest_bdd/plugin.py @@ -15,3 +15,12 @@ def pytest_addhooks(pluginmanager): """Register plugin hooks.""" from pytest_bdd import hooks pluginmanager.addhooks(hooks) + + +@pytest.mark.tryfirst +def pytest_runtest_makereport(item, call, __multicall__): + """Store item in the report object.""" + rep = __multicall__.execute() + if hasattr(item, 'scenario'): + rep.item = item + return rep diff --git a/tests/feature/test_cucumber_json.py b/tests/feature/test_cucumber_json.py index 0c672a586..7f4898db0 100644 --- a/tests/feature/test_cucumber_json.py +++ b/tests/feature/test_cucumber_json.py @@ -6,7 +6,7 @@ def runandparse(testdir, *args): """Run tests in testdir and parse json output.""" resultpath = testdir.tmpdir.join("cucumber.json") - result = testdir.runpytest('--cucumberjson={0}'.format(resultpath), *args) + result = testdir.runpytest('--cucumberjson={0}'.format(resultpath), '-s', *args) jsonobject = json.load(resultpath.open()) return result, jsonobject From 9f6ce33079ed80988c0032cbf1637a3f35b69f00 Mon Sep 17 00:00:00 2001 From: Anatoly Bubenkov Date: Sun, 11 May 2014 13:29:14 +0200 Subject: [PATCH 07/46] add scenario --- pytest_bdd/cucumber_json.py | 2 +- pytest_bdd/plugin.py | 2 +- pytest_bdd/scenario.py | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pytest_bdd/cucumber_json.py b/pytest_bdd/cucumber_json.py index bf9db7f6b..3e048b329 100644 --- a/pytest_bdd/cucumber_json.py +++ b/pytest_bdd/cucumber_json.py @@ -74,7 +74,7 @@ def pytest_runtest_logreport(self, report): classnames = names[:-1] test_id = '.'.join(classnames) try: - scenario = report.item.scenario + scenario = report.item.__scenario__ except AttributeError: # skip reporting for non-bdd tests return diff --git a/pytest_bdd/plugin.py b/pytest_bdd/plugin.py index d75a8c9b7..bbde9b965 100644 --- a/pytest_bdd/plugin.py +++ b/pytest_bdd/plugin.py @@ -21,6 +21,6 @@ def pytest_addhooks(pluginmanager): def pytest_runtest_makereport(item, call, __multicall__): """Store item in the report object.""" rep = __multicall__.execute() - if hasattr(item, 'scenario'): + if hasattr(item, '__scenario__'): rep.item = item return rep diff --git a/pytest_bdd/scenario.py b/pytest_bdd/scenario.py index 33a719e19..db874e731 100644 --- a/pytest_bdd/scenario.py +++ b/pytest_bdd/scenario.py @@ -240,6 +240,7 @@ def decorator(_pytestbdd_function): _scenario.__doc__ = '{feature_name}: {scenario_name}'.format( feature_name=feature_name, scenario_name=scenario_name) + _scenario.__scenario__ = scenario return _scenario return recreate_function(decorator, module=caller_module, firstlineno=caller_function.f_lineno) From bc2c347d95541f0b3fcb283b9f2ce2f6f87b6044 Mon Sep 17 00:00:00 2001 From: Anatoly Bubenkov Date: Sun, 11 May 2014 14:02:43 +0200 Subject: [PATCH 08/46] correct scenario saving --- pytest_bdd/cucumber_json.py | 2 +- pytest_bdd/plugin.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pytest_bdd/cucumber_json.py b/pytest_bdd/cucumber_json.py index 3e048b329..65e2c71a0 100644 --- a/pytest_bdd/cucumber_json.py +++ b/pytest_bdd/cucumber_json.py @@ -74,7 +74,7 @@ def pytest_runtest_logreport(self, report): classnames = names[:-1] test_id = '.'.join(classnames) try: - scenario = report.item.__scenario__ + scenario = report.item.obj.__scenario__ except AttributeError: # skip reporting for non-bdd tests return diff --git a/pytest_bdd/plugin.py b/pytest_bdd/plugin.py index bbde9b965..3247dd372 100644 --- a/pytest_bdd/plugin.py +++ b/pytest_bdd/plugin.py @@ -21,6 +21,6 @@ def pytest_addhooks(pluginmanager): def pytest_runtest_makereport(item, call, __multicall__): """Store item in the report object.""" rep = __multicall__.execute() - if hasattr(item, '__scenario__'): + if hasattr(item.obj, '__scenario__'): rep.item = item return rep From 5935411a1da84057ea309c80fc6700491cce0060 Mon Sep 17 00:00:00 2001 From: Anatoly Bubenkov Date: Sun, 11 May 2014 14:15:25 +0200 Subject: [PATCH 09/46] scenario attrs --- pytest_bdd/cucumber_json.py | 4 ++-- pytest_bdd/feature.py | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/pytest_bdd/cucumber_json.py b/pytest_bdd/cucumber_json.py index 65e2c71a0..e745171c5 100644 --- a/pytest_bdd/cucumber_json.py +++ b/pytest_bdd/cucumber_json.py @@ -57,7 +57,7 @@ def __init__(self, logfile): def append(self, obj): self.tests[-1].append(obj) - def _get_result(report): + def _get_result(self, report): """Get scenario test run result.""" if report.passed: if report.when == 'call': # ignore setup/teardown @@ -84,7 +84,7 @@ def pytest_runtest_logreport(self, report): "id": test_id, "name": scenario.name, "line": scenario.line_number, - "description": scenario.description, + "description": '', "tags": [], "type": "scenario", "time": getattr(report, 'duration', 0), diff --git a/pytest_bdd/feature.py b/pytest_bdd/feature.py index ce0faffe8..54f5f90cc 100644 --- a/pytest_bdd/feature.py +++ b/pytest_bdd/feature.py @@ -190,7 +190,7 @@ def __init__(self, filename, encoding='utf-8'): # Remove Feature, Given, When, Then, And clean_line = remove_prefix(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: @@ -232,7 +232,7 @@ def get_feature(cls, filename, encoding='utf-8'): class Scenario(object): """Scenario.""" - def __init__(self, feature, name, example_converters=None): + def __init__(self, feature, name, line_number, example_converters=None): self.feature = feature self.name = name self.params = set() @@ -240,6 +240,7 @@ def __init__(self, feature, name, example_converters=None): 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): From 375729b2a767b3cd94f24477eae7d89961dbdfa5 Mon Sep 17 00:00:00 2001 From: albertjan Date: Sun, 11 May 2014 14:25:40 +0200 Subject: [PATCH 10/46] use tests var --- pytest_bdd/cucumber_json.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytest_bdd/cucumber_json.py b/pytest_bdd/cucumber_json.py index e745171c5..46b4e6c5e 100644 --- a/pytest_bdd/cucumber_json.py +++ b/pytest_bdd/cucumber_json.py @@ -111,7 +111,7 @@ def pytest_sessionfinish(self): else: logfile = open(self.logfile, 'w', encoding='utf-8') - logfile.write(json.dumps(self.features)) + logfile.write(json.dumps(self.tests)) logfile.close() def pytest_terminal_summary(self, terminalreporter): From fe5611460b4ca9b51bcec28d4b02bc93c77c3fa3 Mon Sep 17 00:00:00 2001 From: albertjan Date: Sun, 11 May 2014 14:39:54 +0200 Subject: [PATCH 11/46] Just get the status --- pytest_bdd/cucumber_json.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pytest_bdd/cucumber_json.py b/pytest_bdd/cucumber_json.py index 46b4e6c5e..225883451 100644 --- a/pytest_bdd/cucumber_json.py +++ b/pytest_bdd/cucumber_json.py @@ -78,6 +78,9 @@ def pytest_runtest_logreport(self, report): except AttributeError: # skip reporting for non-bdd tests return + + result = self._get_result(report) or {} + self.tests.append( { "keyword": "Scenario", @@ -96,7 +99,7 @@ def pytest_runtest_logreport(self, report): "match": { "location": "features/step_definitions/steps.rb:5" }, - "result": self._get_result(report) + "result": result.get("status", None) } ] } From 870c69f6d8c0f0b9dbe60a053e839ae25283f8ba Mon Sep 17 00:00:00 2001 From: albertjan Date: Sun, 11 May 2014 14:58:19 +0200 Subject: [PATCH 12/46] map each step --- pytest_bdd/cucumber_json.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/pytest_bdd/cucumber_json.py b/pytest_bdd/cucumber_json.py index 225883451..f1e851037 100644 --- a/pytest_bdd/cucumber_json.py +++ b/pytest_bdd/cucumber_json.py @@ -81,6 +81,19 @@ def pytest_runtest_logreport(self, report): result = self._get_result(report) or {} + def stepMap(step): + return { + "keyword": step.type, + "name": step._name, + "line": step.line_number, + "match": { + "location": "features/step_definitions/steps.rb:5" + }, + "result": result.get("status", None) + } + + steps = map(stepMap, scenario.steps) + self.tests.append( { "keyword": "Scenario", @@ -91,17 +104,7 @@ def pytest_runtest_logreport(self, report): "tags": [], "type": "scenario", "time": getattr(report, 'duration', 0), - "steps": [ - { - "keyword": "Given ", - "name": "a failing step", - "line": 10, - "match": { - "location": "features/step_definitions/steps.rb:5" - }, - "result": result.get("status", None) - } - ] + "steps": steps } ) From 70472f3117ac1f440ea34eeb9988733cc5b186c5 Mon Sep 17 00:00:00 2001 From: albertjan Date: Sun, 11 May 2014 15:08:39 +0200 Subject: [PATCH 13/46] cast reprcrash to string skip if there are no steps --- pytest_bdd/cucumber_json.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pytest_bdd/cucumber_json.py b/pytest_bdd/cucumber_json.py index f1e851037..822176bbc 100644 --- a/pytest_bdd/cucumber_json.py +++ b/pytest_bdd/cucumber_json.py @@ -65,7 +65,7 @@ def _get_result(self, report): elif report.failed: return { 'status': 'failed', - 'error_message': report.longrepr} + 'error_message': str(report.longrepr.reprcrash) } elif report.skipped: return {'status': 'skipped'} @@ -79,7 +79,9 @@ def pytest_runtest_logreport(self, report): # skip reporting for non-bdd tests return - result = self._get_result(report) or {} + if not(scenario.steps): + #skip if there are no steps + return def stepMap(step): return { @@ -89,7 +91,7 @@ def stepMap(step): "match": { "location": "features/step_definitions/steps.rb:5" }, - "result": result.get("status", None) + "result": self._get_result(report) } steps = map(stepMap, scenario.steps) From 127cba2d4e60f11edb12442aaff486a5c16b139e Mon Sep 17 00:00:00 2001 From: Anatoly Bubenkov Date: Sun, 11 May 2014 15:26:10 +0200 Subject: [PATCH 14/46] equals_any added --- tests/feature/test_cucumber_json.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/feature/test_cucumber_json.py b/tests/feature/test_cucumber_json.py index 03d1648bf..63dfbccc8 100644 --- a/tests/feature/test_cucumber_json.py +++ b/tests/feature/test_cucumber_json.py @@ -2,6 +2,8 @@ import json import textwrap +import pytest + def runandparse(testdir, *args): """Run tests in testdir and parse json output.""" @@ -11,6 +13,20 @@ def runandparse(testdir, *args): return result, jsonobject +@pytest.fixture(scope='session') +def equals_any(): + """Helper object comparison to which is always 'equal'.""" + class equals_any(object): + + def __eq__(self, other): + return True + + def __cmp__(self, other): + return 0 + + return equals_any() + + def test_step_trace(testdir): """Test step trace.""" testdir.makefile('.feature', test=textwrap.dedent(""" From a9ca6db0e3bce795a23ab5187c56ace5971a5d0c Mon Sep 17 00:00:00 2001 From: Anatoly Bubenkov Date: Sun, 11 May 2014 15:36:40 +0200 Subject: [PATCH 15/46] add feature line number --- pytest_bdd/feature.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pytest_bdd/feature.py b/pytest_bdd/feature.py index 54f5f90cc..b2dd05fab 100644 --- a/pytest_bdd/feature.py +++ b/pytest_bdd/feature.py @@ -140,6 +140,7 @@ def __init__(self, filename, encoding='utf-8'): self.scenarios = {} self.filename = filename + self.line_number = 1 scenario = None mode = None prev_mode = None @@ -182,6 +183,7 @@ def __init__(self, filename, encoding='utf-8'): if mode == types.FEATURE: if prev_mode != types.FEATURE: self.name = remove_prefix(clean_line) + self.line_number = line_number else: description.append(clean_line) From 1f62168f1f23b920a716b15dc48ac0a716d74a42 Mon Sep 17 00:00:00 2001 From: albertjan Date: Sun, 11 May 2014 15:37:18 +0200 Subject: [PATCH 16/46] add feature hash --- pytest_bdd/cucumber_json.py | 22 +++++++++++++++------- tests/feature/test_cucumber_json.py | 7 +------ 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/pytest_bdd/cucumber_json.py b/pytest_bdd/cucumber_json.py index 822176bbc..44bc36c0a 100644 --- a/pytest_bdd/cucumber_json.py +++ b/pytest_bdd/cucumber_json.py @@ -42,8 +42,8 @@ class LogBDDCucumberJSON(object): def __init__(self, logfile): logfile = os.path.expanduser(os.path.expandvars(logfile)) self.logfile = os.path.normpath(os.path.abspath(logfile)) - self.tests = [] - self.features = [] + self.tests = {} + self.features = {} # def _write_captured_output(self, report): # for capname in ('out', 'err'): @@ -96,8 +96,17 @@ def stepMap(step): steps = map(stepMap, scenario.steps) - self.tests.append( - { + if (not(self.tests.has_key(scenario.feature.filename))): + self.tests[scenario.feature.filename] = { + "keyword": "Feature", + "name": scenario.feature.name, + "id": scenario.feature.name.lower().replace(' ', '-'), + "line": scenario.feature.line_number, + "tags": [], + "elements": [] + } + + self.tests[scenario.feature.filename].elements.add({ "keyword": "Scenario", "id": test_id, "name": scenario.name, @@ -105,10 +114,9 @@ def stepMap(step): "description": '', "tags": [], "type": "scenario", - "time": getattr(report, 'duration', 0), "steps": steps - } - ) + }) + def pytest_sessionstart(self): self.suite_start_time = time.time() diff --git a/tests/feature/test_cucumber_json.py b/tests/feature/test_cucumber_json.py index 63dfbccc8..d110a09d4 100644 --- a/tests/feature/test_cucumber_json.py +++ b/tests/feature/test_cucumber_json.py @@ -125,12 +125,7 @@ def test_failing(): "keyword": "Feature", "line": 2, "name": "One passing scenario, one failing scenario", - "tags": [ - { - "line": 1, - "name": "@a" - } - ], + "tags": [], "uri": "features/one_passing_one_failing.feature" } ] From 052e74e37a0c150366837a772791c6f9d0cf59ca Mon Sep 17 00:00:00 2001 From: albertjan Date: Sun, 11 May 2014 15:39:26 +0200 Subject: [PATCH 17/46] adds description --- pytest_bdd/cucumber_json.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pytest_bdd/cucumber_json.py b/pytest_bdd/cucumber_json.py index 44bc36c0a..e84397e7a 100644 --- a/pytest_bdd/cucumber_json.py +++ b/pytest_bdd/cucumber_json.py @@ -102,6 +102,7 @@ def stepMap(step): "name": scenario.feature.name, "id": scenario.feature.name.lower().replace(' ', '-'), "line": scenario.feature.line_number, + "description": scenario.feature.description, "tags": [], "elements": [] } From c2dd06f08f037deb337db647b872a48e7636c24a Mon Sep 17 00:00:00 2001 From: albertjan Date: Sun, 11 May 2014 15:41:05 +0200 Subject: [PATCH 18/46] rename tests to features --- pytest_bdd/cucumber_json.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/pytest_bdd/cucumber_json.py b/pytest_bdd/cucumber_json.py index e84397e7a..5c83d6931 100644 --- a/pytest_bdd/cucumber_json.py +++ b/pytest_bdd/cucumber_json.py @@ -42,7 +42,6 @@ class LogBDDCucumberJSON(object): def __init__(self, logfile): logfile = os.path.expanduser(os.path.expandvars(logfile)) self.logfile = os.path.normpath(os.path.abspath(logfile)) - self.tests = {} self.features = {} # def _write_captured_output(self, report): @@ -55,7 +54,7 @@ def __init__(self, logfile): # self.append(tag(bin_xml_escape(allcontent))) def append(self, obj): - self.tests[-1].append(obj) + self.features[-1].append(obj) def _get_result(self, report): """Get scenario test run result.""" @@ -96,8 +95,8 @@ def stepMap(step): steps = map(stepMap, scenario.steps) - if (not(self.tests.has_key(scenario.feature.filename))): - self.tests[scenario.feature.filename] = { + if (not(self.features.has_key(scenario.feature.filename))): + self.features[scenario.feature.filename] = { "keyword": "Feature", "name": scenario.feature.name, "id": scenario.feature.name.lower().replace(' ', '-'), @@ -107,7 +106,7 @@ def stepMap(step): "elements": [] } - self.tests[scenario.feature.filename].elements.add({ + self.features[scenario.feature.filename].elements.add({ "keyword": "Scenario", "id": test_id, "name": scenario.name, @@ -128,7 +127,7 @@ def pytest_sessionfinish(self): else: logfile = open(self.logfile, 'w', encoding='utf-8') - logfile.write(json.dumps(self.tests)) + logfile.write(json.dumps(self.features.values())) logfile.close() def pytest_terminal_summary(self, terminalreporter): From aa81fea6c0ad211acc1a4481a5ecf3bd7ebbc93c Mon Sep 17 00:00:00 2001 From: Anatoly Bubenkov Date: Sun, 11 May 2014 15:52:18 +0200 Subject: [PATCH 19/46] correct expectation --- tests/feature/test_cucumber_json.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/feature/test_cucumber_json.py b/tests/feature/test_cucumber_json.py index 63dfbccc8..1f409c623 100644 --- a/tests/feature/test_cucumber_json.py +++ b/tests/feature/test_cucumber_json.py @@ -66,7 +66,7 @@ def test_failing(): "elements": [ { "description": "", - "id": "one-passing-scenario,-one-failing-scenario;passing", + "id": "test_passing", "keyword": "Scenario", "line": 5, "name": "Passing", @@ -93,7 +93,7 @@ def test_failing(): }, { "description": "", - "id": "one-passing-scenario,-one-failing-scenario;failing", + "id": "test_failing", "keyword": "Scenario", "line": 9, "name": "Failing", From fe499437bbcab37a0bef95a7c4501fcba0189eff Mon Sep 17 00:00:00 2001 From: albertjan Date: Sun, 11 May 2014 15:52:34 +0200 Subject: [PATCH 20/46] change name of scenario --- pytest_bdd/cucumber_json.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pytest_bdd/cucumber_json.py b/pytest_bdd/cucumber_json.py index 5c83d6931..d5ee3a51e 100644 --- a/pytest_bdd/cucumber_json.py +++ b/pytest_bdd/cucumber_json.py @@ -106,9 +106,9 @@ def stepMap(step): "elements": [] } - self.features[scenario.feature.filename].elements.add({ + self.features[scenario.feature.filename]['elements'].append({ "keyword": "Scenario", - "id": test_id, + "id": report.item.name, "name": scenario.name, "line": scenario.line_number, "description": '', From 504d7228648b70306c302f424ba1c378c7522962 Mon Sep 17 00:00:00 2001 From: albertjan Date: Sun, 11 May 2014 16:05:13 +0200 Subject: [PATCH 21/46] makes the keyword nice --- pytest_bdd/cucumber_json.py | 9 ++++++--- tests/feature/test_cucumber_json.py | 7 +------ 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/pytest_bdd/cucumber_json.py b/pytest_bdd/cucumber_json.py index d5ee3a51e..57e19827e 100644 --- a/pytest_bdd/cucumber_json.py +++ b/pytest_bdd/cucumber_json.py @@ -78,13 +78,16 @@ def pytest_runtest_logreport(self, report): # skip reporting for non-bdd tests return - if not(scenario.steps): + if not scenario.steps: #skip if there are no steps return + # if self._get_result(report) is not None: + # return + def stepMap(step): return { - "keyword": step.type, + "keyword": step.type.capitalize(), "name": step._name, "line": step.line_number, "match": { @@ -93,7 +96,7 @@ def stepMap(step): "result": self._get_result(report) } - steps = map(stepMap, scenario.steps) + steps = [stepMap(step) for step in scenario.steps] if (not(self.features.has_key(scenario.feature.filename))): self.features[scenario.feature.filename] = { diff --git a/tests/feature/test_cucumber_json.py b/tests/feature/test_cucumber_json.py index 3479201dd..046ea3006 100644 --- a/tests/feature/test_cucumber_json.py +++ b/tests/feature/test_cucumber_json.py @@ -83,12 +83,7 @@ def test_failing(): } } ], - "tags": [ - { - "line": 4, - "name": "@b" - } - ], + "tags": [], "type": "scenario" }, { From 164ac9c58629fae52af1f4227b4cea8cc0fd87a7 Mon Sep 17 00:00:00 2001 From: Anatoly Bubenkov Date: Sun, 11 May 2014 16:08:17 +0200 Subject: [PATCH 22/46] correct expectation --- pytest_bdd/cucumber_json.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pytest_bdd/cucumber_json.py b/pytest_bdd/cucumber_json.py index 57e19827e..34d7e9a77 100644 --- a/pytest_bdd/cucumber_json.py +++ b/pytest_bdd/cucumber_json.py @@ -82,8 +82,8 @@ def pytest_runtest_logreport(self, report): #skip if there are no steps return - # if self._get_result(report) is not None: - # return + if self._get_result(report) is None: + return def stepMap(step): return { From faa5ececc194bdd7496b103ac0c83aa2011bba3e Mon Sep 17 00:00:00 2001 From: albertjan Date: Sun, 11 May 2014 16:15:11 +0200 Subject: [PATCH 23/46] corrects the reference --- pytest_bdd/cucumber_json.py | 7 ++++--- tests/feature/test_cucumber_json.py | 26 ++++++++++---------------- 2 files changed, 14 insertions(+), 19 deletions(-) diff --git a/pytest_bdd/cucumber_json.py b/pytest_bdd/cucumber_json.py index 57e19827e..b4cf242eb 100644 --- a/pytest_bdd/cucumber_json.py +++ b/pytest_bdd/cucumber_json.py @@ -82,8 +82,8 @@ def pytest_runtest_logreport(self, report): #skip if there are no steps return - # if self._get_result(report) is not None: - # return + if self._get_result(report) is None: + return def stepMap(step): return { @@ -91,7 +91,7 @@ def stepMap(step): "name": step._name, "line": step.line_number, "match": { - "location": "features/step_definitions/steps.rb:5" + "location": "" }, "result": self._get_result(report) } @@ -101,6 +101,7 @@ def stepMap(step): if (not(self.features.has_key(scenario.feature.filename))): self.features[scenario.feature.filename] = { "keyword": "Feature", + "uri": scenario.feature.filename, "name": scenario.feature.name, "id": scenario.feature.name.lower().replace(' ', '-'), "line": scenario.feature.line_number, diff --git a/tests/feature/test_cucumber_json.py b/tests/feature/test_cucumber_json.py index 046ea3006..1661c5606 100644 --- a/tests/feature/test_cucumber_json.py +++ b/tests/feature/test_cucumber_json.py @@ -27,7 +27,7 @@ def __cmp__(self, other): return equals_any() -def test_step_trace(testdir): +def test_step_trace(testdir, equals_any): """Test step trace.""" testdir.makefile('.feature', test=textwrap.dedent(""" Feature: One passing scenario, one failing scenario @@ -72,10 +72,10 @@ def test_failing(): "name": "Passing", "steps": [ { - "keyword": "Given ", + "keyword": "Given", "line": 6, "match": { - "location": "features/step_definitions/steps.rb:1" + "location": "" }, "name": "a passing step", "result": { @@ -90,35 +90,29 @@ def test_failing(): "description": "", "id": "test_failing", "keyword": "Scenario", - "line": 9, + "line": 6, "name": "Failing", "steps": [ { - "keyword": "Given ", - "line": 10, + "keyword": "Given", + "line": 7, "match": { - "location": "features/step_definitions/steps.rb:5" + "location": "" }, "name": "a failing step", "result": { - "error_message": " (RuntimeError)\n./features/step_definitions/steps.rb:6:in /a " - "failing step/'\nfeatures/one_passing_one_failing.feature:10:in Given a failing step'", + "error_message": equals_any, "status": "failed" } } ], - "tags": [ - { - "line": 8, - "name": "@c" - } - ], + "tags": [], "type": "scenario" } ], "id": "one-passing-scenario,-one-failing-scenario", "keyword": "Feature", - "line": 2, + "line": 1, "name": "One passing scenario, one failing scenario", "tags": [], "uri": "features/one_passing_one_failing.feature" From 9c196534f5a0b463616e4bf9ba56cfe61b751066 Mon Sep 17 00:00:00 2001 From: albertjan Date: Sun, 11 May 2014 16:19:32 +0200 Subject: [PATCH 24/46] corrects the reference --- tests/feature/test_cucumber_json.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/feature/test_cucumber_json.py b/tests/feature/test_cucumber_json.py index 1661c5606..5d3cd67d3 100644 --- a/tests/feature/test_cucumber_json.py +++ b/tests/feature/test_cucumber_json.py @@ -68,12 +68,12 @@ def test_failing(): "description": "", "id": "test_passing", "keyword": "Scenario", - "line": 5, + "line": 3, "name": "Passing", "steps": [ { "keyword": "Given", - "line": 6, + "line": 4, "match": { "location": "" }, From 18ae4c981b8b3a3c0da96caca8168cda6eae93a7 Mon Sep 17 00:00:00 2001 From: albertjan Date: Sun, 11 May 2014 16:28:23 +0200 Subject: [PATCH 25/46] cleans some stuff up --- pytest_bdd/cucumber_json.py | 54 +++++++++++++++---------------------- 1 file changed, 22 insertions(+), 32 deletions(-) diff --git a/pytest_bdd/cucumber_json.py b/pytest_bdd/cucumber_json.py index b4cf242eb..2053b8767 100644 --- a/pytest_bdd/cucumber_json.py +++ b/pytest_bdd/cucumber_json.py @@ -30,12 +30,6 @@ def pytest_unconfigure(config): config.pluginmanager.unregister(xml) -def mangle_testnames(names): - names = [x.replace('.py', '') for x in names if x != '()'] - names[0] = names[0].replace('/', '.') - return names - - class LogBDDCucumberJSON(object): """Log plugin for cucumber like json output.""" @@ -69,9 +63,6 @@ def _get_result(self, report): return {'status': 'skipped'} def pytest_runtest_logreport(self, report): - names = mangle_testnames(report.nodeid.split('::')) - classnames = names[:-1] - test_id = '.'.join(classnames) try: scenario = report.item.obj.__scenario__ except AttributeError: @@ -79,26 +70,25 @@ def pytest_runtest_logreport(self, report): return if not scenario.steps: - #skip if there are no steps + # skip if there are no steps return if self._get_result(report) is None: + # skip if there isn't a result return - def stepMap(step): + def stepmap(step): return { - "keyword": step.type.capitalize(), - "name": step._name, - "line": step.line_number, - "match": { - "location": "" - }, - "result": self._get_result(report) - } - - steps = [stepMap(step) for step in scenario.steps] - - if (not(self.features.has_key(scenario.feature.filename))): + "keyword": step.type.capitalize(), + "name": step._name, + "line": step.line_number, + "match": { + "location": "" + }, + "result": self._get_result(report) + } + + if not self.features.has_key(scenario.feature.filename): self.features[scenario.feature.filename] = { "keyword": "Feature", "uri": scenario.feature.filename, @@ -111,15 +101,15 @@ def stepMap(step): } 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": steps - }) + "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): From 3d378633557ae5afcbb150f47fbfcfe2a85a10d6 Mon Sep 17 00:00:00 2001 From: Anatoly Bubenkov Date: Sun, 11 May 2014 16:29:27 +0200 Subject: [PATCH 26/46] store base dir and filename separately --- pytest_bdd/feature.py | 13 ++++++++----- pytest_bdd/scenario.py | 4 +--- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/pytest_bdd/feature.py b/pytest_bdd/feature.py index b2dd05fab..c855921cb 100644 --- a/pytest_bdd/feature.py +++ b/pytest_bdd/feature.py @@ -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 @@ -131,15 +133,15 @@ 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 scenario = None mode = None @@ -212,9 +214,10 @@ def __init__(self, filename, encoding='utf-8'): 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. @@ -226,7 +229,7 @@ def get_feature(cls, filename, encoding='utf-8'): """ feature = features.get(filename) if not feature: - feature = Feature(filename, encoding=encoding) + feature = Feature(base_path, filename, encoding=encoding) features[filename] = feature return feature diff --git a/pytest_bdd/scenario.py b/pytest_bdd/scenario.py index db874e731..10f501938 100644 --- a/pytest_bdd/scenario.py +++ b/pytest_bdd/scenario.py @@ -17,7 +17,6 @@ import imp import inspect # pragma: no cover -from os import path as op # pragma: no cover import pytest @@ -256,8 +255,7 @@ def scenario( # Get the feature base_path = get_fixture(caller_module, 'pytestbdd_feature_base_dir') - feature_path = op.abspath(op.join(base_path, feature_name)) - feature = Feature.get_feature(feature_path, encoding=encoding) + feature = Feature.get_feature(base_path, feature_name, encoding=encoding) # Get the scenario try: From be87dffccba3d4b1a5a5d50edc95dd52d5a7880d Mon Sep 17 00:00:00 2001 From: Anatoly Bubenkov Date: Sun, 11 May 2014 16:36:17 +0200 Subject: [PATCH 27/46] fix failing test --- pytest_bdd/feature.py | 5 +++-- tests/feature/test_cucumber_json.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/pytest_bdd/feature.py b/pytest_bdd/feature.py index c855921cb..2469bb4df 100644 --- a/pytest_bdd/feature.py +++ b/pytest_bdd/feature.py @@ -227,10 +227,11 @@ def get_feature(cls, base_path, 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(base_path, filename, encoding=encoding) - features[filename] = feature + features[full_name] = feature return feature diff --git a/tests/feature/test_cucumber_json.py b/tests/feature/test_cucumber_json.py index 5d3cd67d3..c9a8c89db 100644 --- a/tests/feature/test_cucumber_json.py +++ b/tests/feature/test_cucumber_json.py @@ -115,6 +115,6 @@ def test_failing(): "line": 1, "name": "One passing scenario, one failing scenario", "tags": [], - "uri": "features/one_passing_one_failing.feature" + "uri": testdir.tmpdir.join('test.feature').strpath, } ] From 4ed618ee96b2915e639b15e6eb383e293c99ee01 Mon Sep 17 00:00:00 2001 From: albertjan Date: Sun, 11 May 2014 16:40:44 +0200 Subject: [PATCH 28/46] clean up and use rel path --- pytest_bdd/cucumber_json.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/pytest_bdd/cucumber_json.py b/pytest_bdd/cucumber_json.py index 2053b8767..14a04f7c7 100644 --- a/pytest_bdd/cucumber_json.py +++ b/pytest_bdd/cucumber_json.py @@ -38,15 +38,6 @@ def __init__(self, logfile): self.logfile = os.path.normpath(os.path.abspath(logfile)) self.features = {} - # def _write_captured_output(self, report): - # for capname in ('out', 'err'): - # allcontent = '' - # for name, content in report.get_sections('Captured std%s' % capname): - # allcontent += content - # if allcontent: - # tag = getattr(Junit, 'system-'+capname) - # self.append(tag(bin_xml_escape(allcontent))) - def append(self, obj): self.features[-1].append(obj) @@ -91,7 +82,7 @@ def stepmap(step): if not self.features.has_key(scenario.feature.filename): self.features[scenario.feature.filename] = { "keyword": "Feature", - "uri": scenario.feature.filename, + "uri": scenario.feature.rel_filename, "name": scenario.feature.name, "id": scenario.feature.name.lower().replace(' ', '-'), "line": scenario.feature.line_number, From d740515a9f4bd4d16834f337196514ebe0c3a7ce Mon Sep 17 00:00:00 2001 From: albertjan Date: Sun, 11 May 2014 16:43:45 +0200 Subject: [PATCH 29/46] clean up and use rel path --- tests/feature/test_cucumber_json.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/feature/test_cucumber_json.py b/tests/feature/test_cucumber_json.py index c9a8c89db..41f23dfa1 100644 --- a/tests/feature/test_cucumber_json.py +++ b/tests/feature/test_cucumber_json.py @@ -1,5 +1,6 @@ """Test cucumber json output.""" import json +import os.path import textwrap import pytest @@ -115,6 +116,6 @@ def test_failing(): "line": 1, "name": "One passing scenario, one failing scenario", "tags": [], - "uri": testdir.tmpdir.join('test.feature').strpath, + "uri": os.path.join(testdir.tmpdir.basename, 'test.feature'), } ] From 62898e5a00a67de592f2bbde3e96821fd544b285 Mon Sep 17 00:00:00 2001 From: Anatoly Bubenkov Date: Sun, 11 May 2014 16:47:37 +0200 Subject: [PATCH 30/46] PR --- pytest_bdd/cucumber_json.py | 4 ++-- tests/feature/test_cucumber_json.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/pytest_bdd/cucumber_json.py b/pytest_bdd/cucumber_json.py index 2053b8767..401619b99 100644 --- a/pytest_bdd/cucumber_json.py +++ b/pytest_bdd/cucumber_json.py @@ -25,13 +25,13 @@ def pytest_configure(config): def pytest_unconfigure(config): xml = getattr(config, '_bddcucumberjson', None) - if xml: + if xml is not None: del config._bddcucumberjson config.pluginmanager.unregister(xml) class LogBDDCucumberJSON(object): - """Log plugin for cucumber like json output.""" + """Logging plugin for cucumber like json output.""" def __init__(self, logfile): logfile = os.path.expanduser(os.path.expandvars(logfile)) diff --git a/tests/feature/test_cucumber_json.py b/tests/feature/test_cucumber_json.py index c9a8c89db..cea82c0f3 100644 --- a/tests/feature/test_cucumber_json.py +++ b/tests/feature/test_cucumber_json.py @@ -1,6 +1,7 @@ """Test cucumber json output.""" import json import textwrap +import os.path import pytest @@ -115,6 +116,6 @@ def test_failing(): "line": 1, "name": "One passing scenario, one failing scenario", "tags": [], - "uri": testdir.tmpdir.join('test.feature').strpath, + "uri": os.path.join(testdir.tmpdir.basename, 'test.feature'), } ] From fce9aabbd0e1bf1f1f70297a5bf3eaabeebac4ab Mon Sep 17 00:00:00 2001 From: albertjan Date: Sun, 11 May 2014 16:51:12 +0200 Subject: [PATCH 31/46] changes according to @dimazest --- pytest_bdd/cucumber_json.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pytest_bdd/cucumber_json.py b/pytest_bdd/cucumber_json.py index 14a04f7c7..5eeb6017a 100644 --- a/pytest_bdd/cucumber_json.py +++ b/pytest_bdd/cucumber_json.py @@ -49,7 +49,8 @@ def _get_result(self, report): elif report.failed: return { 'status': 'failed', - 'error_message': str(report.longrepr.reprcrash) } + 'error_message': str(report.longrepr.reprcrash) + } elif report.skipped: return {'status': 'skipped'} @@ -76,7 +77,7 @@ def stepmap(step): "match": { "location": "" }, - "result": self._get_result(report) + "result": self._get_result(report), } if not self.features.has_key(scenario.feature.filename): @@ -88,7 +89,7 @@ def stepmap(step): "line": scenario.feature.line_number, "description": scenario.feature.description, "tags": [], - "elements": [] + "elements": [], } self.features[scenario.feature.filename]['elements'].append({ @@ -99,7 +100,7 @@ def stepmap(step): "description": '', "tags": [], "type": "scenario", - "steps": [stepmap(step) for step in scenario.steps] + "steps": [stepmap(step) for step in scenario.steps], }) From 1fe834170ed53e02e1701024ae4dcd16ef96e0a9 Mon Sep 17 00:00:00 2001 From: Anatoly Bubenkov Date: Sun, 11 May 2014 17:00:10 +0200 Subject: [PATCH 32/46] pep fixes --- pytest_bdd/cucumber_json.py | 7 +++---- pytest_bdd/plugin.py | 5 ++++- tox.ini | 2 +- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/pytest_bdd/cucumber_json.py b/pytest_bdd/cucumber_json.py index 7d328885d..d8c485d68 100644 --- a/pytest_bdd/cucumber_json.py +++ b/pytest_bdd/cucumber_json.py @@ -49,7 +49,7 @@ def _get_result(self, report): elif report.failed: return { 'status': 'failed', - 'error_message': str(report.longrepr.reprcrash) } + 'error_message': str(report.longrepr.reprcrash)} elif report.skipped: return {'status': 'skipped'} @@ -79,7 +79,7 @@ def stepmap(step): "result": self._get_result(report) } - if not self.features.has_key(scenario.feature.filename): + if scenario.feature.filename not in self.features: self.features[scenario.feature.filename] = { "keyword": "Feature", "uri": scenario.feature.rel_filename, @@ -102,7 +102,6 @@ def stepmap(step): "steps": [stepmap(step) for step in scenario.steps] }) - def pytest_sessionstart(self): self.suite_start_time = time.time() @@ -112,7 +111,7 @@ def pytest_sessionfinish(self): else: logfile = open(self.logfile, 'w', encoding='utf-8') - logfile.write(json.dumps(self.features.values())) + logfile.write(json.dumps(list(self.features.values()))) logfile.close() def pytest_terminal_summary(self, terminalreporter): diff --git a/pytest_bdd/plugin.py b/pytest_bdd/plugin.py index 3247dd372..fab72ac10 100644 --- a/pytest_bdd/plugin.py +++ b/pytest_bdd/plugin.py @@ -21,6 +21,9 @@ def pytest_addhooks(pluginmanager): def pytest_runtest_makereport(item, call, __multicall__): """Store item in the report object.""" rep = __multicall__.execute() - if hasattr(item.obj, '__scenario__'): + try: + item.obj.__scenario__ rep.item = item + except AttributeError: + pass return rep diff --git a/tox.ini b/tox.ini index 72b05221a..9cf0c8b87 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] distshare={homedir}/.tox/distshare -envlist=py26,py27,py27-xdist,py27-pytest-latest,py33 +envlist=py26,py27,py27-xdist,py27-pytest-latest,py34 indexserver= pypi = https://pypi.python.org/simple From 3f5da0f4a0a5fd8d3946ae8c1ea17af09c1a4a0f Mon Sep 17 00:00:00 2001 From: Anatoly Bubenkov Date: Sun, 11 May 2014 17:02:11 +0200 Subject: [PATCH 33/46] pep fixes --- pytest_bdd/cucumber_json.py | 3 +-- tests/feature/test_cucumber_json.py | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/pytest_bdd/cucumber_json.py b/pytest_bdd/cucumber_json.py index a5966c688..6e8035684 100644 --- a/pytest_bdd/cucumber_json.py +++ b/pytest_bdd/cucumber_json.py @@ -1,9 +1,8 @@ """Cucumber json output formatter.""" +import json import os import time -import json - import py diff --git a/tests/feature/test_cucumber_json.py b/tests/feature/test_cucumber_json.py index 014db18ce..41f23dfa1 100644 --- a/tests/feature/test_cucumber_json.py +++ b/tests/feature/test_cucumber_json.py @@ -2,7 +2,6 @@ import json import os.path import textwrap -import os.path import pytest From 56d2782b5175deeddb502c5b20f673078f6b9ae8 Mon Sep 17 00:00:00 2001 From: Anatoly Bubenkov Date: Sun, 11 May 2014 17:04:41 +0200 Subject: [PATCH 34/46] pep fixes --- pytest_bdd/cucumber_json.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/pytest_bdd/cucumber_json.py b/pytest_bdd/cucumber_json.py index 6e8035684..d733477d9 100644 --- a/pytest_bdd/cucumber_json.py +++ b/pytest_bdd/cucumber_json.py @@ -107,12 +107,11 @@ def pytest_sessionstart(self): def pytest_sessionfinish(self): if py.std.sys.version_info[0] < 3: - logfile = py.std.codecs.open(self.logfile, 'w', encoding='utf-8') + logfile_open = py.std.codecs.open else: - logfile = open(self.logfile, 'w', encoding='utf-8') - - logfile.write(json.dumps(list(self.features.values()))) - logfile.close() + 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)) From 79b40fb153da9c2561d8847129000cb9a79a3219 Mon Sep 17 00:00:00 2001 From: Anatoly Bubenkov Date: Sun, 11 May 2014 17:08:56 +0200 Subject: [PATCH 35/46] pep fixes --- pytest_bdd/cucumber_json.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/pytest_bdd/cucumber_json.py b/pytest_bdd/cucumber_json.py index d733477d9..49c9f5ddc 100644 --- a/pytest_bdd/cucumber_json.py +++ b/pytest_bdd/cucumber_json.py @@ -60,12 +60,8 @@ def pytest_runtest_logreport(self, report): # skip reporting for non-bdd tests return - if not scenario.steps: - # skip if there are no steps - return - - if self._get_result(report) is None: - # skip if there isn't a result + if not scenario.steps or self._get_result(report) is None: + # skip if there isn't a result or scenario has no steps return def stepmap(step): From 2c6c35f9e85c0b10ec7d039d392f35b22354edee Mon Sep 17 00:00:00 2001 From: Anatoly Bubenkov Date: Sun, 11 May 2014 17:21:53 +0200 Subject: [PATCH 36/46] added docs --- CHANGES.rst | 6 ++++++ README.rst | 14 ++++++++++++++ setup.py | 2 +- 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 501c608fc..7632ebcea 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,12 @@ Changelog ========= +2.2.0 +----- + +- Implemented cucumber json formatter (bubenkoff, albertjan) + + 2.1.0 ----- diff --git a/README.rst b/README.rst index 9d8467ddb..52aac5408 100644 --- a/README.rst +++ b/README.rst @@ -595,6 +595,20 @@ Tools recommended to use for browser testing: - pytest `splinter `_ 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 `_ +which can be used for `this `_ jenkins +plugin + +To have an output in json format: + +:: + + py.test --cucumberjson= + Migration of your tests from versions 0.x.x-1.x.x ------------------------------------------------- diff --git a/setup.py b/setup.py index 97314af66..f6d32cd35 100755 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ from setuptools.command.test import test as TestCommand -version = '2.1.0' +version = '2.2.0' class Tox(TestCommand): From 8f970bd7b8131b588ba730583c6843b6749a732e Mon Sep 17 00:00:00 2001 From: albertjan Date: Sun, 11 May 2014 18:17:17 +0200 Subject: [PATCH 37/46] comma added --- pytest_bdd/cucumber_json.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytest_bdd/cucumber_json.py b/pytest_bdd/cucumber_json.py index 49c9f5ddc..0f496d28b 100644 --- a/pytest_bdd/cucumber_json.py +++ b/pytest_bdd/cucumber_json.py @@ -48,7 +48,7 @@ def _get_result(self, report): elif report.failed: return { 'status': 'failed', - 'error_message': str(report.longrepr.reprcrash) + 'error_message': str(report.longrepr.reprcrash), } elif report.skipped: return {'status': 'skipped'} From fa66c684ce0ad5e0b0697f18a9b0bf2862166dea Mon Sep 17 00:00:00 2001 From: Anatoly Bubenkov Date: Thu, 24 Jul 2014 17:38:52 +0200 Subject: [PATCH 38/46] add makefile --- Makefile | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 Makefile diff --git a/Makefile b/Makefile new file mode 100644 index 000000000..ea526545a --- /dev/null +++ b/Makefile @@ -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 From 59fc75087c9cd23b0492567d9936dbb6233c7eb6 Mon Sep 17 00:00:00 2001 From: Anatoly Bubenkov Date: Thu, 24 Jul 2014 18:30:05 +0200 Subject: [PATCH 39/46] upgrade py --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 181f027fd..e51caf244 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,7 @@ language: python # command to install dependencies install: - pip install python-coveralls - - pip install -U virtualenv + - pip install -U virtualenv py # # command to run tests script: python setup.py test after_success: From 5d5149801a5e5c494a4ac1cbda96a6b9a991e7d2 Mon Sep 17 00:00:00 2001 From: Anatoly Bubenkov Date: Thu, 24 Jul 2014 19:22:24 +0200 Subject: [PATCH 40/46] serialize scenario info --- pytest_bdd/cucumber_json.py | 35 ++++++++++++++++++----------------- pytest_bdd/feature.py | 5 +++++ pytest_bdd/plugin.py | 24 ++++++++++++++++++++++-- setup.py | 2 +- 4 files changed, 46 insertions(+), 20 deletions(-) diff --git a/pytest_bdd/cucumber_json.py b/pytest_bdd/cucumber_json.py index 0f496d28b..579017fd9 100644 --- a/pytest_bdd/cucumber_json.py +++ b/pytest_bdd/cucumber_json.py @@ -30,6 +30,7 @@ def pytest_unconfigure(config): class LogBDDCucumberJSON(object): + """Logging plugin for cucumber like json output.""" def __init__(self, logfile): @@ -55,47 +56,47 @@ def _get_result(self, report): def pytest_runtest_logreport(self, report): try: - scenario = report.item.obj.__scenario__ + scenario = report.scenario except AttributeError: # skip reporting for non-bdd tests return - if not scenario.steps or self._get_result(report) is None: + if not scenario['steps'] or self._get_result(report) is None: # skip if there isn't a result or scenario has no steps return def stepmap(step): return { - "keyword": step.type.capitalize(), - "name": step._name, - "line": step.line_number, + "keyword": step['type'].capitalize(), + "name": step['name'], + "line": step['line_number'], "match": { "location": "" }, "result": self._get_result(report), } - if scenario.feature.filename not in self.features: - self.features[scenario.feature.filename] = { + if scenario['feature']['filename'] not in self.features: + self.features[scenario['feature']['filename']] = { "keyword": "Feature", - "uri": scenario.feature.rel_filename, - "name": scenario.feature.name, - "id": scenario.feature.name.lower().replace(' ', '-'), - "line": scenario.feature.line_number, - "description": scenario.feature.description, + "uri": scenario['feature']['rel_filename'], + "name": scenario['feature']['name'], + "id": scenario['feature']['name'].lower().replace(' ', '-'), + "line": scenario['feature']['line_number'], + "description": scenario['feature']['description'], "tags": [], "elements": [], } - self.features[scenario.feature.filename]['elements'].append({ + self.features[scenario['feature']['filename']]['elements'].append({ "keyword": "Scenario", - "id": report.item.name, - "name": scenario.name, - "line": scenario.line_number, + "id": report.item['name'], + "name": scenario['name'], + "line": scenario['line_number'], "description": '', "tags": [], "type": "scenario", - "steps": [stepmap(step) for step in scenario.steps], + "steps": [stepmap(step) for step in scenario['steps']], }) def pytest_sessionstart(self): diff --git a/pytest_bdd/feature.py b/pytest_bdd/feature.py index 2469bb4df..a01e91d3a 100644 --- a/pytest_bdd/feature.py +++ b/pytest_bdd/feature.py @@ -34,6 +34,7 @@ class FeatureError(Exception): # pragma: no cover + """Feature parse error.""" message = u'{0}.\nLine number: {1}.\nLine: {2}.' @@ -131,6 +132,7 @@ def force_encode(string, encoding='utf-8'): class Feature(object): + """Feature.""" def __init__(self, basedir, filename, encoding='utf-8'): @@ -143,6 +145,7 @@ def __init__(self, basedir, filename, encoding='utf-8'): 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 @@ -236,6 +239,7 @@ def get_feature(cls, base_path, filename, encoding='utf-8'): class Scenario(object): + """Scenario.""" def __init__(self, feature, name, line_number, example_converters=None): @@ -333,6 +337,7 @@ def validate(self): class Step(object): + """Step.""" def __init__(self, name, type, params, scenario, indent, line_number): diff --git a/pytest_bdd/plugin.py b/pytest_bdd/plugin.py index fab72ac10..a9a38dd47 100644 --- a/pytest_bdd/plugin.py +++ b/pytest_bdd/plugin.py @@ -22,8 +22,28 @@ def pytest_runtest_makereport(item, call, __multicall__): """Store item in the report object.""" rep = __multicall__.execute() try: - item.obj.__scenario__ - rep.item = item + scenario = item.obj.__scenario__ except AttributeError: pass + else: + rep.scenario = { + 'steps': [{ + 'name': step._name, + 'type': step.type, + 'line_number': step.line_number + } for step in scenario.steps], + 'name': scenario.name, + 'line_number': scenario.line_number, + 'feature': { + 'name': scenario.feature.name, + 'filename': scenario.feature.filename, + 'rel_filename': scenario.feature.rel_filename, + 'line_number': scenario.feature.line_number, + 'description': scenario.feature.description + } + } + rep.item = { + 'name': item.name + } + return rep diff --git a/setup.py b/setup.py index cb19874e7..a19624de1 100755 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ class Tox(TestCommand): def finalize_options(self): TestCommand.finalize_options(self) - self.test_args = ['--recreate'] + self.test_args = ['--recreate -vv'] self.test_suite = True def run_tests(self): From a427de339d27c99770da338c90a0374d34ac97cb Mon Sep 17 00:00:00 2001 From: Anatoly Bubenkov Date: Thu, 24 Jul 2014 19:24:51 +0200 Subject: [PATCH 41/46] add verbosity --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 0415760e9..cf47be8f2 100644 --- a/tox.ini +++ b/tox.ini @@ -30,3 +30,4 @@ deps = [pytest] pep8maxlinelength=120 +addopts=-vvl From 9f24385897efb3c58170baf41b3b5b17fed7d065 Mon Sep 17 00:00:00 2001 From: Anatoly Bubenkov Date: Thu, 24 Jul 2014 20:46:00 +0200 Subject: [PATCH 42/46] use filename instead of name which can be empty #ep14boat --- pytest_bdd/cucumber_json.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytest_bdd/cucumber_json.py b/pytest_bdd/cucumber_json.py index 579017fd9..fb09782e8 100644 --- a/pytest_bdd/cucumber_json.py +++ b/pytest_bdd/cucumber_json.py @@ -81,7 +81,7 @@ def stepmap(step): "keyword": "Feature", "uri": scenario['feature']['rel_filename'], "name": scenario['feature']['name'], - "id": scenario['feature']['name'].lower().replace(' ', '-'), + "id": scenario['feature']['rel_filename'].lower().replace(' ', '-'), "line": scenario['feature']['line_number'], "description": scenario['feature']['description'], "tags": [], From 5aaf42e4b5ac1d50bab25764c0f40729eaff0209 Mon Sep 17 00:00:00 2001 From: Anatoly Bubenkov Date: Fri, 25 Jul 2014 22:17:58 +0200 Subject: [PATCH 43/46] better reporting --- pytest_bdd/cucumber_json.py | 4 ++-- pytest_bdd/feature.py | 30 ++++++++++++++++------------- pytest_bdd/plugin.py | 3 ++- pytest_bdd/steps.py | 7 ++++--- tests/feature/test_cucumber_json.py | 23 +++++++++++++++++++--- 5 files changed, 45 insertions(+), 22 deletions(-) diff --git a/pytest_bdd/cucumber_json.py b/pytest_bdd/cucumber_json.py index fb09782e8..97c011606 100644 --- a/pytest_bdd/cucumber_json.py +++ b/pytest_bdd/cucumber_json.py @@ -67,7 +67,7 @@ def pytest_runtest_logreport(self, report): def stepmap(step): return { - "keyword": step['type'].capitalize(), + "keyword": step['keyword'], "name": step['name'], "line": step['line_number'], "match": { @@ -80,7 +80,7 @@ def stepmap(step): self.features[scenario['feature']['filename']] = { "keyword": "Feature", "uri": scenario['feature']['rel_filename'], - "name": scenario['feature']['name'], + "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'], diff --git a/pytest_bdd/feature.py b/pytest_bdd/feature.py index a01e91d3a..2ced1a2f8 100644 --- a/pytest_bdd/feature.py +++ b/pytest_bdd/feature.py @@ -96,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 ('', ''). """ 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): @@ -187,7 +186,7 @@ def __init__(self, basedir, 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) @@ -195,7 +194,7 @@ def __init__(self, basedir, filename, encoding='utf-8'): 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, line_number) elif mode == types.EXAMPLES: @@ -212,7 +211,8 @@ def __init__(self, basedir, 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) @@ -253,17 +253,20 @@ def __init__(self, feature, name, line_number, example_converters=None): 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 @@ -340,8 +343,9 @@ 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 diff --git a/pytest_bdd/plugin.py b/pytest_bdd/plugin.py index a9a38dd47..04d689c89 100644 --- a/pytest_bdd/plugin.py +++ b/pytest_bdd/plugin.py @@ -28,8 +28,9 @@ def pytest_runtest_makereport(item, call, __multicall__): else: rep.scenario = { 'steps': [{ - 'name': step._name, + 'name': step.name, 'type': step.type, + 'keyword': step.keyword, 'line_number': step.line_number } for step in scenario.steps], 'name': scenario.name, diff --git a/pytest_bdd/steps.py b/pytest_bdd/steps.py index a31c7c82b..076b8c26b 100644 --- a/pytest_bdd/steps.py +++ b/pytest_bdd/steps.py @@ -39,13 +39,14 @@ def article(author): import pytest # pragma: no cover -from pytest_bdd.feature import remove_prefix # pragma: no cover +from pytest_bdd.feature import parse_line # pragma: no cover from pytest_bdd.types import GIVEN, WHEN, THEN # pragma: no cover PY3 = sys.version_info[0] >= 3 # pragma: no cover class StepError(Exception): # pragma: no cover + """Step declaration error.""" RE_TYPE = type(re.compile('')) # pragma: no cover @@ -63,7 +64,6 @@ def given(name, fixture=None, converters=None): :note: Can't be used as a decorator when the fixture is specified. """ - if fixture is not None: module = get_caller_module() step_func = lambda request: request.getfuncargvalue(fixture) @@ -73,7 +73,8 @@ def given(name, fixture=None, converters=None): step_func.fixture = fixture func = pytest.fixture(lambda: step_func) func.__doc__ = 'Alias for the "{0}" fixture.'.format(fixture) - contribute_to_module(module, remove_prefix(name), func) + _, name = parse_line(name) + contribute_to_module(module, name, func) return _not_a_fixture_decorator return _step_decorator(GIVEN, name, converters=converters) diff --git a/tests/feature/test_cucumber_json.py b/tests/feature/test_cucumber_json.py index 41f23dfa1..16b4fdd07 100644 --- a/tests/feature/test_cucumber_json.py +++ b/tests/feature/test_cucumber_json.py @@ -35,6 +35,7 @@ def test_step_trace(testdir, equals_any): Scenario: Passing Given a passing step + And some other passing step Scenario: Failing Given a failing step @@ -47,6 +48,10 @@ def test_step_trace(testdir, equals_any): def a_passing_step(): return 'pass' + @given('some other passing step') + def some_other_passing_step(): + return 'pass' + @given('a failing step') def a_failing_step(): raise Exception('Error') @@ -82,7 +87,19 @@ def test_failing(): "result": { "status": "passed" } + }, + { + "keyword": "And", + "line": 5, + "match": { + "location": "" + }, + "name": "some other passing step", + "result": { + "status": "passed" + } } + ], "tags": [], "type": "scenario" @@ -91,12 +108,12 @@ def test_failing(): "description": "", "id": "test_failing", "keyword": "Scenario", - "line": 6, + "line": 7, "name": "Failing", "steps": [ { "keyword": "Given", - "line": 7, + "line": 8, "match": { "location": "" }, @@ -111,7 +128,7 @@ def test_failing(): "type": "scenario" } ], - "id": "one-passing-scenario,-one-failing-scenario", + "id": "test_step_trace0/test.feature", "keyword": "Feature", "line": 1, "name": "One passing scenario, one failing scenario", From b08d3853528505e2d366913f8ecf78ccc4e69812 Mon Sep 17 00:00:00 2001 From: Anatoly Bubenkov Date: Fri, 25 Jul 2014 22:32:18 +0000 Subject: [PATCH 44/46] correct reporting of exceptions --- .gitignore | 1 + pytest_bdd/cucumber_json.py | 20 ++++++++++++-------- pytest_bdd/feature.py | 1 + pytest_bdd/plugin.py | 8 +++++++- 4 files changed, 21 insertions(+), 9 deletions(-) diff --git a/.gitignore b/.gitignore index 2133091df..3eadcb4c6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +*.rej *.py[cod] /.env *.orig diff --git a/pytest_bdd/cucumber_json.py b/pytest_bdd/cucumber_json.py index 97c011606..d15182bae 100644 --- a/pytest_bdd/cucumber_json.py +++ b/pytest_bdd/cucumber_json.py @@ -41,15 +41,19 @@ def __init__(self, logfile): def append(self, obj): self.features[-1].append(obj) - def _get_result(self, report): - """Get scenario test run result.""" - if report.passed: - if report.when == 'call': # ignore setup/teardown + 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': '', ['error_message': '']} + """ + if report.passed or not step['failed']: # ignore setup/teardown return {'status': 'passed'} - elif report.failed: + elif report.failed and step['failed']: return { 'status': 'failed', - 'error_message': str(report.longrepr.reprcrash), + 'error_message': unicode(report.longrepr), } elif report.skipped: return {'status': 'skipped'} @@ -61,7 +65,7 @@ def pytest_runtest_logreport(self, report): # skip reporting for non-bdd tests return - if not scenario['steps'] or self._get_result(report) is None: + if not scenario['steps'] or report.when != 'call': # skip if there isn't a result or scenario has no steps return @@ -73,7 +77,7 @@ def stepmap(step): "match": { "location": "" }, - "result": self._get_result(report), + "result": self._get_result(step, report), } if scenario['feature']['filename'] not in self.features: diff --git a/pytest_bdd/feature.py b/pytest_bdd/feature.py index 2ced1a2f8..9f2e27110 100644 --- a/pytest_bdd/feature.py +++ b/pytest_bdd/feature.py @@ -352,6 +352,7 @@ def __init__(self, name, type, params, scenario, indent, line_number, keyword): 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.""" diff --git a/pytest_bdd/plugin.py b/pytest_bdd/plugin.py index 04d689c89..87293e976 100644 --- a/pytest_bdd/plugin.py +++ b/pytest_bdd/plugin.py @@ -17,6 +17,11 @@ def pytest_addhooks(pluginmanager): pluginmanager.addhooks(hooks) +def pytest_bdd_step_error(request, feature, scenario, step, step_func, step_func_args, exception): + """Mark step as failed for later reporting.""" + step.failed = True + + @pytest.mark.tryfirst def pytest_runtest_makereport(item, call, __multicall__): """Store item in the report object.""" @@ -31,7 +36,8 @@ def pytest_runtest_makereport(item, call, __multicall__): 'name': step.name, 'type': step.type, 'keyword': step.keyword, - 'line_number': step.line_number + 'line_number': step.line_number, + 'failed': step.failed } for step in scenario.steps], 'name': scenario.name, 'line_number': scenario.line_number, From 76be97293f091fac53acabd8089726bc2d39cfe5 Mon Sep 17 00:00:00 2001 From: Anatoly Bubenkov Date: Fri, 25 Jul 2014 22:59:23 +0000 Subject: [PATCH 45/46] unicode fixes --- pytest_bdd/cucumber_json.py | 4 +++- pytest_bdd/feature.py | 16 ++++++++++++---- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/pytest_bdd/cucumber_json.py b/pytest_bdd/cucumber_json.py index d15182bae..88a71fad1 100644 --- a/pytest_bdd/cucumber_json.py +++ b/pytest_bdd/cucumber_json.py @@ -5,6 +5,8 @@ import py +from .feature import force_unicode + def pytest_addoption(parser): group = parser.getgroup('pytest-bdd') @@ -53,7 +55,7 @@ def _get_result(self, step, report): elif report.failed and step['failed']: return { 'status': 'failed', - 'error_message': unicode(report.longrepr), + 'error_message': force_unicode(report.longrepr), } elif report.skipped: return {'status': 'skipped'} diff --git a/pytest_bdd/feature.py b/pytest_bdd/feature.py index 9f2e27110..393d19bc7 100644 --- a/pytest_bdd/feature.py +++ b/pytest_bdd/feature.py @@ -116,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'): From 6e80100ae0491ffa02121d5ce7a0f255bd73e712 Mon Sep 17 00:00:00 2001 From: Anatoly Bubenkov Date: Sat, 26 Jul 2014 12:37:55 +0000 Subject: [PATCH 46/46] multiple steps instead of one --- tests/feature/test_cucumber_json.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/tests/feature/test_cucumber_json.py b/tests/feature/test_cucumber_json.py index 16b4fdd07..0fc2c41b1 100644 --- a/tests/feature/test_cucumber_json.py +++ b/tests/feature/test_cucumber_json.py @@ -38,7 +38,8 @@ def test_step_trace(testdir, equals_any): And some other passing step Scenario: Failing - Given a failing step + Given a passing step + And a failing step """)) testdir.makepyfile(textwrap.dedent(""" import pytest @@ -117,6 +118,17 @@ def test_failing(): "match": { "location": "" }, + "name": "a passing step", + "result": { + "status": "passed" + } + }, + { + "keyword": "And", + "line": 9, + "match": { + "location": "" + }, "name": "a failing step", "result": { "error_message": equals_any,