diff options
author | iddqd <iddqd@yandex-team.com> | 2024-05-13 17:19:30 +0300 |
---|---|---|
committer | iddqd <iddqd@yandex-team.com> | 2024-05-13 17:28:44 +0300 |
commit | 84d127b9b7e96ba4352e3f5ddc9222aee9a66053 (patch) | |
tree | 2ebb2689abf65e68dfe92a3ca9b161b4b6ae183f /contrib/python/allure-python-commons/allure_commons | |
parent | b7deb7f0b71db7419781d1b0357dfa443ccc3ff1 (diff) | |
download | ydb-84d127b9b7e96ba4352e3f5ddc9222aee9a66053.tar.gz |
Add allure support to ydb github export
d6cba27d09fb5e50a99c36070a6a3545c8393ea1
Diffstat (limited to 'contrib/python/allure-python-commons/allure_commons')
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() |