diff options
author | iddqd <iddqd@yandex-team.com> | 2024-05-13 17:19:30 +0300 |
---|---|---|
committer | iddqd <iddqd@yandex-team.com> | 2024-05-13 17:28:44 +0300 |
commit | 84d127b9b7e96ba4352e3f5ddc9222aee9a66053 (patch) | |
tree | 2ebb2689abf65e68dfe92a3ca9b161b4b6ae183f | |
parent | b7deb7f0b71db7419781d1b0357dfa443ccc3ff1 (diff) | |
download | ydb-84d127b9b7e96ba4352e3f5ddc9222aee9a66053.tar.gz |
Add allure support to ydb github export
d6cba27d09fb5e50a99c36070a6a3545c8393ea1
27 files changed, 2811 insertions, 0 deletions
diff --git a/contrib/python/allure-pytest/.dist-info/METADATA b/contrib/python/allure-pytest/.dist-info/METADATA new file mode 100644 index 0000000000..475b858087 --- /dev/null +++ b/contrib/python/allure-pytest/.dist-info/METADATA @@ -0,0 +1,58 @@ +Metadata-Version: 2.1 +Name: allure-pytest +Version: 2.13.5 +Summary: Allure pytest integration +Home-page: https://allurereport.org/ +Author: Qameta Software Inc., Stanislav Seliverstov +Author-email: sseliverstov@qameta.io +License: Apache-2.0 +Project-URL: Documentation, https://allurereport.org/docs/pytest/ +Project-URL: Source, https://github.com/allure-framework/allure-python +Keywords: allure reporting pytest +Classifier: Development Status :: 5 - Production/Stable +Classifier: Framework :: Pytest +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: Apache Software License +Classifier: Topic :: Software Development :: Quality Assurance +Classifier: Topic :: Software Development :: Testing +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Description-Content-Type: text/markdown +Requires-Dist: pytest >=4.5.0 +Requires-Dist: allure-python-commons ==2.13.5 + +## Allure Pytest Plugin + +[![Release Status](https://img.shields.io/pypi/v/allure-pytest)](https://pypi.python.org/pypi/allure-pytest) +[![Downloads](https://img.shields.io/pypi/dm/allure-pytest)](https://pypi.python.org/pypi/allure-pytest) + +> An Allure adapter for [pytest](https://docs.pytest.org/en/latest/). + +[<img src="https://allurereport.org/public/img/allure-report.svg" height="85px" alt="Allure Report logo" align="right" />](https://allurereport.org "Allure Report") + +- Learn more about Allure Report at [https://allurereport.org](https://allurereport.org) +- 📚 [Documentation](https://allurereport.org/docs/) – discover official documentation for Allure Report +- ❓ [Questions and Support](https://github.com/orgs/allure-framework/discussions/categories/questions-support) – get help from the team and community +- 📢 [Official announcements](https://github.com/orgs/allure-framework/discussions/categories/announcements) – stay updated with our latest news and updates +- 💬 [General Discussion](https://github.com/orgs/allure-framework/discussions/categories/general-discussion) – engage in casual conversations, share insights and ideas with the community +- 🖥️ [Live Demo](https://demo.allurereport.org/) — explore a live example of Allure Report in action + +--- + +## Quick start + +```shell +$ pip install allure-pytest +$ pytest --alluredir=%allure_result_folder% ./tests +$ allure serve %allure_result_folder% +``` + +## Further readings + +Learn more from [Allure pytest's official documentation](https://allurereport.org/docs/pytest/). diff --git a/contrib/python/allure-pytest/.dist-info/entry_points.txt b/contrib/python/allure-pytest/.dist-info/entry_points.txt new file mode 100644 index 0000000000..ba3be587c0 --- /dev/null +++ b/contrib/python/allure-pytest/.dist-info/entry_points.txt @@ -0,0 +1,2 @@ +[pytest11] +allure_pytest = allure_pytest.plugin diff --git a/contrib/python/allure-pytest/.dist-info/top_level.txt b/contrib/python/allure-pytest/.dist-info/top_level.txt new file mode 100644 index 0000000000..8f72437398 --- /dev/null +++ b/contrib/python/allure-pytest/.dist-info/top_level.txt @@ -0,0 +1 @@ +allure_pytest diff --git a/contrib/python/allure-pytest/README.md b/contrib/python/allure-pytest/README.md new file mode 100644 index 0000000000..39abc503f7 --- /dev/null +++ b/contrib/python/allure-pytest/README.md @@ -0,0 +1,29 @@ +## Allure Pytest Plugin + +[![Release Status](https://img.shields.io/pypi/v/allure-pytest)](https://pypi.python.org/pypi/allure-pytest) +[![Downloads](https://img.shields.io/pypi/dm/allure-pytest)](https://pypi.python.org/pypi/allure-pytest) + +> An Allure adapter for [pytest](https://docs.pytest.org/en/latest/). + +[<img src="https://allurereport.org/public/img/allure-report.svg" height="85px" alt="Allure Report logo" align="right" />](https://allurereport.org "Allure Report") + +- Learn more about Allure Report at [https://allurereport.org](https://allurereport.org) +- 📚 [Documentation](https://allurereport.org/docs/) – discover official documentation for Allure Report +- ❓ [Questions and Support](https://github.com/orgs/allure-framework/discussions/categories/questions-support) – get help from the team and community +- 📢 [Official announcements](https://github.com/orgs/allure-framework/discussions/categories/announcements) – stay updated with our latest news and updates +- 💬 [General Discussion](https://github.com/orgs/allure-framework/discussions/categories/general-discussion) – engage in casual conversations, share insights and ideas with the community +- 🖥️ [Live Demo](https://demo.allurereport.org/) — explore a live example of Allure Report in action + +--- + +## Quick start + +```shell +$ pip install allure-pytest +$ pytest --alluredir=%allure_result_folder% ./tests +$ allure serve %allure_result_folder% +``` + +## Further readings + +Learn more from [Allure pytest's official documentation](https://allurereport.org/docs/pytest/). diff --git a/contrib/python/allure-pytest/allure_pytest/__init__.py b/contrib/python/allure-pytest/allure_pytest/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/contrib/python/allure-pytest/allure_pytest/__init__.py diff --git a/contrib/python/allure-pytest/allure_pytest/compat.py b/contrib/python/allure-pytest/allure_pytest/compat.py new file mode 100644 index 0000000000..bf7db2dd27 --- /dev/null +++ b/contrib/python/allure-pytest/allure_pytest/compat.py @@ -0,0 +1,34 @@ +"""Provides compatibility with different pytest versions.""" + +from inspect import signature + +__GETFIXTUREDEFS_2ND_PAR_IS_STR = None + + +def getfixturedefs(fixturemanager, name, item): + """Calls FixtureManager.getfixturedefs in a way compatible with Python + versions before and after the change described in pytest-dev/pytest#11785. + """ + getfixturedefs = fixturemanager.getfixturedefs + itemarg = __resolve_getfixturedefs_2nd_arg(getfixturedefs, item) + return getfixturedefs(name, itemarg) + + +def __resolve_getfixturedefs_2nd_arg(getfixturedefs, item): + # Starting from pytest 8.1, getfixturedefs requires the item itself. + # In earlier versions it requires the nodeid string. + return item.nodeid if __2nd_parameter_is_str(getfixturedefs) else item + + +def __2nd_parameter_is_str(getfixturedefs): + global __GETFIXTUREDEFS_2ND_PAR_IS_STR + if __GETFIXTUREDEFS_2ND_PAR_IS_STR is None: + __GETFIXTUREDEFS_2ND_PAR_IS_STR =\ + __get_2nd_parameter_type(getfixturedefs) is str + return __GETFIXTUREDEFS_2ND_PAR_IS_STR + + +def __get_2nd_parameter_type(fn): + return list( + signature(fn).parameters.values() + )[1].annotation diff --git a/contrib/python/allure-pytest/allure_pytest/helper.py b/contrib/python/allure-pytest/allure_pytest/helper.py new file mode 100644 index 0000000000..e6944ef40c --- /dev/null +++ b/contrib/python/allure-pytest/allure_pytest/helper.py @@ -0,0 +1,47 @@ +import pytest +import allure_commons +from allure_pytest.utils import ALLURE_DESCRIPTION_MARK, ALLURE_DESCRIPTION_HTML_MARK +from allure_pytest.utils import ALLURE_LABEL_MARK, ALLURE_LINK_MARK +from allure_pytest.utils import format_allure_link + + +class AllureTitleHelper: + @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: + 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): + url = format_allure_link(self.config, url, link_type) + 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/contrib/python/allure-pytest/allure_pytest/listener.py b/contrib/python/allure-pytest/allure_pytest/listener.py new file mode 100644 index 0000000000..1115363091 --- /dev/null +++ b/contrib/python/allure-pytest/allure_pytest/listener.py @@ -0,0 +1,364 @@ +import pytest +import doctest + +import allure_commons +from allure_commons.utils import now +from allure_commons.utils import uuid4 +from allure_commons.utils import represent +from allure_commons.utils import platform_label +from allure_commons.utils import host_tag, thread_tag +from allure_commons.utils import md5 +from allure_commons.reporter import AllureReporter +from allure_commons.model2 import TestStepResult, TestResult, TestBeforeResult, TestAfterResult +from allure_commons.model2 import TestResultContainer +from allure_commons.model2 import StatusDetails +from allure_commons.model2 import Parameter +from allure_commons.model2 import Label, Link +from allure_commons.model2 import Status +from allure_commons.types import LabelType, AttachmentType, ParameterMode +from allure_pytest.utils import allure_description, allure_description_html +from allure_pytest.utils import allure_labels, allure_links, pytest_markers +from allure_pytest.utils import allure_full_name, allure_package, allure_name +from allure_pytest.utils import allure_suite_labels +from allure_pytest.utils import get_status, get_status_details +from allure_pytest.utils import get_outcome_status, get_outcome_status_details +from allure_pytest.utils import get_pytest_report_status +from allure_pytest.utils import format_allure_link +from allure_pytest.utils import get_history_id +from allure_pytest.compat import getfixturedefs + + +class AllureListener: + + SUITE_LABELS = { + LabelType.PARENT_SUITE, + LabelType.SUITE, + LabelType.SUB_SUITE, + } + + def __init__(self, config): + self.config = config + self.allure_logger = AllureReporter() + self._cache = ItemCache() + self._host = host_tag() + self._thread = thread_tag() + + @allure_commons.hookimpl + def start_step(self, uuid, title, params): + parameters = [Parameter(name=name, value=value) for name, value in params.items()] + step = TestStepResult(name=title, start=now(), parameters=parameters) + self.allure_logger.start_step(None, uuid, step) + + @allure_commons.hookimpl + def stop_step(self, uuid, exc_type, exc_val, exc_tb): + self.allure_logger.stop_step(uuid, + stop=now(), + status=get_status(exc_val), + statusDetails=get_status_details(exc_type, exc_val, exc_tb)) + + @allure_commons.hookimpl + def start_fixture(self, parent_uuid, uuid, name): + after_fixture = TestAfterResult(name=name, start=now()) + self.allure_logger.start_after_fixture(parent_uuid, uuid, after_fixture) + + @allure_commons.hookimpl + def stop_fixture(self, parent_uuid, uuid, name, exc_type, exc_val, exc_tb): + self.allure_logger.stop_after_fixture(uuid, + stop=now(), + status=get_status(exc_val), + statusDetails=get_status_details(exc_type, exc_val, exc_tb)) + + def _update_fixtures_children(self, item): + uuid = self._cache.get(item.nodeid) + for fixturedef in _test_fixtures(item): + group_uuid = self._cache.get(fixturedef) + if group_uuid: + group = self.allure_logger.get_item(group_uuid) + else: + group_uuid = self._cache.push(fixturedef) + group = TestResultContainer(uuid=group_uuid) + self.allure_logger.start_group(group_uuid, group) + if uuid not in group.children: + self.allure_logger.update_group(group_uuid, children=uuid) + + @pytest.hookimpl(hookwrapper=True, tryfirst=True) + def pytest_runtest_protocol(self, item, nextitem): + uuid = self._cache.push(item.nodeid) + test_result = TestResult(name=item.name, uuid=uuid, start=now(), stop=now()) + self.allure_logger.schedule_test(uuid, test_result) + yield + uuid = self._cache.pop(item.nodeid) + if uuid: + test_result = self.allure_logger.get_test(uuid) + if test_result.status is None: + test_result.status = Status.SKIPPED + self.allure_logger.close_test(uuid) + + @pytest.hookimpl(hookwrapper=True) + def pytest_runtest_setup(self, item): + if not self._cache.get(item.nodeid): + uuid = self._cache.push(item.nodeid) + test_result = TestResult(name=item.name, uuid=uuid, start=now(), stop=now()) + self.allure_logger.schedule_test(uuid, test_result) + yield + self._update_fixtures_children(item) + uuid = self._cache.get(item.nodeid) + test_result = self.allure_logger.get_test(uuid) + params = self.__get_pytest_params(item) + param_id = self.__get_pytest_param_id(item) + test_result.name = allure_name(item, params, param_id) + full_name = allure_full_name(item) + test_result.fullName = full_name + test_result.testCaseId = md5(full_name) + test_result.description = allure_description(item) + test_result.descriptionHtml = allure_description_html(item) + current_param_names = [param.name for param in test_result.parameters] + test_result.parameters.extend([ + Parameter(name=name, value=represent(value)) + for name, value in params.items() + if name not in current_param_names + ]) + + @pytest.hookimpl(hookwrapper=True) + def pytest_runtest_call(self, item): + uuid = self._cache.get(item.nodeid) + test_result = self.allure_logger.get_test(uuid) + if test_result: + self.allure_logger.drop_test(uuid) + self.allure_logger.schedule_test(uuid, test_result) + test_result.start = now() + yield + self._update_fixtures_children(item) + if test_result: + test_result.stop = now() + + @pytest.hookimpl(hookwrapper=True) + def pytest_runtest_teardown(self, item): + yield + uuid = self._cache.get(item.nodeid) + test_result = self.allure_logger.get_test(uuid) + test_result.historyId = get_history_id( + test_result.fullName, + test_result.parameters, + original_values=self.__get_pytest_params(item) + ) + test_result.labels.extend([Label(name=name, value=value) for name, value in allure_labels(item)]) + test_result.labels.extend([Label(name=LabelType.TAG, value=value) for value in pytest_markers(item)]) + self.__apply_default_suites(item, test_result) + 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')) + test_result.labels.append(Label(name=LabelType.LANGUAGE, value=platform_label())) + test_result.labels.append(Label(name='package', value=allure_package(item))) + test_result.links.extend([Link(link_type, url, name) for link_type, url, name in allure_links(item)]) + + @pytest.hookimpl(hookwrapper=True) + def pytest_fixture_setup(self, fixturedef, request): + fixture_name = getattr(fixturedef.func, '__allure_display_name__', fixturedef.argname) + + container_uuid = self._cache.get(fixturedef) + + if not container_uuid: + container_uuid = self._cache.push(fixturedef) + container = TestResultContainer(uuid=container_uuid) + self.allure_logger.start_group(container_uuid, container) + + self.allure_logger.update_group(container_uuid, start=now()) + + before_fixture_uuid = uuid4() + before_fixture = TestBeforeResult(name=fixture_name, start=now()) + self.allure_logger.start_before_fixture(container_uuid, before_fixture_uuid, before_fixture) + + outcome = yield + + self.allure_logger.stop_before_fixture(before_fixture_uuid, + stop=now(), + status=get_outcome_status(outcome), + statusDetails=get_outcome_status_details(outcome)) + + finalizers = getattr(fixturedef, '_finalizers', []) + for index, finalizer in enumerate(finalizers): + finalizer_name = getattr(finalizer, "__name__", index) + name = f'{fixture_name}::{finalizer_name}' + finalizers[index] = allure_commons.fixture(finalizer, parent_uuid=container_uuid, name=name) + + @pytest.hookimpl(hookwrapper=True) + def pytest_fixture_post_finalizer(self, fixturedef): + yield + if hasattr(fixturedef, 'cached_result') and self._cache.get(fixturedef): + container_uuid = self._cache.pop(fixturedef) + self.allure_logger.stop_group(container_uuid, stop=now()) + + @pytest.hookimpl(hookwrapper=True) + def pytest_runtest_makereport(self, item, call): + uuid = self._cache.get(item.nodeid) + + report = (yield).get_result() + + test_result = self.allure_logger.get_test(uuid) + status = get_pytest_report_status(report) + status_details = None + + if call.excinfo: + message = call.excinfo.exconly() + if hasattr(report, 'wasxfail'): + reason = report.wasxfail + message = (f'XFAIL {reason}' if reason else 'XFAIL') + '\n\n' + message + trace = report.longreprtext + status_details = StatusDetails( + message=message, + trace=trace) + + exception = call.excinfo.value + if (status != Status.SKIPPED and _exception_brokes_test(exception)): + status = Status.BROKEN + + if status == Status.PASSED and hasattr(report, 'wasxfail'): + reason = report.wasxfail + message = f'XPASS {reason}' if reason else 'XPASS' + status_details = StatusDetails(message=message) + + if report.when == 'setup': + test_result.status = status + test_result.statusDetails = status_details + + if report.when == 'call': + if test_result.status == Status.PASSED: + test_result.status = status + test_result.statusDetails = status_details + + if report.when == 'teardown': + if status in (Status.FAILED, Status.BROKEN) and test_result.status == Status.PASSED: + test_result.status = status + test_result.statusDetails = status_details + + if self.config.option.attach_capture: + if report.caplog: + self.attach_data(report.caplog, "log", AttachmentType.TEXT, None) + if report.capstdout: + self.attach_data(report.capstdout, "stdout", AttachmentType.TEXT, None) + if report.capstderr: + self.attach_data(report.capstderr, "stderr", AttachmentType.TEXT, None) + + @pytest.hookimpl(hookwrapper=True) + def pytest_runtest_logfinish(self, nodeid, location): + yield + uuid = self._cache.pop(nodeid) + if uuid: + self.allure_logger.close_test(uuid) + + @allure_commons.hookimpl + def attach_data(self, body, name, attachment_type, extension): + self.allure_logger.attach_data(uuid4(), body, name=name, attachment_type=attachment_type, extension=extension) + + @allure_commons.hookimpl + def attach_file(self, source, name, attachment_type, extension): + self.allure_logger.attach_file(uuid4(), source, name=name, attachment_type=attachment_type, extension=extension) + + @allure_commons.hookimpl + def add_title(self, test_title): + test_result = self.allure_logger.get_test(None) + if test_result: + test_result.name = test_title + + @allure_commons.hookimpl + def add_description(self, test_description): + test_result = self.allure_logger.get_test(None) + if test_result: + test_result.description = test_description + + @allure_commons.hookimpl + def add_description_html(self, test_description_html): + test_result = self.allure_logger.get_test(None) + if test_result: + test_result.descriptionHtml = test_description_html + + @allure_commons.hookimpl + def add_link(self, url, link_type, name): + test_result = self.allure_logger.get_test(None) + if test_result: + link_url = format_allure_link(self.config, url, link_type) + new_link = Link(link_type, link_url, link_url if name is None else name) + for link in test_result.links: + if link.url == new_link.url: + return + test_result.links.append(new_link) + + @allure_commons.hookimpl + def add_label(self, label_type, labels): + test_result = self.allure_logger.get_test(None) + for label in labels if test_result else (): + test_result.labels.append(Label(label_type, label)) + + @allure_commons.hookimpl + def add_parameter(self, name, value, excluded, mode: ParameterMode): + test_result: TestResult = self.allure_logger.get_test(None) + existing_param = next(filter(lambda x: x.name == name, test_result.parameters), None) + if existing_param: + existing_param.value = represent(value) + else: + test_result.parameters.append( + Parameter( + name=name, + value=represent(value), + excluded=excluded or None, + mode=mode.value if mode else None + ) + ) + + @staticmethod + def __get_pytest_params(item): + return item.callspec.params if hasattr(item, 'callspec') else {} + + @staticmethod + def __get_pytest_param_id(item): + return item.callspec.id if hasattr(item, 'callspec') else None + + def __apply_default_suites(self, item, test_result): + default_suites = allure_suite_labels(item) + existing_suites = { + label.name + for label in test_result.labels + if label.name in AllureListener.SUITE_LABELS + } + test_result.labels.extend( + Label(name=name, value=value) + for name, value in default_suites + if name not in existing_suites + ) + + +class ItemCache: + + def __init__(self): + self._items = dict() + + def get(self, _id): + return self._items.get(id(_id)) + + def push(self, _id): + return self._items.setdefault(id(_id), uuid4()) + + def pop(self, _id): + return self._items.pop(id(_id), None) + + +def _test_fixtures(item): + fixturemanager = item.session._fixturemanager + fixturedefs = [] + + if hasattr(item, "_request") and hasattr(item._request, "fixturenames"): + for name in item._request.fixturenames: + fixturedefs_pytest = getfixturedefs(fixturemanager, name, item) + if fixturedefs_pytest: + fixturedefs.extend(fixturedefs_pytest) + + return fixturedefs + + +def _exception_brokes_test(exception): + return not isinstance(exception, ( + AssertionError, + pytest.fail.Exception, + doctest.DocTestFailure + )) diff --git a/contrib/python/allure-pytest/allure_pytest/plugin.py b/contrib/python/allure-pytest/allure_pytest/plugin.py new file mode 100644 index 0000000000..2771722ffc --- /dev/null +++ b/contrib/python/allure-pytest/allure_pytest/plugin.py @@ -0,0 +1,236 @@ +import argparse + +import allure +import allure_commons +import os + +from allure_commons.types import LabelType, Severity +from allure_commons.logger import AllureFileLogger +from allure_commons.utils import get_testplan + +from allure_pytest.utils import allure_label, allure_labels, allure_full_name +from allure_pytest.helper import AllureTestHelper, AllureTitleHelper +from allure_pytest.listener import AllureListener + +from allure_pytest.utils import ALLURE_DESCRIPTION_MARK, ALLURE_DESCRIPTION_HTML_MARK +from allure_pytest.utils import ALLURE_LABEL_MARK, ALLURE_LINK_MARK + + +def pytest_addoption(parser): + parser.getgroup("reporting").addoption('--alluredir', + action="store", + dest="allure_report_dir", + metavar="DIR", + default=None, + help="Generate Allure report in the specified directory (may not exist)") + + parser.getgroup("reporting").addoption('--clean-alluredir', + action="store_true", + dest="clean_alluredir", + help="Clean alluredir folder if it exists") + + parser.getgroup("reporting").addoption('--allure-no-capture', + action="store_false", + dest="attach_capture", + help="Do not attach pytest captured logging/stdout/stderr to report") + + parser.getgroup("reporting").addoption('--inversion', + action="store", + dest="inversion", + default=False, + help="Run tests not in testplan") + + def label_type(type_name, legal_values=set()): + def a_label_type(string): + atoms = set(string.split(',')) + if type_name is LabelType.SEVERITY: + if not atoms <= legal_values: + raise argparse.ArgumentTypeError('Illegal {} values: {}, only [{}] are allowed'.format( + type_name, + ', '.join(atoms - legal_values), + ', '.join(legal_values) + )) + return set((type_name, allure.severity_level(atom)) for atom in atoms) + return set((type_name, atom) for atom in atoms) + return a_label_type + + severities = [x.value for x in list(allure.severity_level)] + formatted_severities = ', '.join(severities) + parser.getgroup("general").addoption('--allure-severities', + action="store", + dest="allure_severities", + metavar="SEVERITIES_SET", + default={}, + type=label_type(LabelType.SEVERITY, legal_values=set(severities)), + help=f"""Comma-separated list of severity names. + Tests only with these severities will be run. + Possible values are: {formatted_severities}.""") + + parser.getgroup("general").addoption('--allure-epics', + action="store", + dest="allure_epics", + metavar="EPICS_SET", + default={}, + type=label_type(LabelType.EPIC), + help="""Comma-separated list of epic names. + Run tests that have at least one of the specified feature labels.""") + + parser.getgroup("general").addoption('--allure-features', + action="store", + dest="allure_features", + metavar="FEATURES_SET", + default={}, + type=label_type(LabelType.FEATURE), + help="""Comma-separated list of feature names. + Run tests that have at least one of the specified feature labels.""") + + parser.getgroup("general").addoption('--allure-stories', + action="store", + dest="allure_stories", + metavar="STORIES_SET", + default={}, + type=label_type(LabelType.STORY), + help="""Comma-separated list of story names. + Run tests that have at least one of the specified story labels.""") + + parser.getgroup("general").addoption('--allure-ids', + action="store", + dest="allure_ids", + metavar="IDS_SET", + default={}, + type=label_type(LabelType.ID), + help="""Comma-separated list of IDs. + Run tests that have at least one of the specified id labels.""") + + def cf_type(string): + type_name, values = string.split("=", 1) + atoms = set(values.split(",")) + return [(type_name, atom) for atom in atoms] + + parser.getgroup("general").addoption('--allure-label', + action="append", + dest="allure_labels", + metavar="LABELS_SET", + default=[], + type=cf_type, + help="""List of labels to run in format label_name=value1,value2. + "Run tests that have at least one of the specified labels.""") + + 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 = False if config.option.collectonly else 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) + test_listener = AllureListener(config) + config.pluginmanager.register(test_listener, 'allure_listener') + allure_commons.plugin_manager.register(test_listener) + config.add_cleanup(cleanup_factory(test_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", f"{ALLURE_LABEL_MARK}: allure label marker") + config.addinivalue_line("markers", f"{ALLURE_LINK_MARK}: allure link marker") + config.addinivalue_line("markers", f"{ALLURE_DESCRIPTION_MARK}: allure description") + config.addinivalue_line("markers", f"{ALLURE_DESCRIPTION_HTML_MARK}: allure description html") + + +def select_by_labels(items, config): + arg_labels = set().union( + config.option.allure_epics, + config.option.allure_features, + config.option.allure_stories, + config.option.allure_ids, + config.option.allure_severities, + *config.option.allure_labels + ) + if arg_labels: + selected, deselected = [], [] + for item in items: + test_labels = set(allure_labels(item)) + test_severity = allure_label(item, LabelType.SEVERITY) + if not test_severity: + test_labels.add((LabelType.SEVERITY, Severity.NORMAL)) + if arg_labels & test_labels: + selected.append(item) + else: + deselected.append(item) + return selected, deselected + else: + return items, [] + + +def select_by_testcase(items, config): + planned_tests = get_testplan() + is_inversion = config.option.inversion + + if planned_tests: + + def is_planed(item): + allure_ids = allure_label(item, LabelType.ID) + allure_string_ids = list(map(str, allure_ids)) + for planed_item in planned_tests: + planed_item_string_id = str(planed_item.get("id")) + planed_item_selector = planed_item.get("selector") + if ( + planed_item_string_id in allure_string_ids + or planed_item_selector == allure_full_name(item) + ): + return True if not is_inversion else False + return False if not is_inversion else True + + selected, deselected = [], [] + for item in items: + selected.append(item) if is_planed(item) else deselected.append(item) + return selected, deselected + else: + return items, [] + + +def pytest_collection_modifyitems(items, config): + selected, deselected_by_testcase = select_by_testcase(items, config) + selected, deselected_by_labels = select_by_labels(selected, config) + + items[:] = selected + + if deselected_by_testcase or deselected_by_labels: + config.hook.pytest_deselected(items=[*deselected_by_testcase, *deselected_by_labels]) diff --git a/contrib/python/allure-pytest/allure_pytest/utils.py b/contrib/python/allure-pytest/allure_pytest/utils.py new file mode 100644 index 0000000000..1e07cb492c --- /dev/null +++ b/contrib/python/allure-pytest/allure_pytest/utils.py @@ -0,0 +1,197 @@ +import pytest +from itertools import chain, islice +from allure_commons.utils import represent, SafeFormatter, md5 +from allure_commons.utils import format_exception, format_traceback +from allure_commons.model2 import Status +from allure_commons.model2 import StatusDetails +from allure_commons.types import LabelType + + +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_marker_value(item, keyword): + marker = item.get_closest_marker(keyword) + return marker.args[0] if marker and marker.args else None + + +def allure_title(item): + return getattr( + getattr(item, "obj", None), + "__allure_display_name__", + 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 allure_description_html(item): + return get_marker_value(item, ALLURE_DESCRIPTION_HTML_MARK) + + +def allure_label(item, label): + labels = [] + for mark in item.iter_markers(name=ALLURE_LABEL_MARK): + if mark.kwargs.get("label_type") == label: + labels.extend(mark.args) + return labels + + +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 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 format_allure_link(config, url, link_type): + pattern = dict(config.option.allure_link_pattern).get(link_type, '{}') + return pattern.format(url) + + +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 mark_to_str(marker): + args = [represent(arg) for arg in marker.args] + kwargs = [f'{key}={represent(value)}' for key, value in marker.kwargs.items()] + if marker.name in ('filterwarnings', 'skip', 'skipif', 'xfail', 'usefixtures', 'tryfirst', 'trylast'): + markstr = f'@pytest.mark.{marker.name}' + else: + markstr = str(marker.name) + if args or kwargs: + parameters = ', '.join(args + kwargs) + markstr = f'{markstr}({parameters})' + return markstr + + +def allure_package(item): + parts = item.nodeid.split('::') + path = parts[0].rsplit('.', 1)[0] + return path.replace('/', '.') + + +def allure_name(item, parameters, param_id=None): + name = item.name + title = allure_title(item) + param_id_kwargs = {} + if param_id: + # if param_id is an ASCII string, it could have been encoded by pytest (_pytest.compat.ascii_escaped) + if param_id.isascii(): + param_id = param_id.encode().decode("unicode-escape") + param_id_kwargs["param_id"] = param_id + return SafeFormatter().format( + title, + **{**param_id_kwargs, **parameters, **item.funcargs} + ) if title else name + + +def allure_full_name(item: pytest.Item): + package = allure_package(item) + class_name = f".{item.parent.name}" if isinstance(item.parent, pytest.Class) else '' + test = item.originalname if isinstance(item, pytest.Function) else item.name.split("[")[0] + full_name = f'{package}{class_name}#{test}' + return full_name + + +def allure_suite_labels(item): + head, possibly_clazz, tail = islice(chain(item.nodeid.split('::'), [None], [None]), 3) + clazz = possibly_clazz if tail else None + file_name, path = islice(chain(reversed(head.rsplit('/', 1)), [None]), 2) + module = file_name.split('.')[0] + package = path.replace('/', '.') if path else None + pairs = dict(zip([LabelType.PARENT_SUITE, LabelType.SUITE, LabelType.SUB_SUITE], [package, module, clazz])) + labels = dict(allure_labels(item)) + default_suite_labels = [] + for label, value in pairs.items(): + if label not in labels.keys() and value: + default_suite_labels.append((label, value)) + + return default_suite_labels + + +def get_outcome_status(outcome): + _, exception, _ = outcome.excinfo or (None, None, None) + return get_status(exception) + + +def get_outcome_status_details(outcome): + exception_type, exception, exception_traceback = outcome.excinfo or (None, None, None) + return get_status_details(exception_type, exception, exception_traceback) + + +def get_status(exception): + if exception: + if isinstance(exception, AssertionError) or isinstance(exception, pytest.fail.Exception): + return Status.FAILED + elif isinstance(exception, pytest.skip.Exception): + return Status.SKIPPED + return Status.BROKEN + else: + return Status.PASSED + + +def get_status_details(exception_type, exception, exception_traceback): + message = format_exception(exception_type, exception) + trace = format_traceback(exception_traceback) + return StatusDetails(message=message, trace=trace) if message or trace else None + + +def get_pytest_report_status(pytest_report): + pytest_statuses = ('failed', 'passed', 'skipped') + statuses = (Status.FAILED, Status.PASSED, Status.SKIPPED) + for pytest_status, status in zip(pytest_statuses, statuses): + if getattr(pytest_report, pytest_status): + return status + + +def get_history_id(full_name, parameters, original_values): + return md5( + full_name, + *(original_values.get(p.name, p.value) for p in sorted( + filter( + lambda p: not p.excluded, + parameters + ), + key=lambda p: p.name + )) + ) diff --git a/contrib/python/allure-pytest/ya.make b/contrib/python/allure-pytest/ya.make new file mode 100644 index 0000000000..68c2880e5d --- /dev/null +++ b/contrib/python/allure-pytest/ya.make @@ -0,0 +1,33 @@ +# Generated by devtools/yamaker (pypi). + +PY3_LIBRARY() + +VERSION(2.13.5) + +LICENSE(Apache-2.0) + +PEERDIR( + contrib/python/allure-python-commons + contrib/python/pytest +) + +NO_LINT() + +PY_SRCS( + TOP_LEVEL + allure_pytest/__init__.py + allure_pytest/compat.py + allure_pytest/helper.py + allure_pytest/listener.py + allure_pytest/plugin.py + allure_pytest/utils.py +) + +RESOURCE_FILES( + PREFIX contrib/python/allure-pytest/ + .dist-info/METADATA + .dist-info/entry_points.txt + .dist-info/top_level.txt +) + +END() diff --git a/contrib/python/allure-python-commons/.dist-info/METADATA b/contrib/python/allure-python-commons/.dist-info/METADATA new file mode 100644 index 0000000000..94cd97681a --- /dev/null +++ b/contrib/python/allure-python-commons/.dist-info/METADATA @@ -0,0 +1,140 @@ +Metadata-Version: 2.1 +Name: allure-python-commons +Version: 2.13.5 +Summary: ('Contains the API for end users as well as helper functions and classes to build Allure adapters for Python test frameworks',) +Home-page: https://allurereport.org/ +Author: Qameta Software Inc., Stanislav Seliverstov +Author-email: sseliverstov@qameta.io +License: Apache-2.0 +Project-URL: Source, https://github.com/allure-framework/allure-python +Keywords: allure reporting report-engine +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: Apache Software License +Classifier: Topic :: Software Development :: Quality Assurance +Classifier: Topic :: Software Development :: Testing +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Requires-Python: >=3.6 +Description-Content-Type: text/markdown +Requires-Dist: attrs >=16.0.0 +Requires-Dist: pluggy >=0.4.0 + +## Allure Common API + +[![Release Status](https://img.shields.io/pypi/v/allure-python-commons)](https://pypi.python.org/pypi/allure-python-commons) +[![Downloads](https://img.shields.io/pypi/dm/allure-python-commons)](https://pypi.python.org/pypi/allure-python-commons) + +> The package contains classes and functions for users of Allure Report. It can +> be used to enhance reports using an existing Allure adapter or to create new +> adapters. + +[<img src="https://allurereport.org/public/img/allure-report.svg" height="85px" alt="Allure Report logo" align="right" />](https://allurereport.org "Allure Report") + +- Learn more about Allure Report at [https://allurereport.org](https://allurereport.org) +- 📚 [Documentation](https://allurereport.org/docs/) – discover official documentation for Allure Report +- ❓ [Questions and Support](https://github.com/orgs/allure-framework/discussions/categories/questions-support) – get help from the team and community +- 📢 [Official announcements](https://github.com/orgs/allure-framework/discussions/categories/announcements) – stay updated with our latest news and updates +- 💬 [General Discussion](https://github.com/orgs/allure-framework/discussions/categories/general-discussion) – engage in casual conversations, share insights and ideas with the community +- 🖥️ [Live Demo](https://demo.allurereport.org/) — explore a live example of Allure Report in action + +--- + +## User's API + +Install an adapter that suits your test framework. You can then add more +information to the report by using functions from the `allure` module. + +### Decorators API + +Use these functions as decorators of your own functions, e.g.: + +```python +import allure + +@allure.title("My test") +def test_fn(): + pass +``` + +The full list of decorators: + + - `allure.title` + - `allure.description` + - `allure.description_html` + - `allure.label` + - `allure.severity` + - `allure.epic` + - `allure.feature` + - `allure.story` + - `allure.suite` + - `allure.parent_suite` + - `allure.sub_suite` + - `allure.tag` + - `allure.id` + - `allure.manual` + - `allure.link` + - `allure.issue` + - `allure.testcase` + - `allure.step` + +Refer to the adapter's documentation for the information about what decorators +are supported and what functions they can be applied to. + +### Runtime API + +Most of the functions of Runtime API can be accessed via `allure.dynamic.*`. +Call them at runtime from your code. + +The full list includes: + + - `allure.dynamic.title` + - `allure.dynamic.description` + - `allure.dynamic.description_html` + - `allure.dynamic.label` + - `allure.dynamic.severity` + - `allure.dynamic.epic` + - `allure.dynamic.feature` + - `allure.dynamic.story` + - `allure.dynamic.suite` + - `allure.dynamic.parent_suite` + - `allure.dynamic.sub_suite` + - `allure.dynamic.tag` + - `allure.dynamic.id` + - `allure.dynamic.manual` + - `allure.dynamic.link` + - `allure.dynamic.issue` + - `allure.dynamic.testcase` + - `allure.dynamic.parameter` + - `allure.attach` + - `allure.attach.file` + - `allure.step` + +Refer to the adapter's documentation for the information about what functions +are supported and where you can use them. + +## Adapter API + +You may use `allure-pytest-commons` to build your own Allure adapter. The key +elements of the corresponding API are: + + - `allure_python_commons.model2`: the object model of Allure Report. + - `allure_python_commons.logger`: classes that are used to emit Allure Report objects (tests, containers, attachments): + - `AllureFileLogger`: emits to the file system. + - `AllureMemoryLogger`: collects the objects in memory. Useful for + testing. + - `allure_python_commons.lifecycle.AllureLifecycle`: an implementation of + Allure lifecycle that doesn't isolate the state between threads. + - `allure_python_commons.reporter.AllureReporter`: an implementation of + Allure lifecycle that supports some multithreaded scenarios. + +A new version of the API is likely to be released in the future as we need +a decent support for multithreaded and async-based concurrency (see +[here](https://github.com/allure-framework/allure-python/issues/697) and +[here](https://github.com/allure-framework/allure-python/issues/720)). diff --git a/contrib/python/allure-python-commons/.dist-info/top_level.txt b/contrib/python/allure-python-commons/.dist-info/top_level.txt new file mode 100644 index 0000000000..6a769a1519 --- /dev/null +++ b/contrib/python/allure-python-commons/.dist-info/top_level.txt @@ -0,0 +1,2 @@ +allure +allure_commons diff --git a/contrib/python/allure-python-commons/README.md b/contrib/python/allure-python-commons/README.md new file mode 100644 index 0000000000..bde7b47d34 --- /dev/null +++ b/contrib/python/allure-python-commons/README.md @@ -0,0 +1,112 @@ +## Allure Common API + +[![Release Status](https://img.shields.io/pypi/v/allure-python-commons)](https://pypi.python.org/pypi/allure-python-commons) +[![Downloads](https://img.shields.io/pypi/dm/allure-python-commons)](https://pypi.python.org/pypi/allure-python-commons) + +> The package contains classes and functions for users of Allure Report. It can +> be used to enhance reports using an existing Allure adapter or to create new +> adapters. + +[<img src="https://allurereport.org/public/img/allure-report.svg" height="85px" alt="Allure Report logo" align="right" />](https://allurereport.org "Allure Report") + +- Learn more about Allure Report at [https://allurereport.org](https://allurereport.org) +- 📚 [Documentation](https://allurereport.org/docs/) – discover official documentation for Allure Report +- ❓ [Questions and Support](https://github.com/orgs/allure-framework/discussions/categories/questions-support) – get help from the team and community +- 📢 [Official announcements](https://github.com/orgs/allure-framework/discussions/categories/announcements) – stay updated with our latest news and updates +- 💬 [General Discussion](https://github.com/orgs/allure-framework/discussions/categories/general-discussion) – engage in casual conversations, share insights and ideas with the community +- 🖥️ [Live Demo](https://demo.allurereport.org/) — explore a live example of Allure Report in action + +--- + +## User's API + +Install an adapter that suits your test framework. You can then add more +information to the report by using functions from the `allure` module. + +### Decorators API + +Use these functions as decorators of your own functions, e.g.: + +```python +import allure + +@allure.title("My test") +def test_fn(): + pass +``` + +The full list of decorators: + + - `allure.title` + - `allure.description` + - `allure.description_html` + - `allure.label` + - `allure.severity` + - `allure.epic` + - `allure.feature` + - `allure.story` + - `allure.suite` + - `allure.parent_suite` + - `allure.sub_suite` + - `allure.tag` + - `allure.id` + - `allure.manual` + - `allure.link` + - `allure.issue` + - `allure.testcase` + - `allure.step` + +Refer to the adapter's documentation for the information about what decorators +are supported and what functions they can be applied to. + +### Runtime API + +Most of the functions of Runtime API can be accessed via `allure.dynamic.*`. +Call them at runtime from your code. + +The full list includes: + + - `allure.dynamic.title` + - `allure.dynamic.description` + - `allure.dynamic.description_html` + - `allure.dynamic.label` + - `allure.dynamic.severity` + - `allure.dynamic.epic` + - `allure.dynamic.feature` + - `allure.dynamic.story` + - `allure.dynamic.suite` + - `allure.dynamic.parent_suite` + - `allure.dynamic.sub_suite` + - `allure.dynamic.tag` + - `allure.dynamic.id` + - `allure.dynamic.manual` + - `allure.dynamic.link` + - `allure.dynamic.issue` + - `allure.dynamic.testcase` + - `allure.dynamic.parameter` + - `allure.attach` + - `allure.attach.file` + - `allure.step` + +Refer to the adapter's documentation for the information about what functions +are supported and where you can use them. + +## Adapter API + +You may use `allure-pytest-commons` to build your own Allure adapter. The key +elements of the corresponding API are: + + - `allure_python_commons.model2`: the object model of Allure Report. + - `allure_python_commons.logger`: classes that are used to emit Allure Report objects (tests, containers, attachments): + - `AllureFileLogger`: emits to the file system. + - `AllureMemoryLogger`: collects the objects in memory. Useful for + testing. + - `allure_python_commons.lifecycle.AllureLifecycle`: an implementation of + Allure lifecycle that doesn't isolate the state between threads. + - `allure_python_commons.reporter.AllureReporter`: an implementation of + Allure lifecycle that supports some multithreaded scenarios. + +A new version of the API is likely to be released in the future as we need +a decent support for multithreaded and async-based concurrency (see +[here](https://github.com/allure-framework/allure-python/issues/697) and +[here](https://github.com/allure-framework/allure-python/issues/720)). diff --git a/contrib/python/allure-python-commons/allure.py b/contrib/python/allure-python-commons/allure.py new file mode 100644 index 0000000000..4acb83e37a --- /dev/null +++ b/contrib/python/allure-python-commons/allure.py @@ -0,0 +1,43 @@ +from allure_commons._allure import title +from allure_commons._allure import description, description_html +from allure_commons._allure import label +from allure_commons._allure import severity +from allure_commons._allure import tag +from allure_commons._allure import id +from allure_commons._allure import suite, parent_suite, sub_suite +from allure_commons._allure import epic, feature, story +from allure_commons._allure import link, issue, testcase +from allure_commons._allure import Dynamic as dynamic +from allure_commons._allure import step +from allure_commons._allure import attach +from allure_commons._allure import manual +from allure_commons.types import Severity as severity_level +from allure_commons.types import AttachmentType as attachment_type +from allure_commons.types import ParameterMode as parameter_mode + + +__all__ = [ + 'title', + 'description', + 'description_html', + 'label', + 'severity', + 'suite', + 'parent_suite', + 'sub_suite', + 'tag', + 'id', + 'epic', + 'feature', + 'story', + 'link', + 'issue', + 'testcase', + 'manual', + 'step', + 'dynamic', + 'severity_level', + 'attach', + 'attachment_type', + 'parameter_mode' +] diff --git a/contrib/python/allure-python-commons/allure_commons/__init__.py b/contrib/python/allure-python-commons/allure_commons/__init__.py new file mode 100644 index 0000000000..111c2d06c2 --- /dev/null +++ b/contrib/python/allure-python-commons/allure_commons/__init__.py @@ -0,0 +1,12 @@ +from allure_commons._hooks import hookimpl # noqa: F401 +from allure_commons._core import plugin_manager # noqa: F401 +from allure_commons._allure import fixture # noqa: F401 +from allure_commons._allure import test # noqa: F401 + + +__all__ = [ + 'hookimpl', + 'plugin_manager', + 'fixture', + 'test' +] diff --git a/contrib/python/allure-python-commons/allure_commons/_allure.py b/contrib/python/allure-python-commons/allure_commons/_allure.py new file mode 100644 index 0000000000..05e01dbd4b --- /dev/null +++ b/contrib/python/allure-python-commons/allure_commons/_allure.py @@ -0,0 +1,265 @@ +from functools import wraps +from typing import Any, Callable, TypeVar + +from allure_commons._core import plugin_manager +from allure_commons.types import LabelType, LinkType, ParameterMode +from allure_commons.utils import uuid4 +from allure_commons.utils import func_parameters, represent + +_TFunc = TypeVar("_TFunc", bound=Callable[..., Any]) + + +def safely(result): + if result: + return result[0] + else: + def dummy(function): + return function + return dummy + + +def title(test_title): + return safely(plugin_manager.hook.decorate_as_title(test_title=test_title)) + + +def description(test_description): + return safely(plugin_manager.hook.decorate_as_description(test_description=test_description)) + + +def description_html(test_description_html): + return safely(plugin_manager.hook.decorate_as_description_html(test_description_html=test_description_html)) + + +def label(label_type, *labels): + return safely(plugin_manager.hook.decorate_as_label(label_type=label_type, labels=labels)) + + +def severity(severity_level): + return label(LabelType.SEVERITY, severity_level) + + +def epic(*epics): + return label(LabelType.EPIC, *epics) + + +def feature(*features): + return label(LabelType.FEATURE, *features) + + +def story(*stories): + return label(LabelType.STORY, *stories) + + +def suite(suite_name): + return label(LabelType.SUITE, suite_name) + + +def parent_suite(parent_suite_name): + return label(LabelType.PARENT_SUITE, parent_suite_name) + + +def sub_suite(sub_suite_name): + return label(LabelType.SUB_SUITE, sub_suite_name) + + +def tag(*tags): + return label(LabelType.TAG, *tags) + + +def id(id): # noqa: A001,A002 + return label(LabelType.ID, id) + + +def manual(fn): + return label(LabelType.MANUAL, True)(fn) + + +def link(url, link_type=LinkType.LINK, name=None): + return safely(plugin_manager.hook.decorate_as_link(url=url, link_type=link_type, name=name)) + + +def issue(url, name=None): + return link(url, link_type=LinkType.ISSUE, name=name) + + +def testcase(url, name=None): + return link(url, link_type=LinkType.TEST_CASE, name=name) + + +class Dynamic: + + @staticmethod + def title(test_title): + plugin_manager.hook.add_title(test_title=test_title) + + @staticmethod + def description(test_description): + plugin_manager.hook.add_description(test_description=test_description) + + @staticmethod + def description_html(test_description_html): + plugin_manager.hook.add_description_html(test_description_html=test_description_html) + + @staticmethod + def label(label_type, *labels): + plugin_manager.hook.add_label(label_type=label_type, labels=labels) + + @staticmethod + def severity(severity_level): + Dynamic.label(LabelType.SEVERITY, severity_level) + + @staticmethod + def epic(*epics): + Dynamic.label(LabelType.EPIC, *epics) + + @staticmethod + def feature(*features): + Dynamic.label(LabelType.FEATURE, *features) + + @staticmethod + def story(*stories): + Dynamic.label(LabelType.STORY, *stories) + + @staticmethod + def tag(*tags): + Dynamic.label(LabelType.TAG, *tags) + + @staticmethod + def id(id): # noqa: A003,A002 + Dynamic.label(LabelType.ID, id) + + @staticmethod + def link(url, link_type=LinkType.LINK, name=None): + plugin_manager.hook.add_link(url=url, link_type=link_type, name=name) + + @staticmethod + def parameter(name, value, excluded=None, mode: ParameterMode = None): + plugin_manager.hook.add_parameter(name=name, value=value, excluded=excluded, mode=mode) + + @staticmethod + def issue(url, name=None): + Dynamic.link(url, link_type=LinkType.ISSUE, name=name) + + @staticmethod + def testcase(url, name=None): + Dynamic.link(url, link_type=LinkType.TEST_CASE, name=name) + + @staticmethod + def suite(suite_name): + Dynamic.label(LabelType.SUITE, suite_name) + + @staticmethod + def parent_suite(parent_suite_name): + Dynamic.label(LabelType.PARENT_SUITE, parent_suite_name) + + @staticmethod + def sub_suite(sub_suite_name): + Dynamic.label(LabelType.SUB_SUITE, sub_suite_name) + + @staticmethod + def manual(): + return Dynamic.label(LabelType.MANUAL, True) + + +def step(title): + if callable(title): + return StepContext(title.__name__, {})(title) + else: + return StepContext(title, {}) + + +class StepContext: + + def __init__(self, title, params): + self.title = title + self.params = params + self.uuid = uuid4() + + def __enter__(self): + plugin_manager.hook.start_step(uuid=self.uuid, title=self.title, params=self.params) + + def __exit__(self, exc_type, exc_val, exc_tb): + plugin_manager.hook.stop_step(uuid=self.uuid, title=self.title, exc_type=exc_type, exc_val=exc_val, + exc_tb=exc_tb) + + def __call__(self, func: _TFunc) -> _TFunc: + @wraps(func) + def impl(*a, **kw): + __tracebackhide__ = True + params = func_parameters(func, *a, **kw) + args = list(map(lambda x: represent(x), a)) + with StepContext(self.title.format(*args, **params), params): + return func(*a, **kw) + + return impl + + +class Attach: + + def __call__(self, body, name=None, attachment_type=None, extension=None): + plugin_manager.hook.attach_data(body=body, name=name, attachment_type=attachment_type, extension=extension) + + def file(self, source, name=None, attachment_type=None, extension=None): + plugin_manager.hook.attach_file(source=source, name=name, attachment_type=attachment_type, extension=extension) + + +attach = Attach() + + +class fixture: + def __init__(self, fixture_function, parent_uuid=None, name=None): + self._fixture_function = fixture_function + self._parent_uuid = parent_uuid + self._name = name if name else fixture_function.__name__ + self._uuid = uuid4() + self.parameters = None + + def __call__(self, *args, **kwargs): + self.parameters = func_parameters(self._fixture_function, *args, **kwargs) + + with self: + return self._fixture_function(*args, **kwargs) + + def __enter__(self): + plugin_manager.hook.start_fixture(parent_uuid=self._parent_uuid, + uuid=self._uuid, + name=self._name, + parameters=self.parameters) + + def __exit__(self, exc_type, exc_val, exc_tb): + plugin_manager.hook.stop_fixture(parent_uuid=self._parent_uuid, + uuid=self._uuid, + name=self._name, + exc_type=exc_type, + exc_val=exc_val, + exc_tb=exc_tb) + + +class test: + def __init__(self, _test, context): + self._test = _test + self._uuid = uuid4() + self.context = context + self.parameters = None + + def __call__(self, *args, **kwargs): + self.parameters = func_parameters(self._test, *args, **kwargs) + + with self: + return self._test(*args, **kwargs) + + def __enter__(self): + plugin_manager.hook.start_test(parent_uuid=None, + uuid=self._uuid, + name=None, + parameters=self.parameters, + context=self.context) + + def __exit__(self, exc_type, exc_val, exc_tb): + plugin_manager.hook.stop_test(parent_uuid=None, + uuid=self._uuid, + name=None, + context=self.context, + exc_type=exc_type, + exc_val=exc_val, + exc_tb=exc_tb) diff --git a/contrib/python/allure-python-commons/allure_commons/_core.py b/contrib/python/allure-python-commons/allure_commons/_core.py new file mode 100644 index 0000000000..40d9deafb7 --- /dev/null +++ b/contrib/python/allure-python-commons/allure_commons/_core.py @@ -0,0 +1,23 @@ +from pluggy import PluginManager +from allure_commons import _hooks + + +class MetaPluginManager(type): + _plugin_manager: PluginManager = None + + @staticmethod + def get_plugin_manager(): + if not MetaPluginManager._plugin_manager: + MetaPluginManager._plugin_manager = PluginManager('allure') + MetaPluginManager._plugin_manager.add_hookspecs(_hooks.AllureUserHooks) + MetaPluginManager._plugin_manager.add_hookspecs(_hooks.AllureDeveloperHooks) + + return MetaPluginManager._plugin_manager + + def __getattr__(cls, attr): + pm = MetaPluginManager.get_plugin_manager() + return getattr(pm, attr) + + +class plugin_manager(metaclass=MetaPluginManager): + pass diff --git a/contrib/python/allure-python-commons/allure_commons/_hooks.py b/contrib/python/allure-python-commons/allure_commons/_hooks.py new file mode 100644 index 0000000000..0ff19a27d3 --- /dev/null +++ b/contrib/python/allure-python-commons/allure_commons/_hooks.py @@ -0,0 +1,102 @@ +from pluggy import HookspecMarker, HookimplMarker + +hookspec = HookspecMarker("allure") +hookimpl = HookimplMarker("allure") + + +class AllureUserHooks: + + @hookspec + def decorate_as_title(self, test_title): + """ title """ + + @hookspec + def add_title(self, test_title): + """ title """ + + @hookspec + def decorate_as_description(self, test_description): + """ description """ + + @hookspec + def add_description(self, test_description): + """ description """ + + @hookspec + def decorate_as_description_html(self, test_description_html): + """ description html""" + + @hookspec + def add_description_html(self, test_description_html): + """ description html""" + + @hookspec + def decorate_as_label(self, label_type, labels): + """ label """ + + @hookspec + def add_label(self, label_type, labels): + """ label """ + + @hookspec + def decorate_as_link(self, url, link_type, name): + """ url """ + + @hookspec + def add_link(self, url, link_type, name): + """ url """ + + @hookspec + def add_parameter(self, name, value, excluded, mode): + """ parameter """ + + @hookspec + def start_step(self, uuid, title, params): + """ step """ + + @hookspec + def stop_step(self, uuid, exc_type, exc_val, exc_tb): + """ step """ + + @hookspec + def attach_data(self, body, name, attachment_type, extension): + """ attach data """ + + @hookspec + def attach_file(self, source, name, attachment_type, extension): + """ attach file """ + + +class AllureDeveloperHooks: + + @hookspec + def start_fixture(self, parent_uuid, uuid, name, parameters): + """ start fixture""" + + @hookspec + def stop_fixture(self, parent_uuid, uuid, name, exc_type, exc_val, exc_tb): + """ stop fixture """ + + @hookspec + def start_test(self, parent_uuid, uuid, name, parameters, context): + """ start test""" + + @hookspec + def stop_test(self, parent_uuid, uuid, name, context, exc_type, exc_val, exc_tb): + """ stop test """ + + @hookspec + def report_result(self, result): + """ reporting """ + + @hookspec + def report_container(self, container): + """ reporting """ + + @hookspec + def report_attached_file(self, source, file_name): + """ reporting """ + + @hookspec + def report_attached_data(self, body, file_name): + """ reporting """ diff --git a/contrib/python/allure-python-commons/allure_commons/lifecycle.py b/contrib/python/allure-python-commons/allure_commons/lifecycle.py new file mode 100644 index 0000000000..2e730e2e43 --- /dev/null +++ b/contrib/python/allure-python-commons/allure_commons/lifecycle.py @@ -0,0 +1,149 @@ +from collections import OrderedDict +from contextlib import contextmanager +from allure_commons._core import plugin_manager +from allure_commons.model2 import TestResultContainer +from allure_commons.model2 import TestResult +from allure_commons.model2 import Attachment, ATTACHMENT_PATTERN +from allure_commons.model2 import TestStepResult +from allure_commons.model2 import ExecutableItem +from allure_commons.model2 import TestBeforeResult +from allure_commons.model2 import TestAfterResult +from allure_commons.utils import uuid4 +from allure_commons.utils import now +from allure_commons.types import AttachmentType + + +class AllureLifecycle: + def __init__(self): + self._items = OrderedDict() + + def _get_item(self, uuid=None, item_type=None): + uuid = uuid or self._last_item_uuid(item_type=item_type) + return self._items.get(uuid) + + def _pop_item(self, uuid=None, item_type=None): + uuid = uuid or self._last_item_uuid(item_type=item_type) + return self._items.pop(uuid, None) + + def _last_item_uuid(self, item_type=None): + for uuid in reversed(self._items): + item = self._items.get(uuid) + if item_type is None: + return uuid + elif isinstance(item, item_type): + return uuid + + @contextmanager + def schedule_test_case(self, uuid=None): + test_result = TestResult() + test_result.uuid = uuid or uuid4() + self._items[test_result.uuid] = test_result + yield test_result + + @contextmanager + def update_test_case(self, uuid=None): + yield self._get_item(uuid=uuid, item_type=TestResult) + + def write_test_case(self, uuid=None): + test_result = self._pop_item(uuid=uuid, item_type=TestResult) + if test_result: + plugin_manager.hook.report_result(result=test_result) + + @contextmanager + def start_step(self, parent_uuid=None, uuid=None): + parent = self._get_item(uuid=parent_uuid, item_type=ExecutableItem) + step = TestStepResult() + step.start = now() + parent.steps.append(step) + self._items[uuid or uuid4()] = step + yield step + + @contextmanager + def update_step(self, uuid=None): + yield self._get_item(uuid=uuid, item_type=TestStepResult) + + def stop_step(self, uuid=None): + step = self._pop_item(uuid=uuid, item_type=TestStepResult) + if step and not step.stop: + step.stop = now() + + @contextmanager + def start_container(self, uuid=None): + container = TestResultContainer(uuid=uuid or uuid4()) + self._items[container.uuid] = container + yield container + + def containers(self): + for item in self._items.values(): + if isinstance(item, TestResultContainer): + yield item + + @contextmanager + def update_container(self, uuid=None): + yield self._get_item(uuid=uuid, item_type=TestResultContainer) + + def write_container(self, uuid=None): + container = self._pop_item(uuid=uuid, item_type=TestResultContainer) + if container and (container.befores or container.afters): + plugin_manager.hook.report_container(container=container) + + @contextmanager + def start_before_fixture(self, parent_uuid=None, uuid=None): + fixture = TestBeforeResult() + parent = self._get_item(uuid=parent_uuid, item_type=TestResultContainer) + if parent: + parent.befores.append(fixture) + self._items[uuid or uuid4()] = fixture + yield fixture + + @contextmanager + def update_before_fixture(self, uuid=None): + yield self._get_item(uuid=uuid, item_type=TestBeforeResult) + + def stop_before_fixture(self, uuid=None): + fixture = self._pop_item(uuid=uuid, item_type=TestBeforeResult) + if fixture and not fixture.stop: + fixture.stop = now() + + @contextmanager + def start_after_fixture(self, parent_uuid=None, uuid=None): + fixture = TestAfterResult() + parent = self._get_item(uuid=parent_uuid, item_type=TestResultContainer) + if parent: + parent.afters.append(fixture) + self._items[uuid or uuid4()] = fixture + yield fixture + + @contextmanager + def update_after_fixture(self, uuid=None): + yield self._get_item(uuid=uuid, item_type=TestAfterResult) + + def stop_after_fixture(self, uuid=None): + fixture = self._pop_item(uuid=uuid, item_type=TestAfterResult) + if fixture and not fixture.stop: + fixture.stop = now() + + def _attach(self, uuid, name=None, attachment_type=None, extension=None, parent_uuid=None): + mime_type = attachment_type + extension = extension if extension else 'attach' + + if type(attachment_type) is AttachmentType: + extension = attachment_type.extension + mime_type = attachment_type.mime_type + + file_name = ATTACHMENT_PATTERN.format(prefix=uuid, ext=extension) + attachment = Attachment(source=file_name, name=name, type=mime_type) + last_uuid = parent_uuid if parent_uuid else self._last_item_uuid(ExecutableItem) + self._items[last_uuid].attachments.append(attachment) + + return file_name + + def attach_file(self, uuid, source, name=None, attachment_type=None, extension=None, parent_uuid=None): + file_name = self._attach(uuid, name=name, attachment_type=attachment_type, + extension=extension, parent_uuid=parent_uuid) + plugin_manager.hook.report_attached_file(source=source, file_name=file_name) + + def attach_data(self, uuid, body, name=None, attachment_type=None, extension=None, parent_uuid=None): + file_name = self._attach(uuid, name=name, attachment_type=attachment_type, + extension=extension, parent_uuid=parent_uuid) + plugin_manager.hook.report_attached_data(body=body, file_name=file_name) diff --git a/contrib/python/allure-python-commons/allure_commons/logger.py b/contrib/python/allure-python-commons/allure_commons/logger.py new file mode 100644 index 0000000000..d0ac1e2491 --- /dev/null +++ b/contrib/python/allure-python-commons/allure_commons/logger.py @@ -0,0 +1,74 @@ +import io +import os +from pathlib import Path +import json +import uuid +import shutil +from attr import asdict +from allure_commons import hookimpl + +INDENT = 4 + + +class AllureFileLogger: + + def __init__(self, report_dir, clean=False): + self._report_dir = Path(report_dir).absolute() + if self._report_dir.is_dir() and clean: + shutil.rmtree(self._report_dir) + self._report_dir.mkdir(parents=True, exist_ok=True) + + def _report_item(self, item): + indent = INDENT if os.environ.get("ALLURE_INDENT_OUTPUT") else None + filename = item.file_pattern.format(prefix=uuid.uuid4()) + data = asdict(item, filter=lambda _, v: v or v is False) + with io.open(self._report_dir / filename, 'w', encoding='utf8') as json_file: + json.dump(data, json_file, indent=indent, ensure_ascii=False) + + @hookimpl + def report_result(self, result): + self._report_item(result) + + @hookimpl + def report_container(self, container): + self._report_item(container) + + @hookimpl + def report_attached_file(self, source, file_name): + destination = self._report_dir / file_name + shutil.copy2(source, destination) + + @hookimpl + def report_attached_data(self, body, file_name): + destination = self._report_dir / file_name + with open(destination, 'wb') as attached_file: + if isinstance(body, str): + attached_file.write(body.encode('utf-8')) + else: + attached_file.write(body) + + +class AllureMemoryLogger: + + def __init__(self): + self.test_cases = [] + self.test_containers = [] + self.attachments = {} + + @hookimpl + def report_result(self, result): + data = asdict(result, filter=lambda _, v: v or v is False) + self.test_cases.append(data) + + @hookimpl + def report_container(self, container): + data = asdict(container, filter=lambda _, v: v or v is False) + self.test_containers.append(data) + + @hookimpl + def report_attached_file(self, source, file_name): + self.attachments[file_name] = source + + @hookimpl + def report_attached_data(self, body, file_name): + self.attachments[file_name] = body diff --git a/contrib/python/allure-python-commons/allure_commons/mapping.py b/contrib/python/allure-python-commons/allure_commons/mapping.py new file mode 100644 index 0000000000..737d3390a4 --- /dev/null +++ b/contrib/python/allure-python-commons/allure_commons/mapping.py @@ -0,0 +1,119 @@ +from itertools import chain, islice +import attr +import re +from allure_commons.types import Severity, LabelType, LinkType +from allure_commons.types import ALLURE_UNIQUE_LABELS +from allure_commons.model2 import Label, Link + + +TAG_PREFIX = "allure" + +semi_sep = re.compile(r"allure[\.\w]+[^:=]*:") +eq_sep = re.compile(r"allure[\.\w]+[^:=]*=") + + +def allure_tag_sep(tag): + if semi_sep.search(tag): + return ":" + if eq_sep.search(tag): + return "=" + + +def __is(kind, t): + return kind in [v for k, v in t.__dict__.items() if not k.startswith('__')] + + +def parse_tag(tag, issue_pattern=None, link_pattern=None): + """ + >>> parse_tag("blocker") + Label(name='severity', value='blocker') + + >>> parse_tag("allure.issue:http://example.com/BUG-42") + Link(type='issue', url='http://example.com/BUG-42', name='http://example.com/BUG-42') + + >>> parse_tag("allure.link.home:http://qameta.io") + Link(type='link', url='http://qameta.io', name='home') + + >>> parse_tag("allure.suite:mapping") + Label(name='suite', value='mapping') + + >>> parse_tag("allure.suite:mapping") + Label(name='suite', value='mapping') + + >>> parse_tag("allure.label.owner:me") + Label(name='owner', value='me') + + >>> parse_tag("foo.label:1") + Label(name='tag', value='foo.label:1') + + >>> parse_tag("allure.foo:1") + Label(name='tag', value='allure.foo:1') + """ + sep = allure_tag_sep(tag) + schema, value = islice(chain(tag.split(sep, 1), [None]), 2) + prefix, kind, name = islice(chain(schema.split('.'), [None], [None]), 3) + + if tag in [severity for severity in Severity]: + return Label(name=LabelType.SEVERITY, value=tag) + + if prefix == TAG_PREFIX and value is not None: + + if __is(kind, LinkType): + if issue_pattern and kind == "issue" and not value.startswith("http"): + value = issue_pattern.format(value) + if link_pattern and kind == "link" and not value.startswith("http"): + value = link_pattern.format(value) + return Link(type=kind, name=name or value, url=value) + + if __is(kind, LabelType): + return Label(name=kind, value=value) + + if kind == "id": + return Label(name=LabelType.ID, value=value) + + if kind == "label" and name is not None: + return Label(name=name, value=value) + + return Label(name=LabelType.TAG, value=tag) + + +def labels_set(labels): + """ + >>> labels_set([Label(name=LabelType.SEVERITY, value=Severity.NORMAL), + ... Label(name=LabelType.SEVERITY, value=Severity.BLOCKER) + ... ]) + [Label(name='severity', value=<Severity.BLOCKER: 'blocker'>)] + + >>> labels_set([Label(name=LabelType.SEVERITY, value=Severity.NORMAL), + ... Label(name='severity', value='minor') + ... ]) + [Label(name='severity', value='minor')] + + >>> labels_set([Label(name=LabelType.EPIC, value="Epic"), + ... Label(name=LabelType.EPIC, value="Epic") + ... ]) + [Label(name='epic', value='Epic')] + + >>> labels_set([Label(name=LabelType.EPIC, value="Epic1"), + ... Label(name=LabelType.EPIC, value="Epic2") + ... ]) + [Label(name='epic', value='Epic1'), Label(name='epic', value='Epic2')] + """ + class Wl: + def __init__(self, label): + self.label = label + + def __repr__(self): + return "{name}{value}".format(**attr.asdict(self.label)) + + def __eq__(self, other): + if self.label.name in ALLURE_UNIQUE_LABELS: + return self.label.name == other.label.name + return repr(self) == repr(other) + + def __hash__(self): + if self.label.name in ALLURE_UNIQUE_LABELS: + return hash(self.label.name) + return hash(repr(self)) + + return sorted([wl.label for wl in set([Wl(label) for label in reversed(labels)])]) diff --git a/contrib/python/allure-python-commons/allure_commons/model2.py b/contrib/python/allure-python-commons/allure_commons/model2.py new file mode 100644 index 0000000000..e8fd330a0b --- /dev/null +++ b/contrib/python/allure-python-commons/allure_commons/model2.py @@ -0,0 +1,110 @@ +from attr import attrs, attrib +from attr import Factory + + +TEST_GROUP_PATTERN = "{prefix}-container.json" +TEST_CASE_PATTERN = "{prefix}-result.json" +ATTACHMENT_PATTERN = '{prefix}-attachment.{ext}' +INDENT = 4 + + +@attrs +class TestResultContainer: + file_pattern = TEST_GROUP_PATTERN + + uuid = attrib(default=None) + name = attrib(default=None) + children = attrib(default=Factory(list)) + description = attrib(default=None) + descriptionHtml = attrib(default=None) + befores = attrib(default=Factory(list)) + afters = attrib(default=Factory(list)) + links = attrib(default=Factory(list)) + start = attrib(default=None) + stop = attrib(default=None) + + +@attrs +class ExecutableItem: + name = attrib(default=None) + status = attrib(default=None) + statusDetails = attrib(default=None) + stage = attrib(default=None) + description = attrib(default=None) + descriptionHtml = attrib(default=None) + steps = attrib(default=Factory(list)) + attachments = attrib(default=Factory(list)) + parameters = attrib(default=Factory(list)) + start = attrib(default=None) + stop = attrib(default=None) + + +@attrs +class TestResult(ExecutableItem): + file_pattern = TEST_CASE_PATTERN + + uuid = attrib(default=None) + historyId = attrib(default=None) + testCaseId = attrib(default=None) + fullName = attrib(default=None) + labels = attrib(default=Factory(list)) + links = attrib(default=Factory(list)) + + +@attrs +class TestStepResult(ExecutableItem): + id = attrib(default=None) + + +@attrs +class TestBeforeResult(ExecutableItem): + pass + + +@attrs +class TestAfterResult(ExecutableItem): + pass + + +@attrs +class Parameter: + name = attrib(default=None) + value = attrib(default=None) + excluded = attrib(default=None) + mode = attrib(default=None) + + +@attrs +class Label: + name = attrib(default=None) + value = attrib(default=None) + + +@attrs +class Link: + type = attrib(default=None) + url = attrib(default=None) + name = attrib(default=None) + + +@attrs +class StatusDetails: + known = attrib(default=None) + flaky = attrib(default=None) + message = attrib(default=None) + trace = attrib(default=None) + + +@attrs +class Attachment: + name = attrib(default=None) + source = attrib(default=None) + type = attrib(default=None) + + +class Status: + FAILED = 'failed' + BROKEN = 'broken' + PASSED = 'passed' + SKIPPED = 'skipped' + UNKNOWN = 'unknown' diff --git a/contrib/python/allure-python-commons/allure_commons/reporter.py b/contrib/python/allure-python-commons/allure_commons/reporter.py new file mode 100644 index 0000000000..2e1f4a89d3 --- /dev/null +++ b/contrib/python/allure-python-commons/allure_commons/reporter.py @@ -0,0 +1,165 @@ +import threading +from collections import OrderedDict, defaultdict + +from allure_commons.types import AttachmentType +from allure_commons.model2 import ExecutableItem +from allure_commons.model2 import TestResult +from allure_commons.model2 import Attachment, ATTACHMENT_PATTERN +from allure_commons.utils import now +from allure_commons._core import plugin_manager + + +class ThreadContextItems: + + _thread_context = defaultdict(OrderedDict) + _init_thread: threading.Thread + + @property + def thread_context(self): + context = self._thread_context[threading.current_thread()] + if not context and threading.current_thread() is not self._init_thread: + uuid, last_item = next(reversed(self._thread_context[self._init_thread].items())) + context[uuid] = last_item + return context + + def __init__(self, *args, **kwargs): + self._init_thread = threading.current_thread() + super().__init__(*args, **kwargs) + + def __setitem__(self, key, value): + self.thread_context.__setitem__(key, value) + + def __getitem__(self, item): + return self.thread_context.__getitem__(item) + + def __iter__(self): + return self.thread_context.__iter__() + + def __reversed__(self): + return self.thread_context.__reversed__() + + def get(self, key): + return self.thread_context.get(key) + + def pop(self, key): + return self.thread_context.pop(key) + + def cleanup(self): + stopped_threads = [] + for thread in self._thread_context.keys(): + if not thread.is_alive(): + stopped_threads.append(thread) + for thread in stopped_threads: + del self._thread_context[thread] + + +class AllureReporter: + def __init__(self): + self._items = ThreadContextItems() + self._orphan_items = [] + + def _update_item(self, uuid, **kwargs): + item = self._items[uuid] if uuid else self._items[next(reversed(self._items))] + for name, value in kwargs.items(): + attr = getattr(item, name) + if isinstance(attr, list): + attr.append(value) + else: + setattr(item, name, value) + + def _last_executable(self): + for _uuid in reversed(self._items): + if isinstance(self._items[_uuid], ExecutableItem): + return _uuid + + def get_item(self, uuid): + return self._items.get(uuid) + + def get_last_item(self, item_type=None): + for _uuid in reversed(self._items): + if item_type is None: + return self._items.get(_uuid) + if isinstance(self._items[_uuid], item_type): + return self._items.get(_uuid) + + def start_group(self, uuid, group): + self._items[uuid] = group + + def stop_group(self, uuid, **kwargs): + self._update_item(uuid, **kwargs) + group = self._items.pop(uuid) + plugin_manager.hook.report_container(container=group) + + def update_group(self, uuid, **kwargs): + self._update_item(uuid, **kwargs) + + def start_before_fixture(self, parent_uuid, uuid, fixture): + self._items.get(parent_uuid).befores.append(fixture) + self._items[uuid] = fixture + + def stop_before_fixture(self, uuid, **kwargs): + self._update_item(uuid, **kwargs) + self._items.pop(uuid) + + def start_after_fixture(self, parent_uuid, uuid, fixture): + self._items.get(parent_uuid).afters.append(fixture) + self._items[uuid] = fixture + + def stop_after_fixture(self, uuid, **kwargs): + self._update_item(uuid, **kwargs) + fixture = self._items.pop(uuid) + fixture.stop = now() + + def schedule_test(self, uuid, test_case): + self._items[uuid] = test_case + + def get_test(self, uuid): + return self.get_item(uuid) if uuid else self.get_last_item(TestResult) + + def close_test(self, uuid): + test_case = self._items.pop(uuid) + self._items.cleanup() + plugin_manager.hook.report_result(result=test_case) + + def drop_test(self, uuid): + self._items.pop(uuid) + + def start_step(self, parent_uuid, uuid, step): + parent_uuid = parent_uuid if parent_uuid else self._last_executable() + if parent_uuid is None: + self._orphan_items.append(uuid) + else: + self._items[parent_uuid].steps.append(step) + self._items[uuid] = step + + def stop_step(self, uuid, **kwargs): + if uuid in self._orphan_items: + self._orphan_items.remove(uuid) + else: + self._update_item(uuid, **kwargs) + self._items.pop(uuid) + + def _attach(self, uuid, name=None, attachment_type=None, extension=None, parent_uuid=None): + mime_type = attachment_type + extension = extension if extension else 'attach' + + if type(attachment_type) is AttachmentType: + extension = attachment_type.extension + mime_type = attachment_type.mime_type + + file_name = ATTACHMENT_PATTERN.format(prefix=uuid, ext=extension) + attachment = Attachment(source=file_name, name=name, type=mime_type) + last_uuid = parent_uuid if parent_uuid else self._last_executable() + self._items[last_uuid].attachments.append(attachment) + + return file_name + + def attach_file(self, uuid, source, name=None, attachment_type=None, extension=None, parent_uuid=None): + file_name = self._attach(uuid, name=name, attachment_type=attachment_type, + extension=extension, parent_uuid=parent_uuid) + plugin_manager.hook.report_attached_file(source=source, file_name=file_name) + + def attach_data(self, uuid, body, name=None, attachment_type=None, extension=None, parent_uuid=None): + file_name = self._attach(uuid, name=name, attachment_type=attachment_type, + extension=extension, parent_uuid=parent_uuid) + plugin_manager.hook.report_attached_data(body=body, file_name=file_name) diff --git a/contrib/python/allure-python-commons/allure_commons/types.py b/contrib/python/allure-python-commons/allure_commons/types.py new file mode 100644 index 0000000000..06b77dfa15 --- /dev/null +++ b/contrib/python/allure-python-commons/allure_commons/types.py @@ -0,0 +1,71 @@ +from enum import Enum + +ALLURE_UNIQUE_LABELS = ['severity', 'thread', 'host'] + + +class Severity(str, Enum): + BLOCKER = 'blocker' + CRITICAL = 'critical' + NORMAL = 'normal' + MINOR = 'minor' + TRIVIAL = 'trivial' + + +class LinkType: + LINK = 'link' + ISSUE = 'issue' + TEST_CASE = 'tms' + + +class LabelType(str): + EPIC = 'epic' + FEATURE = 'feature' + STORY = 'story' + PARENT_SUITE = 'parentSuite' + SUITE = 'suite' + SUB_SUITE = 'subSuite' + SEVERITY = 'severity' + THREAD = 'thread' + HOST = 'host' + TAG = 'tag' + ID = 'as_id' + FRAMEWORK = 'framework' + LANGUAGE = 'language' + MANUAL = 'ALLURE_MANUAL' + + +class AttachmentType(Enum): + + def __init__(self, mime_type, extension): + self.mime_type = mime_type + self.extension = extension + + TEXT = ("text/plain", "txt") + CSV = ("text/csv", "csv") + TSV = ("text/tab-separated-values", "tsv") + URI_LIST = ("text/uri-list", "uri") + + HTML = ("text/html", "html") + XML = ("application/xml", "xml") + JSON = ("application/json", "json") + YAML = ("application/yaml", "yaml") + PCAP = ("application/vnd.tcpdump.pcap", "pcap") + + PNG = ("image/png", "png") + JPG = ("image/jpg", "jpg") + SVG = ("image/svg-xml", "svg") + GIF = ("image/gif", "gif") + BMP = ("image/bmp", "bmp") + TIFF = ("image/tiff", "tiff") + + MP4 = ("video/mp4", "mp4") + OGG = ("video/ogg", "ogg") + WEBM = ("video/webm", "webm") + + PDF = ("application/pdf", "pdf") + + +class ParameterMode(Enum): + HIDDEN = 'hidden' + MASKED = 'masked' + DEFAULT = None diff --git a/contrib/python/allure-python-commons/allure_commons/utils.py b/contrib/python/allure-python-commons/allure_commons/utils.py new file mode 100644 index 0000000000..5ba0d3775b --- /dev/null +++ b/contrib/python/allure-python-commons/allure_commons/utils.py @@ -0,0 +1,385 @@ +import os +import string +import sys +import time +import uuid +import json +import socket +import inspect +import hashlib +import platform +import threading +import traceback +import collections + +from traceback import format_exception_only + + +def md5(*args): + m = hashlib.md5() + for arg in args: + if not isinstance(arg, bytes): + if not isinstance(arg, str): + arg = repr(arg) + arg = arg.encode('utf-8') + m.update(arg) + return m.hexdigest() + + +def uuid4(): + return str(uuid.uuid4()) + + +def now(): + return int(round(1000 * time.time())) + + +def platform_label(): + major_version, *_ = platform.python_version_tuple() + implementation = platform.python_implementation().lower() + return f'{implementation}{major_version}' + + +def thread_tag(): + return '{0}-{1}'.format(os.getpid(), threading.current_thread().name) + + +def host_tag(): + return socket.gethostname() + + +def represent(item): + """ + >>> represent(None) + 'None' + + >>> represent(123) + '123' + + >>> represent('hi') + "'hi'" + + >>> represent('привет') + "'привет'" + + >>> represent(bytearray([0xd0, 0xbf])) # doctest: +ELLIPSIS + "<... 'bytearray'>" + + >>> from struct import pack + >>> represent(pack('h', 0x89)) + "<class 'bytes'>" + + >>> represent(int) + "<class 'int'>" + + >>> represent(represent) # doctest: +ELLIPSIS + '<function represent at ...>' + + >>> represent([represent]) # doctest: +ELLIPSIS + '[<function represent at ...>]' + + >>> class ClassWithName: + ... pass + + >>> represent(ClassWithName) + "<class 'utils.ClassWithName'>" + """ + + if isinstance(item, str): + return f"'{item}'" + elif isinstance(item, (bytes, bytearray)): + return repr(type(item)) + else: + return repr(item) + + +def func_parameters(func, *args, **kwargs): + """ + >>> def helper(func): + ... def wrapper(*args, **kwargs): + ... params = func_parameters(func, *args, **kwargs) + ... print(list(params.items())) + ... return func(*args, **kwargs) + ... return wrapper + + >>> @helper + ... def args(a, b): + ... pass + + >>> args(1, 2) + [('a', '1'), ('b', '2')] + + >>> args(*(1,2)) + [('a', '1'), ('b', '2')] + + >>> args(1, b=2) + [('a', '1'), ('b', '2')] + + >>> @helper + ... def kwargs(a=1, b=2): + ... pass + + >>> kwargs() + [('a', '1'), ('b', '2')] + + >>> kwargs(a=3, b=4) + [('a', '3'), ('b', '4')] + + >>> kwargs(b=4, a=3) + [('a', '3'), ('b', '4')] + + >>> kwargs(a=3) + [('a', '3'), ('b', '2')] + + >>> kwargs(b=4) + [('a', '1'), ('b', '4')] + + >>> @helper + ... def args_kwargs(a, b, c=3, d=4): + ... pass + + >>> args_kwargs(1, 2) + [('a', '1'), ('b', '2'), ('c', '3'), ('d', '4')] + + >>> args_kwargs(1, 2, d=5) + [('a', '1'), ('b', '2'), ('c', '3'), ('d', '5')] + + >>> args_kwargs(1, 2, 5, 6) + [('a', '1'), ('b', '2'), ('c', '5'), ('d', '6')] + + >>> args_kwargs(1, b=2) + [('a', '1'), ('b', '2'), ('c', '3'), ('d', '4')] + + >>> @helper + ... def varargs(*a): + ... pass + + >>> varargs() + [] + + >>> varargs(1, 2) + [('a', '(1, 2)')] + + >>> @helper + ... def keywords(**a): + ... pass + + >>> keywords() + [] + + >>> keywords(a=1, b=2) + [('a', '1'), ('b', '2')] + + >>> @helper + ... def args_varargs(a, b, *c): + ... pass + + >>> args_varargs(1, 2) + [('a', '1'), ('b', '2')] + + >>> args_varargs(1, 2, 2) + [('a', '1'), ('b', '2'), ('c', '(2,)')] + + >>> @helper + ... def args_kwargs_varargs(a, b, c=3, **d): + ... pass + + >>> args_kwargs_varargs(1, 2) + [('a', '1'), ('b', '2'), ('c', '3')] + + >>> args_kwargs_varargs(1, 2, 4, d=5, e=6) + [('a', '1'), ('b', '2'), ('c', '4'), ('d', '5'), ('e', '6')] + + >>> @helper + ... def args_kwargs_varargs_keywords(a, b=2, *c, **d): + ... pass + + >>> args_kwargs_varargs_keywords(1) + [('a', '1'), ('b', '2')] + + >>> args_kwargs_varargs_keywords(1, 2, 4, d=5, e=6) + [('a', '1'), ('b', '2'), ('c', '(4,)'), ('d', '5'), ('e', '6')] + + >>> class Class: + ... @staticmethod + ... @helper + ... def static_args(a, b): + ... pass + ... + ... @classmethod + ... @helper + ... def method_args(cls, a, b): + ... pass + ... + ... @helper + ... def args(self, a, b): + ... pass + + >>> cls = Class() + + >>> cls.args(1, 2) + [('a', '1'), ('b', '2')] + + >>> cls.method_args(1, 2) + [('a', '1'), ('b', '2')] + + >>> cls.static_args(1, 2) + [('a', '1'), ('b', '2')] + + """ + parameters = {} + arg_spec = inspect.getfullargspec(func) + arg_order = list(arg_spec.args) + args_dict = dict(zip(arg_spec.args, args)) + + if arg_spec.defaults: + kwargs_defaults_dict = dict(zip(arg_spec.args[-len(arg_spec.defaults):], arg_spec.defaults)) + parameters.update(kwargs_defaults_dict) + + if arg_spec.varargs: + arg_order.append(arg_spec.varargs) + varargs = args[len(arg_spec.args):] + parameters.update({arg_spec.varargs: varargs} if varargs else {}) + + if arg_spec.args and arg_spec.args[0] in ['cls', 'self']: + args_dict.pop(arg_spec.args[0], None) + + if kwargs: + if sys.version_info < (3, 7): + # Sort alphabetically as old python versions does + # not preserve call order for kwargs. + arg_order.extend(sorted(list(kwargs.keys()))) + else: + # Keep py3.7 behaviour to preserve kwargs order + arg_order.extend(list(kwargs.keys())) + parameters.update(kwargs) + + parameters.update(args_dict) + + items = parameters.items() + sorted_items = sorted( + map( + lambda kv: (kv[0], represent(kv[1])), + items + ), + key=lambda x: arg_order.index(x[0]) + ) + + return collections.OrderedDict(sorted_items) + + +def format_traceback(exc_traceback): + return ''.join(traceback.format_tb(exc_traceback)) if exc_traceback else None + + +def format_exception(etype, value): + """ + >>> import sys + + >>> try: + ... assert False, 'Привет' + ... except AssertionError: + ... etype, e, _ = sys.exc_info() + ... format_exception(etype, e) # doctest: +ELLIPSIS + 'AssertionError: ...\\n' + + >>> try: + ... assert False, 'Привет' + ... except AssertionError: + ... etype, e, _ = sys.exc_info() + ... format_exception(etype, e) # doctest: +ELLIPSIS + 'AssertionError: ...\\n' + + >>> try: + ... compile("bla 'Привет'", "fake.py", "exec") + ... except SyntaxError: + ... etype, e, _ = sys.exc_info() + ... format_exception(etype, e) # doctest: +ELLIPSIS + ' File "fake.py", line 1...SyntaxError: invalid syntax\\n' + + >>> try: + ... compile("bla 'Привет'", "fake.py", "exec") + ... except SyntaxError: + ... etype, e, _ = sys.exc_info() + ... format_exception(etype, e) # doctest: +ELLIPSIS + ' File "fake.py", line 1...SyntaxError: invalid syntax\\n' + + >>> from hamcrest import assert_that, equal_to + + >>> try: + ... assert_that('left', equal_to('right')) + ... except AssertionError: + ... etype, e, _ = sys.exc_info() + ... format_exception(etype, e) # doctest: +ELLIPSIS + "AssertionError: \\nExpected:...but:..." + + >>> try: + ... assert_that('left', equal_to('right')) + ... except AssertionError: + ... etype, e, _ = sys.exc_info() + ... format_exception(etype, e) # doctest: +ELLIPSIS + "AssertionError: \\nExpected:...but:..." + """ + return '\n'.join(format_exception_only(etype, value)) if etype or value else None + + +def get_testplan(): + planned_tests = [] + file_path = os.environ.get("ALLURE_TESTPLAN_PATH") + + if file_path and os.path.exists(file_path): + with open(file_path, 'r') as plan_file: + plan = json.load(plan_file) + planned_tests = plan.get("tests", []) + + return planned_tests + + +class SafeFormatter(string.Formatter): + """ + Format string safely - skip any non-passed keys + >>> f = SafeFormatter().format + + Make sure we don't broke default formatting behaviour + >>> f("literal string") + 'literal string' + >>> f("{expected.format}", expected=str) + "<method 'format' of 'str' objects>" + >>> f("{expected[0]}", expected=["value"]) + 'value' + >>> f("{expected[0]}", expected=123) + Traceback (most recent call last): + ... + TypeError: 'int' object is not subscriptable + >>> f("{expected[0]}", expected=[]) + Traceback (most recent call last): + ... + IndexError: list index out of range + >>> f("{expected.format}", expected=int) + Traceback (most recent call last): + ... + AttributeError: type object 'int' has no attribute 'format' + + Check that unexpected keys do not cause some errors + >>> f("{expected} {unexpected}", expected="value") + 'value {unexpected}' + >>> f("{unexpected[0]}", expected=["value"]) + '{unexpected[0]}' + >>> f("{unexpected.format}", expected=str) + '{unexpected.format}' + """ + + class SafeKeyOrIndexError(Exception): + pass + + def get_field(self, field_name, args, kwargs): + try: + return super().get_field(field_name, args, kwargs) + except self.SafeKeyOrIndexError: + return "{" + field_name + "}", field_name + + def get_value(self, key, args, kwargs): + try: + return super().get_value(key, args, kwargs) + except (KeyError, IndexError): + raise self.SafeKeyOrIndexError() diff --git a/contrib/python/allure-python-commons/ya.make b/contrib/python/allure-python-commons/ya.make new file mode 100644 index 0000000000..7f3ae9bda7 --- /dev/null +++ b/contrib/python/allure-python-commons/ya.make @@ -0,0 +1,38 @@ +# Generated by devtools/yamaker (pypi). + +PY3_LIBRARY() + +VERSION(2.13.5) + +LICENSE(Apache-2.0) + +PEERDIR( + contrib/python/attrs + contrib/python/pluggy +) + +NO_LINT() + +PY_SRCS( + TOP_LEVEL + allure.py + allure_commons/__init__.py + allure_commons/_allure.py + allure_commons/_core.py + allure_commons/_hooks.py + allure_commons/lifecycle.py + allure_commons/logger.py + allure_commons/mapping.py + allure_commons/model2.py + allure_commons/reporter.py + allure_commons/types.py + allure_commons/utils.py +) + +RESOURCE_FILES( + PREFIX contrib/python/allure-python-commons/ + .dist-info/METADATA + .dist-info/top_level.txt +) + +END() |