aboutsummaryrefslogtreecommitdiffstats
path: root/contrib/python/allure-python-commons/allure_commons
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 /contrib/python/allure-python-commons/allure_commons
parentb7deb7f0b71db7419781d1b0357dfa443ccc3ff1 (diff)
downloadydb-84d127b9b7e96ba4352e3f5ddc9222aee9a66053.tar.gz
Add allure support to ydb github export
d6cba27d09fb5e50a99c36070a6a3545c8393ea1
Diffstat (limited to 'contrib/python/allure-python-commons/allure_commons')
-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
11 files changed, 1475 insertions, 0 deletions
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()