aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoriddqd <iddqd@yandex-team.com>2024-05-13 17:19:30 +0300
committeriddqd <iddqd@yandex-team.com>2024-05-13 17:28:44 +0300
commit84d127b9b7e96ba4352e3f5ddc9222aee9a66053 (patch)
tree2ebb2689abf65e68dfe92a3ca9b161b4b6ae183f
parentb7deb7f0b71db7419781d1b0357dfa443ccc3ff1 (diff)
downloadydb-84d127b9b7e96ba4352e3f5ddc9222aee9a66053.tar.gz
Add allure support to ydb github export
d6cba27d09fb5e50a99c36070a6a3545c8393ea1
-rw-r--r--contrib/python/allure-pytest/.dist-info/METADATA58
-rw-r--r--contrib/python/allure-pytest/.dist-info/entry_points.txt2
-rw-r--r--contrib/python/allure-pytest/.dist-info/top_level.txt1
-rw-r--r--contrib/python/allure-pytest/README.md29
-rw-r--r--contrib/python/allure-pytest/allure_pytest/__init__.py0
-rw-r--r--contrib/python/allure-pytest/allure_pytest/compat.py34
-rw-r--r--contrib/python/allure-pytest/allure_pytest/helper.py47
-rw-r--r--contrib/python/allure-pytest/allure_pytest/listener.py364
-rw-r--r--contrib/python/allure-pytest/allure_pytest/plugin.py236
-rw-r--r--contrib/python/allure-pytest/allure_pytest/utils.py197
-rw-r--r--contrib/python/allure-pytest/ya.make33
-rw-r--r--contrib/python/allure-python-commons/.dist-info/METADATA140
-rw-r--r--contrib/python/allure-python-commons/.dist-info/top_level.txt2
-rw-r--r--contrib/python/allure-python-commons/README.md112
-rw-r--r--contrib/python/allure-python-commons/allure.py43
-rw-r--r--contrib/python/allure-python-commons/allure_commons/__init__.py12
-rw-r--r--contrib/python/allure-python-commons/allure_commons/_allure.py265
-rw-r--r--contrib/python/allure-python-commons/allure_commons/_core.py23
-rw-r--r--contrib/python/allure-python-commons/allure_commons/_hooks.py102
-rw-r--r--contrib/python/allure-python-commons/allure_commons/lifecycle.py149
-rw-r--r--contrib/python/allure-python-commons/allure_commons/logger.py74
-rw-r--r--contrib/python/allure-python-commons/allure_commons/mapping.py119
-rw-r--r--contrib/python/allure-python-commons/allure_commons/model2.py110
-rw-r--r--contrib/python/allure-python-commons/allure_commons/reporter.py165
-rw-r--r--contrib/python/allure-python-commons/allure_commons/types.py71
-rw-r--r--contrib/python/allure-python-commons/allure_commons/utils.py385
-rw-r--r--contrib/python/allure-python-commons/ya.make38
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()