Skip to content
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

Pytest bdd tag hooks #740

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
3 changes: 2 additions & 1 deletion allure-pytest-bdd/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@

install_requires = [
"pytest>=4.5.0",
"pytest-bdd>=3.0.0"
"pytest-bdd>=3.0.0",
"six>=1.9.0"
]


Expand Down
50 changes: 50 additions & 0 deletions allure-pytest-bdd/src/helper.py
Original file line number Diff line number Diff line change
@@ -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)
48 changes: 47 additions & 1 deletion allure-pytest-bdd/src/plugin.py
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -17,26 +23,66 @@ 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))

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))
41 changes: 28 additions & 13 deletions allure-pytest-bdd/src/pytest_bdd_listener.py
Original file line number Diff line number Diff line change
@@ -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()
Expand All @@ -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)
Expand All @@ -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):
Expand Down
95 changes: 90 additions & 5 deletions allure-pytest-bdd/src/utils.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down Expand Up @@ -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