diff --git a/allure-pytest/src/utils.py b/allure-pytest/src/utils.py index 93467792..9a7798e5 100644 --- a/allure-pytest/src/utils.py +++ b/allure-pytest/src/utils.py @@ -4,7 +4,7 @@ import six import pytest from itertools import chain, islice -from allure_commons.utils import represent +from allure_commons.utils import represent, SafeFormatter from allure_commons.utils import format_exception, format_traceback, escape_non_unicode_symbols from allure_commons.model2 import Status from allure_commons.model2 import StatusDetails @@ -111,7 +111,7 @@ def allure_package(item): def allure_name(item, parameters): name = escape_name(item.name) title = allure_title(item) - return title.format(**{**parameters, **item.funcargs}) if title else name + return SafeFormatter().format(title, **{**parameters, **item.funcargs}) if title else name def allure_full_name(item): diff --git a/allure-pytest/test/acceptance/display_name/display_name_test.py b/allure-pytest/test/acceptance/display_name/display_name_test.py index b8528637..370259a9 100644 --- a/allure-pytest/test/acceptance/display_name/display_name_test.py +++ b/allure-pytest/test/acceptance/display_name/display_name_test.py @@ -97,3 +97,24 @@ def test_feature_label_for_titled_test(): has_title("Titled test with features") ) ) + + +def test_failed_fixture_value_in_display_name(executed_docstring_source): + """ + >>> import allure + >>> import pytest + + >>> @pytest.fixture + ... def fix(): + ... raise AssertionError("Fixture failed for some reason") + + >>> @allure.title('title with {fix}') + ... def test_fixture_value_name(fix): + ... pass + """ + + assert_that(executed_docstring_source.allure_report, + has_test_case("test_fixture_value_name", + has_title("title with {fix}") + ) + ) diff --git a/allure-python-commons/src/utils.py b/allure-python-commons/src/utils.py index 47278c05..503a47ec 100644 --- a/allure-python-commons/src/utils.py +++ b/allure-python-commons/src/utils.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import os +import string import sys import six import time @@ -404,3 +405,53 @@ def get_testplan(): 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) + "" + >>> 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()