From dec8adde11da26952a1c8baadf4caa830fee253e Mon Sep 17 00:00:00 2001 From: Maksim Stepanov <17935127+delatrie@users.noreply.github.com> Date: Wed, 9 Jul 2025 20:57:35 +0700 Subject: [PATCH 1/3] test(commons): add fullName matcher --- allure-python-commons-test/src/result.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/allure-python-commons-test/src/result.py b/allure-python-commons-test/src/result.py index 84bb6094..ee97cbf9 100644 --- a/allure-python-commons-test/src/result.py +++ b/allure-python-commons-test/src/result.py @@ -224,3 +224,7 @@ def with_mode(mode): def has_history_id(matcher=None): return has_entry('historyId', matcher or anything()) + + +def has_full_name(matcher): + return has_entry("fullName", matcher) From 25a0292fcb01cb2b7de636e5f5bfeabf75f3b1c6 Mon Sep 17 00:00:00 2001 From: Maksim Stepanov <17935127+delatrie@users.noreply.github.com> Date: Wed, 9 Jul 2025 20:58:09 +0700 Subject: [PATCH 2/3] test(pytest): add tests to reproduce #868 --- tests/allure_pytest/defects/issue868_test.py | 47 ++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 tests/allure_pytest/defects/issue868_test.py diff --git a/tests/allure_pytest/defects/issue868_test.py b/tests/allure_pytest/defects/issue868_test.py new file mode 100644 index 00000000..82db7689 --- /dev/null +++ b/tests/allure_pytest/defects/issue868_test.py @@ -0,0 +1,47 @@ +import allure +from hamcrest import assert_that, is_not +from tests.allure_pytest.pytest_runner import AllurePytestRunner + +from allure_commons_test.report import has_test_case +from allure_commons_test.result import has_full_name +from allure_commons_test.label import has_sub_suite + + +@allure.issue("868", name="Issue 868") +def test_nested_class_affects_fullname_and_subsuite(allure_pytest_runner: AllurePytestRunner): + """ + >>> class TestFoo: + ... class TestBar: + ... def test_bar(self): + ... pass + """ + + allure_results = allure_pytest_runner.run_docstring(filename="foo_test.py") + + assert_that( + allure_results, + has_test_case( + "test_bar", + has_full_name("foo_test.TestFoo.TestBar#test_bar"), + has_sub_suite("TestFoo > TestBar"), + ), + ) + + +@allure.issue("868", name="Issue 868") +def test_nested_class_affects_testcaseid_and_historyid(allure_pytest_runner: AllurePytestRunner): + """ + >>> class TestFoo: + ... class TestFoo: + ... def test_foo(self): + ... pass + ... def test_foo(self): + ... pass + """ + + allure_results = allure_pytest_runner.run_docstring(filename="foo_test.py") + test_case_id1, test_case_id2 = [tc["testCaseId"] for tc in allure_results.test_cases] + history_id1, history_id2 = [tc["historyId"] for tc in allure_results.test_cases] + + assert_that(test_case_id1, is_not(test_case_id2)) + assert_that(history_id1, is_not(history_id2)) From 1d385e4fb05b24e63d326344a99cb45d45b7a055 Mon Sep 17 00:00:00 2001 From: Maksim Stepanov <17935127+delatrie@users.noreply.github.com> Date: Wed, 9 Jul 2025 21:15:52 +0700 Subject: [PATCH 3/3] fix(pytest): nested casses break some props --- allure-pytest/src/utils.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/allure-pytest/src/utils.py b/allure-pytest/src/utils.py index 19145510..e90df9c1 100644 --- a/allure-pytest/src/utils.py +++ b/allure-pytest/src/utils.py @@ -1,5 +1,5 @@ import pytest -from itertools import chain, islice +from itertools import chain, islice, repeat from allure_commons.utils import SafeFormatter, md5 from allure_commons.utils import format_exception, format_traceback from allure_commons.model2 import Status @@ -123,19 +123,29 @@ def allure_name(item, parameters, param_id=None): def allure_full_name(item: pytest.Item): package = allure_package(item) - class_name = f".{item.parent.name}" if isinstance(item.parent, pytest.Class) else '' + class_names = item.nodeid.split("::")[1:-1] + class_part = ("." + ".".join(class_names)) if class_names else "" test = item.originalname if isinstance(item, pytest.Function) else item.name.split("[")[0] - full_name = f'{package}{class_name}#{test}' + full_name = f'{package}{class_part}#{test}' return full_name +def ensure_len(value, min_length, fill_value=None): + yield from value + yield from repeat(fill_value, min_length - len(value)) + + def allure_suite_labels(item): - head, possibly_clazz, tail = islice(chain(item.nodeid.split('::'), [None], [None]), 3) - clazz = possibly_clazz if tail else None + head, *class_names, _ = ensure_len(item.nodeid.split("::"), 2) 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])) + pairs = dict( + zip( + [LabelType.PARENT_SUITE, LabelType.SUITE, LabelType.SUB_SUITE], + [package, module, " > ".join(class_names)], + ), + ) labels = dict(allure_labels(item)) default_suite_labels = [] for label, value in pairs.items():