diff --git a/allure-pytest-bdd/setup.py b/allure-pytest-bdd/setup.py index bf166279..9ec559e5 100644 --- a/allure-pytest-bdd/setup.py +++ b/allure-pytest-bdd/setup.py @@ -25,7 +25,8 @@ install_requires = [ "pytest>=4.5.0", - "pytest-bdd>=3.0.0" + "pytest-bdd>=3.0.0", + "six>=1.9.0" ] diff --git a/allure-pytest-bdd/src/helper.py b/allure-pytest-bdd/src/helper.py new file mode 100644 index 00000000..240c945f --- /dev/null +++ b/allure-pytest-bdd/src/helper.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +import pytest +import allure_commons +from .utils import ALLURE_DESCRIPTION_MARK, ALLURE_DESCRIPTION_HTML_MARK +from .utils import ALLURE_LABEL_MARK, ALLURE_LINK_MARK + + +class AllureTitleHelper(object): + @allure_commons.hookimpl + def decorate_as_title(self, test_title): + def decorator(func): + # pytest.fixture wraps function, so we need to get it directly + if getattr(func, '__pytest_wrapped__', None): + function = func.__pytest_wrapped__.obj + else: + function = func + function.__allure_display_name__ = test_title + return func + + return decorator + + +class AllureTestHelper(object): + def __init__(self, config): + self.config = config + + @allure_commons.hookimpl + def decorate_as_description(self, test_description): + allure_description = getattr(pytest.mark, ALLURE_DESCRIPTION_MARK) + return allure_description(test_description) + + @allure_commons.hookimpl + def decorate_as_description_html(self, test_description_html): + allure_description_html = getattr(pytest.mark, ALLURE_DESCRIPTION_HTML_MARK) + return allure_description_html(test_description_html) + + @allure_commons.hookimpl + def decorate_as_label(self, label_type, labels): + allure_label = getattr(pytest.mark, ALLURE_LABEL_MARK) + return allure_label(*labels, label_type=label_type) + + @allure_commons.hookimpl + def decorate_as_link(self, url, link_type, name): + pattern = dict(self.config.option.allure_link_pattern).get(link_type, u'{}') + url = pattern.format(url) + allure_link = getattr(pytest.mark, ALLURE_LINK_MARK) + name = url if name is None else name + return allure_link(url, name=name, link_type=link_type) diff --git a/allure-pytest-bdd/src/plugin.py b/allure-pytest-bdd/src/plugin.py index a5620264..0f1f6fed 100644 --- a/allure-pytest-bdd/src/plugin.py +++ b/allure-pytest-bdd/src/plugin.py @@ -1,7 +1,13 @@ +import argparse + import allure_commons import os from allure_commons.logger import AllureFileLogger from .pytest_bdd_listener import PytestBDDListener +from .utils import ALLURE_DESCRIPTION_MARK, ALLURE_DESCRIPTION_HTML_MARK +from .utils import ALLURE_LABEL_MARK, ALLURE_LINK_MARK + +from .helper import AllureTestHelper, AllureTitleHelper def pytest_addoption(parser): @@ -17,22 +23,57 @@ def pytest_addoption(parser): dest="clean_alluredir", help="Clean alluredir folder if it exists") + parser.getgroup("general").addoption('--allure-environmant-vars-to-tag', + action="store", + dest="env_vars_to_tag", + help="Comma-separated list of environment varibales to add as tags") + + def link_pattern(string): + pattern = string.split(':', 1) + if not pattern[0]: + raise argparse.ArgumentTypeError('Link type is mandatory.') + + if len(pattern) != 2: + raise argparse.ArgumentTypeError('Link pattern is mandatory') + return pattern + + parser.getgroup("general").addoption('--allure-link-pattern', + action="append", + dest="allure_link_pattern", + metavar="LINK_TYPE:LINK_PATTERN", + default=[], + type=link_pattern, + help="""Url pattern for link type. Allows short links in test, + like 'issue-1'. Text will be formatted to full url with python + str.format().""") + def cleanup_factory(plugin): def clean_up(): name = allure_commons.plugin_manager.get_name(plugin) allure_commons.plugin_manager.unregister(name=name) + return clean_up +def pytest_addhooks(pluginmanager): + # Need register title hooks before conftest init + title_helper = AllureTitleHelper() + allure_commons.plugin_manager.register(title_helper) + + def pytest_configure(config): report_dir = config.option.allure_report_dir clean = config.option.clean_alluredir + test_helper = AllureTestHelper(config) + allure_commons.plugin_manager.register(test_helper) + config.add_cleanup(cleanup_factory(test_helper)) + if report_dir: report_dir = os.path.abspath(report_dir) - pytest_bdd_listener = PytestBDDListener() + pytest_bdd_listener = PytestBDDListener(config) config.pluginmanager.register(pytest_bdd_listener) allure_commons.plugin_manager.register(pytest_bdd_listener) config.add_cleanup(cleanup_factory(pytest_bdd_listener)) @@ -40,3 +81,8 @@ def pytest_configure(config): file_logger = AllureFileLogger(report_dir, clean) allure_commons.plugin_manager.register(file_logger) config.add_cleanup(cleanup_factory(file_logger)) + + config.addinivalue_line("markers", "{mark}: allure label marker".format(mark=ALLURE_LABEL_MARK)) + config.addinivalue_line("markers", "{mark}: allure link marker".format(mark=ALLURE_LINK_MARK)) + config.addinivalue_line("markers", "{mark}: allure description".format(mark=ALLURE_DESCRIPTION_MARK)) + config.addinivalue_line("markers", "{mark}: allure description html".format(mark=ALLURE_DESCRIPTION_HTML_MARK)) diff --git a/allure-pytest-bdd/src/pytest_bdd_listener.py b/allure-pytest-bdd/src/pytest_bdd_listener.py index 9a4f5fac..5419d2eb 100644 --- a/allure-pytest-bdd/src/pytest_bdd_listener.py +++ b/allure-pytest-bdd/src/pytest_bdd_listener.py @@ -1,26 +1,30 @@ -import pytest +from functools import partial + import allure_commons -from allure_commons.utils import now -from allure_commons.utils import uuid4 +import pytest +from allure_commons.lifecycle import AllureLifecycle from allure_commons.model2 import Label +from allure_commons.model2 import Link from allure_commons.model2 import Status - +from allure_commons.model2 import StatusDetails from allure_commons.types import LabelType -from allure_commons.utils import platform_label from allure_commons.utils import host_tag, thread_tag from allure_commons.utils import md5 -from .utils import get_uuid -from .utils import get_step_name -from .utils import get_status_details -from .utils import get_pytest_report_status -from allure_commons.model2 import StatusDetails -from functools import partial -from allure_commons.lifecycle import AllureLifecycle +from allure_commons.utils import now +from allure_commons.utils import platform_label +from allure_commons.utils import uuid4 + +from .utils import allure_description, pytest_markers, allure_labels, allure_links, get_tags_from_environment_vars from .utils import get_full_name, get_name, get_params +from .utils import get_pytest_report_status +from .utils import get_status_details +from .utils import get_step_name +from .utils import get_uuid class PytestBDDListener: - def __init__(self): + def __init__(self, config): + self.config = config self.lifecycle = AllureLifecycle() self.host = host_tag() self.thread = thread_tag() @@ -38,16 +42,25 @@ def pytest_bdd_before_scenario(self, request, feature, scenario): uuid = get_uuid(request.node.nodeid) full_name = get_full_name(feature, scenario) name = get_name(request.node, scenario) + with self.lifecycle.schedule_test_case(uuid=uuid) as test_result: test_result.fullName = full_name test_result.name = name test_result.start = now() test_result.historyId = md5(request.node.nodeid) + test_result.labels.extend([Label(name=name, value=value) for name, value in allure_labels(request.node)]) + test_result.labels.extend([Label(name=LabelType.TAG, value=value) for value in + get_tags_from_environment_vars(self.config.option.env_vars_to_tag)]) + test_result.labels.extend( + [Label(name=LabelType.TAG, value=value) for value in pytest_markers(request.node)]) test_result.labels.append(Label(name=LabelType.HOST, value=self.host)) test_result.labels.append(Label(name=LabelType.THREAD, value=self.thread)) test_result.labels.append(Label(name=LabelType.FRAMEWORK, value="pytest-bdd")) test_result.labels.append(Label(name=LabelType.LANGUAGE, value=platform_label())) test_result.labels.append(Label(name=LabelType.FEATURE, value=feature.name)) + test_result.description = allure_description(request.node) + test_result.links.extend( + [Link(link_type, url, name) for link_type, url, name in allure_links(request.node)]) test_result.parameters = get_params(request.node) finalizer = partial(self._scenario_finalizer, scenario) @@ -58,6 +71,8 @@ def pytest_bdd_after_scenario(self, request, feature, scenario): uuid = get_uuid(request.node.nodeid) with self.lifecycle.update_test_case(uuid=uuid) as test_result: test_result.stop = now() + test_result.labels.extend([Label(name=LabelType.TAG, value=value) for value in + get_tags_from_environment_vars(self.config.option.env_vars_to_tag)]) @pytest.hookimpl def pytest_bdd_before_step(self, request, feature, scenario, step, step_func): diff --git a/allure-pytest-bdd/src/utils.py b/allure-pytest-bdd/src/utils.py index ac70aac2..e75dc1e8 100644 --- a/allure-pytest-bdd/src/utils.py +++ b/allure-pytest-bdd/src/utils.py @@ -1,14 +1,34 @@ import os from uuid import UUID -from allure_commons.utils import md5 -from allure_commons.model2 import StatusDetails -from allure_commons.model2 import Status + from allure_commons.model2 import Parameter +from allure_commons.model2 import Status +from allure_commons.model2 import StatusDetails +from allure_commons.types import LabelType from allure_commons.utils import format_exception +from allure_commons.utils import md5 +from allure_commons.utils import represent + +ALLURE_DESCRIPTION_MARK = 'allure_description' +ALLURE_DESCRIPTION_HTML_MARK = 'allure_description_html' +ALLURE_LABEL_MARK = 'allure_label' +ALLURE_LINK_MARK = 'allure_link' +ALLURE_UNIQUE_LABELS = [ + LabelType.SEVERITY, + LabelType.FRAMEWORK, + LabelType.HOST, + LabelType.SUITE, + LabelType.PARENT_SUITE, + LabelType.SUB_SUITE +] -def get_step_name(step): - return f"{step.keyword} {step.name}" +def get_step_name(node, step): + name = "{step_keyword} {step_name}".format(step_keyword=step.keyword, step_name=step.name) + if hasattr(node, 'callspec'): + for key, value in node.callspec.params.items(): + name = name.replace(key, value) + return name def get_name(node, scenario): @@ -48,3 +68,68 @@ def get_params(node): outline_params = params.pop('_pytest_bdd_example', {}) params.update(outline_params) return [Parameter(name=name, value=value) for name, value in params.items()] + + +def get_marker_value(item, keyword): + marker = item.get_closest_marker(keyword) + return marker.args[0] if marker and marker.args else None + + +def allure_description(item): + description = get_marker_value(item, ALLURE_DESCRIPTION_MARK) + if description: + return description + elif hasattr(item, 'function'): + return item.function.__doc__ + + +def pytest_markers(item): + for keyword in item.keywords.keys(): + if any([keyword.startswith('allure_'), keyword == 'parametrize']): + continue + marker = item.get_closest_marker(keyword) + if marker is None: + continue + + yield mark_to_str(marker) + + +def allure_labels(item): + unique_labels = dict() + labels = set() + for mark in item.iter_markers(name=ALLURE_LABEL_MARK): + label_type = mark.kwargs["label_type"] + if label_type in ALLURE_UNIQUE_LABELS: + if label_type not in unique_labels.keys(): + unique_labels[label_type] = mark.args[0] + else: + for arg in mark.args: + labels.add((label_type, arg)) + for k, v in unique_labels.items(): + labels.add((k, v)) + return labels + + +def mark_to_str(marker): + args = [represent(arg) for arg in marker.args] + kwargs = ['{name}={value}'.format(name=key, value=represent(marker.kwargs[key])) for key in marker.kwargs] + if marker.name in ('filterwarnings', 'skip', 'skipif', 'xfail', 'usefixtures', 'tryfirst', 'trylast'): + markstr = '@pytest.mark.{name}'.format(name=marker.name) + else: + markstr = '{name}'.format(name=marker.name) + if args or kwargs: + parameters = ', '.join(args + kwargs) + markstr = '{}({})'.format(markstr, parameters) + return markstr + + +def allure_links(item): + for mark in item.iter_markers(name=ALLURE_LINK_MARK): + yield (mark.kwargs["link_type"], mark.args[0], mark.kwargs["name"]) + + +def get_tags_from_environment_vars(env_vars_list): + tags = set() + for var_name in env_vars_list.split(','): + tags.add(f"{var_name}: {os.environ.get(var_name)}") + return tags