-
Notifications
You must be signed in to change notification settings - Fork 230
Cucumber json formatter #46
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
3b60a9e
b9a9634
c9b509a
c22464f
961b724
fd16428
3d10266
9f6ce33
c7cd4ee
bc2c347
9dbb638
5935411
375729b
fe56114
870c69f
70472f3
127cba2
a9ca6db
1f62168
052e74e
c2dd06f
aa81fea
cb6db6f
fe49943
50d2bd7
504d722
164ac9c
faa5ece
9c19653
29249b6
18ae4c9
3d37863
11c1ef5
be87dff
4ed618e
d740515
62898e5
34a4ac6
fce9aab
02989bb
1fe8341
f1bf4bc
3f5da0f
56d2782
79b40fb
2c6c35f
8f970bd
15f95df
fa66c68
44be5f4
59fc750
5d51498
a427de3
fde61f7
9f24385
5aaf42e
b08d385
76be972
ae5a2a1
7a968fb
6e80100
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 | ||
| ----- | ||
|
|
||
| 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 |
| 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 { | ||
| '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": { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. maybe it can be fitted to one line
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
|
@@ -32,6 +34,7 @@ | |
|
|
||
|
|
||
| class FeatureError(Exception): # pragma: no cover | ||
|
|
||
| """Feature parse error.""" | ||
|
|
||
| message = u'{0}.\nLine number: {1}.\nLine: {2}.' | ||
|
|
@@ -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): | ||
|
|
@@ -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'): | ||
|
|
@@ -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 | ||
|
|
@@ -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: | ||
|
|
@@ -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. | ||
|
|
@@ -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): | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
|
||
|
|
@@ -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.""" | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done