From 68bbafe270a4656ecf5c28f09b9975d14f495c24 Mon Sep 17 00:00:00 2001 From: Denis Sharov Date: Thu, 24 Apr 2025 11:20:34 +0400 Subject: [PATCH 1/6] Set the correct detail message for passed steps (fix #835) (#853) --- .../src/listener/allure_listener.py | 22 +++--- .../statuses/statuses_test.py | 70 +++++++++++++++++++ 2 files changed, 84 insertions(+), 8 deletions(-) diff --git a/allure-robotframework/src/listener/allure_listener.py b/allure-robotframework/src/listener/allure_listener.py index 7c8bccfa..2a4d3a80 100644 --- a/allure-robotframework/src/listener/allure_listener.py +++ b/allure-robotframework/src/listener/allure_listener.py @@ -106,9 +106,10 @@ def start_before_fixture(self, name): fixture.name = name def stop_before_fixture(self, attributes, messages): - self._report_messages(messages) + status = attributes.get('status') + self._report_messages(status, messages) with self.lifecycle.update_before_fixture() as fixture: - fixture.status = get_allure_status(attributes.get('status')) + fixture.status = get_allure_status(status) fixture.statusDetails = StatusDetails(message=self._current_msg, trace=self._current_tb) self.lifecycle.stop_before_fixture() @@ -117,9 +118,10 @@ def start_after_fixture(self, name): fixture.name = name def stop_after_fixture(self, attributes, messages): - self._report_messages(messages) + status = attributes.get('status') + self._report_messages(status, messages) with self.lifecycle.update_after_fixture() as fixture: - fixture.status = get_allure_status(attributes.get('status')) + fixture.status = get_allure_status(status) fixture.statusDetails = StatusDetails(message=self._current_msg, trace=self._current_tb) self.lifecycle.stop_after_fixture() @@ -136,7 +138,7 @@ def start_test(self, name, attributes): container.children.append(uuid) def stop_test(self, _, attributes, messages): - self._report_messages(messages) + self._report_messages(attributes.get('status'), messages) if 'skipped' in [tag.lower() for tag in attributes['tags']]: attributes['status'] = RobotStatus.SKIPPED @@ -168,17 +170,21 @@ def start_keyword(self, name): step.name = name def stop_keyword(self, attributes, messages): - self._report_messages(messages) + status = attributes.get('status') + self._report_messages(status, messages) with self.lifecycle.update_step() as step: - step.status = get_allure_status(attributes.get('status')) + step.status = get_allure_status(status) step.parameters = get_allure_parameters(attributes.get('args')) step.statusDetails = StatusDetails(message=self._current_msg, trace=self._current_tb) self.lifecycle.stop_step() - def _report_messages(self, messages): + def _report_messages(self, status, messages): has_trace = BuiltIn().get_variable_value("${LOG LEVEL}") in (RobotLogLevel.DEBUG, RobotLogLevel.TRACE) attachment = "" + if status == RobotStatus.PASSED: + self._current_tb, self._current_msg = None, None + for message, next_message in zip_longest(messages, messages[1:]): name = message.get('message') level = message.get('level') diff --git a/tests/allure_robotframework/acceptance/robotframework_support/statuses/statuses_test.py b/tests/allure_robotframework/acceptance/robotframework_support/statuses/statuses_test.py index 2f65fd48..51d06aef 100644 --- a/tests/allure_robotframework/acceptance/robotframework_support/statuses/statuses_test.py +++ b/tests/allure_robotframework/acceptance/robotframework_support/statuses/statuses_test.py @@ -99,3 +99,73 @@ def test_steps_after_failed_are_skipped(docstring, robot_runner: AllureRobotRunn ) ) ) + + +def test_only_failed_steps_have_status_details(docstring, robot_runner: AllureRobotRunner): + """ + *** Variables *** + @{TEST_VALUES} 0 5 15 + + *** Test Cases *** + Test Case with mixed step results and status details + FOR ${value} IN @{TEST_VALUES} + Run Keyword And Ignore Error Should Be True ${value} > 10 + END + Log To Console Test message + """ + + robot_runner.run_robotframework( + suite_literals={"status.robot": docstring} + ) + + assert_that( + robot_runner.allure_results, + has_test_case( + "Test Case with mixed step results and status details", + has_step( + "${value} IN @{TEST_VALUES}", + has_step( + "${value} = 0", + has_step( + "BuiltIn.Run Keyword And Ignore Error", + has_step( + "BuiltIn.Should Be True", + with_status("failed"), + has_status_details( + with_message_contains("0 > 10' should be true."), + ) + ), + ), + ), + has_step( + "${value} = 5", + has_step( + "BuiltIn.Run Keyword And Ignore Error", + has_step( + "BuiltIn.Should Be True", + with_status("failed"), + has_status_details( + with_message_contains("5 > 10' should be true."), + ) + ), + ), + ), + has_step( + "${value} = 15", + has_step( + "BuiltIn.Run Keyword And Ignore Error", + has_step( + "BuiltIn.Should Be True", + with_status("passed"), + has_status_details({}) + ), + ), + ) + ), + has_step( + "BuiltIn.Log To Console", + with_status("passed"), + has_status_details({}) + ) + ) + ) From 3cd731aa258a95e159982e6c1ecf3b0f57508117 Mon Sep 17 00:00:00 2001 From: Maksim Stepanov <17935127+delatrie@users.noreply.github.com> Date: Tue, 6 May 2025 18:05:12 +0700 Subject: [PATCH 2/6] fix(behave): add cleanup; fix invalid call from AllureHooks (#860) --- allure-behave/src/formatter.py | 13 ++++- allure-behave/src/hooks.py | 17 ++++-- tests/allure_behave/behave_runner.py | 54 +++++++++++--------- tests/allure_behave/defects/issue858_test.py | 38 ++++++++++++++ 4 files changed, 93 insertions(+), 29 deletions(-) create mode 100644 tests/allure_behave/defects/issue858_test.py diff --git a/allure-behave/src/formatter.py b/allure-behave/src/formatter.py index d2c53922..febd2d96 100644 --- a/allure-behave/src/formatter.py +++ b/allure-behave/src/formatter.py @@ -12,10 +12,10 @@ def __init__(self, stream_opener, config): super(AllureFormatter, self).__init__(stream_opener, config) self.listener = AllureListener(config) - file_logger = AllureFileLogger(self.stream_opener.name) + self.file_logger = AllureFileLogger(self.stream_opener.name) allure_commons.plugin_manager.register(self.listener) - allure_commons.plugin_manager.register(file_logger) + allure_commons.plugin_manager.register(self.file_logger) self.testplan = get_testplan() @@ -45,5 +45,14 @@ def result(self, result): def eof(self): self.listener.stop_feature() + def close(self): + try: + super().close() + finally: + for plugin in [self.file_logger, self.listener]: + name = allure_commons.plugin_manager.get_name(plugin) + if allure_commons.plugin_manager.has_plugin(name): + allure_commons.plugin_manager.unregister(name=name) + def close_stream(self): self.listener.stop_session() diff --git a/allure-behave/src/hooks.py b/allure-behave/src/hooks.py index 86427523..7a153020 100644 --- a/allure-behave/src/hooks.py +++ b/allure-behave/src/hooks.py @@ -6,6 +6,7 @@ from behave.configuration import Configuration HOOKS = [ + "after_all", "before_feature", "after_feature", "before_scenario", @@ -42,15 +43,25 @@ def allure_report(result_dir="allure_results"): class AllureHooks: def __init__(self, result_dir): self.listener = AllureListener(Configuration()) + self.plugins = [] if not hasattr(_storage, 'file_logger'): - _storage.file_logger = AllureFileLogger(result_dir) - allure_commons.plugin_manager.register(_storage.file_logger) + logger = AllureFileLogger(result_dir) + _storage.file_logger = logger + allure_commons.plugin_manager.register(logger) + self.plugins.append(logger) allure_commons.plugin_manager.register(self.listener) + self.plugins.append(self.listener) + + def after_all(self, context): + for plugin in self.plugins: + name = allure_commons.plugin_manager.get_name(plugin) + if allure_commons.plugin_manager.has_plugin(name): + allure_commons.plugin_manager.unregister(name=name) def before_feature(self, context, feature): - self.listener.start_feature() + self.listener.start_file() def after_feature(self, context, feature): self.listener.stop_feature() diff --git a/tests/allure_behave/behave_runner.py b/tests/allure_behave/behave_runner.py index defb1849..da49d738 100644 --- a/tests/allure_behave/behave_runner.py +++ b/tests/allure_behave/behave_runner.py @@ -1,3 +1,4 @@ +from contextlib import contextmanager import behave.step_registry import sys @@ -16,7 +17,8 @@ from allure_behave.formatter import AllureFormatter -def __fix_behave_in_memory_run(): +@contextmanager +def _fixed_in_memory_run(): # Behave has poor support for consecutive prigrammatic runs. This is due to # how step decorators are cached. # There are three ways to introduce behave step decorators (i.e., @given) @@ -50,6 +52,10 @@ def __fixed_add_step_definition(self, *args, **kwargs): StepRegistry.add_step_definition = __fixed_add_step_definition + yield + + StepRegistry.add_step_definition = original_add_step_definition + class _InMemoryBehaveRunner(Runner): def __init__(self, features, steps, environment, args=None): @@ -159,32 +165,32 @@ def run_behave( :attr:`allure_results` attribute. """ - return self._run( - self._get_all_content( - paths=feature_paths, - literals=feature_literals, - rst_ids=feature_rst_ids - ), - self._get_all_content( - paths=step_paths, - literals=step_literals, - rst_ids=step_rst_ids - ), - self._resolve_content( - path=environment_path, - literal=environment_literal, - rst_id=environment_rst_id - ), - testplan_content=testplan_content, - testplan_path=testplan_path, - testplan_rst_id=testplan_rst_id, - options=options - ) + + with _fixed_in_memory_run(): + return self._run( + self._get_all_content( + paths=feature_paths, + literals=feature_literals, + rst_ids=feature_rst_ids + ), + self._get_all_content( + paths=step_paths, + literals=step_literals, + rst_ids=step_rst_ids + ), + self._resolve_content( + path=environment_path, + literal=environment_literal, + rst_id=environment_rst_id + ), + testplan_content=testplan_content, + testplan_path=testplan_path, + testplan_rst_id=testplan_rst_id, + options=options + ) def _run_framework(self, features, steps, environment, options): _InMemoryBehaveRunner(features, steps, environment, options).run() -__fix_behave_in_memory_run() - __all__ = ["AllureBehaveRunner"] diff --git a/tests/allure_behave/defects/issue858_test.py b/tests/allure_behave/defects/issue858_test.py new file mode 100644 index 00000000..80c3168b --- /dev/null +++ b/tests/allure_behave/defects/issue858_test.py @@ -0,0 +1,38 @@ +import allure +import shlex + +from tests.allure_behave.behave_runner import AllureBehaveRunner +from ...e2e import allure_file_context + +from behave import __main__ as runner + + +@allure.issue("858") +def test_test_results_leak(behave_runner: AllureBehaveRunner): + feature_path = behave_runner.pytester.makefile( + ".feature", + ( + """ + Feature: Foo + Scenario: Bar + Given baz + """ + ), + ) + behave_runner.pytester.makefile( + ".py", + **{"steps/steps": "given('baz')(lambda *_: None)"}, + ) + + args = shlex.join([ + feature_path.name, + "-f", "allure_behave.formatter:AllureFormatter", + "-o", "allure-results", + "--no-summary", + ]) + + with allure_file_context("allure-results") as context: + runner.main(args) + runner.main(args) + + assert len(context.allure_results.test_cases) == 2 From 9d12890b445b2c0ebefc3fa2a1ad60b6f72e4b72 Mon Sep 17 00:00:00 2001 From: Maksim Stepanov <17935127+delatrie@users.noreply.github.com> Date: Mon, 9 Jun 2025 21:20:45 +0700 Subject: [PATCH 3/6] fix(pytest): can't change a fixture's title in pytest 8.4 or later (#866) --- allure-pytest/src/helper.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/allure-pytest/src/helper.py b/allure-pytest/src/helper.py index e6944ef4..a9df4b7e 100644 --- a/allure-pytest/src/helper.py +++ b/allure-pytest/src/helper.py @@ -10,7 +10,9 @@ class AllureTitleHelper: def decorate_as_title(self, test_title): def decorator(func): # pytest.fixture wraps function, so we need to get it directly - if getattr(func, '__pytest_wrapped__', None): + if hasattr(func, "_get_wrapped_function"): # pytest >= 8.4 + function = func._get_wrapped_function() + elif hasattr(func, "__pytest_wrapped__"): # pytest < 8.4 function = func.__pytest_wrapped__.obj else: function = func From 4d67928965bd01ed22cc13019f52942b5c10328b Mon Sep 17 00:00:00 2001 From: Denis Sharov Date: Mon, 9 Jun 2025 18:24:05 +0400 Subject: [PATCH 4/6] Don't convert built-in (reserved) pytest markers to allure tags (fix #817) (#862) --- allure-pytest/src/utils.py | 36 ++--- .../acceptance/label/tag/tag_test.py | 148 ++++++++++++------ .../pytest_rerunfailures_test.py | 4 +- 3 files changed, 120 insertions(+), 68 deletions(-) diff --git a/allure-pytest/src/utils.py b/allure-pytest/src/utils.py index 1e07cb49..19145510 100644 --- a/allure-pytest/src/utils.py +++ b/allure-pytest/src/utils.py @@ -1,6 +1,6 @@ import pytest from itertools import chain, islice -from allure_commons.utils import represent, SafeFormatter, md5 +from allure_commons.utils import SafeFormatter, md5 from allure_commons.utils import format_exception, format_traceback from allure_commons.model2 import Status from allure_commons.model2 import StatusDetails @@ -20,6 +20,15 @@ LabelType.SUB_SUITE ] +MARK_NAMES_TO_IGNORE = { + "usefixtures", + "filterwarnings", + "skip", + "skipif", + "xfail", + "parametrize", +} + def get_marker_value(item, keyword): marker = item.get_closest_marker(keyword) @@ -81,27 +90,14 @@ def format_allure_link(config, url, link_type): def pytest_markers(item): - for keyword in item.keywords.keys(): - if any([keyword.startswith('allure_'), keyword == 'parametrize']): - continue - marker = item.get_closest_marker(keyword) - if marker is None: - continue - - yield mark_to_str(marker) + for mark in item.iter_markers(): + if should_convert_mark_to_tag(mark): + yield mark.name -def mark_to_str(marker): - args = [represent(arg) for arg in marker.args] - kwargs = [f'{key}={represent(value)}' for key, value in marker.kwargs.items()] - if marker.name in ('filterwarnings', 'skip', 'skipif', 'xfail', 'usefixtures', 'tryfirst', 'trylast'): - markstr = f'@pytest.mark.{marker.name}' - else: - markstr = str(marker.name) - if args or kwargs: - parameters = ', '.join(args + kwargs) - markstr = f'{markstr}({parameters})' - return markstr +def should_convert_mark_to_tag(mark): + return mark.name not in MARK_NAMES_TO_IGNORE and \ + not mark.args and not mark.kwargs def allure_package(item): diff --git a/tests/allure_pytest/acceptance/label/tag/tag_test.py b/tests/allure_pytest/acceptance/label/tag/tag_test.py index 3f32475b..a738628b 100644 --- a/tests/allure_pytest/acceptance/label/tag/tag_test.py +++ b/tests/allure_pytest/acceptance/label/tag/tag_test.py @@ -1,17 +1,19 @@ -from hamcrest import assert_that, not_ +from hamcrest import assert_that, not_, anything from tests.allure_pytest.pytest_runner import AllurePytestRunner from allure_commons_test.report import has_test_case from allure_commons_test.label import has_tag -def test_pytest_marker(allure_pytest_runner: AllurePytestRunner): +def test_pytest_simple_markers_are_converted_to_allure_tags( + allure_pytest_runner: AllurePytestRunner +): """ >>> import pytest >>> @pytest.mark.cool ... @pytest.mark.stuff - ... def test_pytest_marker_example(): + ... def test_pytest_simple_markers_are_converted_to_allure_tags_example(): ... pass """ @@ -20,25 +22,21 @@ def test_pytest_marker(allure_pytest_runner: AllurePytestRunner): assert_that( allure_results, has_test_case( - "test_pytest_marker_example", + "test_pytest_simple_markers_are_converted_to_allure_tags_example", has_tag("cool"), has_tag("stuff") ) ) -def test_show_reserved_pytest_markers_full_decorator( - allure_pytest_runner: AllurePytestRunner +def test_pytest_marker_with_args_is_not_converted_to_allure_tag( + allure_pytest_runner: AllurePytestRunner ): """ >>> import pytest - >>> @pytest.mark.usermark1 - ... @pytest.mark.usermark2 - ... @pytest.mark.parametrize("param", ["foo"]) - ... @pytest.mark.skipif(False, reason="reason2") - ... @pytest.mark.skipif(False, reason="reason1") - ... def test_show_reserved_pytest_markers_full_decorator_example(param): + >>> @pytest.mark.marker('cool', 'stuff') + ... def test_pytest_marker_with_args_is_not_converted_to_allure_tag_example(): ... pass """ @@ -47,26 +45,46 @@ def test_show_reserved_pytest_markers_full_decorator( assert_that( allure_results, has_test_case( - "test_show_reserved_pytest_markers_full_decorator_example[foo]", - has_tag("usermark1"), - has_tag("usermark2"), - has_tag("@pytest.mark.skipif(False, reason='reason1')"), + "test_pytest_marker_with_args_is_not_converted_to_allure_tag_example", not_( - has_tag("@pytest.mark.skipif(False, reason='reason2')") - ), + has_tag(anything()) + ) + ) + ) + + +def test_pytest_marker_with_kwargs_is_not_converted_to_allure_tag( + allure_pytest_runner: AllurePytestRunner +): + """ + >>> import pytest + + >>> @pytest.mark.marker(stuff='cool') + ... def test_pytest_marker_with_kwargs_is_not_converted_to_allure_tag_example(): + ... pass + """ + + allure_results = allure_pytest_runner.run_docstring() + + assert_that( + allure_results, + has_test_case( + "test_pytest_marker_with_kwargs_is_not_converted_to_allure_tag_example", not_( - has_tag("@pytest.mark.parametrize('param', ['foo'])") + has_tag(anything()) ) ) ) -def test_pytest_xfail_marker(allure_pytest_runner: AllurePytestRunner): +def test_pytest_reserved_marker_usefixtures_is_not_converted_to_allure_tag( + allure_pytest_runner: AllurePytestRunner +): """ >>> import pytest - >>> @pytest.mark.xfail(reason='this is unexpect pass') - ... def test_pytest_xfail_marker_example(): + >>> @pytest.mark.usefixtures('test_fixture') + ... def test_pytest_reserved_marker_usefixtures_is_not_converted_to_allure_tag_example(): ... pass """ @@ -75,18 +93,22 @@ def test_pytest_xfail_marker(allure_pytest_runner: AllurePytestRunner): assert_that( allure_results, has_test_case( - "test_pytest_xfail_marker_example", - has_tag("@pytest.mark.xfail(reason='this is unexpect pass')") + "test_pytest_reserved_marker_usefixtures_is_not_converted_to_allure_tag_example", + not_( + has_tag(anything()) + ) ) ) -def test_pytest_marker_with_args(allure_pytest_runner: AllurePytestRunner): +def test_pytest_reserved_marker_filterwarnings_is_not_converted_to_allure_tag( + allure_pytest_runner: AllurePytestRunner +): """ >>> import pytest - >>> @pytest.mark.marker('cool', 'stuff') - ... def test_pytest_marker_with_args_example(): + >>> @pytest.mark.filterwarnings('ignore:val') + ... def test_pytest_reserved_marker_filterwarnings_is_not_converted_to_allure_tag_example(): ... pass """ @@ -95,18 +117,22 @@ def test_pytest_marker_with_args(allure_pytest_runner: AllurePytestRunner): assert_that( allure_results, has_test_case( - "test_pytest_marker_with_args_example", - has_tag("marker('cool', 'stuff')") + "test_pytest_reserved_marker_filterwarnings_is_not_converted_to_allure_tag_example", + not_( + has_tag(anything()) + ) ) ) -def test_pytest_marker_with_kwargs(allure_pytest_runner: AllurePytestRunner): +def test_pytest_reserved_marker_skip_is_not_converted_to_allure_tag( + allure_pytest_runner: AllurePytestRunner +): """ >>> import pytest - >>> @pytest.mark.marker(stuff='cool') - ... def test_pytest_marker_with_kwargs_example(): + >>> @pytest.mark.skip(reason='reason') + ... def test_pytest_reserved_marker_skip_is_not_converted_to_allure_tag_example(): ... pass """ @@ -115,20 +141,22 @@ def test_pytest_marker_with_kwargs(allure_pytest_runner: AllurePytestRunner): assert_that( allure_results, has_test_case( - "test_pytest_marker_with_kwargs_example", - has_tag("marker(stuff='cool')") + "test_pytest_reserved_marker_skip_is_not_converted_to_allure_tag_example", + not_( + has_tag(anything()) + ) ) ) -def test_pytest_marker_with_kwargs_native_encoding( - allure_pytest_runner: AllurePytestRunner +def test_pytest_reserved_marker_skipif_is_not_converted_to_allure_tag( + allure_pytest_runner: AllurePytestRunner ): """ >>> import pytest - >>> @pytest.mark.marker(stuff='я') - ... def test_pytest_marker_with_kwargs_native_encoding_example(): + >>> @pytest.mark.skipif(False, reason='reason') + ... def test_pytest_reserved_marker_skipif_is_not_converted_to_allure_tag_example(): ... pass """ @@ -137,20 +165,22 @@ def test_pytest_marker_with_kwargs_native_encoding( assert_that( allure_results, has_test_case( - "test_pytest_marker_with_kwargs_native_encoding_example", - has_tag("marker(stuff='я')") + "test_pytest_reserved_marker_skipif_is_not_converted_to_allure_tag_example", + not_( + has_tag(anything()) + ) ) ) -def test_pytest_marker_with_kwargs_utf_encoding( - allure_pytest_runner: AllurePytestRunner +def test_pytest_reserved_marker_xfail_is_not_converted_to_allure_tag( + allure_pytest_runner: AllurePytestRunner ): """ >>> import pytest - >>> @pytest.mark.marker(stuff='я') - ... def test_pytest_marker_with_kwargs_utf_encoding_example(): + >>> @pytest.mark.xfail(reason='this is unexpect pass') + ... def test_pytest_reserved_marker_xfail_is_not_converted_to_allure_tag_example(): ... pass """ @@ -159,7 +189,33 @@ def test_pytest_marker_with_kwargs_utf_encoding( assert_that( allure_results, has_test_case( - "test_pytest_marker_with_kwargs_utf_encoding_example", - has_tag("marker(stuff='я')") + "test_pytest_reserved_marker_xfail_is_not_converted_to_allure_tag_example", + not_( + has_tag(anything()) + ) + ) + ) + + +def test_pytest_reserved_marker_parametrize_is_not_converted_to_allure_tag( + allure_pytest_runner: AllurePytestRunner +): + """ + >>> import pytest + + >>> @pytest.mark.parametrize("param", ["foo"]) + ... def test_pytest_reserved_marker_parametrize_is_not_converted_to_allure_tag_example(param): + ... pass + """ + + allure_results = allure_pytest_runner.run_docstring() + + assert_that( + allure_results, + has_test_case( + "test_pytest_reserved_marker_parametrize_is_not_converted_to_allure_tag_example[foo]", + not_( + has_tag(anything()) + ) ) ) diff --git a/tests/allure_pytest/externals/pytest_rerunfailures/pytest_rerunfailures_test.py b/tests/allure_pytest/externals/pytest_rerunfailures/pytest_rerunfailures_test.py index b242b488..779657c3 100644 --- a/tests/allure_pytest/externals/pytest_rerunfailures/pytest_rerunfailures_test.py +++ b/tests/allure_pytest/externals/pytest_rerunfailures/pytest_rerunfailures_test.py @@ -65,6 +65,6 @@ def __count_labels(tc, name): assert len(output.test_cases) == 2 assert __count_labels(output.test_cases[0], "suite") == 1 - assert __count_labels(output.test_cases[0], "tag") == 1 + assert __count_labels(output.test_cases[0], "tag") == 0 assert __count_labels(output.test_cases[1], "suite") == 1 - assert __count_labels(output.test_cases[1], "tag") == 1 + assert __count_labels(output.test_cases[1], "tag") == 0 From 0f7da56e37ab9ef76465c532b7ef2fd346af7923 Mon Sep 17 00:00:00 2001 From: Maksim Stepanov <17935127+delatrie@users.noreply.github.com> Date: Wed, 9 Jul 2025 22:03:13 +0700 Subject: [PATCH 5/6] fix(pytest): properly include nested classes in fullName, historyId, testCaseId, and subSuite (#869) --- allure-pytest/src/utils.py | 22 ++++++--- allure-python-commons-test/src/result.py | 4 ++ tests/allure_pytest/defects/issue868_test.py | 47 ++++++++++++++++++++ 3 files changed, 67 insertions(+), 6 deletions(-) create mode 100644 tests/allure_pytest/defects/issue868_test.py 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(): 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) 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 ab1431466a121c323883508f1065e16227255585 Mon Sep 17 00:00:00 2001 From: Maksim Stepanov <17935127+delatrie@users.noreply.github.com> Date: Tue, 22 Jul 2025 18:10:15 +0700 Subject: [PATCH 6/6] feat: add titlePath to results generated by all allure-pytest integrations (#870) --- allure-behave/src/listener.py | 2 + allure-behave/src/utils.py | 24 ++++ allure-nose2/src/plugin.py | 2 + allure-nose2/src/utils.py | 5 + allure-pytest-bdd/src/pytest_bdd_listener.py | 2 + allure-pytest-bdd/src/utils.py | 11 ++ allure-pytest/src/listener.py | 2 + allure-pytest/src/stash.py | 61 ++++++++++ allure-pytest/src/utils.py | 72 +++++++----- allure-python-commons-test/src/result.py | 7 ++ .../src/allure_commons/model2.py | 1 + .../src/listener/allure_listener.py | 1 + .../src/listener/robot_listener.py | 5 +- .../behave_support/titlepath/__init__.py | 0 .../titlepath/titlepath_test.py | 110 ++++++++++++++++++ tests/allure_behave/behave_runner.py | 10 +- .../nose2_support/titlepath/__init__.py | 0 .../nose2_support/titlepath/titlepath_test.py | 71 +++++++++++ tests/allure_nose2/nose2_runner.py | 4 +- .../acceptance/label/package/package_test.py | 39 +++++++ .../label/package/regression_test.py | 19 --- .../acceptance/titlepath/__init__.py | 0 .../acceptance/titlepath/titlepath_test.py | 75 ++++++++++++ .../acceptance/titlepath/__init__.py | 0 .../acceptance/titlepath/titlepath_test.py | 91 +++++++++++++++ .../titlepath/__init__.py | 0 .../titlepath/titlepath_test.py | 56 +++++++++ tests/allure_robotframework/robot_runner.py | 5 +- 28 files changed, 622 insertions(+), 53 deletions(-) create mode 100644 allure-pytest/src/stash.py create mode 100644 tests/allure_behave/acceptance/behave_support/titlepath/__init__.py create mode 100644 tests/allure_behave/acceptance/behave_support/titlepath/titlepath_test.py create mode 100644 tests/allure_nose2/acceptance/nose2_support/titlepath/__init__.py create mode 100644 tests/allure_nose2/acceptance/nose2_support/titlepath/titlepath_test.py create mode 100644 tests/allure_pytest/acceptance/label/package/package_test.py create mode 100644 tests/allure_pytest/acceptance/titlepath/__init__.py create mode 100644 tests/allure_pytest/acceptance/titlepath/titlepath_test.py create mode 100644 tests/allure_pytest_bdd/acceptance/titlepath/__init__.py create mode 100644 tests/allure_pytest_bdd/acceptance/titlepath/titlepath_test.py create mode 100644 tests/allure_robotframework/acceptance/robotframework_support/titlepath/__init__.py create mode 100644 tests/allure_robotframework/acceptance/robotframework_support/titlepath/titlepath_test.py diff --git a/allure-behave/src/listener.py b/allure-behave/src/listener.py index b9a86d55..7d5d8753 100644 --- a/allure-behave/src/listener.py +++ b/allure-behave/src/listener.py @@ -20,6 +20,7 @@ from allure_behave.utils import scenario_links from allure_behave.utils import scenario_labels from allure_behave.utils import get_fullname +from allure_behave.utils import get_title_path from allure_behave.utils import TEST_PLAN_SKIP_REASON from allure_behave.utils import get_hook_name @@ -77,6 +78,7 @@ def start_scenario(self, scenario): test_case = TestResult(uuid=self.current_scenario_uuid, start=now()) test_case.name = scenario_name(scenario) test_case.fullName = get_fullname(scenario) + test_case.titlePath = get_title_path(scenario) test_case.historyId = scenario_history_id(scenario) test_case.description = '\n'.join(scenario.description) test_case.parameters = scenario_parameters(scenario) diff --git a/allure-behave/src/utils.py b/allure-behave/src/utils.py index b77fe6c4..ce0f2d70 100644 --- a/allure-behave/src/utils.py +++ b/allure-behave/src/utils.py @@ -1,6 +1,7 @@ import csv import io from enum import Enum +from pathlib import Path from behave.runner_util import make_undefined_step_snippet from allure_commons.types import Severity, LabelType from allure_commons.model2 import Status, Parameter @@ -97,6 +98,29 @@ def get_fullname(scenario): return f"{scenario.feature.name}: {name}" +def get_title_path(scenario): + path_parts = [] + feature_part = scenario.feature.name + + # filename is set to "" if the feature comes from a string literal + if scenario.filename and scenario.filename != "": + path = Path(scenario.filename) + + # remove the filename because it's redundant: a feature file can only have one feature defined + path_parts = path.parts[:-1] + + if not feature_part: + # if no feature name is defined, fallback to the filename + feature_part = path.name + + if not feature_part: + # Neither feature name nor filename is defined, use the "Feature" keyword + feature_part = scenario.feature.keyword + + # reminder: scenario name should not be included in titlePath because it is already part of the test case title + return [*path_parts, feature_part] + + def get_hook_name(name, parameters): tag = None if name in ["before_tag", "after_tag"]: diff --git a/allure-nose2/src/plugin.py b/allure-nose2/src/plugin.py index c7f64608..678fe8f1 100644 --- a/allure-nose2/src/plugin.py +++ b/allure-nose2/src/plugin.py @@ -14,6 +14,7 @@ from .utils import timestamp_millis, status_details, update_attrs, labels, name, fullname, params +from .utils import get_title_path import allure_commons @@ -90,6 +91,7 @@ def startTest(self, event): test_result.fullName = fullname(event) test_result.testCaseId = md5(test_result.fullName) test_result.historyId = md5(event.test.id()) + test_result.titlePath = get_title_path(event) test_result.labels.extend(labels(event.test)) test_result.labels.append(Label(name=LabelType.HOST, value=self._host)) test_result.labels.append(Label(name=LabelType.THREAD, value=self._thread)) diff --git a/allure-nose2/src/utils.py b/allure-nose2/src/utils.py index 691e75c7..4e2e885d 100644 --- a/allure-nose2/src/utils.py +++ b/allure-nose2/src/utils.py @@ -81,6 +81,11 @@ def fullname(event): return test_id.split(":")[0] +def get_title_path(event): + test_id = event.test.id() + return test_id.split(":", 1)[0].rsplit(".")[:-1] + + def params(event): def _params(names, values): return [Parameter(name=name, value=represent(value)) for name, value in zip(names, values)] diff --git a/allure-pytest-bdd/src/pytest_bdd_listener.py b/allure-pytest-bdd/src/pytest_bdd_listener.py index d0697380..bcc6cba0 100644 --- a/allure-pytest-bdd/src/pytest_bdd_listener.py +++ b/allure-pytest-bdd/src/pytest_bdd_listener.py @@ -23,6 +23,7 @@ from .utils import get_allure_links from .utils import convert_params from .utils import get_full_name +from .utils import get_title_path from .utils import get_outline_params from .utils import get_pytest_params from .utils import get_pytest_report_status @@ -59,6 +60,7 @@ def pytest_bdd_before_scenario(self, request, feature, scenario): full_name = get_full_name(feature, scenario) with self.lifecycle.schedule_test_case(uuid=uuid) as test_result: test_result.fullName = full_name + test_result.titlePath = get_title_path(request, feature) test_result.name = get_test_name(item, scenario, params) test_result.description = get_allure_description(item, feature, scenario) test_result.descriptionHtml = get_allure_description_html(item) diff --git a/allure-pytest-bdd/src/utils.py b/allure-pytest-bdd/src/utils.py index 1ba59aa2..f4a838b1 100644 --- a/allure-pytest-bdd/src/utils.py +++ b/allure-pytest-bdd/src/utils.py @@ -3,6 +3,7 @@ import os from urllib.parse import urlparse from uuid import UUID +from pathlib import Path import pytest @@ -171,6 +172,16 @@ def get_full_name(feature, scenario): return f"{feature_path}:{scenario.name}" +def get_rootdir(request): + config = request.config + return getattr(config, "rootpath", None) or Path(config.rootdir) + + +def get_title_path(request, feature): + parts = Path(feature.filename).relative_to(get_rootdir(request)).parts + return [*parts[:-1], feature.name or parts[-1]] + + def get_uuid(*args): return str(UUID(md5(*args))) diff --git a/allure-pytest/src/listener.py b/allure-pytest/src/listener.py index 11153630..42b7ff49 100644 --- a/allure-pytest/src/listener.py +++ b/allure-pytest/src/listener.py @@ -19,6 +19,7 @@ from allure_pytest.utils import allure_description, allure_description_html from allure_pytest.utils import allure_labels, allure_links, pytest_markers from allure_pytest.utils import allure_full_name, allure_package, allure_name +from allure_pytest.utils import allure_title_path from allure_pytest.utils import allure_suite_labels from allure_pytest.utils import get_status, get_status_details from allure_pytest.utils import get_outcome_status, get_outcome_status_details @@ -109,6 +110,7 @@ def pytest_runtest_setup(self, item): test_result.name = allure_name(item, params, param_id) full_name = allure_full_name(item) test_result.fullName = full_name + test_result.titlePath = [*allure_title_path(item)] test_result.testCaseId = md5(full_name) test_result.description = allure_description(item) test_result.descriptionHtml = allure_description_html(item) diff --git a/allure-pytest/src/stash.py b/allure-pytest/src/stash.py new file mode 100644 index 00000000..31d9302b --- /dev/null +++ b/allure-pytest/src/stash.py @@ -0,0 +1,61 @@ +import pytest +from functools import wraps + +HAS_STASH = hasattr(pytest, 'StashKey') + + +def create_stashkey_safe(): + """ + If pytest stash is available, returns a new stash key. + Otherwise, returns `None`. + """ + + return pytest.StashKey() if HAS_STASH else None + + +def stash_get_safe(item, key): + """ + If pytest stash is available and contains the key, retrieves the associated value. + Otherwise, returns `None`. + """ + + if HAS_STASH and key in item.stash: + return item.stash[key] + + +def stash_set_safe(item: pytest.Item, key, value): + """ + If pytest stash is available, associates the value with the key in the stash. + Otherwise, does nothing. + """ + + if HAS_STASH: + item.stash[key] = value + + +def stashed(arg=None): + """ + Cashes the result of the decorated function in the pytest item stash. + The first argument of the function must be a pytest item. + + In pytest<7.0 the stash is not available, so the decorator does nothing. + """ + + key = create_stashkey_safe() if arg is None or callable(arg) else arg + + def decorator(func): + if not HAS_STASH: + return func + + @wraps(func) + def wrapper(item, *args, **kwargs): + if key in item.stash: + return item.stash[key] + + value = func(item, *args, **kwargs) + item.stash[key] = value + return value + + return wrapper + + return decorator(arg) if callable(arg) else decorator diff --git a/allure-pytest/src/utils.py b/allure-pytest/src/utils.py index e90df9c1..56594a09 100644 --- a/allure-pytest/src/utils.py +++ b/allure-pytest/src/utils.py @@ -1,11 +1,11 @@ import pytest -from itertools import chain, islice, repeat +from itertools import repeat from allure_commons.utils import SafeFormatter, md5 from allure_commons.utils import format_exception, format_traceback from allure_commons.model2 import Status from allure_commons.model2 import StatusDetails from allure_commons.types import LabelType - +from allure_pytest.stash import stashed ALLURE_DESCRIPTION_MARK = 'allure_description' ALLURE_DESCRIPTION_HTML_MARK = 'allure_description_html' @@ -30,6 +30,24 @@ } +class ParsedPytestNodeId: + def __init__(self, nodeid): + filepath, *class_names, function_segment = ensure_len(nodeid.split("::"), 2) + self.filepath = filepath + self.path_segments = filepath.split('/') + *parent_dirs, filename = ensure_len(self.path_segments, 1) + self.parent_package = '.'.join(parent_dirs) + self.module = filename.rsplit(".", 1)[0] + self.package = '.'.join(filter(None, [self.parent_package, self.module])) + self.class_names = class_names + self.test_function = function_segment.split("[", 1)[0] + + +@stashed +def parse_nodeid(item): + return ParsedPytestNodeId(item.nodeid) + + def get_marker_value(item, keyword): marker = item.get_closest_marker(keyword) return marker.args[0] if marker and marker.args else None @@ -101,9 +119,7 @@ def should_convert_mark_to_tag(mark): def allure_package(item): - parts = item.nodeid.split('::') - path = parts[0].rsplit('.', 1)[0] - return path.replace('/', '.') + return parse_nodeid(item).package def allure_name(item, parameters, param_id=None): @@ -122,37 +138,41 @@ def allure_name(item, parameters, param_id=None): def allure_full_name(item: pytest.Item): - package = allure_package(item) - 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_part}#{test}' + nodeid = parse_nodeid(item) + class_part = ("." + ".".join(nodeid.class_names)) if nodeid.class_names else "" + test = item.originalname if isinstance(item, pytest.Function) else nodeid.test_function + full_name = f"{nodeid.package}{class_part}#{test}" return full_name +def allure_title_path(item): + nodeid = parse_nodeid(item) + return list( + filter(None, [*nodeid.path_segments, *nodeid.class_names]), + ) + + 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, *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, " > ".join(class_names)], - ), - ) - labels = dict(allure_labels(item)) - default_suite_labels = [] - for label, value in pairs.items(): - if label not in labels.keys() and value: - default_suite_labels.append((label, value)) + nodeid = parse_nodeid(item) + + default_suite_labels = { + LabelType.PARENT_SUITE: nodeid.parent_package, + LabelType.SUITE: nodeid.module, + LabelType.SUB_SUITE: " > ".join(nodeid.class_names), + } + + existing_labels = dict(allure_labels(item)) + resolved_default_suite_labels = [] + for label, value in default_suite_labels.items(): + if label not in existing_labels and value: + resolved_default_suite_labels.append((label, value)) - return default_suite_labels + return resolved_default_suite_labels def get_outcome_status(outcome): diff --git a/allure-python-commons-test/src/result.py b/allure-python-commons-test/src/result.py index ee97cbf9..93a393cd 100644 --- a/allure-python-commons-test/src/result.py +++ b/allure-python-commons-test/src/result.py @@ -74,6 +74,13 @@ def has_title(title): return has_entry('name', title) +def has_title_path(*matchers): + return has_entry( + "titlePath", + contains_exactly(*matchers), + ) + + def has_description(*matchers): return has_entry('description', all_of(*matchers)) diff --git a/allure-python-commons/src/allure_commons/model2.py b/allure-python-commons/src/allure_commons/model2.py index ccaf4459..d8591598 100644 --- a/allure-python-commons/src/allure_commons/model2.py +++ b/allure-python-commons/src/allure_commons/model2.py @@ -49,6 +49,7 @@ class TestResult(ExecutableItem): fullName = attrib(default=None) labels = attrib(default=Factory(list)) links = attrib(default=Factory(list)) + titlePath = attrib(default=Factory(list)) @attrs diff --git a/allure-robotframework/src/listener/allure_listener.py b/allure-robotframework/src/listener/allure_listener.py index 2a4d3a80..236a524e 100644 --- a/allure-robotframework/src/listener/allure_listener.py +++ b/allure-robotframework/src/listener/allure_listener.py @@ -131,6 +131,7 @@ def start_test(self, name, attributes): long_name = attributes.get('longname') test_result.name = name test_result.fullName = long_name + test_result.titlePath = attributes.get("titlepath", []) test_result.historyId = md5(long_name) test_result.start = now() diff --git a/allure-robotframework/src/listener/robot_listener.py b/allure-robotframework/src/listener/robot_listener.py index 9dab210e..0ff4ff2c 100644 --- a/allure-robotframework/src/listener/robot_listener.py +++ b/allure-robotframework/src/listener/robot_listener.py @@ -16,6 +16,7 @@ class allure_robotframework: def __init__(self, logger_path=DEFAULT_OUTPUT_PATH): self.messages = Messages() + self.title_path = [] self.logger = AllureFileLogger(logger_path) self.lifecycle = AllureLifecycle() @@ -25,17 +26,19 @@ def __init__(self, logger_path=DEFAULT_OUTPUT_PATH): allure_commons.plugin_manager.register(self.listener) def start_suite(self, name, attributes): + self.title_path.append(name) self.messages.start_context() self.listener.start_suite_container(name, attributes) def end_suite(self, name, attributes): self.messages.stop_context() self.listener.stop_suite_container(name, attributes) + self.title_path.pop() def start_test(self, name, attributes): self.messages.start_context() self.listener.start_test_container(name, attributes) - self.listener.start_test(name, attributes) + self.listener.start_test(name, {**attributes, "titlepath": self.title_path}) def end_test(self, name, attributes): messages = self.messages.stop_context() diff --git a/tests/allure_behave/acceptance/behave_support/titlepath/__init__.py b/tests/allure_behave/acceptance/behave_support/titlepath/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/allure_behave/acceptance/behave_support/titlepath/titlepath_test.py b/tests/allure_behave/acceptance/behave_support/titlepath/titlepath_test.py new file mode 100644 index 00000000..7e035a3f --- /dev/null +++ b/tests/allure_behave/acceptance/behave_support/titlepath/titlepath_test.py @@ -0,0 +1,110 @@ +from pathlib import Path +from hamcrest import assert_that +from tests.allure_behave.behave_runner import AllureBehaveRunner +from allure_commons_test.report import has_test_case +from allure_commons_test.result import has_title_path + + +def test_titlepath_of_top_level_feature_file(docstring, behave_runner: AllureBehaveRunner): + """ + Feature: Foo + Scenario: Bar + Given baz + """ + + behave_runner.run_behave( + feature_files={"foo.feature": docstring}, + step_literals=["given('baz')(lambda c:None)"], + ) + + assert_that( + behave_runner.allure_results, + has_test_case( + "Bar", + has_title_path("Foo"), + ) + ) + + +def test_titlepath_of_nested_feature_file(docstring, behave_runner: AllureBehaveRunner): + """ + Feature: Foo + Scenario: Bar + Given baz + """ + + behave_runner.run_behave( + feature_files={"foo/bar/baz.feature": docstring}, + step_literals=["given('baz')(lambda c:None)"], + ) + + assert_that( + behave_runner.allure_results, + has_test_case( + "Bar", + has_title_path("foo", "bar", "Foo"), + ) + ) + + +def test_titlepath_if_feature_name_empty(docstring, behave_runner: AllureBehaveRunner): + """ + Feature: + Scenario: Bar + Given baz + """ + + behave_runner.run_behave( + feature_files={str(Path("foo.feature").absolute()): docstring}, + step_literals=["given('baz')(lambda c:None)"], + ) + + assert_that( + behave_runner.allure_results, + has_test_case( + "Bar", + has_title_path("foo.feature"), + ) + ) + + +def test_titlepath_of_feature_without_filename(docstring, behave_runner: AllureBehaveRunner): + """ + Feature: Foo + Scenario: Bar + Given baz + """ + + behave_runner.run_behave( + feature_literals=[docstring], + step_literals=["given('baz')(lambda c:None)"], + ) + + assert_that( + behave_runner.allure_results, + has_test_case( + "Bar", + has_title_path("Foo"), + ) + ) + + +def test_titlepath_of_feature_without_filename_and_name(docstring, behave_runner: AllureBehaveRunner): + """ + Feature: + Scenario: Bar + Given baz + """ + + behave_runner.run_behave( + feature_literals=[docstring], + step_literals=["given('baz')(lambda c:None)"], + ) + + assert_that( + behave_runner.allure_results, + has_test_case( + "Bar", + has_title_path("Feature"), + ) + ) diff --git a/tests/allure_behave/behave_runner.py b/tests/allure_behave/behave_runner.py index da49d738..1cbaf845 100644 --- a/tests/allure_behave/behave_runner.py +++ b/tests/allure_behave/behave_runner.py @@ -11,7 +11,7 @@ from behave.step_registry import setup_step_decorators from behave.step_registry import StepRegistry from pytest import FixtureRequest, Pytester -from typing import Sequence +from typing import Sequence, Mapping from tests.e2e import AllureFrameworkRunner, PathlikeT from allure_behave.formatter import AllureFormatter @@ -91,7 +91,10 @@ def load_step_definitions(self, extra_step_paths=None): def load_features(self): self.features.extend( - parse_feature(f) for f in self.__features + parse_feature(feature) if isinstance(feature, str) else parse_feature( + feature[1], + filename=feature[0], + ) for feature in self.__features ) def load_formatter(self): @@ -123,6 +126,7 @@ def run_behave( feature_paths: Sequence[PathlikeT] = None, feature_literals: Sequence[str] = None, feature_rst_ids: Sequence[str] = None, + feature_files: Mapping[str, str] = None, step_paths: Sequence[PathlikeT] = None, step_literals: Sequence[str] = None, step_rst_ids: Sequence[str] = None, @@ -172,7 +176,7 @@ def run_behave( paths=feature_paths, literals=feature_literals, rst_ids=feature_rst_ids - ), + ) + list((feature_files or {}).items()), self._get_all_content( paths=step_paths, literals=step_literals, diff --git a/tests/allure_nose2/acceptance/nose2_support/titlepath/__init__.py b/tests/allure_nose2/acceptance/nose2_support/titlepath/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/allure_nose2/acceptance/nose2_support/titlepath/titlepath_test.py b/tests/allure_nose2/acceptance/nose2_support/titlepath/titlepath_test.py new file mode 100644 index 00000000..004c866b --- /dev/null +++ b/tests/allure_nose2/acceptance/nose2_support/titlepath/titlepath_test.py @@ -0,0 +1,71 @@ +import pytest +from hamcrest import assert_that +from tests.allure_nose2.nose2_runner import AllureNose2Runner + +from allure_commons_test.report import has_test_case +from allure_commons_test.result import has_title_path + + +@pytest.mark.parametrize(["module", "path_segments"], [ + pytest.param("foo", ["foo"], id="root"), + pytest.param("foo.bar", ["foo", "bar"], id="level1"), + pytest.param("foo.bar.baz", ["foo", "bar", "baz"], id="level2"), +]) +def test_function_title_path(nose2_runner: AllureNose2Runner, module, path_segments): + """ + >>> def test_qux(): + ... pass + """ + + allure_results = nose2_runner.run_docstring(module_name=module) + + assert_that( + allure_results, + has_test_case( + "test_qux", + has_title_path(*path_segments), + ) + ) + + +@pytest.mark.parametrize(["module", "path_segments"], [ + pytest.param("foo", ["foo"], id="root"), + pytest.param("foo.bar", ["foo", "bar"], id="level1"), + pytest.param("foo.bar.baz", ["foo", "bar", "baz"], id="level2"), +]) +def test_method_title_path(nose2_runner: AllureNose2Runner, module, path_segments): + """ + >>> from unittest import TestCase + >>> class TestQux(TestCase): + ... def test_quux(self): + ... pass + """ + + allure_results = nose2_runner.run_docstring(module_name=module) + + assert_that( + allure_results, + has_test_case( + "test_quux", + has_title_path(*path_segments, "TestQux"), + ) + ) + + +def test_params_ignored(nose2_runner: AllureNose2Runner): + """ + >>> from nose2.tools import params + >>> @params("a.b:c") + ... def test_bar(v): + ... pass + """ + + allure_results = nose2_runner.run_docstring(module_name="foo") + + assert_that( + allure_results, + has_test_case( + "test_bar", + has_title_path("foo"), + ) + ) diff --git a/tests/allure_nose2/nose2_runner.py b/tests/allure_nose2/nose2_runner.py index 67b35d7e..e5cf1802 100644 --- a/tests/allure_nose2/nose2_runner.py +++ b/tests/allure_nose2/nose2_runner.py @@ -12,10 +12,10 @@ class AllureNose2Runner(AllureFrameworkRunner): def __init__(self, request: FixtureRequest, pytester: Pytester): super().__init__(request, pytester, AllureNose2Runner.LOGGER_PATH) - def run_docstring(self): + def run_docstring(self, module_name=None): docstring = self._find_docstring() example_code = script_from_examples(docstring) - spec = importlib.machinery.ModuleSpec(self.request.node.name, None) + spec = importlib.machinery.ModuleSpec(module_name or self.request.node.name, None) module = importlib.util.module_from_spec(spec) return self._run(module, example_code) diff --git a/tests/allure_pytest/acceptance/label/package/package_test.py b/tests/allure_pytest/acceptance/label/package/package_test.py new file mode 100644 index 00000000..7acbdc72 --- /dev/null +++ b/tests/allure_pytest/acceptance/label/package/package_test.py @@ -0,0 +1,39 @@ +from hamcrest import assert_that +from tests.allure_pytest.pytest_runner import AllurePytestRunner + +from allure_commons_test.report import has_test_case +from allure_commons_test.label import has_package + + +def test_with_no_package(allure_pytest_runner: AllurePytestRunner): + """ + >>> def test_bar(request): + ... pass + """ + + allure_results = allure_pytest_runner.run_docstring(filename="foo_test.py") + + assert_that( + allure_results, + has_test_case( + "test_bar", + has_package("foo_test") + ) + ) + + +def test_with_package(allure_pytest_runner: AllurePytestRunner): + """ + >>> def test_qux(request): + ... pass + """ + + allure_results = allure_pytest_runner.run_docstring(filename="foo/bar/baz_test.py") + + assert_that( + allure_results, + has_test_case( + "test_qux", + has_package("foo.bar.baz_test"), + ) + ) diff --git a/tests/allure_pytest/acceptance/label/package/regression_test.py b/tests/allure_pytest/acceptance/label/package/regression_test.py index 42c90d31..ba8f5ecb 100644 --- a/tests/allure_pytest/acceptance/label/package/regression_test.py +++ b/tests/allure_pytest/acceptance/label/package/regression_test.py @@ -26,22 +26,3 @@ def test_path_with_dots_test_example(): has_package("path.with.dots.test_path") ) ) - - -def test_with_no_package(allure_pytest_runner: AllurePytestRunner): - """ - >>> def test_package_less(request): - ... pass - """ - - allure_pytest_runner.pytester.makeini("""[pytest]""") - - allure_results = allure_pytest_runner.run_docstring() - - assert_that( - allure_results, - has_test_case( - "test_package_less", - has_package("test_with_no_package") - ) - ) diff --git a/tests/allure_pytest/acceptance/titlepath/__init__.py b/tests/allure_pytest/acceptance/titlepath/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/allure_pytest/acceptance/titlepath/titlepath_test.py b/tests/allure_pytest/acceptance/titlepath/titlepath_test.py new file mode 100644 index 00000000..f731cedd --- /dev/null +++ b/tests/allure_pytest/acceptance/titlepath/titlepath_test.py @@ -0,0 +1,75 @@ +import pytest +from hamcrest import assert_that +from tests.allure_pytest.pytest_runner import AllurePytestRunner + +from allure_commons_test.report import has_test_case +from allure_commons_test.result import has_title_path + + +@pytest.mark.parametrize(["path", "path_segments"], [ + pytest.param("foo_test.py", ["foo_test.py"], id="root"), + pytest.param("foo/bar_test.py", ["foo", "bar_test.py"], id="dir"), + pytest.param("foo/bar/baz_test.py", ["foo", "bar", "baz_test.py"], id="subdir"), +]) +def test_function_title_path(allure_pytest_runner: AllurePytestRunner, path, path_segments): + """ + >>> def test_bar(): + ... pass + """ + + allure_results = allure_pytest_runner.run_docstring(filename=path) + + assert_that( + allure_results, + has_test_case( + "test_bar", + has_title_path(*path_segments), + ) + ) + + +@pytest.mark.parametrize(["path", "path_segments"], [ + pytest.param("foo_test.py", ["foo_test.py"], id="root"), + pytest.param("foo/bar_test.py", ["foo", "bar_test.py"], id="dir"), + pytest.param("foo/bar/baz_test.py", ["foo", "bar", "baz_test.py"], id="subdir"), +]) +def test_method_title_path(allure_pytest_runner: AllurePytestRunner, path, path_segments): + """ + >>> class TestBar: + ... def test_baz(self): + ... pass + """ + + allure_results = allure_pytest_runner.run_docstring(filename=path) + + assert_that( + allure_results, + has_test_case( + "test_baz", + has_title_path(*path_segments, "TestBar"), + ) + ) + + +@pytest.mark.parametrize(["path", "path_segments"], [ + pytest.param("foo_test.py", ["foo_test.py"], id="root"), + pytest.param("foo/bar_test.py", ["foo", "bar_test.py"], id="dir"), + pytest.param("foo/bar/baz_test.py", ["foo", "bar", "baz_test.py"], id="subdir"), +]) +def test_nested_class_method_title_path(allure_pytest_runner: AllurePytestRunner, path, path_segments): + """ + >>> class TestBar: + ... class TestBaz: + ... def test_qux(self): + ... pass + """ + + allure_results = allure_pytest_runner.run_docstring(filename=path) + + assert_that( + allure_results, + has_test_case( + "test_qux", + has_title_path(*path_segments, "TestBar", "TestBaz"), + ) + ) diff --git a/tests/allure_pytest_bdd/acceptance/titlepath/__init__.py b/tests/allure_pytest_bdd/acceptance/titlepath/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/allure_pytest_bdd/acceptance/titlepath/titlepath_test.py b/tests/allure_pytest_bdd/acceptance/titlepath/titlepath_test.py new file mode 100644 index 00000000..60ee2363 --- /dev/null +++ b/tests/allure_pytest_bdd/acceptance/titlepath/titlepath_test.py @@ -0,0 +1,91 @@ +import pytest +from hamcrest import assert_that +from tests.allure_pytest.pytest_runner import AllurePytestRunner +import allure + +from allure_commons_test.report import has_test_case +from allure_commons_test.result import has_title_path + + +@pytest.mark.parametrize(["path", "path_segments"], [ + pytest.param("foo.feature", ["Qux"], id="root"), + pytest.param("foo/bar.feature", ["foo", "Qux"], id="dir"), + pytest.param("foo/bar/baz.feature", ["foo", "bar", "Qux"], id="subdir"), +]) +def test_title_path(allure_pytest_bdd_runner: AllurePytestRunner, path, path_segments): + allure.dynamic.parent_suite("my suite") + allure.dynamic.suite("my suite") + allure.dynamic.sub_suite("my suite") + + allure.dynamic.epic("my suite") + allure.dynamic.feature("my suite") + allure.dynamic.story("my suite") + + feature_content = ( + """ + Feature: Qux + Scenario: Quux + Given pass + """ + ) + pytest_content = ( + f""" + from pytest_bdd import scenarios, given + import allure + + scenarios("{path}") + + @given("pass") + def given_pass(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + (path, feature_content), + pytest_content, + ) + + assert_that( + allure_results, + has_test_case( + "Quux", + has_title_path(*path_segments), + ) + ) + + +def test_feature_name_missing(allure_pytest_bdd_runner: AllurePytestRunner): + feature_content = ( + """ + Feature: + Scenario: Bar + Given pass + """ + ) + pytest_content = ( + """ + from pytest_bdd import scenarios, given + import allure + + scenarios("foo.feature") + + @given("pass") + def given_pass(): + pass + """ + ) + + allure_results = allure_pytest_bdd_runner.run_pytest( + ("foo.feature", feature_content), + pytest_content, + cli_args=["--capture=no"] + ) + + assert_that( + allure_results, + has_test_case( + "Bar", + has_title_path("foo.feature"), + ) + ) diff --git a/tests/allure_robotframework/acceptance/robotframework_support/titlepath/__init__.py b/tests/allure_robotframework/acceptance/robotframework_support/titlepath/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/allure_robotframework/acceptance/robotframework_support/titlepath/titlepath_test.py b/tests/allure_robotframework/acceptance/robotframework_support/titlepath/titlepath_test.py new file mode 100644 index 00000000..24a88064 --- /dev/null +++ b/tests/allure_robotframework/acceptance/robotframework_support/titlepath/titlepath_test.py @@ -0,0 +1,56 @@ +from hamcrest import assert_that, all_of +from tests.allure_robotframework.robot_runner import AllureRobotRunner +from allure_commons_test.report import has_test_case +from allure_commons_test.result import has_title_path + + +def test_titlepath_of_directly_run_suite(docstring, robot_runner: AllureRobotRunner): + """ + *** Test Cases *** + Bar + No Operation + """ + + robot_runner.run_robotframework( + suite_literals={"foo.robot": docstring} + ) + + assert_that( + robot_runner.allure_results, + has_test_case( + "Bar", + has_title_path("Foo"), + ) + ) + + +def test_titlepath_of_nested_suites(docstring, robot_runner: AllureRobotRunner): + """ + *** Test Cases *** + Qux + No Operation + """ + + robot_runner.rootdir = "foo" + + robot_runner.run_robotframework( + suite_literals={ + "foo/bar/baz.robot": docstring, + "foo/bor/buz.robot": docstring, + } + + ) + + assert_that( + robot_runner.allure_results, + all_of( + has_test_case( + "Foo.Bar.Baz.Qux", + has_title_path("Foo", "Bar", "Baz"), + ), + has_test_case( + "Foo.Bor.Buz.Qux", + has_title_path("Foo", "Bor", "Buz"), + ), + ), + ) diff --git a/tests/allure_robotframework/robot_runner.py b/tests/allure_robotframework/robot_runner.py index bfb13cee..1d019ffe 100644 --- a/tests/allure_robotframework/robot_runner.py +++ b/tests/allure_robotframework/robot_runner.py @@ -1,7 +1,7 @@ import robot from pytest import FixtureRequest, Pytester from tests.e2e import AllureFrameworkRunner, PathlikeT -from typing import Sequence, Mapping +from typing import Sequence, Mapping, Union from allure_robotframework import allure_robotframework @@ -12,6 +12,7 @@ class AllureRobotRunner(AllureFrameworkRunner): def __init__(self, request: FixtureRequest, pytester: Pytester): super().__init__(request, pytester, AllureRobotRunner.LOGGER_PATH) + self.rootdir: Union[str, None] = None def run_robotframework( self, @@ -79,7 +80,7 @@ def run_robotframework( ) def _run_framework(self, suites, options): - robot.run(*suites, listener=allure_robotframework(None), **options) + robot.run(*[self.rootdir] if self.rootdir else suites, listener=allure_robotframework(None), **options) def __resolve_options(self, options): return {